a73x

1d43cd59

fix: preserve native command mode across delayed End replies

a73x   2026-09-05 13:11

Commit message
fix: preserve native command mode across delayed End replies

RETRO.md
Old New
@@ -272,26 +272,31 @@ within the release review or should remain an explicit follow-up.
272 ### Actions for Sprint 5 272 ### Actions for Sprint 5
273 273
274 Owner: the sprint lead and implementation/review pair. Close each with evidence 274 Owner: the sprint lead and implementation/review pair. Close each with evidence
275 at the final milestone retrospective. 275 at the final milestone retrospective. Completed items are evidenced in the
276 Sprint 5 retrospective below and the delivery plan.
276 277
277 - [ ] Include `make native-stress` and a real Sway raw-output run in the final 278 - [x] Include `make native-stress` and a real Sway raw-output run in the final
278 release review; build both demo binaries together in an isolated ReleaseSafe 279 release review; build both demo binaries together in an isolated ReleaseSafe
279 prefix and report sampled input latency separately from frame timing. 280 prefix and report sampled input latency separately from frame timing.
280 - [ ] Obtain explicit Sprint 4 demo acceptance before beginning the release review. 281 - [x] Resolve the Sprint 4 acceptance gate before beginning the release review.
282 The user explicitly said “Let’s start sprint 5”, authorizing advancement.
283 This supersedes waiting at that gate; separate Sprint 4 demo approval and
284 hands-on feedback are not claimed. Lifecycle remains in the final demo.
281 - [ ] Run the next ergonomic trial from proposed defaults through implementation 285 - [ ] Run the next ergonomic trial from proposed defaults through implementation
282 and demo, apply any requested adjustments, and record whether feedback came 286 and demo, apply any requested adjustments, and record whether feedback came
283 from the recording or a hands-on trial, if available, separately from automation. 287 from the recording or a hands-on trial, if available, separately from automation.
284 - [ ] Start with a bounded review/cleanup of the accumulated native workflow, then 288 Trial recordings are shared; acceptance and any requested adjustments remain open.
289 - [x] Start with a bounded review/cleanup of the accumulated native workflow, then
285 retain a closing cleanup pass; avoid introducing tabs UI or terminal tiling changes. 290 retain a closing cleanup pass; avoid introducing tabs UI or terminal tiling changes.
286 - [ ] Review delayed destructive actions across focus, picker, resize, drag, 291 - [x] Review delayed destructive actions across focus, picker, resize, drag,
287 detach, and replacement transitions; keep confirmation target identity visible. 292 detach, and replacement transitions; keep confirmation target identity visible.
288 - [ ] Review the retained SSH EOF/authentication classification limit and record 293 - [x] Review the retained SSH EOF/authentication classification limit and record
289 the disposition without weakening bounded reconnect or join-only restoration. 294 the disposition without weakening bounded reconnect or join-only restoration.
290 - [ ] Run the complete multi-host workflow and final adversarial review, including 295 - [x] Run the complete multi-host workflow and final adversarial review, including
291 shared sessions, state ownership/failures, high DPI, and cleanup of owned fixtures. 296 shared sessions, state ownership/failures, high DPI, and cleanup of owned fixtures.
292 - [ ] Record final demo acceptance separately from automation and any hands-on 297 - [ ] Record final demo acceptance separately from automation and any hands-on
293 ergonomic feedback, update the spec's completion status, and commit the milestone. 298 ergonomic feedback, update the spec's completion status, and commit the milestone.
294 - [ ] Evaluate model economy for the sprint using available evidence: record the 299 - [x] Evaluate model economy for the sprint using available evidence: record the
295 bounded pair's task scope, review yield, and observed token/time cost, explicitly 300 bounded pair's task scope, review yield, and observed token/time cost, explicitly
296 noting unavailable measurements, then decide whether the current model defaults 301 noting unavailable measurements, then decide whether the current model defaults
297 remain economical for the next sprint. 302 remain economical for the next sprint.
@@ -329,3 +334,90 @@ before launching fixtures as intended. Adversarial review accepted the test
329 after adding its Makefile target to `.PHONY`. 334 after adding its Makefile target to `.PHONY`.
330 As a negative control, deliberately bypassing the build target and pairing the 335 As a negative control, deliberately bypassing the build target and pairing the
331 release GUI with the Debug daemon failed the new latency assertion at 878 ms. 336 release GUI with the Debug daemon failed the new latency assertion at 878 ms.
337
338
339 ## Native tiling — Sprint 5, 2026-09-05
340
341 The user explicitly authorized the release review. Implementation, adversarial
342 review, closing cleanup assessment and final gates are complete. Final demo
343 acceptance remains pending; advancing from Sprint 4 is not recorded as a separate demo approval.
344 The final trial retains the existing keys and asks for feedback on target clarity,
345 Cancel-first confirmation, and returning to terminal input. Hands-on feedback is
346 optional and will be recorded separately from video approval.
347
348 ### What worked
349
350 - A bounded opening review found no justified attachment or persistence refactor.
351 The closing review reached the same conclusion after the targeted fix; no
352 cleanup quota or new abstraction was introduced.
353 - Adversarial review found a missed command-mode transition: a delayed End refusal
354 could open a force menu over an armed prefix. Dismissing the menu left the
355 prefix armed, so later terminal input could become another End command. The
356 fix preserves command mode and prevents that unsolicited menu.
357 - The real-daemon regression failed on the previous release binary, then passed
358 after the fix. Eight scenarios hold a reply by pausing only an owned daemon,
359 then exercise command, focus, picker, resize, drag, replacement, detach and
360 accepted-End paths. Another real client causes the shared-session refusal.
361 Explicit refusal assertions distinguish an actual reply from a timeout;
362 detach deliberately records an unknown outcome.
363 - Final-build SSH/QUIC checks passed on the VM and verified fixture cleanup.
364 The GUI demo and compositor mouse recording were captured on an isolated Sway
365 output at 200%, keeping the sleeping desktop unchanged.
366
367 ### What to change
368
369 - The first Wayland stress setup tried to resize an unmapped window. Use a
370 compositor mapping rule or wait for mapping before fixture placement.
371 - A software-rendered headless compositor exceeded the frame-time budget even
372 though input remained responsive. Record renderer provenance as well as both
373 binaries' build modes; do not infer hardware performance from functional
374 headless checks. Keep performance runs separate from other GUI fixtures.
375 - Include the pinned compiler and writable cache directory in short agent briefs.
376 The implementer's local validation attempt used a blocked cache and could not
377 fetch dependencies; root's existing cache completed the final native gates.
378
379 ### Validation and demo
380
381 `make ci`, 44 native unit tests, 63 native integration scenarios (including the
382 separately run old-daemon picker case), Wayland lifecycle/scale checks, real
383 compositor mouse input and SSH/QUIC acceptance passed. The GPU-backed Sway stress
384 run passed unchanged budgets: shared-daemon sampled input maximum 50.4 ms,
385 separate-daemon 65.3 ms, frame p99 16.0 ms, 113.7 MB over 30.1 seconds. The software
386 run remains a recorded failure; no budget was relaxed. Exact logs and measurement
387 limits are in the delivery plan. An earlier CI process received SIGTERM before
388 completion; the subsequent full CI run passed.
389
390 The [private final demo](https://charizard.folk-amberjack.ts.net/sprint5/) contains
391 the 61.2-second lifecycle/raw-output trial and a 23.4-second mouse recording. Both
392 use the final release GUI on isolated Sway at 200%, with no audio. Test fixtures
393 and compositors were stopped; only the explicitly shared demo service remains.
394 The implementation is ready for user acceptance; no approval is inferred.
395
396 ### Delegation and retained debt
397
398 Luna performed the opening assessment and focused implementation. A new Terra
399 reviewer could not start because old agent threads exhausted the session limit.
400 Root disclosed the fallback and reused the existing reviewer with a narrow brief;
401 that reviewer used the more expensive inherited model. The pair resolved one
402 production finding directly, and root independently reproduced it and validated
403 the integrated result. Per-agent token/cost totals were unavailable, so this
404 sprint does not establish savings from the intended Luna/Terra pair. Retain those
405 model defaults, and start the next sprint in a fresh session with both slots free.
406
407 The SSH announce EOF/authentication classification limit remains in
408 `src/client/session_pump.zig` and the shared handoff layer. Some permanent auth
409 failures receive bounded reconnect attempts. Reasons stay visible; End and
410 session creation are never retried. The final reviewer accepted deferral to a
411 shared handoff classification change, whose owner must preserve join-only
412 restoration and bounded shutdown. Tabs UI and pane rearrangement remain outside
413 this milestone.
414
415 ### Actions for the next authorized sprint
416
417 - [ ] Record final Sprint 5 demo acceptance and any requested ergonomic adjustment;
418 distinguish recorded approval from any hands-on trial.
419 - [ ] Start with available Luna/Terra slots and short briefs containing the pinned
420 compiler/cache command; record model selection, review yield and available cost
421 evidence without estimating missing token totals.
422 - [ ] When shared handoff errors are changed, separate unreachable-host EOF from
423 authentication refusal and verify bounded reconnect and join-only restore.
docs/native-sprint-workflow.md
Old New
@@ -45,6 +45,13 @@ packages. Keep one sprint active, ending in a working demo and explicit acceptan
45 exercise SSH/QUIC when changes affect their behavior. Create and clean up only 45 exercise SSH/QUIC when changes affect their behavior. Create and clean up only
46 owned fixture sessions and daemons. 46 owned fixture sessions and daemons.
47 47
48 An isolated compositor can exercise scale changes without changing the user's
49 monitor. Start it in its own process session. Wait for a window to map before
50 resizing it, or use a compositor mapping rule for fixture geometry. Record the
51 compositor's renderer: a software compositor is useful for functional checks,
52 but does not establish hardware frame-time performance. Run performance checks
53 separately from recordings and other GUI fixtures on that compositor.
54
48 For responsiveness, run `make native-stress` on Linux. It builds both binaries 55 For responsiveness, run `make native-stress` on Linux. It builds both binaries
49 in ReleaseSafe from the same build graph and drives raw `cat /dev/random` in 56 in ReleaseSafe from the same build graph and drives raw `cat /dev/random` in
50 one pane while probing neighbours on the same daemon and a separate daemon. 57 one pane while probing neighbours on the same daemon and a separate daemon.
docs/superpowers/plans/2026-09-05-native-tiling.md
Old New
@@ -1,8 +1,9 @@
1 # Native tiling — delegated delivery plan 1 # Native tiling — delegated delivery plan
2 2
3 Design authority: [native tiling spec](../specs/2026-09-05-native-tiling-design.md). 3 Design authority: [native tiling spec](../specs/2026-09-05-native-tiling-design.md).
4 Status: Sprint 4 implemented, reviewed, and validated; recorded-demo acceptance 4 Status: Sprint 5 implemented, reviewed, and validated; final demo acceptance pending.
5 pending. Sprint 3 acceptance passed. 5 Sprint 4 is implemented, reviewed, and validated; separate recorded-demo
6 acceptance was not given. Sprint 3 acceptance passed.
6 7
7 ## Working agreement 8 ## Working agreement
8 9
@@ -504,7 +505,8 @@ Implemented lifecycle/recovery, native persistence, and restore integration.
504 Opening cleanup is `aa5e977`. Closing cleanup shares attachment startup for add 505 Opening cleanup is `aa5e977`. Closing cleanup shares attachment startup for add
505 and restore, consolidates modal held-key ownership and popup reset, and consumes 506 and restore, consolidates modal held-key ownership and popup reset, and consumes
506 End outcomes through Runtime's captured status. No unresolved production review 507 End outcomes through Runtime's captured status. No unresolved production review
507 finding remains. Recorded-demo acceptance is pending; Sprint 5 has not started. 508 finding remains. Separate recorded-demo acceptance was not given; the user later
509 explicitly authorized proceeding to Sprint 5, whose final demo includes lifecycle.
508 510
509 The native controls are prefix `d` to detach, `x` to request End, and `p` for 511 The native controls are prefix `d` to detach, `x` to request End, and `p` for
510 Retry/Choose session/Detach. End travels over the pane's own connection, respecting 512 Retry/Choose session/Detach. End travels over the pane's own connection, respecting
@@ -561,3 +563,81 @@ HTTPS 206 byte-range delivery. The loopback server is
561 `tailscale serve --https=443 --set-path /sprint4 off`, then stop the owned server; 563 `tailscale serve --https=443 --set-path /sprint4 off`, then stop the owned server;
562 earlier demo routes remain available. The recording does not claim a hands-on 564 earlier demo routes remain available. The recording does not claim a hands-on
563 trial or a live remote host outage; remote behavior is validated separately. 565 trial or a live remote host outage; remote behavior is validated separately.
566
567
568 ## Sprint 5 — final review and ergonomic trial
569
570 The user explicitly authorized this sprint. Opening and closing cleanup reviews
571 found no justified structural refactor. Adversarial review found one missed
572 transition: a delayed shared-session End refusal could open a force menu while
573 command mode was armed, leaving the prefix active after dismissal. The focused
574 fix prevents that menu from interrupting command mode. Other lifecycle, runtime,
575 persistence ownership and stale-generation checks passed the final review.
576
577 Root's real-daemon command-mode regression failed on the previous release binary
578 (`/tmp/muxg-sprint5-command-before.log`) and passed after the fix. The new matrix
579 in `test/native_lifecycle.py` pauses only owned daemons and covers command, focus,
580 picker, resize, drag, replacement, detach and accepted-End transitions. Another
581 real client supplies shared-session refusals. Non-detach refusal paths assert
582 the actual daemon reason, preventing timeouts from falsely passing. Detach keeps
583 the remote outcome explicitly unknown. Paused daemons resume in cleanup.
584
585 Validation on the reviewed source:
586
587 - ReleaseSafe daemon/GUI pair and **44/44 native unit tests**; complete native
588 integration: **10 viewer + 14 tiling + 10 picker + 8 resize + 21 lifecycle**.
589 `/tmp/muxg-sprint5-build.log`, `/tmp/muxg-sprint5-native-e2e.log`. The optional
590 tenth picker scenario ran separately against the retained prior-sprint daemon:
591 `/tmp/muxg-sprint5-old-daemon.log` (passed).
592 - All **21 lifecycle scenarios on Wayland at 200%**, including the new matrix:
593 `/tmp/muxg-sprint5-lifecycle-sway.log`. Scale transitions **200% → 100% →
594 150% → 200%**, stale-drag cancellation, independent kernel PTY sizes and
595 retained pixels passed: `/tmp/muxg-sprint5-scale.log`.
596 - Real compositor mouse/button movement on the final binary passed both axes,
597 release and PTY checks: `/tmp/muxg-sprint5-mouse/record.log`.
598 - Real VM SSH fallback and direct QUIC lifecycle **3/3** with verified cleanup:
599 `/tmp/muxg-sprint5-remote/validation.log`. Both local demo binaries and the
600 uploaded static remote daemon were built in ReleaseSafe from this tree.
601 - `make native-stress` passed offscreen and on GPU-backed isolated Sway at 200%:
602 `/tmp/muxg-sprint5-stress-offscreen.log` and
603 `/tmp/muxg-sprint5-stress-gpu-sway.log`. Offscreen sampled input maxima were
604 **15.3 ms** for both neighbours. On Sway they were **50.4 ms shared-daemon** and
605 **65.3 ms separate-daemon**, with **113,704,475 bytes / 30.1 seconds** and frame
606 p99 **16,040 us**. Input figures are observed upper bounds with 5 ms polling.
607 Reports: `/tmp/muxg-tiling-0v2lvdd9/stress-result.json` and
608 `/tmp/muxg-tiling-q3hz7zxu/stress-result.json`.
609 - **`make ci` passed**: `/tmp/muxg-sprint5-ci-final.log`, including repository
610 checks, end-to-end and agent scenarios. Throughput: 4 ms solo, 48 ms attached,
611 91 ms detached repaint. The earlier run received SIGTERM without a reported
612 assertion failure and is not counted as a completed gate.
613
614 The first Sway stress attempt raced window mapping; the isolated compositor now
615 sizes fixtures when they map. A software-rendered run exceeded the frame budget
616 (p99 55.1 ms) despite responsive input. It is retained in
617 `/tmp/muxg-sprint5-stress-sway-final.log`, not counted as a pass. The final run used
618 Sway's verified AMD/radeonsi renderer with the same budgets and ran without other
619 GUI fixtures on that compositor. Test compositors have been stopped; the sleeping
620 physical desktop was unchanged.
621
622 Final trial: [private video page](https://charizard.folk-amberjack.ts.net/sprint5/).
623 The main recording is **61.2 seconds, 1100×700, H.264, no audio**, with three
624 continuous clips joined across window restarts. It shows host-picker cancellation,
625 raw output with usable neighbours, keyboard resize, delayed End, Cancel-first
626 confirmation, restore, an unavailable target returning, missing-session recovery,
627 Detach and End. A **23.4-second mouse recording** verifies real compositor input.
628 Both were recorded on isolated Sway at 200% using local daemon targets; SSH/QUIC
629 was validated separately. No video-based latency measurement is claimed.
630
631 Retained videos are `native-tiling-sprint5.mp4` and
632 `native-tiling-sprint5-mouse.mp4` under
633 `/home/xanderle/.codex/visualizations/2026/09/04/01a06dd2-1bb6-79c3-8a66-bc6bd8f2dedf/`.
634 The owned page/video server is `/tmp/muxg-sprint5-demo/server.py` on loopback
635 18769; both videos passed HTTPS 206 byte-range checks. Teardown removes only
636 `tailscale serve --https=443 --set-path /sprint5 off`, then stops that server.
637 Earlier demo routes remain available. Scripts and assertions are retained under
638 `/tmp/muxg-sprint5-demo/` and `/tmp/muxg-sprint5-mouse/`.
639
640 Reviewer approval and implementation are complete; user demo acceptance and any
641 requested ergonomic adjustments remain pending. SSH EOF/authentication error
642 classification is retained with the follow-up owner/trigger in RETRO.md. Tabs UI
643 and rearranging panes are outside the delivered milestone.
docs/superpowers/specs/2026-09-05-native-tiling-design.md
Old New
@@ -1,8 +1,9 @@
1 # Native tiling — draft design and delivery plan 1 # Native tiling — draft design and delivery plan
2 2
3 2026-09-05. Status: Sprint 4 lifecycle and persistence implemented, reviewed, and 3 2026-09-05. Status: the native tiling milestone is implemented, reviewed, and
4 validated; recorded-demo acceptance pending. Sprint 3 was accepted after its 4 validated through Sprint 5; final recorded-demo acceptance is pending. Separate
5 mouse-focused recording. 5 Sprint 4 demo approval was not given; the user explicitly authorized advancing
6 to Sprint 5. Sprint 3 was accepted after its mouse-focused recording.
6 7
7 Execution: [delegated work packages and review gates](../plans/2026-09-05-native-tiling.md). 8 Execution: [delegated work packages and review gates](../plans/2026-09-05-native-tiling.md).
8 9
src/gui/frame.zig
Old New
@@ -644,7 +644,7 @@ const Events = struct {
644 .accepted => try self.detach(pending.key.pane), 644 .accepted => try self.detach(pending.key.pane),
645 .refused => { 645 .refused => {
646 self.setNotice(result.reasonText()); 646 self.setNotice(result.reasonText());
647 if (result.others > 0 and self.rt.workspace.tab().focus == pending.key.pane and self.picker == null and self.recovery == null and !self.resize_mode and self.drag == null) { 647 if (result.others > 0 and self.rt.workspace.tab().focus == pending.key.pane and self.picker == null and self.recovery == null and !self.resize_mode and self.drag == null and !self.command_mode) {
648 self.recovery = .{ .kind = .force_end, .key = pending.key }; 648 self.recovery = .{ .kind = .force_end, .key = pending.key };
649 self.recovery.?.setNotice(result.reasonText()); 649 self.recovery.?.setNotice(result.reasonText());
650 } 650 }
@@ -1125,6 +1125,25 @@ test "delayed End refusal respects changed focus and recovery context names its
1125 try std.testing.expectEqualStrings("End for all clients", popup.lines[2].bytes[0..popup.lines[2].len]); 1125 try std.testing.expectEqualStrings("End for all clients", popup.lines[2].bytes[0..popup.lines[2].len]);
1126 } 1126 }
1127 1127
1128 test "delayed End refusal does not open force menu during command mode" {
1129 const a = std.testing.allocator;
1130 var rt = runtime.Runtime.init(a, .{});
1131 defer rt.deinit();
1132 const metrics: model.Metrics = .{ .cell_w = 10, .cell_h = 20 };
1133 const id = try rt.add(.{ .via = "cat" }, "end-target", 800, 600, metrics);
1134 var wake: Wake = .{ .event_type = c.SDL_EVENT_USER };
1135 var events: Events = .{ .rt = &rt, .win = undefined, .wake = &wake, .hook = null, .cache = undefined, .base_font_px = 16, .metrics = metrics, .fb_w = 800, .fb_h = 600, .command_mode = true };
1136 defer events.deinit();
1137 try events.relayout();
1138 const key = rt.get(id).?.key;
1139 events.pending_end = .{ .key = key, .request = 1 };
1140 rt.get(id).?.status.ending = .{ .request = 1, .phase = .refused, .others = 1 };
1141 try events.pollEnd();
1142 try std.testing.expect(events.recovery == null);
1143 try std.testing.expect(events.pending_end == null);
1144 try std.testing.expect(events.command_mode);
1145 }
1146
1128 test "held detach removes one pane and recovery Enter stays consumed after closing" { 1147 test "held detach removes one pane and recovery Enter stays consumed after closing" {
1129 const a = std.testing.allocator; 1148 const a = std.testing.allocator;
1130 var rt = runtime.Runtime.init(a, .{}); 1149 var rt = runtime.Runtime.init(a, .{});
test/native_lifecycle.py
Old New
@@ -483,6 +483,115 @@ def recovery_workflow(mux, muxg):
483 rig.close() 483 rig.close()
484 484
485 485
486 def delayed_end_transitions(mux, muxg):
487 # Stop only our real daemon to hold its reply across ordinary GUI input.
488 # A second real client supplies the daemon's refusal; no wire reply is forged.
489 for transition in ('command', 'focus', 'picker', 'resize', 'drag', 'replacement', 'detach', 'accepted'):
490 rig = LifecycleRig(mux, muxg)
491 paused = None
492 try:
493 a, daemon = rig.daemon('delayed-end')
494 b, _ = rig.daemon('healthy-neighbour')
495 rig.start_gui(a, b, 'gui-delayed-' + transition)
496 rig.host('--sock ' + b, 'v')
497 rig.choose('0')
498 state = rig.wait_state(lambda s: len(s['panes']) == 2 and
499 all(p['phase'] == 'attached' for p in s['panes']))
500 origin, neighbour = [p['id'] for p in state['panes']]
501 generation = by_id(state)[origin]['generation']
502 holder = None if transition == 'accepted' else rig.holder(a, 'left')
503 rig.focus(origin)
504 daemon.send_signal(signal.SIGSTOP)
505 paused = daemon
506 rig.chord('x')
507 rig.wait_state(lambda s: s['pending_end'] is not None)
508 rig.key('escape')
509 rig.wait_state(lambda s: s['recovery'] is None)
510
511 if transition in ('focus', 'accepted'):
512 rig.focus(neighbour)
513 elif transition == 'command':
514 rig.key('prefix')
515 rig.wait_state(lambda s: s['command_mode'])
516 elif transition == 'picker':
517 rig.chord('enter')
518 rig.picker('hosts')
519 elif transition == 'resize':
520 rig.chord('r')
521 rig.wait_state(lambda s: s['resize_mode'])
522 elif transition == 'drag':
523 state = rig.state()
524 rect = state['dividers'][0]['rect']
525 point = rig.point(state, rect['x'] + rect['w'] / 2,
526 rect['y'] + rect['h'] / 4)
527 rig.send('mousedown:' + point)
528 rig.wait_state(lambda s: s['drag'] is not None)
529 elif transition == 'replacement':
530 rig.recover('Choose session')
531 state = rig.wait_state(lambda s: 'pending End' in s['notice'])
532 require(state['picker'] is None and by_id(state)[origin]['generation'] == generation,
533 'replacement started while its End outcome was pending')
534 # Leave the recovery menu open while the refusal arrives.
535 rig.chord('p')
536 rig.wait_state(lambda s: s['recovery'] is not None)
537 elif transition == 'detach':
538 rig.chord('d')
539 rig.wait_state(lambda s: origin not in by_id(s))
540
541 daemon.send_signal(signal.SIGCONT)
542 paused = None
543 state = rig.wait_state(lambda s: s['pending_end'] is None)
544 if transition == 'accepted':
545 state = rig.wait_state(lambda s: origin not in by_id(s))
546 eventually(lambda: 'left' not in sessions(a), 'accepted End left its original session alive')
547 require(state['focus'] == neighbour and neighbour in by_id(state),
548 'late accepted End removed or focused the wrong pane')
549 else:
550 require('left' in sessions(a) and holder.poll() is None,
551 'delayed shared-session End bypassed the daemon refusal')
552 if transition != 'detach':
553 require(state['notice'] == 'others attached',
554 'expected the real daemon refusal, not a timeout or unknown outcome')
555 if transition == 'replacement':
556 require(state['recovery'] is not None and
557 all(row['label'] != 'End for all clients' for row in state['recovery']['rows']),
558 'late refusal replaced the active recovery menu')
559 else:
560 require(state['recovery'] is None, 'late refusal installed a force menu in another context')
561 if transition == 'command':
562 require(state['command_mode'], 'late refusal interrupted command mode')
563 elif transition == 'picker':
564 require(state['picker'] is not None, 'late refusal dismissed the picker')
565 elif transition == 'resize':
566 require(state['resize_mode'], 'late refusal interrupted resize mode')
567 elif transition == 'drag':
568 require(state['drag'] is not None, 'late refusal interrupted the active drag')
569 rig.send('mouseup:' + point)
570 if transition in ('command', 'picker', 'resize', 'replacement'):
571 rig.key('escape')
572 if transition == 'replacement':
573 rig.recover('Choose session')
574 rig.picker('hosts')
575 rig.choose('--sock ' + b)
576 rig.picker('sessions')
577 rig.choose('0')
578 state = rig.wait_state(lambda s: by_id(s)[origin]['generation'] > generation and
579 by_id(s)[origin]['phase'] == 'attached')
580 require(state['recovery'] is None, 'old End refusal leaked into replacement')
581 rig.release_holder(holder)
582 rig.mark(neighbour, b, '0', 'DELAYED-END-' + transition.upper())
583 rig.assert_cli_untouched()
584 rig.quit()
585 rig.ok('real delayed End preserves target and GUI context through ' + transition)
586 except BaseException:
587 rig.failure_artifacts()
588 raise
589 finally:
590 if paused is not None and paused.poll() is None:
591 paused.send_signal(signal.SIGCONT)
592 rig.close()
593
594
486 def main(): 595 def main():
487 require(len(sys.argv) == 3, 'usage: native_lifecycle.py MUX MUXG') 596 require(len(sys.argv) == 3, 'usage: native_lifecycle.py MUX MUXG')
488 mux, muxg = sys.argv[1:] 597 mux, muxg = sys.argv[1:]
@@ -505,6 +614,7 @@ def main():
505 preservation_case(mux, muxg, 'malformed', b'{"version": 1, "tabs": [ BROKEN ORIGINAL\n') 614 preservation_case(mux, muxg, 'malformed', b'{"version": 1, "tabs": [ BROKEN ORIGINAL\n')
506 preservation_case(mux, muxg, 'unreadable', valid_bytes, unreadable=True) 615 preservation_case(mux, muxg, 'unreadable', valid_bytes, unreadable=True)
507 relative_target_reopen(mux, muxg) 616 relative_target_reopen(mux, muxg)
617 delayed_end_transitions(mux, muxg)
508 618
509 619
510 if __name__ == '__main__': 620 if __name__ == '__main__':