Fix five nightly failures from the anchored response bar and receipt rows - #182
Conversation
…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
left a comment
There was a problem hiding this comment.
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.
`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.
|
Verified against the new head rather than #182's base: merging RunsBoth
|
Problem
The nightly suite on
28fa324came 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 thecirun 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 on28fa324; each was attributed by running the failing test against that commit's tests withskills/leaf/assetschecked out at7df11b9(before #180) and7a9d875(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_journey—scripts/record-demo.pyselected a passage and clicked.lf-fab, which timed out withelement 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 isdisplay: contents. The demo types into.lf-fab-inputand sends with Enter.test_a_coined_class_cannot_reach_the_chromes_rules—lf-fab-inputappeared 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 forlf-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-itemexists 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_about— Acknowledge 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_bodyread 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— anUncaught TypeError … (reading 'event') (runtime/projection/fold.js:41)beside theread failedthe test expects. Acknowledge reader actions through Target Buttons #180 wired the margin's wholerenderto alf-acknowledgmentseventpaintAcknowledgmentsdispatches. 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 beforereconcileState()— early enough to read a candidate the same read is about to reject — and inside adispatchEvent, where the fault it throws is reported as an uncaught page error instead of rejecting the read. The row followslf-actionsinstead, 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:
drainOutboxreconciles the projection on a refused action and on a read event, and unlikewatchProjectionDrag,tickand a state application it dispatched nolf-actionsafterwards. 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✓ Outcomefor a move the log never took. A decision hid it, sincelf-optionsannounces a pick it rewinds and the margin hears that. The dispatch is now there,skills/leaf/CLAUDE.mdnames that pass beside the read and the heartbeat, andtest_a_refused_action_withdraws_its_outcome_from_the_margin_in_one_passsamples 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. Pinnedruff@0.16.1 check/format --check,prettier@3.9.6 --checkandeslint@10.9.1 --max-warnings=0on the touched files: all clean.typosis not installable in this sandbox, so CI'slintjob is the first to run it.Not in this PR: docs/demo.gif and docs/session-*.png are stale
record-demo.pydrives again, butdocs/demo.gifand the twodocs/session-*.pngstills 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 turningmaingreen.Automated fix for failed run