cmux-next: a classic upgrade keeps its groups and order when sidebar placements trail the run

TE
7d ago
+82-6

ClassicUpgradeSimulationTests and ClassicMigrationTests.aReUpgradeMergesOnlyWhatClassicMadeMeanwhile failed a different subset of order and group checks on nearly every daemon-tests run, base included (base run 41037828764 at :158 and :206; #28016 at :184; also #27797, #27921, #27825). That left every daemon-tests verdict ambiguous.

Cause

Since #26153 the app's sidebar placements go through PersonalPlacementQueue, one at a time. On a loaded runner that queue trails the migration run, and this broke two things:

  1. Product: groups lost. The migration creates each workspace with slot .bottom, which is queued as a placement once the daemon lists the workspace, and then puts the workspace in its classic group right away. When the queued placement lands after that group write, it moves the workspace out of the group again (group: .clear), and the group lifecycle deletes the group that is now empty. The deterministic red below shows this once the test waits long enough: small: api group nil, small: group Work nil.
  2. Test: order read too early. ClassicUpgradeSimulator.settle() waited for daemon events plus 0.5 s without changes. With placements still queued, verify() read the daemon's own order instead of classic's: small: order differs: ["scratch","db","web","api"] for ["api","web","db","scratch"].

Change

  • PersonalPlacementQueue gains isIdle and drained().
  • The classic import waits for the new workspace's slot placement before it returns: first the placement gets listed (spawn.onListed, capped at 5 s), then the queue drains. That puts the group write after the placement.
  • In the tests, AppServices.placementsSettled() waits for the queue to drain and the store to catch up with the daemon. ClassicUpgradeSimulator.settle() calls it before and after its quiet window and doesn't count a busy queue as quiet. The ClassicMigrationTests launch helper calls it before reading the tree.

No retry, skip or longer timeout.

Proof

  • Commit 1 (red): theOrderIsReadOnlyOnceQueuedPlacementsLand holds the queue for 4 s before launch, like a slow runner. It fails on current code with CI's signature.
  • Commit 2 (fix) makes it pass.
  • Fleet loop: both suites run 20 times on base and 20 times on this head, results in the comment below.

No capture: no visible UI change; this changes when the classic migration writes its group and sidebar slot, and the test waits.

Risk: low to medium. The product change only affects the classic migration path: each imported workspace waits for its own placement, which is one round trip, capped at 5 s if the placement never gets listed.

Agent session: agent session

🤖 Generated with Claude Code