test(codex): a fresh start's continuation claims its attempt first and keeps the launch's guidance choice

Two concurrent continuations of a fresh Codex start that stopped at first login must deliver once,
and neither may write the repository AGENTS.md when the launch carried the guidance as developer
instructions. Also pins that a seat with no declared CODEX_HOME reads the person's ~/.codex.
This commit is contained in:
dev60-review committed 2026-11-03 06:49:06 +00:00
1 parent fb24a4ea8f
commit 648ef07d3e
2 files changed
+40

No files matched your search

@@ -286,6 +286,35 @@ describe("late Codex fork identity", () => {
expect(tmux.sendText).toHaveBeenCalledTimes(1);
expect(tmux.sendShellCommand).toHaveBeenCalledTimes(1);
});
// #875: a fresh start that chose launch developer instructions and stopped at Codex's first login. Its continuations
// claim the attempt before any await, and keep the launch's choice: nothing goes into the repository AGENTS.md.
it("consumes a fresh start's pending attempt before concurrent continuations, and keeps its launch guidance choice (#875)", async () => {
let screen = fs.readFileSync(new URL("./fixtures/codex-first-login.txt", import.meta.url), "utf8");
vi.mocked(tmux.capturePaneContent).mockImplementation(async () => screen);
processRows[1] = { ...processRows[1]!, command: "codex", argv: ["codex"] };
const written: string[] = [];
adapter = new CodexRuntimeAdapter({
tmux, codexHome: "/fixture/codex-home",
fsOps: { readFile: file => file.endsWith("culture.md") ? "Team culture." : ROLE, exists: file => file.endsWith("role.md") || file.endsWith("culture.md"),
writeFile: file => { written.push(file); }, mkdirp: () => {}, listFiles: () => [] },
listProcesses: async () => processRows, readThreadIdByPid: () => threadId,
readinessSettings: { resolveOne: () => ({ value: readinessSeconds }) } as unknown as Pick<SettingsStore, "resolveOne">,
resolveManagedConfigSettings: async () => ({ enabled: false, codexVersion: "0.164.0" }), sleep: async () => {},
});
const culture = { category: "guidance", effectiveId: "culture.md", sourceSpec: "base", sourcePath: "/fixture", resourcePath: "culture.md",
absolutePath: "/fixture/culture.md", mergeStrategy: "managed_block", classification: "safe_projection" };
const fresh = { ...input, adapter, forkSource: undefined, plan: { ...input.plan, entries: [culture] } } as unknown as StartupInput;
expect(await orch.startNode(fresh)).toMatchObject({ ok: false, startupStatus: "attention_required" });
expect(vi.mocked(tmux.sendShellCommand).mock.calls[0]?.[1]).toContain("developer_instructions");
screen = "OpenAI Codex (v0.164.0)\n› ";
threadId = CHILD;
const results = await Promise.all([lifecycle().continueStartup("builder@fork-recovery"), lifecycle().continueStartup("builder@fork-recovery")]);
expect(results.filter(result => result.ok)).toHaveLength(1);
expect(vi.mocked(tmux.sendText).mock.calls.filter(([, text]) => String(text).includes(ROLE))).toHaveLength(1);
expect(tmux.sendShellCommand).toHaveBeenCalledTimes(1);
expect(written.filter(file => file.endsWith("AGENTS.md"))).toEqual([]);
});
describe("with the real Codex record reader", () => {
const START = lstartToMinTs(STARTED)!;
let codexHome: string;
@@ -2829,6 +2829,17 @@ describe("a fresh Codex seat's managed guidance as launch developer instructions
expect(chosen.note).toMatch(/repository AGENTS\.md/);
});
// With no CODEX_HOME declared for the seat or the daemon, the launch's Codex uses the person's ~/.codex, so that's the
// home read. (One exported only in the person's shell startup, with the daemon started without it, isn't seen.)
it("reads the person's ~/.codex when neither the seat nor the daemon declares CODEX_HOME", async () => {
const withoutHome = (fs: CodexAdapterFsOps) => new CodexRuntimeAdapter({ tmux: mockTmux(), fsOps: { ...fs, homedir: "/person" },
listProcesses: () => [], sleep: async () => {}, resolveManagedConfigSettings: async () => ({ enabled: false, codexVersion: "0.164.0" }) });
expect(await withoutHome(mockFs()).guidanceFileFor(makeBinding())).toEqual({ file: null, launchInstructions: true });
const chosen = await withoutHome(mockFs({ "/person/.codex/config.toml": 'developer_instructions = "Mine."\n' })).guidanceFileFor(makeBinding());
expect(chosen.launchInstructions).toBeUndefined();
expect(chosen.note).toContain("/person/.codex/config.toml sets developer_instructions");
});
it("sends the guidance with the fresh launch, writes neither the repository nor the home AGENTS.md, and keeps roles and the seat-skills line out", async () => {
const fs = mockFs({ "/spec/culture.md": "Team culture.", "/spec/SOP.md": "How work flows here.", "/spec/rig-role.md": "Seat role text.",
"/spec/openrig-seat-skills.md": "Seat skills line." }) as CodexAdapterFsOps & { _store: Record<string, string> };