Skip to content

Finish the test consolidation #162 described but left behind - #163

Merged
thalida merged 19 commits into
mainfrom
chore/test-consolidation
Aug 10, 2026
Merged

Finish the test consolidation #162 described but left behind#163
thalida merged 19 commits into
mainfrom
chore/test-consolidation

Conversation

@thalida

@thalida thalida commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Follow-on to #162, which described a backlog of "still on the list, all
mechanical" work in its description and then merged without filing it. This is
that work, plus a correction to what it actually amounted to.

Numbers

before after
runtime cases, frontend 1,800 1,797 −3
runtime cases, backend 435 435 0
static it / def 1,931 1,905 −26
not.toThrow sites 53 16 −70%
LOC −539

43 files changed, 485 insertions, 1024 deletions. Backend coverage 90.19%,
gate is 80%. Both goldens have a zero-byte diff against the merge-base.

The LOC number is the real one. Collapsing ten it()s onto an it.each with
ten rows removes ten declarations and a hundred lines while running exactly the
same assertions, so a reader's burden falls much further than the runtime count
does. Where the runtime count should fall is deletion, and only three
deletions here were justified.

Most of the backlog was already done

The audit document #162 left behind lists nine finding classes. Working through
them one at a time, most of what it said remained had in fact been fixed inside
that same PR — the document was written mid-flight and never reconciled against
the merge:

  • test_cache.py's load-missing/corrupt/version triple across four cache kinds
    is already test_damaged_cache_reads_miss, a 4x3 parametrize.
  • _parse_svg_length is already parametrized.
  • getStreetWidth is already an it.each.
  • _commitFixtures.ts, repoLabelPositioning.test.ts and
    treeRendererCommitLookup.test.ts no longer exist.

Every claim in this PR was checked against the merged tree before acting on it.
That is also how the two rejections below were found.

Two of the audit's prescriptions were wrong

statsFixtures.ts does not need literal RepoStats. The audit called its
authorHue a mirror of the backend's _author_hue that had to track it, and
prescribed replacing the derivation with literal fixtures at each call site.
But nothing on this side reads a hue it did not get from the stats object it was
handed: authorColor.test.ts says outright that the name-to-hue hash is
backend-owned, and the one hue assertion in firefliesPlacement looks the value
up from the same stats object. There is no circular assertion to break, and the
prescription would have expanded 25 call sites to fix it. The mirror stays, with
a comment that says what it actually is and why the values are frozen —
decorationGolden digests the orb colours they produce, which is how the
attempt to change them was caught.

The ring cases in fireflies.test.ts do not duplicate orbitRings.test.ts.
orbitRings drives the pool directly; these drive it through the assembly, and
cover the wiring between the two that neither side covers alone.

What was fixed

Assertions that agreed with the code no matter what either did.
spatialGrid recomputed Math.ceil(100 / MIN_CELL_SIZE) on the expected side
of three assertions. islandGeometry recomputed the sqrt(2) x 1/cos(pi/12)
corner correction, then asserted its own arithmetic exceeded hypot(100, 100)
a comparison between two literals with the subject nowhere in it.

Assertions the type system already makes. 28 typeof/Array.isArray sites
and 9 assertIsInstance. Every backend one deleted was already subsumed by the
next line: isinstance(commits, list) in front of len(commits) > 0 and a
sortedness check. Two stay — busyness avg/busy can really drift int to float
through a division and the JSON contract cares, and test_config's
isinstance(settings(), Settings) is the no-crash assertion, not a type claim.

not.toThrow hiding its own diagnostics. The sixteen layout invariant
checks in algorithm.test.ts wrapped helpers that throw with the offending pair
of rects in the message; the wrapper caught it and reported "expected function
not to throw" instead.

Five copies of one EventSource stub, three of which carried a comment saying
they mirrored one of the others.

Three copies of the WebGL renderer stub. city/index,
city/initialFraming and city/interaction/inputHandlers each carried the same
~35-line FakeWebGLRenderer plus the same postFx stub, differing in one place:
index's forceContextLoss calls a spy, which is the assertion behind "dispose
releases the context, not just its resources". The shared factory takes that
callback, so the one real difference is visible instead of buried in three
near-copies. They come in via await import(...) inside the mock factory, since
vi.mock hoists above the import block. Dropping the class also dropped the
only use of import * as THREE in all three files.

