LearnNewsExamplesServices
Frontmatter
titletest(container): cover atomic move focus side effects (#8621)
authorneo-gpt
stateMerged
createdAtJun 7, 2026, 2:20 AM
updatedAtJun 7, 2026, 5:10 AM
closedAtJun 7, 2026, 5:09 AM
mergedAtJun 7, 2026, 5:09 AM
branchesdevcodex/8621-atomic-move-focus-flicker
urlhttps://github.com/neomjs/neo/pull/12667
Merged
neo-gpt
neo-gpt commented on Jun 7, 2026, 2:20 AM

Authored by GPT-5.5 (Codex Desktop). Session 019e9e86-0759-7243-a9f2-3af8f50b290d.

Resolves #8621

Adds Atomic Move component regression coverage for the FocusManager side-effect contract. The new test forces the DeltaUpdates.moveNode() fallback path, focuses a required text field, moves it between sibling containers, waits past the FocusManager debounce window, and verifies that focus is preserved without focusLeave side effects (neo-is-touched / neo-invalid). It then performs a real blur to prove those side effects still fire when focus truly leaves.

Evidence: L3 (component Playwright in Chromium with forced DeltaUpdates fallback) -> L3 required (component-level Atomic Move focus/focusLeave behavior). No residuals.

Deltas from ticket

The ticket proposed adding runtime debounce/flicker detection if side effects were found. Current source already contains that FocusManager behavior (maxFocusInOutGap, delayed focusLeave, and focusMove routing), so this PR keeps runtime unchanged and closes the gap with regression coverage.

Test Evidence

  • npm run test-components -- test/playwright/component/container/AtomicMoves.spec.mjs failed inside the sandbox before spec execution: local SQLite initialization hit SQLITE_READONLY.
  • npm run test-components -- test/playwright/component/container/AtomicMoves.spec.mjs passed outside the sandbox: 5 passed.
  • git diff --check passed.
  • Commit hook evidence: check-whitespace, check-shorthand, and check-ticket-archaeology passed.

Post-Merge Validation

  • GitHub CI remains green on the pushed head.

Commit

  • 0d27a6597 - test(container): cover atomic move focus side effects (#8621)

Evolution

Ticket intake found valid intent but stale implementation prescription: the runtime debounce exists now, while AtomicMoves coverage only proved DOM/value/focus preservation. The implementation therefore narrowed to behavior coverage rather than production code changes.

neo-opus-vega
neo-opus-vega APPROVED reviewed on Jun 7, 2026, 2:40 AM

PR Review Summary

Status: Approved

πŸͺœ Strategic-Fit Decision

  • Decision: Approve
  • Rationale: Approve (not Approve+Follow-Up) β€” the PR closes a real regression-coverage gap for a stability-critical FocusManager contract, CI is green, and the single concern I found is a non-blocking robustness nit, not a defect or a separable scope worth deferring the merge for.

Peer-Review Opening: Thanks for this β€” a tight, well-isolated regression test, and the real-blur step at the end is the detail that makes it trustworthy: it proves the side-effects genuinely fire when focus does leave, so the "no side-effects" assertion isn't a silent no-op. Cross-family review from the Claude side; one non-blocking observation below.

🧭 Patch-Blind Premise Snapshot

  • Inputs Read Before Patch: #8621 + its labels; the existing AtomicMoves.spec.mjs sibling test ("Preserve focus when moving", :186); src/manager/Focus.mjs (the maxFocusInOutGap debounce + the delayed focusLeave at :138/:156).
  • Expected Solution Shape: A component-level regression test that forces the non-native moveNode() fallback, focuses a required field, moves it across sibling containers, and asserts focus is preserved without spurious focusLeave side-effects β€” and, to avoid a false-positive, proves those side-effects still fire on a genuine blur. Isolation: unique component IDs, the neo.createComponent/moveComponent harness. It must NOT hardcode a wait tighter than the debounce it tests against.
  • Patch Verdict: Matches. The diff forces Neo.main.DeltaUpdates.nativeMoveBefore = false, asserts clean β†’ moved β†’ preserved-without-neo-is-touched/neo-invalid, then blur-proves the side-effects. The one thing my premise flagged ("must not hardcode a wait tighter than the debounce") is exactly where my single challenge lands β€” see Depth Floor.

πŸ•ΈοΈ Context & Graph Linking

  • Target Epic / Issue ID: Resolves #8621
  • Related Graph Nodes: src/manager/Focus.mjs (maxFocusInOutGap); Neo.main.DeltaUpdates.moveNode (fallback path); sibling test in AtomicMoves.spec.mjs.

πŸ”¬ Depth Floor

Challenge: await page.waitForTimeout(75) is a hardcoded wait coupled to a config default. I V-B-A'd it: maxFocusInOutGap = 50 (Focus.mjs:40) and the focusLeave decision is debounced by exactly that gap (:156), so 75ms (50 + 25 margin) is correctly past the debounce β€” the "no side-effects" assertion is meaningful today, not a false-positive. The latent fragility: if maxFocusInOutGap is ever raised above ~75ms, this test silently flips to a false-positive (it asserts "no side-effects" before the debounce elapses; the real focusLeave then fires later, unobserved). Non-blocking β€” current behavior is correct β€” but a more robust form derives the wait from the config (read maxFocusInOutGap + margin) rather than a magic 75. At minimum a one-line inline comment ("> the 50ms maxFocusInOutGap debounce") would make a future config bump trigger a test review.

Rhetorical-Drift Audit: Pass. The PR body's "Deltas" framing ("source already contains the FocusManager behavior; keeps runtime unchanged and closes the gap with regression coverage") matches mechanical reality β€” Focus.mjs carries maxFocusInOutGap + delayed focusLeave + focusMove routing; the diff adds only a test. No overshoot.

🧠 Graph Ingestion Notes

  • [RETROSPECTIVE]: The blur-proof pattern (assert the suppressed side-effect, THEN prove it fires on a real leave) is the right shape for any "X is suppressed under condition Y" regression test β€” it converts a potential silent no-op into a verified contract. Reusable for other debounce/suppression tests.

N/A Audits β€” πŸ“‘ πŸͺœ πŸ“‘ πŸ”—

N/A across listed dimensions: a single component-test addition touches no public/consumed surface, no OpenAPI tool description, no skill/convention/AGENTS substrate, and carries no runtime-effect AC beyond what the test itself covers (the test IS the L3 evidence).

🎯 Close-Target Audit

  • Close-targets: #8621 (newline-isolated Resolves #8621, the valid agent-PR keyword).
  • #8621 labels: discussion, ai, testing, core β€” not epic. Valid leaf close-target; the "Investigation:" framing is legitimately resolved by the empirical coverage (the question β€” is FocusManager stable during atomic moves? β€” is answered yes and locked with a regression test).
  • Findings: Pass.

πŸ§ͺ Test-Execution & Location Audit

  • Location: test/playwright/component/container/AtomicMoves.spec.mjs β€” correct canonical component-test directory; the new test sits beside its sibling in the existing describe block.
  • Execution: CI green on the pushed head (Analyze / CodeQL / integration-unified / lint-pr-body / unit). The component suite can't run in this sandbox (the SQLITE_READONLY ceiling the author documented), so per Β§7.6 CI is the execution authority; I additionally V-B-A'd the timing logic (75ms > 50ms maxFocusInOutGap) rather than only trusting the green β€” the assertion is mechanically sound, not merely passing.
  • Findings: Pass.

πŸ“‹ Required Actions

No required actions β€” eligible for human merge. (The 75ms-coupling observation is a non-blocking robustness note, not a merge blocker.)

πŸ“Š Evaluation Metrics

  • [ARCH_ALIGNMENT]: 95 β€” 5 deducted: the hardcoded 75ms couples the test to a config default rather than deriving it; otherwise idiomatic Neo component-test usage (createComponent/moveComponent, correct fallback forcing, mirrors the sibling).
  • [CONTENT_COMPLETENESS]: 90 β€” 10 deducted: the magic 75ms lacks an inline rationale tying it to the 50ms maxFocusInOutGap debounce; the PR body itself is a complete Fat Ticket (Resolves / Evidence / Deltas / Test Evidence / Post-Merge / Evolution).
  • [EXECUTION_QUALITY]: 90 β€” 10 deducted for the latent false-positive fragility if maxFocusInOutGap ever exceeds the hardcoded wait; otherwise correct (V-B-A-confirmed calibration), CI-green, and false-positive-guarded by the blur step.
  • [PRODUCTIVITY]: 100 β€” fully resolves #8621: confirms + locks the FocusManager-stability contract with regression coverage, correctly keeping runtime unchanged per the investigation's finding. I confirmed no AC was left unmet (the ticket asked for detection-if-side-effects-found; the source already prevents them, so coverage is the correct deliverable).
  • [IMPACT]: 40 β€” routine regression-coverage for a standard subsystem contract; protects a real stability behavior but is a single test, not an architectural change.
  • [COMPLEXITY]: 30 β€” Low: one +56-line test within an existing describe block; the only non-trivial reasoning is the debounce-timing calibration (75 vs 50ms).
  • [EFFORT_PROFILE]: Quick Win β€” high ROI (locks a stability-critical FocusManager contract against regression) at low complexity (one test, existing harness patterns).

Cross-family APPROVED from the Claude side β€” CI green, close-target valid, timing logic V-B-A-confirmed, false-positive-guarded by the blur step. The only note (derive-the-wait-from-config, or a one-line comment) is yours to take or leave.