Take 3D inspection off Preflight, and stop CI measuring the machine - #1
Merged
Conversation
Preflight answers two questions in order — which aircraft am I making, and is my paper ready — and then offers one action. An Inspect in 3D control sitting on the hero offered a second thing to do at the exact moment the screen exists to make one choice, and what it offered was a mouse-orbited model of the aircraft the cadet is about to build with their hands. The Hangar keeps live inspection, where browsing the fleet is the point; the fold engine remains authoritative for Tutorial geometry. So `OrbitViewer` becomes `AircraftHero`: production artwork, the engine's build-time SVG beneath it for the case where the image will not load, and nothing to operate. The renderer, the intersection observer, the retry path and the reset control all go with the button, and Preflight now never reaches for the renderer chunk at all rather than merely deferring it — `lazy-renderer.spec.ts` says so in those words. The five strings the Hangar borrowed from `preflight.*` move to `hangar.*`, which is now their only caller; `preflight.inspect3d` and `preflight.orbitHint` have no caller left and are dropped from all twelve locales. The per-plane inspection visual baselines go too, and the display stage — standing aircraft, graded backdrop, contact shadow — is baselined in the Hangar bay instead, which is where it still lives.
CI failed on `the tutorial holds one shape on every fold, on desktop`, and had the laptop and phone widths, a tutorial resume and a hangar cadence assertion flaky beside it. None of them is a product defect: the E2E suite rasterises every WebGL frame in software on a two-worker runner, the full run took 16.2 minutes, and the heaviest test in it sat through fourteen real Hammer folds three times over before running out of its 180 second ceiling. The subject of that test is where the rail's controls sit at each settled step — fourteen static layouts, not fourteen animations. Under `prefers-reduced-motion` the engine jumps straight to each settled state (`REDUCED_MOTION_SECONDS` is 1ms), so the walk costs rasterisation and nothing else. The 180s ceiling stays as what a genuine hang hits. The same guard was already on the visual-baseline spec and had been lost from it in this branch, which is what made three approved tutorial baselines appear to drift; restored, they match byte for byte. Local suite: 4.3 minutes to 1.5, with the Preflight inspection specs gone as well.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
flight-school | 089e898 | Aug 22 2026, 06:23 PM |
Smoking the deployed site lost two to four journey specs a run, on a different plane each time, always the same way: the step readout the test asserts on never appeared. It reproduced nowhere locally, and CI had been losing the same assertion in tutorial.spec for weeks with the same unattributable look. One race, one cause. The rail's fold selector is a controlled React `<select>` on a statically exported page, so between the HTML arriving and hydration finishing it is real, visible and operable with nothing listening. A `change` fired in that window is dropped, and React's first render restores `value` from its own state, so the page quietly stays on the fold it was already showing. Locally that window is a few milliseconds; over a network it is wide enough to lose. `data-material-ready` is the product's own answer to whether the stage is live — set from an effect on both the WebGL path and the static fallback, so it cannot be true before React is running. `chooseFold` waits for it and then selects, which is a gate rather than a retry: the test now waits for the same thing a person waits for. Live suite against https://flight-school-chi.vercel.app: 135 passed, 0 failed, where the run before it lost four.
Two follow-ups to the hydration fix, both about a threshold describing the machine instead of the product. `chooseFold` gated on `data-material-ready`, which is correct and costs a SwiftShader engine boot per call - four minutes across the suite for a wait no assertion afterwards needs. The step readout is the cheapest thing on the page only React can produce: the rail re-renders on the step change without waiting for the stage. Same gate, without the boot. `hangar-perf` asserted `> 20 frames in 3000ms` for the live scene, which is a 150ms-per-frame budget - the exact thing that test's own header says it must not assert, having been burnt by it twice already. It scored 10 on a 2-core CI runner under 4x throttling and 4 on a busy laptop, neither run carrying a defect. The failure it exists to catch is a stall, which produces one frame or none, so the floor is set there and the cadence stays in the annotation where a human can read it.
CI caught this on the new inspection spec, and it is a real defect, not a test artefact: clicking where Reset view appears in the Hangar navigates to the selected plane's Preflight instead. `.hangar-bay-status` was anchored bottom-left, which is where the placard already lives. Measured overlap with `.hangar-primary` is 63px at 1024x768, 1280x720, 1280x800 and 1440x900 — every desktop width — and the button is not merely unclickable but invisible, painted behind the red Start folding CTA. `document.elementFromPoint` at the centre of Reset view returns `a.hangar-primary`, and a raw click there lands on /en/plane/dart/. The z-index reads 18 against the placard's 16, and that comparison never happens: the placard hangs off `.hangar-main` while the status row hangs off `.hangar-bay` inside `.hangar-bay-column`, so the two indices resolve in different stacking contexts and never meet. Raising the number is not the fix, and would not be the fix even if it worked — two controls competing for one patch of screen is a layout bug, not a paint order one. The narrow breakpoint had already worked this out and put the row 8px under the control that opens inspection. This is the same relationship at desktop metrics, which also makes Reset view read as what it is: part of the inspection cluster, beside Show artwork, rather than a stray control in the corner. The mobile override's `bottom: auto` went with the base rule's bottom anchor. Verified at 1024x768, 1280x720, 1440x900 and 390x844: the hit test at the button's centre returns the button, the gap under the toggle is 8px at every width, and it is on screen.
The desktop width failed all three attempts on CI, at three different lines including a bare `page.evaluate`. That is not a hang, it is a pegged main thread, and the cause is upstream of the test: the stage's render loop has no dirty check, so it re-rasterises for as long as it is on screen whether or not anything has moved. A test that sits on the tutorial for two minutes therefore holds a WebGL surface at 60fps for two minutes, and `fullyParallel` had put the two most expensive widths on a two-core runner at once. Serialising the three takes the contention out. Scoped to a describe rather than the file, so the cheap layout tests around them still run in parallel. The loop itself is the real defect and is now written down in docs/KNOWN-ISSUES.md rather than fixed here: a still aeroplane redrawn sixty times a second is a warm laptop and a shorter battery on the screen a beginner spends longest on, and PRD criterion 8 already asks for the offscreen and hidden half of the same argument. Fixing it means marking dirty on pose, camera, resize, material load and control damping, and keeping the loop alive for OrbitControls damping, the display stage's breathing and lift, and any in-flight timeline. That is a renderer change with real edges, not something to squeeze into a layout fix. Local: layout.spec 14 passed in 34s, desktop 16.0s against a 180s ceiling.
The desktop width kept timing out on CI, and the failure snapshot said why: Playwright reported it could not resolve the Next fold button, while the page capture taken at that moment shows `button "Next fold" [active]` sitting there on step 11 of 14. The element was never missing. The main thread was too busy to answer the query. Too busy with what: `FoldRenderer.startLoop` has no dirty check, so an on-screen stage re-rasterises every frame whether or not anything moved, and a test that sits on the tutorial for three minutes holds a WebGL surface at 60fps for three minutes. On this laptop that is free — the same walk finishes in 13s at 6x CPU throttling — because there is a GPU here and none on the runner. Serialising the three widths was not enough, because the loop still competes with whatever the other worker is doing. So suspend it, through the product's own path rather than a stub: `useRenderActivity` pauses a stage while the document is hidden, which is PRD acceptance criterion 8, and a hidden document changes no layout at all — which is the entire subject of this test. Rendering is still exercised once per step, because `gotoStep` draws a frame explicitly when the loop is inactive. Measured, with a probe counting the frames the page will give it over two seconds: 52 with the loop running, 121 with it suspended, and the canvas still changes on every step in both. Locally the desktop walk goes 15.6s to 2s, layout.spec 34.2s to 10.2s, and the full E2E suite 141 passed in 1.3 minutes. The loop itself remains the real defect and stays recorded in docs/KNOWN-ISSUES.md; this makes the suite honest about what it is measuring rather than fixing the renderer from a layout test.
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.
Five commits: one product change the user asked for, three test fixes needed to prove it, and one real UI defect the third of those uncovered.
Take 3D inspection off Preflight (
b2e7f6e)Preflight answers two questions in order - which aircraft am I making, and is my paper ready - and then offers one action. An Inspect in 3D control on the hero offered a second thing to do at the exact moment the screen exists to make one choice, and what it offered was a mouse-orbited model of the aircraft the cadet is about to build with their hands.
OrbitViewerbecomesAircraftHero: production artwork, the engine's build-time SVG beneath it for when the image will not load, and nothing to operate. The renderer, the intersection observer, the retry path and the reset control all go with the button, so Preflight now never reaches for the renderer chunk at all rather than merely deferring it -lazy-renderer.spec.tssays so in those words.The Hangar keeps live inspection, where browsing the fleet is the point, and the fold engine stays authoritative for Tutorial geometry. The five strings the Hangar borrowed from
preflight.*move tohangar.*, now their only caller;preflight.inspect3dandpreflight.orbitHinthave no caller left and are dropped from all twelve locales.The Hangar's Reset view was buried under Start folding (
ed05f63)The per-plane Preflight inspection baselines went with the control, so the display stage is now baselined in the Hangar bay instead - and covering the Hangar's reset control for the first time is what found this.
Clicking where Reset view appears navigates to the selected plane's Preflight instead.
.hangar-bay-statuswas anchored bottom-left, which is where the placard already lives: measured overlap with.hangar-primaryis 63px at 1024x768, 1280x720, 1280x800 and 1440x900 - every desktop width - and the button is not merely unclickable but invisible, painted behind the red CTA.The z-index reads 18 against the placard's 16 and that comparison never happens: the placard hangs off
.hangar-mainwhile the status row hangs off.hangar-bayinside.hangar-bay-column, so the two indices resolve in different stacking contexts and never meet. The narrow breakpoint had already worked this out and put the row 8px under the control that opens inspection; this is the same relationship at desktop metrics.Three thresholds that were measuring the machine
30b9bde-the tutorial holds one shape on every foldmeasures fourteen static layouts and was sitting through fourteen real WebGL fold animations to reach them, until it hit its 180s ceiling. Underprefers-reduced-motionthe engine jumps straight to each settled state, so the walk costs rasterisation alone.244fd50- smoking the deployed site lost two to four journey specs a run, on a different plane each time. One race: the fold selector is a controlled React<select>on a statically exported page, so between the HTML arriving and hydration finishing it is visible and operable with nothing listening. Achangefired in that window is dropped and React then restoresvaluefrom its own state.bab4804-hangar-perfasserted> 20 frames in 3000msfor a live scene, which is a 150ms-per-frame budget: the exact thing that test's own header says it must not assert. It scored 10 on a 2-core CI runner and 4 on a busy laptop, neither run carrying a defect. The floor now sits where a stall lives.Verification
typecheck,lintclean🤖 Generated with Claude Code