SPB Git

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%
4.3 KB · 113 lines typescript
Raw Blame History
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