test(codex-local): cover EEXIST race rejection with mismatched symlink (#5269)

## 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 <devin@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Abhishek gahlot
2026-06-11 03:45:39 +05:30
committed by GitHub
parent f3db7b88ea
commit c32193c85e
@@ -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 // Regression for #5028: older Paperclip versions copied auth.json into the
// managed home instead of symlinking. After upgrading to the symlink-based // managed home instead of symlinking. After upgrading to the symlink-based
// logic, the stale regular file at the target stayed in place and every // logic, the stale regular file at the target stayed in place and every