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/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