Skip to content

test(cli): fingerprint the reference shape, cover the wizard's reload banner - #305

Merged
thecodedrift merged 2 commits into
test/binary-source-and-match-modefrom
test/reference-version-and-wizard-notice
Sep 8, 2026
Merged

test(cli): fingerprint the reference shape, cover the wizard's reload banner#305
thecodedrift merged 2 commits into
test/binary-source-and-match-modefrom
test/reference-version-and-wizard-notice

Conversation

@thecodedrift

@thecodedrift thecodedrift commented Sep 7, 2026

Copy link
Copy Markdown
Member

Stack (root → tip):

Summary

Closes three of the coverage gaps enumerated in #284. Coverage only — no production behavior changed.

1. REFERENCE_VERSION had no structural fingerprint

packages/cli/src/rules/reference.ts:45 was asserted only as expect(reference.version).toBe(2). Nothing tied a shape change in Reference / ReferenceRule / ReferenceTests / ReferenceLayout to bumping that constant — which is exactly the v1 -> v2 episode CLAUDE.md documents: tests went from a flat array to an object and the version moved, but only because someone remembered to move it by hand.

Design. Added shapeFingerprint() in packages/cli/test/reference.test.ts: it walks the live Reference object (read from assets/reference.json, same as every other test in the file) and records one string per reachable position — path:kind, e.g. $.rules[].tests.grouping:string — into a Set. Array positions are recorded as [] (never an index) and object keys are visited in sorted order, so the set of recorded strings is invariant under reordering an array or reordering an object literal's keys. Only the kind of each value is recorded (object / array / string / number / boolean / null), never the value itself, so renaming a rule id or editing a prompt string doesn't move anything. The sorted set is joined and hashed with node:crypto's createHash("sha256") (built-in, no new dependency, matches the style guide's "don't add a dependency to test an assertion").

Why derived rather than hand-copied: a hand-written list of expected fields is exactly the kind of copy the CLAUDE.md episode already burned us with once (a fact transcribed out of the corpus, silently stale). Walking the actual object at test time means the fingerprint always describes what's really there.

This is deliberately a companion to REFERENCE_VERSION, not a replacement: the fingerprint has no opinion on whether a version bump is warranted, only on whether the shape moved without anyone updating the recorded hash (which forces a human to look and decide).

2. Wizard's reload notice had no test

packages/cli/src/wizard/index.ts:91 wires up getReloadNotice identically to packages/cli/src/commands/init.ts:291, whose own test comment names the risk directly ("a dropped console.log or swapped field leaves every unit test green"). Only init had a test for it (init-no-interactive.test.ts, "the restart-your-agents banner" describe block).

Added the mirror-image block to wizard-integration.test.ts — same three cases (prints on a version move, quiet on first install, quiet when unchanged), adapted to runWizard's in-process/mocked-clack shape rather than spawning the built CLI: spies on console.log, plants a stale install.cliVersion between two runWizard calls the same way the init test plants it between two CLI invocations.

3. Migration matrix doesn't exercise 0006's rewrite

packages/cli/test/migrate-install.test.ts:184-206's version matrix seeds every start version with a bare taskless.json and no pre-existing README.md, so by the time migration 0006 runs there's no stale README for its rewrite to act on — the test only reads the final version counter. Real coverage of 0006's actual behavior already lives in installed-documentation.test.ts:57-87 ("rewrites a stale README that no other migration would touch"), which seeds a real stale README and asserts on the rewritten content.

I left this as a documentation-only change (a comment on the matrix loop) rather than trying to make the matrix loop itself exercise 0006's rewrite: doing so properly would mean seeding a different stale artifact for every start version (or special-casing the version just below 0006), which duplicates what installed-documentation.test.ts already does correctly and would make the matrix loop's single, uniform seed less uniform for no real gain. The comment says what the loop does and does not cover and points at the real coverage.

Mutation checks

Test Mutation applied Result
"carries a structural fingerprint that moves with the corpus shape" (reference.test.ts) Added an extra field (mutationTestField: true) to the object built by buildReference in src/rules/reference.ts, regenerated assets/reference.json via pnpm --filter @taskless/cli reference, without bumping REFERENCE_VERSION Failed as expected (expected 'fdfa88cc…' to be '0add3275…'). Reverted both files; hash matched again and all 21 tests in the file passed.
"prints on a second run whose recorded version moved" (wizard-integration.test.ts) Commented out if (reloadNotice !== undefined) console.log(reloadNotice); in src/wizard/index.ts Failed as expected (expected false to be true, banner text absent). The other two banner tests in the block still passed (they assert absence, which a dropped log line trivially satisfies — expected). Reverted; all 10 tests in the file passed.
migrate-install.test.ts comment Comment-only change, no assertion added No mutation check applicable — recorded per the issue's stated intent ("mainly to RECORD this").

Verification

  • NODE_OPTIONS= pnpm test — 82 files, 1329 tests passed
  • NODE_OPTIONS= pnpm typecheck — clean
  • NODE_OPTIONS= pnpm lint — clean (fixed two unicorn/no-array-sort findings by using toSorted() in the new helper)

No changeset: tests only, no user-visible behavior.

Refs #284

@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 4m 47s —— View job


Review mode: incremental — read 0 prior review thread(s) before reviewing.

.prior-review.json contained no review threads, no reviews, and no comments other than the @claude /review trigger itself — this is the first review of this PR.

  • Read .prior-review.json
  • Understand the request (/review)
  • Fetch PR diff (gh pr diff 305) and changed files
  • Review reference.test.ts (shape fingerprint)
  • Review wizard-integration.test.ts (reload banner)
  • Review migrate-install.test.ts (comment-only change)
  • Post final review

Assessment

