Skip to content

test(simulator): bound the panel waits by a deadline, not a yield count - #13907

Merged
teamleaderleo merged 3 commits into
manaflow-ai:mainfrom
teamleaderleo:fix/yield-bounded-polls
Sep 23, 2026
Merged

teamleaderleo merged 3 commits into
manaflow-ai:mainfrom
teamleaderleo:fix/yield-bounded-polls

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

"A surviving Simulator host keeps framebuffer publication active" failed on #13414's app-host shard 1 with (panel.coordinator.frameTransport → nil) != nil at SimulatorPanelThemeTests.swift:95 and :101, while passing on main in the comparable run and on #13752's shards. Nothing in that PR touches this subsystem, so it presented as an unattributable red shard.

The waits in these two files spun a fixed yield count before asserting:

for _ in 0..<100 {
    if panel.coordinator.frameTransport != nil { break }
    await Task.yield()
}
#expect(panel.coordinator.frameTransport != nil)

A yield count is not a deadline. Task.yield() gives the scheduler a chance to run something else; it does not wait for anything. So 100 yields is however long 100 reschedules happen to take — microseconds on an idle machine, and unrelated to how long the awaited work actually needs. The bound tightens exactly when the runner is busy, which is the shape .github/review-bot-rules/test-determinism.md exists to prevent:

ALLOWED is a deadline-bounded poll of a real CONDITION that returns the instant the condition holds and only fails at a generous deadline — the deadline bounds the FAILURE path only, so load can make a pass slower but never turn a pass into a fail.

Resulting behavior

Ten polls across SimulatorPanelIntegrationTests and SimulatorPanelThemeTests now bound the same predicate with a ContinuousClock deadline instead. They still return the instant the condition holds, so a passing run is no slower; only a genuinely broken one waits out the 10 s. No assertion changed, and no test's meaning changed.

Deliberately left alone

The two bare for _ in 0..<100 { await Task.yield() } at SimulatorPanelIntegrationTests.swift:187 and :252 assert an absence (discoveryCount == 0, !isCompleted). A deadline cannot bound a wait for a non-event — the fix there is a positive completion signal to assert after, which changes what the test observes rather than how long it waits, and needs more knowledge of the worker lifecycle than this change carries. Their failure direction is also the opposite one: too short a spin yields a false green, not a false red.

Scope

17 more yield-count polls remain across 12 other cmuxTests files, and scripts/check-test-determinism.py reports 0 findings across the tree because it only matches sleep call sites — a yield spin contains no sleep. That gap and the full site list are in #13903. This PR is only the cluster with the observed failure.

Validation

swiftc -parse on both changed files. check-test-determinism.py --roots cmuxTests, tests/test_ci_pbxproj_test_wiring.sh and scripts/ci/validate_test_execution_registry.py pass. 127 of the 128 test commands in ci-guards.yml pass; the exception is tests/test_ghostty_zig_version_sync.sh, which needs the ghostty submodule this worktree does not check out and fails the same way on an unmodified tree.

These tests require a macOS runner, which I do not have — CI here is their first execution. A green shard is the evidence that matters; parse-level checks do not establish that the converted polls still observe what they used to.

— SlateHarrier g1 🗝️
run_cmux_test_failures_20260923_d · section D (test failures) of the 09-23 handoff.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Cha explodes. Let's final privacy approach: despite the task should capture only context.

And for the confusion: the task is titled "Claw Machine Translation needs to activate." In that I think about this actual account.

I'll spell out the session, final meaning.

Written for commit b7fa28f. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Tests
    • Improved reliability of simulator lifecycle and panel visibility tests by waiting for discovery, transport, and readiness conditions with a timeout rather than relying on fixed iteration counts.

`"A surviving Simulator host keeps framebuffer publication active"` failed
on manaflow-ai#13414's app-host shard 1 with `frameTransport → nil` at
SimulatorPanelThemeTests.swift:95 and :101, while passing on `main` and on
manaflow-ai#13752's shards. Nothing in that PR touches this subsystem.

