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/verify/verify.test.ts4 * Description: Native-verification tests — config parsing, errors-first truncation, repair-loop accounting, runner events (v2 §4).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { describe, expect, it } from "vitest";11import {12 countRepairFailures,13 parseVerifyConfig,14 truncateErrorsFirst,15 VerifyRunner,16} from "../../src/verify/index.js";17import type { DurableEvent, DurableEventInput } from "../../src/session/index.js";18import type { Command, ProcessResult, Workspace } from "../../src/workspace/index.js";1920function fakeSession(): {21 events(): readonly DurableEvent[];22 publishDurable(input: DurableEventInput): DurableEvent;23} {24 const events: DurableEvent[] = [];25 let seq = 0;26 return {27 events: () => events,28 publishDurable(input: DurableEventInput): DurableEvent {29 seq += 1;30 const event = {31 v: 1,32 id: `e${seq}`,33 sessionId: "s1",34 seq,35 ts: seq,36 type: input.type,37 payload: input.payload,38 } as DurableEvent;39 events.push(event);40 return event;41 },42 };43}4445function fakeWorkspace(results: Record<string, Partial<ProcessResult>>): Workspace {46 return {47 cwd: () => "/repo",48 readFile: () => Promise.reject(new Error("not used")),49 writeFile: () => Promise.resolve(),50 exec: (command: Command) =>51 Promise.resolve({52 exitCode: 0,53 stdout: "",54 stderr: "",55 durationMs: 1,56 truncated: false,57 ...(results[command.cmd] ?? {}),58 }),59 };60}6162describe("parseVerifyConfig", () => {63 it("parses checks, policy, and maxRepairLoops", () => {64 const config = parseVerifyConfig({65 typecheck: { cmd: "npx tsc --noEmit", timeout: 60 },66 lint: { cmd: "npx eslint --fix", timeout: 30, autofix: true },67 policy: "before-final-answer",68 maxRepairLoops: 2,69 });70 expect(config?.checks).toHaveLength(2);71 expect(config?.checks[0]?.timeoutMs).toBe(60_000);72 expect(config?.checks[1]?.autofix).toBe(true);73 expect(config?.policy).toBe("before-final-answer");74 expect(config?.maxRepairLoops).toBe(2);75 });7677 it("skips invalid entries and defaults the policy", () => {78 const config = parseVerifyConfig({ bad: { nope: true }, test: { cmd: "npm test" } });79 expect(config?.checks.map((check) => check.name)).toEqual(["test"]);80 expect(config?.policy).toBe("after-each-edit-batch");81 expect(config?.maxRepairLoops).toBe(3);82 });83});8485describe("truncateErrorsFirst", () => {86 it("keeps error-looking lines ahead of noise", () => {87 const noise = Array.from({ length: 300 }, (_, i) => `line ${i} of ordinary output padding`).join("\n");88 const output = `${noise}\nsrc/a.ts(42): error TS2345: nope`;89 const truncated = truncateErrorsFirst(output, 500);90 expect(truncated).toContain("error TS2345");91 expect(truncated).toContain("[... verify output truncated");92 expect(truncated.indexOf("error TS2345")).toBeLessThan(truncated.indexOf("line 0"));93 });9495 it("returns small output untouched", () => {96 expect(truncateErrorsFirst("all good", 100)).toBe("all good");97 });98});99100describe("countRepairFailures", () => {101 it("counts failing rounds since the last user message", () => {102 const session = fakeSession();103 session.publishDurable({ type: "user.message-created", payload: { text: "go", mentions: [] } });104 session.publishDurable({105 type: "verify.result",106 payload: { check: "t", command: "c", ok: false, exitCode: 1, output: "x", durationMs: 1 },107 });108 session.publishDurable({109 type: "verify.result",110 payload: { check: "t", command: "c", ok: true, exitCode: 0, output: "", durationMs: 1 },111 });112 expect(countRepairFailures(session.events())).toBe(1);113 session.publishDurable({ type: "user.message-created", payload: { text: "next", mentions: [] } });114 expect(countRepairFailures(session.events())).toBe(0);115 });116});117118describe("VerifyRunner", () => {119 it("publishes one verify.result per check with real exit codes", async () => {120 const session = fakeSession();121 const workspace = fakeWorkspace({122 "npm test": { exitCode: 1, stderr: "1 test failed\nFAIL src/a.test.ts" },123 });124 const runner = new VerifyRunner({125 workspace,126 session,127 config: {128 checks: [129 { name: "typecheck", cmd: "npx tsc --noEmit", timeoutMs: 1000, autofix: false },130 { name: "test", cmd: "npm test", timeoutMs: 1000, autofix: false },131 ],132 policy: "after-each-edit-batch",133 maxRepairLoops: 3,134 },135 });136 const outcome = await runner.runAll();137 expect(outcome.ok).toBe(false);138 const results = session.events().filter((event) => event.type === "verify.result");139 expect(results).toHaveLength(2);140 const failed = results.find((event) => event.type === "verify.result" && !event.payload.ok);141 expect(failed !== undefined && failed.type === "verify.result" ? failed.payload.output : "").toContain(142 "test failed",143 );144 });145146 it("withinRepairBudget flips false after maxRepairLoops failures", async () => {147 const session = fakeSession();148 session.publishDurable({ type: "user.message-created", payload: { text: "go", mentions: [] } });149 const workspace = fakeWorkspace({ bad: { exitCode: 1, stderr: "boom" } });150 const runner = new VerifyRunner({151 workspace,152 session,153 config: {154 checks: [{ name: "bad", cmd: "bad", timeoutMs: 1000, autofix: false }],155 policy: "after-each-edit-batch",156 maxRepairLoops: 2,157 },158 });159 expect(runner.withinRepairBudget()).toBe(true);160 await runner.runAll();161 expect(runner.withinRepairBudget()).toBe(true);162 await runner.runAll();163 expect(runner.withinRepairBudget()).toBe(false);164 });165});166