Files
hikari-desktop/src/lib/stores/workflow.test.ts
T
hikari e6e9f7ae59
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m39s
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled
CI / Lint & Test (push) Has been cancelled
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>
2026-03-07 03:08:33 -08:00

212 lines
7.2 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
buildDiscussPrompt,
buildVerifyPrompt,
canAdvancePhase,
canGoBack,
getPhaseLabel,
generateCriterionId,
type WorkflowState,
type VerifyCriterion,
} from "./workflow";
// ─── buildDiscussPrompt ───────────────────────────────────────────────────────
describe("buildDiscussPrompt", () => {
it("includes the description in the output", () => {
const prompt = buildDiscussPrompt("Build a user authentication system");
expect(prompt).toContain("Build a user authentication system");
});
it("references CONTEXT.md", () => {
const prompt = buildDiscussPrompt("some project");
expect(prompt).toContain("CONTEXT.md");
});
it("mentions acceptance criteria section", () => {
const prompt = buildDiscussPrompt("some project");
expect(prompt.toLowerCase()).toContain("acceptance criteria");
});
it("returns a non-empty string for any description", () => {
expect(buildDiscussPrompt("a").length).toBeGreaterThan(0);
expect(buildDiscussPrompt("very long description ".repeat(20)).length).toBeGreaterThan(0);
});
});
// ─── buildVerifyPrompt ────────────────────────────────────────────────────────
describe("buildVerifyPrompt", () => {
it("references VERIFY.md", () => {
const prompt = buildVerifyPrompt([]);
expect(prompt).toContain("VERIFY.md");
});
it("handles empty criteria list gracefully", () => {
const prompt = buildVerifyPrompt([]);
expect(prompt.length).toBeGreaterThan(0);
expect(prompt).not.toContain("undefined");
});
it("includes all criteria text in the output", () => {
const criteria: VerifyCriterion[] = [
{ id: "c1", text: "Login must work", status: "pending" },
{ id: "c2", text: "Tests must pass", status: "pending" },
];
const prompt = buildVerifyPrompt(criteria);
expect(prompt).toContain("Login must work");
expect(prompt).toContain("Tests must pass");
});
it("numbers criteria starting from 1", () => {
const criteria: VerifyCriterion[] = [{ id: "c1", text: "First criterion", status: "pending" }];
const prompt = buildVerifyPrompt(criteria);
expect(prompt).toContain("1. First criterion");
});
it("includes all criteria when multiple are provided", () => {
const criteria: VerifyCriterion[] = [
{ id: "c1", text: "Alpha", status: "pending" },
{ id: "c2", text: "Beta", status: "pending" },
{ id: "c3", text: "Gamma", status: "pending" },
];
const prompt = buildVerifyPrompt(criteria);
expect(prompt).toContain("1. Alpha");
expect(prompt).toContain("2. Beta");
expect(prompt).toContain("3. Gamma");
});
});
// ─── canAdvancePhase ──────────────────────────────────────────────────────────
function makeState(overrides: Partial<WorkflowState> = {}): WorkflowState {
return {
version: 1,
currentPhase: 1,
quickMode: false,
discuss: { description: "", contextCaptured: false },
plan: { tasksApproved: false },
execute: { completed: false },
verify: { criteria: [], verificationComplete: false, report: "" },
...overrides,
};
}
describe("canAdvancePhase", () => {
describe("phase 1 (Discuss)", () => {
it("returns false when context not captured and not quick mode", () => {
const state = makeState({ currentPhase: 1 });
expect(canAdvancePhase(state)).toBe(false);
});
it("returns true when context is captured", () => {
const state = makeState({
currentPhase: 1,
discuss: { description: "test", contextCaptured: true },
});
expect(canAdvancePhase(state)).toBe(true);
});
it("returns true in quick mode even without context captured", () => {
const state = makeState({ currentPhase: 1, quickMode: true });
expect(canAdvancePhase(state)).toBe(true);
});
});
describe("phase 2 (Plan)", () => {
it("returns false when plan not approved", () => {
const state = makeState({ currentPhase: 2 });
expect(canAdvancePhase(state)).toBe(false);
});
it("returns true when plan is approved", () => {
const state = makeState({ currentPhase: 2, plan: { tasksApproved: true } });
expect(canAdvancePhase(state)).toBe(true);
});
});
describe("phase 3 (Execute)", () => {
it("returns false when execution not completed", () => {
const state = makeState({ currentPhase: 3 });
expect(canAdvancePhase(state)).toBe(false);
});
it("returns true when execution is completed", () => {
const state = makeState({ currentPhase: 3, execute: { completed: true } });
expect(canAdvancePhase(state)).toBe(true);
});
});
describe("phase 4 (Verify)", () => {
it("returns false when verification not complete", () => {
const state = makeState({ currentPhase: 4 });
expect(canAdvancePhase(state)).toBe(false);
});
it("returns true when verification is complete", () => {
const state = makeState({
currentPhase: 4,
verify: { criteria: [], verificationComplete: true, report: "All good" },
});
expect(canAdvancePhase(state)).toBe(true);
});
});
});
// ─── canGoBack ────────────────────────────────────────────────────────────────
describe("canGoBack", () => {
it("returns false for phase 1", () => {
expect(canGoBack(1)).toBe(false);
});
it("returns true for phase 2", () => {
expect(canGoBack(2)).toBe(true);
});
it("returns true for phase 3", () => {
expect(canGoBack(3)).toBe(true);
});
it("returns true for phase 4", () => {
expect(canGoBack(4)).toBe(true);
});
});
// ─── getPhaseLabel ────────────────────────────────────────────────────────────
describe("getPhaseLabel", () => {
it("returns Discuss for phase 1", () => {
expect(getPhaseLabel(1)).toBe("Discuss");
});
it("returns Plan for phase 2", () => {
expect(getPhaseLabel(2)).toBe("Plan");
});
it("returns Execute for phase 3", () => {
expect(getPhaseLabel(3)).toBe("Execute");
});
it("returns Verify for phase 4", () => {
expect(getPhaseLabel(4)).toBe("Verify");
});
});
// ─── generateCriterionId ─────────────────────────────────────────────────────
describe("generateCriterionId", () => {
it("returns a non-empty string", () => {
expect(generateCriterionId().length).toBeGreaterThan(0);
});
it("returns unique IDs on successive calls", () => {
const ids = new Set(Array.from({ length: 20 }, () => generateCriterionId()));
expect(ids.size).toBe(20);
});
it("starts with the expected prefix", () => {
expect(generateCriterionId()).toMatch(/^criterion-/);
});
});