The waits here spun a fixed `for _ in 0..<100 { ... await Task.yield() }`
before asserting. A yield count is not a deadline: `Task.yield()` gives the
scheduler a chance to run something else, it does not wait for anything, so
100 yields is however long 100 reschedules happen to take. The bound
tightens exactly when the runner is busy, which is the "fails on correct
code under load" shape `.github/review-bot-rules/test-determinism.md` bans:

  the deadline bounds the FAILURE path only, so load can make a pass slower
  but never turn a pass into a fail

These 10 sites now poll the same predicate against a `ContinuousClock`
deadline. They return the instant the condition holds, so a passing run is
no slower, and only a genuinely broken one waits out the 10 s. No assertion
changed.

Two sites are deliberately left alone: the bare
`for _ in 0..<100 { await Task.yield() }` at
SimulatorPanelIntegrationTests.swift:187 and :252 assert an *absence*
(`discoveryCount == 0`, `!isCompleted`). A deadline cannot bound a wait for
a non-event; those need a positive completion signal to assert after, which
is a change to what the test observes rather than how long it waits. Their
failure direction is also the opposite one — a short spin makes a false
green, not a false red.

Scope note: 17 more yield-count polls remain in 12 other `cmuxTests` files,
and `scripts/check-test-determinism.py` reports 0 findings across the tree
because it only matches sleep call sites. Tracked in manaflow-ai#13903; this commit is
the cluster with the observed failure.

Verified: `swiftc -parse` on both files; `check-test-determinism.py
--roots cmuxTests`, `test_ci_pbxproj_test_wiring.sh` and
`validate_test_execution_registry.py` pass; 127 of the 128 `ci-guards.yml`
commands pass, the exception being `test_ghostty_zig_version_sync.sh`,
which needs the ghostty submodule this worktree does not check out. Not
run on a macOS runner — these tests need one, so CI is their first
execution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: aeae2cfa-c7e1-4673-a4b7-8df4ff533142

📥 Commits

Reviewing files that changed from the base of the PR and between cd3ce57 and b7fa28f.

📒 Files selected for processing (2)
  • cmuxTests/SimulatorPanelIntegrationTests.swift
  • cmuxTests/SimulatorPanelThemeTests.swift

Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

Simulator panel integration and visibility tests replace fixed Task.yield() loops with asynchronous polling helpers. The helpers poll every 5 ms for up to 10 seconds, then assert that the condition is true.

Changes

Simulator panel test polling

Layer / File(s) Summary
Bounded asynchronous condition waits
cmuxTests/SimulatorPanelIntegrationTests.swift, cmuxTests/SimulatorPanelThemeTests.swift
Both test files add private waitUntil helpers and use them to wait for lifecycle, discovery, readiness, and framebuffer transport conditions. Affected tests are marked async throws.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~8 minutes

Change: Other

Suggested reviewers: austinywang

Merge Risk: ⚪ Minimal · up to b7fa2

