/** * @file GET /api/sessions/:id/transcript for a Codex session: the conversation * comes from the session's own rollout journal (`codex_rollouts`, root thread), * mapped to the Claude transcript shape — user/assistant text, reasoning * summaries as thinking, function and custom tool calls with their outputs, * sub-agent messages as system — with the harness's developer messages and * injected context (AGENTS.md, environment_context) left out. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("fs"); const os = require("os"); const path = require("path"); const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-codex-transcript-")); process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db"); process.env.CLAUDE_HOME = path.join(TMP, "claude"); process.env.CODEX_HOME = path.join(TMP, "codex"); const { createApp, startServer } = require("../index"); const { db } = require("../db"); const ID = "01a10b66-ced6-7f30-af72-18e8a77e11f9"; let server; let BASE; const item = (ts, payload) => ({ timestamp: ts, type: "response_item", payload }); const text = (t, kind = "input_text") => ({ type: kind, text: t }); before(async () => { const file = path.join(TMP, "codex", "sessions", "2026", "10", "05", `rollout-x-${ID}.jsonl`); fs.mkdirSync(path.dirname(file), { recursive: true }); const lines = [ { timestamp: "2026-10-05T09:30:49Z", type: "session_meta", payload: { id: ID, cwd: "/w" } }, item("2026-10-05T09:30:52Z", { type: "message", role: "developer", content: [text("…")], }), item("2026-10-05T09:30:52Z", { type: "message", role: "user", content: [ text("# AGENTS.md instructions for /w\n…"), text("\n /w"), ], }), item("2026-10-05T09:30:53Z", { type: "message", role: "user", content: [text("Hi")] }), item("2026-10-05T09:30:54Z", { type: "reasoning", summary: [{ type: "summary_text", text: "Greet back" }], }), item("2026-10-05T09:30:54Z", { type: "reasoning", summary: [], encrypted_content: "gAAA" }), item("2026-10-05T09:30:54Z", { type: "function_call", name: "exec_command", arguments: '{"cmd":"ls"}', call_id: "call_1", }), item("2026-10-05T09:30:55Z", { type: "function_call_output", call_id: "call_1", output: [text("Script completed\n"), text("a.txt\n")], }), item("2026-10-05T09:30:55Z", { type: "custom_tool_call", name: "apply_patch", input: "*** Begin Patch", call_id: "call_2", }), item("2026-10-05T09:30:55Z", { type: "custom_tool_call_output", call_id: "call_2", output: "Done", }), item("2026-10-05T09:30:55Z", { type: "message", role: "assistant", content: [text("Hi! What can I help you with?", "output_text")], }), item("2026-10-05T09:30:56Z", { type: "agent_message", author: "/root/task1", content: [text("Message Type: FINAL_ANSWER\nPayload: ok")], }), ]; fs.writeFileSync(file, lines.map((l) => JSON.stringify(l)).join("\n") + "\n"); db.prepare("INSERT INTO sessions (id, tool, status) VALUES (?, 'codex', 'completed')").run(ID); db.prepare("INSERT INTO codex_rollouts (path, session_id, thread_id) VALUES (?, ?, ?)").run( file, ID, ID ); server = await startServer(createApp(), 0); BASE = `http://127.0.0.1:${server.address().port}`; }); after(() => { server?.close(); try { db.close(); } catch {} fs.rmSync(TMP, { recursive: true, force: true }); }); const get = async (q = "") => (await fetch(`${BASE}/api/sessions/${ID}/transcript${q}`)).json(); describe("Codex session transcript", () => { it("maps the rollout to the transcript shape, without the harness's injections", async () => { const body = await get(); const shape = body.messages.map((m) => [ m.type, m.sender, m.content.map((c) => c.type).join("+"), ]); assert.deepEqual(shape, [ ["user", "user", "text"], ["assistant", "assistant", "thinking"], ["assistant", "assistant", "tool_use"], ["user", "tool", "tool_result"], ["assistant", "assistant", "tool_use"], ["user", "tool", "tool_result"], ["assistant", "assistant", "text"], ["user", "system", "text"], ]); const [hi, think, call, out, , patchOut, reply] = body.messages; assert.equal(hi.content[0].text, "Hi"); assert.equal(hi.timestamp, "2026-10-05T09:30:53Z"); assert.equal(think.content[0].text, "Greet back"); assert.deepEqual(call.content[0], { type: "tool_use", name: "exec_command", id: "call_1", input: { cmd: "ls" }, }); assert.deepEqual(out.content[0], { type: "tool_result", id: "call_1", output: "Script completed\na.txt\n", is_error: false, }); assert.equal(patchOut.content[0].output, "Done"); assert.equal(reply.content[0].text, "Hi! What can I help you with?"); assert.equal(body.total, 8); }); it("pages like a Claude transcript (limit, after, before)", async () => { const latest = await get("?limit=2"); assert.equal(latest.messages.length, 2); assert.equal(latest.has_more, true); assert.equal(latest.messages[1].content[0].type, "text"); const after = await get(`?after=${latest.first_line}`); assert.equal(after.messages.length, 1); const before = await get(`?before=${latest.first_line}&limit=50`); assert.equal(before.messages.length, 6); }); });