From c32193c85ef45efdc36b4057d0f566e9b3de074c Mon Sep 17 00:00:00 2001 From: Abhishek gahlot Date: Thu, 11 Jun 2026 03:45:39 +0530 Subject: [PATCH] test(codex-local): cover EEXIST race rejection with mismatched symlink (#5269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - The `codex-local` adapter sets up a per-company Codex home with an auth symlink. Between `lstat` and `symlink` there is a race where two concurrent setups can both try to create the same symlink, surfacing `EEXIST`. > - Master already handles this at runtime via `createExpectedSymlink`, which accepts `EEXIST` only when the raced-in entry resolves to the expected source, and ships a regression test for the tolerated-race path (symlink already points at the right place). > - The symmetric path — `EEXIST` raised by a symlink pointing somewhere else — must stay strictly rejected so a future refactor cannot silently weaken the guard. > - This PR locks that in with a single additive test. No production code change. ## What Changed - Added one regression test in `packages/adapters/codex-local/src/server/codex-home.test.ts` that injects an `EEXIST` whose raced-in symlink target points at a different file, and asserts: - `prepareManagedCodexHome` rejects with `code: "EEXIST"`. - The mismatched symlink is left on disk (we do not blindly overwrite the raced-in entry). Complements the existing "treats a concurrently-created expected auth symlink as success" test already on master. Refs #5240 (Stack B — codex-home adapter session/auth handling). ## Verification - `pnpm --filter @paperclipai/adapter-codex-local exec vitest run src/server/codex-home.test.ts` — passes. - `pnpm --filter @paperclipai/adapter-codex-local typecheck` — clean. ## Risks - Test-only change. No production code is modified. ## Model Used - Provider: Anthropic - Model: Claude (Opus 4.7) - Mode/capabilities: tool-using coding agent with shell execution and test verification ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge - [x] I searched the GitHub PR list for similar PRs and confirmed this is not a duplicate Co-authored-by: Devin Foley Co-authored-by: Paperclip --- .../codex-local/src/server/codex-home.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/adapters/codex-local/src/server/codex-home.test.ts b/packages/adapters/codex-local/src/server/codex-home.test.ts index 840db456..a6e6aa86 100644 --- a/packages/adapters/codex-local/src/server/codex-home.test.ts +++ b/packages/adapters/codex-local/src/server/codex-home.test.ts @@ -55,6 +55,54 @@ describe("codex managed home", () => { } }); + it("still throws on EEXIST when a raced-in auth symlink points elsewhere", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-home-")); + const sharedCodexHome = path.join(root, "shared-codex-home"); + const paperclipHome = path.join(root, "paperclip-home"); + const managedCodexHome = path.join( + paperclipHome, + "instances", + "default", + "companies", + "company-1", + "codex-home", + ); + const sharedAuth = path.join(sharedCodexHome, "auth.json"); + const wrongAuth = path.join(sharedCodexHome, "other-auth.json"); + const managedAuth = path.join(managedCodexHome, "auth.json"); + + await fs.mkdir(sharedCodexHome, { recursive: true }); + await fs.writeFile(sharedAuth, '{"token":"shared"}\n', "utf8"); + await fs.writeFile(wrongAuth, '{"token":"other"}\n', "utf8"); + + const originalSymlink = fs.symlink.bind(fs); + vi.spyOn(fs, "symlink").mockImplementationOnce(async (_source, target, type) => { + await originalSymlink(wrongAuth, target, type); + const error = new Error("file already exists") as NodeJS.ErrnoException; + error.code = "EEXIST"; + throw error; + }); + + try { + await expect( + prepareManagedCodexHome( + { + CODEX_HOME: sharedCodexHome, + PAPERCLIP_HOME: paperclipHome, + PAPERCLIP_INSTANCE_ID: "default", + }, + async () => {}, + "company-1", + ), + ).rejects.toMatchObject({ code: "EEXIST" }); + + expect((await fs.lstat(managedAuth)).isSymbolicLink()).toBe(true); + expect(await fs.readlink(managedAuth)).toBe(wrongAuth); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + // Regression for #5028: older Paperclip versions copied auth.json into the // managed home instead of symlinking. After upgrading to the symlink-based // logic, the stale regular file at the target stayed in place and every