openclaw / src /commands /agent-exec.construction.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
76289e7 verified
Raw History Blame Contribute Delete
6.37 kB
import fs from "node:fs/promises";
import path from "node:path";
import { setTimeout as realDelay } from "node:timers/promises";
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, describe, expect, it, vi } from "vitest";
import { isProcessAlive, waitForDead, waitForPidFile } from "../../test/helpers/process-wait.js";
import { createDeferred } from "../../test/helpers/promise.js";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import * as cliBackends from "../plugins/cli-backends.runtime.js";
import * as processSupervisor from "../process/supervisor/index.js";
import { createProcessSupervisor } from "../process/supervisor/supervisor.js";
import type { SpawnInput } from "../process/supervisor/types.js";
import type { RuntimeEnv } from "../runtime.js";
import { withEnvAsync } from "../test-utils/env.js";
import { agentExecCommand } from "./agent-exec.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
describe("agent exec command composition", () => {
it("bounds blocked private-input construction through the shipped CLI command", async () => {
const root = tempDirs.make("openclaw-agent-exec-service-construction-");
const pidPath = path.join(root, "command.pid");
const configPath = path.join(root, "openclaw.json");
const createSecretData = vi.fn(() => Buffer.alloc(8 * 1024 * 1024, 97));
// Claude's plugin-owned SDK bypasses the supervisor. A registered process backend
// keeps the real command route and blocks construction on an unread secret pipe.
vi.spyOn(cliBackends, "resolveRuntimeCliBackends").mockReturnValue([
{
id: "construction-cli",
pluginId: "construction-test",
config: {
command: process.execPath,
args: [
"-e",
`require("node:fs").writeFileSync(${JSON.stringify(pidPath)}, String(process.pid)); setInterval(() => {}, 1000);`,
],
input: "stdin",
output: "text",
},
prepareExecution: () => ({
beforeExecution: async () => {
// Freeze at the backend's queue admission, after cold command preparation.
// Real process readiness must precede advancing the construction deadline.
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
},
secretInput: {
fd: 3,
fingerprint: "synthetic-construction",
createData: createSecretData,
},
}),
},
]);
await fs.writeFile(
configPath,
JSON.stringify({
agents: {
defaults: {
model: { primary: "construction-cli/model-a" },
// The synthetic backend has no reasoning capability to discover.
thinkingDefault: "off",
},
},
}),
"utf8",
);
const completed = await withEnvAsync(
{
NODE_DISABLE_COMPILE_CACHE: "1",
OPENCLAW_SERVICE_MARKER: "openclaw",
},
async () => {
const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
// POSIX relay cancellation loses cleanup identity. Keep that failed owner
// local so shared-worker teardown cannot inherit its expected uncertainty.
const supervisor = createProcessSupervisor();
vi.spyOn(processSupervisor, "getProcessSupervisor").mockReturnValue(supervisor);
const spawn = supervisor.spawn.bind(supervisor);
const admitted = createDeferred<SpawnInput>();
let pendingRun: ReturnType<typeof spawn> | undefined;
vi.spyOn(supervisor, "spawn").mockImplementation((input) => {
const pending = spawn(input);
pendingRun = pending;
admitted.resolve(input);
return pending;
});
const result = agentExecCommand(
"probe",
{ config: configPath, cwd: root, timeout: "1", json: true },
runtime,
);
let commandPid: number | undefined;
let input: SpawnInput | undefined;
try {
input = await Promise.race([
admitted.promise,
result.then((finished) => {
throw new Error(
`Command ended before supervisor admission: ${JSON.stringify(finished)}`,
);
}),
]);
commandPid = await waitForPidFile(pidPath, 3_000, realDelay);
expect(isProcessAlive(commandPid)).toBe(true);
expect(createSecretData).toHaveBeenCalledOnce();
expect(input).toMatchObject({ mode: "child" });
const remainingMs = expectDefined(input.timeoutMs, "remaining construction deadline");
expect(remainingMs).toBeGreaterThan(0);
expect(remainingMs).toBeLessThanOrEqual(1_000);
const processRun = expectDefined(pendingRun, "admitted supervisor process");
const settled = vi.fn();
void processRun.then(settled, settled);
await vi.advanceTimersByTimeAsync(remainingMs - 1);
expect(settled).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);
const managed = await processRun;
await expect(managed.wait()).resolves.toMatchObject({ reason: "overall-timeout" });
expect(managed.activity.resultSettled).toBe(true);
vi.useRealTimers();
const finished = await result;
await waitForDead(commandPid, 5_000);
return finished;
} finally {
vi.useRealTimers();
if (input?.runId) {
supervisor.cancel(input.runId);
}
if (commandPid && isProcessAlive(commandPid)) {
process.kill(commandPid, "SIGKILL");
}
await result;
const cleanup = supervisor.shutdown();
// Windows uses a direct child; only POSIX loses the relay's cleanup authority.
if (process.platform === "win32") {
await expect(cleanup).resolves.toBeUndefined();
} else {
await expect(cleanup).rejects.toThrow("service child cleanup identity lost");
}
}
},
);
expect(completed.exitCode).toBe(2);
expect(completed.envelope).toMatchObject({
ok: false,
status: "timeout",
});
});
});