* exec: clean up PTY resources on timeout and exit * cli: harden resume cleanup and watchdog stalled runs * cli: productionize PTY and resume reliability paths * docs: add PTY process supervision architecture plan * docs: rewrite PTY supervision plan as pre-rewrite baseline * docs: switch PTY supervision plan to one-go execution * docs: add one-line root cause to PTY supervision plan * docs: add OS contracts and test matrix to PTY supervision plan * docs: define process-supervisor package placement and scope * docs: tie supervisor plan to existing CI lanes * docs: place PTY supervisor plan under src/process * refactor(process): route exec and cli runs through supervisor * docs(process): refresh PTY supervision plan * wip * fix(process): harden supervisor timeout and PTY termination * fix(process): harden supervisor adapters env and wait handling * ci: avoid failing formal conformance on comment permissions * test(ui): fix cron request mock argument typing * fix(ui): remove leftover conflict marker * fix: supervise PTY processes (#14257) (openclaw#14257) (thanks @onutc)
77 lines
2.1 KiB
TypeScript
77 lines
2.1 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { createPtyAdapterMock } = vi.hoisted(() => ({
|
|
createPtyAdapterMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../../agents/shell-utils.js", () => ({
|
|
getShellConfig: () => ({ shell: "sh", args: ["-c"] }),
|
|
}));
|
|
|
|
vi.mock("./adapters/pty.js", () => ({
|
|
createPtyAdapter: (...args: unknown[]) => createPtyAdapterMock(...args),
|
|
}));
|
|
|
|
function createStubPtyAdapter() {
|
|
return {
|
|
pid: 1234,
|
|
stdin: undefined,
|
|
onStdout: (_listener: (chunk: string) => void) => {
|
|
// no-op
|
|
},
|
|
onStderr: (_listener: (chunk: string) => void) => {
|
|
// no-op
|
|
},
|
|
wait: async () => ({ code: 0, signal: null }),
|
|
kill: (_signal?: NodeJS.Signals) => {
|
|
// no-op
|
|
},
|
|
dispose: () => {
|
|
// no-op
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("process supervisor PTY command contract", () => {
|
|
beforeEach(() => {
|
|
createPtyAdapterMock.mockReset();
|
|
});
|
|
|
|
it("passes PTY command verbatim to shell args", async () => {
|
|
createPtyAdapterMock.mockResolvedValue(createStubPtyAdapter());
|
|
const { createProcessSupervisor } = await import("./supervisor.js");
|
|
const supervisor = createProcessSupervisor();
|
|
const command = `printf '%s\\n' "a b" && printf '%s\\n' '$HOME'`;
|
|
|
|
const run = await supervisor.spawn({
|
|
sessionId: "s1",
|
|
backendId: "test",
|
|
mode: "pty",
|
|
ptyCommand: command,
|
|
timeoutMs: 1_000,
|
|
});
|
|
const exit = await run.wait();
|
|
|
|
expect(exit.reason).toBe("exit");
|
|
expect(createPtyAdapterMock).toHaveBeenCalledTimes(1);
|
|
const params = createPtyAdapterMock.mock.calls[0]?.[0] as { args?: string[] };
|
|
expect(params.args).toEqual(["-c", command]);
|
|
});
|
|
|
|
it("rejects empty PTY command", async () => {
|
|
createPtyAdapterMock.mockResolvedValue(createStubPtyAdapter());
|
|
const { createProcessSupervisor } = await import("./supervisor.js");
|
|
const supervisor = createProcessSupervisor();
|
|
|
|
await expect(
|
|
supervisor.spawn({
|
|
sessionId: "s1",
|
|
backendId: "test",
|
|
mode: "pty",
|
|
ptyCommand: " ",
|
|
}),
|
|
).rejects.toThrow("PTY command cannot be empty");
|
|
expect(createPtyAdapterMock).not.toHaveBeenCalled();
|
|
});
|
|
});
|