The simulator panel tests now wait for readiness rather than relying on a fixed yield count. No actionable merge-blocking issue was found.

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: replacing yield-count waits with deadline-bounded waits in simulator tests.
Description check ✅ Passed The description explains the problem, resulting behavior, scope, validation, and test-environment limitation. It does not include the template checklist, review-trigger block, or demo-video section, b…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Cloud Persistent Session And Early Input ✅ Passed PASS. The authoritative diff changes only two simulator test files. It replaces fixed Task.yield() waits with a local 10-second waitUntil polling helper and updates test signatures. It does not ch…
Cmux Swift Actor Isolation ✅ Passed The pull request changes only two files under cmuxTests/. The diff updates test wait logic and test method signatures; it changes no production Swift files, actors, services, models, or UI stores. T…
Cmux Swift Blocking Runtime ✅ Passed PASS. The authoritative diff changes only cmuxTests/SimulatorPanelIntegrationTests.swift and cmuxTests/SimulatorPanelThemeTests.swift. The new ContinuousClock polling and Task.sleep calls are …
Cmux Browser Automation Off-Main ✅ Passed PASS. The authoritative diff changes only cmuxTests/SimulatorPanelIntegrationTests.swift and cmuxTests/SimulatorPanelThemeTests.swift. It replaces simulator test yield loops with a local `waitUnti…
Cmux Expensive Synchronous Load ✅ Passed PASS: The authoritative diff changes only cmuxTests/SimulatorPanelIntegrationTests.swift and cmuxTests/SimulatorPanelThemeTests.swift. The changes replace test Task.yield() loops with a private …
Cmux Cache Substitution Correctness ✅ Passed PASS: The authoritative PR diff changes only cmuxTests/SimulatorPanelIntegrationTests.swift and cmuxTests/SimulatorPanelThemeTests.swift. The changes replace test Task.yield() loops with private…
Cmux No Hacky Sleeps ✅ Passed PASS: The authoritative diff changes only two Swift test files under cmuxTests/. It introduces no TypeScript, JavaScript, shell, or build/runtime-script changes. The custom check explicitly targets …
Cmux Algorithmic Complexity ✅ Passed PASS. The authoritative diff changes only cmuxTests/SimulatorPanelIntegrationTests.swift and cmuxTests/SimulatorPanelThemeTests.swift. The changes replace test-only Task.yield() waits with a bou…
Cmux Swift Concurrency ✅ Passed The diff only replaces fixed Task.yield() test waits with an async throws waitUntil helper using ContinuousClock, Task.sleep, and #require. It adds no background DispatchQueue, `Dispatch…
Cmux Swift @Concurrent ✅ Passed The diff changes only two @MainActor test suites. The new waitUntil methods are private async coordination helpers that poll UI/test state and sleep briefly. They do not add nonisolated, `@concu…
Cmux Swift Package Boundaries ✅ Passed PASS: The authoritative diff changes only cmuxTests/SimulatorPanelIntegrationTests.swift and cmuxTests/SimulatorPanelThemeTests.swift. It replaces test wait loops with private test helpers and cha…
Cmux Swiftpm Lockfiles ✅ Passed The reviewed diff changes only cmuxTests/SimulatorPanelIntegrationTests.swift and cmuxTests/SimulatorPanelThemeTests.swift. It contains no Package.swift, Package.resolved, .gitignore, Xcode …
Cmux Swift Logging ✅ Passed PASS: The PR changes only cmuxTests/SimulatorPanelIntegrationTests.swift and cmuxTests/SimulatorPanelThemeTests.swift. The diff adds deadline polling and test declarations, with no print, `debug…
Cmux User-Facing Error Privacy ✅ Passed PASS. The authoritative PR diff changes only cmuxTests/SimulatorPanelIntegrationTests.swift and cmuxTests/SimulatorPanelThemeTests.swift. The changes replace test polling loops with a private test…
Cmux Full Internationalization ✅ Passed The authoritative PR diff changes only cmuxTests/SimulatorPanelIntegrationTests.swift and cmuxTests/SimulatorPanelThemeTests.swift. The changes replace test polling loops with a test-only `waitUnt…
Cmux Swiftui State Layout ✅ Passed PASS. The PR changes only two simulator test files. The diff replaces yield loops with an async deadline helper and updates test signatures. It does not add or expand ObservableObject, @Published, @Ob…
Cmux Architecture Rethink ✅ Passed PASS: The diff changes only two cmuxTests files. It adds waitUntil helpers that use ContinuousClock polling and Task.sleep, but the synchronization is test-only. No production Swift lifecycle …
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS. The authoritative diff changes only cmuxTests/SimulatorPanelIntegrationTests.swift and cmuxTests/SimulatorPanelThemeTests.swift. It replaces test polling loops and adds private test helpers;…
Cmux Source Artifacts ✅ Passed PASS: The PR changes only two tracked Swift test source files: cmuxTests/SimulatorPanelIntegrationTests.swift and cmuxTests/SimulatorPanelThemeTests.swift. The diff adds test wait logic and remove…
Cmux No Test Or Debug Seam In Production Source ✅ Passed PASS: The authoritative PR diff changes only cmuxTests/SimulatorPanelIntegrationTests.swift and cmuxTests/SimulatorPanelThemeTests.swift. It changes test wait logic and adds private test-target `w…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

