Skip to content

Recapture the gallery stills so the preview digest matches main - #156

Closed
leaf-agent wants to merge 1 commit into
mainfrom
fix/ci-33343623121
Closed

Recapture the gallery stills so the preview digest matches main#156
leaf-agent wants to merge 1 commit into
mainfrom
fix/ci-33343623121

Conversation

@leaf-agent

Copy link
Copy Markdown
Collaborator

Problem

The nightly ci run on 7d219d1 reported 30 failures. 29 of them were fixed by the 15 commits that landed on main afterwards (#137, #145, #148#153); rerunning all 30 node ids against c0a5b65 leaves exactly one still red, and it is red for a reason none of those commits addressed:

FAILED tests/test_site.py::test_the_public_catalog_is_a_visual_index_of_full_page_routes
AssertionError: preview inputs changed — rerun scripts/example-previews.py

The cause is a merge-order gap, not a flake. #134 recaptured the gallery stills from a branch based at 4b5b4b8 (#149) — before 6d68a1b (#152) rewrote skills/leaf/assets/runtime/chrome-style.js to hoist the keycap box into KEY_BOX. The squash merge took main's stylesheet and the branch's stills together, so docs/example-previews.json shipped an inputs_sha256 computed over a tree that no longer exists. capture_input_files() digests that stylesheet, so the manifest and the checkout disagreed from the moment #134 landed.

Isolating the single differing input

Enumerating capture_input_files() at #134's head and at c0a5b65 and diffing per-file hashes gives exactly one row:

< skills/leaf/assets/runtime/chrome-style.js 3052c62e…  (#134's branch, pre-#152)
> skills/leaf/assets/runtime/chrome-style.js 1fca956b…  (main, post-#152)

git merge-base cd998bf c0a5b65 is 4b5b4b8, which is an ancestor of 6d68a1b — the branch predates the stylesheet change.

Solution

Rerun scripts/example-previews.py against current main, which is the remedy the assertion message names. This is the same fix #134 applied, redone from a base that has #152 in it — #134 merged rather than being turned down, so the approach stands; only its base was stale.

Eight of the nine JPEGs re-encode byte-identically, so the capture is reproducible here and no font or renderer drift is being introduced. Only example-ship-review.jpg moves, and only across an 8×8 pixel region at (776, 520) with a maximum channel difference of 8 — the key line corner that #152's keycap box accounts for. The manifest's inputs_sha256 now reads 3c23a202…, matching the digest computed over the checkout.

Left alone, and worth a maintainer's call rather than a bot's: nothing stops this recurring. Any recapture PR silently goes stale if a commit touching a capture input merges between its base and its own merge, and the staleness only surfaces on main after the fact. Requiring branches to be up to date before merging would close it, but that is a repository setting with effects well past this script.

Testing

  • uv run pytest --run-nightly over all 30 node ids from the failed run at c0a5b65: 29 passed, 1 failed — establishing this as the only surviving failure.
  • uv run pytest --run-nightly tests/test_site.py — 20 passed on this branch (was 1 failed on main).
  • uv run pytest tests — 740 passed, 6 skipped.
  • Recomputed preview_build.digest(preview_build.capture_input_files()) against the working tree and confirmed it equals the committed inputs_sha256.

Automated fix for failed run

PR #134 captured the stills from a branch based at #149, before #152 rewrote
skills/leaf/assets/runtime/chrome-style.js. The squash merge kept main's
stylesheet and the branch's stills, so docs/example-previews.json recorded an
inputs_sha256 for a tree that no longer exists and test_site.py's catalog check
went red on main.

Rerun scripts/example-previews.py against current main. Eight of the nine JPEGs
re-encode byte-identically; only example-ship-review.jpg moves, by an 8x8 pixel
region in the key line corner that #152's keycap box accounts for.
@leaf-agent

Copy link
Copy Markdown
Collaborator Author

Reproduced this capture independently and it comes back byte-identical, which is the thing a binary diff cannot show on its own. I was dispatched for run 33344080789 (21332fc, "Simplify tab selection"), whose 32 failures leave three on c0a5b65 — this PR's digest and #157's two margin cases. Before finding this PR I ran scripts/example-previews.py on c0a5b65 on a different runner and got the same inputs_sha256 and the same nine preview hashes, example-ship-review.jpg included. So the recapture is a function of the checkout rather than of the machine that took it, and nothing here needs a second PR.

What was compared

uv run pytest --run-nightly over all 32 node ids from run 33344080789 at c0a5b65: 3 failed, 29 passed in 75.84s — this test, plus the two test_render_margin.py page-map cases #157 and #159 both carry.

Fresh uv run python scripts/example-previews.py on c0a5b65, Chrome 151.0.7922.173 via channel="chrome". git status afterwards reported exactly the two paths this PR changes: docs/example-previews.json and docs/example-ship-review.jpg. Comparing that manifest against this branch's:

inputs equal: True
previews identical: True count 9 differing: []

inputs_sha256 3c23a202c81cef43340e43469baf35de948201754ae23233dbf49ea3027d958a and ship-review c9aa4f07472ad5c65bf90652c5a000f1dc2e267b30b8861d7bb9bf48a2198b55 on both sides. uv run pytest --run-nightly tests/test_site.py — 20 passed with those bytes in place.

@leaf-agent

Copy link
Copy Markdown
Collaborator Author

This recapture no longer satisfies the gate at main's tip, and it is not this branch that moved: 834e68c ("Open anchored comments in inline threads") edits assets/leaf.js, runtime/living-margin.js and runtime/composing/selection.js, all hashed inputs, so it re-staled the manifest the same way #133 predicts.

Measured here with capture_input_files()/digest() out of scripts/example-previews.py, PYTHONPATH=scripts:

tree manifest computed
c0a5b65 0836f336… 3c23a202… this branch's value was current here
834e68c (tip) 0836f336… 77e6dd79… stale

Since none of the nine stills nor example-previews.json is itself hashed, squashing this branch onto the tip leaves 3c23a202… against a computed 77e6dd79… — still red. #162, which I have just opened for the fourth failure on run 33347154927, edits runtime/conversation/thread-list.js and moves it again to 6da94927….

I am not recapturing from CI: the fonts here substitute, per the fc-match results in #133, so the stills would ship re-set in a generic serif. What this needs is the rebase-and-recapture-last scheduling that #133 is open about.

@max-sixty

Copy link
Copy Markdown
Owner

Folded into #161 at 5efdaf9: the public gallery was recaptured with scripts/example-previews.py, including the stale ship-review still and the generator-owned manifest hashes. #161 passes the complete 1,574-test nightly suite.

Closing this duplicate in favor of #161.

This was written by Codex on behalf of max-sixty

@max-sixty max-sixty closed this Aug 31, 2026
max-sixty pushed a commit that referenced this pull request Aug 31, 2026
… is hidden (#160)

## Problem

`ci` on
[72be4be](https://github.com/max-sixty/leaf/actions/runs/33345068975)
failed with 35 nightly tests red. Rerunning all 35 node ids against
`c0a5b65` leaves four: the catalog digest (#156), the two page-map chord
reads (#157), and this one, which nothing owns.

```
FAILED tests/test_render_controls.py::test_a_wide_banner_spends_status_copy_before_action_reach
AssertionError: Locator expected to be hidden
  6 × resolved to <button ... class="lf-btn lf-answer-all ...">✓ Accept all (3)</button>
  3 × resolved to ... ✓ Accept all (2)
  4 × resolved to ... ✓ Accept all (1)
```

The count is walking down and running out of budget. The blanket answer
[decides its decisions one at a
time](https://github.com/max-sixty/leaf/blob/8683b9b1d2b9d408506c9f88bf3baf5a4f84685c/skills/leaf/assets/runtime/decisions/view.js#L68-L71)
— `await source[verb]?.()` per decision — so the press owes one round
trip per decision, and the test released the held first answer straight
into `expect(answer_all).to_be_hidden()`. That single 5s budget was
covering three sequential server trips, not the repaint it is for.
`tests/CLAUDE.md` names this exactly: "Letting `expect` absorb the
page's next read hides which mechanism supplied the wait and spends its
timeout budget on transport rather than on the assertion."

Instrumenting the sequence on an idle runner gives `event → state →
event → state → event`, 1.89s end to end. Three trips at ~0.6s each fit
inside 5s on an idle machine and do not on a loaded one, which is why
this is nightly-and-under-load only.

`test_accept_all_decides_every_pending_suggestion` drives the same
control and does not have the bug: it waits for each widget's own settle
first, so its `to_be_hidden` only has to cover the last repaint, and
[its comment says
why](https://github.com/max-sixty/leaf/blob/8683b9b1d2b9d408506c9f88bf3baf5a4f84685c/tests/test_render_widgets.py#L1541-L1543).
This site was the one that skipped the staging.

## Solution

State the transport with `_until` on the `Traffic` counters — the same
causal helper this test already uses eight lines above to hold the
answer in the wire — then let `to_be_hidden` assert only the repaint it
is about. The number of trips owed is read off the control's own face
(`✓ Accept all (3)`) rather than written out, so the fixture's
arithmetic stays in one place, and an `owed > 1` guard fails loudly if a
fixture change ever leaves this test a single trip and stops it
exercising the sequence at all.

This is not a raised timeout. `_until` is bounded at 30s with the
counters in its failure message, so a genuinely stuck answer now fails
naming the missing evidence instead of pointing at a control that "never
went".

## Testing

Playwright driving real Chromium on this runner, at `8683b9b`:

- `uv run pytest tests` — **740 passed, 6 skipped**. This is the gate
`ci` runs on the PR; the test fixed here is nightly-only and not
exercised by it.
- `uv run pytest tests/test_render_controls.py --run-nightly -n4` — **64
passed**, three times consecutively on an otherwise-idle machine.
- `ruff check` and `ruff format --check` on the changed file, at the
`v0.16.1` `.pre-commit-config.yaml` pin — clean.

<details><summary>Reproducing the failure, and the mutation
checks</summary>

The test passes 6/6 at `-n0` on `c0a5b65` unmodified, so `-n0` alone
leaves it unclassified. Stalling the driver for 300ms between
`held[0].continue_()` and the hide assertion reproduces the CI shape
exactly on the unfixed test — the button walks `(3) → (2) → (1)` and the
expect runs out:

```
5 × ... ✓ Accept all (3)
5 × ... ✓ Accept all (2)
3 × ... ✓ Accept all (1)
```

With this change in place the same stall passes. Two mutations confirm
the new wait bites rather than just returning:

| mutation | result |
| --- | --- |
| `traffic.sends == owed` → `owed + 1` | fails: `the page never settled
every one of the 3 answers the blanket press owed: the wait began on
sends=1 acked=0 pending=1 asked=3 heard=3 and gave up on sends=3 acked=3
pending=0 asked=6 heard=6` — and this also confirms `owed` reads 3 and
that the predicate's own terms are what carry it |
| driver stall re-applied to the fixed test | passes in 8.55s |

Timed trace of the released sequence on an idle runner, seconds from
`continue_()`:

```
0.38 FIN event    0.66 FIN state
1.17 FIN event    1.38 FIN state
1.81 FIN event    → hidden at 1.89
```

The other `.lf-answer-all` sites were checked for the same shape and do
not have it: `test_render_gate.py` navigates away,
`test_render_startup.py` puts `wait_for_function(BOTH_STAMPS)` between
the release and the hide assertion and has one decision rather than
three, and `test_render_projection.py` asserts the control is absent.

</details>

---
Automated fix for [failed
run](https://github.com/max-sixty/leaf/actions/runs/33345068975)

Co-authored-by: leaf-agent <318509791+leaf-agent@users.noreply.github.com>
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.

2 participants