import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { Usage } from "../src/client.js";
import type { LoopEvent } from "../src/loop.js";
import { SessionStats } from "../src/telemetry/stats.js";
import {
openTranscriptFile,
parseTranscript,
recordFromLoopEvent,
writeRecord,
} from "../src/transcript/log.js";
describe("acp --transcript", () => {
let tmpDir: string;
beforeAll(() => {
tmpDir = mkdtempSync(join(tmpdir(), "reasonix-acp-transcript-"));
});
afterAll(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
it("writes _meta with source 'reasonix acp' and records the ACP session sequence", async () => {
const path = join(tmpDir, "session.jsonl");
const stream = openTranscriptFile(path, {
version: 1,
source: "reasonix acp",
model: "deepseek-chat",
startedAt: "2026-05-13T00:00:00Z",
});
const ctx = { model: "deepseek-chat", prefixHash: "acp-prefix-hash" };
const stats = new SessionStats();
const usage = new Usage(1200, 80, 1280, 1100, 100);
const turnStats = stats.record(1, "deepseek-chat", usage);
const events: LoopEvent[] = [
{ turn: 1, role: "assistant_delta", content: "Writing" },
{ turn: 1, role: "assistant_delta", content: " the file." },
{
turn: 1,
role: "tool",
content: "wrote 9 chars to /tmp/x",
toolName: "write_file",
toolArgs: '{"path":"/tmp/x","content":"ACP WORKS"}',
},
{ turn: 1, role: "assistant_final", content: "Done.", stats: turnStats },
];
for (const ev of events) {
writeRecord(stream, recordFromLoopEvent(ev, ctx));
}
await new Promise<void>((resolve) => stream.end(resolve));
const { meta, records } = parseTranscript(readFileSync(path, "utf8"));
expect(meta).not.toBeNull();
expect(meta?.source).toBe("reasonix acp");
expect(meta?.version).toBe(1);
expect(meta?.model).toBe("deepseek-chat");
expect(records).toHaveLength(4);
expect(records.map((r) => r.role)).toEqual([
"assistant_delta",
"assistant_delta",
"tool",
"assistant_final",
]);
const toolRec = records.find((r) => r.role === "tool");
expect(toolRec?.tool).toBe("write_file");
expect(toolRec?.args).toBe('{"path":"/tmp/x","content":"ACP WORKS"}');
const finalRec = records.find((r) => r.role === "assistant_final");
expect(finalRec?.usage?.total_tokens).toBe(1280);
expect(finalRec?.usage?.prompt_cache_hit_tokens).toBe(1100);
expect(finalRec?.cost).toBeGreaterThan(0);
expect(finalRec?.model).toBe("deepseek-chat");
expect(finalRec?.prefixHash).toBe("acp-prefix-hash");
});
it("appends records across multiple session/prompt turns in order", async () => {
const path = join(tmpDir, "multi-turn.jsonl");
const stream = openTranscriptFile(path, {
version: 1,
source: "reasonix acp",
startedAt: "2026-05-13T00:00:00Z",
});
const ctx = { model: "deepseek-chat", prefixHash: "h" };
const turn1: LoopEvent = { turn: 1, role: "assistant_final", content: "first" };
const turn2: LoopEvent = { turn: 2, role: "assistant_final", content: "second" };
writeRecord(stream, recordFromLoopEvent(turn1, ctx));
writeRecord(stream, recordFromLoopEvent(turn2, ctx));
await new Promise<void>((resolve) => stream.end(resolve));
const { records } = parseTranscript(readFileSync(path, "utf8"));
expect(records.map((r) => r.turn)).toEqual([1, 2]);
expect(records.map((r) => r.content)).toEqual(["first", "second"]);
});
});
describe("acp-driver.ndjson fixture", () => {
it("is valid NDJSON and drives the verified happy path (initialize → session/new → session/prompt)", () => {
const here = dirname(fileURLToPath(import.meta.url));
const raw = readFileSync(join(here, "fixtures", "acp-driver.ndjson"), "utf8");
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
const requests = lines.map((l) => JSON.parse(l));
expect(requests).toHaveLength(3);
expect(requests.map((r) => r.method)).toEqual(["initialize", "session/new", "session/prompt"]);
for (const r of requests) {
expect(r.jsonrpc).toBe("2.0");
expect(typeof r.id).toBe("number");
}
expect(requests[0].params.protocolVersion).toBe(1);
expect(requests[2].params.prompt[0].type).toBe("text");
expect(requests[2].params.sessionId).toMatch(/^<.+>$/);
});
});