fix(tasktree): project lifecycle times into timelines

This commit is contained in:
root
2026-07-21 08:59:31 +02:00
parent 9c8cc97632
commit 3fa0a7ec17
2 changed files with 105 additions and 6 deletions
+37 -3
View File
@@ -3,7 +3,7 @@ import { test } from "bun:test";
import { createTaskTreeDispatcher } from "./dispatcher.ts";
import { createTaskTreeHttpApp } from "./http.ts";
import { TaskTreeStore } from "./store.ts";
import { projectTimelineTasks, TaskTreeStore } from "./store.ts";
test("HTTP command endpoint returns the same dispatcher DTO", async () => {
const group = {
@@ -71,6 +71,35 @@ test("dispatcher keeps batch validation atomic and completion gated", async () =
assert.equal(workflow.error?.code, "temporal_address_required");
});
test("timeline projects lifecycle timestamps and expands parents around descendants", () => {
const tasks = projectTimelineTasks([
{
id: "tt_root", groupId: "tg_1", parentId: null, kind: "task", title: "Root", description: "", status: "in_progress",
startAt: "2026-07-21T10:00:00.000Z", dueAt: "2026-07-21T11:00:00.000Z", sortOrder: 0,
createdAt: "2026-07-21T10:00:00.000Z", updatedAt: "2026-07-21T11:00:00.000Z"
},
{
id: "tt_child", groupId: "tg_1", parentId: "tt_root", kind: "subtask", title: "Child", description: "", status: "completed",
startAt: null, dueAt: null, sortOrder: 1,
createdAt: "2026-07-21T09:00:00.000Z", updatedAt: "2026-07-21T12:00:00.000Z"
},
{
id: "tt_grandchild", groupId: "tg_1", parentId: "tt_child", kind: "subsubtask", title: "Grandchild", description: "", status: "pending",
startAt: null, dueAt: "2026-07-21T15:00:00.000Z", sortOrder: 2,
createdAt: "2026-07-21T13:00:00.000Z", updatedAt: "2026-07-21T13:00:00.000Z"
}
], [{
id: "tr_1", taskId: "tt_child", title: "Execution", body: "done", status: "succeeded",
createdAt: "2026-07-21T14:00:00.000Z"
}]);
assert.deepEqual(tasks.map(({ id, startAt, dueAt }) => ({ id, startAt, dueAt })), [
{ id: "tt_root", startAt: "2026-07-21T09:00:00.000Z", dueAt: "2026-07-21T15:00:00.000Z" },
{ id: "tt_child", startAt: "2026-07-21T09:00:00.000Z", dueAt: "2026-07-21T15:00:00.000Z" },
{ id: "tt_grandchild", startAt: "2026-07-21T13:00:00.000Z", dueAt: "2026-07-21T15:00:00.000Z" }
]);
});
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");
@@ -146,6 +175,11 @@ test("native PostgreSQL store exposes overview and three task levels", async ()
assert.equal(timeline?.tasks[2]?.kind, "subsubtask");
assert.equal(timeline?.milestones.length, 1);
assert.equal(timeline?.reports.length, 1);
assert.ok(timeline?.tasks.every((item) => item.startAt !== null && item.dueAt !== null));
const timelineStart = Math.min(...timeline!.tasks.map((item) => new Date(item.startAt!).valueOf()));
const timelineDue = Math.max(...timeline!.tasks.map((item) => new Date(item.dueAt!).valueOf()));
assert.equal(timeline?.tasks[0]?.startAt, new Date(timelineStart).toISOString());
assert.equal(timeline?.tasks[0]?.dueAt, new Date(timelineDue).toISOString());
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");
@@ -158,8 +192,8 @@ test("native PostgreSQL store exposes overview and three task levels", async ()
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");
assert.equal(overview?.dueAt, "2026-07-18T00:00:00.000Z");
assert.equal(overview?.startAt, new Date(timelineStart).toISOString());
assert.equal(overview?.dueAt, new Date(timelineDue).toISOString());
} finally {
await store.deleteGroup(otherGroup.id);
await store.deleteGroup(group.id);