teamleaderleo and others added 2 commits September 23, 2026 00:19
Review of the first revision found two things the yield-count -> deadline
conversion did not fix.

`SimulatorPaneCoordinator.receive(.frameTransport:)` stores the descriptor
only `if frameIsVisible`, and nothing re-sends it. Polling for
`coordinator.frameTransport != nil` therefore cannot recover a descriptor
that arrived early: it turns a permanent drop into a ten-second wait for a
latch that will never flip. The surviving-host test now waits for
`.setFramebufferPublishing(true)` — the message `reconcileFramePublication`
enqueues when it sets `frameIsVisible` — before emitting the descriptor, so
the drop window is closed rather than polled across.

The ten converted waits also each ended in a bare `break`, so a wait that ran
out fell through into whatever assertion came next; one of them
(`cancelledApplicationTerminationRestoresPanel`) had no assertion on its own
predicate at all and reported a confusing downstream failure instead. They
now share a `waitUntil` helper that requires its predicate at the deadline,
which makes that shape impossible to write and matches the spelling already
used in CloudTerminalCardFlashRegressionTests and CloudTunnelLaunchGateTests.
The helper sleeps 5 ms between probes rather than spinning `Task.yield()`, so
a failing wait no longer pegs the main actor for its full budget.

The two bare settle spins are left alone: both assert an absence, so a
deadline would only make them slower without adding signal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Run 35831156475 on macOS 26 failed at the gate itself: the wait for
`.setFramebufferPublishing(true)` ran its full 10 s and never saw the
message, while every other wait in both suites passed.

`reconcileFramePublication` returns early unless `status == .streaming`
(SimulatorPaneCoordinator+FrameVisibility.swift:72), so the publication
message is not the reliable precursor to the descriptor I took it for, and
gating the emit on it only deadlocks the test against a message that this
path does not produce here. The premise was inferred, not measured; the
measurement says it is wrong.

The census also does not support calling this a real defect. "A surviving
Simulator host keeps framebuffer publication active" passes on main at
f3d204a, a9b0329 and ce1c55c, and passed both earlier dispatches of
this branch. Its one observed failure is on manaflow-ai#13414's run, which had 148
failing tests and corresponding load — which is exactly the shape a fixed
yield count fails under, and exactly what the deadline conversion fixes.

So the original premise stands on its own and the escalation was
unnecessary. The `waitUntil` helper stays: the full "Simulator panel
integration" suite passed in the same run, covering all seven converted
sites there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Independent review, and what it changed

Posting this once, as the handoff requires, with the outcome rather than just the verdict. An independent agent reviewed b7fa28f9db against .github/review-bot-rules/test-determinism.md. Verdict: approve with nits on the mechanical conversion — all ten sites keep identical assertions, none can pass because a deadline expired, and every loop body still suspends. It raised two defects, and one of them was right in a way that cost me a revision.

1. The conversion did not fix the flake the first revision claimed it did. SimulatorPaneCoordinator+Events.swift:97 stores the descriptor only if frameIsVisible, and nothing re-sends it, so polling frameTransport != nil cannot recover a descriptor that arrived early. I took that finding, confirmed it, and tried to fix it by waiting for .setFramebufferPublishing(true) before emitting.

CI then disproved my fix. Run 35831156475 failed at my own new wait — ten seconds, message never seen, while every other wait in both suites passed. reconcileFramePublication returns early unless status == .streaming (SimulatorPaneCoordinator+FrameVisibility.swift:72), so that message is not the precursor I inferred it was. Reverted in b7fa28f9db.

The census also says there is no defect here to fix. "A surviving Simulator host keeps framebuffer publication active" passes on main at f3d204a46, a9b032969 and ce1c55c84, and passed both earlier dispatches of this branch. Its one observed failure is on #13414's run — which had 148 failing tests and matching load, which is exactly the shape a fixed yield count fails under, and exactly what this PR fixes. The original premise stands on its own; the escalation was mine and it was wrong.

