test(KB-315): enhance cron-runner and worktree-pool test coverage

- Add error handling tests for cron-runner job execution
- Add stop idempotency and unknown step type tests to cron-runner
- Add startPoint parameter tests for worktree-pool
- Add worktree-pool error handling and edge case tests
This commit is contained in:
gsxdsm
2026-03-31 11:45:01 -07:00
parent ade5ac6523
commit 89bd17f06b
2 changed files with 102 additions and 1 deletions

View File

@@ -1,9 +1,31 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { CronRunner } from "./cron-runner.js";
import type { TaskStore, AutomationStore, ScheduledTask, AutomationRunResult, AutomationStep, Settings } from "@kb/core";
import { DEFAULT_SETTINGS } from "@kb/core";
import { randomUUID } from "node:crypto";
// Default settings inline to avoid @kb/core build dependency during tests
const DEFAULT_SETTINGS: Settings = {
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 30000,
autoResolveConflicts: true,
requirePlanApproval: false,
recycleWorktrees: false,
worktreeNaming: "random",
globalPause: false,
enginePaused: false,
ntfyEnabled: false,
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
planningProvider: "anthropic",
planningModelId: "claude-sonnet-4-5",
validatorProvider: "openai",
validatorModelId: "gpt-4o",
autoUpdatePrStatus: true,
autoCreatePr: false,
taskStuckTimeoutMs: undefined,
};
function createMockSchedule(overrides: Partial<ScheduledTask> = {}): ScheduledTask {
return {
id: "test-schedule-id",
@@ -74,6 +96,15 @@ describe("CronRunner", () => {
runner.start(); // should not double-start
runner.stop();
});
it("is safe to stop when not started", () => {
const store = createMockStore();
const automationStore = createMockAutomationStore();
runner = new CronRunner(store, automationStore);
// Should not throw
expect(() => runner.stop()).not.toThrow();
});
});
describe("tick", () => {
@@ -108,6 +139,21 @@ describe("CronRunner", () => {
expect(automationStore.recordRun).not.toHaveBeenCalled();
});
it("handles errors in tick gracefully", async () => {
const store = createMockStore();
const automationStore = createMockAutomationStore([createMockSchedule()]);
// Make getDueSchedules throw an error
(automationStore.getDueSchedules as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error("Database error")
);
runner = new CronRunner(store, automationStore);
// Should not throw
await expect(runner.tick()).resolves.toBeUndefined();
});
it("executes due schedules", async () => {
const store = createMockStore();
const schedule = createMockSchedule({ command: "echo test-output" });
@@ -552,6 +598,25 @@ describe("CronRunner", () => {
expect(result.stepResults![0].error).toContain("no command specified");
});
it("handles unknown step type gracefully", async () => {
const store = createMockStore();
const schedule = createMockSchedule({
command: "",
steps: [
makeStep({ name: "Unknown step", type: "unknown-type" as any }),
],
});
const automationStore = createMockAutomationStore([schedule]);
runner = new CronRunner(store, automationStore);
const result = await runner.executeSchedule(schedule);
expect(result.success).toBe(false);
expect(result.stepResults).toHaveLength(1);
expect(result.stepResults![0].success).toBe(false);
expect(result.stepResults![0].error).toContain("Unknown step type");
});
it("aggregates output from all steps with headers", async () => {
const store = createMockStore();
const schedule = createMockSchedule({

View File

@@ -149,6 +149,16 @@ describe("WorktreePool", () => {
}
});
it("creates branch from custom startPoint when provided", () => {
pool.prepareForTask("/tmp/wt", "kb/kb-042", "kb/kb-041");
const checkoutCall = mockedExecSync.mock.calls.find(
(c) => typeof c[0] === "string" && (c[0] as string).includes("checkout -B"),
);
expect(checkoutCall).toBeDefined();
expect(checkoutCall![0]).toBe('git checkout -B "kb/kb-042" kb/kb-041');
});
it("tolerates git checkout -- . failure (already clean)", () => {
mockedExecSync.mockImplementation((cmd: any) => {
if (cmd === "git checkout -- .") throw new Error("nothing to checkout");
@@ -297,6 +307,16 @@ describe("scanIdleWorktrees", () => {
expect(idle).toContain("/root/.worktrees/wt-1");
expect(idle).toContain("/root/.worktrees/wt-2");
});
it("returns empty array when readdirSync throws", async () => {
mockedReaddirSync.mockImplementation(() => {
throw new Error("Permission denied");
});
const store = createMockStore([]);
const idle = await scanIdleWorktrees("/root", store);
expect(idle).toEqual([]);
});
});
// ── cleanupOrphanedWorktrees tests ────────────────────────────────────
@@ -377,4 +397,20 @@ describe("cleanupOrphanedWorktrees", () => {
expect(cleaned).toBe(0);
expect(mockedExecSync).not.toHaveBeenCalled();
});
it("returns 0 when all worktrees are assigned to active tasks", async () => {
mockedReaddirSync.mockReturnValue([
makeDirEntry("active-1"),
makeDirEntry("active-2"),
] as any);
const store = createMockStore([
makeTask("KB-001", "in-progress", "/root/.worktrees/active-1"),
makeTask("KB-002", "in-review", "/root/.worktrees/active-2"),
]);
const cleaned = await cleanupOrphanedWorktrees("/root", store);
expect(cleaned).toBe(0);
expect(mockedExecSync).not.toHaveBeenCalled();
});
});