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