From df748c99775080145dd9cb97a9bf70287f6f6e93 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 5 Oct 2026 19:31:28 -0500 Subject: [PATCH] fix: canonicalize owned Runner E2E temporary roots --- doc/plans/2026-10-02-pi-production-readiness.md | 11 +++++++++++ tests/runner-e2e/launch.ts | 10 +++++----- tests/runner-e2e/server-config.test.ts | 16 ++++++++++++++-- tests/runner-e2e/server-config.ts | 8 +++++++- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/doc/plans/2026-10-02-pi-production-readiness.md b/doc/plans/2026-10-02-pi-production-readiness.md index 9335bb13326..b446525c15b 100644 --- a/doc/plans/2026-10-02-pi-production-readiness.md +++ b/doc/plans/2026-10-02-pi-production-readiness.md @@ -15,6 +15,17 @@ failures remain retained. The Mac file-edit correction also passes. ### Latest boundary fixes — 2026-10-05 +The next explicit Mac attempt at `f5024863e` confirms that the description is +no longer duplicated, but still times out with cleanup passing. Its runtime +context advertises `/tmp/.../live`, while the native Pi grant binds the physical +`/private/tmp/.../live` directory. The fixture now canonicalizes its owned temp +root before configuring the instance and validates cleanup against that same +physical parent. A symlink-ancestor regression fails before the change. The +normal installed Pi policy rejects the old advertised alias, accepts the +corrected physical root, and continues to deny an unrelated root, with zero +model calls. The byte and timeout assertions remain unchanged. This correction +only changes the harness; shipping runtime artifacts remain `f5024863e`. + The explicit version-12 Mac memory attempt at `867fa4711` still times out at the unchanged native-session limit; cleanup passes. Its valid fenced JSON arrives in a duplicated description: the Markdown paste capture inserts the diff --git a/tests/runner-e2e/launch.ts b/tests/runner-e2e/launch.ts index 708c3fecbc3..90694c00f34 100644 --- a/tests/runner-e2e/launch.ts +++ b/tests/runner-e2e/launch.ts @@ -2,6 +2,7 @@ import { runnerE2EPlaywrightInvocation } from "./web-server-command.js"; import { verifyInstalledDaytonaPlugin } from "./installed-daytona-plugin.js"; import { assertInstalledCliSelection, assertInstalledStartupOnly, verifyInstalledCli } from "./installed-cli.js"; import { createProcessTreeOwner, stopOwnedProcessTree } from "./process-tree-owner.js"; +import { createRunnerE2ETemporaryRoot } from "./server-config.js"; import { randomBytes } from "node:crypto"; import { prepareCodexCiSandbox, requiresCodexCiSandbox } from "./codex-ci-sandbox.js"; import { spawn } from "node:child_process"; @@ -15,8 +16,8 @@ import { cp, lstat, mkdir, - mkdtemp, readFile, + realpath, readdir, rm, symlink, @@ -479,9 +480,8 @@ async function runAttempt(input: { const installedPlugin = await verifyInstalledDaytonaPlugin(process.env, executions); const startedAtMs = Date.now(); const sharedMemoryBaseline = snapshotDarwinSharedMemory(); - const temporaryRoot = await mkdtemp( - path.join(os.tmpdir(), "paperclip-runner-e2e-"), - ); + const temporaryParent = await realpath(os.tmpdir()); + const temporaryRoot = await createRunnerE2ETemporaryRoot(temporaryParent); const publishedResults: RunnerE2EResult[] = []; const publishedResultPaths = new Map(); let attemptSecrets: string[] = []; @@ -818,7 +818,7 @@ async function runAttempt(input: { if (processCleanupFailed) { cleanupError = new Error(`Preserving temporary state after incomplete process cleanup: ${temporaryRoot}`); } else if ( - temporaryRoot.startsWith(`${os.tmpdir()}${path.sep}paperclip-runner-e2e-`) + temporaryRoot.startsWith(`${temporaryParent}${path.sep}paperclip-runner-e2e-`) ) { for (let cleanupAttempt = 1; cleanupAttempt <= 3; cleanupAttempt += 1) { try { diff --git a/tests/runner-e2e/server-config.test.ts b/tests/runner-e2e/server-config.test.ts index 121e0dc2102..ff69beff4f8 100644 --- a/tests/runner-e2e/server-config.test.ts +++ b/tests/runner-e2e/server-config.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { mkdir, mkdtemp, realpath, readFile, rm, symlink } from "node:fs/promises"; import { createServer } from "node:net"; import os from "node:os"; import path from "node:path"; @@ -8,7 +8,7 @@ import { reserveRunnerE2EDatabasePort, type LoopbackPortReservation, } from "./ports.js"; -import { prepareRunnerE2EServerConfig } from "./server-config.js"; +import { createRunnerE2ETemporaryRoot, prepareRunnerE2EServerConfig } from "./server-config.js"; const roots: string[] = []; const reservations: LoopbackPortReservation[] = []; @@ -22,6 +22,18 @@ afterEach(async () => { }); describe("isolated paid E2E database ports", () => { + it("uses a physical temporary root when the host temp directory has an alias", async () => { + const parent = await realpath(await mkdtemp(path.join(os.tmpdir(), "e2e-temp-alias-"))); + roots.push(parent); + const physical = path.join(parent, "physical"); + const alias = path.join(parent, "alias"); + await mkdir(physical); + await symlink(physical, alias); + const root = await createRunnerE2ETemporaryRoot(alias); + expect(root).toBe(await realpath(root)); + expect(path.dirname(root)).toBe(physical); + }); + it("keeps parallel database reservations distinct and held until server spawn", async () => { reservations.push( ...(await Promise.all( diff --git a/tests/runner-e2e/server-config.ts b/tests/runner-e2e/server-config.ts index c9e28b288a1..3d9a42c0552 100644 --- a/tests/runner-e2e/server-config.ts +++ b/tests/runner-e2e/server-config.ts @@ -1,8 +1,14 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, realpath, readFile, writeFile } from "node:fs/promises"; +import os from "node:os"; import path from "node:path"; import { paperclipConfigSchema } from "../../packages/shared/src/config-schema.js"; import { reserveRunnerE2EDatabasePort } from "./ports.js"; +/** Runtime instructions and native filesystem grants must use the same path. */ +export async function createRunnerE2ETemporaryRoot(parent = os.tmpdir()) { + return await realpath(await mkdtemp(path.join(parent, "paperclip-runner-e2e-"))); +} + /** Seed only the disposable fixture. Onboarding preserves this validated config * and still creates the normal secrets and database. Restarts keep the same DB. */