diff --git a/src/agents/pi-embedded-runner.e2e.test.ts b/src/agents/pi-embedded-runner.e2e.test.ts index 5617af016..24f8f68a8 100644 --- a/src/agents/pi-embedded-runner.e2e.test.ts +++ b/src/agents/pi-embedded-runner.e2e.test.ts @@ -6,6 +6,31 @@ import "./test-helpers/fast-coding-tools.js"; import type { OpenClawConfig } from "../config/config.js"; import { ensureOpenClawModelsJson } from "./models-config.js"; +vi.mock("@mariozechner/pi-coding-agent", async () => { + const actual = await vi.importActual( + "@mariozechner/pi-coding-agent", + ); + + return { + ...actual, + createAgentSession: async ( + ...args: Parameters + ): ReturnType => { + const result = await actual.createAgentSession(...args); + const modelId = (args[0] as { model?: { id?: string } } | undefined)?.model?.id; + if (modelId === "mock-throw") { + const session = result.session as { prompt?: (...params: unknown[]) => Promise }; + if (session && typeof session.prompt === "function") { + session.prompt = async () => { + throw new Error("transport failed"); + }; + } + } + return result; + }, + }; +}); + vi.mock("@mariozechner/pi-ai", async () => { const actual = await vi.importActual("@mariozechner/pi-ai"); @@ -73,9 +98,6 @@ vi.mock("@mariozechner/pi-ai", async () => { return buildAssistantMessage(model); }, streamSimple: (model: { api: string; provider: string; id: string }) => { - if (model.id === "mock-throw") { - throw new Error("transport failed"); - } const stream = actual.createAssistantMessageEventStream(); queueMicrotask(() => { stream.push({ @@ -384,34 +406,28 @@ describe("runEmbeddedPiAgent", () => { expect(userIndex).toBeGreaterThanOrEqual(0); }); - it("persists prompt transport errors as transcript entries", async () => { + it("fails fast on prompt transport errors", async () => { const sessionFile = nextSessionFile(); const cfg = makeOpenAiConfig(["mock-throw"]); await ensureModels(cfg); - const result = await runEmbeddedPiAgent({ - sessionId: "session:test", - sessionKey: testSessionKey, - sessionFile, - workspaceDir, - config: cfg, - prompt: "transport error", - provider: "openai", - model: "mock-throw", - timeoutMs: 5_000, - agentDir, - runId: nextRunId("transport-error"), - enqueue: immediateEnqueue, - }); - expect(result.payloads?.[0]?.isError).toBe(true); - - const entries = await readSessionEntries(sessionFile); - const promptErrorEntry = entries.find( - (entry) => entry.type === "custom" && entry.customType === "openclaw:prompt-error", - ) as { data?: { error?: string } } | undefined; - - expect(promptErrorEntry).toBeTruthy(); - expect(promptErrorEntry?.data?.error).toContain("transport failed"); + await expect( + runEmbeddedPiAgent({ + sessionId: "session:test", + sessionKey: testSessionKey, + sessionFile, + workspaceDir, + config: cfg, + prompt: "transport error", + provider: "openai", + model: "mock-throw", + timeoutMs: 5_000, + agentDir, + runId: nextRunId("transport-error"), + enqueue: immediateEnqueue, + }), + ).rejects.toThrow("transport failed"); + await expect(fs.stat(sessionFile)).rejects.toBeTruthy(); }); it( diff --git a/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts b/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts index a054377d7..573922e61 100644 --- a/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts +++ b/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts @@ -20,10 +20,10 @@ vi.mock("./models-config.js", async (importOriginal) => { }; }); -let runEmbeddedPiAgent: typeof import("./pi-embedded-runner.js").runEmbeddedPiAgent; +let runEmbeddedPiAgent: typeof import("./pi-embedded-runner/run.js").runEmbeddedPiAgent; beforeAll(async () => { - ({ runEmbeddedPiAgent } = await import("./pi-embedded-runner.js")); + ({ runEmbeddedPiAgent } = await import("./pi-embedded-runner/run.js")); }); beforeEach(() => { diff --git a/src/agents/pi-tools.safe-bins.e2e.test.ts b/src/agents/pi-tools.safe-bins.e2e.test.ts index a06c7bf31..551d18e13 100644 --- a/src/agents/pi-tools.safe-bins.e2e.test.ts +++ b/src/agents/pi-tools.safe-bins.e2e.test.ts @@ -24,7 +24,7 @@ vi.mock("../infra/shell-env.js", async (importOriginal) => { return { ...mod, getShellPathFromLoginShell: vi.fn(() => null), - resolveShellEnvFallbackTimeoutMs: vi.fn(() => 500), + resolveShellEnvFallbackTimeoutMs: vi.fn(() => 50), }; }); @@ -174,10 +174,10 @@ describe("createOpenClawCodingTools safeBins", () => { ); }); - it("does not leak file existence from sort output flags", async () => { + it("blocks sort output/compress bypass attempts in safeBins mode", async () => { await withSafeBinsExecTool( { - tmpPrefix: "openclaw-safe-bins-oracle-", + tmpPrefix: "openclaw-safe-bins-sort-", safeBins: ["sort"], files: [{ name: "existing.txt", contents: "x\n" }], }, @@ -196,42 +196,21 @@ describe("createOpenClawCodingTools safeBins", () => { const existing = await run("sort -o existing.txt"); const missing = await run("sort -o missing.txt"); expect(existing).toEqual(missing); - }, - ); - }); - it("blocks sort output flags from writing files via safeBins", async () => { - await withSafeBinsExecTool( - { - tmpPrefix: "openclaw-safe-bins-sort-", - safeBins: ["sort"], - }, - async ({ tmpDir, execTool }) => { - const cases = [ + const outputFlagCases = [ { command: "sort -oblocked-short.txt", target: "blocked-short.txt" }, { command: "sort --output=blocked-long.txt", target: "blocked-long.txt" }, ] as const; - - for (const [index, testCase] of cases.entries()) { + for (const [index, testCase] of outputFlagCases.entries()) { await expect( - execTool.execute(`call${index + 1}`, { + execTool.execute(`call-output-${index + 1}`, { command: testCase.command, workdir: tmpDir, }), ).rejects.toThrow("exec denied: allowlist miss"); expect(fs.existsSync(path.join(tmpDir, testCase.target))).toBe(false); } - }, - ); - }); - it("blocks sort --compress-program from bypassing safeBins", async () => { - await withSafeBinsExecTool( - { - tmpPrefix: "openclaw-safe-bins-sort-compress-", - safeBins: ["sort"], - }, - async ({ tmpDir, execTool }) => { await expect( execTool.execute("call1", { command: "sort --compress-program=sh",