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