spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: tests/session/fork-sdiff.test.ts4 * Description: Fork + sdiff tests — prefix copy with lineage, checkpoint discovery, structured run diff (v2 §2).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { mkdtempSync } from "node:fs";11import { readFile } from "node:fs/promises";12import { tmpdir } from "node:os";13import { join } from "node:path";14import { describe, expect, it } from "vitest";15import {16 SessionLog,17 extractUserTurns,18 forkSession,19 listForkCheckpoints,20 readSessionMeta,21 renderSessionDiff,22 summarizeSessionRun,23} from "../../src/session/index.js";2425const HASH = "abcd1234abcd1234";2627async function seedSession(sessionsDir: string): Promise<SessionLog> {28 const log = await SessionLog.create({ projectHash: HASH, sessionsDir });29 log.append({ type: "user.message-created", payload: { text: "fix the auth bug", mentions: [] } });30 log.append({31 type: "model.request-started",32 payload: {33 requestId: "req_1",34 model: "claude-sonnet-4-5",35 purpose: "main",36 contextStats: { estimatedInputTokens: 10, sections: [] },37 },38 });39 log.append({40 type: "model.response-completed",41 payload: {42 requestId: "req_1",43 stopReason: "end_turn",44 usage: { inputTokens: 100, outputTokens: 20, cacheReadTokens: 0, cacheWriteTokens: 0 },45 durationMs: 5,46 },47 });48 log.append({49 type: "phase.approved",50 payload: { phase: "design", approvedBy: "auto-policy", artifactId: "a1" },51 });52 log.append({ type: "user.message-created", payload: { text: "now add tests", mentions: [] } });53 await log.flush();54 return log;55}5657describe("fork", () => {58 it("copies the prefix, rewrites sessionIds, and records lineage", async () => {59 const sessionsDir = mkdtempSync(join(tmpdir(), "khaelor-fork-"));60 const source = await seedSession(sessionsDir);61 const checkpoints = listForkCheckpoints(source.replayedEvents.length > 0 ? source.replayedEvents : []);62 // Freshly created log: read events back from disk for checkpoints.63 const reopened = await SessionLog.open({ projectHash: HASH, sessionId: source.sessionId, sessionsDir });64 const points = listForkCheckpoints(reopened.replayedEvents);65 expect(checkpoints).toHaveLength(0);66 expect(points.map((point) => point.kind)).toEqual(["user-turn", "design-approved", "user-turn"]);6768 const fork = await forkSession({69 sessionsDir,70 projectHash: HASH,71 sourceSessionId: source.sessionId,72 uptoSeq: 4,73 });74 expect(fork.copiedEvents).toBe(4);75 expect(fork.forkPoint).toBe(4);7677 const forked = await SessionLog.open({ projectHash: HASH, sessionId: fork.sessionId, sessionsDir });78 expect(forked.replayedEvents).toHaveLength(4);79 expect(forked.replayedEvents.every((event) => event.sessionId === fork.sessionId)).toBe(true);8081 const meta = await readSessionMeta(sessionsDir, HASH, fork.sessionId);82 expect(meta?.parent).toBe(source.sessionId);83 expect(meta?.forkPoint).toBe(4);8485 const raw = await readFile(fork.filePath, "utf8");86 expect(raw.trim().split("\n")).toHaveLength(4);87 });8889 it("extracts user turns for /replay", async () => {90 const sessionsDir = mkdtempSync(join(tmpdir(), "khaelor-fork-"));91 const source = await seedSession(sessionsDir);92 const reopened = await SessionLog.open({ projectHash: HASH, sessionId: source.sessionId, sessionsDir });93 expect(extractUserTurns(reopened.replayedEvents)).toEqual(["fix the auth bug", "now add tests"]);94 });95});9697describe("sdiff", () => {98 it("summarizes runs from real events and renders the comparison", async () => {99 const sessionsDir = mkdtempSync(join(tmpdir(), "khaelor-sdiff-"));100 const source = await seedSession(sessionsDir);101 const reopened = await SessionLog.open({ projectHash: HASH, sessionId: source.sessionId, sessionsDir });102 const summary = summarizeSessionRun(source.sessionId, reopened.replayedEvents);103 expect(summary.agentTurns).toBe(1);104 expect(summary.inputTokens).toBe(100);105 expect(summary.models).toEqual(["claude-sonnet-4-5"]);106 expect(summary.outcome).toBe("open");107108 const lines = renderSessionDiff(summary, { ...summary, sessionId: "other", outputTokens: 999 });109 expect(lines.join("\n")).toContain("agent turns");110 expect(lines.join("\n")).toContain("tokens (in/out)");111 });112});113