feat: complete TaskTree CLI parity
This commit is contained in:
@@ -29,6 +29,31 @@ test("HTTP command endpoint returns the same dispatcher DTO", async () => {
|
||||
assert.deepEqual(await response.json(), expected);
|
||||
});
|
||||
|
||||
test("dispatcher keeps batch validation atomic and completion gated", async () => {
|
||||
let batchCalls = 0;
|
||||
let updateCalls = 0;
|
||||
const store = {
|
||||
async createTasks() { batchCalls += 1; return []; },
|
||||
async updateTask() { updateCalls += 1; return null; },
|
||||
async deleteTask() { return false; }
|
||||
};
|
||||
const dispatch = createTaskTreeDispatcher({ store: store as TaskTreeStore });
|
||||
|
||||
const batch = await dispatch({ operation: "task.create-batch", groupId: "tg_1", titles: ["valid", "invalid\nline"] });
|
||||
assert.equal(batch.ok, false);
|
||||
assert.equal(batch.error?.code, "invalid_title");
|
||||
assert.equal(batchCalls, 0);
|
||||
|
||||
const bypass = await dispatch({ operation: "task.update", taskId: "tt_1", status: "completed" });
|
||||
assert.equal(bypass.ok, false);
|
||||
assert.equal(bypass.error?.code, "completion_command_required");
|
||||
assert.equal(updateCalls, 0);
|
||||
|
||||
const missing = await dispatch({ operation: "task.delete", taskId: "tt_missing" });
|
||||
assert.equal(missing.ok, false);
|
||||
assert.equal(missing.error?.code, "task_not_found");
|
||||
});
|
||||
|
||||
test("native PostgreSQL store exposes overview and three task levels", async () => {
|
||||
const databaseUrl = process.env.TASKTREE_DATABASE_URL;
|
||||
assert.ok(databaseUrl, "TASKTREE_DATABASE_URL is required for the TaskTree store integration test");
|
||||
@@ -43,23 +68,50 @@ test("native PostgreSQL store exposes overview and three task levels", async ()
|
||||
});
|
||||
const subtask = await store.createTask({ groupId: group.id, parentId: task.id, title: "Subtask" });
|
||||
const subsubtask = await store.createTask({ groupId: group.id, parentId: subtask.id, title: "Subsubtask" });
|
||||
const batch = await store.createTasks({ groupId: group.id, parentId: task.id, titles: ["Batch one", "Batch two"] });
|
||||
assert.deepEqual(batch.map((item) => item.title), ["Batch one", "Batch two"]);
|
||||
await store.createMilestone({ groupId: group.id, taskId: task.id, title: "Review", occursAt: "2026-07-17T00:00:00.000Z" });
|
||||
await store.createReport({ taskId: subtask.id, title: "Execution", body: "passed" });
|
||||
await assert.rejects(
|
||||
store.completeTask(task.id),
|
||||
(error: any) => error?.code === "execution_report_required"
|
||||
);
|
||||
const firstReport = await store.writeReport({ taskId: task.id, title: "Execution", body: "passed" });
|
||||
const repeatedReport = await store.writeReport({ taskId: task.id, title: "Execution", body: "passed" });
|
||||
const changedReport = await store.writeReport({ taskId: task.id, title: "Execution", body: "passed with evidence" });
|
||||
assert.equal(firstReport.mutation, true);
|
||||
assert.equal(repeatedReport.mutation, false);
|
||||
assert.equal(changedReport.mutation, true);
|
||||
assert.equal(firstReport.report.id, repeatedReport.report.id);
|
||||
assert.equal(firstReport.report.id, changedReport.report.id);
|
||||
assert.equal((await store.completeTask(task.id))?.status, "completed");
|
||||
|
||||
await assert.rejects(
|
||||
store.createTask({ groupId: group.id, parentId: subsubtask.id, title: "Unsupported fourth level" }),
|
||||
(error: any) => error?.code === "maximum_depth_exceeded"
|
||||
);
|
||||
const countBeforeRejectedBatch = (await store.listTasks(group.id)).length;
|
||||
await assert.rejects(
|
||||
store.createTasks({ groupId: group.id, parentId: subsubtask.id, titles: ["Unsupported one", "Unsupported two"] }),
|
||||
(error: any) => error?.code === "maximum_depth_exceeded"
|
||||
);
|
||||
assert.equal((await store.listTasks(group.id)).length, countBeforeRejectedBatch);
|
||||
const timeline = await store.timeline(group.id);
|
||||
assert.equal(timeline?.tasks.length, 3);
|
||||
assert.equal(timeline?.tasks.length, 5);
|
||||
assert.equal(timeline?.tasks[0]?.kind, "task");
|
||||
assert.equal(timeline?.tasks[1]?.kind, "subtask");
|
||||
assert.equal(timeline?.tasks[2]?.kind, "subsubtask");
|
||||
assert.equal(timeline?.milestones.length, 1);
|
||||
assert.equal(timeline?.reports.length, 1);
|
||||
assert.equal((await store.getTask(subtask.id))?.title, "Subtask");
|
||||
assert.equal((await store.listReports(task.id)).length, 1);
|
||||
assert.equal((await store.getReport(firstReport.report.id))?.body, "passed with evidence");
|
||||
const stats = await store.groupStats(group.id);
|
||||
assert.equal(stats.total, 5);
|
||||
assert.equal(stats.completed, 1);
|
||||
assert.equal(stats.reports, 1);
|
||||
const overview = (await store.groupOverview()).find((item) => item.group.id === group.id);
|
||||
assert.equal(overview?.taskCount, 1);
|
||||
assert.equal(overview?.subtaskCount, 1);
|
||||
assert.equal(overview?.subtaskCount, 3);
|
||||
assert.equal(overview?.subsubtaskCount, 1);
|
||||
assert.equal(overview?.reportCount, 1);
|
||||
assert.equal(overview?.startAt, "2026-07-16T00:00:00.000Z");
|
||||
|
||||
Reference in New Issue
Block a user