Added a task state reconciliation script (`scripts/reconcile-task-state-consistency.mjs`) with tests to detect and resolve inconsistencies between the task database and git worktree status, with support for explicit project directories and documented usage in the task management docs. Fusion-Task-Id: FN-4000
107 lines
3.0 KiB
JavaScript
107 lines
3.0 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { findTaskStateInconsistencies, runReconciliation } from "../reconcile-task-state-consistency.mjs";
|
|
|
|
function createStore(tasks) {
|
|
const state = new Map(tasks.map((task) => [task.id, { ...task }]));
|
|
const calls = { moveTask: 0, logEntry: 0 };
|
|
|
|
return {
|
|
calls,
|
|
async listTasks() {
|
|
return Array.from(state.values()).map((task) => ({ ...task }));
|
|
},
|
|
async moveTask(id, toColumn) {
|
|
calls.moveTask += 1;
|
|
const task = state.get(id);
|
|
assert.ok(task);
|
|
assert.equal(toColumn, "done");
|
|
if (task.column === "done") {
|
|
task.status = undefined;
|
|
task.error = undefined;
|
|
task.worktree = undefined;
|
|
task.blockedBy = undefined;
|
|
task.recoveryRetryCount = undefined;
|
|
task.nextRecoveryAt = undefined;
|
|
}
|
|
return { ...task };
|
|
},
|
|
async logEntry(id, action, outcome) {
|
|
calls.logEntry += 1;
|
|
assert.equal(action, "FN-4000 reconciliation");
|
|
assert.match(outcome, /FN-4000 reconciliation/);
|
|
return { id };
|
|
},
|
|
getTask(id) {
|
|
return state.get(id);
|
|
},
|
|
};
|
|
}
|
|
|
|
test("detects known bad done/failed fixture", () => {
|
|
const issues = findTaskStateInconsistencies({
|
|
id: "FN-X",
|
|
column: "done",
|
|
status: "failed",
|
|
error: "oops",
|
|
worktree: "wt",
|
|
});
|
|
|
|
assert.deepEqual(issues, [
|
|
"done-task-has-transient-failure-state",
|
|
"failed-status-outside-in-review",
|
|
]);
|
|
});
|
|
|
|
test("dry-run reports inconsistency without mutating", async () => {
|
|
const store = createStore([
|
|
{ id: "FN-1", column: "done", status: "failed", error: "bad" },
|
|
{ id: "FN-2", column: "in-review", status: "failed" },
|
|
]);
|
|
|
|
const result = await runReconciliation({ store, dryRun: true });
|
|
|
|
assert.equal(result.findings.length, 1);
|
|
assert.equal(result.findings[0].taskId, "FN-1");
|
|
assert.equal(store.calls.moveTask, 0);
|
|
assert.equal(store.calls.logEntry, 0);
|
|
assert.equal(store.getTask("FN-1").status, "failed");
|
|
});
|
|
|
|
test("apply reconciles done task and emits exactly one note", async () => {
|
|
const store = createStore([
|
|
{
|
|
id: "FN-3990",
|
|
column: "done",
|
|
status: "failed",
|
|
error: "stale",
|
|
worktree: "worktrees/old",
|
|
blockedBy: "FN-1",
|
|
recoveryRetryCount: 2,
|
|
nextRecoveryAt: "2026-05-11T00:00:00.000Z",
|
|
},
|
|
]);
|
|
|
|
const result = await runReconciliation({
|
|
store,
|
|
dryRun: false,
|
|
noteByTaskId: {
|
|
"FN-3990": "FN-4000 reconciliation: custom note",
|
|
},
|
|
});
|
|
|
|
assert.equal(result.findings.length, 1);
|
|
assert.equal(result.actions[0].action, "reconciled");
|
|
assert.equal(store.calls.moveTask, 1);
|
|
assert.equal(store.calls.logEntry, 1);
|
|
|
|
const task = store.getTask("FN-3990");
|
|
assert.equal(task.status, undefined);
|
|
assert.equal(task.error, undefined);
|
|
assert.equal(task.worktree, undefined);
|
|
assert.equal(task.blockedBy, undefined);
|
|
assert.equal(task.recoveryRetryCount, undefined);
|
|
assert.equal(task.nextRecoveryAt, undefined);
|
|
});
|