Seven TestCase methods for build_authors_list, each re-importing the
function to assert one input-output pair, now one parametrize with named rows
[email-only] rather than a row index.

A local mkDir shadowing the imported one with different behaviour.
algorithm.test.ts imports mkFile/mkDir from cityFixtures and then
redefines both. The local mkFile was byte-identical. The local mkDir was
not: it re-prefixes paths recursively and accumulates descendants_count down
the tree where the shared one does one level. Two functions, one name, one file,
and nothing announcing the difference. The local one is now mkDeepDir.

useManifestSourceExcludes.test.ts, named for the behaviour it was filed
for rather than the module it tests, folded into useManifestSource's suite —
same two entry points, same stub.

Two copies of _final_manifest, byte-identical, the second's docstring
saying outright that it mirrored the first. It moves to conftest.py, which
already declares itself the home for exactly that. test_scan_dirty no longer
imports scan_tree or Manifest, since the helper was its only use of either.

Four building-dimension assertions that computed their expected value from
the same config the subject reads — dim.h against dim.floors * FLOOR_HEIGHT
is the implementation compared to itself. Each replacement number was derived by
hand from the documented formula and then confirmed: square 24.8 →
round(2.48) = 2 floors, portrait clamps to aspect 2.5 → round(6.2) = 6, panorama
clamps to 0.4 → round(0.992) = 1, non-media 100 lines → 6.

That last one is the payoff. The comment claimed the sqrt interpolation "lands
at 8 floors"; it lands at 6. The assertion was floors > 1, true either way, so
the file had been carrying a wrong number with nothing to catch it.

Coverage went up, not down

Three assertions were strengthened rather than shortened, and the proof is that
the strengthened one bites where the original could not. Changing lerpRange's
Math.round to Math.floor now fails getLightness > interpolates linearly in between — 47.5 becomes 47 instead of 48. The assertion it replaced was
toBeGreaterThan(cfg.min) and toBeLessThan(cfg.max), which 47 satisfies. That
regression was previously invisible.

The same held for the sky teardown test. "dispose() stops the effect — later
SCENE mutations do not throw" was true whether the effect stopped or not. It now
captures uSkyColor, disposes, writes a new SKY_COLOR, and asserts the uniform
did not move. Commenting out stopEffect() in components/sky/index.ts makes it
red; the old version stayed green.

repoLabel and island took the same treatment for the same reason —
_applyTransform writes group.position and the island effect writes
group.visible, both unguarded — and both were verified the same way, by
commenting out their stopEffect() and watching the right test go red.
repoLabel's old assertion mutated OPACITY, which only reaches the materials
behind their guards, so it could not have caught that under any wording.

gem and footprint could not be fixed that way, and it is worth saying why.
gem's dispose() nulls edges/body and its effect body is entirely
edges?.material guards; footprint's nulls material and its effect sits
behind if (material). A leaked subscription is absorbed and looks identical
from the test. There is nothing to observe, so those two are renamed to pin what
they actually check — the guards — rather than claim a teardown check they
cannot make.

The rest of the sweep took not.toThrow from 53 sites to 16. Most were wrappers
around assertions already sitting on the next line (trees ended in
expect(refreshSpy).not.toHaveBeenCalled(), which is the spy proof the audit
asked for; streets ended in a sidewalk colour check), and the wrapper only
swallowed the stack on failure. Five construction-time cases — build before the
picker exists, poke the settings signal, assert nothing threw — now assert the
inertness instead: nothing built, group still empty.

Also strengthened: the unknown-extension hue, which asserted the hash was
reproducible and in range without saying what it was and now pins 259; and the
tree placement client's Worker fallback, which asserted Array.isArray on a
typed TreePlacement[] and now pins the empty result.

Comments

A pass over the test comments, since they had drifted the same way the
assertions had. Gone: which issue introduced a system, that a MIN_BUFFER floor
used to pad tiny bboxes and is gone, that consumers previously built
THREE.Colors inline, that the camera now lives on perFrame.rig. Where the
history was carrying the actual reason, the reason is restated in the present
tense; where it was not, it is deleted. Also gone: the ASCII banner rules and
their "Tests" headings across five component files, footprint's restatement of
its own API signature, and its "mirrors sky/island/repoLabel test style" aside.

