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%
7.7 KB · 207 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: tests/daemon/daemon.test.ts4 * Description: Daemon unit tests — cron parsing, active hours, goal store, budget guard, approvals, scheduler decisions (v2 §7).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { mkdtempSync } from "node:fs";11import { tmpdir } from "node:os";12import { join } from "node:path";13import { describe, expect, it } from "vitest";14import {15  ApprovalQueue,16  BudgetGuard,17  ChannelRouter,18  GoalStore,19  KhaelorDaemon,20  nextCronRun,21  parseActiveHours,22  parseCron,23  withinActiveHours,24} from "../../src/daemon/index.js";25import { costFromUsage } from "../../src/daemon/runner.js";2627describe("cron", () => {28  it("parses */30 and computes the next run", () => {29    const spec = parseCron("*/30 * * * *");30    expect(spec).not.toBeNull();31    const next = nextCronRun(spec!, new Date(2026, 7, 10, 12, 10));32    expect(next?.getMinutes()).toBe(30);33    expect(next?.getHours()).toBe(12);34  });3536  it("parses '0 6 * * 1' as Monday 06:00", () => {37    const spec = parseCron("0 6 * * 1");38    // 2026-08-10 is a Monday.39    const next = nextCronRun(spec!, new Date(2026, 7, 10, 7, 0));40    expect(next?.getDay()).toBe(1);41    expect(next?.getHours()).toBe(6);42    expect(next?.getDate()).toBe(17);43  });4445  it("rejects malformed expressions", () => {46    expect(parseCron("nope")).toBeNull();47    expect(parseCron("61 * * * *")).toBeNull();48    expect(parseCron("* * * *")).toBeNull();49  });5051  it("active hours window (incl. wrapping)", () => {52    const hours = parseActiveHours("07:00-23:00");53    expect(withinActiveHours(hours, new Date(2026, 7, 10, 12, 0))).toBe(true);54    expect(withinActiveHours(hours, new Date(2026, 7, 10, 3, 0))).toBe(false);55    const night = parseActiveHours("22:00-06:00");56    expect(withinActiveHours(night, new Date(2026, 7, 10, 23, 30))).toBe(true);57    expect(withinActiveHours(night, new Date(2026, 7, 10, 12, 0))).toBe(false);58    expect(withinActiveHours(null, new Date())).toBe(true);59  });60});6162describe("GoalStore", () => {63  it("creates, lists, and event-sources goals", async () => {64    const store = new GoalStore(mkdtempSync(join(tmpdir(), "khaelor-daemon-")));65    const goal = await store.create({66      description: "keep deps fresh",67      type: "maintain",68      schedule: "0 6 * * 1",69      escalation: "draft-pr",70      budget: { maxUsdPerDay: 5 },71    });72    expect(goal.budget.maxUsdPerDay).toBe(5);73    expect(goal.status).toBe("active");74    const listed = await store.list();75    expect(listed).toHaveLength(1);76    const events = await store.events(goal.id);77    expect(events[0]?.type).toBe("goal.created");7879    await store.appendEvent(goal.id, { ts: Date.now(), type: "run.started", payload: { runId: "r1" } });80    expect(await store.runsToday(goal.id)).toBe(1);8182    await store.setStatus(goal.id, "paused");83    expect((await store.get(goal.id))?.status).toBe("paused");84  });85});8687describe("BudgetGuard", () => {88  it("enforces daemon and per-goal daily ceilings from recorded real costs", async () => {89    const guard = new BudgetGuard(mkdtempSync(join(tmpdir(), "khaelor-budget-")), {90      maxUsdPerDay: 10,91      maxUsdPerRun: 3,92      hardStop: true,93    });94    await guard.load();95    expect(guard.canStart("g1", 5).ok).toBe(true);96    await guard.record("g1", 5);97    expect(guard.canStart("g1", 5).ok).toBe(false); // goal ceiling98    expect(guard.canStart("g2", 5).ok).toBe(true);99    await guard.record("g2", 5);100    expect(guard.canStart("g3", 5).ok).toBe(false); // daemon ceiling101    expect(guard.spentToday()).toBe(10);102  });103});104105describe("ApprovalQueue", () => {106  it("persists requests and resolves them once", async () => {107    const dir = mkdtempSync(join(tmpdir(), "khaelor-approvals-"));108    const queue = new ApprovalQueue(dir);109    const entry = await queue.request({110      runId: "r1",111      goalId: "g1",112      capability: "bash:git push",113      context: "Draft PR, diff +42 −13, verify ✓",114    });115    expect((await queue.list("pending"))).toHaveLength(1);116    const resolved = await queue.resolve(entry.id, "approved");117    expect(resolved?.status).toBe("approved");118    expect(await queue.resolve(entry.id, "denied")).toBeNull();119    // A fresh instance reads the same file (persistence).120    const reloaded = new ApprovalQueue(dir);121    expect((await reloaded.list("approved"))).toHaveLength(1);122  });123});124125describe("costFromUsage", () => {126  it("prices real usage only when pricing is configured", () => {127    const usage = { inputTokens: 1_000_000, outputTokens: 500_000, cacheReadTokens: 0, cacheWriteTokens: 0 };128    expect(costFromUsage(usage, undefined)).toBe(0);129    expect(130      costFromUsage(usage, { inputPerMTok: 3, outputPerMTok: 15, cacheReadPerMTok: 0.3, cacheWritePerMTok: 3.75 }),131    ).toBeCloseTo(3 + 7.5);132  });133});134135describe("KhaelorDaemon tick", () => {136  it("skips a goal whose check passes and runs one whose check fails", async () => {137    const daemonDir = mkdtempSync(join(tmpdir(), "khaelor-tick-"));138    const goals = new GoalStore(daemonDir);139    const passing = await goals.create({ description: "all green", type: "watch", schedule: "heartbeat", check: "true" });140    const failing = await goals.create({ description: "needs work", type: "watch", schedule: "heartbeat", check: "false" });141142    const ran: string[] = [];143    const daemon = new KhaelorDaemon({144      daemonDir,145      config: { budget: { maxUsdPerDay: 10, maxUsdPerRun: 3, hardStop: true }, heartbeatMinutes: 30, model: {}, channels: {} },146      goals,147      budget: new BudgetGuard(daemonDir),148      approvals: new ApprovalQueue(daemonDir),149      channels: new ChannelRouter([]),150      runGoal: (goal, runId) => {151        ran.push(goal.id);152        return Promise.resolve({153          outcome: "done" as const,154          detail: `run ${runId}`,155          costUsd: 0.5,156          sessionId: "s-child",157          verifyOk: true,158          branch: null,159        });160      },161      execCheck: (cmd) => Promise.resolve({ exitCode: cmd === "true" ? 0 : 1 }),162      log: () => undefined,163    });164165    await daemon.tick(new Date(2026, 7, 10, 12, 0));166    expect(ran).toEqual([failing.id]);167168    const skipEvents = await goals.events(passing.id);169    expect(skipEvents.some((event) => event.type === "run.skipped")).toBe(true);170    const runEvents = await goals.events(failing.id);171    expect(runEvents.some((event) => event.type === "run.started")).toBe(true);172    expect(runEvents.some((event) => event.type === "run.completed")).toBe(true);173  });174175  it("stops running when the goal's daily run budget is exhausted", async () => {176    const daemonDir = mkdtempSync(join(tmpdir(), "khaelor-tick2-"));177    const goals = new GoalStore(daemonDir);178    const goal = await goals.create({179      description: "hungry goal",180      type: "watch",181      schedule: "heartbeat",182      check: "false",183      budget: { maxRunsPerDay: 1 },184    });185    let runs = 0;186    const daemon = new KhaelorDaemon({187      daemonDir,188      config: { budget: { maxUsdPerDay: 100, maxUsdPerRun: 3, hardStop: true }, heartbeatMinutes: 0, model: {}, channels: {} },189      goals,190      budget: new BudgetGuard(daemonDir),191      approvals: new ApprovalQueue(daemonDir),192      channels: new ChannelRouter([]),193      runGoal: () => {194        runs += 1;195        return Promise.resolve({ outcome: "done" as const, detail: "", costUsd: 0, sessionId: null, verifyOk: null, branch: null });196      },197      execCheck: () => Promise.resolve({ exitCode: 1 }),198      log: () => undefined,199    });200    await daemon.tick(new Date(2026, 7, 10, 12, 0));201    await daemon.tick(new Date(2026, 7, 10, 13, 0));202    expect(runs).toBe(1);203    const events = await goals.events(goal.id);204    expect(events.some((event) => event.type === "run.skipped" && String(event.payload["reason"]).includes("maxRunsPerDay"))).toBe(true);205  });206});207