import { describe, expect, it, spyOn } from "bun:test"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { getCodexSessionsStreaming, parseCodexSessionFile } from "./codex"; describe("codex ingestion", () => { it("parses codex jsonl sessions and normalizes response items", async () => { const dir = await mkdtemp(join(tmpdir(), "scribe-codex-test-")); const sessionFile = join(dir, "session.jsonl"); try { const lines = [ JSON.stringify({ type: "session_meta", payload: { id: "thread-123", cwd: "/Users/satchmo/code/scribe", originator: "codex-cli", cli_version: "0.1.0", }, }), JSON.stringify({ timestamp: "2026-02-09T12:00:00.000Z", type: "response_item", payload: { type: "message", id: "msg-user", role: "user", content: [{ type: "input_text", text: "Run tests for parser" }], }, }), JSON.stringify({ timestamp: "2026-02-09T12:01:00.000Z", type: "response_item", payload: { type: "function_call", id: "msg-tool-call", name: "list_files", arguments: { path: "." }, }, }), JSON.stringify({ timestamp: "2026-02-09T12:02:00.000Z", type: "response_item", payload: { type: "reasoning", id: "msg-reasoning", summary: [{ text: "Thinking through fixture structure" }], }, }), JSON.stringify({ timestamp: "2026-02-09T12:03:00.000Z", type: "response_item", payload: { type: "function_call_output", id: "msg-tool-result", call_id: "call-1", output: { ok: true, files: ["a.ts", "b.ts"] }, }, }), "not-json", ]; await writeFile(sessionFile, `${lines.join("\n")}\n`, "utf8"); const session = await parseCodexSessionFile(sessionFile); expect(session).not.toBeNull(); expect(session?.providerId).toBe("codex"); expect(session?.externalThreadId).toBe("thread-123"); expect(session?.projectPath).toBe("/Users/satchmo/code/scribe"); expect(session?.messages).toHaveLength(4); expect(session?.messages[0]?.role).toBe("user"); expect(session?.messages[0]?.text).toContain("Run tests for parser"); expect(session?.messages[1]?.role).toBe("tool"); expect(session?.messages[1]?.toolName).toBe("list_files"); expect(session?.messages[1]?.text).toContain("list_files"); expect(session?.messages[2]?.role).toBe("assistant"); expect(session?.messages[2]?.text).toContain("fixture structure"); expect(session?.messages[3]?.role).toBe("tool"); expect(session?.messages[3]?.text).toContain('"ok":true'); } finally { await rm(dir, { recursive: true, force: true }); } }); it("streams sessions and applies per-message since filtering", async () => { const dir = await mkdtemp(join(tmpdir(), "scribe-codex-stream-test-")); const nested = join(dir, "2026", "02", "09"); const sessionFile = join(nested, "session.jsonl"); try { await mkdir(nested, { recursive: true }); await writeFile( sessionFile, [ JSON.stringify({ type: "session_meta", payload: { id: "thread-filter", }, }), JSON.stringify({ timestamp: "2026-02-09T12:00:00.000Z", type: "response_item", payload: { type: "message", id: "m-1", role: "user", content: [{ type: "text", text: "before window" }], }, }), JSON.stringify({ timestamp: "2026-02-09T12:05:00.000Z", type: "response_item", payload: { type: "message", id: "m-2", role: "assistant", content: [{ type: "text", text: "inside window" }], }, }), ].join("\n"), "utf8", ); const since = new Date("2026-02-09T12:01:00.000Z"); const sessions = []; for await (const session of getCodexSessionsStreaming([], since, { dataRoots: [dir], })) { sessions.push(session); } expect(sessions).toHaveLength(1); expect(sessions[0]?.messages).toHaveLength(1); expect(sessions[0]?.messages[0]?.text).toContain("inside window"); } finally { await rm(dir, { recursive: true, force: true }); } }); it("uses workspace roots as cwd filters instead of session storage roots", async () => { const storageDir = await mkdtemp(join(tmpdir(), "scribe-codex-storage-")); const workspaceDir = await mkdtemp( join(tmpdir(), "scribe-codex-workspace-"), ); const nested = join(storageDir, "2026", "02", "09"); try { await mkdir(nested, { recursive: true }); await writeFile( join(nested, "inside.jsonl"), [ JSON.stringify({ type: "session_meta", payload: { id: "thread-inside", cwd: join(workspaceDir, "app"), }, }), JSON.stringify({ timestamp: "2026-02-09T12:00:00.000Z", type: "response_item", payload: { type: "message", id: "inside-message", role: "user", content: [{ type: "text", text: "inside workspace" }], }, }), ].join("\n"), "utf8", ); await writeFile( join(nested, "outside.jsonl"), [ JSON.stringify({ type: "session_meta", payload: { id: "thread-outside", cwd: "/tmp/other-project", }, }), JSON.stringify({ timestamp: "2026-02-09T12:00:00.000Z", type: "response_item", payload: { type: "message", id: "outside-message", role: "user", content: [{ type: "text", text: "outside workspace" }], }, }), ].join("\n"), "utf8", ); const sessions = []; for await (const session of getCodexSessionsStreaming( [workspaceDir], undefined, { dataRoots: [storageDir] }, )) { sessions.push(session); } expect(sessions).toHaveLength(1); expect(sessions[0]?.externalThreadId).toBe("thread-inside"); expect(sessions[0]?.messages[0]?.text).toBe("inside workspace"); } finally { await Promise.all([ rm(storageDir, { recursive: true, force: true }), rm(workspaceDir, { recursive: true, force: true }), ]); } }); it("counts and reports skipped malformed lines instead of dropping them silently", async () => { const dir = await mkdtemp(join(tmpdir(), "scribe-codex-skip-test-")); const sessionFile = join(dir, "session.jsonl"); const warn = spyOn(console, "warn").mockImplementation(() => {}); try { const lines = [ JSON.stringify({ type: "session_meta", payload: { id: "thread-skip" }, }), "{ broken json", JSON.stringify({ timestamp: "2026-02-09T12:00:00.000Z", type: "response_item", payload: { type: "message", id: "m-ok", role: "user", content: [{ type: "text", text: "survives the bad lines" }], }, }), "also not json", ]; await writeFile(sessionFile, `${lines.join("\n")}\n`, "utf8"); const session = await parseCodexSessionFile(sessionFile); // the one valid message survives despite two malformed lines expect(session?.messages).toHaveLength(1); expect(session?.messages[0]?.text).toContain("survives the bad lines"); // and the skips are now SURFACED, not silently lost const warned = warn.mock.calls.map((c) => String(c[0])).join("\n"); expect(warned).toContain("[codex] skipped 2 malformed line(s)"); } finally { warn.mockRestore(); await rm(dir, { recursive: true, force: true }); } }); });