Name the browser that drew the page, and find the headless shell without counting directory levels - #203
Merged
Merged
Conversation
…ing levels Three findings from the review on #200, which merged before they were addressed. The `headless_shell` fixture found Playwright's registry root by counting two levels up from the Chromium executable. That is the Linux layout; on macOS the binary sits inside an app bundle, so `parents[1].name` is `Contents` and the `rsplit` raises IndexError before the fixture's own AssertionError can say anything useful — every test requesting the fixture errors. Naming the `chromium-<build>` directory rather than its depth holds on both. `version check --render` still reported "renders clean in Chrome" on a host that never ran Chrome. That is the same false claim the failure messages stopped making, so `launched_name()` sits beside `browser_hint()` and answers from the same place. The two arms of the end-to-end test now pin the browser each one names. The CLI help and export docstrings named Chrome alone. That is the surface a host reads with `--help` before it ever sees a failure message.
This was referenced Sep 1, 2026
leaf-agent
added a commit
that referenced
this pull request
Sep 1, 2026
#203 makes the same edit to the same line of `tests/conftest.py`, and it got there first (`47edb74` at 20:44Z against this branch's `77205a3` at 21:38Z). Two open PRs rewriting one line conflict whichever lands first, and put the same change through review twice; #203 is the narrower home, existing only for #200's follow-ups and naming the macOS app-bundle layout as its cause. The four `test_render_commands.py` setup errors this hunk cleared are #203's, which is where the PR body already attributed them.
A bare `next()` over `chromium.parents` raises `StopIteration` inside a fixture, which reads as an exhausted generator rather than as a Playwright layout this code does not know. #192 had made the same fix to this line with the guard on it; that branch has dropped its copy in favour of this one, so the readable failure comes here.
max-sixty
added a commit
that referenced
this pull request
Sep 1, 2026
## Problem [`ci` run 33516407327](https://github.com/max-sixty/leaf/actions/runs/33516407327) came back **10 failed, 1614 passed, 6 skipped**. Eight belong to #189, whose `ci` run was cancelled by #190's push before the nightly leg ever ran on it. #188's `test_mcp_app.py` case arrived on the same collision as the MCP test below and is carried here rather than left to red the next nightly. The everyday suite that gates a pull request does not run `test_render_*` or `test_site.py`, so a PR can land its nightly half unread — which is how these arrived, and how five more have arrived since, from #193, #194 and #200. `main` has moved a long way under this branch; the ones it now owns are taken on merge, and three of the new ones are cleared here. #203 reached `tests/conftest.py`'s `headless_shell` first, so the four `test_render_commands.py` setup errors are its. ## Solution Grouped by cause rather than by test, since several tests share one. **The example the tests still name.** #189 rewrote `examples/pr-walkthrough.html` into the Worktrunk review packet, and the shipped patch is now a collapsed manifest that builds no lines until a reader opens a file, which the scroller gate read as `getComputedStyle(null)`. The gate reads the diffs that have drawn lines, with its own non-vacuity floor kept. **The new package's chrome and surfaces.** `.lf-pr-description` stacked a second translucent accent onto a card that is already a tint of one, and the layer's own `--muted` reads 4.32:1 there; the description now sits on the raised-surface token. CallDiff's disclosure was a bare `<button>` rather than an `offer`, so an exported copy kept a hand over a press nothing can take. Its location anchor carried an `href` on the header row, which names no location at all — and `reachScrollers` reads a candidate for a focusable descendant before granting it a stop, so that hidden anchor answered "there is already a way in here" with a link nobody can reach, for a box whose words run off the side. **A reading that counted clipped text as painted.** `coveredWords` measured each run's whole rect, so an ellipsised name in a narrow column read as covering its neighbour. It now intersects each run with the boxes that clip it, stopping at an out-of-flow ancestor where a hidden overflow further out need not reach. The one test that plants this fault plants it properly now: the runtime's note is parked in a one-pixel box with hidden overflow, so opacity alone never put its characters on the screen. **Three focus rings the corpus stopped painting.** `code-pre-light` and `code-pre-shadow` are the tab stops on a scrolling code block and on a diff's lines, and with the old example gone no example held either. The page gets one rendered hunk and one code line long enough to scroll. Then `.lf-diff-review`: it took the outward ring it shared with `.lf-diff-next`, which stands on its own in the toolbar, while this one rides a file's summary row inside a box that clips at the row's bottom edge — its lower run fell 3px past what the sweep can see. It takes the inset the summary beside it already takes, and splitting the rule made each half declare the ring name neither had. **`--diffs-fg-number` on changed rows.** Surfaced by that rendered hunk: a changed row's number is drawn in the base its own fill is mixed from, green on green at 4.03:1. Pierre's number overrides now take the layer's tinted-surface inks. **A sidebar the page got shorter under.** #190's zero-height ToC anchor shortened the release page 58px, putting a named `scrollTo(0, 900)` past the stretch where the box stands on its own offset. Both edges of that stretch are the page's, so they are read off it, with a floor asserting the stretch exists before a point halfway along it says anything. **Playwright's loop against `asyncio.run` and `anyio.run`.** `sync_playwright()` keeps an asyncio loop running in its thread for its whole lifetime, and the `browser` fixture is session-scoped per xdist worker — so an MCP test's own loop start raises in any worker that has opened a browser and passes in one that has not, leaving the schedule to decide. Verified directly: `asyncio.events._get_running_loop()` is `None` before `sync_playwright()`, the loop inside it, `None` after. `interact_support.run_async` runs the entry point on a thread with no loop on it, and both MCP modules go through it — #204's three new exchanges included, since they landed on the direct call while this was in flight. **A declared verb no page replayed.** #194 added `lf-diff`'s `review` to the registry without an event for it on the standing-state page — the one fixture built so that exactly this fails rather than going unexercised. It gets a diff and a standing review. **A focus contract two site tests never heard about.** #193 kept the response field passive so a drag leaves the browser's own selection alone and a native copy still has something to take, and aligned the render tests with it. `test_site.py` is nightly-only. Its label test turned on `document.activeElement === field`, which is now false for the page's own words and the site's label alike — the discriminator was gone, not inverted — so it reads whether the field was offered at all, which is the difference it was always after. ## Testing `uv run pytest tests --run-nightly` and `pre-commit run --all-files` on this head. The everyday suite and lint are green on this branch in CI. Every claim about what `main` does was taken from a control run of the same tests in a clean checkout of `main`, not from reading. <details><summary>Where each failure landed</summary> | test | fix | | --- | --- | | `test_render_gate.py::test_the_runtime_holds_a_scroller_the_page_wrote` | reads the diffs that drew lines | | `test_render_export.py::test_an_exported_example_stands_on_its_own[pr-walkthrough]`, `[corpus]` | widget, probe, theme | | `test_render_controls.py::test_every_ring_the_layer_draws_is_shown_whole_somewhere_in_the_corpus` | example, then `.lf-diff-review`'s ring and both ring names | | `test_render_pages.py::test_a_left_sidebar_uses_the_margin_until_the_page_needs_it_back` | scroll position read off the page | | `test_interact_mcp.py::test_stdio_protocol_carries_the_app_resource_and_private_tool_result` | own thread | | `test_mcp_app.py::test_registered_server_prefers_full_page_and_keeps_snapshot_as_fallback` | own thread | | `test_render_projection.py::test_the_render_gate_applies_every_standing_action_a_second_time` | standing fixture (#194's) | | `test_site.py::test_the_label_is_chrome_rather_than_words_to_quote`, `::test_a_comment_lands_in_the_thread_with_its_quote` | the field is offered, not entered (#193's) | | `test_render_anchors.py::test_a_data_bound_diff_aims_and_selects_one_source_line` | **#194's and #193's, taken on merge** | | `test_site.py`, `test_render_export.py` stale `<h1>` | **#202's, taken on merge** | The two exported-copy cases each carried four faults, three of them masked: `offering` asserts before `covered`, which asserts before the copy's axe run, so each fix uncovered the next. The ring sweep did the same — the geometry fault stood in front of a naming check that had been silent about a rule missing its name since it was written. </details> <details><summary>Three this leaves, all of them main's</summary> **`test_render_controls.py::test_examples_have_no_serious_wcag_a_or_aa_violations[pr-walkthrough]` stays red.** The `.lf-pr-description` contrast above is fixed and confirmed gone. What holds the test red is 46 `nested-interactive` findings from #194: each file's "Mark reviewed" `<button>` sits inside that file's `<summary>`, and a disclosure with a focusable descendant is a serious WCAG failure. Clearing it means taking the press out of the disclosure and re-laying the row — `entry.node` is the `<details>` in ten places, and the theme, print and export rules key on it — which is a change to that widget's shape with visual verification of its own, not a line this branch should slip in. **`test_render_anchors.py::test_a_failed_fragment_hydration_waits_for_a_reader_retry` fails whenever anything runs before it.** Confirmed on unmodified `main`: three runs of the file at `-n4`, three failures, and the same failure at `-n0` over the whole file. It passes alone every time. `assert len(requests) == 2` sees one — the reopened disclosure's second fragment fetch never reaches the route. Arrived with #189. **`test_render_anchors.py::test_a_drag_released_mid_word_hugs_words_and_sentences` fails over the whole file and passes alone.** `assert 'inside' == 'inside it'` — the selection did not grow to the word boundary. It looks like the next instance of what #191 diagnosed, Chromium 151 collapsing a synthetic range on mouse release. Also still behind: the gallery's `docs/example-pr-walkthrough.jpg`. Regenerating here rewrites all nine stills in substituted faces, because `theme.css` asks for Charter and this runner's `fc-match` has none of the serif stack — the same reading #175 made. Left for the authoring machine. </details> --- Automated fix for [failed run](https://github.com/max-sixty/leaf/actions/runs/33516407327) > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: leaf-agent <318509791+leaf-agent@users.noreply.github.com> Co-authored-by: Maximilian Roos <m@maxroos.com> 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.
The review on #200 raised four things; the ISC004 lint fix landed before the merge and these three did not. They are all on
mainnow, so this is the follow-up.The
headless_shellfixture only works on Linux. It found Playwright's registry root by counting two levels up from the Chromium executable, which is the Linux layout (ms-playwright/chromium-<build>/chrome-linux/chrome). On macOS the binary is inside an app bundle —chromium-<build>/chrome-mac/Chromium.app/Contents/MacOS/Chromium— soparents[1].nameisContentsand"Contents".rsplit("-", 1)[1]raisesIndexErrorbefore the fixture's ownAssertionErrorguard can say anything useful. Four tests request the fixture, so all four error out. CI is ubuntu-only, which is why it stays green here, buttests/CLAUDE.mdtells a developer to run the complete browser file before handing over a browser-facing change, and that file is this one. Naming thechromium-directory instead of counting depth holds on both layouts.The success line named Chrome on a host that never ran Chrome.
renders clean in Chromeis the same false claim on the way out that #200 stopped the failure messages making.launched_name()sits besidebrowser_hint()inrender_gate/browser.pyand answers from the same place, so the two cannot drift.The CLI help named Chrome alone.
check's--renderoption help, its docstring, andexport_page's docstring are the surface a host reads with--helpbefore it ever sees a failure message.Verification
The two arms of
test_check_render_refuses_what_only_a_browser_can_seenow pin the browser each one names rather than just"renders clean", so the success-line change is what the test is about:I have not run macOS from CI, so the app-bundle layout is Playwright's documented one rather than one I observed. The fix does not depend on it: searching the parents for
chromium-<build>holds however deep the executable sits.Fixture change:
tests/conftest.py