generated from nhcarrigan/template
feat: automated task loop panel with per-task conversation orchestration
Implements issue #179. Adds a Task Loop panel that imports hikari-tasks.json files and automatically runs Claude Code through each task in sequence, one per conversation tab. Includes Start/Pause/Stop/Resume controls, real-time status indicators per task, and a StatusBar badge. WSL UNC paths from the file picker are normalised to Unix paths at import time.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
findNextPendingIndex,
|
||||
countByStatus,
|
||||
buildTaskPrompt,
|
||||
normalizeToUnixPath,
|
||||
type TaskLoopTask,
|
||||
} from "./taskLoop";
|
||||
|
||||
const makeTask = (id: string, status: TaskLoopTask["status"] = "pending"): TaskLoopTask => ({
|
||||
id,
|
||||
title: `Task ${id}`,
|
||||
prompt: `Do the thing for ${id}`,
|
||||
priority: "medium",
|
||||
status,
|
||||
});
|
||||
|
||||
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("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("");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user