Skip to content

Fix five nightly failures from the anchored response bar and receipt rows - #182

Merged
max-sixty merged 2 commits into
mainfrom
fix/ci-33465817706
Sep 1, 2026
Merged

Fix five nightly failures from the anchored response bar and receipt rows#182
max-sixty merged 2 commits into
mainfrom
fix/ci-33465817706

Conversation

@leaf-agent

@leaf-agent leaf-agent commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Problem

The nightly suite on 28fa324 came back 5 failed, 1598 passed, 6 skipped. None of them are #177's: #178, #180 and #181 landed within twenty minutes of each other, and #179's new concurrency group cancelled the ci run for each one (33462972424, 33464164957, 33464333976), so the first nightly run to complete after them was #177's and it reported all three PRs' debts at once. Every one reproduces deterministically on 28fa324; each was attributed by running the failing test against that commit's tests with skills/leaf/assets checked out at 7df11b9 (before #180) and 7a9d875 (before #181).

Solution

Four of the five follow the two runtime changes; the fifth is a fault the same read already rejected, reported twice.

  • test_demo_recording_drives_the_browser_journeyscripts/record-demo.py selected a passage and clicked .lf-fab, which timed out with element is not visible. Open anchored response fields immediately #181 opens the anchored field in place, so the Comment button is a Tab away and the composer around the field is display: contents. The demo types into .lf-fab-input and sends with Enter.
  • test_a_coined_class_cannot_reach_the_chromes_ruleslf-fab-input appeared in the document-level class surface. Open anchored response fields immediately #181 names it there only inside :where(:not(.lf-fab-input)) on the general textarea rule — the same negation shape the pin's own comment already describes for lf-copy — so the pin widens on purpose rather than the rule moving.
  • test_reaction_choices_and_their_receipt_share_an_unided_selected_block — the reaction choices a selection raises are the bar's own since Open anchored response fields immediately #181, and no .lf-margin-item exists at that moment at all. The test presses the token in the bar and keeps its claim, which is about the receipt's placement: the durable section coordinate must not pull the receipt to the section's top.
  • test_the_pointer_over_a_comment_lights_the_passage_it_is_aboutAcknowledge reader actions through Target Buttons #180's receipt row makes each thread card 36px taller (178px → 214px), so with three cards the last one's bottom (852) sits below the scroller (797) and behind the panel foot. card_body read its point off the card's rect alone and landed on .lf-general, hovering no card. The helper now clamps to the part of the list the reader can see.
  • test_a_reply_toast_survives_a_failed_state_and_keeps_its_agent — an Uncaught TypeError … (reading 'event') (runtime/projection/fold.js:41) beside the read failed the test expects. Acknowledge reader actions through Target Buttons #180 wired the margin's whole render to a lf-acknowledgments event paintAcknowledgments dispatches. The margin row's acknowledgment face comes from the state projection, so that put a projection read inside the panel render the state application performs before reconcileState() — early enough to read a candidate the same read is about to reject — and inside a dispatchEvent, where the fault it throws is reported as an uncaught page error instead of rejecting the read. The row follows lf-actions instead, which every state application dispatches once it has reconciled and which both receipt-painting paths sit inside; the now-unlistened dispatch goes with it.

Review of the row's new coordinate found the one pass that reconciles without telling anyone, and this branch fixes that too: drainOutbox reconciles the projection on a refused action and on a read event, and unlike watchProjectionDrag, tick and a state application it dispatched no lf-actions afterwards. So a surface reading the projection rather than the DOM stood on the withdrawn state until the heartbeat — a board move the server refuses returns to its column while the margin goes on showing ✓ Outcome for a move the log never took. A decision hid it, since lf-options announces a pick it rewinds and the margin hears that. The dispatch is now there, skills/leaf/CLAUDE.md names that pass beside the read and the heartbeat, and test_a_refused_action_withdraws_its_outcome_from_the_margin_in_one_pass samples the card and the marker together on every frame so the heartbeat's repair cannot stand in for the pass under test.

Testing

uv run pytest tests --run-nightly — the exact gate the failing job runs — 1604 passed, 6 skipped (23:50 before the review commit, 34:48 with it on a loaded CI runner). uv run pytest tests (the PR gate): 755 passed, 6 skipped. Pinned ruff@0.16.1 check / format --check, prettier@3.9.6 --check and eslint@10.9.1 --max-warnings=0 on the touched files: all clean. typos is not installable in this sandbox, so CI's lint job is the first to run it.

Not in this PR: docs/demo.gif and docs/session-*.png are stale

record-demo.py drives again, but docs/demo.gif and the two docs/session-*.png stills committed beside it were recorded before #181 and still show the Comment button where the page now shows the open field. Nothing in the suite checks their freshness, so a regeneration is a separate change with a large binary diff rather than part of turning main green.


Automated fix for failed run

…rows

#178, #180 and #181 landed within twenty minutes of each other, and #179's
new concurrency group cancelled the `ci` run for each one, so the nightly
browser suite first ran on #177's push and reported all three PRs' debts at
once.

