* 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)
102 lines
3.0 KiB
TypeScript
102 lines
3.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { captureEnv } from "../test-utils/env.js";
|
|
import { runCommandWithTimeout, shouldSpawnWithShell } from "./exec.js";
|
|
|
|
describe("runCommandWithTimeout", () => {
|
|
it("never enables shell execution (Windows cmd.exe injection hardening)", () => {
|
|
expect(
|
|
shouldSpawnWithShell({
|
|
resolvedCommand: "npm.cmd",
|
|
platform: "win32",
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("passes env overrides to child", async () => {
|
|
const result = await runCommandWithTimeout(
|
|
[process.execPath, "-e", 'process.stdout.write(process.env.OPENCLAW_TEST_ENV ?? "")'],
|
|
{
|
|
timeoutMs: 5_000,
|
|
env: { OPENCLAW_TEST_ENV: "ok" },
|
|
},
|
|
);
|
|
|
|
expect(result.code).toBe(0);
|
|
expect(result.stdout).toBe("ok");
|
|
expect(result.termination).toBe("exit");
|
|
});
|
|
|
|
it("merges custom env with process.env", async () => {
|
|
const envSnapshot = captureEnv(["OPENCLAW_BASE_ENV"]);
|
|
process.env.OPENCLAW_BASE_ENV = "base";
|
|
try {
|
|
const result = await runCommandWithTimeout(
|
|
[
|
|
process.execPath,
|
|
"-e",
|
|
'process.stdout.write((process.env.OPENCLAW_BASE_ENV ?? "") + "|" + (process.env.OPENCLAW_TEST_ENV ?? ""))',
|
|
],
|
|
{
|
|
timeoutMs: 5_000,
|
|
env: { OPENCLAW_TEST_ENV: "ok" },
|
|
},
|
|
);
|
|
|
|
expect(result.code).toBe(0);
|
|
expect(result.stdout).toBe("base|ok");
|
|
expect(result.termination).toBe("exit");
|
|
} finally {
|
|
envSnapshot.restore();
|
|
}
|
|
});
|
|
|
|
it("kills command when no output timeout elapses", async () => {
|
|
const startedAt = Date.now();
|
|
const result = await runCommandWithTimeout(
|
|
[process.execPath, "-e", "setTimeout(() => {}, 10_000)"],
|
|
{
|
|
timeoutMs: 5_000,
|
|
noOutputTimeoutMs: 300,
|
|
},
|
|
);
|
|
|
|
const durationMs = Date.now() - startedAt;
|
|
expect(durationMs).toBeLessThan(2_500);
|
|
expect(result.termination).toBe("no-output-timeout");
|
|
expect(result.noOutputTimedOut).toBe(true);
|
|
expect(result.code).not.toBe(0);
|
|
});
|
|
|
|
it("resets no output timer when command keeps emitting output", async () => {
|
|
const result = await runCommandWithTimeout(
|
|
[
|
|
process.execPath,
|
|
"-e",
|
|
'let i=0; const t=setInterval(() => { process.stdout.write("."); i += 1; if (i >= 5) { clearInterval(t); process.exit(0); } }, 50);',
|
|
],
|
|
{
|
|
timeoutMs: 5_000,
|
|
noOutputTimeoutMs: 200,
|
|
},
|
|
);
|
|
|
|
expect(result.code).toBe(0);
|
|
expect(result.termination).toBe("exit");
|
|
expect(result.noOutputTimedOut).toBe(false);
|
|
expect(result.stdout.length).toBeGreaterThanOrEqual(5);
|
|
});
|
|
|
|
it("reports global timeout termination when overall timeout elapses", async () => {
|
|
const result = await runCommandWithTimeout(
|
|
[process.execPath, "-e", "setTimeout(() => {}, 10_000)"],
|
|
{
|
|
timeoutMs: 200,
|
|
},
|
|
);
|
|
|
|
expect(result.termination).toBe("timeout");
|
|
expect(result.noOutputTimedOut).toBe(false);
|
|
expect(result.code).not.toBe(0);
|
|
});
|
|
});
|