Not done

The two files that dominate the remaining LOC are structural, not mechanical,
and do not belong in a test-only PR:

Also left, and all small:

  • The other pure-function TestCase classes in test_scan.py
    (LineCountCapTests, TreeSignatureTests, ScanTreeStreamingTests). Each
    builds a different file or tree per case, so a table needs a factory per row
    and the win is thinner than build_authors_list's was.
  • The TREES/BUILDING_DIMENSIONS reset block repeated in three files, and the
    one-line flush() in two hook tests.
  • streetOpacity.test.ts and the small buildings/ files, which are named for
    behaviours rather than modules but are not obviously wrong where they sit.

scrubController's withCommitDateRanges — a port of the backend's
compute_commit_date_ranges — is deliberately not on that list. It should
become literal ranges, but doing it against the current shape of that file is
wasted work; it belongs with #161.

Closes nothing. #161 stays open.

🤖 Generated with Claude Code

thalida and others added 19 commits August 10, 2026 18:38
color.test.ts declared 31 tests to cover six functions, five of them just
reading one palette entry each. Saturation and lightness run the same
lerpRange curve over the same last-modified axis, so they now share one
describe.each with different bounds.

Three assertions got stronger on the way through. The two "interpolates
linearly" cases asserted only that the result sat somewhere between the
bounds, which passes for any monotonic curve; they now pin the midpoint
value. The unknown-extension case asserted the hash was reproducible and
in [0, 360) without saying what it was; it now pins 259. And getModifiedAge's
midpoint case, likewise bounds-only, pins 0.5.

Dropped: "does not crash on empty extension", whose only assertion was
typeof === 'number' on a function typed to return one. The empty string is
still covered, by the case that pins its hue at 0.

statsFixtures' authorHue is not a mirror of the backend that has to track
_author_hue: nothing on this side reads a hue it did not get from the stats
object it was handed. Say so, and say why the values are frozen anyway
(decorationGolden digests the orb colours they produce).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twenty-eight expect(typeof x) / Array.isArray sites in the frontend and
nine assertIsInstance in the backend, all restating a declared type. Two
whole tests went with them: layoutCity "returns { streets, buildings }
arrays", and city/index's handle-shape check, both of which asserted only
that typed fields had their declared types.

Every backend one deleted here was already subsumed by the assertion on
the next line — isinstance(commits, list) in front of len(commits) > 0 and
an oldest-first sort, isinstance(sig, str) in front of len(sig) == 16 and
int(sig, 16). Two stay: busyness avg/busy really can drift int -> float
through a division and the JSON contract cares, and test_config's
isinstance(settings(), Settings) is the no-crash assertion, not a type
claim.

Two tests got a stronger assertion rather than a shorter one. The tree
placement client's Worker fallback asserted Array.isArray on a typed
TreePlacement[]; it now pins the empty result an empty layout produces.
commitPane asserted the author dot's colour three ways (truthy, not '',
and typeof colorForAuthor().hex) and now asserts it once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sixteen layout invariant checks in algorithm.test.ts wrapped their
helper in expect(() => assertNoOverlap(layout)).not.toThrow(). Those
helpers throw with the offending pair of rects in the message; the wrapper
caught it and reported "expected function not to throw" instead. Calling
them directly gives the failure back its diagnostic.

Three no-op assertions had something observable to assert and now assert
it: moveTooltip before showTooltip creates no tooltip element, ticking an
unnamed repo label leaves its group empty (merged into the test that
already covered the empty group), and an empty repo name still yields a
texture.