- `record-demo.py` still clicked the Comment button and typed into a second
  composer box. #181 opens the anchored field in place, so the button is one
  Tab away and the composer draws nothing: the demo types into the field and
  sends with Enter.
- `lf-fab-input` joins the document-level class pin. #181 names it there only
  under a negation, the way `lf-copy` is named, so the pin widens on purpose.
- The reaction choices a selection raises are the bar's own since #181; the
  margin holds the receipt they leave. The unided-block test presses the token
  in the bar and keeps its claim on the receipt's placement.
- `card_body` reads a point low on a comment's card. #180's receipt row makes
  each card 36px taller, which puts the last card's bottom behind the panel
  foot, so the helper clamps to the part of the list the reader can see.
- The margin row's acknowledgment face comes from the state projection, so it
  follows `lf-actions` rather than the receipt paint. Driven from the paint it
  ran inside the panel render the state application performs before
  reconciliation, reading a candidate that read was about to reject — and it
  ran inside a dispatch, where the fault became an uncaught page error instead
  of rejecting the read.

`uv run pytest tests --run-nightly`: 1603 passed, 6 skipped.

@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.

Moving the row onto lf-actions is the right coordinate — skills/leaf/CLAUDE.md already defines it as firing "after a complete state has reconciled", which is exactly what the row's projection read needs, and it removes the read from inside a dispatchEvent where the fault surfaces as an uncaught page error. One finding on the justification the comment gives for it, inline.

The rest reads clean against the runtime: selection.js sets composer.style.display = open ? "contents" : "none" and sendKey: "Enter" is the composer's send binding, so both of the demo's new assumptions hold; createThreadCards is imported only by conversation/reconcile.js, so .lf-thread cards render only inside .lf-threads and card_body's clamp is reading the right scroller; and the reaction test keeps the receipt-placement assertion its docstring is about.

Secondary and non-blocking: adding lf-fab-input to the document-level set also drops it out of the leak probe, since the probe wears [...scoped].filter(c => !global_.has(c)). That costs nothing for lf-copy — all four of its occurrences sit outside the @scope (.lf-chrome) block, so it has no scoped rule to leak — but lf-fab-input is scoped-dressed (textarea.lf-fab-input { width: 216px; … }). Nothing observable today, because the probe is a <div> and those selectors are textarea-qualified; worth knowing that the exemption keys on "named at document level" rather than "dressed at document level", so the next class of this shape leaves the probe silently too.

Comment thread skills/leaf/assets/runtime/living-margin.js Outdated
`drainOutbox` reconciles the projection on a refused action and on a read
event, and it was the one such pass that told no one: `watchProjectionDrag`
and `tick` both dispatch `lf-actions` after theirs, and a state application
dispatches its own. A surface that reads the projection rather than the DOM
therefore kept a withdrawn winner until the heartbeat two seconds later —
measured on a board move the server refused, where the card returns to its
column while the margin goes on showing `✓ Outcome` for a move the log never
took. A decision hides it: `lf-options` dispatches `lf-answered` for a pick it
rewinds, which the margin also listens to, so the same refusal on `lf-decision`
repaints from the widget's own signal.

The new test samples the card and the margin marker together on every frame and
every task from before the refusal is answered, since the reconciliation and the
render it announces are one turn: a margin left behind is sampled hundreds of
times, and an `expect` would have taken the heartbeat's repair for the pass
under test.
@leaf-agent

Copy link
Copy Markdown
Collaborator Author

main went red again on 91e8a9d (#183) with run 334727044485 failed, 1598 passed, 6 skipped, the same five tests this PR fixes. #183 touches only skills/leaf/scripts/leaf/http.py, served_state/service.py and tests/test_interact_server.py, none of which this branch goes near, so nothing here needs revising; main just hasn't got the fix yet.

Verified against the new head rather than #182's base: merging origin/main at 91e8a9d into this branch is clean, and the five run green on the merge while all five fail on 91e8a9d itself. Not pushing the merge — the PR still reports MERGEABLE.

Runs
# on 91e8a9d (origin/main)
5 failed in 43.47s

# on 91e8a9d + this branch (clean merge, caddd70)
5 passed in 27.00s

Both uv run pytest --run-nightly over:

  • tests/test_product_page.py::test_demo_recording_drives_the_browser_journey
  • tests/test_render_conversations.py::test_a_coined_class_cannot_reach_the_chromes_rules
  • tests/test_render_margin.py::test_reaction_choices_and_their_receipt_share_an_unided_selected_block
  • tests/test_render_navigation.py::test_the_pointer_over_a_comment_lights_the_passage_it_is_about
  • tests/test_render_pages.py::test_a_reply_toast_survives_a_failed_state_and_keeps_its_agent

@max-sixty
max-sixty merged commit 0c48ee3 into main Sep 1, 2026
6 checks passed
@max-sixty
max-sixty deleted the fix/ci-33465817706 branch September 1, 2026 05:54
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