This is a test-only PR (no production code touched) and it holds up well under a full trace of the wiring it's testing. No correctness issues found. Summary of what I verified:

reference.test.tsshapeOf/shapePaths fingerprint

  • Traced shapeOf against Reference/ReferenceRule/ReferenceTests/ReferenceLayout/RuleConstraint in src/rules/reference.ts and src/rules/constraints.ts: every field is required and typed as string/number/boolean/array/object/null, matching what the fingerprint records.
  • Cross-checked the 3 engines (sg, vale, runtimeENGINES in src/rules/layout.ts) and the grouping/signature/cases optionality against the literal list; spot-checked the shipped assets/reference.json (grep for "grouping"/"signature"/"engine" counts) and the union-of-shapes logic is consistent with what's actually shipped (1 ast-grep-test, 1 case-directories, 1 case-documents; 1 signature, present only on the runtime rule).
  • Array.prototype.toSorted is already used elsewhere in this package (src/install/state.ts, src/filesystem/snapshot.ts) and the package requires Node ≥22.22, so no compatibility concern.
  • Recursion has no cycle risk (input is always the result of JSON.parse).
  • Minor doc-only inconsistency, not a code issue: the PR description's "Design" section says the fingerprint is "hashed with node:crypto's createHash("sha256")", but the code that actually shipped (and the in-file comment "ASSERTED AS A LIST, NOT A HASH, ON PURPOSE") compares the sorted path list directly — this looks like the description wasn't updated after the second commit (f05bbbc) switched from a hash to a list. Not something to fix in code, just flagging so the description doesn't mislead a future reader.

wizard-integration.test.ts — reload banner

  • Traced getReloadNotice end to end: runWizard (src/wizard/index.ts:92) reads previousState.cliVersion from readInstallState before applyInstallPlan runs, matching the test's "plant the old version, then call runWizard again" structure.
  • Confirmed the manifest shape the test mutates (install.cliVersion) is exactly what readInstallState/toInstallManifest in src/install/state.ts read and write — same field the existing init-no-interactive.test.ts plants.
  • Checked for false-positive risk in the substring assertions ("RESTART YOUR AGENTS", the planted version string): getOnboardTrailer and the clack mocks never emit either string, and reload-notice.ts's wrap() only breaks on spaces, so the version token can't be split across lines.
  • Confirmed readManifest (used by readInstallState) is a pure read with no migration side effects — migrations only run through ensureTasklessDirectory — so planting the version between the two runWizard calls can't be silently undone by a migration.
  • The three cases (moved / first install / unchanged) mirror init-no-interactive.test.ts's existing "restart-your-agents banner" block appropriately for runWizard's in-process shape.

migrate-install.test.ts — comment-only change

  • Verified the cross-reference is accurate: installed-documentation.test.ts does contain a describe("migration 0006, on a project that is already current") block with an it("rewrites a stale README that no other migration would touch") test.
  • Checked 0006-refresh-readme.ts: it unconditionally overwrites README.md with no read of prior content, so the comment's claim (a no-op regression in 0006 wouldn't be caught by this matrix) is correct, and seeding no pre-existing README can't cause a throw either.

No inline comments to post — nothing rose to the level of a concrete, actionable issue.

@thecodedrift
thecodedrift force-pushed the test/reference-version-and-wizard-notice branch from f05bbbc to e2c0f16 Compare September 7, 2026 22:41
… banner

Three coverage gaps from #284:

- REFERENCE_VERSION was asserted only as a literal (`toBe(2)`), with nothing
  tying a shape change in Reference/ReferenceRule/ReferenceTests/ReferenceLayout
  to a version bump. Adds a structural fingerprint (sha256 over a sorted list
  of "path:kind" pairs, walked from the live object) asserted beside the
  version, so adding/removing/retyping a field changes the hash while
  reordering, reformatting, or content-only edits do not.

- runWizard wired up getReloadNotice identically to init, but only init's path
  had a test for it. Mirrors init-no-interactive.test.ts's three cases
  (prints on an upgrade, quiet on first install, quiet when unchanged) for the
  wizard.

- The migration matrix in migrate-install.test.ts seeds every start version
  with a bare taskless.json and no README, so migration 0006's rewrite has
  nothing stale to act on; the test only reads the final version counter.
  Left as documented non-coverage with a comment pointing at
  installed-documentation.test.ts, which already seeds a real stale README
  and asserts on the rewrite.

Each new test was mutation-checked (see PR description for the mutation used
per test and confirmation it was caught).
…he field

The fingerprint was compared as a sha256 digest, which reports "expected 0add…
to be ab12…" on failure. That says the shape moved but not what moved, leaving
whoever hit it to rebuild the shape set by hand to find the one added field.

A guard that fires this rarely has to be legible the one time it does. The
shape is 56 (path, kind) pairs, so it is asserted as the sorted list itself and
vitest prints the difference. Measured, by adding a field to the corpus without
bumping the version:

  +   "$.mutationProbe:boolean"

which names the offending field directly. Same coverage, same derivation from
the live object, no digest to reconcile by hand, and one fewer import.
@thecodedrift

Copy link
Copy Markdown
Member Author

Clean review, thanks. Worth flagging one thing that changed after the agent's first pass and before you saw it: the shape check was originally asserted as a sha256 digest, which reports "expected 0add… to be ab12…" and tells you the corpus shape moved without telling you what moved. It is now asserted as the sorted 56-entry path list, so a failure prints + "$.mutationProbe:boolean" and names the field. Measured that way rather than assumed.

— AI Coding Agent

@thecodedrift
thecodedrift merged commit 8813a52 into main Sep 8, 2026
8 checks passed
@thecodedrift
thecodedrift deleted the test/reference-version-and-wizard-notice branch September 8, 2026 00:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant