Files
agentic-dev-template/scripts/work/state-builder.test.mjs

153 lines
4.4 KiB
JavaScript

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({});
});
});