140 lines
5.0 KiB
TypeScript
140 lines
5.0 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
|
|
import { mutateMdtodoDocument, parseMdtodoDocument, readMdtodoTaskDetail } from "./mdtodo.ts";
|
|
|
|
test("parseMdtodoDocument projects Rxx hierarchy without raw body leakage", () => {
|
|
const document = parseMdtodoDocument(`# Demo
|
|
|
|
## R1 Root task [Processing]
|
|
|
|
Root notes with [context](https://example.test/context).
|
|
|
|
### R1.1 Child done [Finished]
|
|
|
|
- implementation notes stay in source markdown
|
|
|
|
## R2 Backlog item
|
|
`, {
|
|
sourceId: "demo",
|
|
relativePath: "project-management/MDTODO.md"
|
|
});
|
|
|
|
expect(document.parserMode).toBe("rxx");
|
|
expect(document.taskCount).toBe(3);
|
|
expect(document.tasks.map((task) => task.taskId)).toEqual(["R1", "R1.1", "R2"]);
|
|
expect(document.tasks.map((task) => task.status)).toEqual(["in_progress", "completed", "open"]);
|
|
expect(document.tasks[1].parentTaskRef).toBe(document.tasks[0].taskRef);
|
|
expect(document.tasks[0].depth).toBe(0);
|
|
expect(document.tasks[1].depth).toBe(1);
|
|
expect(document.tasks[0].linkCount).toBe(1);
|
|
expect(document.tasks[0].links).toEqual([
|
|
expect.objectContaining({ label: "context", kind: "external", relativePath: null })
|
|
]);
|
|
expect(document.tasks[0].bodyPreview).toBe("Root notes with [context](https://example.test/context).");
|
|
expect(document.tasks[0].body).toBeUndefined();
|
|
expect(document.tasks[0].rawContent).toBeUndefined();
|
|
expect(document.document.parserMode).toBe("rxx");
|
|
expect(document.document.valuesRedacted).toBe(true);
|
|
});
|
|
|
|
test("readMdtodoTaskDetail exposes Rxx body and safe Markdown report links", () => {
|
|
const detail = readMdtodoTaskDetail(`# Demo
|
|
|
|
## R1 Root task
|
|
|
|
Task details
|
|
|
|
- report: [R1.1](./20260110_LOG/R1.1_log_implementation.md)
|
|
- external: [context](https://example.test/context)
|
|
|
|
### R1.1 Child
|
|
|
|
Child details
|
|
`, {
|
|
sourceId: "demo",
|
|
relativePath: "20260609_feedback.md",
|
|
taskRef: "mdtodo:demo:file_plan:R1",
|
|
fileRef: "file_plan",
|
|
projectId: "project_test"
|
|
});
|
|
|
|
expect(detail?.body).toContain("Task details");
|
|
expect(detail?.body).not.toContain("Child details");
|
|
expect(detail?.links.find((link) => link.label === "R1.1")).toMatchObject({
|
|
kind: "markdown-report",
|
|
relativePath: "20260110_LOG/R1.1_log_implementation.md"
|
|
});
|
|
expect(detail?.links.find((link) => link.label === "context")).toMatchObject({ kind: "external", relativePath: null });
|
|
});
|
|
|
|
test("parseMdtodoDocument keeps legacy checkbox parsing explicit", () => {
|
|
const document = parseMdtodoDocument(`# Demo
|
|
|
|
- [ ] Root task
|
|
- [x] Child done
|
|
- [-] Blocked item
|
|
`, {
|
|
sourceId: "demo",
|
|
relativePath: "project-management/MDTODO.md"
|
|
});
|
|
|
|
expect(document.parserMode).toBe("legacy-checkbox");
|
|
expect(document.taskCount).toBe(3);
|
|
expect(document.tasks[0].status).toBe("open");
|
|
expect(document.tasks[1].status).toBe("completed");
|
|
expect(document.tasks[1].parentTaskRef).toBe(document.tasks[0].taskRef);
|
|
expect(document.tasks[2].status).toBe("open");
|
|
expect(document.document.valuesRedacted).toBe(true);
|
|
});
|
|
|
|
test("mutateMdtodoDocument supports Rxx add update delete operations", () => {
|
|
const initial = `# Demo
|
|
|
|
## R1 Root task
|
|
|
|
Original body.
|
|
|
|
### R1.1 Existing child
|
|
`;
|
|
|
|
let result = mutateMdtodoDocument(initial, { type: "addTask", title: "Second root" });
|
|
expect(result.ok).toBe(true);
|
|
expect(result.changedTaskId).toBe("R2");
|
|
expect(result.markdown).toContain("## R2 Second root");
|
|
|
|
result = mutateMdtodoDocument(result.markdown, { type: "addTask", parentRxxId: "R1", title: "Second child", body: "Child body" });
|
|
expect(result.changedTaskId).toBe("R1.2");
|
|
expect(result.markdown).toContain("### R1.2 Second child\n\nChild body");
|
|
|
|
result = mutateMdtodoDocument(result.markdown, { type: "addTask", afterRxxId: "R1.1", title: "Continue child" });
|
|
expect(result.changedTaskId).toBe("R1.3");
|
|
expect(result.markdown).toContain("### R1.3 Continue child");
|
|
|
|
result = mutateMdtodoDocument(result.markdown, { type: "updateStatus", rxxId: "R1", status: "completed" });
|
|
expect(result.markdown).toContain("## R1 Root task [completed]");
|
|
|
|
result = mutateMdtodoDocument(result.markdown, { type: "updateTitle", rxxId: "R1", title: "Renamed root" });
|
|
expect(result.markdown).toContain("## R1 Renamed root [completed]");
|
|
|
|
result = mutateMdtodoDocument(result.markdown, { type: "updateBody", rxxId: "R1", body: "Rewritten body." });
|
|
expect(result.markdown).toContain("## R1 Renamed root [completed]\n\nRewritten body.\n\n### R1.1 Existing child");
|
|
|
|
result = mutateMdtodoDocument(result.markdown, { type: "deleteTask", rxxId: "R1.2" });
|
|
expect(result.affectedTaskIds).toEqual(["R1.2"]);
|
|
expect(result.markdown).not.toContain("R1.2");
|
|
expect(result.document.tasks.map((task) => task.taskId)).toEqual(["R1", "R1.1", "R1.3", "R2"]);
|
|
});
|
|
|
|
test("mutateMdtodoDocument reports optimistic revision conflicts", () => {
|
|
const conflict = mutateMdtodoDocument("# Demo\n\n## R1 Root\n", {
|
|
type: "updateTitle",
|
|
rxxId: "R1",
|
|
title: "No write",
|
|
expectedFingerprint: "stale"
|
|
});
|
|
|
|
expect(conflict.ok).toBe(false);
|
|
expect(conflict.conflict).toBe(true);
|
|
expect(conflict.code).toBe("revision_conflict");
|
|
});
|