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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user