Skip to content

Audit the test suite: cut what guards nothing, and stop tests copying the code they check - #162

Merged
thalida merged 14 commits into
mainfrom
chore/issue-160-test-suite-audit
Aug 10, 2026
Merged

Audit the test suite: cut what guards nothing, and stop tests copying the code they check#162
thalida merged 14 commits into
mainfrom
chore/issue-160-test-suite-audit

Conversation

@thalida

@thalida thalida commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Closes #160

Numbers

before after
runtime cases, frontend 2,999 1,785 −40%
runtime cases, backend 403 432 +7%
static it / def 2,037 1,950 −4%
files 206 196 −10
LOC 39,375 38,283 −1,092

Both directions are intended. Frontend runtime cases fall 40%, nearly all of it one file. Backend runtime cases rise, because collapsing a loop-inside-one-test into a parametrized table turns ten silent iterations into ten named cases: same coverage, better failure reporting.

Backend coverage is 90.19%, unchanged. Both goldens have a zero-byte diff from the merge-base.

The two questions this audit asked

The issue framed the problem as size. Sorting by size alone missed the interesting failures, so every file was judged on two questions instead: what bug does this catch, and does it reimplement what it tests.

hsl-glsl-parity.test.ts was 40% of the suite and could not catch the drift it existed for

1,181 of 2,999 cases. Its job was to stop hsl.glsl diverging from hsl.ts. What it compared was hsl.ts against a hand-written JS twin inside the test file. The real shader was read once, for three signature checks. Edit a shader body and all 1,181 cases still pass. Edit hsl.ts and it goes red, and the fix is to edit the twin, which resyncs the twin while the shipped shader keeps the old behaviour.

Three implementations, three legs to keep in sync, one leg checked, and it was the leg that does not ship.

Deleted. hsl.test.ts already covers hsl.ts directly with literal expected values. The shader now gets a contract on the operative lines of each body rather than its signature. That is still a text match and cannot catch a compile error; a GPU-backed check is the real fix and is not this PR.

Tests that reimplement their subject

A test that recreates its subject and compares the two proves only that the copies agree. It is blind to a shared mistake, it rots silently, and it hides the expected value. Fixed here: layoutAsserts (a character-for-character copy of overlaps.ts, down to the half-unit slop), rainbowChase (expectedRgb restating the formula under test), and the height/radius assertions in treeRenderer (now literal numbers, still pinning that the renderer feeds treeEncodings curves into the instance matrices).

api/tests/services/test_stats.py already does this correctly, pinning a cross-language hash with golden literals, and is the model.

Files that guard nothing

Five deleted outright: four asserted that a typed settings store has the keys its own type declares, one asserted that optional chaining does not throw.

Source changes

Dead exports whose only callers were the deleted tests: sortForRendering, translateRectsToWorld (with TypedRect), HARD_FALLBACK_FILE / HARD_FALLBACK_FOLDER, and the __test seam that re-exported one symbol rect.ts already exports publicly.

createFireflies was exported from both the component door and the inner assembly; index.ts already had to alias it to say which one it meant. The inner one is now createFireflyAssembly.

overlaps.ts exports isStreetJoinPair so the test helper can use it instead of carrying a copy.

Kept despite matching the dead-export rule: shadeByRatio has no production caller, but its GLSL twin is used by building.frag.glsl and it is what the contract check is written against.

Structure

src/city has no top-level fireflies/ or trees/; both have lived under components/ since the scene-to-city refactor, and every one of those test files was already importing from the new location. They moved. So did armOnFirstTick, onSettings, cityState, and rect, each of which sat in a different directory from the module it tests. layoutPacker.test.ts tests stemSolver and is renamed; there is no layoutPacker module.

Notes for review

  • One bookkeeping slip: the parity file deletion physically landed in aa0aadd rather than 10cb818 where its message lives, because it was already staged. Content is correct and green; I did not rewrite history to fix the split.
  • The test_clone table was verified to bite: making the classifier raise the wrong error type fails exactly the two branch rows, and restoring it goes green.

Not done, and why