Kept: disposeObject3D(null). Null tolerance is that function's contract
and nothing else covers it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five files carried their own StubEventSource — three of them said in a
comment that they mirrored one of the others. Two shapes were in play: the
api/ layer takes an EventSource ctor as a parameter, the hooks layer reads
it off globalThis. tests/_helpers/eventSource.ts now serves both, with
makeES() for injection and installEventSource() returning its own restore
function so the afterEach cannot forget which global it saved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
spatialGrid recomputed Math.ceil(100 / MIN_CELL_SIZE) on the expected side
of three assertions, so it agreed with the grid no matter what either did.
islandGeometry did the same with the sqrt(2) x 1/cos(pi/12) corner
correction, then asserted its own arithmetic was greater than
hypot(100, 100) — a comparison between two literals with the subject
nowhere in it. Both now carry the value, with the derivation in a comment
where it cannot be mistaken for a check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"all placements have a defined commitIndex" built the same layout with the
same commitCount as "assigns commitIndex 0..N-1 in distance order" eleven
lines above, then asserted the weaker half of what that one already
asserts.

Checked and kept: the ring cases in fireflies.test.ts, which the audit
listed as duplicating orbitRings.test.ts. They do not. orbitRings drives
the pool directly; these drive it through the assembly, and cover the
wiring between them (ring group present, ORBIT_RING_ENABLED honoured,
hover tinted from the author colour) that neither side covers alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four assertions in algorithm.test.ts computed their expected value from
the same config the subject reads: dim.h against dim.floors * FLOOR_HEIGHT
(the implementation compared to itself), and expectedFloors as
Math.max(MIN_FLOORS, Math.round(dim.w * aspect / FLOOR_HEIGHT)) twice,
which is the function's own body on the expected side.

Each number below was derived by hand from the documented formula and then
confirmed against the code: square 24.8 -> round(2.48) = 2 floors, portrait
clamps to aspect 2.5 -> round(6.2) = 6, panorama clamps to 0.4 ->
round(0.992) = 1, non-media 100 lines -> 6.

That last one is why this matters. The comment claimed the sqrt
interpolation "lands at 8 floors" and it lands at 6. The assertion was
floors > 1, which is true either way, so the file has been carrying a wrong
number in a comment with nothing to catch it.

Left alone: dimensions.test.ts's stubHeight and slabHeight. Those read
live config rather than a test fixture, and naming a config constant on the
expected side survives a config change where a literal would just break.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Byte-identical bodies in test_scan.py and test_scan_dirty.py, the latter's
docstring saying outright that it mirrored the former. It moves to
conftest.py, which already declares itself the home for helpers that would
otherwise be duplicated. test_scan_dirty no longer imports scan_tree or
Manifest at all, since the helper was its only use of either.

Also drops that docstring's account of a migration ("Future tasks 11-13
will migrate the existing test files") that never finished and no longer
describes anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jsdom has no GL context, so city/index, city/initialFraming and
city/interaction/inputHandlers each carried the same ~35-line
FakeWebGLRenderer and the same postFx stub. They differed in one place:
index's forceContextLoss calls a spy, which is the assertion behind
"dispose releases the context, not just its resources". The shared factory
takes that callback, so the one real difference stays visible instead of
being buried in three near-copies.

They are pulled in with `await import(...)` from inside the mock factory
rather than a top-level import, because vi.mock hoists above the import
block and a top-level binding is not initialised when the factory runs.

Dropping the class also dropped the only use of `import * as THREE` in all
three files — it was there for one Vector2 type annotation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seven TestCase methods, each re-importing the function and asserting one
input-output pair. One parametrize with named rows runs the same seven
cases and still reports each by name — [email-only] rather than a row
index, so a failure says which branch broke.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"dispose() stops the effect — later SCENE mutations do not throw" asserted
only that nothing threw, which is true whether the effect stopped or not.
It now pins the uniform: capture uSkyColor, dispose, write a new SKY_COLOR,
assert the uniform did not move. Commenting out stopEffect() in
components/sky/index.ts makes it red; the old version stayed green.

The gem case cannot be strengthened the same way and is renamed instead.
dispose() nulls edges/body, and the effect body is all `edges?.material`
guards, so a leaked subscription would be absorbed and look identical from
the test. What that test actually pins is the guards, so it now says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
repoLabel's could be strengthened and was. HEIGHT_PCT drives
_applyTransform, which writes group.position with no null guard, so a
subscription outliving dispose is visible from the test. It now captures
position.y, disposes, bumps HEIGHT_PCT and asserts the group did not move.
Commenting out stopEffect() in components/repoLabel/index.ts makes it red.
The old assertion mutated OPACITY, which only reaches the materials behind
their guards, so it could not have caught that under any wording.

