/** * KHAELOR * File: tests/session/fork-sdiff.test.ts * Description: Fork + sdiff tests — prefix copy with lineage, checkpoint discovery, structured run diff (v2 §2). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { mkdtempSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { SessionLog, extractUserTurns, forkSession, listForkCheckpoints, readSessionMeta, renderSessionDiff, summarizeSessionRun, } from "../../src/session/index.js"; const HASH = "abcd1234abcd1234"; async function seedSession(sessionsDir: string): Promise { const log = await SessionLog.create({ projectHash: HASH, sessionsDir }); log.append({ type: "user.message-created", payload: { text: "fix the auth bug", mentions: [] } }); log.append({ type: "model.request-started", payload: { requestId: "req_1", model: "claude-sonnet-4-5", purpose: "main", contextStats: { estimatedInputTokens: 10, sections: [] }, }, }); log.append({ type: "model.response-completed", payload: { requestId: "req_1", stopReason: "end_turn", usage: { inputTokens: 100, outputTokens: 20, cacheReadTokens: 0, cacheWriteTokens: 0 }, durationMs: 5, }, }); log.append({ type: "phase.approved", payload: { phase: "design", approvedBy: "auto-policy", artifactId: "a1" }, }); log.append({ type: "user.message-created", payload: { text: "now add tests", mentions: [] } }); await log.flush(); return log; } describe("fork", () => { it("copies the prefix, rewrites sessionIds, and records lineage", async () => { const sessionsDir = mkdtempSync(join(tmpdir(), "khaelor-fork-")); const source = await seedSession(sessionsDir); const checkpoints = listForkCheckpoints(source.replayedEvents.length > 0 ? source.replayedEvents : []); // Freshly created log: read events back from disk for checkpoints. const reopened = await SessionLog.open({ projectHash: HASH, sessionId: source.sessionId, sessionsDir }); const points = listForkCheckpoints(reopened.replayedEvents); expect(checkpoints).toHaveLength(0); expect(points.map((point) => point.kind)).toEqual(["user-turn", "design-approved", "user-turn"]); const fork = await forkSession({ sessionsDir, projectHash: HASH, sourceSessionId: source.sessionId, uptoSeq: 4, }); expect(fork.copiedEvents).toBe(4); expect(fork.forkPoint).toBe(4); const forked = await SessionLog.open({ projectHash: HASH, sessionId: fork.sessionId, sessionsDir }); expect(forked.replayedEvents).toHaveLength(4); expect(forked.replayedEvents.every((event) => event.sessionId === fork.sessionId)).toBe(true); const meta = await readSessionMeta(sessionsDir, HASH, fork.sessionId); expect(meta?.parent).toBe(source.sessionId); expect(meta?.forkPoint).toBe(4); const raw = await readFile(fork.filePath, "utf8"); expect(raw.trim().split("\n")).toHaveLength(4); }); it("extracts user turns for /replay", async () => { const sessionsDir = mkdtempSync(join(tmpdir(), "khaelor-fork-")); const source = await seedSession(sessionsDir); const reopened = await SessionLog.open({ projectHash: HASH, sessionId: source.sessionId, sessionsDir }); expect(extractUserTurns(reopened.replayedEvents)).toEqual(["fix the auth bug", "now add tests"]); }); }); describe("sdiff", () => { it("summarizes runs from real events and renders the comparison", async () => { const sessionsDir = mkdtempSync(join(tmpdir(), "khaelor-sdiff-")); const source = await seedSession(sessionsDir); const reopened = await SessionLog.open({ projectHash: HASH, sessionId: source.sessionId, sessionsDir }); const summary = summarizeSessionRun(source.sessionId, reopened.replayedEvents); expect(summary.agentTurns).toBe(1); expect(summary.inputTokens).toBe(100); expect(summary.models).toEqual(["claude-sonnet-4-5"]); expect(summary.outcome).toBe("open"); const lines = renderSessionDiff(summary, { ...summary, sessionId: "other", outputTokens: 999 }); expect(lines.join("\n")).toContain("agent turns"); expect(lines.join("\n")).toContain("tokens (in/out)"); }); });