fix(tasktree): complete tasks when reports are written
This commit is contained in:
@@ -6,7 +6,6 @@ export function createTaskTreeActivities(store: TaskTreeStore) {
|
|||||||
const task = await store.getTask(input.taskId);
|
const task = await store.getTask(input.taskId);
|
||||||
if (!task) throw Object.assign(new Error("task was not found"), { code: "task_not_found" });
|
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" });
|
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 };
|
return { taskId: task.id, reportId: report.id };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+64
-26
@@ -57,6 +57,17 @@ const schemaMigrations: SchemaMigration[] = [
|
|||||||
error_code TEXT NOT NULL DEFAULT '', error_message TEXT NOT NULL DEFAULT '',
|
error_code TEXT NOT NULL DEFAULT '', error_message TEXT NOT NULL DEFAULT '',
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now())`,
|
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 schemaMigrationIds = schemaMigrations.map((migration) => migration.id);
|
||||||
const schemaAdvisoryLockKeys = [0x48574c42, 0x54545245] as const;
|
const schemaAdvisoryLockKeys = [0x48574c42, 0x54545245] as const;
|
||||||
@@ -346,14 +357,14 @@ export class TaskTreeStore {
|
|||||||
const client = await this.pool.connect();
|
const client = await this.pool.connect();
|
||||||
try {
|
try {
|
||||||
await client.query("BEGIN");
|
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(
|
const result = await client.query(
|
||||||
"INSERT INTO tasktree_execution_reports (id,task_id,title,body,status) VALUES ($1,$2,$3,$4,$5) RETURNING *",
|
"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"]
|
[`tr_${randomUUID()}`, input.taskId, input.title, input.body, input.status ?? "succeeded"]
|
||||||
);
|
);
|
||||||
await client.query(
|
await client.query("UPDATE tasktree_tasks SET status='completed',updated_at=now() WHERE id=$1", [input.taskId]);
|
||||||
"UPDATE tasktree_groups g SET updated_at=now() FROM tasktree_tasks t WHERE t.id=$1 AND g.id=t.group_id",
|
await client.query("UPDATE tasktree_groups SET updated_at=now() WHERE id=$1", [task.rows[0].group_id]);
|
||||||
[input.taskId]
|
|
||||||
);
|
|
||||||
await client.query("COMMIT");
|
await client.query("COMMIT");
|
||||||
return reportRow(result.rows[0]);
|
return reportRow(result.rows[0]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -376,29 +387,55 @@ export class TaskTreeStore {
|
|||||||
return result.rows[0] ? reportRow(result.rows[0]) : null;
|
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();
|
await this.ensureSchema();
|
||||||
const task = await this.getTask(input.taskId);
|
const client = await this.pool.connect();
|
||||||
if (!task) throw domainError("task_not_found", "task was not found");
|
try {
|
||||||
const current = await this.pool.query(
|
await client.query("BEGIN");
|
||||||
"SELECT * FROM tasktree_execution_reports WHERE task_id=$1 AND title=$2 ORDER BY created_at DESC,id LIMIT 1",
|
const taskResult = await client.query("SELECT * FROM tasktree_tasks WHERE id=$1 FOR UPDATE", [input.taskId]);
|
||||||
[input.taskId, input.title]
|
if (!taskResult.rows[0]) throw domainError("task_not_found", "task was not found");
|
||||||
);
|
const current = await client.query(
|
||||||
const status = input.status ?? "succeeded";
|
"SELECT * FROM tasktree_execution_reports WHERE task_id=$1 AND title=$2 ORDER BY created_at DESC,id LIMIT 1",
|
||||||
if (current.rows[0] && current.rows[0].body === input.body && current.rows[0].status === status) {
|
[input.taskId, input.title]
|
||||||
return { report: reportRow(current.rows[0]), mutation: false };
|
|
||||||
}
|
|
||||||
const result = current.rows[0]
|
|
||||||
? await this.pool.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(
|
|
||||||
"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]);
|
const status = input.status ?? "succeeded";
|
||||||
return { report: reportRow(result.rows[0]), mutation: true };
|
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 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]
|
||||||
|
);
|
||||||
|
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) {
|
async importMdtodo(plan: MdtodoImportPlan) {
|
||||||
@@ -422,7 +459,8 @@ export class TaskTreeStore {
|
|||||||
await client.query(
|
await client.query(
|
||||||
`INSERT INTO tasktree_tasks (id,group_id,parent_id,kind,title,description,status,start_at,due_at,sort_order)
|
`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)`,
|
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) {
|
for (const report of task.reports) {
|
||||||
await client.query(
|
await client.query(
|
||||||
|
|||||||
@@ -166,11 +166,20 @@ test("native PostgreSQL store exposes overview and three task levels", async ()
|
|||||||
(error: any) => error?.code === "execution_report_required"
|
(error: any) => error?.code === "execution_report_required"
|
||||||
);
|
);
|
||||||
const firstReport = await store.writeReport({ taskId: task.id, title: "Execution", body: "passed" });
|
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" });
|
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" });
|
const changedReport = await store.writeReport({ taskId: task.id, title: "Execution", body: "passed with evidence" });
|
||||||
assert.equal(firstReport.mutation, true);
|
assert.equal(firstReport.mutation, true);
|
||||||
assert.equal(repeatedReport.mutation, false);
|
assert.equal(repeatedReport.mutation, false);
|
||||||
assert.equal(changedReport.mutation, true);
|
assert.equal(changedReport.mutation, true);
|
||||||
|
assert.equal(changedReport.taskMutation, false);
|
||||||
assert.equal(firstReport.report.id, repeatedReport.report.id);
|
assert.equal(firstReport.report.id, repeatedReport.report.id);
|
||||||
assert.equal(firstReport.report.id, changedReport.report.id);
|
assert.equal(firstReport.report.id, changedReport.report.id);
|
||||||
assert.equal((await store.completeTask(task.id))?.status, "completed");
|
assert.equal((await store.completeTask(task.id))?.status, "completed");
|
||||||
|
|||||||
Reference in New Issue
Block a user