Filed rather than fixed: #161, createScrubController takes the whole world, which is why scrubController.test.ts reaches line 535 before its first assertion. Restructuring a 558-line controller does not belong in a test PR.

Still on the list from the audit, all mechanical: the remaining small-file merges (StreetPaneExclude, useManifestSourceExcludes, infoPane, filePreviewSection, pathLine/renderer, streetOpacity, the three tiny buildings/ files), the test_cache.py load-missing/corrupt/version triple repeated across four cache kinds, scrubController and commitPane, and replacing the derived _helpers fixtures with literal manifest data.

The full per-file audit, including the finding classes and the sweeps behind them, is in .claude/superpowers/specs/issue-160-test-audit.md (untracked by request).

🤖 Generated with Claude Code

thalida and others added 9 commits August 10, 2026 01:18
Four assert that a typed settings store has the keys its own TypeScript type
declares — skyConfig, footprint, island, and the repoLabel default shape. The
type system already rejects every case they cover; none can fail without a
compile error firing first.

The fifth, sceneCommands, asserts that selectCommit and focusCommit do not
throw before the scene boots. Both are `SCENE_HANDLE.peek()?.…` — optional
chaining. There is nothing there to break.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…checks

sortForRendering, translateRectsToWorld (with TypedRect), and the
HARD_FALLBACK_FILE / HARD_FALLBACK_FOLDER pair have no production caller: the
tests deleted here were the only thing referencing them. The __test seam went
too, since it re-exported nothing but _rectsOverlap, which rect.ts already
exports publicly. Those _rectsOverlap cases move to rect.test.ts, which also
moves out of city/utils to sit beside the module it covers.

layoutAsserts carried its own copy of overlaps.ts's T-junction predicate,
character for character down to the half-unit slop, plus its own re-derivation
of rectOfStreet and rectOfBuilding. Two copies of geometry that have to agree
is a hand-sync waiting to rot, so assertNoOverlap now runs on
findLayoutOverlaps and the predicate is exported rather than duplicated.

assertStemOrder was building a childBuildings list and then filtering every
entry back out before asserting. That work is gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hsl-glsl-parity held 1,181 of the suite's 2,999 cases, 40% of every run, to
stop hsl.glsl drifting from hsl.ts. It could not do that. What it compared was
hsl.ts against a hand-written JS twin living in the test file; the real shader
was read once, for three signature checks. Edit a shader body and all 1,181
cases still pass. Edit hsl.ts and it goes red, and the fix is to edit the twin,
which resyncs the twin while the shipped shader keeps the old behaviour.

Three implementations, three legs to keep in sync, one leg checked, and it was
the leg that does not ship.

hsl.test.ts already covers hsl.ts directly with literal expected values. What
the shader gets instead is a contract on the operative lines of each body
rather than its signature, so an edited formula has to break something. That is
still a text match and cannot catch a compile error; a GPU-backed check is the
real fix and is not this PR.

rainbowChase had a smaller version of the same thing: an expectedRgb helper
restating the formula under test. Its cases now assert literal colours.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ives

src/city has no top-level fireflies/ or trees/; both have lived under
components/ since the scene-to-city refactor. The tests stayed behind, so each
subject was split across two directories and every one of these files was
already importing @/city/components/... from the old location.

The two underscore-prefixed helpers were consumed from outside trees/, so they
move to _helpers alongside the rest.

treeRendererCommitLookup folds into treeRenderer: it is the picker-facing half
of the same handle, not a separate module. Its three WIDTH_AGE_FLOOR cases go,
since treeEncoding already covers that attenuation against treeRadius directly,
and its four sha-lookup null cases collapse into one table.

The renderer's height and radius assertions now carry literal numbers instead
of restating treeEncoding's curves. They still pin that the renderer feeds
those curves into the instance matrices; they no longer pass by construction.

The commits helper now defaults to a distinct sha per entry. It was handing
every fixture commit the same one, which made the sha-keyed lookups
untestable without hand-written shas at each call site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…owns

