import { describe, it, expect } from "vitest"; import { findNextPendingIndex, countByStatus, buildTaskPrompt, buildAutoCommitPrompt, normalizeToUnixPath, isTaskBlocked, getReadyTasks, computeWaves, type TaskLoopTask, } from "./taskLoop"; const makeTask = ( id: string, status: TaskLoopTask["status"] = "pending", dependsOn?: string[] ): TaskLoopTask => ({ id, title: `Task ${id}`, prompt: `Do the thing for ${id}`, priority: "medium", status, dependsOn, }); describe("findNextPendingIndex", () => { it("returns -1 for an empty list", () => { expect(findNextPendingIndex([])).toBe(-1); }); it("returns 0 when the first task is pending", () => { const tasks = [makeTask("1", "pending"), makeTask("2", "pending")]; expect(findNextPendingIndex(tasks)).toBe(0); }); it("skips completed and failed tasks to find the next pending", () => { const tasks = [ makeTask("1", "completed"), makeTask("2", "failed"), makeTask("3", "pending"), makeTask("4", "pending"), ]; expect(findNextPendingIndex(tasks)).toBe(2); }); it("returns -1 when all tasks are completed", () => { const tasks = [makeTask("1", "completed"), makeTask("2", "completed")]; expect(findNextPendingIndex(tasks)).toBe(-1); }); it("skips running tasks", () => { const tasks = [makeTask("1", "running"), makeTask("2", "pending")]; expect(findNextPendingIndex(tasks)).toBe(1); }); }); describe("countByStatus", () => { it("returns 0 for an empty list", () => { expect(countByStatus([], "pending")).toBe(0); }); it("counts only tasks with the specified status", () => { const tasks = [ makeTask("1", "pending"), makeTask("2", "completed"), makeTask("3", "pending"), makeTask("4", "failed"), ]; expect(countByStatus(tasks, "pending")).toBe(2); expect(countByStatus(tasks, "completed")).toBe(1); expect(countByStatus(tasks, "failed")).toBe(1); expect(countByStatus(tasks, "running")).toBe(0); }); it("counts all tasks when all have the same status", () => { const tasks = [makeTask("1", "completed"), makeTask("2", "completed")]; expect(countByStatus(tasks, "completed")).toBe(2); }); }); describe("buildTaskPrompt", () => { it("includes the task number and total", () => { const task = makeTask("1"); const result = buildTaskPrompt(task, 1, 5); expect(result).toContain("1/5"); }); it("includes the task title", () => { const task = makeTask("abc"); const result = buildTaskPrompt(task, 2, 3); expect(result).toContain("Task abc"); }); it("includes the task prompt", () => { const task = makeTask("x"); const result = buildTaskPrompt(task, 1, 1); expect(result).toContain("Do the thing for x"); }); it("labels the output as an automated task loop entry", () => { const task = makeTask("1"); const result = buildTaskPrompt(task, 1, 1); expect(result).toContain("Automated Task Loop"); }); }); describe("buildAutoCommitPrompt", () => { const task = makeTask("task-1"); it("includes the git add and commit commands", () => { const result = buildAutoCommitPrompt(task, "feat", false, "2026-03-07T12:00:00"); expect(result).toContain("git add -A"); expect(result).toContain("git commit -m"); }); it("uses the provided prefix in the commit message", () => { const result = buildAutoCommitPrompt(task, "fix", false, "2026-03-07T12:00:00"); expect(result).toContain("fix:"); }); it("includes the task title in the commit message", () => { const result = buildAutoCommitPrompt(task, "feat", false, "2026-03-07T12:00:00"); expect(result).toContain("Task task-1"); }); it("includes the task id in the commit body", () => { const result = buildAutoCommitPrompt(task, "feat", false, "2026-03-07T12:00:00"); expect(result).toContain("task-1"); }); it("includes the session timestamp in the commit body", () => { const result = buildAutoCommitPrompt(task, "feat", false, "2026-03-07T12:00:00"); expect(result).toContain("2026-03-07T12:00:00"); }); it("does not include SUMMARY.md instructions when includeSummary is false", () => { const result = buildAutoCommitPrompt(task, "feat", false, "2026-03-07T12:00:00"); expect(result).not.toContain("SUMMARY.md"); }); it("includes SUMMARY.md instructions when includeSummary is true", () => { const result = buildAutoCommitPrompt(task, "feat", true, "2026-03-07T12:00:00"); expect(result).toContain("SUMMARY.md"); }); it("mentions non-blocking failure handling", () => { const result = buildAutoCommitPrompt(task, "feat", false, "2026-03-07T12:00:00"); expect(result).toContain("do not retry"); }); it("escapes double quotes in the task title", () => { const quotedTask = makeTask("q1"); quotedTask.title = 'Fix "quoted" title'; const result = buildAutoCommitPrompt(quotedTask, "fix", false, "2026-03-07T12:00:00"); expect(result).toContain('\\"quoted\\"'); }); }); describe("normalizeToUnixPath", () => { it("converts a WSL UNC path with wsl.localhost to a Unix path", () => { expect( normalizeToUnixPath("\\\\wsl.localhost\\Ubuntu\\home\\naomi\\code\\temp\\file.json") ).toBe("/home/naomi/code/temp/file.json"); }); it("converts a WSL UNC path with wsl$ (legacy) to a Unix path", () => { expect(normalizeToUnixPath("\\\\wsl$\\Ubuntu\\home\\naomi\\file.json")).toBe( "/home/naomi/file.json" ); }); it("handles forward-slash UNC paths produced by some tools", () => { expect(normalizeToUnixPath("//wsl.localhost/Ubuntu/home/naomi/file.json")).toBe( "/home/naomi/file.json" ); }); it("leaves a plain Unix path unchanged", () => { expect(normalizeToUnixPath("/home/naomi/code/temp/file.json")).toBe( "/home/naomi/code/temp/file.json" ); }); it("leaves an empty string unchanged", () => { expect(normalizeToUnixPath("")).toBe(""); }); }); describe("isTaskBlocked", () => { it("returns false when dependsOn is empty", () => { const task = makeTask("a", "pending", []); expect(isTaskBlocked(task, [task])).toBe(false); }); it("returns false when dependsOn is undefined", () => { const task = makeTask("a", "pending"); expect(isTaskBlocked(task, [task])).toBe(false); }); it("returns false when all dependencies are completed", () => { const dep = makeTask("dep", "completed"); const task = makeTask("a", "pending", ["dep"]); expect(isTaskBlocked(task, [dep, task])).toBe(false); }); it("returns true when a dependency has failed", () => { const dep = makeTask("dep", "failed"); const task = makeTask("a", "pending", ["dep"]); expect(isTaskBlocked(task, [dep, task])).toBe(true); }); it("returns true when a dependency is blocked", () => { const dep = makeTask("dep", "blocked"); const task = makeTask("a", "pending", ["dep"]); expect(isTaskBlocked(task, [dep, task])).toBe(true); }); it("returns false when a dependency is still pending (not yet failed)", () => { const dep = makeTask("dep", "pending"); const task = makeTask("a", "pending", ["dep"]); expect(isTaskBlocked(task, [dep, task])).toBe(false); }); it("returns false when dependency ID does not exist in task list", () => { const task = makeTask("a", "pending", ["nonexistent"]); expect(isTaskBlocked(task, [task])).toBe(false); }); }); describe("getReadyTasks", () => { it("returns empty array when task list is empty", () => { expect(getReadyTasks([], 3)).toEqual([]); }); it("returns all pending tasks with no deps when under limit", () => { const tasks = [makeTask("a", "pending"), makeTask("b", "pending"), makeTask("c", "pending")]; expect(getReadyTasks(tasks, 5)).toEqual([0, 1, 2]); }); it("respects the concurrency limit", () => { const tasks = [makeTask("a", "pending"), makeTask("b", "pending"), makeTask("c", "pending")]; expect(getReadyTasks(tasks, 2)).toEqual([0, 1]); }); it("skips tasks whose dependencies are not completed", () => { const tasks = [makeTask("a", "pending"), makeTask("b", "pending", ["a"])]; // b depends on a which is pending, not completed — so only a is ready expect(getReadyTasks(tasks, 5)).toEqual([0]); }); it("includes task when all its dependencies are completed", () => { const tasks = [makeTask("a", "completed"), makeTask("b", "pending", ["a"])]; expect(getReadyTasks(tasks, 5)).toEqual([1]); }); it("skips running, completed, failed, and blocked tasks", () => { const tasks = [ makeTask("a", "running"), makeTask("b", "completed"), makeTask("c", "failed"), makeTask("d", "blocked"), makeTask("e", "pending"), ]; expect(getReadyTasks(tasks, 5)).toEqual([4]); }); it("returns empty when limit is 0", () => { const tasks = [makeTask("a", "pending")]; expect(getReadyTasks(tasks, 0)).toEqual([]); }); }); describe("computeWaves", () => { it("returns empty array for empty task list", () => { expect(computeWaves([])).toEqual([]); }); it("puts all independent tasks in a single wave", () => { const tasks = [makeTask("a", "pending"), makeTask("b", "pending"), makeTask("c", "pending")]; expect(computeWaves(tasks)).toEqual([[0, 1, 2]]); }); it("creates one wave per task for a linear chain", () => { const tasks = [ makeTask("a", "pending"), makeTask("b", "pending", ["a"]), makeTask("c", "pending", ["b"]), ]; expect(computeWaves(tasks)).toEqual([[0], [1], [2]]); }); it("handles diamond dependency: A → B,C → D", () => { // A has no deps, B and C depend on A, D depends on B and C const tasks = [ makeTask("a", "pending"), makeTask("b", "pending", ["a"]), makeTask("c", "pending", ["a"]), makeTask("d", "pending", ["b", "c"]), ]; const waves = computeWaves(tasks); expect(waves).toHaveLength(3); expect(waves[0]).toEqual([0]); expect(waves[1]).toEqual([1, 2]); expect(waves[2]).toEqual([3]); }); it("groups circular dependencies into a final overflow wave", () => { // a→b, b→a — circular; c has no deps so goes in wave 0 const tasks = [ makeTask("a", "pending", ["b"]), makeTask("b", "pending", ["a"]), makeTask("c", "pending"), ]; const waves = computeWaves(tasks); // c goes in wave 0, then a+b get dumped in overflow expect(waves[0]).toEqual([2]); expect(waves[1]).toEqual([0, 1]); }); it("ignores unknown dependency IDs (treats them as satisfied)", () => { const tasks = [makeTask("a", "pending", ["nonexistent"])]; expect(computeWaves(tasks)).toEqual([[0]]); }); });