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/context/engine.test.ts4 * Description: ContextEngine tests — byte-stability, volatile isolation, budget observation, compress integration.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { describe, expect, it } from "vitest";11import type { SystemTier } from "../../src/anthropic/index.js";12import {13 ContextBudget,14 KhaelorContextEngine,15 parseCheckpoint,16} from "../../src/context/index.js";17import { isPairingSafeCut } from "../../src/session/index.js";18import { FAKE_CHECKPOINT_TEXT, FakeModelClient, sampleSession } from "./fixtures.js";1920const SYSTEM_TIERS: SystemTier[] = [21 { name: "identity", text: "You are KHAELOR." },22 { name: "tools", text: "Available tools: read, bash." },23 { name: "project-instructions", text: "# Project instructions\n\nUse tabs." },24];2526function makeEngine(client = new FakeModelClient(FAKE_CHECKPOINT_TEXT)): KhaelorContextEngine {27 return new KhaelorContextEngine({28 model: "main-model",29 auxModel: "aux-model",30 maxOutputTokens: 4_000,31 systemTiers: SYSTEM_TIERS,32 tools: [{ name: "read", description: "Read a file", inputSchema: { type: "object" } }],33 modelClient: client,34 budget: new ContextBudget({35 model: "main-model",36 reservedOutputTokens: 4_000,37 contextWindow: { defaultWindow: 100_000 },38 compactionBufferTokens: 6_000,39 }),40 compaction: { protectTailTokens: 1 },41 });42}4344describe("selectContext byte-stability (ADR-7)", () => {45 it("same session state → byte-identical system tiers and message history across calls", async () => {46 const engine = makeEngine();47 const state = { events: sampleSession().events };48 const first = await engine.selectContext(state);49 const second = await engine.selectContext(state);50 expect(JSON.stringify(second.request.system)).toBe(JSON.stringify(first.request.system));51 expect(JSON.stringify(second.request.messages)).toBe(JSON.stringify(first.request.messages));52 expect(first.request.model).toBe("main-model");53 expect(first.request.maxOutputTokens).toBe(4_000);54 });5556 it("injects volatile context ONLY into the API copy of the current message", async () => {57 const engine = makeEngine();58 const events = sampleSession().events;59 const stable = await engine.selectContext({ events });60 const withVolatile = await engine.selectContext({61 events,62 volatile: [{ name: "git", text: "branch main, 2 dirty files" }],63 });6465 // History prefix is byte-identical; only the final (current) message differs.66 const count = stable.request.messages.length;67 expect(withVolatile.request.messages).toHaveLength(count);68 expect(JSON.stringify(withVolatile.request.messages.slice(0, -1))).toBe(69 JSON.stringify(stable.request.messages.slice(0, -1)),70 );71 const last = withVolatile.request.messages[count - 1];72 const stableLast = stable.request.messages[count - 1];73 expect(last?.content.length).toBe((stableLast?.content.length ?? 0) + 1);74 expect(JSON.stringify(last)).toContain("branch main, 2 dirty files");7576 // Volatile never mutates recorded history: rebuilding without it is byte-identical.77 const after = await engine.selectContext({ events });78 expect(JSON.stringify(after.request.messages)).toBe(JSON.stringify(stable.request.messages));79 expect(JSON.stringify(after.request.system)).toBe(JSON.stringify(stable.request.system));80 // The stable build's own last message was not touched by the volatile build.81 expect(JSON.stringify(stable.request.messages[count - 1])).not.toContain("branch main");82 });8384 it("reports per-section token estimates for /context", async () => {85 const engine = makeEngine();86 const built = await engine.selectContext({87 events: sampleSession().events,88 volatile: [{ name: "git", text: "clean" }],89 });90 const names = built.stats.sections.map((s) => s.name);91 expect(names).toEqual([92 "system:identity",93 "system:tools",94 "system:project-instructions",95 "tools",96 "history",97 "volatile",98 ]);99 expect(built.stats.estimatedInputTokens).toBe(100 built.stats.sections.reduce((sum, s) => sum + s.estimatedTokens, 0),101 );102 expect(built.stats.estimatedInputTokens).toBeGreaterThan(0);103 });104});105106describe("onTurnComplete and budget triggers", () => {107 it("records real usage and flips the compaction trigger when the threshold is crossed", () => {108 const engine = makeEngine();109 engine.onTurnComplete({ inputTokens: 10_000, outputTokens: 500, cacheReadTokens: 0, cacheWriteTokens: 0 });110 expect(engine.budget.shouldCompact()).toBe(false);111 engine.onTurnComplete({ inputTokens: 60_000, outputTokens: 1_000, cacheReadTokens: 30_000, cacheWriteTokens: 0 });112 expect(engine.budget.lastTotalTokens).toBe(91_000);113 expect(engine.budget.shouldCompact()).toBe(true); // usable window = 90_000114 });115});116117describe("pruneToolResults", () => {118 it("returns a decision shaped like the ContextPruned payload", () => {119 const engine = makeEngine();120 const big = "y".repeat(10_000);121 const session = sampleSession({ bigToolOutput: big });122 const decision = engine.pruneToolResults({ events: session.events });123 // Default protection (~40K tokens) covers this small session entirely.124 expect(decision.placeholder).toBeTypeOf("string");125 expect(Array.isArray(decision.toolUseIds)).toBe(true);126 expect(decision.tokensReclaimedEstimate).toBeGreaterThanOrEqual(0);127 });128});129130describe("compress", () => {131 it("produces a pairing-safe checkpoint via the aux model with real token accounting", async () => {132 const client = new FakeModelClient(FAKE_CHECKPOINT_TEXT);133 const engine = makeEngine(client);134 engine.onTurnComplete({ inputTokens: 88_000, outputTokens: 3_000, cacheReadTokens: 0, cacheWriteTokens: 0 });135136 const events = sampleSession().events;137 const result = await engine.compress({ events });138139 expect(isPairingSafeCut(events, result.cut)).toBe(true);140 expect(result.summaryModel).toBe("aux-model");141 expect(result.tokensBefore).toBe(91_000); // from real usage, not estimated142 expect(result.trigger).toBe("proactive-token-budget");143 expect(parseCheckpoint(result.checkpointYaml).ok).toBe(true);144 expect(client.requests[0]?.model).toBe("aux-model");145 });146147 it("reports a reactive-overflow trigger when a context-overflow failure is recorded", async () => {148 const engine = makeEngine();149 const session = sampleSession();150 session.add({151 type: "model.request-failed",152 payload: { requestId: "req_9", kind: "context-overflow", message: "too long", retriesExhausted: false },153 });154 const result = await engine.compress({ events: session.events });155 expect(result.trigger).toBe("reactive-overflow");156 });157158 it("throws when the conversation is too small to cut safely", async () => {159 const engine = makeEngine();160 await expect(engine.compress({ events: [] })).rejects.toThrow(/no pairing-safe/);161 });162});163