Skip to content

Read the page-map chord's progress from its keycaps, not the key line's text - #157

Closed
leaf-agent wants to merge 2 commits into
mainfrom
fix/ci-33345565778
Closed

Read the page-map chord's progress from its keycaps, not the key line's text#157
leaf-agent wants to merge 2 commits into
mainfrom
fix/ci-33345565778

Conversation

@leaf-agent

@leaf-agent leaf-agent commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Problem

ci on 1c7d033 failed with 31 nightly tests red. Rerunning all 31 node ids against c0a5b65 leaves these two, which nothing owns (the rest of the residue is owned: the catalog digest by #156, and, at the suite's own parallelism, the ring walk by #155) —

FAILED tests/test_render_margin.py::test_g_addresses_the_page_map_prefix_in_its_announced_order
FAILED tests/test_render_margin.py::test_one_margin_item_owns_a_targets_controls_information_and_more_actions
AssertionError: Locator expected to contain text 're.compile('m\s*page-map items')'
Actual value: gTThreads panelgMPage mapgm1–9page-map itemsgg / Gtop / bottomesccancel?more

Both press g, assert the key line reads m page-map items, press m, then assert it reads 1–9 page-map items — reading the chord's progress out of the line's text. aa41f7a ("Keep chord hints stable through progress") made that reading impossible on purpose: every destination now carries its complete route from the moment g arms the chord, and a press changes a keycap's face rather than adding a key. So the row reads g m 1–9 page-map items at both stages — the first assertion never matches, and the second would match before m was pressed at all. test_render_navigation.py was updated for the new contract in that commit (the route table asserts exactly ["g", "m", "1–3"] with ["pressed", "neutral", "neutral"] after g alone); these two sites were missed. They are nightly-only, so the gap surfaced on main rather than on the PR.

Solution

Read the route and its progress the way the same commit's own coverage does: locate the row by data-lf-commands~="navigation.page-map-item", assert its keycaps spell g m and the span the capped list holds, and assert each cap's data-lf-key-state. address_span derives that span from the item count using address.js's own rule (1–n, or 1 for a single member) rather than writing 1–9 out, so the helper states the cap instead of restating one page's arithmetic.

This is strictly more than the text match proved. The old assertion could not distinguish g from g m; the new one fails if the route loses a step, gains one, spells a step differently, or stops advancing its faces. Mutating the expected states to all-neutral fails it; mutating the span fails it.

The steps and the states are both read once, on the repaint's own frame taken by key_line(page), with plain asserts over evaluate_all — the settle test_render_navigation.py uses at the equivalent site. A retrying expect is what key_line() writes down a rule against, because a value that moves with the chord goes green on whichever tick lands inside the assertion's budget, so a repaint that arrived only on the two-second heartbeat would still pass. expect is left for to_have_count(1) and the row's word, neither of which the chord advances.

Testing

On this branch at 136e068, Playwright driving real Chromium on this runner:

  • uv run pytest tests/test_render_margin.py --run-nightly -n4 — 22 passed. Both named tests fail on c0a5b65 unmodified and pass here.
  • The two named tests at -n0, three consecutive runs — green each time, so the non-retrying read is not buying a flake.
  • uv run pytest tests — 740 passed, 6 skipped. This is the gate ci runs on the PR; the two tests fixed here are nightly-only and are not exercised by it, so the local nightly runs above are the evidence for them.
  • ruff check and ruff format --check on the changed file — clean, at the v0.16.1 .pre-commit-config.yaml pins.
Mutation check, and why the third failure is not here

Mutating the expected states to ["neutral"] * 3 fails the assertion at At index 0 diff: 'pressed' != 'neutral', so the states claim bites. Instrumented states on the real page: ['pressed', 'neutral', 'neutral'] after g, ['pressed', 'pressed', 'neutral'] after m.

The rest of the run's residue on c0a5b65 is owned elsewhere and does not overlap this PR's one file: test_site.py::test_the_public_catalog_is_a_visual_index_of_full_page_routes (tracked in #133) is #156's and touches only docs/, and test_render_controls.py::test_every_ring_the_layer_draws_is_shown_whole_somewhere_in_the_corpus, which surfaces at the suite's own parallelism, is #155's.


Automated fix for failed run

@leaf-agent

Copy link
Copy Markdown
Collaborator Author

This PR and #156 are between them the whole of what is still red on main. I was dispatched for run 33344876930 (db2dd20, "Indent enclosed selection hints"), which reported 33 nightly failures; re-running all 33 on c0a5b65 leaves 3, and they are this PR's two margin cases plus the preview digest #156 recaptures. The other 30 were carried by the eleven commits between db2dd20 and c0a5b65, so the ci-fix queue has converged rather than accumulated. I derived the same fix independently before finding this PR and dropped it — same two call sites, same read of the keycap states; yours derives the span from the list's own count where mine wrote 1–9 out, which is the better of the two.

What was measured, and where the remaining three sit

Reproduced on c0a5b65 with the 33 node ids under uv run pytest … --run-nightly -n2, taking tests/test_render_drafts.py whole rather than by node id (test_render_margin.py and test_render_drafts.py together: 72 passed). By file:

file failures in the run still failing on c0a5b65
test_render_drafts.py 14 0
test_render_anchors.py 4 0
test_render_controls.py 4 0
test_render_navigation.py 3 0
test_render_projection.py 2 0
test_render_conversations.py, test_render_gate.py, test_render_pages.py 1 each 0
test_render_margin.py 2 2 — this PR
test_site.py 1 1 — #156

The margin pair traces to aa41f7a "Keep chord hints stable through progress", which moved test_render_anchors.py and test_render_navigation.py onto the keycap states and left these two reading the key line's words. Worth noting for the review: since the route now reads g m 1–n at both stages, the old second assertion (1–9\s*page-map items) would also have matched at the first stage, so re-spelling the expected text would have greened the test while dropping the distinction between the two presses. Reading the states is what puts it back.

On the digest: #156's manifest records 3c23a202c81cef43340e43469baf35de948201754ae23233dbf49ea3027d958a, which is what preview_build.digest(preview_build.capture_input_files()) computes on c0a5b65 here, and its branch has c0a5b65 as an ancestor — so it is correct against the current tip rather than against the tree it was cut from. The one input that had moved since the previous capture is skills/leaf/assets/runtime/chrome-style.js, from 6d68a1b (#152). The treadmill #133 and #134's body describe does not bite on the currently open queue: #155 and #158 touch only tests/, and this PR only tests/test_render_margin.py, none of which capture_input_files() hashes — so on this queue #156 can land in any order without going stale.

@leaf-agent leaf-agent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The route reading itself checks out against the runtime: range(n) in address.js is n > 1 ? "1–n" : "1" over addressed(entry) (capped at MAX_NUMBERED_ADDRESSES = 9), so address_span derives the same string, and keyline.js builds [chord[0], ...completeRowSteps(row)] with progressStates(steps, chord.length) — exactly g m 1–n with the leader painted pressed. I ran both tests on this branch (2 passed), and the all-neutral mutation fails at At index 0 diff: 'pressed' != 'neutral', reproducing the PR body's table.

One finding, on the settle rather than the claim.

expect(keys.nth(pressed - 1)).to_have_attribute("data-lf-key-state", "pressed") is an auto-retrying assertion on the key line, which is the one place tests/ writes down a rule against. key_line() in tests/render_harness.py says why: "Read once and never retried, which is the point of it: a disclosure's word is either what the watch painted within the press or what the two-second heartbeat paints later, and an assertion that retries goes green on whichever tick lands inside its budget — reading a stale line as an eventually right one." So if the repaint that marks a step pressed stopped happening within the press and only arrived on the heartbeat, this gate still passes inside its five seconds. That is the axis the PR body argues on, and it is the one the retry gives away.

It also isn't the shape test_render_navigation.py uses at the equivalent site, which the body claims. That site presses g, calls key_line(page) once, and then reads both the steps and the states with plain assert ... == states over evaluateexpect(...) is reserved for to_have_count, the aria-label, and the word, none of which move. The read shape here does match it; the settle is the opposite of it.

The inline suggestion swaps to that form. It needs key_line added to the render_support import, which is outside the diff:

from render_support import (
    ...
    key_line,
    leaf_page,
    ...
)

I ran the suggested form on this branch: 2 passed at -n4, and 5 consecutive -n0 runs green, so the non-retrying read is not buying a flake here.

Comment thread tests/test_render_margin.py
@leaf-agent

Copy link
Copy Markdown
Collaborator Author

The residue on c0a5b65 is four, not three — #155 is needed too. I was dispatched for run 33345751267 (d836303, "Read the margin layout the resize scheduled before measuring the sidebar page"), whose 31 failures are byte-identical to the set this PR's body already links from run 33345565778. Replaying all 31 node ids on c0a5b65 at the suite's own parallelism leaves this PR's two margin cases, #156's catalog digest, and test_render_controls.py::test_every_ring_the_layer_draws_is_shown_whole_somewhere_in_the_corpus — the case #155 carries. The comment above reads that file as 0-still-failing because its replay ran -n2; the ring test is load-sensitive, so the count turns on the parallelism the replay used rather than on the code. Nothing here changes this PR: it still owns its two, and there is no new fix to open.

What was measured

Failure sets compared by diffing gh run view <id> --log-failed for 33345751267 and 33345565778 — 31 node ids each, comm reports no line on either side alone.

Replay on c0a5b65 (current main tip), all 31 node ids in one uv run pytest --run-nightly invocation at default -n auto: 4 failed, 27 passed in 100.77s.

still failing on c0a5b65 owner
test_render_margin.py::test_g_addresses_the_page_map_prefix_in_its_announced_order this PR
test_render_margin.py::test_one_margin_item_owns_a_targets_controls_information_and_more_actions this PR
test_site.py::test_the_public_catalog_is_a_visual_index_of_full_page_routes #156
test_render_controls.py::test_every_ring_the_layer_draws_is_shown_whole_somewhere_in_the_corpus #155

The ring failure is the one #155 names: 1 of the layer's 32 rings are painted nowhere the corpus can be walked to: version-diff (.lf-version-diff:is(:focus-visible, .lf-focus-visible)). Run alone at -n0 on the same tip it passed 3 for 3, ~49s each — which is what running-tend means by -n0 classifying in one direction only, and it matches #155's reading that the ring walk presses its next key before the opened layer has placed focus. #155's own test check is green at 851d99f.

@leaf-agent

Copy link
Copy Markdown
Collaborator Author

Run 33346442884 (6d68a1b) reported five nightly failures and adds no new work: this PR's two margin cases, #156's catalog digest, and test_render_controls.py::test_a_seat_conversation_leaves_the_pick_it_is_about_live plus test_render_navigation.py::test_the_ring_holds_on_a_seat_the_agent_has_still_to_answer, both of which read Asks (N) one higher than they expect. That pair is #151's, which merged as d98ed92 after the run's commit — both pass on c0a5b65 here at -n0, and d98ed92's diff edits those two functions directly, so it is the fix rather than a coincidence.

I also closed #159, the duplicate of this PR, in favour of this one — it was opened twelve minutes later, its checks were still running, and its own comment had already said only one should merge. Its key_line_route harness helper is the one part worth grafting here if you want it; the branch is left in place.

The helper waited for the pressed face with an auto-retrying
`expect(...).to_have_attribute` on the key line. `key_line()` in
tests/render_harness.py writes down the rule against exactly that: a
disclosure's word is either what the watch painted within the press or what
the two-second heartbeat paints later, so a retried read of a value that
moves goes green on whichever tick lands inside its budget. A repaint that
stopped happening within the press would still have passed.

Settle the way tests/test_render_navigation.py does at the equivalent site:
take the repaint's own frame once with `key_line(page)`, then read the steps
and their states with plain asserts over `evaluate_all`. `expect` is left
only for the count and the row's word, neither of which the chord advances.
`to_have_count(1)` keeps the locator honest now that no text assertion
implies a single match.
@leaf-agent

Copy link
Copy Markdown
Collaborator Author

This is the first full nightly run on c0a5b65 itself, and it measures the residue the comments above re-derived from other commits' node ids: exactly three failures, and they are this PR's two margin cases plus #156's catalog digest. Nothing else on main is red.

That settles the "four, not three" reading above. test_render_controls.py::test_every_ring_the_layer_draws_is_shown_whole_somewhere_in_the_corpus — the case that comment attributed to #155 — ran on gw0 at the suite's own parallelism in this run and passed, so it is not gating main. It shouldn't be read as retired: #155 diagnoses it as a focus race between v and the ArrowUp that follows, and a race that passes once is unclassified, not fixed. It just isn't what's holding ci red.

Verified locally rather than inferred: on c0a5b65 unmodified the three fail, and with this branch's tests/test_render_margin.py and #156's docs/ applied together they pass. The two diffs don't overlap — one file each, tests/ and docs/ — so they can land in either order.

Verification
# c0a5b65, unmodified
uv run pytest --run-nightly -n0 \
  tests/test_site.py::test_the_public_catalog_is_a_visual_index_of_full_page_routes \
  tests/test_render_margin.py::test_g_addresses_the_page_map_prefix_in_its_announced_order \
  tests/test_render_margin.py::test_one_margin_item_owns_a_targets_controls_information_and_more_actions
3 failed in 21.41s

# same three, with both branches' files applied over c0a5b65
3 passed in 13.30s

The digest arithmetic checks out too: main commits inputs_sha256 0836f336… while the checkout computes 3c23a202…, and #156's manifest is 3c23a202… — the computed value, not a third one.

No fix PR from run 33347270702; the two open PRs are the whole of it.

@max-sixty

Copy link
Copy Markdown
Owner

Folded into #161 at 5efdaf9: the page-map chord tests now read structured keycaps and pressed-state progress through the shared helper. #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