generated from nhcarrigan/template
feat: productivity suite — task loop, workflow, theming, docs & more (#197)
## Summary A large productivity-focused feature branch delivering a suite of improvements across automation, project management, theming, performance, and documentation. ### Features - **Guided Project Workflow** (#189) — Four-phase workflow panel (Discuss → Plan → Execute → Verify) to keep projects structured from idea to completion - **Automated Task Loop** (#179) — Per-task conversation orchestration with wave-based parallel execution, blocked-task detection, and concurrency control - **Wave-Based Parallel Execution** (#191) — Tasks run in dependency-aware waves with configurable concurrency; independent tasks execute in parallel - **Auto-Commit After Task Completion** (#192) — Task Loop optionally commits after each completed task so progress is never lost - **PRD Creator** (#180) — AI-assisted PRD and task list panel that outputs `hikari-tasks.json` for the Task Loop to consume - **Project Context Panel** (#188) — Persistent `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, and `STATE.md` files injected into Claude's context automatically - **Codebase Mapper** (#190) — Generates a `CODEBASE.md` architectural summary so Claude always understands the project structure - **Community Preset Themes** (#181) — Six built-in community themes: Dracula, Catppuccin Mocha, Nord, Solarized Dark, Gruvbox Dark, and Rosé Pine - **In-App Changelog Panel** (#193) — Fetches release notes from GitHub at runtime and displays them inside the app - **Full Embedded Documentation** (#196) — Replaced the single-page help modal with a 12-page paginated docs browser featuring a sidebar TOC, prev/next navigation, keyboard navigation (arrow keys, `?` shortcut), and comprehensive coverage of every feature ### Performance & Fixes - **Lazy Loading & Virtualisation** (#194) — Virtual windowing for conversation history, markdown memoisation, and debounced search for smooth rendering of large sessions - **Ctrl+C Copy Fix** (#195) — `Ctrl+C` now copies selected text as expected; interrupt-Claude behaviour only fires when no text is selected ### UX - Back-to-workflow button in PRD Creator and Task Loop panels for easy navigation - Navigation icon cluster replaced with a single clean dropdown menu ## Closes Closes #179 Closes #180 Closes #181 Closes #188 Closes #189 Closes #190 Closes #191 Closes #192 Closes #193 Closes #194 Closes #195 Closes #196 --- ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #197 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #197.
This commit is contained in:
@@ -0,0 +1,615 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { get } from "svelte/store";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { setMockInvokeResult } from "../../../vitest.setup";
|
||||
import { prdStore, PRD_FILENAME, type PrdTask } from "./prd";
|
||||
|
||||
describe("PRD_FILENAME", () => {
|
||||
it("is hikari-tasks.json", () => {
|
||||
expect(PRD_FILENAME).toBe("hikari-tasks.json");
|
||||
});
|
||||
});
|
||||
|
||||
describe("prdStore", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
prdStore.reset();
|
||||
});
|
||||
|
||||
describe("initial state", () => {
|
||||
it("has empty goal", () => {
|
||||
expect(get(prdStore.goal)).toBe("");
|
||||
});
|
||||
|
||||
it("has empty tasks array", () => {
|
||||
expect(get(prdStore.tasks)).toEqual([]);
|
||||
});
|
||||
|
||||
it("has false isGenerating", () => {
|
||||
expect(get(prdStore.isGenerating)).toBe(false);
|
||||
});
|
||||
|
||||
it("has false isLoaded", () => {
|
||||
expect(get(prdStore.isLoaded)).toBe(false);
|
||||
});
|
||||
|
||||
it("has false isLoading", () => {
|
||||
expect(get(prdStore.isLoading)).toBe(false);
|
||||
});
|
||||
|
||||
it("has false isSaving", () => {
|
||||
expect(get(prdStore.isSaving)).toBe(false);
|
||||
});
|
||||
|
||||
it("exposes all expected methods", () => {
|
||||
expect(typeof prdStore.loadFromFile).toBe("function");
|
||||
expect(typeof prdStore.saveToFile).toBe("function");
|
||||
expect(typeof prdStore.generatePrd).toBe("function");
|
||||
expect(typeof prdStore.finishGenerating).toBe("function");
|
||||
expect(typeof prdStore.addTask).toBe("function");
|
||||
expect(typeof prdStore.updateTask).toBe("function");
|
||||
expect(typeof prdStore.removeTask).toBe("function");
|
||||
expect(typeof prdStore.moveTaskUp).toBe("function");
|
||||
expect(typeof prdStore.moveTaskDown).toBe("function");
|
||||
expect(typeof prdStore.reset).toBe("function");
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadFromFile", () => {
|
||||
const mockPrdFile = JSON.stringify({
|
||||
version: 1,
|
||||
goal: "Build a REST API",
|
||||
tasks: [
|
||||
{
|
||||
id: "task-1",
|
||||
title: "Set up project",
|
||||
prompt: "Initialise the Node.js project",
|
||||
priority: "high",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
it("calls read_file_content with the correct path", async () => {
|
||||
setMockInvokeResult("read_file_content", mockPrdFile);
|
||||
|
||||
await prdStore.loadFromFile("/home/naomi/myproject");
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("read_file_content", {
|
||||
path: "/home/naomi/myproject/hikari-tasks.json",
|
||||
});
|
||||
});
|
||||
|
||||
it("sets goal from loaded file", async () => {
|
||||
setMockInvokeResult("read_file_content", mockPrdFile);
|
||||
|
||||
await prdStore.loadFromFile("/home/naomi/myproject");
|
||||
|
||||
expect(get(prdStore.goal)).toBe("Build a REST API");
|
||||
});
|
||||
|
||||
it("sets tasks from loaded file", async () => {
|
||||
setMockInvokeResult("read_file_content", mockPrdFile);
|
||||
|
||||
await prdStore.loadFromFile("/home/naomi/myproject");
|
||||
|
||||
const tasks = get(prdStore.tasks);
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].title).toBe("Set up project");
|
||||
});
|
||||
|
||||
it("sets isLoaded to true on success", async () => {
|
||||
setMockInvokeResult("read_file_content", mockPrdFile);
|
||||
|
||||
await prdStore.loadFromFile("/home/naomi/myproject");
|
||||
|
||||
expect(get(prdStore.isLoaded)).toBe(true);
|
||||
});
|
||||
|
||||
it("sets isLoaded to false when file not found", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("read_file_content", new Error("File not found"));
|
||||
|
||||
await prdStore.loadFromFile("/home/naomi/myproject");
|
||||
|
||||
expect(get(prdStore.isLoaded)).toBe(false);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("sets isLoaded to false on JSON parse error", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("read_file_content", "not valid json {{{");
|
||||
|
||||
await prdStore.loadFromFile("/home/naomi/myproject");
|
||||
|
||||
expect(get(prdStore.isLoaded)).toBe(false);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("sets isLoading to false after success", async () => {
|
||||
setMockInvokeResult("read_file_content", mockPrdFile);
|
||||
|
||||
await prdStore.loadFromFile("/home/naomi/myproject");
|
||||
|
||||
expect(get(prdStore.isLoading)).toBe(false);
|
||||
});
|
||||
|
||||
it("sets isLoading to false on error", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("read_file_content", new Error("Read error"));
|
||||
|
||||
await prdStore.loadFromFile("/home/naomi/myproject");
|
||||
|
||||
expect(get(prdStore.isLoading)).toBe(false);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("logs error when file load fails", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("read_file_content", new Error("File not found"));
|
||||
|
||||
await prdStore.loadFromFile("/home/naomi/myproject");
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Failed to load PRD file:", expect.any(Error));
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveToFile", () => {
|
||||
it("calls write_file_content with the correct path", async () => {
|
||||
setMockInvokeResult("write_file_content", undefined);
|
||||
|
||||
await prdStore.saveToFile("/home/naomi/myproject");
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("write_file_content", {
|
||||
path: "/home/naomi/myproject/hikari-tasks.json",
|
||||
content: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it("writes valid JSON with version 1", async () => {
|
||||
setMockInvokeResult("write_file_content", undefined);
|
||||
|
||||
await prdStore.saveToFile("/home/naomi/myproject");
|
||||
|
||||
const writeCall = vi.mocked(invoke).mock.calls.find(([cmd]) => cmd === "write_file_content");
|
||||
const content = (writeCall?.[1] as { content: string } | undefined)?.content ?? "";
|
||||
const parsed = JSON.parse(content) as { version: number };
|
||||
expect(parsed.version).toBe(1);
|
||||
});
|
||||
|
||||
it("includes current goal in the saved file", async () => {
|
||||
setMockInvokeResult("write_file_content", undefined);
|
||||
const mockPrdFile = JSON.stringify({ version: 1, goal: "My goal", tasks: [] });
|
||||
setMockInvokeResult("read_file_content", mockPrdFile);
|
||||
await prdStore.loadFromFile("/home/naomi/myproject");
|
||||
|
||||
vi.clearAllMocks();
|
||||
setMockInvokeResult("write_file_content", undefined);
|
||||
await prdStore.saveToFile("/home/naomi/myproject");
|
||||
|
||||
const writeCall = vi.mocked(invoke).mock.calls.find(([cmd]) => cmd === "write_file_content");
|
||||
const content = (writeCall?.[1] as { content: string } | undefined)?.content ?? "";
|
||||
const parsed = JSON.parse(content) as { goal: string };
|
||||
expect(parsed.goal).toBe("My goal");
|
||||
});
|
||||
|
||||
it("returns true on success", async () => {
|
||||
setMockInvokeResult("write_file_content", undefined);
|
||||
|
||||
const result = await prdStore.saveToFile("/home/naomi/myproject");
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false on failure", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("write_file_content", new Error("Write failed"));
|
||||
|
||||
const result = await prdStore.saveToFile("/home/naomi/myproject");
|
||||
|
||||
expect(result).toBe(false);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("logs error on failure", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("write_file_content", new Error("Write failed"));
|
||||
|
||||
await prdStore.saveToFile("/home/naomi/myproject");
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Failed to save PRD file:", expect.any(Error));
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("sets isSaving to false after success", async () => {
|
||||
setMockInvokeResult("write_file_content", undefined);
|
||||
|
||||
await prdStore.saveToFile("/home/naomi/myproject");
|
||||
|
||||
expect(get(prdStore.isSaving)).toBe(false);
|
||||
});
|
||||
|
||||
it("sets isSaving to false on error", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("write_file_content", new Error("Write failed"));
|
||||
|
||||
await prdStore.saveToFile("/home/naomi/myproject");
|
||||
|
||||
expect(get(prdStore.isSaving)).toBe(false);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("generatePrd", () => {
|
||||
it("calls send_prompt with the conversation id", async () => {
|
||||
setMockInvokeResult("send_prompt", undefined);
|
||||
|
||||
await prdStore.generatePrd("Build an API", "/home/naomi/myproject", "conv-123");
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("send_prompt", {
|
||||
conversationId: "conv-123",
|
||||
message: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it("prompt includes the user goal", async () => {
|
||||
setMockInvokeResult("send_prompt", undefined);
|
||||
|
||||
await prdStore.generatePrd("Build an API", "/home/naomi/myproject", "conv-123");
|
||||
|
||||
const sendCall = vi.mocked(invoke).mock.calls.find(([cmd]) => cmd === "send_prompt");
|
||||
const message = (sendCall?.[1] as { message: string } | undefined)?.message ?? "";
|
||||
expect(message).toContain("Build an API");
|
||||
});
|
||||
|
||||
it("prompt includes the working directory", async () => {
|
||||
setMockInvokeResult("send_prompt", undefined);
|
||||
|
||||
await prdStore.generatePrd("Build an API", "/home/naomi/myproject", "conv-123");
|
||||
|
||||
const sendCall = vi.mocked(invoke).mock.calls.find(([cmd]) => cmd === "send_prompt");
|
||||
const message = (sendCall?.[1] as { message: string } | undefined)?.message ?? "";
|
||||
expect(message).toContain("/home/naomi/myproject");
|
||||
});
|
||||
|
||||
it("prompt mentions hikari-tasks.json", async () => {
|
||||
setMockInvokeResult("send_prompt", undefined);
|
||||
|
||||
await prdStore.generatePrd("Build an API", "/home/naomi/myproject", "conv-123");
|
||||
|
||||
const sendCall = vi.mocked(invoke).mock.calls.find(([cmd]) => cmd === "send_prompt");
|
||||
const message = (sendCall?.[1] as { message: string } | undefined)?.message ?? "";
|
||||
expect(message).toContain("hikari-tasks.json");
|
||||
});
|
||||
|
||||
it("sets the goal in the store", async () => {
|
||||
setMockInvokeResult("send_prompt", undefined);
|
||||
|
||||
await prdStore.generatePrd("Build an API", "/home/naomi/myproject", "conv-123");
|
||||
|
||||
expect(get(prdStore.goal)).toBe("Build an API");
|
||||
});
|
||||
|
||||
it("resets isGenerating to false on send_prompt error", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("send_prompt", new Error("Send failed"));
|
||||
|
||||
await prdStore.generatePrd("Build an API", "/home/naomi/myproject", "conv-123");
|
||||
|
||||
expect(get(prdStore.isGenerating)).toBe(false);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("logs error when send_prompt fails", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("send_prompt", new Error("Send failed"));
|
||||
|
||||
await prdStore.generatePrd("Build an API", "/home/naomi/myproject", "conv-123");
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Failed to generate PRD:", expect.any(Error));
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("finishGenerating", () => {
|
||||
it("sets isGenerating to false", () => {
|
||||
prdStore.finishGenerating();
|
||||
expect(get(prdStore.isGenerating)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("executePrd", () => {
|
||||
beforeEach(() => {
|
||||
setMockInvokeResult("write_file_content", undefined);
|
||||
setMockInvokeResult("send_prompt", undefined);
|
||||
prdStore.addTask({
|
||||
title: "Set up project",
|
||||
prompt: "Initialise the repo",
|
||||
priority: "high",
|
||||
});
|
||||
prdStore.addTask({ title: "Write tests", prompt: "Add vitest tests", priority: "medium" });
|
||||
});
|
||||
|
||||
it("saves the file before sending the prompt", async () => {
|
||||
await prdStore.executePrd("/home/naomi/myproject", "conv-123");
|
||||
|
||||
const writeCall = vi.mocked(invoke).mock.calls.find(([cmd]) => cmd === "write_file_content");
|
||||
expect(writeCall).toBeDefined();
|
||||
});
|
||||
|
||||
it("calls send_prompt with the conversation id", async () => {
|
||||
await prdStore.executePrd("/home/naomi/myproject", "conv-123");
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("send_prompt", {
|
||||
conversationId: "conv-123",
|
||||
message: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it("prompt includes all task titles", async () => {
|
||||
await prdStore.executePrd("/home/naomi/myproject", "conv-123");
|
||||
|
||||
const sendCall = vi.mocked(invoke).mock.calls.find(([cmd]) => cmd === "send_prompt");
|
||||
const message = (sendCall?.[1] as { message: string } | undefined)?.message ?? "";
|
||||
expect(message).toContain("Set up project");
|
||||
expect(message).toContain("Write tests");
|
||||
});
|
||||
|
||||
it("prompt includes all task prompts", async () => {
|
||||
await prdStore.executePrd("/home/naomi/myproject", "conv-123");
|
||||
|
||||
const sendCall = vi.mocked(invoke).mock.calls.find(([cmd]) => cmd === "send_prompt");
|
||||
const message = (sendCall?.[1] as { message: string } | undefined)?.message ?? "";
|
||||
expect(message).toContain("Initialise the repo");
|
||||
expect(message).toContain("Add vitest tests");
|
||||
});
|
||||
|
||||
it("prompt includes the working directory", async () => {
|
||||
await prdStore.executePrd("/home/naomi/myproject", "conv-123");
|
||||
|
||||
const sendCall = vi.mocked(invoke).mock.calls.find(([cmd]) => cmd === "send_prompt");
|
||||
const message = (sendCall?.[1] as { message: string } | undefined)?.message ?? "";
|
||||
expect(message).toContain("/home/naomi/myproject");
|
||||
});
|
||||
|
||||
it("prompt references hikari-tasks.json", async () => {
|
||||
await prdStore.executePrd("/home/naomi/myproject", "conv-123");
|
||||
|
||||
const sendCall = vi.mocked(invoke).mock.calls.find(([cmd]) => cmd === "send_prompt");
|
||||
const message = (sendCall?.[1] as { message: string } | undefined)?.message ?? "";
|
||||
expect(message).toContain("hikari-tasks.json");
|
||||
});
|
||||
|
||||
it("exposes executePrd as a method", () => {
|
||||
expect(typeof prdStore.executePrd).toBe("function");
|
||||
});
|
||||
});
|
||||
|
||||
describe("addTask", () => {
|
||||
it("appends a new task to the list", () => {
|
||||
prdStore.addTask({ title: "Task A", prompt: "Do A", priority: "high" });
|
||||
|
||||
const tasks = get(prdStore.tasks);
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].title).toBe("Task A");
|
||||
});
|
||||
|
||||
it("assigns a unique id to each task", () => {
|
||||
prdStore.addTask({ title: "Task A", prompt: "Do A", priority: "high" });
|
||||
prdStore.addTask({ title: "Task B", prompt: "Do B", priority: "low" });
|
||||
|
||||
const tasks = get(prdStore.tasks);
|
||||
expect(tasks[0].id).not.toBe(tasks[1].id);
|
||||
});
|
||||
|
||||
it("preserves all task fields", () => {
|
||||
prdStore.addTask({ title: "My Task", prompt: "Do the thing", priority: "medium" });
|
||||
|
||||
const task = get(prdStore.tasks)[0];
|
||||
expect(task.title).toBe("My Task");
|
||||
expect(task.prompt).toBe("Do the thing");
|
||||
expect(task.priority).toBe("medium");
|
||||
});
|
||||
|
||||
it("adds tasks in order", () => {
|
||||
prdStore.addTask({ title: "First", prompt: "A", priority: "high" });
|
||||
prdStore.addTask({ title: "Second", prompt: "B", priority: "low" });
|
||||
|
||||
const tasks = get(prdStore.tasks);
|
||||
expect(tasks[0].title).toBe("First");
|
||||
expect(tasks[1].title).toBe("Second");
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateTask", () => {
|
||||
let taskId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
prdStore.addTask({ title: "Original", prompt: "Original prompt", priority: "high" });
|
||||
taskId = get(prdStore.tasks)[0].id;
|
||||
});
|
||||
|
||||
it("updates the title of the specified task", () => {
|
||||
prdStore.updateTask(taskId, { title: "Updated" });
|
||||
|
||||
expect(get(prdStore.tasks)[0].title).toBe("Updated");
|
||||
});
|
||||
|
||||
it("updates the prompt of the specified task", () => {
|
||||
prdStore.updateTask(taskId, { prompt: "New prompt" });
|
||||
|
||||
expect(get(prdStore.tasks)[0].prompt).toBe("New prompt");
|
||||
});
|
||||
|
||||
it("updates the priority of the specified task", () => {
|
||||
prdStore.updateTask(taskId, { priority: "low" });
|
||||
|
||||
expect(get(prdStore.tasks)[0].priority).toBe("low");
|
||||
});
|
||||
|
||||
it("does not affect other tasks", () => {
|
||||
prdStore.addTask({ title: "Other", prompt: "Other prompt", priority: "medium" });
|
||||
const otherId = get(prdStore.tasks)[1].id;
|
||||
|
||||
prdStore.updateTask(taskId, { title: "Changed" });
|
||||
|
||||
const otherTask = get(prdStore.tasks).find((t) => t.id === otherId);
|
||||
expect(otherTask?.title).toBe("Other");
|
||||
});
|
||||
|
||||
it("does nothing when id is not found", () => {
|
||||
prdStore.updateTask("nonexistent-id", { title: "Ghost" });
|
||||
|
||||
expect(get(prdStore.tasks)[0].title).toBe("Original");
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeTask", () => {
|
||||
it("removes the task with the given id", () => {
|
||||
prdStore.addTask({ title: "Keep", prompt: "A", priority: "high" });
|
||||
prdStore.addTask({ title: "Remove", prompt: "B", priority: "low" });
|
||||
const removeId = get(prdStore.tasks)[1].id;
|
||||
|
||||
prdStore.removeTask(removeId);
|
||||
|
||||
const tasks = get(prdStore.tasks);
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].title).toBe("Keep");
|
||||
});
|
||||
|
||||
it("does nothing when id is not found", () => {
|
||||
prdStore.addTask({ title: "Task", prompt: "A", priority: "high" });
|
||||
|
||||
prdStore.removeTask("nonexistent-id");
|
||||
|
||||
expect(get(prdStore.tasks)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("results in empty array when removing the only task", () => {
|
||||
prdStore.addTask({ title: "Only", prompt: "A", priority: "high" });
|
||||
const id = get(prdStore.tasks)[0].id;
|
||||
|
||||
prdStore.removeTask(id);
|
||||
|
||||
expect(get(prdStore.tasks)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("moveTaskUp", () => {
|
||||
let tasks: PrdTask[];
|
||||
|
||||
beforeEach(() => {
|
||||
prdStore.addTask({ title: "First", prompt: "A", priority: "high" });
|
||||
prdStore.addTask({ title: "Second", prompt: "B", priority: "medium" });
|
||||
prdStore.addTask({ title: "Third", prompt: "C", priority: "low" });
|
||||
tasks = get(prdStore.tasks);
|
||||
});
|
||||
|
||||
it("swaps the task with the one above it", () => {
|
||||
prdStore.moveTaskUp(tasks[1].id);
|
||||
|
||||
const result = get(prdStore.tasks);
|
||||
expect(result[0].title).toBe("Second");
|
||||
expect(result[1].title).toBe("First");
|
||||
});
|
||||
|
||||
it("does nothing when task is already first", () => {
|
||||
prdStore.moveTaskUp(tasks[0].id);
|
||||
|
||||
const result = get(prdStore.tasks);
|
||||
expect(result[0].title).toBe("First");
|
||||
});
|
||||
|
||||
it("does nothing when id is not found", () => {
|
||||
prdStore.moveTaskUp("nonexistent");
|
||||
|
||||
const result = get(prdStore.tasks);
|
||||
expect(result[0].title).toBe("First");
|
||||
expect(result[1].title).toBe("Second");
|
||||
expect(result[2].title).toBe("Third");
|
||||
});
|
||||
|
||||
it("does not change array length", () => {
|
||||
prdStore.moveTaskUp(tasks[2].id);
|
||||
|
||||
expect(get(prdStore.tasks)).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("moveTaskDown", () => {
|
||||
let tasks: PrdTask[];
|
||||
|
||||
beforeEach(() => {
|
||||
prdStore.addTask({ title: "First", prompt: "A", priority: "high" });
|
||||
prdStore.addTask({ title: "Second", prompt: "B", priority: "medium" });
|
||||
prdStore.addTask({ title: "Third", prompt: "C", priority: "low" });
|
||||
tasks = get(prdStore.tasks);
|
||||
});
|
||||
|
||||
it("swaps the task with the one below it", () => {
|
||||
prdStore.moveTaskDown(tasks[1].id);
|
||||
|
||||
const result = get(prdStore.tasks);
|
||||
expect(result[1].title).toBe("Third");
|
||||
expect(result[2].title).toBe("Second");
|
||||
});
|
||||
|
||||
it("does nothing when task is already last", () => {
|
||||
prdStore.moveTaskDown(tasks[2].id);
|
||||
|
||||
const result = get(prdStore.tasks);
|
||||
expect(result[2].title).toBe("Third");
|
||||
});
|
||||
|
||||
it("does nothing when id is not found", () => {
|
||||
prdStore.moveTaskDown("nonexistent");
|
||||
|
||||
const result = get(prdStore.tasks);
|
||||
expect(result[0].title).toBe("First");
|
||||
expect(result[1].title).toBe("Second");
|
||||
expect(result[2].title).toBe("Third");
|
||||
});
|
||||
|
||||
it("does not change array length", () => {
|
||||
prdStore.moveTaskDown(tasks[0].id);
|
||||
|
||||
expect(get(prdStore.tasks)).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reset", () => {
|
||||
it("clears the goal", async () => {
|
||||
setMockInvokeResult("send_prompt", undefined);
|
||||
await prdStore.generatePrd("Some goal", "/wd", "conv-1");
|
||||
|
||||
prdStore.reset();
|
||||
|
||||
expect(get(prdStore.goal)).toBe("");
|
||||
});
|
||||
|
||||
it("clears all tasks", () => {
|
||||
prdStore.addTask({ title: "Task", prompt: "A", priority: "high" });
|
||||
|
||||
prdStore.reset();
|
||||
|
||||
expect(get(prdStore.tasks)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("sets isLoaded to false", async () => {
|
||||
const mockPrdFile = JSON.stringify({ version: 1, goal: "goal", tasks: [] });
|
||||
setMockInvokeResult("read_file_content", mockPrdFile);
|
||||
await prdStore.loadFromFile("/wd");
|
||||
|
||||
prdStore.reset();
|
||||
|
||||
expect(get(prdStore.isLoaded)).toBe(false);
|
||||
});
|
||||
|
||||
it("sets isGenerating to false", () => {
|
||||
prdStore.finishGenerating();
|
||||
prdStore.reset();
|
||||
|
||||
expect(get(prdStore.isGenerating)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user