fix(smoke): assert the SPA DOM marker is attached, not visible #29

Merged
forgejo-admin merged 1 commit from fix-spa-marker-attached into main 2026-08-01 14:22:55 +00:00

Closes the last firing lunacycle health-check alert. The app was healthy the whole time — the check was asserting something the health-check host cannot do.

Symptom

✗ smoke_spa: SPA smoke check failed: forSelector: Timeout 15000ms exceeded.
  - waiting for locator('text=smoketest_bot') to be visible
    28 × locator resolved to hidden <span class="text-sm hidden sm:inline">smoketest_bot</span>

The marker element is present, with the right text, at a 1280×720 viewport where the sm: breakpoint applies.

Diagnosis

The failure post-mortem wired up in #20 is what gave it away. In /tmp/lunacycle-smoke on celilo-mgr:

  • spa-failure.html — the fully rendered app. Every task, category, and the navbar username.
  • spa-failure.requests.txt — all three /api batch requests succeeded.
  • spa-failure.png — 1280×720, complete layout: icons, cards, colors… and zero text anywhere.

celilo-mgr has no fonts installed. No /usr/share/fonts, no fontconfig, fc-list absent.

Probed it directly on the host with the deployed playwright-core, unauthenticated against the public login page:

"Waning Gibbous"          w=0    h=0    display:block       visibility:visible
"Lunacycle"               w=0    h=0    display:block       visibility:visible
"Sign In"                 w=382  h=0    display:block       visibility:visible
"Sign in with Authentik"  w=382  h=44   display:inline-flex   (height is padding, not text)

state=attached: OK in 153ms
state=visible:  FAILED after 5022ms

Every text box collapses to zero height — there are no glyphs to lay out. Computed visibility is visible and display is block; it's the empty bounding box that makes Playwright call it hidden. The only element with real height is the button, and that height is padding.

So the alert was firing on the health-check host's font packaging, not on anything about the deployment.

Fix

state: 'attached' — presence, not visibility.

That is the assertion this check actually wanted. Its job is to prove the SPA executed and rendered data it had to fetch first. Layout and visibility belong to the e2e suite, which runs in the Playwright container where fonts exist. Tying a production alert to text rendering means it fires on host font packaging instead of on the deployment.

The change is in runSpaSmokeCheck, so it covers all three callers: the health_check hook, the smoke-after-deploy bus subscriber, and the simulated e2e.

