Files
openclaw/src/process/supervisor/supervisor.test.ts
Onur cd44a0d01e fix: codex and similar processes keep dying on pty, solved by refactoring process spawning (#14257)
* 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)
2026-02-16 02:32:05 +01:00

103 lines
3.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { createProcessSupervisor } from "./supervisor.js";
describe("process supervisor", () => {
it("spawns child runs and captures output", async () => {
const supervisor = createProcessSupervisor();
const run = await supervisor.spawn({
sessionId: "s1",
backendId: "test",
mode: "child",
argv: [process.execPath, "-e", 'process.stdout.write("ok")'],
timeoutMs: 2_000,
stdinMode: "pipe-closed",
});
const exit = await run.wait();
expect(exit.reason).toBe("exit");
expect(exit.exitCode).toBe(0);
expect(exit.stdout).toBe("ok");
});
it("enforces no-output timeout for silent processes", async () => {
const supervisor = createProcessSupervisor();
const run = await supervisor.spawn({
sessionId: "s1",
backendId: "test",
mode: "child",
argv: [process.execPath, "-e", "setTimeout(() => {}, 10_000)"],
timeoutMs: 5_000,
noOutputTimeoutMs: 200,
stdinMode: "pipe-closed",
});
const exit = await run.wait();
expect(exit.reason).toBe("no-output-timeout");
expect(exit.noOutputTimedOut).toBe(true);
expect(exit.timedOut).toBe(true);
});
it("cancels prior scoped run when replaceExistingScope is enabled", async () => {
const supervisor = createProcessSupervisor();
const first = await supervisor.spawn({
sessionId: "s1",
backendId: "test",
scopeKey: "scope:a",
mode: "child",
argv: [process.execPath, "-e", "setTimeout(() => {}, 10_000)"],
timeoutMs: 10_000,
stdinMode: "pipe-open",
});
const second = await supervisor.spawn({
sessionId: "s1",
backendId: "test",
scopeKey: "scope:a",
replaceExistingScope: true,
mode: "child",
argv: [process.execPath, "-e", 'process.stdout.write("new")'],
timeoutMs: 2_000,
stdinMode: "pipe-closed",
});
const firstExit = await first.wait();
const secondExit = await second.wait();
expect(firstExit.reason === "manual-cancel" || firstExit.reason === "signal").toBe(true);
expect(secondExit.reason).toBe("exit");
expect(secondExit.stdout).toBe("new");
});
it("applies overall timeout even for near-immediate timer firing", async () => {
const supervisor = createProcessSupervisor();
const run = await supervisor.spawn({
sessionId: "s-timeout",
backendId: "test",
mode: "child",
argv: [process.execPath, "-e", "setTimeout(() => {}, 10_000)"],
timeoutMs: 1,
stdinMode: "pipe-closed",
});
const exit = await run.wait();
expect(exit.reason).toBe("overall-timeout");
expect(exit.timedOut).toBe(true);
});
it("can stream output without retaining it in RunExit payload", async () => {
const supervisor = createProcessSupervisor();
let streamed = "";
const run = await supervisor.spawn({
sessionId: "s-capture",
backendId: "test",
mode: "child",
argv: [process.execPath, "-e", 'process.stdout.write("streamed")'],
timeoutMs: 2_000,
stdinMode: "pipe-closed",
captureOutput: false,
onStdout: (chunk) => {
streamed += chunk;
},
});
const exit = await run.wait();
expect(streamed).toBe("streamed");
expect(exit.stdout).toBe("");
});
});