Finish the test consolidation #162 described but left behind - #163
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
it/defnot.toThrowsites43 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 anit.eachwithten 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 kindsis already
test_damaged_cache_reads_miss, a 4x3parametrize._parse_svg_lengthis already parametrized.getStreetWidthis already anit.each._commitFixtures.ts,repoLabelPositioning.test.tsandtreeRendererCommitLookup.test.tsno 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.tsdoes not need literalRepoStats. The audit called itsauthorHuea mirror of the backend's_author_huethat had to track it, andprescribed 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.tssays outright that the name-to-hue hash isbackend-owned, and the one hue assertion in
firefliesPlacementlooks the valueup 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 —
decorationGoldendigests the orb colours they produce, which is how theattempt to change them was caught.
The ring cases in
fireflies.test.tsdo not duplicateorbitRings.test.ts.orbitRingsdrives the pool directly; these drive it through the assembly, andcover the wiring between the two that neither side covers alone.
What was fixed
Assertions that agreed with the code no matter what either did.
spatialGridrecomputedMath.ceil(100 / MIN_CELL_SIZE)on the expected sideof three assertions.
islandGeometryrecomputed thesqrt(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.isArraysitesand 9
assertIsInstance. Every backend one deleted was already subsumed by thenext line:
isinstance(commits, list)in front oflen(commits) > 0and asortedness check. Two stay —
busynessavg/busy can really drift int to floatthrough a division and the JSON contract cares, and
test_config'sisinstance(settings(), Settings)is the no-crash assertion, not a type claim.not.toThrowhiding its own diagnostics. The sixteen layout invariantchecks in
algorithm.test.tswrapped helpers that throw with the offending pairof 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/initialFramingandcity/interaction/inputHandlerseach carried the same~35-line
FakeWebGLRendererplus the same postFx stub, differing in one place:index's
forceContextLosscalls a spy, which is the assertion behind "disposereleases 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, sincevi.mockhoists above the import block. Dropping the class also dropped theonly use of
import * as THREEin all three files.Seven
TestCasemethods forbuild_authors_list, each re-importing thefunction to assert one input-output pair, now one
parametrizewith named rows—
[email-only]rather than a row index.A local
mkDirshadowing the imported one with different behaviour.algorithm.test.tsimportsmkFile/mkDirfromcityFixturesand thenredefines both. The local
mkFilewas byte-identical. The localmkDirwasnot: it re-prefixes paths recursively and accumulates
descendants_countdownthe 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 filedfor 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 docstringsaying outright that it mirrored the first. It moves to
conftest.py, whichalready declares itself the home for exactly that.
test_scan_dirtyno longerimports
scan_treeorManifest, 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.hagainstdim.floors * FLOOR_HEIGHTis 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, sothe 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'sMath.roundtoMath.floornow failsgetLightness > interpolates linearly in between— 47.5 becomes 47 instead of 48. The assertion it replaced wastoBeGreaterThan(cfg.min)andtoBeLessThan(cfg.max), which 47 satisfies. Thatregression 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 newSKY_COLOR, and asserts the uniformdid not move. Commenting out
stopEffect()incomponents/sky/index.tsmakes itred; the old version stayed green.
repoLabelandislandtook the same treatment for the same reason —_applyTransformwritesgroup.positionand the island effect writesgroup.visible, both unguarded — and both were verified the same way, bycommenting out their
stopEffect()and watching the right test go red.repoLabel's old assertion mutatedOPACITY, which only reaches the materialsbehind their guards, so it could not have caught that under any wording.
gemandfootprintcould not be fixed that way, and it is worth saying why.gem'sdispose()nullsedges/bodyand its effect body is entirelyedges?.materialguards;footprint's nullsmaterialand its effect sitsbehind
if (material). A leaked subscription is absorbed and looks identicalfrom 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.toThrowfrom 53 sites to 16. Most were wrappersaround assertions already sitting on the next line (
treesended inexpect(refreshSpy).not.toHaveBeenCalled(), which is the spy proof the auditasked for;
streetsended in a sidewalk colour check), and the wrapper onlyswallowed 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.isArrayon atyped
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_BUFFERfloorused to pad tiny bboxes and is gone, that consumers previously built
THREE.Colors inline, that the camera now lives onperFrame.rig. Where thehistory 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 ofits 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:
scrubController.test.ts— 53 tests, 2,131 lines, first assertion on line535. That is createScrubController takes the whole world: 534 lines of scaffolding before the first assertion #161, and it is a source problem:
createScrubControllertakesthe whole world, so every test must build one.
commitPane.test.tsx— 40 tests, 748 lines.Also left, and all small:
TestCaseclasses intest_scan.py(
LineCountCapTests,TreeSignatureTests,ScanTreeStreamingTests). Eachbuilds 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.TREES/BUILDING_DIMENSIONSreset block repeated in three files, and theone-line
flush()in two hook tests.streetOpacity.test.tsand the smallbuildings/files, which are named forbehaviours rather than modules but are not obviously wrong where they sit.
scrubController'swithCommitDateRanges— a port of the backend'scompute_commit_date_ranges— is deliberately not on that list. It shouldbecome 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