Verification

  • bun run test:unit — 58 passed, 7 files.
  • tsc on scripts/smoke/spa-smoke.ts — clean. (Two pre-existing Browser type errors in the two celilo hook scripts reproduce identically on main; they're an artifact of the ad-hoc --ignoreConfig invocation, not this change.)
  • The probe above is the direct proof: attached resolves in 153ms where visible times out, on the actual host, against the actual deployment.
  • The probe script was copied into the module dir to run, then removed — /var/celilo/modules/lunacycle is back to its published contents.

Also worth doing (not in this PR — your call, it's a host change)

apt install fonts-dejavu-core on celilo-mgr. Not needed for the check to pass, but the failure screenshots are currently textless, which cost a round trip on this very diagnosis.

Status of the other checks on 1.0.4+1

Verified live via celilo module health lunacycle --debug:

✓ smoke_login: OIDC login succeeded for smoketest_bot
✓ smoke_api:   API responded (HTTP 200, 6ms)
✓ smoke_ws:    WS upgrade succeeded (25ms)

smoke_login passing with the real bot credentials was the last unverified piece of #20.

Closes the last firing lunacycle health-check alert. The app was healthy the whole time — the check was asserting something the health-check host cannot do. ## Symptom ``` ✗ smoke_spa: SPA smoke check failed: forSelector: Timeout 15000ms exceeded. - waiting for locator('text=smoketest_bot') to be visible 28 × locator resolved to hidden <span class="text-sm hidden sm:inline">smoketest_bot</span> ``` The marker element is present, with the right text, at a 1280×720 viewport where the `sm:` breakpoint applies. ## Diagnosis The failure post-mortem wired up in #20 is what gave it away. In `/tmp/lunacycle-smoke` on celilo-mgr: - `spa-failure.html` — the fully rendered app. Every task, category, and the navbar username. - `spa-failure.requests.txt` — all three `/api` batch requests succeeded. - `spa-failure.png` — 1280×720, complete layout: icons, cards, colors… and **zero text anywhere**. celilo-mgr has no fonts installed. No `/usr/share/fonts`, no fontconfig, `fc-list` absent. Probed it directly on the host with the deployed `playwright-core`, unauthenticated against the public login page: ``` "Waning Gibbous" w=0 h=0 display:block visibility:visible "Lunacycle" w=0 h=0 display:block visibility:visible "Sign In" w=382 h=0 display:block visibility:visible "Sign in with Authentik" w=382 h=44 display:inline-flex (height is padding, not text) state=attached: OK in 153ms state=visible: FAILED after 5022ms ``` Every text box collapses to **zero height** — there are no glyphs to lay out. Computed `visibility` is `visible` and `display` is `block`; it's the empty bounding box that makes Playwright call it hidden. The only element with real height is the button, and that height is padding. So the alert was firing on the health-check host's font packaging, not on anything about the deployment. ## Fix `state: 'attached'` — presence, not visibility. That is the assertion this check actually wanted. Its job is to prove the SPA executed and rendered *data it had to fetch first*. Layout and visibility belong to the e2e suite, which runs in the Playwright container where fonts exist. Tying a production alert to text rendering means it fires on host font packaging instead of on the deployment. The change is in `runSpaSmokeCheck`, so it covers all three callers: the `health_check` hook, the `smoke-after-deploy` bus subscriber, and the simulated e2e. ## Verification - `bun run test:unit` — 58 passed, 7 files. - `tsc` on `scripts/smoke/spa-smoke.ts` — clean. (Two pre-existing `Browser` type errors in the two celilo hook scripts reproduce identically on `main`; they're an artifact of the ad-hoc `--ignoreConfig` invocation, not this change.) - The probe above is the direct proof: `attached` resolves in 153ms where `visible` times out, on the actual host, against the actual deployment. - The probe script was copied into the module dir to run, then removed — `/var/celilo/modules/lunacycle` is back to its published contents. ## Also worth doing (not in this PR — your call, it's a host change) `apt install fonts-dejavu-core` on celilo-mgr. Not needed for the check to pass, but the failure screenshots are currently textless, which cost a round trip on this very diagnosis. ## Status of the other checks on 1.0.4+1 Verified live via `celilo module health lunacycle --debug`: ``` ✓ smoke_login: OIDC login succeeded for smoketest_bot ✓ smoke_api: API responded (HTTP 200, 6ms) ✓ smoke_ws: WS upgrade succeeded (25ms) ``` `smoke_login` passing with the real bot credentials was the last unverified piece of #20.
fix(smoke): assert the SPA DOM marker is attached, not visible
All checks were successful
pr-validate / validate (pull_request) Successful in 17s
24f7439fd2
smoke_spa was the last failing production health check, and the app was
healthy the whole time. Playwright reported:

  28 × locator resolved to hidden
       <span class="text-sm hidden sm:inline">smoketest_bot</span>

The marker was present with the right text, at a 1280x720 viewport where
the sm: breakpoint applies. The failure post-mortem wired up in the
previous fix is what gave it away: spa-failure.html holds the fully
rendered app and all three /api batch requests succeeded, while
spa-failure.png shows the complete layout — icons, cards, colors — with
zero text anywhere.

celilo-mgr has no fonts: no /usr/share/fonts, no fontconfig. Probed it
directly on the host with the deployed playwright-core, unauthenticated:

  "Waning Gibbous"  w=0   h=0   display:block  visibility:visible
  "Sign In"         w=382 h=0   display:block  visibility:visible
  "Sign in with Authentik" w=382 h=44          (height is padding, not text)

  state=attached: OK in 153ms
  state=visible:  FAILED after 5022ms

Every text box collapses to zero height because there are no glyphs to
lay out, so Playwright calls it hidden. The alert was firing on the
health-check host's font packaging, not on the deployment.

state: 'attached' is what this check actually wanted. Its job is to prove
the SPA executed and rendered data it had to fetch first; layout and
visibility belong to the e2e suite, which runs in the Playwright
container where fonts exist.

Applies to all three callers of runSpaSmokeCheck — the health_check hook,
the smoke-after-deploy bus subscriber, and the simulated e2e.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.