## `main` is currently red on `check-fnxc-future-dates` Four stamps read `2026-07-30-26:10` — an hour that cannot exist. They're exactly what #2995 taught this gate to catch. That PR landed the hour validation (`00-23`) *after* #2999 had already merged these four, so the gate started reporting a defect that was already sitting there rather than one introduced afterwards. **The guard is working**; nothing was checking before it. ``` scripts/lib/backend-db.mjs:41 scripts/reconcile-task-state-consistency.mjs:8, :51 scripts/__tests__/reconcile-task-state-consistency.test.mjs:109 ``` Corrected by **literal normalisation** — 26:10 on the 30th *is* 02:10 on the 31st — rather than flattening them to an arbitrary in-range hour. AGENTS.md specifies `yyyy-MM-dd-hh:mm`, and the stamp exists to give a readable why-does-this-exist trail, so the ordering is the part worth preserving. ## The baseline tightening rides along, and it's a date rollover Stamps written yesterday as `2026-07-31` were future *then* and were baselined as such. Today they're past, so **176 files ratchet to zero**. Nobody did anything. The gate rewrites the baseline as a side effect and exits 0, so leaving it uncommitted dirties the tree on every subsequent run **for everyone** — which is why it belongs in this commit rather than a later one. Re-recording on a decrease is the rule this gate and its siblings already state. Worth knowing about the design, since I wrote it: this churn recurs whenever a day boundary passes with future-dated stamps in the baseline, and it shrinks only as people stop writing them — which is the behaviour the gate exists to produce. **93 files still carry a non-zero allowance**, so the drain isn't finished. If it stays noisy once those clear, the gate's fail-on-tighten contract is the thing to revisit, not the stamps. ## Measured | check | result | |---|---| | gate | red before, **exit 0 after**, stable across two consecutive runs | | baseline | −176/+25 entries, all date-rollover | | inert-seam · sql-literal · lane-wiring · census | all green | | reconciler's own suite | green | ## One correction to a claim I made earlier this session While investigating I reported the gate as hanging for 600s. It wasn't — the harness killed the process (exit 144) and the empty output made it look like a stall. The gate completes in seconds. Noting it because I nearly filed a performance bug against a healthy script. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
169 lines
5.6 KiB
JavaScript
169 lines
5.6 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);
|
|
});
|
|
|
|
/*
|
|
FNXC:OperatorScriptLaneAssumptions 2026-07-31-02:10:
|
|
THE INVARIANT: both consistency checks ask the task's OWN lanes, and they failed in OPPOSITE directions.
|
|
|
|
Keyed on the literals, `hasDoneTransient` (`column === "done"`) NEVER fires on a renamed board, so a
|
|
finished card still holding a worktree and `status:"failed"` goes unreported and unnormalized. Meanwhile
|
|
`failed-status-outside-in-review` (`column !== "in-review"`) fires for EVERY failed card, because no
|
|
column equals the literal — a report listing the whole board, which looks like the tool working.
|
|
|
|
Reverted, the first case returns [] (the miss) and the second returns the spurious flag (the flood).
|
|
*/
|
|
test("reports stale transient state in a RENAMED complete lane", () => {
|
|
/* No `status:"failed"` here on purpose: it would ALSO trip the second check (a failed card outside
|
|
the review lane is genuinely flagged), which would blur which of the two this case is pinning. */
|
|
const task = { id: "FN-R1", column: "shipped", worktree: "/tmp/wt" };
|
|
|
|
assert.deepEqual(
|
|
findTaskStateInconsistencies(task, { complete: "shipped", review: "checking" }),
|
|
["done-task-has-transient-failure-state"],
|
|
);
|
|
});
|
|
|
|
test("does NOT flag a failed card that is sitting in the board's own review lane", () => {
|
|
const task = { id: "FN-R2", column: "checking", status: "failed" };
|
|
|
|
assert.deepEqual(findTaskStateInconsistencies(task, { complete: "shipped", review: "checking" }), []);
|
|
});
|
|
|
|
test("still flags a failed card outside the resolved review lane", () => {
|
|
const task = { id: "FN-R3", column: "building", status: "failed" };
|
|
|
|
assert.deepEqual(
|
|
findTaskStateInconsistencies(task, { complete: "shipped", review: "checking" }),
|
|
["failed-status-outside-in-review"],
|
|
);
|
|
});
|
|
|
|
test("unresolved lanes keep exactly the legacy behaviour", () => {
|
|
assert.deepEqual(
|
|
findTaskStateInconsistencies({ id: "FN-L", column: "done", status: "failed" }),
|
|
["done-task-has-transient-failure-state", "failed-status-outside-in-review"],
|
|
);
|
|
});
|
|
|
|
test("runReconciliation normalizes a renamed complete lane by moving the card to its OWN column", async () => {
|
|
const moves = [];
|
|
const store = {
|
|
async listTasks() { return [{ id: "FN-R4", column: "shipped", status: "failed", worktree: "/tmp/w" }]; },
|
|
async moveTask(id, toColumn) { moves.push([id, toColumn]); return { id }; },
|
|
async logEntry() { return { id: "FN-R4" }; },
|
|
};
|
|
|
|
const result = await runReconciliation({
|
|
store,
|
|
dryRun: false,
|
|
resolveLanes: async () => ({ complete: "shipped", review: "checking" }),
|
|
});
|
|
|
|
assert.deepEqual(moves, [["FN-R4", "shipped"]]);
|
|
assert.equal(result.actions[0].action, "reconciled");
|
|
});
|