test(core): pin the last two uncovered lane reads (mission archive, lineage gate) (#3235)
## What Pins the **last two uncovered lane reads** in `packages/core`. Test-only. This closes the per-site core audit. | site | what it decides | |---|---| | `async-mission-store.ts:1179` | is an ARCHIVED card valid terminal evidence for mission repair? | | `task-id-integrity.ts:502` | does an archived child still count as a LIVE lineage child? | ## Measured ``` mission-store: 39 passed clean; 1 failed | 38 passed blinded lineage: 3 passed clean; 1 failed | 2 passed blinded lint clean; fnxc-future-dates: none added; census unchanged ``` Both blinds confirmed applied with `git diff --stat` before each run. ## The third adjacent-pair split `:1179` is the **archived** half of a pair whose **complete** half (`:1178`, *one line above*) was already covered by a test in the same file, written for exactly this concern. Terminal evidence is "done OR supported archived state," so an archived card is equally valid repair evidence — but on a board whose archive lane is `vaulted` the archived half could not see it, and reconciliation threw `TASK_NOT_TERMINAL` for a card that was genuinely filed away. Same refusal the covered case fixed, reached through the other door. That is now the third confirmed instance in core (after `team-analytics` in #3227 and the scheduler pair earlier). **Being adjacent to a covered resolver is not coverage**, and it is the most reliable place to look. ## What breaks without the lineage read An archived child is filed away, not live, so it must not hold the delete gate shut. Renamed, it still counted as live and `TaskHasLineageChildrenError` blocked the parent's delete **forever** — the operator archived the child *precisely* to clear the way, and the gate could not see that they had. ## A fixture detail I got wrong first My first mission fixture created a live card in a `vaulted` column and failed with `deleted or archived without a valid retained tombstone and archive snapshot` — nothing to do with the lane read. The `archived` verdict requires **all three** of `deletedAt !== null`, an archive-snapshot row, and `isArchived(column)`. A live card merely sitting in an archive-trait column is `invalid-deleted`, not `archived`. The test now archives for real and *then* renames the recorded lane, which isolates the third condition — the only one under test. Recorded in the file so the next person does not re-derive it. ## Paired positives Both files pin the complement: a WORKING child still counts as live. Recognising the renamed archive lane must not degrade into "no child is ever live" — that would silently **disable** the lineage gate and let a parent be deleted out from under real descendants, which is worse than the bug being fixed. ## Core audit complete **14 sites blinded individually: 9 already covered, 5 uncovered, all 5 now pinned** (#3233, #3234, this PR). Every `resolveProjectColumnsForRoles` call site in `packages/engine` and `packages/core` has now been blinded. Remaining unaudited: `dashboard` (2 files) and `cli` (1) — I claim nothing about those.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-23:45:
|
||||
THE LINEAGE-INTEGRITY GATE'S ARCHIVE READ, on a RENAMED board.
|
||||
|
||||
`findLiveLineageChildren` answers "does this parent still have LIVE children?" — the gate that
|
||||
refuses to delete a parent while lineage descendants remain. An ARCHIVED child is filed away, not
|
||||
live, so it must not hold the gate shut.
|
||||
|
||||
WHY THIS FILE EXISTS. The archive read was converted to
|
||||
`resolveProjectColumnsForRoles(store, ["archived"])`, and blinding it back to the legacy id left the
|
||||
whole 16-file lane-detector set green. `store.findLiveLineageChildren` is public and nothing in
|
||||
`packages/core` exercises it against a renamed board.
|
||||
|
||||
WHAT BREAKS WITHOUT THE CONVERSION. On a board whose archive lane is `vaulted`, an archived child is
|
||||
not recognised as archived, so it still counts as live and `TaskHasLineageChildrenError` blocks the
|
||||
parent's delete forever. The operator archived the child precisely to clear the way, and the gate
|
||||
cannot see that they did. This is the renamed-board twin of the defect #3162 fixed.
|
||||
|
||||
DIFFERENTIAL. Same seeded rows under two vocabularies with identical traits; only the ids differ, and
|
||||
`vaulted` collides with no legacy id. The default-vocabulary case is the control.
|
||||
*/
|
||||
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "vitest";
|
||||
import { sql } from "drizzle-orm";
|
||||
import {
|
||||
createSharedPgTaskStoreTestHarness,
|
||||
pgDescribe,
|
||||
type SharedPgTaskStoreHarness,
|
||||
} from "../../__test-utils__/pg-test-harness.js";
|
||||
import { BUILTIN_CODING_WORKFLOW_IR } from "../../builtin-coding-workflow-ir.js";
|
||||
|
||||
pgDescribe("findLiveLineageChildren under a renamed board vocabulary", () => {
|
||||
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||
prefix: "fusion_lineage_children_lanes",
|
||||
});
|
||||
|
||||
beforeAll(h.beforeAll);
|
||||
beforeEach(h.beforeEach);
|
||||
afterEach(h.afterEach);
|
||||
afterAll(h.afterAll);
|
||||
|
||||
async function seedRenamedWorkflow(): Promise<void> {
|
||||
const RENAME: Record<string, string> = {
|
||||
todo: "drafting",
|
||||
"in-progress": "building",
|
||||
"in-review": "checking",
|
||||
done: "shipped",
|
||||
archived: "vaulted",
|
||||
};
|
||||
const rename = (id: string | undefined) => (id && RENAME[id]) ?? id;
|
||||
const ir = JSON.parse(JSON.stringify(BUILTIN_CODING_WORKFLOW_IR)) as {
|
||||
id: string;
|
||||
nodes?: { column?: string }[];
|
||||
columns?: { id: string }[];
|
||||
};
|
||||
ir.id = "custom:renamed-lineage";
|
||||
for (const node of ir.nodes ?? []) node.column = rename(node.column);
|
||||
for (const column of ir.columns ?? []) column.id = rename(column.id) as string;
|
||||
|
||||
const ids = (ir.columns ?? []).map((column) => column.id);
|
||||
expect(ids).toContain("vaulted");
|
||||
expect(ids).not.toContain("archived");
|
||||
|
||||
await h.store().createWorkflowDefinition({ name: "Renamed", kind: "workflow", ir } as never);
|
||||
}
|
||||
|
||||
/** A parent plus one child linked by `source_parent_task_id`, the child parked in `childLane`. */
|
||||
async function seedLineagePair(childLane: string): Promise<void> {
|
||||
const store = h.store();
|
||||
for (const id of ["KB-PARENT", "KB-CHILD"]) {
|
||||
await store.createTaskWithReservedId(
|
||||
{ description: id, column: "todo" },
|
||||
{ taskId: id, applyDefaultWorkflowSteps: false },
|
||||
);
|
||||
}
|
||||
/* Seeded directly: moveTask would reject a target the default workflow does not declare. */
|
||||
await h.adminDb().execute(sql`
|
||||
UPDATE project.tasks
|
||||
SET "column" = ${childLane}, source_parent_task_id = 'KB-PARENT'
|
||||
WHERE id = 'KB-CHILD'`);
|
||||
store.taskCache.delete("KB-CHILD");
|
||||
}
|
||||
|
||||
it("default vocabulary: an ARCHIVED child does not count as live", async () => {
|
||||
await seedLineagePair("archived");
|
||||
|
||||
expect(await h.store().findLiveLineageChildren("KB-PARENT")).toEqual([]);
|
||||
});
|
||||
|
||||
it("renamed vocabulary: a child in the RENAMED archive lane does not count as live", async () => {
|
||||
await seedRenamedWorkflow();
|
||||
await seedLineagePair("vaulted");
|
||||
|
||||
expect(await h.store().findLiveLineageChildren("KB-PARENT")).toEqual([]);
|
||||
});
|
||||
|
||||
/*
|
||||
The paired positive. Recognising the renamed archive lane must not degrade into "no child is ever
|
||||
live" — that would silently disable the lineage gate and let a parent be deleted out from under
|
||||
real descendants, which is worse than the bug being fixed.
|
||||
*/
|
||||
it("renamed vocabulary: a WORKING child still counts as live", async () => {
|
||||
await seedRenamedWorkflow();
|
||||
await seedLineagePair("building");
|
||||
|
||||
expect(await h.store().findLiveLineageChildren("KB-PARENT")).toEqual(["KB-CHILD"]);
|
||||
});
|
||||
});
|
||||
@@ -514,6 +514,60 @@ pgTest("MissionStore (PostgreSQL backend mode)", () => {
|
||||
expect(reconciled).toMatchObject({ taskId: task.id, status: "done" });
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-23:40:
|
||||
THE ARCHIVED HALF OF THE SAME PAIR. The case above pins the `complete` resolver; the `archived`
|
||||
one is declared on the very next line and nothing reached it — blinding it back to `["archived"]`
|
||||
left the whole 16-file lane-detector set green while blinding its neighbour failed immediately.
|
||||
|
||||
Terminal evidence is "done OR supported archived state", so an archived card is equally valid
|
||||
repair evidence. On a board whose archive lane is `vaulted`, the archived half could not see it and
|
||||
the method threw `TASK_NOT_TERMINAL` for a card that was genuinely filed away — the same refusal
|
||||
the case above fixed, reached through the other door.
|
||||
|
||||
Being adjacent to a covered resolver is not coverage; this is the third such split found in core.
|
||||
*/
|
||||
it("accepts an ARCHIVED card whose board calls the archive lane something else", async () => {
|
||||
const m = missions();
|
||||
const store = h.store();
|
||||
await store.createWorkflowDefinition({
|
||||
name: "Renamed archive",
|
||||
ir: {
|
||||
version: "v2",
|
||||
name: "Renamed archive",
|
||||
columns: [
|
||||
{ id: "todo", name: "Todo", traits: [{ trait: "intake" }, { trait: "hold" }] },
|
||||
{ id: "done", name: "Done", traits: [{ trait: "complete" }] },
|
||||
{ id: "vaulted", name: "Vaulted", traits: [{ trait: "archived" }] },
|
||||
],
|
||||
nodes: [
|
||||
{ id: "start", kind: "start", column: "todo" },
|
||||
{ id: "end", kind: "end", column: "done" },
|
||||
],
|
||||
edges: [{ from: "start", to: "end", condition: "success" }],
|
||||
} as never,
|
||||
});
|
||||
const mission = await m.createMission({ title: "Renamed-archive repair" });
|
||||
const milestone = await m.addMilestone(mission.id, { title: "MS" });
|
||||
const slice = await m.addSlice(milestone.id, { title: "SL" });
|
||||
const feature = await m.addFeature(slice.id, { title: "Delivered" });
|
||||
const task = await store.createTask({ description: "filed away", column: "done" });
|
||||
/*
|
||||
A REAL archive, then the lane rename. The `archived` verdict requires all three of
|
||||
`deletedAt !== null`, an archive-snapshot row, and `isArchived(column)` — a live card merely
|
||||
sitting in an archive-trait column is `invalid-deleted`, not `archived`, so seeding one would
|
||||
fail for a reason that has nothing to do with the lane read under test. Archiving first and
|
||||
then renaming the recorded lane isolates exactly the third condition.
|
||||
*/
|
||||
await store.archiveTask(task.id, { cleanup: false });
|
||||
await h.adminDb().execute(sql`UPDATE project.tasks SET "column" = 'vaulted' WHERE id = ${task.id}`);
|
||||
store.taskCache.delete(task.id);
|
||||
|
||||
const reconciled = await m.reconcileFeatureDoneWithTerminalTask(feature.id, task.id);
|
||||
|
||||
expect(reconciled).toMatchObject({ taskId: task.id, status: "done" });
|
||||
});
|
||||
|
||||
it("atomically reconciles live done evidence and remains idempotent", async () => {
|
||||
const m = missions();
|
||||
const mission = await m.createMission({ title: "Parked repair" });
|
||||
|
||||
Reference in New Issue
Block a user