footprint's could not, for gem's reason: dispose() nulls `material` and the
effect body sits behind `if (material)`. Renamed to say it pins the guard.

armOnFirstTick's "dispose before arm() is safe" already carried the real
assertion (setup was never called) next to a redundant not.toThrow. Just
call dispose().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
useManifestSourceExcludes.test.ts was named after the behaviour it was
filed for rather than the module it tests, and it exercised loadSource and
setupLiveUpdates — the same two entry points as useManifestSource.test.ts,
against the same EventSource stub. One file, one suite each.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
algorithm.test.ts imports mkFile/mkDir from cityFixtures, then redefines
both inside the quickjs describe. The local mkFile was byte-identical, so
it is gone. The local mkDir was not: it re-prefixes paths recursively and
accumulates descendants_count down the tree, where the shared one prefixes
only immediate children. Two functions with one name in one file, differing
in a way nothing announced.

The local one is now mkDeepDir, with a comment on what it does that the
shared one does not, and why this scenario needs it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deletes the historical narration: which issue introduced a system, that a
MIN_BUFFER floor used to pad tiny bboxes and is gone, that consumers
previously built THREE.Colors inline, that the camera now lives on
perFrame.rig. Where the removed history was carrying the actual reason,
the reason is restated in present tense; where it was not, it is gone.

Also drops the ASCII banner rules and their "Tests" headings across five
component files, footprint's restatement of its own API signature and its
"mirrors sky/island/repoLabel test style" aside, and three comments
narrating what the line below them plainly does.

One test went with them: facadePanels' "dispose does not throw", whose
only assertion was that a dispose call returned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
island's could be strengthened the way sky's and repoLabel's were: its
effect writes group.visible with no null guard, so a subscription that
outlived dispose flips it. The test now captures visible, disposes, sets
ENABLED false and asserts the group did not react. Commenting out
stopEffect() in components/island/index.ts makes it red. The old body
asserted only that neither call threw, under a title claiming it checked
geometry release and effect teardown, and it checked neither.

The two "idempotent" clear() cases now assert the state a second clear
leaves (renderer null, group empty) rather than that it returned. trees'
null-picker tick already had its real assertion on the next line, so the
wrapper around the call is gone.

fireflies' onResize test covered "before and after the assembly is built".
The after half asserted nothing — onResize is a one-line `_inner?.onResize`
forward, so the guard IS the contract. It keeps the before half, says why
that path is real (a resize can land mid-load), and asserts the no-op.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The construction-time ones were the same shape in five components: build
before the picker exists, poke the settings signal, assert nothing threw.
What actually holds there is the effect's optional chaining against a null
inner renderer, so each now says that and asserts the inertness — nothing
built, group still empty.

The rest were wrappers around assertions that were already there. trees'
post-dispose case ended in expect(refreshSpy).not.toHaveBeenCalled(), which
is the spy proof; streets' ended in a sidewalk colour check. Both kept
their assertion and dropped the wrapper that was swallowing the stack.

53 not.toThrow sites at the start of this branch, 16 now, and the ones left
are places where "did not throw" is the actual contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Trivy failed on msgpack 1.1.2 and setuptools 70.3.0, neither of which is in
uv.lock. They are pip's vendored copies, and pip is in the image only
because python:3.13-slim runs ensurepip during its own build — nothing here
installs it and nothing imports it, since uv does the installing for both
the build and the runtime venv. Removing it takes both CVEs with it.

The glob is guarded by a find that fails the build if pip survives, so a
base-image bump to 3.14 cannot quietly turn the rm into a no-op.

Verified against the rebuilt image: trivy --severity HIGH,CRITICAL
--ignore-unfixed --exit-code 1 exits 0, the pip dist-info target is gone
from the scan, api and the runtime deps still import, and the backend suite
is 435 passed at 90.24%.

Also removes .github/workflows/claude.yml and claude-code-review.yml. No
other file referenced either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thalida
thalida merged commit 9eaa784 into main Aug 10, 2026
1 check passed
@thalida
thalida deleted the chore/test-consolidation branch August 10, 2026 23:58
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