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:
gsxdsm
2026-07-31 13:00:22 -07:00
committed by GitHub
parent 623581837a
commit fb8d37e30d
2 changed files with 162 additions and 0 deletions

View File

@@ -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"]);
});
});

View File

@@ -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" });