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:
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user