test(diarie): the tests were never in the package they test — `pass 0` said so
`npm test --workspace=diarie` reported **pass 0** while 1,238 lines of tests for it
sat in the plugin's scripts/. A package about to be published, whose own test command
was vacuously green. A subtree split would have shipped it with no tests at all.
161 assertions move into diarie/test/{ready,validate,cli,migrate}.spec.js as node:test.
Parity is exact and per-file — 40/36/54/31 before, 40/36/54/31 after — and each suite is
mutation-proven able to go RED. `pass 161` would prove nothing on its own; `pass 0` also
passed.
The actual extraction blocker was one line: the migrator tests read their fixture from
.diarie/_archive/bd-final-export.jsonl, the PLUGIN's frozen archive, which cannot exist
in an extracted package. It is replaced by a synthetic test/fixtures/bd-export.jsonl —
19 records, 4.9 KB against 272 KB — that covers strictly more: every TYPE_MAP entry,
both edge-drop paths, an unmappable priority, the escaped-newline body.
Three silent defects surfaced while wiring it up. Each would have passed green:
- type-coverage's --ignore-files resolves from CWD, not the tsconfig dir. The
template's 'test/*' is a NO-OP here (5439/5593 with or without it) AND drops
coverage to 97.24%, under the 98 gate — which invites cutting the threshold.
It has to be 'diarie/test/*'. Measured, not assumed.
- check-ast-grep.mjs scans an explicit path list, so a rule scoped to diarie/test/
would have been green and INERT. check:ast-grep-test cannot catch that: `ast-grep
test` replays a rule against its own snapshots and never walks the tree. The
directory is added to the runner, and the rule is proven to fire by planting a
duplicate and watching CI go red.
- monkey-patching process.stdout.write under `node --test` swallows the runner's own
TAP stream. It silently dropped 40 of cli.spec.js's 54 results while still printing
"pass". Replaced with a subprocess; the guarantee (cli() must read its own argv, not
process.argv) survives, and tsc already catches that bug anyway — verified by
mutation: TS2353.
Putting test/** into diarie/tsconfig.json type-checks the specs for the first time in
their existence, and it immediately found real gaps: fixtures minting bare ids against
the GlobalId brand, and groupTasks fixtures that were not TaskRows.
New rule: no-identical-test-title. node:test runs BOTH copies of a duplicated it()
title — no warning, no dedup, one extra passing test. ast-grep unifies metavariables
across `follows`, and `follows` is sibling-scoped, so per-describe scoping is free. The
proper tool (eslint-node-test) is UNINSTALLABLE here: its peer range `eslint >=10.4` is
disjoint from @voxpelli/eslint-config@25's `^9.38.0`. Reported: voxpelli/eslint-config#476.
Filed vp-beads-asg: both diarie-owned ast-grep rules live at the repo root and would be
silently lost by a subtree split — the failure mode the rules exist to prevent, applied
to the rules themselves.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(diarie): an epic is not work — and the fix that would have gone green
`diarie ready` led with the migration epic: a container with three open
children, offered at high priority as the next thing to work on. bd excluded
containers structurally because it had an `epic` TYPE; collapsing epic to
`task` + `parent:` is the better model, but it moved that check into logic
nobody wrote.
The fix was nearly a no-op that passed its own tests. `loadTasks` globalized
`id` and `deps` into the `slug/id` namespace and handed `parent` back RAW, so
`parent` could never equal any `id`. Indexing children by parent would have
found ZERO children for every epic, excluded nothing, changed no behaviour,
and gone green — including under a unit test, which writes `id` and `parent`
by hand in one consistent id-space and so structurally cannot see the
incoherence.
Root cause was the id rule existing twice: `store.nsId` and a private,
byte-identical `validate.glob`. One copy applied it to `parent`; the other
forgot. It now lives once, in schema.js, and `loadTasks` hands out a single
coherent id-space.
Behaviour:
- a parent with open children (status != completed) is blocked BY those
children, in its own `children:` field — `blockers` still means deps, which
must FINISH FIRST, versus children, which are CONTAINED
- an `epic`-labelled task with no open children surfaces in needsAttention
("close it or add children") rather than vanishing
- a parent whose children are ALL completed is ready again — only open
children contain work
- the "0 ready, N blocked → possible cycle" hint now counts dep-blocked rows
only; a tree of finished leaves is not a broken graph
- self-parenting is a validation error (it would block a task on itself)
Verified by mutation, not assertion count: reverting the namespacing turns 4
rows red. That also exposed two tests of mine that were green for the wrong
reason — `!inReady(epic)` passes on a labelled epic even with containment
ripped out, and a cross-file parent is already slug-qualified so it never
needed the fix at all. Both retargeted at the bare-parent shape the live store
actually uses.
Container fixtures live in their own store (`test/fixtures-epics/`): folding
them into `test/fixtures/` would have forced a rewrite of that suite's passing
count assertions, and editing a green assertion to make room for your own
change is how a regression gets waved through.
Live store: 24 total / 17 ready / 0 blocked → 24 / 16 / 1, with the epic's
three children still ready. Closes vp-beads-epc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>