import assert from "node:assert/strict"; import test from "node:test"; import { TaskState } from "../src/state.ts"; import { TaskValidationError } from "../src/types.ts"; function names(state: TaskState): string[] { return state.list().filter((task) => task.parentId === null).map((task) => task.name); } test("adds a nested batch with generated preorder IDs and derived relations", () => { const state = new TaskState(); const added = state.add([ { name: "Parent", description: "Parent description", children: [ { name: "First child", description: "First" }, { name: "Second child", description: "Second", status: "blocked" }, ], }, ]); assert.deepEqual(added.map((task) => task.id), [1, 2, 3]); assert.deepEqual(state.get([1])[0]?.childrenIds, [2, 3]); assert.equal(state.get([2])[0]?.parentId, 1); assert.equal(state.get([2])[0]?.status, "pending"); assert.equal(state.snapshot().nextId, 4); }); test("rejects invalid nested parentId atomically", () => { const state = new TaskState(); state.add([{ name: "Existing", description: "Existing" }]); const before = state.snapshot(); assert.throws( () => state.add([ { name: "Parent", description: "Parent", children: [{ name: "Child", description: "Child", parentId: 1 }], }, ]), TaskValidationError, ); assert.deepEqual(state.snapshot(), before); }); test("enforces done descendants against the final atomic update state", () => { const state = new TaskState(); state.add([{ name: "Parent", description: "Parent", children: [{ name: "Child", description: "Child" }] }]); const before = state.snapshot(); assert.throws(() => state.update([{ id: 1, status: "done" }]), /unfinished descendants/); assert.deepEqual(state.snapshot(), before); state.update([ { id: 2, status: "done" }, { id: 1, status: "done" }, ]); assert.deepEqual(state.list().map((task) => task.status), ["done", "done"]); assert.throws(() => state.update([{ id: 2, status: "in-progress" }]), /unfinished descendants/); state.update([ { id: 1, status: "in-progress" }, { id: 2, status: "in-progress" }, ]); assert.deepEqual(state.list().map((task) => task.status), ["in-progress", "in-progress"]); }); test("rejects cycles and duplicate patches without partial changes", () => { const state = new TaskState(); state.add([{ name: "Root", description: "Root", children: [{ name: "Child", description: "Child" }] }]); const before = state.snapshot(); assert.throws(() => state.update([{ id: 1, parentId: 2 }, { id: 2, name: "Changed" }]), /cycle/); assert.deepEqual(state.snapshot(), before); assert.throws(() => state.update([{ id: 2, name: "One" }, { id: 2, name: "Two" }]), /duplicate task patches/); assert.deepEqual(state.snapshot(), before); }); test("reorders roots with zero-based sequential positions", () => { const state = new TaskState(); state.add([ { name: "A", description: "A" }, { name: "B", description: "B" }, { name: "C", description: "C" }, ]); state.update([{ id: 1, position: 2 }]); assert.deepEqual(names(state), ["B", "C", "A"]); state.update([ { id: 1, position: 0 }, { id: 2, position: 2 }, ]); assert.deepEqual(names(state), ["A", "C", "B"]); }); test("reparents and positions tasks among their final siblings", () => { const state = new TaskState(); state.add([ { name: "One", description: "One", children: [ { name: "A", description: "A" }, { name: "B", description: "B" }, ], }, { name: "Two", description: "Two", children: [{ name: "C", description: "C" }] }, ]); state.update([{ id: 3, parentId: 4, position: 0 }]); assert.deepEqual(state.get([4])[0]?.childrenIds, [3, 5]); assert.deepEqual(state.get([1])[0]?.childrenIds, [2]); }); test("removes overlapping parent and child IDs as one cascade", () => { const state = new TaskState(); state.add([ { name: "Root", description: "Root", children: [{ name: "Child", description: "Child", children: [{ name: "Leaf", description: "Leaf" }] }], }, { name: "Keep", description: "Keep" }, ]); const removed = state.remove([1, 2, 1]); assert.deepEqual(removed.map((task) => task.id), [1, 2, 3]); assert.deepEqual(state.list().map((task) => task.id), [4]); assert.equal(state.snapshot().nextId, 5); const before = state.snapshot(); assert.throws(() => state.remove([999]), /do not exist/); assert.deepEqual(state.snapshot(), before); }); test("restores snapshots and never reuses deleted IDs", () => { const state = new TaskState(); state.add([{ name: "One", description: "One" }, { name: "Two", description: "Two" }]); state.remove([2]); const snapshot = state.snapshot(); const restored = new TaskState(); restored.restore(snapshot); const added = restored.add([{ name: "Three", description: "Three" }]); assert.equal(added[0]?.id, 3); }); test("loads the latest valid mutation snapshot from a session branch", () => { const first = new TaskState(); first.add([{ name: "First", description: "First" }]); const second = new TaskState(); second.restore(first.snapshot()); second.add([{ name: "Second", description: "Second" }]); const branch = [first.snapshot(), second.snapshot()].map((snapshot, index) => ({ type: "message", message: { role: "toolResult", toolName: index === 0 ? "add_task" : "update_task", details: { action: "add", snapshot, affected: [] }, }, })); const state = new TaskState(); state.loadFromSession({ sessionManager: { getBranch: () => branch } } as never); assert.deepEqual(state.list().map((task) => task.name), ["First", "Second"]); }); test("rejects multiline names while allowing multiline descriptions", () => { const state = new TaskState(); assert.throws(() => state.add([{ name: "Bad\nname", description: "Valid" }]), /single line/); state.add([{ name: "Valid", description: "Line one\nLine two" }]); assert.equal(state.list()[0]?.description, "Line one\nLine two"); });