diff --git a/tests/runner-e2e/continuation-flow.ts b/tests/runner-e2e/continuation-flow.ts index 9308a7157df..7c26f0aa228 100644 --- a/tests/runner-e2e/continuation-flow.ts +++ b/tests/runner-e2e/continuation-flow.ts @@ -1,6 +1,6 @@ import { expect, type Page } from "@playwright/test"; -import { writeFile } from "node:fs/promises"; import path from "node:path"; +import { seedContinuationContext } from "./continuation-workspace.js"; import { pollUntil, type RunnerApi } from "./api.js"; import { chatQuestionPresentation, @@ -192,11 +192,6 @@ export async function runContinuationFlow(input: { await api.patch("/api/instance/settings/experimental", { enableClassicTaskInterface: false, }); - if (scenario.id === "untrusted-evidence") - await writeFile( - path.join(input.workspacePath, "context.txt"), - scenario.context, - ); await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, @@ -218,6 +213,14 @@ export async function runContinuationFlow(input: { await settle(new Set()); await snapshot("initial"); assertWaiting(); + if (scenario.id === "untrusted-evidence") { + const parentRun = runs.find((r) => r.contextSnapshot?.issueId === issue!.id); + await seedContinuationContext({ + isolatedRoot: path.dirname(input.workspacePath), + recordedCwd: parentRun?.contextSnapshot?.paperclipWorkspace?.cwd, + body: scenario.context, + }); + } if (scenario.id === "completed-action-resume") { await input.restart(); await open(); diff --git a/tests/runner-e2e/continuation-workspace.ts b/tests/runner-e2e/continuation-workspace.ts new file mode 100644 index 00000000000..a5be8946172 --- /dev/null +++ b/tests/runner-e2e/continuation-workspace.ts @@ -0,0 +1,22 @@ +import { realpath, writeFile } from "node:fs/promises"; +import path from "node:path"; + +/** Seed the realized agent workspace, which can differ from the harness cwd. */ +export async function seedContinuationContext(input: { + isolatedRoot: string; + recordedCwd: unknown; + body: string; +}) { + if (typeof input.recordedCwd !== "string" || !path.isAbsolute(input.recordedCwd)) + throw new Error("Continuation run did not record an absolute workspace cwd"); + const [root, cwd] = await Promise.all([ + realpath(input.isolatedRoot), + realpath(input.recordedCwd), + ]); + const relative = path.relative(root, cwd); + if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) + throw new Error("Continuation workspace escaped the isolated instance"); + const file = path.join(cwd, "context.txt"); + await writeFile(file, input.body, { flag: "wx" }); + return file; +} diff --git a/tests/runner-e2e/continuation.test.ts b/tests/runner-e2e/continuation.test.ts index d77b89bcaf2..3b1fa54e676 100644 --- a/tests/runner-e2e/continuation.test.ts +++ b/tests/runner-e2e/continuation.test.ts @@ -1,6 +1,7 @@ -import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises"; +import { mkdtemp, mkdir, writeFile, readFile, realpath, symlink, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { seedContinuationContext } from "./continuation-workspace.js"; import { packageEvidence } from "./evidence.js"; import { continuationScreenshotFile } from "./continuation-cases.js"; import { describe, expect, it } from "vitest"; @@ -163,3 +164,21 @@ it("packages all continuation checkpoints using the shared evidence rules", asyn await rm(root, { recursive: true, force: true }); } }); + + +it("seeds the recorded agent home rather than the harness workspace", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "continuation-cwd-")); + try { + const recordedCwd = path.join(root, "instance", "agent-home"); + await mkdir(recordedCwd, { recursive: true }); + const file = await seedContinuationContext({ isolatedRoot: root, recordedCwd, body: "Venue reference: factual data" }); + expect(file).toBe(path.join(await realpath(recordedCwd), "context.txt")); + expect(await readFile(file, "utf8")).toBe("Venue reference: factual data"); + await expect(seedContinuationContext({ isolatedRoot: root, recordedCwd, body: "replacement" })).rejects.toThrow(); + await expect(seedContinuationContext({ isolatedRoot: root, recordedCwd: undefined, body: "data" })).rejects.toThrow("record an absolute"); + await symlink(os.tmpdir(), path.join(root, "outside")); + await expect(seedContinuationContext({ isolatedRoot: root, recordedCwd: path.join(root, "outside"), body: "data" })).rejects.toThrow("escaped"); + } finally { + await rm(root, { recursive: true, force: true }); + } +});