From 1275ef8282317de030d71d5131a544cc25060218 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Sat, 3 Oct 2026 23:19:25 -0700 Subject: [PATCH] test: isolate runtime port claims and canonicalize fixture paths Co-Authored-By: Paperclip --- .../src/__tests__/workspace-runtime.test.ts | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index 187135d1959..d2b2e3b8b3a 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -43,6 +43,7 @@ import { releaseRuntimeServicesForRun, UnresolvedWorkspaceBaseRefError, resetRuntimeServicesForTests, + resetRuntimeServicePortReservationsForTests, MANAGED_RUNTIME_PUBLIC_URL_ENV, resolveManagedPaperclipRuntimePublicOrigin, resolveRuntimeProvisionCommand, @@ -454,6 +455,9 @@ afterEach(async () => { delete process.env.PAPERCLIP_WORKTREES_DIR; delete process.env.DATABASE_URL; await resetRuntimeServicesForTests(); + // Registry reset does not clear the process-local allocation claims. A + // failed-start fixture must not reserve another test's ephemeral port. + resetRuntimeServicePortReservationsForTests(); }); describe("sanitizeRuntimeServiceBaseEnv", () => { @@ -4099,6 +4103,7 @@ describe("realizeExecutionWorkspace", () => { "utf8", ); process.env.PAPERCLIP_WORKTREES_DIR = worktreesDir; + const canonicalInstanceRoot = await fs.realpath(instanceRoot); await cleanupExecutionWorkspaceArtifacts({ workspace: { @@ -4133,7 +4138,7 @@ describe("realizeExecutionWorkspace", () => { expect(operations[0]?.command).toBe("printf 'cleanup ok\\n'"); expect(operations[1]?.metadata).toMatchObject({ cleanupAction: "remove_worktree_instance", - instanceRoot, + instanceRoot: canonicalInstanceRoot, }); expect(operations[2]?.metadata).toMatchObject({ cleanupAction: "worktree_remove", @@ -7643,16 +7648,25 @@ describeEmbeddedPostgres("workspace runtime startup reconciliation", () => { process.env.PAPERCLIP_HOME = paperclipHome; process.env.PAPERCLIP_INSTANCE_ID = `runtime-desired-reconcile-${randomUUID()}`; - const reservePort = async () => { - const probe = net.createServer(); - await new Promise((resolve) => probe.listen(0, "127.0.0.1", resolve)); - const address = probe.address(); - const port = typeof address === "object" && address ? address.port : null; - await new Promise((resolve, reject) => { - probe.close((error) => error ? reject(error) : resolve()); - }); - if (!port) throw new Error("Failed to reserve runtime reconciliation test port"); - return port; + const reservePorts = async () => { + const probes = [net.createServer(), net.createServer()]; + try { + // Keep both sockets open until both allocations finish. Closing the + // first probe early lets the kernel return that same port again. + for (const probe of probes) await new Promise((resolve, reject) => { + probe.once("error", reject); + probe.listen(0, "127.0.0.1", resolve); + }); + return probes.map(probe => { + const address = probe.address(); + if (!address || typeof address !== "object") throw new Error("Failed to reserve runtime reconciliation test port"); + return address.port; + }); + } finally { + await Promise.all(probes.filter(probe => probe.listening).map(probe => new Promise((resolve, reject) => { + probe.close(error => error ? reject(error) : resolve()); + }))); + } }; const isLoopbackPortFree = async (port: number) => { const probe = net.createServer(); @@ -7673,8 +7687,8 @@ describeEmbeddedPostgres("workspace runtime startup reconciliation", () => { } throw new Error(`Port ${port} did not become free in time`); }; - const stoppedPort = await reservePort(); - const livePort = await reservePort(); + const [stoppedPort, livePort] = await reservePorts(); + expect(stoppedPort).not.toBe(livePort); const companyId = randomUUID(); const projectId = randomUUID(); const projectWorkspaceId = randomUUID(); @@ -9728,7 +9742,7 @@ describe("realizeExecutionWorkspace with an exact existing branch", () => { const workspace = await realizeExistingBranch(repoRoot, "feature/legacy-checkout"); - expect(workspace.cwd).toBe(path.resolve(legacyPath)); + expect(workspace.cwd).toBe(await fs.realpath(legacyPath)); expect(workspace.branchName).toBe("feature/legacy-checkout"); expect(workspace.created).toBe(false); expect(await readGit(workspace.cwd, ["rev-parse", "HEAD"])).toBe(branchTip);