createFireflies was exported from both the component door and the inner
assembly it builds. index.ts already had to import it as `assembleFireflies` to
say which one it meant, and both test files needed a prose header for the same
reason. Every sibling component distinguishes the two (createTrees /
createTreeRenderer), so the inner one is now createFireflyAssembly.

Five cases in fireflies.test.ts asserted ring behaviour that orbitRings.test.ts
already asserts directly against the pool: hover-plus-selected collapsing to
one mesh, deselect restoring the hover ring, clearing hover emptying the group.
Testing the pool through the assembly adds a layer, not a guarantee. The
author-colour case stays, since that path (author to hue to lightRgb to
material) is only covered end to end here.

Also gone: a typeof check on two methods the type already declares, and two
no-throw-only cases on an empty renderer.

firefliesPlacement's four per-orb range checks were the same assertion over
four fields, now one table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
layoutPacker.test.ts tests stemSolver; there is no layoutPacker module, so it
is renamed. It also carried three import statements in the middle of the file
and a layoutCity end-to-end block that belongs with layoutCity: imports are
hoisted, the block moves to algorithm.test.ts.

slimManifest and stemScanEquivalence were a file each for one invariant about
layoutCity (the worker payload slice produces an identical layout; the hot scan
matches the traced scan). Both are real and both now live in algorithm.test.ts.

armOnFirstTick and onSettings sat under components/ while testing city/utils/;
cityState sat at city/ while testing city/state/. All three moved to match.

getStreetWidth was ten it() blocks differing by two numbers, now one table over
each tier's first and last count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_extension, _is_binary, _compute_busyness, _derive_tree_signals' date ranges,
_annotate_same_day_totals, _parse_svg_length, media_kind and probe_media_dims
were 45 methods across seven unittest classes, most of them one input and one
expected value. They are now parametrized, so a new case is a row rather than a
method, and a failure names the input that broke.

Three of the classes existed only to hold a fixture builder shared by their
methods; those builders are now module-level functions and the classes are
gone. _is_binary drops its mkstemp/addCleanup helper for pytest's tmp_path.

Coverage of these functions goes up, not down. Collapsing the media_kind
extension loops into rows means each extension is its own case, so a failure
says which one; the same for the svg length table. Three cases are new: a unit
SVG does not define, explicit svg dimensions outranking a disagreeing viewBox,
and same-day totals over non-adjacent dates.

Backend: 394 test declarations to 368, 426 cases running, coverage 90%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twelve methods each fed _maybe_raise_clean_clone_error one stderr string and
asserted one exception type. They are now two tables: the classification rows,
and the five auth-failure spellings that all have to read as unreachable
without the message ever claiming the repo is private.

The class kept the tests that are about retry behaviour rather than
classification, so it is renamed for what is left in it.

Verified the table bites: making the classifier raise RepoNotFoundError where
it should raise BranchNotFoundError fails exactly the two branch rows, and
restoring it goes green.

Backend now 363 test declarations from 394, 432 cases running, coverage 90%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cityScene-collision and cityScene-stem-diagnostic were two files for one
module, city/diagnostics, named after a scene that no longer exists. They are
now diagnostics.test.ts. Both had an import wedged into the middle of their
header comment; that is fixed on the way through.

repoLabelPositioning restated four of repoLabel.test.ts's cases: the same
setAnchor arithmetic, the same HEIGHT_PCT=0 floor case, and the same two
settings-effect assertions with different wording. The one case it had that
repoLabel did not, a non-zero anchor.y adding to heightWorld rather than being
ignored, moves across; the file goes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thalida thalida linked an issue Aug 10, 2026 that may be closed by this pull request
thalida and others added 2 commits August 10, 2026 11:48
renderer, buildingIndex, cellTile and dataFacade were a file each for one
function; StreetPaneExclude, infoPane and filePreviewSection were a file each
for one or two cases about a component already covered next door. All seven
move into the suite for their module. dataFacadeKind's three extension loops
become a table, so a failure says which extension.

