import { describe, it, expect } from "vitest"; import path from "node:path"; import os from "node:os"; import fs from "node:fs"; import { buildState, parseFrontmatter, countTaskCheckboxes } from "./state-builder.mjs"; function makeWorkTree({ epics }) { const root = fs.mkdtempSync(path.join(os.tmpdir(), "work-state-")); for (const [epicId, epicData] of Object.entries(epics)) { const epicDir = path.join(root, epicId); fs.mkdirSync(epicDir, { recursive: true }); fs.writeFileSync( path.join(epicDir, "_epic.md"), `--- id: ${epicId} title: ${epicData.title ?? epicId} status: ${epicData.status ?? "todo"} --- `, ); for (const [storyId, storyData] of Object.entries(epicData.stories ?? {})) { const storyDir = path.join(epicDir, storyId); fs.mkdirSync(storyDir, { recursive: true }); const tasksBlock = (storyData.tasks ?? []) .map((t) => `- [${t.done ? "x" : " "}] ${t.label}`) .join("\n"); fs.writeFileSync( path.join(storyDir, "_story.md"), `--- id: ${storyId} title: ${storyData.title ?? storyId} status: ${storyData.status ?? "todo"} --- ## Tasks ${tasksBlock} `, ); } } return root; } describe("buildState", () => { it("returns an empty epics map for a non-existent workRoot", () => { const state = buildState("/nonexistent/path"); expect(state.epics).toEqual({}); expect(typeof state.updated_at).toBe("string"); }); it("collects epics + stories with status + task counts", () => { const root = makeWorkTree({ epics: { epic1: { status: "in-progress", stories: { story1: { status: "done", tasks: [ { label: "a", done: true }, { label: "b", done: true }, ], }, story2: { status: "todo", tasks: [ { label: "c", done: false }, { label: "d", done: false }, { label: "e", done: false }, ], }, }, }, }, }); const state = buildState(root); expect(state.epics).toEqual({ epic1: { status: "in-progress", title: "epic1", stories: { story1: { status: "done", title: "story1", ac_total: 2, ac_completed: 2 }, story2: { status: "todo", title: "story2", ac_total: 3, ac_completed: 0 }, }, }, }); }); it("skips _templates and prds folders", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "work-skip-")); fs.mkdirSync(path.join(root, "_templates"), { recursive: true }); fs.writeFileSync(path.join(root, "_templates", "_epic.md"), "---\nid: x\n---"); fs.mkdirSync(path.join(root, "prds"), { recursive: true }); fs.writeFileSync(path.join(root, "prds", "_epic.md"), "---\nid: y\n---"); expect(buildState(root).epics).toEqual({}); }); it("skips directories without _epic.md", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "work-skip2-")); fs.mkdirSync(path.join(root, "incomplete"), { recursive: true }); expect(buildState(root).epics).toEqual({}); }); }); describe("countTaskCheckboxes", () => { it("counts both unchecked and checked", () => { const content = `## Tasks - [x] done one - [ ] open one - [X] capital X also counts `; expect(countTaskCheckboxes(content)).toEqual({ total: 3, completed: 2 }); }); it("only counts inside the ## Tasks section", () => { const content = `## Tasks - [x] in tasks ## Notes - [x] outside, should not count `; expect(countTaskCheckboxes(content)).toEqual({ total: 1, completed: 1 }); }); it("returns zeros when no Tasks section exists", () => { expect(countTaskCheckboxes(`## Goal\n\nFoo\n`)).toEqual({ total: 0, completed: 0 }); }); }); describe("parseFrontmatter", () => { it("extracts simple string keys", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fm-")); const fp = path.join(dir, "f.md"); fs.writeFileSync(fp, `--- id: x title: A title status: done --- Body`); expect(parseFrontmatter(fp)).toEqual({ id: "x", title: "A title", status: "done" }); }); it("returns {} when no frontmatter present", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fm2-")); const fp = path.join(dir, "f.md"); fs.writeFileSync(fp, "# Just a heading\n"); expect(parseFrontmatter(fp)).toEqual({}); }); });