/** * KHAELOR * File: tests/daemon/daemon.test.ts * Description: Daemon unit tests โ€” cron parsing, active hours, goal store, budget guard, approvals, scheduler decisions (v2 ยง7). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { ApprovalQueue, BudgetGuard, ChannelRouter, GoalStore, KhaelorDaemon, nextCronRun, parseActiveHours, parseCron, withinActiveHours, } from "../../src/daemon/index.js"; import { costFromUsage } from "../../src/daemon/runner.js"; describe("cron", () => { it("parses */30 and computes the next run", () => { const spec = parseCron("*/30 * * * *"); expect(spec).not.toBeNull(); const next = nextCronRun(spec!, new Date(2026, 7, 10, 12, 10)); expect(next?.getMinutes()).toBe(30); expect(next?.getHours()).toBe(12); }); it("parses '0 6 * * 1' as Monday 06:00", () => { const spec = parseCron("0 6 * * 1"); // 2026-08-10 is a Monday. const next = nextCronRun(spec!, new Date(2026, 7, 10, 7, 0)); expect(next?.getDay()).toBe(1); expect(next?.getHours()).toBe(6); expect(next?.getDate()).toBe(17); }); it("rejects malformed expressions", () => { expect(parseCron("nope")).toBeNull(); expect(parseCron("61 * * * *")).toBeNull(); expect(parseCron("* * * *")).toBeNull(); }); it("active hours window (incl. wrapping)", () => { const hours = parseActiveHours("07:00-23:00"); expect(withinActiveHours(hours, new Date(2026, 7, 10, 12, 0))).toBe(true); expect(withinActiveHours(hours, new Date(2026, 7, 10, 3, 0))).toBe(false); const night = parseActiveHours("22:00-06:00"); expect(withinActiveHours(night, new Date(2026, 7, 10, 23, 30))).toBe(true); expect(withinActiveHours(night, new Date(2026, 7, 10, 12, 0))).toBe(false); expect(withinActiveHours(null, new Date())).toBe(true); }); }); describe("GoalStore", () => { it("creates, lists, and event-sources goals", async () => { const store = new GoalStore(mkdtempSync(join(tmpdir(), "khaelor-daemon-"))); const goal = await store.create({ description: "keep deps fresh", type: "maintain", schedule: "0 6 * * 1", escalation: "draft-pr", budget: { maxUsdPerDay: 5 }, }); expect(goal.budget.maxUsdPerDay).toBe(5); expect(goal.status).toBe("active"); const listed = await store.list(); expect(listed).toHaveLength(1); const events = await store.events(goal.id); expect(events[0]?.type).toBe("goal.created"); await store.appendEvent(goal.id, { ts: Date.now(), type: "run.started", payload: { runId: "r1" } }); expect(await store.runsToday(goal.id)).toBe(1); await store.setStatus(goal.id, "paused"); expect((await store.get(goal.id))?.status).toBe("paused"); }); }); describe("BudgetGuard", () => { it("enforces daemon and per-goal daily ceilings from recorded real costs", async () => { const guard = new BudgetGuard(mkdtempSync(join(tmpdir(), "khaelor-budget-")), { maxUsdPerDay: 10, maxUsdPerRun: 3, hardStop: true, }); await guard.load(); expect(guard.canStart("g1", 5).ok).toBe(true); await guard.record("g1", 5); expect(guard.canStart("g1", 5).ok).toBe(false); // goal ceiling expect(guard.canStart("g2", 5).ok).toBe(true); await guard.record("g2", 5); expect(guard.canStart("g3", 5).ok).toBe(false); // daemon ceiling expect(guard.spentToday()).toBe(10); }); }); describe("ApprovalQueue", () => { it("persists requests and resolves them once", async () => { const dir = mkdtempSync(join(tmpdir(), "khaelor-approvals-")); const queue = new ApprovalQueue(dir); const entry = await queue.request({ runId: "r1", goalId: "g1", capability: "bash:git push", context: "Draft PR, diff +42 โˆ’13, verify โœ“", }); expect((await queue.list("pending"))).toHaveLength(1); const resolved = await queue.resolve(entry.id, "approved"); expect(resolved?.status).toBe("approved"); expect(await queue.resolve(entry.id, "denied")).toBeNull(); // A fresh instance reads the same file (persistence). const reloaded = new ApprovalQueue(dir); expect((await reloaded.list("approved"))).toHaveLength(1); }); }); describe("costFromUsage", () => { it("prices real usage only when pricing is configured", () => { const usage = { inputTokens: 1_000_000, outputTokens: 500_000, cacheReadTokens: 0, cacheWriteTokens: 0 }; expect(costFromUsage(usage, undefined)).toBe(0); expect( costFromUsage(usage, { inputPerMTok: 3, outputPerMTok: 15, cacheReadPerMTok: 0.3, cacheWritePerMTok: 3.75 }), ).toBeCloseTo(3 + 7.5); }); }); describe("KhaelorDaemon tick", () => { it("skips a goal whose check passes and runs one whose check fails", async () => { const daemonDir = mkdtempSync(join(tmpdir(), "khaelor-tick-")); const goals = new GoalStore(daemonDir); const passing = await goals.create({ description: "all green", type: "watch", schedule: "heartbeat", check: "true" }); const failing = await goals.create({ description: "needs work", type: "watch", schedule: "heartbeat", check: "false" }); const ran: string[] = []; const daemon = new KhaelorDaemon({ daemonDir, config: { budget: { maxUsdPerDay: 10, maxUsdPerRun: 3, hardStop: true }, heartbeatMinutes: 30, model: {}, channels: {} }, goals, budget: new BudgetGuard(daemonDir), approvals: new ApprovalQueue(daemonDir), channels: new ChannelRouter([]), runGoal: (goal, runId) => { ran.push(goal.id); return Promise.resolve({ outcome: "done" as const, detail: `run ${runId}`, costUsd: 0.5, sessionId: "s-child", verifyOk: true, branch: null, }); }, execCheck: (cmd) => Promise.resolve({ exitCode: cmd === "true" ? 0 : 1 }), log: () => undefined, }); await daemon.tick(new Date(2026, 7, 10, 12, 0)); expect(ran).toEqual([failing.id]); const skipEvents = await goals.events(passing.id); expect(skipEvents.some((event) => event.type === "run.skipped")).toBe(true); const runEvents = await goals.events(failing.id); expect(runEvents.some((event) => event.type === "run.started")).toBe(true); expect(runEvents.some((event) => event.type === "run.completed")).toBe(true); }); it("stops running when the goal's daily run budget is exhausted", async () => { const daemonDir = mkdtempSync(join(tmpdir(), "khaelor-tick2-")); const goals = new GoalStore(daemonDir); const goal = await goals.create({ description: "hungry goal", type: "watch", schedule: "heartbeat", check: "false", budget: { maxRunsPerDay: 1 }, }); let runs = 0; const daemon = new KhaelorDaemon({ daemonDir, config: { budget: { maxUsdPerDay: 100, maxUsdPerRun: 3, hardStop: true }, heartbeatMinutes: 0, model: {}, channels: {} }, goals, budget: new BudgetGuard(daemonDir), approvals: new ApprovalQueue(daemonDir), channels: new ChannelRouter([]), runGoal: () => { runs += 1; return Promise.resolve({ outcome: "done" as const, detail: "", costUsd: 0, sessionId: null, verifyOk: null, branch: null }); }, execCheck: () => Promise.resolve({ exitCode: 1 }), log: () => undefined, }); await daemon.tick(new Date(2026, 7, 10, 12, 0)); await daemon.tick(new Date(2026, 7, 10, 13, 0)); expect(runs).toBe(1); const events = await goals.events(goal.id); expect(events.some((event) => event.type === "run.skipped" && String(event.payload["reason"]).includes("maxRunsPerDay"))).toBe(true); }); });