streetPane carried _extBreakdown, a reimplementation of the backend's per-dir
extension aggregation. StreetPane reads descendants_ext_breakdown verbatim off
the manifest, so the fixture now takes it as data. The counts the tests assert
on are visible at the call site instead of being derived by a copy of scan.py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eleven methods across four cache kinds asserted the same thing: a read of a
file that is gone, truncated, or written by older code has to miss rather than
raise, or a stale cache bricks the scan. They are now one table of four kinds
by three kinds of damage, so a new cache kind is a row.

The files row initially did not bite. Its stale-version payload carried
malformed entries, which the entry filter drops regardless, so a broken version
guard still produced an empty result and the assertion could not tell the two
apart. It now carries a well-formed entry: breaking the guard fails exactly
[stale-version-files], and restoring it goes green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread api/tests/test_scan.py
assert _is_binary(p) is is_binary


def _commits_per_day(*per_day: int) -> list:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, shouldnt this be a mock instead?

Comment thread app/tests/city/components/fireflies/fireflies.test.ts Outdated
Comment thread app/tests/city/components/fireflies/firefliesPlacement.test.ts Outdated
const seeded = (seed: number, commitIndex: number) =>
placement(seed * 10, seed * 10, seed, commitIndex);

const lookupCommits = (n: number) =>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again! I'm going to stop commenting on all of them, please do a thorough pass over all tests.

// estimateDirAlongReach fix, the phantom seeded into api's local occupancy
// was sized with parentMaxBoundary*2 + 1000 at recursion start (when api
// was apps' first child, parentMaxBoundary was tiny); deep grandchildren
// placed past the phantom could land on top of apps' trunk.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment too long.

thalida and others added 3 commits August 10, 2026 11:52
The same-day block was seven cases over one line of UI: plural vs singular,
the tooltip, and four permutations of whether the swatch shows. The swatch
ones are now one table over (colour, count), which states the rule the four
were circling: the swatch needs both a colour to show and a non-zero count to
belong to.

The four "renders X synchronously" cases each blocked fetch, mounted, and
asserted one selector. One table over (selector, text) does the same and makes
the shared point once: everything except the body comes off the CommitEntry the
picker already holds, so a blocked fetch cannot delay it.

748 lines to 668, 40 declarations to 32, same 41 cases running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_is_binary reads a file and hands the bytes to gitobj.is_binary_bytes, which is
already public. The table wrote four real files to exercise that pure function.
It now calls the classifier with byte literals and touches no disk, and the
read itself gets two mocked cases: that it reads one chunk, and that an
unreadable file fails safe to binary.

'firefly-orbit-rings' was a literal in orbitRings.ts copied into two test
files. The name is exported now and both import it. The scene-graph traversals
that went looking for it move to a _helpers module, so no test walks children
by hand.

The fireflies tests hand-wrote CommitEntry and TreePlacement literals that the
shared factories already build; both now use them, and the two dynamic
await import() calls for authorColor are static imports.

Comment pass over every test file: dropped nine headers that echoed the file's
own path, and rewrote the longest blocks. Gone with them are the references to
Stage 4 Commit 2, Task 9, Task 15, and the drift narration in buildingFixture.
Blocks over four lines: 76 down to 63.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commitTarget was declared three times, twice needing an `as unknown as
PickTarget` cast because the literal it built was incomplete. One typed factory
in _helpers replaces all three, built on the commits helper so the cast is gone.

Nine files hand-wrote CommitEntry literals, and four more declared their own
commit(i) / makeCommits(n) generators. They all use the shared factory now, with
a commitSeries(n) helper for the n-consecutive-days shape that treeRenderer,
picker-commit and picker-timeline each had their own copy of.

'fireflies-orbs' was a second name literal duplicated out of source; it is
exported as FIREFLY_ORBS_MESH and imported, like ORBIT_RINGS_GROUP before it.

The last four dynamic await import() calls are static imports, so the tests
name their dependencies at the top like everything else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thalida
thalida merged commit 5033617 into main Aug 10, 2026
1 check passed
@thalida
thalida deleted the chore/issue-160-test-suite-audit branch August 10, 2026 22:24
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.

Audit the test suite: cut what's stale, duplicated, or too fine-grained

1 participant