Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions __tests__/lib/codex-sessions.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @vitest-environment node
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync, readdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { tmpdir } from "node:os";

const line = (obj: Record<string, unknown>): string => JSON.stringify(obj);
Expand Down Expand Up @@ -195,6 +195,7 @@ describe("lib/codex-sessions: findCodexTranscript", () => {
let originalHome: string | undefined;
let fakeHome: string;
let findCodexTranscript: typeof import("@/lib/codex-sessions").findCodexTranscript;
let _getCacheFilePath: typeof import("@/lib/codex-sessions")._getCacheFilePath;

beforeEach(async () => {
originalHome = process.env.HOME;
Expand All @@ -206,7 +207,7 @@ describe("lib/codex-sessions: findCodexTranscript", () => {
const actual = await vi.importActual<typeof import("node:os")>("node:os");
return { ...actual, homedir: () => fakeHome };
});
({ findCodexTranscript } = await import("@/lib/codex-sessions"));
({ findCodexTranscript, _getCacheFilePath } = await import("@/lib/codex-sessions"));
});

afterEach(() => {
Expand Down Expand Up @@ -246,6 +247,28 @@ describe("lib/codex-sessions: findCodexTranscript", () => {
const result = findCodexTranscript(sid);
expect(result).toBe(file);
});

it("writes the session cache atomically and leaves no .tmp files", () => {
const sid = "019dd672-cccc-7a30-8671-deadbeefcafe";
const today = new Date();
const y = String(today.getUTCFullYear());
const m = String(today.getUTCMonth() + 1).padStart(2, "0");
const d = String(today.getUTCDate()).padStart(2, "0");
const dir = join(fakeHome, ".codex", "sessions", y, m, d);
mkdirSync(dir, { recursive: true });
const file = join(dir, `rollout-${sid}.jsonl`);
writeFileSync(file, "{}\n");

expect(findCodexTranscript(sid)).toBe(file);

const cachePath = _getCacheFilePath();
expect(existsSync(cachePath)).toBe(true);
const parsed = JSON.parse(readFileSync(cachePath, "utf-8")) as Record<string, string>;
expect(parsed[sid]).toBe(file);

const leftovers = readdirSync(dirname(cachePath)).filter((name) => name.endsWith(".tmp"));
expect(leftovers).toEqual([]);
});
});

// Imports at the bottom for the test helpers above; primary imports happen
Expand Down
11 changes: 9 additions & 2 deletions lib/codex-sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* parser produces (`lib/log-entries.ts`) so the existing log viewer renders
* Codex sessions without any UI-side branching.
*/
import { readFileSync, readdirSync, existsSync, writeFileSync, mkdirSync, statSync } from "node:fs";
import { readFileSync, readdirSync, existsSync, writeFileSync, mkdirSync, statSync, renameSync, unlinkSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { homedir } from "node:os";
Expand Down Expand Up @@ -49,13 +49,20 @@ function readCache(): Record<string, string> {
}

function writeCacheEntry(sessionId: string, path: string): void {
const tmp = `${CACHE_PATH}.${process.pid}.tmp`;
try {
mkdirSync(dirname(CACHE_PATH), { recursive: true });
const cache = readCache();
cache[sessionId] = path;
writeFileSync(CACHE_PATH, JSON.stringify(cache), "utf-8");
writeFileSync(tmp, JSON.stringify(cache), "utf-8");
renameSync(tmp, CACHE_PATH);
} catch {
// Cache is best-effort
try {
unlinkSync(tmp);
} catch {
// ignore leftover cleanup failure
}
}
}

Expand Down