Download src/commands/agent-exec.construction.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 6.37 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/commands/agent-exec.construction.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/commands/agent-exec.construction.test.ts
-
curl -L -o agent-exec.construction.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/commands/agent-exec.construction.test.ts
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", | |
| }); | |
| }); | |
| }); | |