fix(tasktree): complete tasks when reports are written

This commit is contained in:
root
2026-07-21 14:09:06 +02:00
parent 50c693fc50
commit bb5452930b
3 changed files with 73 additions and 27 deletions
-1
View File
@@ -6,7 +6,6 @@ export function createTaskTreeActivities(store: TaskTreeStore) {
const task = await store.getTask(input.taskId);
if (!task) throw Object.assign(new Error("task was not found"), { code: "task_not_found" });
const { report } = await store.writeReport({ taskId: task.id, title: "Temporal execution report", body: `Task ${task.title} completed by Temporal worker.`, status: "succeeded" });
await store.completeTask(task.id);
return { taskId: task.id, reportId: report.id };
}
};
+55 -17
View File
@@ -57,6 +57,17 @@ const schemaMigrations: SchemaMigration[] = [
error_code TEXT NOT NULL DEFAULT '', error_message TEXT NOT NULL DEFAULT '',
updated_at TIMESTAMPTZ NOT NULL DEFAULT now())`,
] },
{ id: "tasktree-20260721-v4-report-completes-task", statements: [
`UPDATE tasktree_groups g SET updated_at=now()
WHERE EXISTS (
SELECT 1 FROM tasktree_tasks t
JOIN tasktree_execution_reports r ON r.task_id=t.id
WHERE t.group_id=g.id AND t.status <> 'completed'
)`,
`UPDATE tasktree_tasks t SET status='completed',updated_at=now()
WHERE t.status <> 'completed'
AND EXISTS (SELECT 1 FROM tasktree_execution_reports r WHERE r.task_id=t.id)`,
] },
];
const schemaMigrationIds = schemaMigrations.map((migration) => migration.id);
const schemaAdvisoryLockKeys = [0x48574c42, 0x54545245] as const;
@@ -346,14 +357,14 @@ export class TaskTreeStore {
const client = await this.pool.connect();
try {
await client.query("BEGIN");
const task = await client.query("SELECT group_id FROM tasktree_tasks WHERE id=$1 FOR UPDATE", [input.taskId]);
if (!task.rows[0]) throw domainError("task_not_found", "task was not found");
const result = await client.query(
"INSERT INTO tasktree_execution_reports (id,task_id,title,body,status) VALUES ($1,$2,$3,$4,$5) RETURNING *",
[`tr_${randomUUID()}`, input.taskId, input.title, input.body, input.status ?? "succeeded"]
);
await client.query(
"UPDATE tasktree_groups g SET updated_at=now() FROM tasktree_tasks t WHERE t.id=$1 AND g.id=t.group_id",
[input.taskId]
);
await client.query("UPDATE tasktree_tasks SET status='completed',updated_at=now() WHERE id=$1", [input.taskId]);
await client.query("UPDATE tasktree_groups SET updated_at=now() WHERE id=$1", [task.rows[0].group_id]);
await client.query("COMMIT");
return reportRow(result.rows[0]);
} catch (error) {
@@ -376,29 +387,55 @@ export class TaskTreeStore {
return result.rows[0] ? reportRow(result.rows[0]) : null;
}
async writeReport(input: { taskId: string; title: string; body: string; status?: string }): Promise<{ report: ExecutionReport; mutation: boolean }> {
async writeReport(input: { taskId: string; title: string; body: string; status?: string }): Promise<{
report: ExecutionReport;
mutation: boolean;
task: TaskItem;
taskMutation: boolean;
}> {
await this.ensureSchema();
const task = await this.getTask(input.taskId);
if (!task) throw domainError("task_not_found", "task was not found");
const current = await this.pool.query(
const client = await this.pool.connect();
try {
await client.query("BEGIN");
const taskResult = await client.query("SELECT * FROM tasktree_tasks WHERE id=$1 FOR UPDATE", [input.taskId]);
if (!taskResult.rows[0]) throw domainError("task_not_found", "task was not found");
const current = await client.query(
"SELECT * FROM tasktree_execution_reports WHERE task_id=$1 AND title=$2 ORDER BY created_at DESC,id LIMIT 1",
[input.taskId, input.title]
);
const status = input.status ?? "succeeded";
if (current.rows[0] && current.rows[0].body === input.body && current.rows[0].status === status) {
return { report: reportRow(current.rows[0]), mutation: false };
}
const result = current.rows[0]
? await this.pool.query(
const mutation = !current.rows[0] || current.rows[0].body !== input.body || current.rows[0].status !== status;
const reportResult = !mutation
? current
: current.rows[0]
? await client.query(
"UPDATE tasktree_execution_reports SET body=$2,status=$3,created_at=now() WHERE id=$1 RETURNING *",
[current.rows[0].id, input.body, status]
)
: await this.pool.query(
: await client.query(
"INSERT INTO tasktree_execution_reports (id,task_id,title,body,status) VALUES ($1,$2,$3,$4,$5) RETURNING *",
[`tr_${randomUUID()}`, input.taskId, input.title, input.body, status]
);
await this.pool.query("UPDATE tasktree_groups SET updated_at=now() WHERE id=$1", [task.groupId]);
return { report: reportRow(result.rows[0]), mutation: true };
const taskMutation = taskResult.rows[0].status !== "completed";
const completedTask = taskMutation
? await client.query("UPDATE tasktree_tasks SET status='completed',updated_at=now() WHERE id=$1 RETURNING *", [input.taskId])
: taskResult;
if (mutation || taskMutation) {
await client.query("UPDATE tasktree_groups SET updated_at=now() WHERE id=$1", [taskResult.rows[0].group_id]);
}
await client.query("COMMIT");
return {
report: reportRow(reportResult.rows[0]),
mutation,
task: taskRow(completedTask.rows[0]),
taskMutation,
};
} catch (error) {
await client.query("ROLLBACK").catch(() => {});
throw error;
} finally {
client.release();
}
}
async importMdtodo(plan: MdtodoImportPlan) {
@@ -422,7 +459,8 @@ export class TaskTreeStore {
await client.query(
`INSERT INTO tasktree_tasks (id,group_id,parent_id,kind,title,description,status,start_at,due_at,sort_order)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
[taskId, groupId, parentId, kind, task.title, task.description, task.status, task.startAt, task.dueAt, index]
[taskId, groupId, parentId, kind, task.title, task.description,
task.reports.length > 0 ? "completed" : task.status, task.startAt, task.dueAt, index]
);
for (const report of task.reports) {
await client.query(
+9
View File
@@ -166,11 +166,20 @@ test("native PostgreSQL store exposes overview and three task levels", async ()
(error: any) => error?.code === "execution_report_required"
);
const firstReport = await store.writeReport({ taskId: task.id, title: "Execution", body: "passed" });
assert.equal(firstReport.task.status, "completed");
assert.equal(firstReport.taskMutation, true);
const repeatedReport = await store.writeReport({ taskId: task.id, title: "Execution", body: "passed" });
assert.equal(repeatedReport.taskMutation, false);
await store.updateTask(task.id, { status: "blocked" });
const correctedReport = await store.writeReport({ taskId: task.id, title: "Execution", body: "passed" });
assert.equal(correctedReport.mutation, false);
assert.equal(correctedReport.taskMutation, true);
assert.equal(correctedReport.task.status, "completed");
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(changedReport.taskMutation, false);
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");