mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 05:00:43 +00:00
160 lines
4.5 KiB
TypeScript
160 lines
4.5 KiB
TypeScript
import { afterEach, beforeAll, beforeEach, expect, test, vi } from "vitest";
|
|
import {
|
|
onInternalDiagnosticEvent,
|
|
resetDiagnosticEventsForTest,
|
|
type DiagnosticEventPayload,
|
|
} from "../infra/diagnostic-events.js";
|
|
import type { ManagedRun, SpawnInput } from "../process/supervisor/index.js";
|
|
|
|
let listRunningSessions: typeof import("./bash-process-registry.js").listRunningSessions;
|
|
let resetProcessRegistryForTests: typeof import("./bash-process-registry.js").resetProcessRegistryForTests;
|
|
let runExecProcess: typeof import("./bash-tools.exec-runtime.js").runExecProcess;
|
|
|
|
const { supervisorSpawnMock } = vi.hoisted(() => ({
|
|
supervisorSpawnMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../process/supervisor/index.js", () => ({
|
|
getProcessSupervisor: () => ({
|
|
spawn: supervisorSpawnMock,
|
|
cancel: vi.fn(),
|
|
cancelScope: vi.fn(),
|
|
reconcileOrphans: vi.fn(),
|
|
getRecord: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
function createSuccessfulRun(input: SpawnInput): ManagedRun {
|
|
input.onStdout?.("ok");
|
|
return {
|
|
runId: input.runId ?? "test-run",
|
|
pid: 1234,
|
|
startedAtMs: Date.now(),
|
|
stdin: {
|
|
write: vi.fn(),
|
|
end: vi.fn(),
|
|
destroy: vi.fn(),
|
|
},
|
|
cancel: vi.fn(),
|
|
wait: vi.fn(async () => ({
|
|
reason: "exit" as const,
|
|
exitCode: 0,
|
|
exitSignal: null,
|
|
durationMs: 1,
|
|
stdout: "",
|
|
stderr: "",
|
|
timedOut: false,
|
|
noOutputTimedOut: false,
|
|
})),
|
|
};
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
({ listRunningSessions, resetProcessRegistryForTests } =
|
|
await import("./bash-process-registry.js"));
|
|
({ runExecProcess } = await import("./bash-tools.exec-runtime.js"));
|
|
});
|
|
|
|
beforeEach(() => {
|
|
supervisorSpawnMock.mockReset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
resetProcessRegistryForTests();
|
|
resetDiagnosticEventsForTest();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
function runPtyFallback(warnings: string[] = []) {
|
|
return runExecProcess({
|
|
command: "printf ok",
|
|
workdir: process.cwd(),
|
|
env: {},
|
|
usePty: true,
|
|
warnings,
|
|
maxOutput: 20_000,
|
|
pendingMaxOutput: 20_000,
|
|
notifyOnExit: false,
|
|
timeoutSec: 5,
|
|
});
|
|
}
|
|
|
|
test("exec falls back when PTY spawn fails", async () => {
|
|
supervisorSpawnMock
|
|
.mockRejectedValueOnce(new Error("pty spawn failed"))
|
|
.mockImplementationOnce(async (input: SpawnInput) => createSuccessfulRun(input));
|
|
|
|
const warnings: string[] = [];
|
|
const handle = await runPtyFallback(warnings);
|
|
const outcome = await handle.promise;
|
|
|
|
expect(outcome.status).toBe("completed");
|
|
expect(outcome.aggregated).toContain("ok");
|
|
expect(warnings.join("\n")).toContain("PTY spawn failed");
|
|
expect(supervisorSpawnMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ mode: "pty" }));
|
|
expect(supervisorSpawnMock).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({ mode: "child" }),
|
|
);
|
|
});
|
|
|
|
test("exec cleans session state when PTY fallback spawn also fails", async () => {
|
|
supervisorSpawnMock
|
|
.mockRejectedValueOnce(new Error("pty spawn failed"))
|
|
.mockRejectedValueOnce(new Error("child fallback failed"));
|
|
|
|
await expect(runPtyFallback()).rejects.toThrow("child fallback failed");
|
|
|
|
expect(listRunningSessions()).toHaveLength(0);
|
|
});
|
|
|
|
function flushDiagnosticEvents() {
|
|
return new Promise<void>((resolve) => setImmediate(resolve));
|
|
}
|
|
|
|
test("exec emits bounded process diagnostics without command text", async () => {
|
|
supervisorSpawnMock.mockImplementationOnce(async (input: SpawnInput) =>
|
|
createSuccessfulRun(input),
|
|
);
|
|
const events: DiagnosticEventPayload[] = [];
|
|
const unsubscribe = onInternalDiagnosticEvent((event) => {
|
|
events.push(event);
|
|
});
|
|
try {
|
|
const command = "printf super-secret-value";
|
|
const handle = await runExecProcess({
|
|
command,
|
|
workdir: process.cwd(),
|
|
env: {},
|
|
usePty: false,
|
|
warnings: [],
|
|
maxOutput: 20_000,
|
|
pendingMaxOutput: 20_000,
|
|
notifyOnExit: false,
|
|
sessionKey: "session-1",
|
|
timeoutSec: 5,
|
|
});
|
|
|
|
await handle.promise;
|
|
await flushDiagnosticEvents();
|
|
|
|
const event = events.find((item) => item.type === "exec.process.completed");
|
|
expect(event).toMatchObject({
|
|
type: "exec.process.completed",
|
|
target: "host",
|
|
mode: "child",
|
|
outcome: "completed",
|
|
durationMs: expect.any(Number),
|
|
commandLength: command.length,
|
|
exitCode: 0,
|
|
sessionKey: "session-1",
|
|
});
|
|
const serialized = JSON.stringify(event);
|
|
expect(serialized).not.toContain("printf");
|
|
expect(serialized).not.toContain("super-secret-value");
|
|
expect(serialized).not.toContain(process.cwd());
|
|
} finally {
|
|
unsubscribe();
|
|
}
|
|
});
|