fix: deduplicate Codex log followers
This commit is contained in:
@@ -737,6 +737,135 @@ describe("logsWorkflow", () => {
|
||||
expect(output).toContain("initial log content");
|
||||
});
|
||||
|
||||
it("rejects a second follower from the same Codex thread", async () => {
|
||||
const { runDir, writeState, initWorkflowState, gitHead, readState } = await import("./workflow-state.js");
|
||||
const runId = "logs-follow-dedupe-004";
|
||||
const dir = runDir(repoRoot, runId);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const state = initWorkflowState({
|
||||
runId,
|
||||
repoRoot,
|
||||
baselineHead: gitHead(repoRoot),
|
||||
plan: { version: "1", title: "Test", planMarkdown: "Test", scope: ["src/"], acceptanceCriteria: ["Ok"], verificationCommands: [["echo"]] },
|
||||
executor: "claude",
|
||||
maxCycles: 3,
|
||||
});
|
||||
state.status = "executing";
|
||||
state.currentCycle = 1;
|
||||
state.cycles = [{ cycleIndex: 1, startedAt: new Date().toISOString(), executorLogFile: "cycle-1-exec.log" }];
|
||||
writeState(dir, state);
|
||||
fs.writeFileSync(path.join(dir, "cycle-1-exec.log"), "initial log content\n");
|
||||
|
||||
const previousThread = process.env.CODEX_THREAD_ID;
|
||||
process.env.CODEX_THREAD_ID = "thread-for-follower-dedupe";
|
||||
const { logsWorkflow } = await import("./workflow-engine.js");
|
||||
const chunks: string[] = [];
|
||||
const origWrite = process.stdout.write.bind(process.stdout);
|
||||
process.stdout.write = (chunk: unknown) => { chunks.push(String(chunk)); return true; };
|
||||
const firstFollower = logsWorkflow({ runId, follow: true, tail: 5 });
|
||||
try {
|
||||
await expect(logsWorkflow({ runId, follow: true, tail: 5 }))
|
||||
.rejects.toThrow(/already active for this Codex thread/);
|
||||
const curState = readState(dir)!;
|
||||
curState.status = "awaiting_review";
|
||||
writeState(dir, curState);
|
||||
await firstFollower;
|
||||
expect(chunks.join("")).toContain("initial log content");
|
||||
} finally {
|
||||
process.stdout.write = origWrite;
|
||||
if (previousThread === undefined) delete process.env.CODEX_THREAD_ID;
|
||||
else process.env.CODEX_THREAD_ID = previousThread;
|
||||
}
|
||||
});
|
||||
|
||||
it("allows different Codex threads to hold followers for the same run", async () => {
|
||||
const {
|
||||
acquireLogsFollowLock,
|
||||
logsFollowLockPath,
|
||||
releaseLogsFollowLock,
|
||||
runDir,
|
||||
} = await import("./workflow-state.js");
|
||||
const dir = runDir(repoRoot, "logs-follow-different-threads-005");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
const first = acquireLogsFollowLock(dir, "thread-a");
|
||||
const second = acquireLogsFollowLock(dir, "thread-b");
|
||||
try {
|
||||
expect(first).toBe(logsFollowLockPath(dir, "thread-a"));
|
||||
expect(second).toBe(logsFollowLockPath(dir, "thread-b"));
|
||||
expect(first).not.toBe(second);
|
||||
} finally {
|
||||
releaseLogsFollowLock(first);
|
||||
releaseLogsFollowLock(second);
|
||||
}
|
||||
expect(fs.existsSync(first)).toBe(false);
|
||||
expect(fs.existsSync(second)).toBe(false);
|
||||
});
|
||||
|
||||
it("reclaims a follower lock whose process is dead", async () => {
|
||||
const {
|
||||
acquireLogsFollowLock,
|
||||
logsFollowLockPath,
|
||||
releaseLogsFollowLock,
|
||||
runDir,
|
||||
} = await import("./workflow-state.js");
|
||||
const dir = runDir(repoRoot, "logs-follow-stale-006");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const lockPath = logsFollowLockPath(dir, "stale-thread");
|
||||
fs.writeFileSync(lockPath, JSON.stringify({
|
||||
pid: 999_999_999,
|
||||
codexThreadId: "stale-thread",
|
||||
startedAt: new Date(0).toISOString(),
|
||||
}));
|
||||
|
||||
const acquired = acquireLogsFollowLock(dir, "stale-thread");
|
||||
expect(acquired).toBe(lockPath);
|
||||
expect(JSON.parse(fs.readFileSync(lockPath, "utf8")).pid).toBe(process.pid);
|
||||
releaseLogsFollowLock(acquired);
|
||||
expect(fs.existsSync(lockPath)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps manual follow behavior unchanged without CODEX_THREAD_ID", async () => {
|
||||
const { runDir, writeState, initWorkflowState, gitHead, readState } = await import("./workflow-state.js");
|
||||
const runId = "logs-follow-manual-007";
|
||||
const dir = runDir(repoRoot, runId);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const state = initWorkflowState({
|
||||
runId,
|
||||
repoRoot,
|
||||
baselineHead: gitHead(repoRoot),
|
||||
plan: { version: "1", title: "Test", planMarkdown: "Test", scope: ["src/"], acceptanceCriteria: ["Ok"], verificationCommands: [["echo"]] },
|
||||
executor: "claude",
|
||||
maxCycles: 3,
|
||||
});
|
||||
state.status = "executing";
|
||||
state.currentCycle = 1;
|
||||
state.cycles = [{ cycleIndex: 1, startedAt: new Date().toISOString(), executorLogFile: "cycle-1-exec.log" }];
|
||||
writeState(dir, state);
|
||||
fs.writeFileSync(path.join(dir, "cycle-1-exec.log"), "manual viewer\n");
|
||||
|
||||
const previousThread = process.env.CODEX_THREAD_ID;
|
||||
delete process.env.CODEX_THREAD_ID;
|
||||
const { logsWorkflow } = await import("./workflow-engine.js");
|
||||
const origWrite = process.stdout.write.bind(process.stdout);
|
||||
process.stdout.write = () => true;
|
||||
const first = logsWorkflow({ runId, follow: true, tail: 5 });
|
||||
const second = logsWorkflow({ runId, follow: true, tail: 5 });
|
||||
setTimeout(() => {
|
||||
const curState = readState(dir)!;
|
||||
curState.status = "awaiting_review";
|
||||
writeState(dir, curState);
|
||||
}, 100);
|
||||
try {
|
||||
await Promise.all([first, second]);
|
||||
expect(fs.readdirSync(dir).some((name) => name.startsWith(".logs-follow-"))).toBe(false);
|
||||
} finally {
|
||||
process.stdout.write = origWrite;
|
||||
if (previousThread === undefined) delete process.env.CODEX_THREAD_ID;
|
||||
else process.env.CODEX_THREAD_ID = previousThread;
|
||||
}
|
||||
});
|
||||
|
||||
it("follow mode switches to retry log when sidecar changes and emits new content", async () => {
|
||||
const { runDir, writeState, initWorkflowState, gitHead, writeProgressSidecar, readState } = await import("./workflow-state.js");
|
||||
const runId = "logs-rotation-003";
|
||||
|
||||
Reference in New Issue
Block a user