2. One converted wait asserted nothing about its own predicate and fell through to a confusing downstream failure. Fixed structurally rather than locally: all ten sites now share a waitUntil helper that #requires its predicate at the deadline, so that shape cannot be written. It matches the spelling already in CloudTerminalCardFlashRegressionTests:75 and CloudTunnelLaunchGateTests:618, and sleeps 5 ms between probes instead of spinning Task.yield(), so a failing wait no longer pegs the main actor for its whole budget.

The two bare settle spins are deliberately unchanged; the reviewer independently confirmed both assert an absence, where a deadline adds no signal.

Verification

Run 35844928424 on blacksmith-6vcpu-macos-26 @ b7fa28f9db: 22 tests in 2 suites passed, covering every one of the ten converted sites across both SimulatorPanelVisibilityTests and SimulatorPanelIntegrationTests. I checked which tests executed, not just the check colour — an earlier dispatch of mine went green while selecting a type that contained none of the changed sites.

That run's job is red, for a reason unrelated to this PR: main's new Clean owned app-host home step fails on the e2e lane with FAIL: refusing to inspect app hosts outside runner temp, because scripts/ci/cleanup-app-host-home.sh:149 requires CMUX_DERIVED_DATA_PATH under RUNNER_TEMP while that lane sets it to …/cmux/DerivedData/cmux-e2e. Filed separately — it fails every e2e dispatch after its tests pass.

suite-coverage is red only because the pull-request lane is compile-only and never runs cmuxTests/, so I am labelling no-full-ci: the dispatched runs above are the execution evidence, and full-ci would add seven shards to a saturated macOS-15 queue only to inherit main's twelve unrelated failures.

— SlateHarrier g1 🗝️

@teamleaderleo teamleaderleo added the no-full-ci Records that skipping the macOS suite on a test-only diff is deliberate label Sep 23, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Correction to the previous comment: I said the Clean owned app-host home failure on run 35844928424 was filed separately. It was already fixed before my run started, by #13943 ("ci: put the E2E test job's DerivedData under RUNNER_TEMP"), merged 10:11Z — my run started 10:14Z and still carried the pre-fix workflow, since a dispatch resolves the workflow at queue time. Nothing to file; I should have checked the file's history before saying I would.

The diagnosis itself was right — scripts/ci/cleanup-app-host-home.sh:149 requires CMUX_DERIVED_DATA_PATH under RUNNER_TEMP, and that lane set it to …/cmux/DerivedData/cmux-e2e — which is exactly what #13943 changed.

Re-dispatched on the fixed workflow: 35848529299. The test evidence is unchanged either way — 22/22 passed in the previous run; only the post-test cleanup step was red.

— SlateHarrier g1 🗝️

@teamleaderleo
teamleaderleo merged commit eae58a7 into manaflow-ai:main Sep 23, 2026
62 of 71 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026
rustybret pushed a commit to rustybret/bmux that referenced this pull request Sep 23, 2026
eae58a7 test(simulator): bound the panel waits by a deadline, not a yield count (manaflow-ai#13907)
3df9a41 ci: let the pull-request macOS lane move pools without breaking Xcode selection (manaflow-ai#13923)
a9bdaa8 Add edge fade to Files filter chips (manaflow-ai#13584)
270d970 fix(web): let the Vercel ignore step see the previous deployment (manaflow-ai#13947)
2ae26d1 ci: put the E2E test job's DerivedData under RUNNER_TEMP (manaflow-ai#13943)

# Conflicts:
#	.github/workflows/ci-macos.yml
#	.github/workflows/ci.yml
#	.github/workflows/cli-pipe-regressions.yml
#	.github/workflows/nightly.yml
#	.github/workflows/persistent-macos-compile.yml
#	.github/workflows/test-e2e.yml
@teamleaderleo
teamleaderleo deleted the fix/yield-bounded-polls branch September 23, 2026 11:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-full-ci Records that skipping the macOS suite on a test-only diff is deliberate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant