oh-my-codex 0.20.2 → 0.20.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +7 -6
- package/Cargo.toml +1 -1
- package/README.md +26 -0
- package/crates/omx-runtime/Cargo.toml +1 -0
- package/crates/omx-runtime/src/main.rs +25 -2
- package/crates/omx-runtime/tests/execution.rs +219 -0
- package/crates/omx-runtime-core/src/dispatch.rs +96 -10
- package/crates/omx-runtime-core/src/engine.rs +839 -29
- package/crates/omx-runtime-core/src/lib.rs +8 -0
- package/dist/agents/__tests__/definitions.test.js +81 -0
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +25 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/native-config.d.ts +2 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/hotswap.d.ts +6 -0
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +85 -15
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +60 -6
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +80 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +103 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +136 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +258 -4
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +287 -1
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +4 -1
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +84 -6
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +3 -3
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +56 -171
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan.test.js +37 -111
- package/dist/cli/__tests__/ralplan.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +11 -10
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js +25 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +42 -4
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +105 -7
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +46 -0
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +2 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/doctor.d.ts +3 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +15 -2
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +138 -38
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-parity.js +2 -2
- package/dist/cli/mcp-parity.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +16 -5
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
- package/dist/cli/native-hook-claim-journal.js +42 -29
- package/dist/cli/native-hook-claim-journal.js.map +1 -1
- package/dist/cli/ralph.js +2 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +4 -8
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +19 -112
- package/dist/cli/ralplan.js.map +1 -1
- package/dist/cli/session-search.d.ts +5 -0
- package/dist/cli/session-search.d.ts.map +1 -1
- package/dist/cli/session-search.js +87 -0
- package/dist/cli/session-search.js.map +1 -1
- package/dist/cli/setup.d.ts +5 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +80 -31
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/uninstall.d.ts +9 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +61 -19
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +330 -1
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +2 -2
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/__tests__/models.test.js +67 -3
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/generator.d.ts +4 -20
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +499 -119
- package/dist/config/generator.js.map +1 -1
- package/dist/config/models.d.ts +12 -2
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +15 -1
- package/dist/config/models.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +2 -2
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +4 -4
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +238 -543
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +38 -4
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +96 -9
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +531 -12
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +155 -23
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +43 -4
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +348 -2
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +1 -1
- package/dist/hooks/session.d.ts +26 -2
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +169 -13
- package/dist/hooks/session.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +57 -7
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +16 -0
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +79 -4
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
- package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
- package/dist/mcp/__tests__/wiki-server.test.js +18 -0
- package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js +13 -3
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/wiki-server.d.ts +11 -0
- package/dist/mcp/wiki-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.js +9 -2
- package/dist/mcp/wiki-server.js.map +1 -1
- package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
- package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
- package/dist/notifications/temp-contract.d.ts.map +1 -1
- package/dist/notifications/temp-contract.js +4 -0
- package/dist/notifications/temp-contract.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +4 -4
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +8 -8
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +134 -10
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js +46 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
- package/dist/ralplan/__tests__/runtime.test.js +8 -4
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +58 -50
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +34 -0
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
- package/dist/ralplan/documented-leader-preflight.js +58 -0
- package/dist/ralplan/documented-leader-preflight.js.map +1 -0
- package/dist/ralplan/runtime.d.ts +1 -0
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +11 -7
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/__tests__/bridge.test.js +42 -1
- package/dist/runtime/__tests__/bridge.test.js.map +1 -1
- package/dist/runtime/bridge.d.ts +12 -0
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +52 -1
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +2620 -487
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +46 -173
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +39 -159
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +254 -2
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts +4 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +11013 -1153
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +82 -108
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +74 -3
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +234 -79
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +39 -13
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +19 -18
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +37 -28
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +28 -2
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.js +1 -0
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +48 -3
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +2796 -13
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/state/__tests__/mode-state-context.test.js +54 -0
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +2 -0
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/mode-state-context.d.ts +5 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +59 -0
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +24 -245
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +1 -82
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +3 -275
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +91 -1
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +52 -10
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/idle-nudge.test.js +45 -15
- package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
- package/dist/team/__tests__/mcp-comm.test.js +93 -27
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +110 -3
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +9 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +3204 -335
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +2359 -142
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +804 -36
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/team-ops-contract.test.js +6 -0
- package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +3049 -784
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +38 -2
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/__tests__/worktree.test.js +6 -0
- package/dist/team/__tests__/worktree.test.js.map +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +15 -9
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/exact-pane.d.ts +23 -0
- package/dist/team/exact-pane.d.ts.map +1 -0
- package/dist/team/exact-pane.js +133 -0
- package/dist/team/exact-pane.js.map +1 -0
- package/dist/team/idle-nudge.d.ts +12 -1
- package/dist/team/idle-nudge.d.ts.map +1 -1
- package/dist/team/idle-nudge.js +41 -14
- package/dist/team/idle-nudge.js.map +1 -1
- package/dist/team/model-contract.d.ts +2 -5
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +59 -17
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/runtime.d.ts +1 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +1244 -180
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +13 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +1430 -384
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch.d.ts.map +1 -1
- package/dist/team/state/dispatch.js +26 -8
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/locks.d.ts.map +1 -1
- package/dist/team/state/locks.js +61 -13
- package/dist/team/state/locks.js.map +1 -1
- package/dist/team/state/tasks.d.ts.map +1 -1
- package/dist/team/state/tasks.js +4 -3
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state.d.ts +67 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +636 -129
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +5 -0
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js +4 -0
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts +123 -23
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +1250 -291
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worktree.d.ts.map +1 -1
- package/dist/team/worktree.js +4 -0
- package/dist/team/worktree.js.map +1 -1
- package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
- package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
- package/dist/utils/__tests__/file-durability.test.js +57 -0
- package/dist/utils/__tests__/file-durability.test.js.map +1 -0
- package/dist/utils/file-durability.d.ts +15 -0
- package/dist/utils/file-durability.d.ts.map +1 -0
- package/dist/utils/file-durability.js +37 -0
- package/dist/utils/file-durability.js.map +1 -0
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/dist/wiki/lint.d.ts +4 -1
- package/dist/wiki/lint.d.ts.map +1 -1
- package/dist/wiki/lint.js +3 -2
- package/dist/wiki/lint.js.map +1 -1
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +24 -0
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +3 -2
- package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
- package/skills/ralph/SKILL.md +6 -4
- package/skills/ralplan/SKILL.md +3 -2
- package/skills/team/SKILL.md +13 -9
- package/src/scripts/__tests__/codex-native-hook.test.ts +2826 -497
- package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +48 -185
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +44 -177
- package/src/scripts/__tests__/smoke-packed-install.test.ts +320 -2
- package/src/scripts/codex-native-hook.ts +10715 -1069
- package/src/scripts/notify-fallback-watcher.ts +84 -115
- package/src/scripts/notify-hook/auto-nudge.ts +71 -3
- package/src/scripts/notify-hook/team-dispatch.ts +242 -75
- package/src/scripts/notify-hook/team-leader-nudge.ts +40 -18
- package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
- package/src/scripts/notify-hook/team-worker-stop.ts +20 -16
- package/src/scripts/notify-hook/team-worker.ts +39 -26
- package/src/scripts/notify-hook/tmux-injection.ts +28 -2
- package/src/scripts/notify-hook.ts +1 -0
- package/src/scripts/smoke-packed-install.ts +3196 -14
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
- package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
- package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
- package/dist/subagents/adapted-role-binding.d.ts +0 -17
- package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
- package/dist/subagents/adapted-role-binding.js +0 -75
- package/dist/subagents/adapted-role-binding.js.map +0 -1
|
@@ -4,13 +4,14 @@ import fs from 'node:fs';
|
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { syncBuiltinESMExports } from 'node:module';
|
|
6
6
|
import { PassThrough } from 'node:stream';
|
|
7
|
-
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'fs/promises';
|
|
7
|
+
import { chmod, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'fs/promises';
|
|
8
8
|
import { dirname, join } from 'path';
|
|
9
9
|
import { tmpdir } from 'os';
|
|
10
|
-
import { buildClientAttachedReconcileHookName, assertTeamWorkerCliBinaryAvailable, buildWorkerProcessLaunchSpec, buildReconcileHudResizeArgs, buildRegisterClientAttachedReconcileArgs, buildRegisterResizeHookArgs, buildResizeHookName, buildResizeHookTarget, buildScheduleDelayedHudResizeArgs, buildUnregisterClientAttachedReconcileArgs, buildUnregisterResizeHookArgs, buildWorkerStartupCommand, trustWorkerMiseConfigIfAvailable, writeWorkerStartupScriptCommand, shouldSourceTeamWorkerShellRc, buildHudPaneTarget, chooseTeamLeaderPaneId, createTeamSession, enableMouseScrolling, isMsysOrGitBash, isNativeWindows, isTmuxAvailable, isWorkerPaneOpen, restoreStandaloneHudPane, translatePathForMsys, isWsl2, isWorkerAlive, killWorker, killWorkerByPaneId, teardownWorkerPanes, listTeamSessions, resolveTeamWorkerCli, resolveTeamWorkerLaunchMode, resolveWorkerCliForSend, resolveTeamWorkerCliPlan, buildWorkerSubmitPlan, sanitizeTeamName, shouldAttemptAdaptiveRetry, sendToWorker, sendToWorkerStdin, sleepFractionalSeconds, translateWorkerLaunchArgsForCli, waitForWorkerReady, waitForWorkerReadyAsync, paneIsBootstrapping, classifyWorkerStartupInjectSafety, checkWorkerStartupInjectSafety, dismissTrustPromptIfPresent, evaluateStartupDirectTriggerSafetyCapture, mitigateCopyModeUnderlineArtifacts, } from '../tmux-session.js';
|
|
10
|
+
import { buildClientAttachedReconcileHookName, assertTeamWorkerCliBinaryAvailable, buildWorkerProcessLaunchSpec, buildReconcileHudResizeArgs, buildRegisterClientAttachedReconcileArgs, buildRegisterResizeHookArgs, buildResizeHookName, buildResizeHookTarget, buildScheduleDelayedHudResizeArgs, buildUnregisterClientAttachedReconcileArgs, buildUnregisterResizeHookArgs, buildWorkerStartupCommand, trustWorkerMiseConfigIfAvailable, writeWorkerStartupScriptCommand, shouldSourceTeamWorkerShellRc, buildHudPaneTarget, chooseTeamLeaderPaneId, createTeamSession, CreateTeamSessionPartialError, enableMouseScrolling, isMsysOrGitBash, isNativeWindows, isTmuxAvailable, isWorkerPaneOpen, restoreStandaloneHudPane, finalizeRestoredHudCleanupDebtSync, reconcileRestoredHudCleanupDebtSync, translatePathForMsys, isWsl2, isWorkerAlive, killWorker, getWorkerPanePid, killWorkerByPaneId, killWorkerByPaneIdAsync, teardownWorkerPanes, listTeamSessions, destroyTeamSession, queryDetachedTeamSession, requestDetachedTeamSessionDestroy, resolveTeamWorkerCli, resolveTeamWorkerLaunchMode, resolveWorkerCliForSend, resolveTeamWorkerCliPlan, buildWorkerSubmitPlan, sanitizeTeamName, shouldAttemptAdaptiveRetry, sendToWorker, sendToWorkerStdin, sleepFractionalSeconds, translateWorkerLaunchArgsForCli, waitForWorkerReady, waitForWorkerReadyAsync, paneIsBootstrapping, classifyWorkerStartupInjectSafety, checkWorkerStartupInjectSafety, dismissTrustPromptIfPresent, evaluateStartupDirectTriggerSafetyCapture, mitigateCopyModeUnderlineArtifacts, listPaneIds, } from '../tmux-session.js';
|
|
11
11
|
import { HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES } from '../../hud/constants.js';
|
|
12
12
|
import * as tmuxSessionModule from '../tmux-session.js';
|
|
13
13
|
import { OMX_ENTRY_PATH_ENV, OMX_STARTUP_CWD_ENV } from '../../utils/paths.js';
|
|
14
|
+
import { readExactPaneProof, readExactPaneProofSync } from '../exact-pane.js';
|
|
14
15
|
const fsMutable = fs;
|
|
15
16
|
function withEmptyPath(fn) {
|
|
16
17
|
const prev = process.env.PATH;
|
|
@@ -49,6 +50,18 @@ function withMockedStatSync(mock, fn) {
|
|
|
49
50
|
syncBuiltinESMExports();
|
|
50
51
|
}
|
|
51
52
|
}
|
|
53
|
+
function withMockedFsyncSync(mock, fn) {
|
|
54
|
+
const original = fsMutable.fsyncSync;
|
|
55
|
+
fsMutable.fsyncSync = mock;
|
|
56
|
+
syncBuiltinESMExports();
|
|
57
|
+
try {
|
|
58
|
+
return fn();
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
fsMutable.fsyncSync = original;
|
|
62
|
+
syncBuiltinESMExports();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
52
65
|
function escapeRegExp(value) {
|
|
53
66
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
54
67
|
}
|
|
@@ -87,7 +100,33 @@ async function withMockTmuxFixture(dirPrefix, tmuxScript, run) {
|
|
|
87
100
|
const tmuxStubPath = join(fakeBinDir, 'tmux');
|
|
88
101
|
const previousPath = process.env.PATH;
|
|
89
102
|
try {
|
|
90
|
-
|
|
103
|
+
const fixtureScript = tmuxScript(logPath);
|
|
104
|
+
const needsStandaloneGlobalProof = dirPrefix.includes('standalone') && !fixtureScript.includes('list-panes)');
|
|
105
|
+
const needsTeamOwnerState = !fixtureScript.includes('show-option');
|
|
106
|
+
if (needsStandaloneGlobalProof || needsTeamOwnerState) {
|
|
107
|
+
const fixturePath = `${tmuxStubPath}.fixture`;
|
|
108
|
+
const ownerStateDir = `${tmuxStubPath}.team-owner-state`;
|
|
109
|
+
await writeFile(fixturePath, fixtureScript);
|
|
110
|
+
await chmod(fixturePath, 0o755);
|
|
111
|
+
await writeFile(tmuxStubPath, `#!/bin/sh
|
|
112
|
+
if [ "${'$'}1" = "show-option" ] && [ "${'$'}{2:-}" = "-qv" ] && [ "${'$'}{3:-}" = "-p" ] && [ "${'$'}{4:-}" = "-t" ] && [ "${'$'}{6:-}" = "@omx_team_pane_owner_id" ]; then
|
|
113
|
+
if [ -f "${ownerStateDir}/${'$'}5" ]; then cat "${ownerStateDir}/${'$'}5"; exit 0; fi
|
|
114
|
+
exit 1
|
|
115
|
+
fi
|
|
116
|
+
if [ "${'$'}1" = "set-option" ] && [ "${'$'}{2:-}" = "-p" ] && [ "${'$'}{3:-}" = "-t" ] && [ "${'$'}{5:-}" = "@omx_team_pane_owner_id" ]; then
|
|
117
|
+
mkdir -p "${ownerStateDir}"
|
|
118
|
+
printf '%s' "${'$'}6" > "${ownerStateDir}/${'$'}4"
|
|
119
|
+
fi
|
|
120
|
+
${needsStandaloneGlobalProof ? `if [ "${'$'}1" = "list-panes" ] && [ "${'$'}{2:-}" = "-a" ]; then
|
|
121
|
+
printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000044\\n'
|
|
122
|
+
exit 0
|
|
123
|
+
fi
|
|
124
|
+
` : ''}exec "${fixturePath}" "${'$'}@"
|
|
125
|
+
`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
await writeFile(tmuxStubPath, fixtureScript);
|
|
129
|
+
}
|
|
91
130
|
await chmod(tmuxStubPath, 0o755);
|
|
92
131
|
process.env.PATH = `${fakeBinDir}:${previousPath ?? ''}`;
|
|
93
132
|
return await run({ logPath });
|
|
@@ -153,7 +192,9 @@ describe('HUD resize hook command builders', () => {
|
|
|
153
192
|
assert.equal(args[1], '-t');
|
|
154
193
|
assert.equal(args[2], 'my-session:0');
|
|
155
194
|
assert.match(args[3] ?? '', /^client-resized\[\d+\]$/);
|
|
156
|
-
assert.
|
|
195
|
+
assert.match(args[4] ?? '', /list-panes -a -F/);
|
|
196
|
+
assert.match(args[4] ?? '', /awk -F/);
|
|
197
|
+
assert.match(args[4] ?? '', new RegExp(`resize-pane -t %1 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
|
|
157
198
|
});
|
|
158
199
|
it('buildUnregisterResizeHookArgs removes the exact numeric hook slot', () => {
|
|
159
200
|
const registered = buildRegisterResizeHookArgs('my-session:0', 'omx_resize_team_session_0_1', '%1');
|
|
@@ -170,7 +211,46 @@ describe('HUD resize hook command builders', () => {
|
|
|
170
211
|
assert.equal(args[1], '-t');
|
|
171
212
|
assert.equal(args[2], 'my-session:0');
|
|
172
213
|
assert.match(args[3] ?? '', /^client-attached\[\d+\]$/);
|
|
173
|
-
assert.match(args[4] ?? '',
|
|
214
|
+
assert.match(args[4] ?? '', /list-panes -a -F/);
|
|
215
|
+
assert.match(args[4] ?? '', /awk -F/);
|
|
216
|
+
assert.match(args[4] ?? '', /resize-pane -t %1 -y \d+/);
|
|
217
|
+
assert.match(args[4] ?? '', /set-hook -u -t my-session:0 client-attached\[\d+\]/);
|
|
218
|
+
});
|
|
219
|
+
it('pins Team hook and delayed HUD reconciliation to the created pane PID and owner', () => {
|
|
220
|
+
const expectedPid = 2000000123;
|
|
221
|
+
const expectedOwner = 'team:exact-owner';
|
|
222
|
+
const commands = [
|
|
223
|
+
buildRegisterResizeHookArgs('my-session:0', 'omx_resize_team_session_0_1', '%1', HUD_TMUX_TEAM_HEIGHT_LINES, expectedPid, expectedOwner),
|
|
224
|
+
buildRegisterClientAttachedReconcileArgs('my-session:0', 'omx_attached_team_session_0_1', '%1', HUD_TMUX_TEAM_HEIGHT_LINES, expectedPid, expectedOwner),
|
|
225
|
+
buildScheduleDelayedHudResizeArgs('%1', HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES, expectedPid, expectedOwner),
|
|
226
|
+
buildReconcileHudResizeArgs('%1', HUD_TMUX_TEAM_HEIGHT_LINES, expectedPid, expectedOwner),
|
|
227
|
+
];
|
|
228
|
+
for (const args of commands) {
|
|
229
|
+
const command = args.at(-1) ?? '';
|
|
230
|
+
assert.match(command, new RegExp(`\\$3 == "${expectedPid}"`));
|
|
231
|
+
assert.match(command, /show-option -qv -p -t %1 @omx_team_pane_owner_id/);
|
|
232
|
+
assert.match(command, /final_snapshot=/);
|
|
233
|
+
assert.match(command, /team:exact-owner/);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
it('fails closed when a Team HUD hook observes an owner change between PID proofs', async () => {
|
|
237
|
+
await withMockTmuxFixture('omx-hud-hook-owner-change-', (logPath) => `#!/bin/sh
|
|
238
|
+
set -eu
|
|
239
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
240
|
+
case "$1" in
|
|
241
|
+
list-panes) printf '%%7\\t0\\t2000000007\\n' ;;
|
|
242
|
+
show-option) printf '%s\\n' 'team:foreign' ;;
|
|
243
|
+
resize-pane) exit 0 ;;
|
|
244
|
+
esac
|
|
245
|
+
`, async ({ logPath }) => {
|
|
246
|
+
const command = buildReconcileHudResizeArgs('%7', HUD_TMUX_TEAM_HEIGHT_LINES, 2000000007, 'team:expected')[1] ?? '';
|
|
247
|
+
const result = spawnSync('/bin/sh', ['-c', command], { encoding: 'utf-8' });
|
|
248
|
+
assert.equal(result.status, 0);
|
|
249
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
250
|
+
assert.match(commands, /list-panes -a -F/);
|
|
251
|
+
assert.match(commands, /show-option -qv -p -t %7 @omx_team_pane_owner_id/);
|
|
252
|
+
assert.doesNotMatch(commands, /resize-pane/);
|
|
253
|
+
});
|
|
174
254
|
});
|
|
175
255
|
it('buildUnregisterClientAttachedReconcileArgs removes the exact numeric client-attached slot', () => {
|
|
176
256
|
const registered = buildRegisterClientAttachedReconcileArgs('my-session:0', 'omx_attached_team_session_0_1', '%1');
|
|
@@ -201,13 +281,100 @@ describe('HUD resize hook command builders', () => {
|
|
|
201
281
|
const d = buildRegisterClientAttachedReconcileArgs('s:0', name, '%1');
|
|
202
282
|
assert.equal(c[3], d[3]);
|
|
203
283
|
});
|
|
204
|
-
it('buildScheduleDelayedHudResizeArgs schedules tmux-side delayed reconcile', () => {
|
|
205
|
-
|
|
284
|
+
it('buildScheduleDelayedHudResizeArgs schedules a proof-bearing tmux-side delayed reconcile', () => {
|
|
285
|
+
const args = buildScheduleDelayedHudResizeArgs('%1');
|
|
286
|
+
assert.equal(args[0], 'run-shell');
|
|
287
|
+
assert.equal(args[1], '-b');
|
|
288
|
+
assert.match(args[2] ?? '', new RegExp(`sleep ${HUD_RESIZE_RECONCILE_DELAY_SECONDS};`));
|
|
289
|
+
assert.match(args[2] ?? '', /list-panes -a -F/);
|
|
290
|
+
assert.match(args[2] ?? '', /awk -F/);
|
|
291
|
+
assert.match(args[2] ?? '', new RegExp(`resize-pane -t %1 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
|
|
206
292
|
});
|
|
207
|
-
it('buildReconcileHudResizeArgs executes a
|
|
293
|
+
it('buildReconcileHudResizeArgs executes only after a global exact-pane proof', () => {
|
|
208
294
|
const args = buildReconcileHudResizeArgs('%7');
|
|
209
295
|
assert.equal(args.join(' ').includes('split-window'), false);
|
|
210
|
-
assert.
|
|
296
|
+
assert.equal(args[0], 'run-shell');
|
|
297
|
+
assert.match(args[1] ?? '', /list-panes -a -F/);
|
|
298
|
+
assert.match(args[1] ?? '', /awk -F/);
|
|
299
|
+
assert.match(args[1] ?? '', new RegExp(`resize-pane -t %7 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
|
|
300
|
+
});
|
|
301
|
+
it('fails closed in hook shell proofs without resizing malformed, duplicate, dead, absent, or unavailable panes', async () => {
|
|
302
|
+
const previousFixture = process.env.OMX_HUD_HOOK_FIXTURE;
|
|
303
|
+
try {
|
|
304
|
+
await withMockTmuxFixture('omx-hud-hook-proof-', (logPath) => `#!/bin/sh
|
|
305
|
+
set -eu
|
|
306
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
307
|
+
case "$1" in
|
|
308
|
+
list-panes)
|
|
309
|
+
case "\${OMX_HUD_HOOK_FIXTURE:-}" in
|
|
310
|
+
missing) printf '%%7\\t0\\n' ;;
|
|
311
|
+
extra) printf '%%7\\t0\\t2000000007\\textra\\n' ;;
|
|
312
|
+
duplicate) printf '%%7\\t0\\t2000000007\\n%%7\\t0\\t2000000007\\n' ;;
|
|
313
|
+
dead) printf '%%7\\t1\\t2000000007\\n' ;;
|
|
314
|
+
absent) printf '%%8\\t0\\t2000000008\\n' ;;
|
|
315
|
+
query-failure) echo 'tmux query failed' >&2; exit 1 ;;
|
|
316
|
+
esac
|
|
317
|
+
;;
|
|
318
|
+
resize-pane) exit 0 ;;
|
|
319
|
+
esac
|
|
320
|
+
`, async ({ logPath }) => {
|
|
321
|
+
const command = buildReconcileHudResizeArgs('%7')[1] ?? '';
|
|
322
|
+
for (const fixture of ['missing', 'extra', 'duplicate', 'dead', 'absent', 'query-failure']) {
|
|
323
|
+
process.env.OMX_HUD_HOOK_FIXTURE = fixture;
|
|
324
|
+
const result = spawnSync('/bin/sh', ['-c', command], { encoding: 'utf-8' });
|
|
325
|
+
assert.equal(result.status, 0, fixture);
|
|
326
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
327
|
+
assert.doesNotMatch(commands, /resize-pane/, fixture);
|
|
328
|
+
await writeFile(logPath, '');
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
if (typeof previousFixture === 'string')
|
|
334
|
+
process.env.OMX_HUD_HOOK_FIXTURE = previousFixture;
|
|
335
|
+
else
|
|
336
|
+
delete process.env.OMX_HUD_HOOK_FIXTURE;
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
it('tolerates unrelated PID-less rows while rejecting malformed or duplicate HUD targets in generated hook proofs', async () => {
|
|
340
|
+
const previousFixture = process.env.OMX_HUD_HOOK_FIXTURE;
|
|
341
|
+
try {
|
|
342
|
+
await withMockTmuxFixture('omx-hud-hook-unrelated-pidless-', (logPath) => `#!/bin/sh
|
|
343
|
+
set -eu
|
|
344
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
345
|
+
case "$1" in
|
|
346
|
+
list-panes)
|
|
347
|
+
case "\${OMX_HUD_HOOK_FIXTURE:-}" in
|
|
348
|
+
unrelated-pidless) printf '%%99\\t0\\t\\n%%7\\t0\\t2000000007\\n' ;;
|
|
349
|
+
target-malformed) printf '%%7\\t0\\t\\n%%99\\t0\\t2000000099\\n' ;;
|
|
350
|
+
target-duplicate) printf '%%7\\t0\\t2000000007\\n%%7\\t0\\t2000000007\\n' ;;
|
|
351
|
+
esac
|
|
352
|
+
;;
|
|
353
|
+
show-option) printf 'team:expected\\n' ;;
|
|
354
|
+
resize-pane) exit 0 ;;
|
|
355
|
+
esac
|
|
356
|
+
`, async ({ logPath }) => {
|
|
357
|
+
const command = buildReconcileHudResizeArgs('%7', HUD_TMUX_TEAM_HEIGHT_LINES, 2000000007, 'team:expected')[1] ?? '';
|
|
358
|
+
for (const [fixture, shouldResize] of [
|
|
359
|
+
['unrelated-pidless', true],
|
|
360
|
+
['target-malformed', false],
|
|
361
|
+
['target-duplicate', false],
|
|
362
|
+
]) {
|
|
363
|
+
process.env.OMX_HUD_HOOK_FIXTURE = fixture;
|
|
364
|
+
const result = spawnSync('/bin/sh', ['-c', command], { encoding: 'utf-8' });
|
|
365
|
+
assert.equal(result.status, 0, fixture);
|
|
366
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
367
|
+
assert.equal(/resize-pane/.test(commands), shouldResize, `${fixture}: ${commands}`);
|
|
368
|
+
await writeFile(logPath, '');
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
if (typeof previousFixture === 'string')
|
|
374
|
+
process.env.OMX_HUD_HOOK_FIXTURE = previousFixture;
|
|
375
|
+
else
|
|
376
|
+
delete process.env.OMX_HUD_HOOK_FIXTURE;
|
|
377
|
+
}
|
|
211
378
|
});
|
|
212
379
|
it('resolves the tmux executable for win32 hook shell snippets', async () => {
|
|
213
380
|
const fakeBin = await mkdtemp(join(tmpdir(), 'omx-win32-hook-tmux-'));
|
|
@@ -257,7 +424,7 @@ describe('HUD resize hook command builders', () => {
|
|
|
257
424
|
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
258
425
|
const args = buildRegisterClientAttachedReconcileArgs('my-session:0', 'omx_attached_team_session_0_1', '%1');
|
|
259
426
|
const matches = (args[4] ?? '').match(new RegExp(escapeRegExp(tmuxPath), 'g')) || [];
|
|
260
|
-
assert.equal(matches.length,
|
|
427
|
+
assert.equal(matches.length, 4, 'client-attached hook should resolve tmux for separate resize and hook-unregister proofs');
|
|
261
428
|
assert.doesNotMatch(args[4] ?? '', /; tmux set-hook -u -t my-session:0 client-attached/);
|
|
262
429
|
}
|
|
263
430
|
finally {
|
|
@@ -3158,11 +3325,161 @@ describe('tmux-dependent functions when tmux is unavailable', () => {
|
|
|
3158
3325
|
assert.throws(() => createTeamSession('My Team', 1, process.cwd()), /tmux is not available/i);
|
|
3159
3326
|
});
|
|
3160
3327
|
});
|
|
3161
|
-
it('
|
|
3328
|
+
it('distinguishes a failed session query from a successful empty result', async () => {
|
|
3162
3329
|
withEmptyPath(() => {
|
|
3330
|
+
assert.equal(listTeamSessions(), null);
|
|
3331
|
+
});
|
|
3332
|
+
await withMockTmuxFixture('omx-empty-team-sessions-', () => `#!/bin/sh
|
|
3333
|
+
case "$1" in
|
|
3334
|
+
list-sessions) exit 0 ;;
|
|
3335
|
+
*) exit 1 ;;
|
|
3336
|
+
esac
|
|
3337
|
+
`, async () => {
|
|
3163
3338
|
assert.deepEqual(listTeamSessions(), []);
|
|
3164
3339
|
});
|
|
3165
3340
|
});
|
|
3341
|
+
it('classifies detached tmux incarnation evidence strictly', async () => {
|
|
3342
|
+
const cases = [
|
|
3343
|
+
{ name: 'exact', output: 'omx-team-incarnation\t$41\t1700000001', expected: { status: 'exact', incarnation: { sessionId: '$41', sessionCreated: '1700000001' } } },
|
|
3344
|
+
{ name: 'session-id replacement', output: 'omx-team-incarnation\t$42\t1700000001', expected: { status: 'replacement', incarnation: { sessionId: '$42', sessionCreated: '1700000001' } } },
|
|
3345
|
+
{ name: 'creation replacement', output: 'omx-team-incarnation\t$41\t1700000002', expected: { status: 'replacement', incarnation: { sessionId: '$41', sessionCreated: '1700000002' } } },
|
|
3346
|
+
];
|
|
3347
|
+
for (const entry of cases) {
|
|
3348
|
+
await withMockTmuxFixture(`omx-detached-incarnation-${entry.name.replaceAll(' ', '-')}-`, () => `#!/bin/sh
|
|
3349
|
+
case "$1" in
|
|
3350
|
+
list-sessions) printf '%s\\n' '${entry.output}' ;;
|
|
3351
|
+
*) exit 1 ;;
|
|
3352
|
+
esac
|
|
3353
|
+
`, async () => {
|
|
3354
|
+
assert.deepEqual(queryDetachedTeamSession('omx-team-incarnation', { sessionId: '$41', sessionCreated: '1700000001' }), entry.expected);
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
});
|
|
3358
|
+
it('queues a stable-ID kill behind the exact server-side leader predicate', async () => {
|
|
3359
|
+
await withMockTmuxFixture('omx-detached-incarnation-stable-kill-', (tmuxLogPath) => `#!/bin/sh
|
|
3360
|
+
printf '%s\\n' "$*" >> "${tmuxLogPath}"
|
|
3361
|
+
case "$1" in
|
|
3362
|
+
list-sessions) printf '%s\\n' 'omx-team-incarnation $41 1700000001' ;;
|
|
3363
|
+
list-panes) case "$*" in *'#{session_name}'*) printf '%s\n' '%1 0 4241 omx-team-incarnation $41 1700000001' ;; *) printf '%s\n' '%1 0 4241' ;; esac ;;
|
|
3364
|
+
show-option|show-options) printf '%s\\n' 'team:incarnation' ;;
|
|
3365
|
+
if-shell) : > "${tmuxLogPath}.session-killed" ;;
|
|
3366
|
+
*) exit 1 ;;
|
|
3367
|
+
esac
|
|
3368
|
+
`, async ({ logPath }) => {
|
|
3369
|
+
assert.equal(requestDetachedTeamSessionDestroy('omx-team-incarnation', {
|
|
3370
|
+
sessionId: '$41', sessionCreated: '1700000001', leaderPaneId: '%1', leaderPanePid: 4241, ownerId: 'team:incarnation',
|
|
3371
|
+
}), true);
|
|
3372
|
+
const log = await readFile(logPath, 'utf-8');
|
|
3373
|
+
assert.match(log, /if-shell -F -t %1/);
|
|
3374
|
+
assert.match(log, /#\{==:#\{pane_dead\},0\}/);
|
|
3375
|
+
assert.match(log, /#\{==:#\{pane_id\},%1\}/);
|
|
3376
|
+
assert.match(log, /#\{==:#\{pane_pid\},4241\}/);
|
|
3377
|
+
assert.match(log, /#\{==:#\{@omx_team_pane_owner_id\},team:incarnation\}/);
|
|
3378
|
+
assert.match(log, /#\{==:#\{session_id\},\$41\}/);
|
|
3379
|
+
assert.match(log, /#\{==:#\{session_created\},1700000001\}/);
|
|
3380
|
+
assert.match(log, /kill-session -t \$41/);
|
|
3381
|
+
assert.equal(await readFile(`${logPath}.session-killed`, 'utf8'), '');
|
|
3382
|
+
assert.doesNotMatch(log, /^kill-session -t /m);
|
|
3383
|
+
});
|
|
3384
|
+
});
|
|
3385
|
+
it('reports server-side predicate denial without retrying or accepting a detached kill', async () => {
|
|
3386
|
+
await withMockTmuxFixture('omx-detached-incarnation-server-denial-', (tmuxLogPath) => `#!/bin/sh
|
|
3387
|
+
printf '%s\\n' "$*" >> "${tmuxLogPath}"
|
|
3388
|
+
case "$1" in
|
|
3389
|
+
list-sessions) printf '%s\\n' 'omx-team-denied $41 1700000001' ;;
|
|
3390
|
+
list-panes) case "$*" in *'#{session_name}'*) printf '%s\\n' '%1 0 4241 omx-team-denied $41 1700000001' ;; *) printf '%s\\n' '%1 0 4241' ;; esac ;;
|
|
3391
|
+
show-option|show-options) printf '%s\\n' 'team:denied' ;;
|
|
3392
|
+
if-shell) : > "${tmuxLogPath}.denied"; exit 1 ;;
|
|
3393
|
+
kill-session) : > "${tmuxLogPath}.killed" ;;
|
|
3394
|
+
*) exit 1 ;;
|
|
3395
|
+
esac
|
|
3396
|
+
`, async ({ logPath }) => {
|
|
3397
|
+
assert.equal(requestDetachedTeamSessionDestroy('omx-team-denied', {
|
|
3398
|
+
sessionId: '$41', sessionCreated: '1700000001', leaderPaneId: '%1', leaderPanePid: 4241, ownerId: 'team:denied',
|
|
3399
|
+
}), false);
|
|
3400
|
+
const log = await readFile(logPath, 'utf8');
|
|
3401
|
+
assert.match(log, /if-shell -F -t %1/);
|
|
3402
|
+
assert.match(log, /run-shell "exit 1"/);
|
|
3403
|
+
assert.equal(fs.existsSync(`${logPath}.denied`), true);
|
|
3404
|
+
assert.equal(fs.existsSync(`${logPath}.killed`), false);
|
|
3405
|
+
assert.equal((log.match(/if-shell/g) ?? []).length, 1);
|
|
3406
|
+
});
|
|
3407
|
+
});
|
|
3408
|
+
it('fails closed when destroy authority has already drifted', async () => {
|
|
3409
|
+
const cases = [
|
|
3410
|
+
{ name: 'session', panePid: 4241, owner: 'team:final', session: '$42\t1700000002' },
|
|
3411
|
+
{ name: 'pid', panePid: 9999, owner: 'team:final', session: '$41\t1700000001' },
|
|
3412
|
+
{ name: 'owner', panePid: 4241, owner: 'foreign-owner', session: '$41\t1700000001' },
|
|
3413
|
+
];
|
|
3414
|
+
for (const entry of cases) {
|
|
3415
|
+
await withMockTmuxFixture(`omx-detached-final-composite-${entry.name}-`, (tmuxLogPath) => `#!/bin/sh
|
|
3416
|
+
printf '%s\\n' "$*" >> "${tmuxLogPath}"
|
|
3417
|
+
case "$1" in
|
|
3418
|
+
list-sessions) printf 'omx-team-final\\t${entry.session}\\n' ;;
|
|
3419
|
+
list-panes) case "$*" in *'#{session_name}'*) printf '%%1\\t0\\t${entry.panePid}\\tomx-team-final\\t$41\\t1700000001\\n' ;; *) printf '%%1\\t0\\t${entry.panePid}\\n' ;; esac ;;
|
|
3420
|
+
show-option|show-options) printf '%s\\n' '${entry.owner}' ;;
|
|
3421
|
+
if-shell) : > "${tmuxLogPath}.session-killed" ;;
|
|
3422
|
+
*) exit 1 ;;
|
|
3423
|
+
esac
|
|
3424
|
+
`, async ({ logPath }) => {
|
|
3425
|
+
assert.equal(requestDetachedTeamSessionDestroy('omx-team-final', {
|
|
3426
|
+
sessionId: '$41', sessionCreated: '1700000001', leaderPaneId: '%1', leaderPanePid: 4241, ownerId: 'team:final',
|
|
3427
|
+
}), false);
|
|
3428
|
+
const log = await readFile(logPath, 'utf8');
|
|
3429
|
+
assert.doesNotMatch(log, /if-shell/);
|
|
3430
|
+
assert.doesNotMatch(log, /kill-session/);
|
|
3431
|
+
assert.equal(fs.existsSync(`${logPath}.session-killed`), false);
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
});
|
|
3435
|
+
it('treats malformed and unavailable detached incarnation evidence as unavailable', async () => {
|
|
3436
|
+
await withMockTmuxFixture('omx-detached-incarnation-malformed-', () => `#!/bin/sh
|
|
3437
|
+
case "$1" in
|
|
3438
|
+
list-sessions) printf '%s\\n' 'omx-team-incarnation\\t$41' ;;
|
|
3439
|
+
*) exit 1 ;;
|
|
3440
|
+
esac
|
|
3441
|
+
`, async () => assert.deepEqual(queryDetachedTeamSession('omx-team-incarnation'), {
|
|
3442
|
+
status: 'unavailable', detail: 'malformed_session_incarnation',
|
|
3443
|
+
}));
|
|
3444
|
+
await withMockTmuxFixture('omx-detached-incarnation-unavailable-', () => `#!/bin/sh
|
|
3445
|
+
case "$1" in
|
|
3446
|
+
list-sessions) printf '%s\\n' 'permission denied' >&2; exit 1 ;;
|
|
3447
|
+
*) exit 1 ;;
|
|
3448
|
+
esac
|
|
3449
|
+
`, async () => assert.deepEqual(queryDetachedTeamSession('omx-team-incarnation'), {
|
|
3450
|
+
status: 'unavailable', detail: 'permission denied',
|
|
3451
|
+
}));
|
|
3452
|
+
});
|
|
3453
|
+
it('recognizes authoritative detached-session absence', async () => {
|
|
3454
|
+
await withMockTmuxFixture('omx-detached-incarnation-absence-', () => `#!/bin/sh
|
|
3455
|
+
case "$1" in
|
|
3456
|
+
list-sessions) exit 0 ;;
|
|
3457
|
+
*) exit 1 ;;
|
|
3458
|
+
esac
|
|
3459
|
+
`, async () => assert.deepEqual(queryDetachedTeamSession('omx-team-incarnation'), { status: 'absent' }));
|
|
3460
|
+
});
|
|
3461
|
+
it('accepts final-session destruction when tmux reports its expected no-server absence', async () => {
|
|
3462
|
+
await withMockTmuxFixture('omx-destroy-final-team-session-', () => `#!/bin/sh
|
|
3463
|
+
case "$1" in
|
|
3464
|
+
kill-session) exit 0 ;;
|
|
3465
|
+
list-sessions) printf '%s\\n' 'no server running on /tmp/tmux-1000/default' >&2; exit 1 ;;
|
|
3466
|
+
*) exit 1 ;;
|
|
3467
|
+
esac
|
|
3468
|
+
`, async () => {
|
|
3469
|
+
assert.equal(destroyTeamSession('omx-team-final-session'), true);
|
|
3470
|
+
});
|
|
3471
|
+
});
|
|
3472
|
+
it('does not accept a session kill when the absence query fails for another reason', async () => {
|
|
3473
|
+
await withMockTmuxFixture('omx-destroy-team-session-query-failure-', () => `#!/bin/sh
|
|
3474
|
+
case "$1" in
|
|
3475
|
+
kill-session) exit 0 ;;
|
|
3476
|
+
list-sessions) printf '%s\\n' 'permission denied' >&2; exit 1 ;;
|
|
3477
|
+
*) exit 1 ;;
|
|
3478
|
+
esac
|
|
3479
|
+
`, async () => {
|
|
3480
|
+
assert.equal(destroyTeamSession('omx-team-query-failure'), false);
|
|
3481
|
+
});
|
|
3482
|
+
});
|
|
3166
3483
|
it('waitForWorkerReady uses visible capture-pane argv without tail flags', async () => {
|
|
3167
3484
|
await withMockTmuxFixture('omx-tmux-worker-ready-visible-capture-', (logPath) => `#!/bin/sh
|
|
3168
3485
|
set -eu
|
|
@@ -3626,7 +3943,9 @@ exit 0
|
|
|
3626
3943
|
const prevTmuxPane = process.env.TMUX_PANE;
|
|
3627
3944
|
const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
3628
3945
|
try {
|
|
3629
|
-
await withMockTmuxFixture('omx-tmux-redraw-leader-', (logPath) =>
|
|
3946
|
+
await withMockTmuxFixture('omx-tmux-redraw-leader-', (logPath) => {
|
|
3947
|
+
const proofStatePath = `${logPath}.leader-redraw-proof`;
|
|
3948
|
+
return `#!/bin/sh
|
|
3630
3949
|
set -eu
|
|
3631
3950
|
printf '%s\n' "$*" >> "${logPath}"
|
|
3632
3951
|
case "\${1:-}" in
|
|
@@ -3647,11 +3966,22 @@ case "\${1:-}" in
|
|
|
3647
3966
|
;;
|
|
3648
3967
|
list-panes)
|
|
3649
3968
|
case "$*" in
|
|
3969
|
+
*"-a -F #{pane_id}"*)
|
|
3970
|
+
proof_count=0
|
|
3971
|
+
if [ -f "${proofStatePath}" ]; then proof_count=$(cat "${proofStatePath}"); fi
|
|
3972
|
+
proof_count=$((proof_count + 1))
|
|
3973
|
+
printf '%s' "$proof_count" > "${proofStatePath}"
|
|
3974
|
+
if [ "$proof_count" -eq 100 ]; then
|
|
3975
|
+
printf 'not-a-pane-snapshot\n'
|
|
3976
|
+
else
|
|
3977
|
+
printf "%%1\t0\t2000000001\n%%2\t0\t2000000002\n%%3\t0\t2000000003\n"
|
|
3978
|
+
fi
|
|
3979
|
+
;;
|
|
3650
3980
|
*"pane_current_command"*)
|
|
3651
|
-
printf "%%1\\tnode\\t'codex'\\n"
|
|
3981
|
+
if [ -f "${logPath}.hud" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n%%3\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE=%%1 node /omx.js hud --watch\\n"; elif [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
|
|
3652
3982
|
;;
|
|
3653
3983
|
*)
|
|
3654
|
-
printf "%%1\\n"
|
|
3984
|
+
if [ -f "${logPath}.hud" ]; then printf "%%1\\n%%2\\n%%3\\n"; elif [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi
|
|
3655
3985
|
;;
|
|
3656
3986
|
esac
|
|
3657
3987
|
exit 0
|
|
@@ -3659,9 +3989,11 @@ case "\${1:-}" in
|
|
|
3659
3989
|
split-window)
|
|
3660
3990
|
case "$*" in
|
|
3661
3991
|
*" -h "*)
|
|
3992
|
+
: > "${logPath}.worker"
|
|
3662
3993
|
echo "%2"
|
|
3663
3994
|
;;
|
|
3664
3995
|
*)
|
|
3996
|
+
: > "${logPath}.hud"
|
|
3665
3997
|
echo "%3"
|
|
3666
3998
|
;;
|
|
3667
3999
|
esac
|
|
@@ -3674,7 +4006,8 @@ case "\${1:-}" in
|
|
|
3674
4006
|
exit 0
|
|
3675
4007
|
;;
|
|
3676
4008
|
esac
|
|
3677
|
-
|
|
4009
|
+
`;
|
|
4010
|
+
}, async ({ logPath }) => {
|
|
3678
4011
|
const fakeBinDir = join(logPath, '..');
|
|
3679
4012
|
const geminiPath = join(fakeBinDir, 'gemini');
|
|
3680
4013
|
await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
|
|
@@ -3684,10 +4017,17 @@ esac
|
|
|
3684
4017
|
process.env.OMX_TEAM_WORKER_CLI = 'gemini';
|
|
3685
4018
|
createTeamSession('Diff Gutter Redraw', 1, cwd);
|
|
3686
4019
|
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
4020
|
+
const commands = tmuxLog.trim().split('\n').filter(Boolean);
|
|
3687
4021
|
assert.match(tmuxLog, /select-layout -t leader:0 main-vertical/);
|
|
3688
4022
|
assert.match(tmuxLog, /set-window-option -t leader:0 main-pane-width 60/);
|
|
3689
|
-
assert.match(tmuxLog, /split-window -v -f -l 3 -t
|
|
3690
|
-
|
|
4023
|
+
assert.match(tmuxLog, /split-window -v -f -l 3 -t %1 -d -P -F #\{pane_id\}/);
|
|
4024
|
+
const redrawIndices = commands
|
|
4025
|
+
.map((command, index) => command === 'send-keys -t %1 C-l' ? index : -1)
|
|
4026
|
+
.filter((index) => index >= 0);
|
|
4027
|
+
assert.equal(redrawIndices.length, 1);
|
|
4028
|
+
const redrawIndex = redrawIndices[0];
|
|
4029
|
+
assert.ok(redrawIndex > 0);
|
|
4030
|
+
assert.match(commands[redrawIndex - 1] ?? '', /^list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}$/, 'leader Codex pane redraw must be immediately preceded by exact global live-pane proof');
|
|
3691
4031
|
});
|
|
3692
4032
|
}
|
|
3693
4033
|
finally {
|
|
@@ -3706,6 +4046,92 @@ esac
|
|
|
3706
4046
|
await rm(cwd, { recursive: true, force: true });
|
|
3707
4047
|
}
|
|
3708
4048
|
});
|
|
4049
|
+
it('blocks window-wide layout when an extra pane appears during a later owner check', async () => {
|
|
4050
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-final-window-proof-'));
|
|
4051
|
+
const previousTmux = process.env.TMUX;
|
|
4052
|
+
const previousTmuxPane = process.env.TMUX_PANE;
|
|
4053
|
+
const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
4054
|
+
try {
|
|
4055
|
+
await withMockTmuxFixture('omx-tmux-final-window-proof-', (logPath) => `#!/bin/sh
|
|
4056
|
+
set -eu
|
|
4057
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
4058
|
+
case "\${1:-}" in
|
|
4059
|
+
-V) echo 'tmux 3.4' ;;
|
|
4060
|
+
display-message)
|
|
4061
|
+
case "$*" in *'#{window_width}'*) echo 120 ;; *) echo 'leader:0 %1' ;; esac
|
|
4062
|
+
;;
|
|
4063
|
+
list-panes)
|
|
4064
|
+
case "$*" in
|
|
4065
|
+
*'-a -F #{pane_id}\t#{pane_dead}\t#{pane_pid}'*)
|
|
4066
|
+
if [ -f "${logPath}.replace" ]; then
|
|
4067
|
+
printf '%%1\\t0\\t2000000001\\n%%2\\t0\\t2000000002\\n%%3\\t0\\t2000000003\\n'
|
|
4068
|
+
else
|
|
4069
|
+
printf '%%1\\t0\\t2000000001\\n%%2\\t0\\t2000000002\\n'
|
|
4070
|
+
fi
|
|
4071
|
+
;;
|
|
4072
|
+
*'pane_current_command'*)
|
|
4073
|
+
if [ -f "${logPath}.replace" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n%%3\\tzsh\\tzsh\\n"; elif [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
|
|
4074
|
+
;;
|
|
4075
|
+
*)
|
|
4076
|
+
if [ -f "${logPath}.replace" ]; then printf '%%1\\n%%2\\n%%3\\n'; elif [ -f "${logPath}.worker" ]; then printf '%%1\\n%%2\\n'; else printf '%%1\\n'; fi
|
|
4077
|
+
;;
|
|
4078
|
+
esac
|
|
4079
|
+
;;
|
|
4080
|
+
split-window) : > "${logPath}.worker"; echo '%2' ;;
|
|
4081
|
+
show-option)
|
|
4082
|
+
case "$*" in
|
|
4083
|
+
*' -t %1 @omx_team_pane_owner_id') echo 'team:final-window-proof' ;;
|
|
4084
|
+
*' -t %2 @omx_team_pane_owner_id') : > "${logPath}.replace"; echo 'team:final-window-proof' ;;
|
|
4085
|
+
*) exit 1 ;;
|
|
4086
|
+
esac
|
|
4087
|
+
;;
|
|
4088
|
+
set-option|resize-pane|select-pane|set-hook|run-shell|send-keys) ;;
|
|
4089
|
+
kill-pane|select-layout|set-window-option) exit 97 ;;
|
|
4090
|
+
esac
|
|
4091
|
+
`, async ({ logPath }) => {
|
|
4092
|
+
const fakeBinDir = join(logPath, '..');
|
|
4093
|
+
const geminiPath = join(fakeBinDir, 'gemini');
|
|
4094
|
+
await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
|
|
4095
|
+
await chmod(geminiPath, 0o755);
|
|
4096
|
+
process.env.TMUX = 'leader-session,stub,0';
|
|
4097
|
+
process.env.TMUX_PANE = '%1';
|
|
4098
|
+
process.env.OMX_TEAM_WORKER_CLI = 'gemini';
|
|
4099
|
+
assert.throws(() => createTeamSession('Final Window Proof', 1, cwd, [], undefined, {
|
|
4100
|
+
teamPaneOwnerId: 'team:final-window-proof',
|
|
4101
|
+
}), (error) => {
|
|
4102
|
+
assert.ok(error instanceof CreateTeamSessionPartialError);
|
|
4103
|
+
assert.ok(error.originalError instanceof Error);
|
|
4104
|
+
assert.match(error.originalError.message, /tmux window topology changed before layout mutation/);
|
|
4105
|
+
assert.equal(error.partialSession.name, 'leader:0');
|
|
4106
|
+
assert.equal(error.partialSession.teamPaneOwnerId, 'team:final-window-proof');
|
|
4107
|
+
assert.deepEqual(error.partialSession.workerPaneIds, ['%2']);
|
|
4108
|
+
assert.deepEqual(error.partialSession.workerPanePidsByIndex, [2000000002]);
|
|
4109
|
+
assert.deepEqual(error.proofUnavailable, []);
|
|
4110
|
+
assert.ok(error.cleanupErrors.some((message) => /failed to kill tmux pane %2/.test(message)));
|
|
4111
|
+
return true;
|
|
4112
|
+
});
|
|
4113
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
4114
|
+
assert.match(commands, /show-option -qv -p -t %2 @omx_team_pane_owner_id/);
|
|
4115
|
+
assert.doesNotMatch(commands, /select-layout|set-window-option/);
|
|
4116
|
+
assert.match(commands, /kill-pane -t %2/);
|
|
4117
|
+
});
|
|
4118
|
+
}
|
|
4119
|
+
finally {
|
|
4120
|
+
if (typeof previousTmux === 'string')
|
|
4121
|
+
process.env.TMUX = previousTmux;
|
|
4122
|
+
else
|
|
4123
|
+
delete process.env.TMUX;
|
|
4124
|
+
if (typeof previousTmuxPane === 'string')
|
|
4125
|
+
process.env.TMUX_PANE = previousTmuxPane;
|
|
4126
|
+
else
|
|
4127
|
+
delete process.env.TMUX_PANE;
|
|
4128
|
+
if (typeof previousWorkerCli === 'string')
|
|
4129
|
+
process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
|
|
4130
|
+
else
|
|
4131
|
+
delete process.env.OMX_TEAM_WORKER_CLI;
|
|
4132
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4133
|
+
}
|
|
4134
|
+
});
|
|
3709
4135
|
it('tags leader, worker, and HUD panes with pane-scoped instance ownership', async () => {
|
|
3710
4136
|
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-pane-tags-'));
|
|
3711
4137
|
const prevTmux = process.env.TMUX;
|
|
@@ -3734,16 +4160,20 @@ case "\${1:-}" in
|
|
|
3734
4160
|
;;
|
|
3735
4161
|
list-panes)
|
|
3736
4162
|
case "$*" in
|
|
4163
|
+
*"-a -F #{pane_id}"*)
|
|
4164
|
+
printf "%%1\t0\t2000000001\n%%2\t0\t2000000002\n%%3\t0\t2000000003\n"
|
|
4165
|
+
;;
|
|
3737
4166
|
*"pane_current_command"*)
|
|
3738
|
-
printf "%%1\\tnode\\t'codex'\\n"
|
|
4167
|
+
if [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
|
|
3739
4168
|
;;
|
|
3740
4169
|
*)
|
|
3741
|
-
printf "%%1\\n"
|
|
4170
|
+
if [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi
|
|
3742
4171
|
;;
|
|
3743
4172
|
esac
|
|
3744
4173
|
exit 0
|
|
3745
4174
|
;;
|
|
3746
4175
|
split-window)
|
|
4176
|
+
: > "${logPath}.worker"
|
|
3747
4177
|
case "$*" in
|
|
3748
4178
|
*" -h "*)
|
|
3749
4179
|
echo "%2"
|
|
@@ -3784,6 +4214,20 @@ esac
|
|
|
3784
4214
|
assert.match(tmuxLog, /set-option -p -t %2 @omx_team_pane_owner_id team:pane-tags/);
|
|
3785
4215
|
assert.match(tmuxLog, /set-option -p -t %3 @omx_team_pane_owner_id team:pane-tags/);
|
|
3786
4216
|
assert.match(tmuxLog, /exec env OMX_SESSION_ID='omx-pane-scope' OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%1' .*hud --watch/);
|
|
4217
|
+
const commands = tmuxLog.trim().split('\n').filter(Boolean);
|
|
4218
|
+
const globalExactPanePidProof = /^list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}$/;
|
|
4219
|
+
const targetScopedExactPaneSetProof = /^list-panes -t shared:0 -F #\{pane_id\}\t#\{pane_current_command\}\t#\{pane_start_command\}$/;
|
|
4220
|
+
const exactPaneEffects = /^(set-option -p -t %|split-window .* -t %|resize-pane -t %|select-pane -t %|send-keys -t %)/;
|
|
4221
|
+
for (const [index, command] of commands.entries()) {
|
|
4222
|
+
if (!exactPaneEffects.test(command))
|
|
4223
|
+
continue;
|
|
4224
|
+
const immediatelyPrevious = commands[index - 1] ?? '';
|
|
4225
|
+
const previousProof = commands[index - 2] ?? '';
|
|
4226
|
+
const hasAdjacentAuthority = globalExactPanePidProof.test(immediatelyPrevious)
|
|
4227
|
+
|| (targetScopedExactPaneSetProof.test(immediatelyPrevious)
|
|
4228
|
+
&& globalExactPanePidProof.test(previousProof));
|
|
4229
|
+
assert.equal(hasAdjacentAuthority, true, `exact-pane effect must be immediately preceded by an authoritative exact-pane proof: ${command}`);
|
|
4230
|
+
}
|
|
3787
4231
|
});
|
|
3788
4232
|
}
|
|
3789
4233
|
finally {
|
|
@@ -3806,6 +4250,71 @@ esac
|
|
|
3806
4250
|
await rm(cwd, { recursive: true, force: true });
|
|
3807
4251
|
}
|
|
3808
4252
|
});
|
|
4253
|
+
it('fails closed when a tagged worker keeps its PID but loses Team owner continuity before a later split', async () => {
|
|
4254
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-owner-change-'));
|
|
4255
|
+
const prevTmux = process.env.TMUX;
|
|
4256
|
+
const prevTmuxPane = process.env.TMUX_PANE;
|
|
4257
|
+
const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
4258
|
+
try {
|
|
4259
|
+
await withMockTmuxFixture('omx-tmux-owner-change-', (logPath) => `#!/bin/sh
|
|
4260
|
+
set -eu
|
|
4261
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
4262
|
+
case "\${1:-}" in
|
|
4263
|
+
-V) echo "tmux 3.4" ;;
|
|
4264
|
+
display-message)
|
|
4265
|
+
case "$*" in *"#{window_width}"*) echo "120" ;; *) echo "leader:0 %1" ;; esac
|
|
4266
|
+
;;
|
|
4267
|
+
list-panes)
|
|
4268
|
+
case "$*" in
|
|
4269
|
+
*"-a -F #{pane_id}"*) printf "%%1\\t0\\t2000000001\\n%%2\\t0\\t2000000002\\n" ;;
|
|
4270
|
+
*"pane_current_command"*)
|
|
4271
|
+
if [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
|
|
4272
|
+
;;
|
|
4273
|
+
*) if [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi ;;
|
|
4274
|
+
esac
|
|
4275
|
+
;;
|
|
4276
|
+
split-window) : > "${logPath}.worker"; echo "%2" ;;
|
|
4277
|
+
set-option)
|
|
4278
|
+
case "$*" in
|
|
4279
|
+
*"-p -t %2 @omx_team_pane_owner_id"*)
|
|
4280
|
+
printf '%s' 'team:foreign' > "$(dirname "${logPath}")/tmux.team-owner-state/%2"
|
|
4281
|
+
;;
|
|
4282
|
+
esac
|
|
4283
|
+
;;
|
|
4284
|
+
*) ;;
|
|
4285
|
+
esac
|
|
4286
|
+
`, async ({ logPath }) => {
|
|
4287
|
+
const fakeBinDir = join(logPath, '..');
|
|
4288
|
+
const geminiPath = join(fakeBinDir, 'gemini');
|
|
4289
|
+
await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
|
|
4290
|
+
await chmod(geminiPath, 0o755);
|
|
4291
|
+
process.env.TMUX = 'leader-session,stub,0';
|
|
4292
|
+
process.env.TMUX_PANE = '%1';
|
|
4293
|
+
process.env.OMX_TEAM_WORKER_CLI = 'gemini';
|
|
4294
|
+
assert.throws(() => createTeamSession('Owner Change', 2, cwd), (error) => error instanceof CreateTeamSessionPartialError
|
|
4295
|
+
&& error.originalError instanceof Error
|
|
4296
|
+
&& /tmux pane team owner changed: %2/.test(error.originalError.message));
|
|
4297
|
+
const effects = await readFile(logPath, 'utf-8');
|
|
4298
|
+
assert.match(effects, /split-window -h -t %1/);
|
|
4299
|
+
assert.doesNotMatch(effects, /split-window -v|select-layout|set-window-option|resize-pane|select-pane|send-keys|set-hook|run-shell/);
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
finally {
|
|
4303
|
+
if (typeof prevTmux === 'string')
|
|
4304
|
+
process.env.TMUX = prevTmux;
|
|
4305
|
+
else
|
|
4306
|
+
delete process.env.TMUX;
|
|
4307
|
+
if (typeof prevTmuxPane === 'string')
|
|
4308
|
+
process.env.TMUX_PANE = prevTmuxPane;
|
|
4309
|
+
else
|
|
4310
|
+
delete process.env.TMUX_PANE;
|
|
4311
|
+
if (typeof prevWorkerCli === 'string')
|
|
4312
|
+
process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
|
|
4313
|
+
else
|
|
4314
|
+
delete process.env.OMX_TEAM_WORKER_CLI;
|
|
4315
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4316
|
+
}
|
|
4317
|
+
});
|
|
3809
4318
|
it('does not reuse the Team pane owner token as the HUD logical session id', async () => {
|
|
3810
4319
|
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-hud-session-boundary-'));
|
|
3811
4320
|
const prevTmux = process.env.TMUX;
|
|
@@ -3834,19 +4343,23 @@ case "\${1:-}" in
|
|
|
3834
4343
|
;;
|
|
3835
4344
|
list-panes)
|
|
3836
4345
|
case "$*" in
|
|
4346
|
+
*"-a -F #{pane_id}"*)
|
|
4347
|
+
printf "%%1\t0\t2000000001\n%%2\t0\t2000002222\n%%3\t0\t2000000003\n"
|
|
4348
|
+
;;
|
|
3837
4349
|
*"-t %2 -F #{pane_pid}"*)
|
|
3838
4350
|
echo "2000002222"
|
|
3839
4351
|
;;
|
|
3840
4352
|
*"pane_current_command"*)
|
|
3841
|
-
printf "%%1\\tnode\\t'codex'\\n"
|
|
4353
|
+
if [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
|
|
3842
4354
|
;;
|
|
3843
4355
|
*)
|
|
3844
|
-
printf "%%1\\n"
|
|
4356
|
+
if [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi
|
|
3845
4357
|
;;
|
|
3846
4358
|
esac
|
|
3847
4359
|
exit 0
|
|
3848
4360
|
;;
|
|
3849
4361
|
split-window)
|
|
4362
|
+
: > "${logPath}.worker"
|
|
3850
4363
|
case "$*" in
|
|
3851
4364
|
*" -h "*)
|
|
3852
4365
|
echo "%2"
|
|
@@ -3906,7 +4419,7 @@ esac
|
|
|
3906
4419
|
await rm(cwd, { recursive: true, force: true });
|
|
3907
4420
|
}
|
|
3908
4421
|
});
|
|
3909
|
-
it('
|
|
4422
|
+
it('rejects an unowned pane in startup topology before any window-wide mutation', async () => {
|
|
3910
4423
|
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-owned-hud-startup-'));
|
|
3911
4424
|
const prevTmux = process.env.TMUX;
|
|
3912
4425
|
const prevTmuxPane = process.env.TMUX_PANE;
|
|
@@ -3934,6 +4447,10 @@ case "\${1:-}" in
|
|
|
3934
4447
|
;;
|
|
3935
4448
|
list-panes)
|
|
3936
4449
|
case "$*" in
|
|
4450
|
+
*"-a -F #{pane_id}"*)
|
|
4451
|
+
printf "%%1\t0\t2000000001\n%%3\t0\t2000000003\n%%4\t0\t2000000004\n%%7\t0\t2000000007\n%%8\t0\t2000000008\n"
|
|
4452
|
+
if [ ! -f "${logPath}.killed-%2" ]; then printf "%%2\t0\t2000000002\n"; fi
|
|
4453
|
+
;;
|
|
3937
4454
|
*"pane_current_command"*)
|
|
3938
4455
|
printf "%%1\\tnode\\t'codex'\\n"
|
|
3939
4456
|
printf "%%7\\tnode\\t'codex neighbor'\\n"
|
|
@@ -3957,7 +4474,11 @@ case "\${1:-}" in
|
|
|
3957
4474
|
esac
|
|
3958
4475
|
exit 0
|
|
3959
4476
|
;;
|
|
3960
|
-
|
|
4477
|
+
kill-pane)
|
|
4478
|
+
: > "${logPath}.killed-$3"
|
|
4479
|
+
exit 0
|
|
4480
|
+
;;
|
|
4481
|
+
set-option|resize-pane|select-layout|set-window-option|select-pane|set-hook|run-shell|send-keys)
|
|
3961
4482
|
exit 0
|
|
3962
4483
|
;;
|
|
3963
4484
|
*)
|
|
@@ -3973,13 +4494,9 @@ esac
|
|
|
3973
4494
|
process.env.TMUX_PANE = '%1';
|
|
3974
4495
|
process.env.OMX_SESSION_ID = 'leader-session-a';
|
|
3975
4496
|
process.env.OMX_TEAM_WORKER_CLI = 'gemini';
|
|
3976
|
-
|
|
3977
|
-
assert.equal(session.leaderPaneId, '%1');
|
|
3978
|
-
assert.equal(session.hudPaneId, '%4');
|
|
4497
|
+
assert.throws(() => createTeamSession('Owned HUD Startup', 1, cwd), /tmux window topology changed before layout mutation/);
|
|
3979
4498
|
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
3980
|
-
assert.
|
|
3981
|
-
assert.doesNotMatch(tmuxLog, /kill-pane -t %8/);
|
|
3982
|
-
assert.match(tmuxLog, /split-window -v -f -l 3 -t shared:0 -d -P -F #\{pane_id\}/);
|
|
4499
|
+
assert.doesNotMatch(tmuxLog, /set-option|kill-pane|split-window|resize-pane|select-layout|set-window-option|select-pane|send-keys/);
|
|
3983
4500
|
});
|
|
3984
4501
|
}
|
|
3985
4502
|
finally {
|
|
@@ -4002,71 +4519,64 @@ esac
|
|
|
4002
4519
|
await rm(cwd, { recursive: true, force: true });
|
|
4003
4520
|
}
|
|
4004
4521
|
});
|
|
4005
|
-
it('
|
|
4006
|
-
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-
|
|
4007
|
-
const
|
|
4008
|
-
const
|
|
4009
|
-
const
|
|
4010
|
-
const prevWarn = console.warn;
|
|
4011
|
-
const warnings = [];
|
|
4522
|
+
it('preserves rollback debt when a successfully tagged pane owner changes before cleanup', async () => {
|
|
4523
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-partial-rollback-proof-'));
|
|
4524
|
+
const previousTmux = process.env.TMUX;
|
|
4525
|
+
const previousTmuxPane = process.env.TMUX_PANE;
|
|
4526
|
+
const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
4012
4527
|
try {
|
|
4013
|
-
await withMockTmuxFixture('omx-tmux-
|
|
4528
|
+
await withMockTmuxFixture('omx-tmux-partial-rollback-proof-', (logPath) => `#!/bin/sh
|
|
4014
4529
|
set -eu
|
|
4015
|
-
printf '%s
|
|
4016
|
-
case "
|
|
4530
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
4531
|
+
case "$1" in
|
|
4017
4532
|
-V)
|
|
4018
|
-
echo "tmux 3.
|
|
4533
|
+
echo "tmux 3.4"
|
|
4019
4534
|
exit 0
|
|
4020
4535
|
;;
|
|
4021
4536
|
display-message)
|
|
4022
4537
|
case "$*" in
|
|
4023
|
-
*"#{window_width}"*)
|
|
4024
|
-
|
|
4025
|
-
;;
|
|
4026
|
-
*)
|
|
4027
|
-
echo "leader:0 %1"
|
|
4028
|
-
;;
|
|
4538
|
+
*"#{window_width}"*) echo "120" ;;
|
|
4539
|
+
*) echo "leader:0 %1" ;;
|
|
4029
4540
|
esac
|
|
4030
4541
|
exit 0
|
|
4031
4542
|
;;
|
|
4032
4543
|
list-panes)
|
|
4033
4544
|
case "$*" in
|
|
4545
|
+
*"-a -F #{pane_id}"*)
|
|
4546
|
+
proof_count=0
|
|
4547
|
+
if [ -f "${logPath}.proof-count" ]; then proof_count=$(cat "${logPath}.proof-count"); fi
|
|
4548
|
+
proof_count=$((proof_count + 1))
|
|
4549
|
+
printf '%s' "$proof_count" > "${logPath}.proof-count"
|
|
4550
|
+
if [ "$proof_count" -gt 11 ]; then printf 'not-a-pane-snapshot\n'; else printf "%%1\t0\t2000000001\n%%2\t0\t2000000002\n"; fi
|
|
4551
|
+
|
|
4552
|
+
;;
|
|
4034
4553
|
*"pane_current_command"*)
|
|
4035
|
-
printf "%%1
|
|
4554
|
+
if [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
|
|
4036
4555
|
;;
|
|
4037
4556
|
*)
|
|
4038
|
-
printf "%%1
|
|
4557
|
+
if [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi
|
|
4039
4558
|
;;
|
|
4040
4559
|
esac
|
|
4041
4560
|
exit 0
|
|
4042
4561
|
;;
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
*" -h "*)
|
|
4046
|
-
echo "%2"
|
|
4047
|
-
;;
|
|
4048
|
-
*)
|
|
4049
|
-
echo "%3"
|
|
4050
|
-
;;
|
|
4051
|
-
esac
|
|
4562
|
+
show-option)
|
|
4563
|
+
if [ -f "${logPath}.worker" ]; then echo "team:foreign"; else echo "team:partial-rollback-proof"; fi
|
|
4052
4564
|
exit 0
|
|
4053
4565
|
;;
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
;;
|
|
4067
|
-
esac
|
|
4566
|
+
split-window)
|
|
4567
|
+
: > "${logPath}.worker"
|
|
4568
|
+
is_horizontal=0
|
|
4569
|
+
for arg in "$@"; do
|
|
4570
|
+
if [ "$arg" = "-h" ]; then is_horizontal=1; fi
|
|
4571
|
+
done
|
|
4572
|
+
if [ "$is_horizontal" = "1" ]; then
|
|
4573
|
+
echo "%2"
|
|
4574
|
+
else
|
|
4575
|
+
echo "second worker rejected" >&2
|
|
4576
|
+
exit 1
|
|
4577
|
+
fi
|
|
4068
4578
|
;;
|
|
4069
|
-
set-option|
|
|
4579
|
+
set-option|select-layout|set-window-option|select-pane|set-hook|run-shell|kill-pane)
|
|
4070
4580
|
exit 0
|
|
4071
4581
|
;;
|
|
4072
4582
|
*)
|
|
@@ -4081,82 +4591,164 @@ esac
|
|
|
4081
4591
|
process.env.TMUX = 'leader-session,stub,0';
|
|
4082
4592
|
process.env.TMUX_PANE = '%1';
|
|
4083
4593
|
process.env.OMX_TEAM_WORKER_CLI = 'gemini';
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4594
|
+
assert.throws(() => createTeamSession('Partial Rollback Proof', 2, cwd), (error) => {
|
|
4595
|
+
assert.ok(error instanceof Error);
|
|
4596
|
+
assert.equal(error.name, 'CreateTeamSessionPartialError');
|
|
4597
|
+
const partial = error;
|
|
4598
|
+
assert.equal(partial.partialSession.name, 'leader:0');
|
|
4599
|
+
assert.equal(partial.partialSession.workerCount, 2);
|
|
4600
|
+
assert.deepEqual(partial.partialSession.workerPaneIds, ['%2']);
|
|
4601
|
+
assert.ok(partial.proofUnavailable.length > 0 || partial.cleanupErrors.length > 0);
|
|
4602
|
+
return true;
|
|
4603
|
+
});
|
|
4090
4604
|
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
4091
|
-
assert.match(tmuxLog, /
|
|
4092
|
-
assert.doesNotMatch(tmuxLog, /window-resized\[/);
|
|
4093
|
-
assert.doesNotMatch(tmuxLog, /set-hook -w /);
|
|
4094
|
-
assert.match(tmuxLog, /set-hook -t leader:0 client-attached\[\d+\]/);
|
|
4095
|
-
assert.match(tmuxLog, new RegExp(`run-shell -b sleep ${HUD_RESIZE_RECONCILE_DELAY_SECONDS}; .*resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
|
|
4096
|
-
assert.match(tmuxLog, new RegExp(`run-shell .*resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
|
|
4605
|
+
assert.match(tmuxLog, /list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}/);
|
|
4097
4606
|
assert.doesNotMatch(tmuxLog, /kill-pane -t %2/);
|
|
4098
|
-
assert.doesNotMatch(tmuxLog, /kill-pane -t %3/);
|
|
4099
4607
|
});
|
|
4100
4608
|
}
|
|
4101
4609
|
finally {
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
process.env.TMUX = prevTmux;
|
|
4610
|
+
if (typeof previousTmux === 'string')
|
|
4611
|
+
process.env.TMUX = previousTmux;
|
|
4105
4612
|
else
|
|
4106
4613
|
delete process.env.TMUX;
|
|
4107
|
-
if (typeof
|
|
4108
|
-
process.env.TMUX_PANE =
|
|
4614
|
+
if (typeof previousTmuxPane === 'string')
|
|
4615
|
+
process.env.TMUX_PANE = previousTmuxPane;
|
|
4109
4616
|
else
|
|
4110
4617
|
delete process.env.TMUX_PANE;
|
|
4111
|
-
if (typeof
|
|
4112
|
-
process.env.OMX_TEAM_WORKER_CLI =
|
|
4618
|
+
if (typeof previousWorkerCli === 'string')
|
|
4619
|
+
process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
|
|
4113
4620
|
else
|
|
4114
4621
|
delete process.env.OMX_TEAM_WORKER_CLI;
|
|
4115
4622
|
await rm(cwd, { recursive: true, force: true });
|
|
4116
4623
|
}
|
|
4117
4624
|
});
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
await withMockTmuxFixture('omx-tmux-runshell-fallback-', (logPath) => `#!/bin/sh
|
|
4625
|
+
for (const firstProofFailure of ['malformed', 'unavailable']) {
|
|
4626
|
+
it(`preserves unproven split pane debt when its first proof is ${firstProofFailure}`, async () => {
|
|
4627
|
+
const cwd = await mkdtemp(join(tmpdir(), `omx-team-first-split-${firstProofFailure}-`));
|
|
4628
|
+
const previousTmux = process.env.TMUX;
|
|
4629
|
+
const previousTmuxPane = process.env.TMUX_PANE;
|
|
4630
|
+
const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
4631
|
+
try {
|
|
4632
|
+
await withMockTmuxFixture(`omx-tmux-first-split-${firstProofFailure}-`, (logPath) => `#!/bin/sh
|
|
4127
4633
|
set -eu
|
|
4128
|
-
printf '%s
|
|
4129
|
-
case "
|
|
4634
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
4635
|
+
case "$1" in
|
|
4130
4636
|
-V)
|
|
4131
4637
|
echo "tmux 3.4"
|
|
4132
|
-
exit 0
|
|
4133
4638
|
;;
|
|
4134
4639
|
display-message)
|
|
4135
4640
|
case "$*" in
|
|
4136
4641
|
*"#{window_width}"*) echo "120" ;;
|
|
4137
4642
|
*) echo "leader:0 %1" ;;
|
|
4138
4643
|
esac
|
|
4139
|
-
exit 0
|
|
4140
4644
|
;;
|
|
4141
4645
|
list-panes)
|
|
4142
4646
|
case "$*" in
|
|
4647
|
+
*"-a -F #{pane_id}"*)
|
|
4648
|
+
if [ -f "${logPath}.worker-created" ]; then
|
|
4649
|
+
${firstProofFailure === 'malformed'
|
|
4650
|
+
? "printf 'not-a-pane-snapshot\\n'"
|
|
4651
|
+
: "echo 'topology unavailable' >&2; exit 1"}
|
|
4652
|
+
else
|
|
4653
|
+
printf "%%1\\t0\\t2000000001\\n"
|
|
4654
|
+
fi
|
|
4655
|
+
;;
|
|
4143
4656
|
*"pane_current_command"*) printf "%%1\\tnode\\t'codex'\\n" ;;
|
|
4144
4657
|
*) printf "%%1\\n" ;;
|
|
4145
4658
|
esac
|
|
4146
|
-
|
|
4659
|
+
;;
|
|
4660
|
+
show-option)
|
|
4661
|
+
echo "team:first-split-proof"
|
|
4147
4662
|
;;
|
|
4148
4663
|
split-window)
|
|
4664
|
+
: > "${logPath}.worker-created"
|
|
4665
|
+
echo "%2"
|
|
4666
|
+
;;
|
|
4667
|
+
set-option|select-layout|set-window-option|select-pane|set-hook|run-shell|kill-pane|send-keys|resize-pane)
|
|
4668
|
+
exit 0
|
|
4669
|
+
;;
|
|
4670
|
+
*)
|
|
4671
|
+
exit 0
|
|
4672
|
+
;;
|
|
4673
|
+
esac
|
|
4674
|
+
`, async ({ logPath }) => {
|
|
4675
|
+
const fakeBinDir = join(logPath, '..');
|
|
4676
|
+
const geminiPath = join(fakeBinDir, 'gemini');
|
|
4677
|
+
await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
|
|
4678
|
+
await chmod(geminiPath, 0o755);
|
|
4679
|
+
process.env.TMUX = 'leader-session,stub,0';
|
|
4680
|
+
process.env.TMUX_PANE = '%1';
|
|
4681
|
+
process.env.OMX_TEAM_WORKER_CLI = 'gemini';
|
|
4682
|
+
assert.throws(() => createTeamSession('First Split Proof', 1, cwd, [], undefined, {
|
|
4683
|
+
teamPaneOwnerId: 'team:first-split-proof',
|
|
4684
|
+
}), (error) => {
|
|
4685
|
+
assert.ok(error instanceof CreateTeamSessionPartialError);
|
|
4686
|
+
assert.deepEqual(error.partialSession.workerPaneIds, ['%2']);
|
|
4687
|
+
assert.deepEqual(error.partialSession.workerPaneIdsByIndex, ['%2']);
|
|
4688
|
+
assert.deepEqual(error.partialSession.workerPanePidsByIndex, [null]);
|
|
4689
|
+
assert.ok(error.proofUnavailable.length > 0);
|
|
4690
|
+
return true;
|
|
4691
|
+
});
|
|
4692
|
+
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
4693
|
+
assert.match(tmuxLog, /split-window .* -t %1 .* -P -F #\{pane_id\}/);
|
|
4694
|
+
assert.doesNotMatch(tmuxLog, /(?:kill-pane|send-keys|resize-pane|select-pane|set-option) .* -t %2/, tmuxLog);
|
|
4695
|
+
});
|
|
4696
|
+
}
|
|
4697
|
+
finally {
|
|
4698
|
+
if (typeof previousTmux === 'string')
|
|
4699
|
+
process.env.TMUX = previousTmux;
|
|
4700
|
+
else
|
|
4701
|
+
delete process.env.TMUX;
|
|
4702
|
+
if (typeof previousTmuxPane === 'string')
|
|
4703
|
+
process.env.TMUX_PANE = previousTmuxPane;
|
|
4704
|
+
else
|
|
4705
|
+
delete process.env.TMUX_PANE;
|
|
4706
|
+
if (typeof previousWorkerCli === 'string')
|
|
4707
|
+
process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
|
|
4708
|
+
else
|
|
4709
|
+
delete process.env.OMX_TEAM_WORKER_CLI;
|
|
4710
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4711
|
+
}
|
|
4712
|
+
});
|
|
4713
|
+
}
|
|
4714
|
+
it('does not report partial session metadata when proof loss occurs before a worker pane exists', async () => {
|
|
4715
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-no-resource-proof-'));
|
|
4716
|
+
const previousTmux = process.env.TMUX;
|
|
4717
|
+
const previousTmuxPane = process.env.TMUX_PANE;
|
|
4718
|
+
const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
4719
|
+
const previousEntryPath = process.env[OMX_ENTRY_PATH_ENV];
|
|
4720
|
+
const previousArgv = process.argv;
|
|
4721
|
+
try {
|
|
4722
|
+
await withMockTmuxFixture('omx-tmux-no-resource-proof-', (logPath) => `#!/bin/sh
|
|
4723
|
+
set -eu
|
|
4724
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
4725
|
+
case "$1" in
|
|
4726
|
+
-V)
|
|
4727
|
+
echo "tmux 3.4"
|
|
4728
|
+
exit 0
|
|
4729
|
+
;;
|
|
4730
|
+
display-message)
|
|
4149
4731
|
case "$*" in
|
|
4150
|
-
*"
|
|
4151
|
-
*) echo "%
|
|
4732
|
+
*"#{window_width}"*) echo "120" ;;
|
|
4733
|
+
*) echo "leader:0 %1" ;;
|
|
4152
4734
|
esac
|
|
4153
4735
|
exit 0
|
|
4154
4736
|
;;
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4737
|
+
list-panes)
|
|
4738
|
+
case "$*" in
|
|
4739
|
+
*"-a -F #{pane_id}"*)
|
|
4740
|
+
proof_count=0
|
|
4741
|
+
if [ -f "${logPath}.proof-count" ]; then proof_count=$(cat "${logPath}.proof-count"); fi
|
|
4742
|
+
proof_count=$((proof_count + 1))
|
|
4743
|
+
printf '%s' "$proof_count" > "${logPath}.proof-count"
|
|
4744
|
+
if [ "$proof_count" -le 4 ]; then printf "%%1\t0\t2000000001\n"; else printf 'not-a-pane-snapshot\n'; fi
|
|
4745
|
+
;;
|
|
4746
|
+
*"pane_current_command"*) printf '%s\n' "%1\tnode\tcodex" ;;
|
|
4747
|
+
*) printf "%%1\n" ;;
|
|
4748
|
+
esac
|
|
4749
|
+
exit 0
|
|
4158
4750
|
;;
|
|
4159
|
-
set-option
|
|
4751
|
+
set-option)
|
|
4160
4752
|
exit 0
|
|
4161
4753
|
;;
|
|
4162
4754
|
*)
|
|
@@ -4164,299 +4756,495 @@ case "\${1:-}" in
|
|
|
4164
4756
|
;;
|
|
4165
4757
|
esac
|
|
4166
4758
|
`, async ({ logPath }) => {
|
|
4167
|
-
const
|
|
4168
|
-
|
|
4169
|
-
await
|
|
4170
|
-
await chmod(geminiPath, 0o755);
|
|
4759
|
+
const codexStubPath = join(dirname(logPath), 'codex');
|
|
4760
|
+
await writeFile(codexStubPath, '#!/bin/sh\nexit 0\n');
|
|
4761
|
+
await chmod(codexStubPath, 0o755);
|
|
4171
4762
|
process.env.TMUX = 'leader-session,stub,0';
|
|
4172
4763
|
process.env.TMUX_PANE = '%1';
|
|
4173
|
-
process.env.OMX_TEAM_WORKER_CLI = '
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4764
|
+
process.env.OMX_TEAM_WORKER_CLI = 'codex';
|
|
4765
|
+
process.env[OMX_ENTRY_PATH_ENV] = join(cwd, 'omx.js');
|
|
4766
|
+
process.argv = [previousArgv[0] || 'node', join(cwd, 'omx.js')];
|
|
4767
|
+
let caught;
|
|
4768
|
+
try {
|
|
4769
|
+
createTeamSession('No Resource Proof Loss', 1, cwd);
|
|
4770
|
+
}
|
|
4771
|
+
catch (error) {
|
|
4772
|
+
caught = error;
|
|
4773
|
+
}
|
|
4179
4774
|
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
4180
|
-
assert.
|
|
4181
|
-
assert.
|
|
4182
|
-
assert.
|
|
4775
|
+
assert.equal(caught.message, 'exact_pane_proof_unavailable:%1:malformed_snapshot', tmuxLog);
|
|
4776
|
+
assert.ok(!(caught instanceof CreateTeamSessionPartialError));
|
|
4777
|
+
assert.match(tmuxLog, /list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}/);
|
|
4778
|
+
assert.doesNotMatch(tmuxLog, /split-window|resize-pane|select-pane|send-keys/);
|
|
4183
4779
|
});
|
|
4184
4780
|
}
|
|
4185
4781
|
finally {
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
process.env.TMUX = prevTmux;
|
|
4782
|
+
if (typeof previousTmux === 'string')
|
|
4783
|
+
process.env.TMUX = previousTmux;
|
|
4189
4784
|
else
|
|
4190
4785
|
delete process.env.TMUX;
|
|
4191
|
-
if (typeof
|
|
4192
|
-
process.env.TMUX_PANE =
|
|
4786
|
+
if (typeof previousTmuxPane === 'string')
|
|
4787
|
+
process.env.TMUX_PANE = previousTmuxPane;
|
|
4193
4788
|
else
|
|
4194
4789
|
delete process.env.TMUX_PANE;
|
|
4195
|
-
if (typeof
|
|
4196
|
-
process.env.OMX_TEAM_WORKER_CLI =
|
|
4790
|
+
if (typeof previousWorkerCli === 'string')
|
|
4791
|
+
process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
|
|
4197
4792
|
else
|
|
4198
4793
|
delete process.env.OMX_TEAM_WORKER_CLI;
|
|
4794
|
+
if (typeof previousEntryPath === 'string')
|
|
4795
|
+
process.env[OMX_ENTRY_PATH_ENV] = previousEntryPath;
|
|
4796
|
+
else
|
|
4797
|
+
delete process.env[OMX_ENTRY_PATH_ENV];
|
|
4798
|
+
process.argv = previousArgv;
|
|
4199
4799
|
await rm(cwd, { recursive: true, force: true });
|
|
4200
4800
|
}
|
|
4201
4801
|
});
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
const
|
|
4206
|
-
const
|
|
4207
|
-
const prevTmuxPane = process.env.TMUX_PANE;
|
|
4208
|
-
const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
4209
|
-
const prevMsystem = process.env.MSYSTEM;
|
|
4210
|
-
const prevOstype = process.env.OSTYPE;
|
|
4211
|
-
const prevWsl = process.env.WSL_DISTRO_NAME;
|
|
4212
|
-
const prevWslInterop = process.env.WSL_INTEROP;
|
|
4213
|
-
const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
4802
|
+
it('rejects duplicate pane IDs in successful create topology before mutation', async () => {
|
|
4803
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-duplicate-topology-'));
|
|
4804
|
+
const previousTmux = process.env.TMUX;
|
|
4805
|
+
const previousTmuxPane = process.env.TMUX_PANE;
|
|
4806
|
+
const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
4214
4807
|
try {
|
|
4215
|
-
await withMockTmuxFixture('omx-tmux-
|
|
4808
|
+
await withMockTmuxFixture('omx-tmux-duplicate-topology-', (logPath) => `#!/bin/sh
|
|
4216
4809
|
set -eu
|
|
4217
4810
|
printf '%s\\n' "$*" >> "${logPath}"
|
|
4218
|
-
case "
|
|
4219
|
-
-V)
|
|
4220
|
-
|
|
4221
|
-
exit 0
|
|
4222
|
-
;;
|
|
4223
|
-
display-message)
|
|
4224
|
-
case "$*" in
|
|
4225
|
-
*"#{window_width}"*)
|
|
4226
|
-
echo "120"
|
|
4227
|
-
;;
|
|
4228
|
-
*)
|
|
4229
|
-
echo "leader:0 %1"
|
|
4230
|
-
;;
|
|
4231
|
-
esac
|
|
4232
|
-
exit 0
|
|
4233
|
-
;;
|
|
4811
|
+
case "$1" in
|
|
4812
|
+
-V) echo "tmux 3.4" ;;
|
|
4813
|
+
display-message) echo "leader:0 %1" ;;
|
|
4234
4814
|
list-panes)
|
|
4235
4815
|
case "$*" in
|
|
4236
|
-
*"pane_current_command"*)
|
|
4237
|
-
|
|
4238
|
-
;;
|
|
4239
|
-
*)
|
|
4240
|
-
printf "%%1\\n%%2\\n%%3\\n"
|
|
4241
|
-
;;
|
|
4242
|
-
esac
|
|
4243
|
-
exit 0
|
|
4244
|
-
;;
|
|
4245
|
-
split-window)
|
|
4246
|
-
case "$*" in
|
|
4247
|
-
*" -h "*)
|
|
4248
|
-
echo "%2"
|
|
4249
|
-
;;
|
|
4250
|
-
*)
|
|
4251
|
-
echo "%3"
|
|
4252
|
-
;;
|
|
4816
|
+
*"pane_current_command"*) printf '%%1\\tnode\\tcodex\\n%%1\\tnode\\tcodex\\n' ;;
|
|
4817
|
+
*) printf '%%1\\n' ;;
|
|
4253
4818
|
esac
|
|
4254
|
-
exit 0
|
|
4255
|
-
;;
|
|
4256
|
-
resize-pane|select-layout|set-window-option|select-pane|kill-pane|set-hook|run-shell)
|
|
4257
|
-
exit 0
|
|
4258
|
-
;;
|
|
4259
|
-
*)
|
|
4260
|
-
exit 0
|
|
4261
4819
|
;;
|
|
4820
|
+
*) exit 0 ;;
|
|
4262
4821
|
esac
|
|
4263
4822
|
`, async ({ logPath }) => {
|
|
4264
|
-
const
|
|
4265
|
-
const geminiPath = join(fakeBinDir, 'gemini');
|
|
4823
|
+
const geminiPath = join(dirname(logPath), 'gemini');
|
|
4266
4824
|
await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
|
|
4267
4825
|
await chmod(geminiPath, 0o755);
|
|
4268
|
-
|
|
4269
|
-
|
|
4826
|
+
process.env.TMUX = 'leader-session,stub,0';
|
|
4827
|
+
process.env.TMUX_PANE = '%1';
|
|
4270
4828
|
process.env.OMX_TEAM_WORKER_CLI = 'gemini';
|
|
4271
|
-
|
|
4272
|
-
delete process.env.OSTYPE;
|
|
4273
|
-
delete process.env.WSL_DISTRO_NAME;
|
|
4274
|
-
delete process.env.WSL_INTEROP;
|
|
4275
|
-
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
4276
|
-
const session = createTeamSession('Windows Team', 1, cwd);
|
|
4277
|
-
assert.equal(session.name, 'leader:0');
|
|
4278
|
-
assert.equal(session.leaderPaneId, '%1');
|
|
4279
|
-
assert.equal(session.hudPaneId, '%3');
|
|
4829
|
+
assert.throws(() => createTeamSession('Duplicate topology', 1, cwd), /failed to read tmux pane topology: malformed pane topology/);
|
|
4280
4830
|
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
4281
|
-
assert.
|
|
4282
|
-
assert.match(tmuxLog, /powershell\.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -EncodedCommand/);
|
|
4283
|
-
assert.doesNotMatch(tmuxLog, /\/bin\/sh -lc/);
|
|
4284
|
-
assert.match(tmuxLog, new RegExp(`resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
|
|
4831
|
+
assert.doesNotMatch(tmuxLog, /set-option|split-window|resize-pane|select-pane|send-keys/);
|
|
4285
4832
|
});
|
|
4286
4833
|
}
|
|
4287
4834
|
finally {
|
|
4288
|
-
if (
|
|
4289
|
-
|
|
4290
|
-
if (typeof prevTmux === 'string')
|
|
4291
|
-
process.env.TMUX = prevTmux;
|
|
4835
|
+
if (typeof previousTmux === 'string')
|
|
4836
|
+
process.env.TMUX = previousTmux;
|
|
4292
4837
|
else
|
|
4293
4838
|
delete process.env.TMUX;
|
|
4294
|
-
if (typeof
|
|
4295
|
-
process.env.TMUX_PANE =
|
|
4839
|
+
if (typeof previousTmuxPane === 'string')
|
|
4840
|
+
process.env.TMUX_PANE = previousTmuxPane;
|
|
4296
4841
|
else
|
|
4297
4842
|
delete process.env.TMUX_PANE;
|
|
4298
|
-
if (typeof
|
|
4299
|
-
process.env.OMX_TEAM_WORKER_CLI =
|
|
4843
|
+
if (typeof previousWorkerCli === 'string')
|
|
4844
|
+
process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
|
|
4300
4845
|
else
|
|
4301
4846
|
delete process.env.OMX_TEAM_WORKER_CLI;
|
|
4302
|
-
if (typeof prevMsystem === 'string')
|
|
4303
|
-
process.env.MSYSTEM = prevMsystem;
|
|
4304
|
-
else
|
|
4305
|
-
delete process.env.MSYSTEM;
|
|
4306
|
-
if (typeof prevOstype === 'string')
|
|
4307
|
-
process.env.OSTYPE = prevOstype;
|
|
4308
|
-
else
|
|
4309
|
-
delete process.env.OSTYPE;
|
|
4310
|
-
if (typeof prevWsl === 'string')
|
|
4311
|
-
process.env.WSL_DISTRO_NAME = prevWsl;
|
|
4312
|
-
else
|
|
4313
|
-
delete process.env.WSL_DISTRO_NAME;
|
|
4314
|
-
if (typeof prevWslInterop === 'string')
|
|
4315
|
-
process.env.WSL_INTEROP = prevWslInterop;
|
|
4316
|
-
else
|
|
4317
|
-
delete process.env.WSL_INTEROP;
|
|
4318
4847
|
await rm(cwd, { recursive: true, force: true });
|
|
4319
4848
|
}
|
|
4320
4849
|
});
|
|
4321
|
-
it('
|
|
4322
|
-
const cwd = await mkdtemp(join(tmpdir(), 'omx-
|
|
4323
|
-
const prevTmux = process.env.TMUX;
|
|
4324
|
-
const prevTmuxPane = process.env.TMUX_PANE;
|
|
4325
|
-
const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
4326
|
-
const prevMsystem = process.env.MSYSTEM;
|
|
4327
|
-
const prevOstype = process.env.OSTYPE;
|
|
4328
|
-
const prevWsl = process.env.WSL_DISTRO_NAME;
|
|
4329
|
-
const prevWslInterop = process.env.WSL_INTEROP;
|
|
4330
|
-
const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
4850
|
+
it('guards duplicate standalone HUD removal with a fresh global pane proof', async () => {
|
|
4851
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-duplicate-hud-global-proof-'));
|
|
4331
4852
|
try {
|
|
4332
|
-
await withMockTmuxFixture('omx-tmux-
|
|
4853
|
+
await withMockTmuxFixture('omx-tmux-duplicate-hud-global-proof-', (logPath) => {
|
|
4854
|
+
const proofStatePath = `${logPath}.proof-state`;
|
|
4855
|
+
return `#!/bin/sh
|
|
4333
4856
|
set -eu
|
|
4334
4857
|
printf '%s\\n' "$*" >> "${logPath}"
|
|
4335
|
-
case "
|
|
4336
|
-
-
|
|
4337
|
-
echo "tmux 3.4"
|
|
4338
|
-
exit 0
|
|
4339
|
-
;;
|
|
4340
|
-
display-message)
|
|
4858
|
+
case "$1" in
|
|
4859
|
+
list-panes)
|
|
4341
4860
|
case "$*" in
|
|
4342
|
-
*"#{
|
|
4343
|
-
|
|
4861
|
+
*"-a -F #{pane_id}"*)
|
|
4862
|
+
proof_count=0
|
|
4863
|
+
if [ -f "${proofStatePath}" ]; then proof_count=$(cat "${proofStatePath}"); fi
|
|
4864
|
+
proof_count=$((proof_count + 1))
|
|
4865
|
+
printf '%s' "$proof_count" > "${proofStatePath}"
|
|
4866
|
+
if [ "$proof_count" -eq 15 ]; then
|
|
4867
|
+
printf 'not-a-pane-snapshot\n'
|
|
4868
|
+
else
|
|
4869
|
+
printf "%%11\t0\t2000000011\n%%44\t0\t2000000044\n"
|
|
4870
|
+
if [ ! -f "${logPath}.killed-%45" ]; then printf "%%45\t0\t2000000045\n"; fi
|
|
4871
|
+
fi
|
|
4344
4872
|
;;
|
|
4345
4873
|
*)
|
|
4346
|
-
|
|
4874
|
+
printf "%%11\\tzsh\\tzsh\\n"
|
|
4875
|
+
printf "%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%%11' /node /omx.js hud --watch\\n"
|
|
4876
|
+
printf "%%45\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%%11' /node /omx.js hud --watch\\n"
|
|
4347
4877
|
;;
|
|
4348
4878
|
esac
|
|
4349
4879
|
exit 0
|
|
4350
4880
|
;;
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
*"pane_current_command"*)
|
|
4354
|
-
printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n%%3\\tnode\\t'node omx hud --watch'\\n"
|
|
4355
|
-
;;
|
|
4356
|
-
*)
|
|
4357
|
-
printf "%%1\\n%%2\\n%%3\\n"
|
|
4358
|
-
;;
|
|
4359
|
-
esac
|
|
4881
|
+
kill-pane)
|
|
4882
|
+
: > "${logPath}.killed-$3"
|
|
4360
4883
|
exit 0
|
|
4361
4884
|
;;
|
|
4362
|
-
|
|
4363
|
-
case "$*" in
|
|
4364
|
-
*" -h "*)
|
|
4365
|
-
echo "%2"
|
|
4366
|
-
;;
|
|
4367
|
-
*)
|
|
4368
|
-
echo "%3"
|
|
4369
|
-
;;
|
|
4370
|
-
esac
|
|
4885
|
+
run-shell|select-pane|resize-pane)
|
|
4371
4886
|
exit 0
|
|
4372
4887
|
;;
|
|
4373
|
-
|
|
4888
|
+
*)
|
|
4374
4889
|
exit 0
|
|
4375
4890
|
;;
|
|
4376
|
-
|
|
4891
|
+
esac
|
|
4892
|
+
`;
|
|
4893
|
+
}, async ({ logPath }) => {
|
|
4894
|
+
assert.equal(restoreStandaloneHudPane('%11', cwd), '%44');
|
|
4895
|
+
assert.throws(() => restoreStandaloneHudPane('%11', cwd), /exact_pane_proof_unavailable:%45:malformed_snapshot/);
|
|
4896
|
+
const commands = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
|
|
4897
|
+
const duplicateHudKill = commands.indexOf('kill-pane -t %45');
|
|
4898
|
+
assert.ok(duplicateHudKill > 0);
|
|
4899
|
+
assert.match(commands[duplicateHudKill - 1] ?? '', /^list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}$/);
|
|
4900
|
+
assert.equal(commands.filter((command) => command === 'kill-pane -t %45').length, 1);
|
|
4901
|
+
});
|
|
4902
|
+
}
|
|
4903
|
+
finally {
|
|
4904
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4905
|
+
}
|
|
4906
|
+
});
|
|
4907
|
+
it('rejects a replaced leader PID or owner before standalone HUD topology discovery', async () => {
|
|
4908
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-hud-entry-authorization-'));
|
|
4909
|
+
try {
|
|
4910
|
+
await withMockTmuxFixture('omx-tmux-standalone-hud-entry-authorization-', (logPath) => `#!/bin/sh
|
|
4911
|
+
set -eu
|
|
4912
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
4913
|
+
case "$1" in
|
|
4914
|
+
list-panes)
|
|
4915
|
+
if [ "$2" = "-a" ]; then
|
|
4916
|
+
proof_count=0
|
|
4917
|
+
[ -f "${logPath}.proof-count" ] && proof_count=$(cat "${logPath}.proof-count")
|
|
4918
|
+
proof_count=$((proof_count + 1))
|
|
4919
|
+
printf '%s' "$proof_count" > "${logPath}.proof-count"
|
|
4920
|
+
if [ "$proof_count" -eq 1 ]; then printf '%%11\\t0\\t2000000011\\n'; else printf '%%11\\t0\\t2000000099\\n'; fi
|
|
4921
|
+
else
|
|
4922
|
+
printf '%%11\\tzsh\\tzsh\\n%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE=%%11 node /omx.js hud --watch\\n'
|
|
4923
|
+
fi
|
|
4924
|
+
;;
|
|
4925
|
+
split-window) printf '%%45\\n' ;;
|
|
4926
|
+
*) exit 0 ;;
|
|
4927
|
+
esac
|
|
4928
|
+
`, async ({ logPath }) => {
|
|
4929
|
+
let ownerAuthorizationChecks = 0;
|
|
4930
|
+
assert.throws(() => restoreStandaloneHudPane('%11', cwd, {
|
|
4931
|
+
expectedLeaderPanePid: 2000000011,
|
|
4932
|
+
assertLeaderPaneAuthorization: () => {
|
|
4933
|
+
ownerAuthorizationChecks += 1;
|
|
4934
|
+
throw new Error('leader owner changed');
|
|
4935
|
+
},
|
|
4936
|
+
}), /leader owner changed/);
|
|
4937
|
+
assert.equal(ownerAuthorizationChecks, 1);
|
|
4938
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
4939
|
+
assert.doesNotMatch(commands, /list-panes -t %11|kill-pane|split-window|resize-pane|select-pane/);
|
|
4940
|
+
});
|
|
4941
|
+
}
|
|
4942
|
+
finally {
|
|
4943
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4944
|
+
}
|
|
4945
|
+
});
|
|
4946
|
+
it('fails closed before standalone HUD effects when leader topology query fails', async () => {
|
|
4947
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-hud-topology-query-failure-'));
|
|
4948
|
+
try {
|
|
4949
|
+
await withMockTmuxFixture('omx-tmux-standalone-hud-topology-query-failure-', (logPath) => `#!/bin/sh
|
|
4950
|
+
set -eu
|
|
4951
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
4952
|
+
case "$1" in
|
|
4953
|
+
list-panes)
|
|
4954
|
+
if [ "$2" = "-a" ]; then
|
|
4955
|
+
printf '%%11\\t0\\t2000000011\\n'
|
|
4956
|
+
exit 0
|
|
4957
|
+
fi
|
|
4958
|
+
printf 'leader topology unavailable\\n' >&2
|
|
4959
|
+
exit 1
|
|
4960
|
+
;;
|
|
4961
|
+
kill-pane|split-window|resize-pane|select-pane)
|
|
4962
|
+
exit 99
|
|
4963
|
+
;;
|
|
4964
|
+
*) exit 0 ;;
|
|
4965
|
+
esac
|
|
4966
|
+
`, async ({ logPath }) => {
|
|
4967
|
+
assert.throws(() => restoreStandaloneHudPane('%11', cwd), /failed to read tmux pane topology: leader topology unavailable/);
|
|
4968
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
4969
|
+
assert.doesNotMatch(commands, /kill-pane|split-window|resize-pane|select-pane/);
|
|
4970
|
+
});
|
|
4971
|
+
}
|
|
4972
|
+
finally {
|
|
4973
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4974
|
+
}
|
|
4975
|
+
});
|
|
4976
|
+
it('fails closed before standalone HUD effects when leader topology is malformed', async () => {
|
|
4977
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-hud-topology-malformed-'));
|
|
4978
|
+
try {
|
|
4979
|
+
await withMockTmuxFixture('omx-tmux-standalone-hud-topology-malformed-', (logPath) => `#!/bin/sh
|
|
4980
|
+
set -eu
|
|
4981
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
4982
|
+
case "$1" in
|
|
4983
|
+
list-panes)
|
|
4984
|
+
if [ "$2" = "-a" ]; then
|
|
4985
|
+
printf '%%11\\t0\\t2000000011\\n'
|
|
4986
|
+
else
|
|
4987
|
+
printf '%%11\\tzsh\\n'
|
|
4988
|
+
fi
|
|
4377
4989
|
exit 0
|
|
4378
4990
|
;;
|
|
4379
|
-
|
|
4991
|
+
kill-pane|split-window|resize-pane|select-pane)
|
|
4992
|
+
exit 99
|
|
4993
|
+
;;
|
|
4994
|
+
*) exit 0 ;;
|
|
4995
|
+
esac
|
|
4996
|
+
`, async ({ logPath }) => {
|
|
4997
|
+
assert.throws(() => restoreStandaloneHudPane('%11', cwd), /failed to read tmux pane topology: malformed pane topology/);
|
|
4998
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
4999
|
+
assert.doesNotMatch(commands, /kill-pane|split-window|resize-pane|select-pane/);
|
|
5000
|
+
});
|
|
5001
|
+
}
|
|
5002
|
+
finally {
|
|
5003
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5004
|
+
}
|
|
5005
|
+
});
|
|
5006
|
+
it('fails closed before a standalone HUD split when the adjacent leader proof is unavailable', async () => {
|
|
5007
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-hud-proof-loss-'));
|
|
5008
|
+
try {
|
|
5009
|
+
await withMockTmuxFixture('omx-tmux-standalone-hud-proof-loss-', (logPath) => `#!/bin/sh
|
|
5010
|
+
set -eu
|
|
5011
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
5012
|
+
case "$1" in
|
|
5013
|
+
list-panes)
|
|
5014
|
+
if [ "$2" = "-a" ]; then
|
|
5015
|
+
count=0
|
|
5016
|
+
if [ -f "${logPath}.proof-count" ]; then count=$(cat "${logPath}.proof-count"); fi
|
|
5017
|
+
count=$((count + 1))
|
|
5018
|
+
printf '%s' "$count" > "${logPath}.proof-count"
|
|
5019
|
+
if [ "$count" -eq 1 ]; then printf '%%11\\t0\\t2000000011\\n'; else printf 'not-a-pane-snapshot\\n'; fi
|
|
5020
|
+
fi
|
|
4380
5021
|
exit 0
|
|
4381
5022
|
;;
|
|
5023
|
+
split-window) echo '%44' ;;
|
|
5024
|
+
*) exit 0 ;;
|
|
4382
5025
|
esac
|
|
4383
5026
|
`, async ({ logPath }) => {
|
|
4384
|
-
|
|
4385
|
-
const
|
|
5027
|
+
assert.throws(() => restoreStandaloneHudPane('%11', cwd), /exact_pane_proof_unavailable:%11:malformed_snapshot/);
|
|
5028
|
+
const commands = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
|
|
5029
|
+
assert.doesNotMatch(commands.join('\n'), /split-window|resize-pane|select-pane/);
|
|
5030
|
+
});
|
|
5031
|
+
}
|
|
5032
|
+
finally {
|
|
5033
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5034
|
+
}
|
|
5035
|
+
});
|
|
5036
|
+
it('does not adopt a leader PID replacement before standalone HUD topology discovery', async () => {
|
|
5037
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-hud-leader-pid-reuse-'));
|
|
5038
|
+
try {
|
|
5039
|
+
await withMockTmuxFixture('omx-tmux-standalone-hud-leader-pid-reuse-', (logPath) => `#!/bin/sh
|
|
5040
|
+
set -eu
|
|
5041
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
5042
|
+
case "$1" in
|
|
5043
|
+
list-panes)
|
|
5044
|
+
if [ "$2" = "-a" ]; then
|
|
5045
|
+
case "$*" in
|
|
5046
|
+
*"#{pane_pid}"*)
|
|
5047
|
+
count=0
|
|
5048
|
+
[ -f "${logPath}.proof-count" ] && count=$(cat "${logPath}.proof-count")
|
|
5049
|
+
count=$((count + 1))
|
|
5050
|
+
printf '%s' "$count" > "${logPath}.proof-count"
|
|
5051
|
+
if [ "$count" -eq 1 ]; then printf '%%11\\t0\\t2000000011\\n'; else printf '%%11\\t0\\t2000000099\\n'; fi
|
|
5052
|
+
;;
|
|
5053
|
+
*) printf '%%11\\tzsh\\tzsh\\n' ;;
|
|
5054
|
+
esac
|
|
5055
|
+
fi
|
|
5056
|
+
exit 0
|
|
5057
|
+
;;
|
|
5058
|
+
split-window) printf '%%44\\n' ;;
|
|
5059
|
+
*) exit 0 ;;
|
|
5060
|
+
esac
|
|
5061
|
+
`, async ({ logPath }) => {
|
|
5062
|
+
assert.throws(() => restoreStandaloneHudPane('%11', cwd, { expectedLeaderPanePid: 2000000011 }), /tmux pane identity changed: %11/);
|
|
5063
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
5064
|
+
assert.doesNotMatch(commands, /list-panes -t %11|display-message|kill-pane|split-window|resize-pane|select-pane/);
|
|
5065
|
+
});
|
|
5066
|
+
}
|
|
5067
|
+
finally {
|
|
5068
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5069
|
+
}
|
|
5070
|
+
});
|
|
5071
|
+
it('does not resize a PID-reused existing standalone HUD on POSIX', async () => {
|
|
5072
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-existing-hud-posix-pid-reuse-'));
|
|
5073
|
+
try {
|
|
5074
|
+
await withMockTmuxFixture('omx-tmux-standalone-existing-hud-posix-pid-reuse-', (logPath) => `#!/bin/sh
|
|
5075
|
+
set -eu
|
|
5076
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
5077
|
+
case "$1" in
|
|
5078
|
+
list-panes)
|
|
5079
|
+
if [ "$2" = "-a" ]; then
|
|
5080
|
+
count=0
|
|
5081
|
+
if [ -f "${logPath}.proof-count" ]; then count=$(cat "${logPath}.proof-count"); fi
|
|
5082
|
+
count=$((count + 1))
|
|
5083
|
+
printf '%s' "$count" > "${logPath}.proof-count"
|
|
5084
|
+
if [ "$count" -ge 4 ]; then
|
|
5085
|
+
printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000099\\n'
|
|
5086
|
+
else
|
|
5087
|
+
printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000044\\n'
|
|
5088
|
+
fi
|
|
5089
|
+
else
|
|
5090
|
+
printf '%%11\\tzsh\\tzsh\\n%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE=%%11 node /omx.js hud --watch\\n'
|
|
5091
|
+
fi
|
|
5092
|
+
;;
|
|
5093
|
+
run-shell) /bin/sh -c "\${3:-$2}" ;;
|
|
5094
|
+
resize-pane|select-pane) exit 0 ;;
|
|
5095
|
+
*) exit 0 ;;
|
|
5096
|
+
esac
|
|
5097
|
+
`, async ({ logPath }) => {
|
|
5098
|
+
assert.equal(restoreStandaloneHudPane('%11', cwd), '%44');
|
|
5099
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
5100
|
+
assert.doesNotMatch(commands, /^resize-pane/m);
|
|
5101
|
+
assert.match(commands, /\$3 == "2000000044"/);
|
|
5102
|
+
});
|
|
5103
|
+
}
|
|
5104
|
+
finally {
|
|
5105
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5106
|
+
}
|
|
5107
|
+
});
|
|
5108
|
+
it('does not resize a PID-reused newly created standalone HUD on POSIX', async () => {
|
|
5109
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-new-hud-posix-pid-reuse-'));
|
|
5110
|
+
try {
|
|
5111
|
+
await withMockTmuxFixture('omx-tmux-standalone-new-hud-posix-pid-reuse-', (logPath) => `#!/bin/sh
|
|
5112
|
+
set -eu
|
|
5113
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
5114
|
+
case "$1" in
|
|
5115
|
+
list-panes)
|
|
5116
|
+
if [ "$2" = "-a" ]; then
|
|
5117
|
+
count=0
|
|
5118
|
+
if [ -f "${logPath}.proof-count" ]; then count=$(cat "${logPath}.proof-count"); fi
|
|
5119
|
+
count=$((count + 1))
|
|
5120
|
+
printf '%s' "$count" > "${logPath}.proof-count"
|
|
5121
|
+
if [ "$count" -ge 6 ]; then
|
|
5122
|
+
printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000099\\n'
|
|
5123
|
+
else
|
|
5124
|
+
printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000044\\n'
|
|
5125
|
+
fi
|
|
5126
|
+
else
|
|
5127
|
+
printf '%%11\\tzsh\\tzsh\\n'
|
|
5128
|
+
fi
|
|
5129
|
+
;;
|
|
5130
|
+
split-window) printf '%%44\\n' ;;
|
|
5131
|
+
run-shell) /bin/sh -c "\${3:-$2}" ;;
|
|
5132
|
+
resize-pane|select-pane) exit 0 ;;
|
|
5133
|
+
*) exit 0 ;;
|
|
5134
|
+
esac
|
|
5135
|
+
`, async ({ logPath }) => {
|
|
5136
|
+
assert.equal(restoreStandaloneHudPane('%11', cwd), '%44');
|
|
5137
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
5138
|
+
assert.doesNotMatch(commands, /^resize-pane/m);
|
|
5139
|
+
assert.match(commands, /\$3 == "2000000044"/);
|
|
5140
|
+
});
|
|
5141
|
+
}
|
|
5142
|
+
finally {
|
|
5143
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5144
|
+
}
|
|
5145
|
+
});
|
|
5146
|
+
it('rejects standalone HUD PID reuse before native resize', async () => {
|
|
5147
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-hud-pid-reuse-'));
|
|
5148
|
+
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
5149
|
+
try {
|
|
5150
|
+
await withMockTmuxFixture('omx-tmux-standalone-hud-pid-reuse-', (logPath) => `#!/bin/sh
|
|
5151
|
+
set -eu
|
|
5152
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
5153
|
+
case "$1" in
|
|
5154
|
+
list-panes)
|
|
5155
|
+
if [ "$2" = "-a" ]; then
|
|
5156
|
+
count=0
|
|
5157
|
+
if [ -f "${logPath}.proof-count" ]; then count=$(cat "${logPath}.proof-count"); fi
|
|
5158
|
+
count=$((count + 1))
|
|
5159
|
+
printf '%s' "$count" > "${logPath}.proof-count"
|
|
5160
|
+
if [ "$count" -ge 4 ]; then
|
|
5161
|
+
printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000099\\n'
|
|
5162
|
+
else
|
|
5163
|
+
printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000044\\n'
|
|
5164
|
+
fi
|
|
5165
|
+
else
|
|
5166
|
+
printf '%%11\\tzsh\\tzsh\\n%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE=%%11 node /omx.js hud --watch\\n'
|
|
5167
|
+
fi
|
|
5168
|
+
exit 0
|
|
5169
|
+
;;
|
|
5170
|
+
resize-pane|select-pane) exit 0 ;;
|
|
5171
|
+
*) exit 0 ;;
|
|
5172
|
+
esac
|
|
5173
|
+
`, async ({ logPath }) => {
|
|
5174
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
5175
|
+
assert.throws(() => restoreStandaloneHudPane('%11', cwd), /tmux pane identity changed: %44/);
|
|
5176
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
5177
|
+
assert.doesNotMatch(commands, /resize-pane|select-pane/);
|
|
5178
|
+
});
|
|
5179
|
+
}
|
|
5180
|
+
finally {
|
|
5181
|
+
if (originalPlatform)
|
|
5182
|
+
Object.defineProperty(process, 'platform', originalPlatform);
|
|
5183
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5184
|
+
}
|
|
5185
|
+
});
|
|
5186
|
+
it('reports failed rollback pane teardown as retryable partial-session cleanup debt', async () => {
|
|
5187
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-kill-failure-partial-'));
|
|
5188
|
+
const previousTmux = process.env.TMUX;
|
|
5189
|
+
const previousTmuxPane = process.env.TMUX_PANE;
|
|
5190
|
+
const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
5191
|
+
try {
|
|
5192
|
+
await withMockTmuxFixture('omx-tmux-kill-failure-partial-', (logPath) => `#!/bin/sh
|
|
5193
|
+
set -eu
|
|
5194
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
5195
|
+
case "$1" in
|
|
5196
|
+
display-message) echo 'leader:0 %1' ;;
|
|
5197
|
+
list-panes)
|
|
5198
|
+
if [ "$2" = "-a" ]; then printf '%%1\\t0\\t2000000001\\n%%2\\t0\\t2000000002\\n'; else printf '%%1\\tnode\\tcodex\\n'; fi
|
|
5199
|
+
;;
|
|
5200
|
+
split-window)
|
|
5201
|
+
case "$*" in *' -h '*) echo '%2' ;; *) echo 'second split rejected' >&2; exit 1 ;; esac
|
|
5202
|
+
;;
|
|
5203
|
+
kill-pane) echo 'kill rejected' >&2; exit 1 ;;
|
|
5204
|
+
*) exit 0 ;;
|
|
5205
|
+
esac
|
|
5206
|
+
`, async ({ logPath }) => {
|
|
5207
|
+
const geminiPath = join(dirname(logPath), 'gemini');
|
|
4386
5208
|
await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
|
|
4387
5209
|
await chmod(geminiPath, 0o755);
|
|
4388
5210
|
process.env.TMUX = 'leader-session,stub,0';
|
|
4389
5211
|
process.env.TMUX_PANE = '%1';
|
|
4390
5212
|
process.env.OMX_TEAM_WORKER_CLI = 'gemini';
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
delete process.env.WSL_INTEROP;
|
|
4395
|
-
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
4396
|
-
const session = createTeamSession('Windows Team', 1, cwd);
|
|
4397
|
-
assert.equal(session.hudPaneId, '%3');
|
|
4398
|
-
assert.equal(session.resizeHookName, null);
|
|
4399
|
-
assert.equal(session.resizeHookTarget, null);
|
|
4400
|
-
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
4401
|
-
assert.match(tmuxLog, new RegExp(`resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
|
|
4402
|
-
assert.doesNotMatch(tmuxLog, /set-hook -w /);
|
|
4403
|
-
assert.doesNotMatch(tmuxLog, /window-resized\[/);
|
|
4404
|
-
assert.doesNotMatch(tmuxLog, /set-hook -t leader:0 client-attached\[\d+\]/);
|
|
4405
|
-
assert.doesNotMatch(tmuxLog, /run-shell -b sleep \d+; tmux resize-pane -t %3 -y \d+ >/);
|
|
4406
|
-
assert.doesNotMatch(tmuxLog, /run-shell tmux resize-pane -t %3 -y \d+ >/);
|
|
5213
|
+
assert.throws(() => createTeamSession('Kill Failure Partial', 2, cwd), (error) => error instanceof CreateTeamSessionPartialError
|
|
5214
|
+
&& error.partialSession.workerPaneIds.includes('%2')
|
|
5215
|
+
&& error.cleanupErrors.some((message) => message.includes('failed to kill tmux pane %2')));
|
|
4407
5216
|
});
|
|
4408
5217
|
}
|
|
4409
5218
|
finally {
|
|
4410
|
-
if (
|
|
4411
|
-
|
|
4412
|
-
if (typeof prevTmux === 'string')
|
|
4413
|
-
process.env.TMUX = prevTmux;
|
|
5219
|
+
if (typeof previousTmux === 'string')
|
|
5220
|
+
process.env.TMUX = previousTmux;
|
|
4414
5221
|
else
|
|
4415
5222
|
delete process.env.TMUX;
|
|
4416
|
-
if (typeof
|
|
4417
|
-
process.env.TMUX_PANE =
|
|
5223
|
+
if (typeof previousTmuxPane === 'string')
|
|
5224
|
+
process.env.TMUX_PANE = previousTmuxPane;
|
|
4418
5225
|
else
|
|
4419
5226
|
delete process.env.TMUX_PANE;
|
|
4420
|
-
if (typeof
|
|
4421
|
-
process.env.OMX_TEAM_WORKER_CLI =
|
|
5227
|
+
if (typeof previousWorkerCli === 'string')
|
|
5228
|
+
process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
|
|
4422
5229
|
else
|
|
4423
5230
|
delete process.env.OMX_TEAM_WORKER_CLI;
|
|
4424
|
-
if (typeof prevMsystem === 'string')
|
|
4425
|
-
process.env.MSYSTEM = prevMsystem;
|
|
4426
|
-
else
|
|
4427
|
-
delete process.env.MSYSTEM;
|
|
4428
|
-
if (typeof prevOstype === 'string')
|
|
4429
|
-
process.env.OSTYPE = prevOstype;
|
|
4430
|
-
else
|
|
4431
|
-
delete process.env.OSTYPE;
|
|
4432
|
-
if (typeof prevWsl === 'string')
|
|
4433
|
-
process.env.WSL_DISTRO_NAME = prevWsl;
|
|
4434
|
-
else
|
|
4435
|
-
delete process.env.WSL_DISTRO_NAME;
|
|
4436
|
-
if (typeof prevWslInterop === 'string')
|
|
4437
|
-
process.env.WSL_INTEROP = prevWslInterop;
|
|
4438
|
-
else
|
|
4439
|
-
delete process.env.WSL_INTEROP;
|
|
4440
5231
|
await rm(cwd, { recursive: true, force: true });
|
|
4441
5232
|
}
|
|
4442
5233
|
});
|
|
4443
|
-
it('
|
|
4444
|
-
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-
|
|
5234
|
+
it('uses tmux 3.2a-compatible client-resized hook registration for team HUD resize', async () => {
|
|
5235
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-resize-hook-fallback-'));
|
|
4445
5236
|
const prevTmux = process.env.TMUX;
|
|
4446
5237
|
const prevTmuxPane = process.env.TMUX_PANE;
|
|
4447
5238
|
const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
4448
|
-
const
|
|
4449
|
-
const
|
|
4450
|
-
const prevWsl = process.env.WSL_DISTRO_NAME;
|
|
4451
|
-
const prevWslInterop = process.env.WSL_INTEROP;
|
|
4452
|
-
const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
5239
|
+
const prevWarn = console.warn;
|
|
5240
|
+
const warnings = [];
|
|
4453
5241
|
try {
|
|
4454
|
-
await withMockTmuxFixture('omx-tmux-
|
|
5242
|
+
await withMockTmuxFixture('omx-tmux-resize-hook-fallback-', (logPath) => `#!/bin/sh
|
|
4455
5243
|
set -eu
|
|
4456
|
-
printf '%s
|
|
5244
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
4457
5245
|
case "\${1:-}" in
|
|
4458
5246
|
-V)
|
|
4459
|
-
echo "tmux 3.
|
|
5247
|
+
echo "tmux 3.2a"
|
|
4460
5248
|
exit 0
|
|
4461
5249
|
;;
|
|
4462
5250
|
display-message)
|
|
@@ -4472,16 +5260,20 @@ case "\${1:-}" in
|
|
|
4472
5260
|
;;
|
|
4473
5261
|
list-panes)
|
|
4474
5262
|
case "$*" in
|
|
5263
|
+
*"-a -F #{pane_id}"*)
|
|
5264
|
+
printf "%%1\t0\t2000000001\n%%2\t0\t2000000002\n%%3\t0\t2000000003\n"
|
|
5265
|
+
;;
|
|
4475
5266
|
*"pane_current_command"*)
|
|
4476
|
-
printf "%%1\\tnode\\t'codex'\\n"
|
|
5267
|
+
if [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
|
|
4477
5268
|
;;
|
|
4478
5269
|
*)
|
|
4479
|
-
printf "%%1\\n"
|
|
5270
|
+
if [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi
|
|
4480
5271
|
;;
|
|
4481
5272
|
esac
|
|
4482
5273
|
exit 0
|
|
4483
5274
|
;;
|
|
4484
5275
|
split-window)
|
|
5276
|
+
: > "${logPath}.worker"
|
|
4485
5277
|
case "$*" in
|
|
4486
5278
|
*" -h "*)
|
|
4487
5279
|
echo "%2"
|
|
@@ -4492,9 +5284,24 @@ case "\${1:-}" in
|
|
|
4492
5284
|
esac
|
|
4493
5285
|
exit 0
|
|
4494
5286
|
;;
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
5287
|
+
set-hook)
|
|
5288
|
+
case "$*" in
|
|
5289
|
+
*"window-resized["*)
|
|
5290
|
+
echo "invalid option: window-resized[]" >&2
|
|
5291
|
+
exit 1
|
|
5292
|
+
;;
|
|
5293
|
+
*" -w "*)
|
|
5294
|
+
echo "invalid option: -w" >&2
|
|
5295
|
+
exit 1
|
|
5296
|
+
;;
|
|
5297
|
+
*)
|
|
5298
|
+
exit 0
|
|
5299
|
+
;;
|
|
5300
|
+
esac
|
|
5301
|
+
;;
|
|
5302
|
+
set-option|resize-pane|select-layout|set-window-option|select-pane|run-shell)
|
|
5303
|
+
exit 0
|
|
5304
|
+
;;
|
|
4498
5305
|
*)
|
|
4499
5306
|
exit 0
|
|
4500
5307
|
;;
|
|
@@ -4502,28 +5309,30 @@ esac
|
|
|
4502
5309
|
`, async ({ logPath }) => {
|
|
4503
5310
|
const fakeBinDir = join(logPath, '..');
|
|
4504
5311
|
const geminiPath = join(fakeBinDir, 'gemini');
|
|
4505
|
-
const powershellExePath = join(fakeBinDir, 'powershell.exe');
|
|
4506
5312
|
await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
|
|
4507
5313
|
await chmod(geminiPath, 0o755);
|
|
4508
|
-
await writeFile(powershellExePath, '');
|
|
4509
5314
|
process.env.TMUX = 'leader-session,stub,0';
|
|
4510
5315
|
process.env.TMUX_PANE = '%1';
|
|
4511
5316
|
process.env.OMX_TEAM_WORKER_CLI = 'gemini';
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
assert.
|
|
5317
|
+
console.warn = (...args) => { warnings.push(args.map(String).join(' ')); };
|
|
5318
|
+
const session = createTeamSession('Resize Hook Fallback', 1, cwd);
|
|
5319
|
+
assert.equal(session.hudPaneId, '%3');
|
|
5320
|
+
assert.ok(session.resizeHookName);
|
|
5321
|
+
assert.equal(session.resizeHookTarget, 'leader:0');
|
|
5322
|
+
assert.equal(warnings.join('\n'), '');
|
|
4518
5323
|
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
4519
|
-
|
|
4520
|
-
assert.
|
|
4521
|
-
assert.
|
|
5324
|
+
assert.match(tmuxLog, /set-hook -t leader:0 client-resized\[\d+\]/);
|
|
5325
|
+
assert.doesNotMatch(tmuxLog, /window-resized\[/);
|
|
5326
|
+
assert.doesNotMatch(tmuxLog, /set-hook -w /);
|
|
5327
|
+
assert.match(tmuxLog, /set-hook -t leader:0 client-attached\[\d+\]/);
|
|
5328
|
+
assert.match(tmuxLog, new RegExp(`run-shell -b sleep ${HUD_RESIZE_RECONCILE_DELAY_SECONDS}; .*resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
|
|
5329
|
+
assert.match(tmuxLog, new RegExp(`run-shell .*resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
|
|
5330
|
+
assert.doesNotMatch(tmuxLog, /kill-pane -t %2/);
|
|
5331
|
+
assert.doesNotMatch(tmuxLog, /kill-pane -t %3/);
|
|
4522
5332
|
});
|
|
4523
5333
|
}
|
|
4524
5334
|
finally {
|
|
4525
|
-
|
|
4526
|
-
Object.defineProperty(process, 'platform', origPlatform);
|
|
5335
|
+
console.warn = prevWarn;
|
|
4527
5336
|
if (typeof prevTmux === 'string')
|
|
4528
5337
|
process.env.TMUX = prevTmux;
|
|
4529
5338
|
else
|
|
@@ -4536,162 +5345,257 @@ esac
|
|
|
4536
5345
|
process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
|
|
4537
5346
|
else
|
|
4538
5347
|
delete process.env.OMX_TEAM_WORKER_CLI;
|
|
4539
|
-
if (typeof prevMsystem === 'string')
|
|
4540
|
-
process.env.MSYSTEM = prevMsystem;
|
|
4541
|
-
else
|
|
4542
|
-
delete process.env.MSYSTEM;
|
|
4543
|
-
if (typeof prevOstype === 'string')
|
|
4544
|
-
process.env.OSTYPE = prevOstype;
|
|
4545
|
-
else
|
|
4546
|
-
delete process.env.OSTYPE;
|
|
4547
|
-
if (typeof prevWsl === 'string')
|
|
4548
|
-
process.env.WSL_DISTRO_NAME = prevWsl;
|
|
4549
|
-
else
|
|
4550
|
-
delete process.env.WSL_DISTRO_NAME;
|
|
4551
|
-
if (typeof prevWslInterop === 'string')
|
|
4552
|
-
process.env.WSL_INTEROP = prevWslInterop;
|
|
4553
|
-
else
|
|
4554
|
-
delete process.env.WSL_INTEROP;
|
|
4555
5348
|
await rm(cwd, { recursive: true, force: true });
|
|
4556
5349
|
}
|
|
4557
5350
|
});
|
|
4558
|
-
it('
|
|
4559
|
-
const cwd = await mkdtemp(join(tmpdir(), 'omx-
|
|
4560
|
-
const
|
|
4561
|
-
const
|
|
4562
|
-
const
|
|
4563
|
-
const
|
|
4564
|
-
const prevWslInterop = process.env.WSL_INTEROP;
|
|
4565
|
-
const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
5351
|
+
it('unregisters only the successfully registered HUD hook after partial hook registration failure', async () => {
|
|
5352
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-partial-hook-registration-'));
|
|
5353
|
+
const previousTmux = process.env.TMUX;
|
|
5354
|
+
const previousTmuxPane = process.env.TMUX_PANE;
|
|
5355
|
+
const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
5356
|
+
const previousWarn = console.warn;
|
|
4566
5357
|
try {
|
|
4567
|
-
await withMockTmuxFixture('omx-tmux-
|
|
5358
|
+
await withMockTmuxFixture('omx-tmux-partial-hook-registration-', (logPath) => `#!/bin/sh
|
|
4568
5359
|
set -eu
|
|
5360
|
+
registration_failed_file="${logPath}.registration-failed"
|
|
4569
5361
|
printf '%s\\n' "$*" >> "${logPath}"
|
|
4570
5362
|
case "\${1:-}" in
|
|
4571
|
-
|
|
4572
|
-
echo
|
|
4573
|
-
exit 0
|
|
5363
|
+
-V)
|
|
5364
|
+
echo 'tmux 3.4'
|
|
4574
5365
|
;;
|
|
4575
|
-
|
|
4576
|
-
|
|
5366
|
+
display-message)
|
|
5367
|
+
case "$*" in
|
|
5368
|
+
*'#{window_width}'*) echo '120' ;;
|
|
5369
|
+
*) echo 'leader:0 %1' ;;
|
|
5370
|
+
esac
|
|
4577
5371
|
;;
|
|
4578
|
-
|
|
4579
|
-
|
|
5372
|
+
list-panes)
|
|
5373
|
+
case "$*" in
|
|
5374
|
+
*'pane_current_command'*)
|
|
5375
|
+
if [ -f "${logPath}.hud" ]; then
|
|
5376
|
+
printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n%%3\\tnode\\t'hud --watch'\\n"
|
|
5377
|
+
elif [ -f "${logPath}.worker" ]; then
|
|
5378
|
+
printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"
|
|
5379
|
+
else
|
|
5380
|
+
printf "%%1\\tnode\\t'codex'\\n"
|
|
5381
|
+
fi
|
|
5382
|
+
;;
|
|
5383
|
+
*'-a -F #{pane_id}'*)
|
|
5384
|
+
if [ -f "$registration_failed_file" ]; then
|
|
5385
|
+
printf 'malformed snapshot\n'
|
|
5386
|
+
else
|
|
5387
|
+
printf '%%1\t0\t2000000001\n%%2\t0\t2000000002\n%%3\t0\t2000000003\n'
|
|
5388
|
+
fi
|
|
5389
|
+
;;
|
|
5390
|
+
*) if [ -f "${logPath}.hud" ]; then printf '%%1\n%%2\n%%3\n'; elif [ -f "${logPath}.worker" ]; then printf '%%1\n%%2\n'; else printf '%%1\n'; fi ;;
|
|
5391
|
+
esac
|
|
4580
5392
|
;;
|
|
4581
|
-
|
|
4582
|
-
|
|
5393
|
+
split-window)
|
|
5394
|
+
case "$*" in
|
|
5395
|
+
*' -h '*) : > "${logPath}.worker"; echo '%2' ;;
|
|
5396
|
+
*) : > "${logPath}.hud"; echo '%3' ;;
|
|
5397
|
+
esac
|
|
5398
|
+
;;
|
|
5399
|
+
set-hook)
|
|
5400
|
+
case "$*" in
|
|
5401
|
+
*'client-attached['*) : > "$registration_failed_file"; echo 'client-attached hook registration failed' >&2; exit 1 ;;
|
|
5402
|
+
esac
|
|
4583
5403
|
;;
|
|
4584
5404
|
esac
|
|
4585
5405
|
`, async ({ logPath }) => {
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
process.env.
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
assert.
|
|
5406
|
+
const geminiPath = join(dirname(logPath), 'gemini');
|
|
5407
|
+
await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
|
|
5408
|
+
await chmod(geminiPath, 0o755);
|
|
5409
|
+
process.env.TMUX = 'leader-session,stub,0';
|
|
5410
|
+
process.env.TMUX_PANE = '%1';
|
|
5411
|
+
process.env.OMX_TEAM_WORKER_CLI = 'gemini';
|
|
5412
|
+
console.warn = () => { };
|
|
5413
|
+
let caught;
|
|
5414
|
+
try {
|
|
5415
|
+
createTeamSession('Partial hook registration', 1, cwd);
|
|
5416
|
+
}
|
|
5417
|
+
catch (error) {
|
|
5418
|
+
caught = error;
|
|
5419
|
+
}
|
|
5420
|
+
assert.ok(caught instanceof CreateTeamSessionPartialError);
|
|
5421
|
+
assert.equal(caught.partialSession.hudPaneId, '%3');
|
|
5422
|
+
assert.equal(caught.partialSession.hudPanePid, 2000000003);
|
|
5423
|
+
assert.ok(caught.partialSession.resizeHookName);
|
|
5424
|
+
const commands = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
|
|
5425
|
+
const registeredResize = commands.filter((command) => command.startsWith('set-hook ') && command.includes('client-resized[') && !command.startsWith('set-hook -u '));
|
|
5426
|
+
const registeredAttached = commands.filter((command) => command.startsWith('set-hook ') && command.includes('client-attached[') && !command.startsWith('set-hook -u '));
|
|
5427
|
+
const unregisteredResize = commands.filter((command) => command.startsWith('set-hook -u ') && command.includes('client-resized['));
|
|
5428
|
+
const unregisteredAttached = commands.filter((command) => command.startsWith('set-hook -u ') && command.includes('client-attached['));
|
|
5429
|
+
assert.equal(registeredResize.length, 1);
|
|
5430
|
+
assert.equal(registeredAttached.length, 1);
|
|
5431
|
+
assert.equal(unregisteredResize.length, 0);
|
|
5432
|
+
assert.equal(unregisteredAttached.length, 0);
|
|
4601
5433
|
});
|
|
4602
5434
|
}
|
|
4603
5435
|
finally {
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
process.env.OMX_LEADER_NODE_PATH = prevLeaderNodePath;
|
|
4608
|
-
else
|
|
4609
|
-
delete process.env.OMX_LEADER_NODE_PATH;
|
|
4610
|
-
if (typeof prevMsystem === 'string')
|
|
4611
|
-
process.env.MSYSTEM = prevMsystem;
|
|
4612
|
-
else
|
|
4613
|
-
delete process.env.MSYSTEM;
|
|
4614
|
-
if (typeof prevOstype === 'string')
|
|
4615
|
-
process.env.OSTYPE = prevOstype;
|
|
5436
|
+
console.warn = previousWarn;
|
|
5437
|
+
if (typeof previousTmux === 'string')
|
|
5438
|
+
process.env.TMUX = previousTmux;
|
|
4616
5439
|
else
|
|
4617
|
-
delete process.env.
|
|
4618
|
-
if (typeof
|
|
4619
|
-
process.env.
|
|
5440
|
+
delete process.env.TMUX;
|
|
5441
|
+
if (typeof previousTmuxPane === 'string')
|
|
5442
|
+
process.env.TMUX_PANE = previousTmuxPane;
|
|
4620
5443
|
else
|
|
4621
|
-
delete process.env.
|
|
4622
|
-
if (typeof
|
|
4623
|
-
process.env.
|
|
5444
|
+
delete process.env.TMUX_PANE;
|
|
5445
|
+
if (typeof previousWorkerCli === 'string')
|
|
5446
|
+
process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
|
|
4624
5447
|
else
|
|
4625
|
-
delete process.env.
|
|
5448
|
+
delete process.env.OMX_TEAM_WORKER_CLI;
|
|
4626
5449
|
await rm(cwd, { recursive: true, force: true });
|
|
4627
5450
|
}
|
|
4628
5451
|
});
|
|
4629
|
-
it('
|
|
4630
|
-
const cwd = await mkdtemp(join(tmpdir(), 'omx-
|
|
5452
|
+
it('degrades HUD run-shell resize failures to warnings during team startup', async () => {
|
|
5453
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-runshell-fallback-'));
|
|
5454
|
+
const prevTmux = process.env.TMUX;
|
|
5455
|
+
const prevTmuxPane = process.env.TMUX_PANE;
|
|
5456
|
+
const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
5457
|
+
const prevWarn = console.warn;
|
|
5458
|
+
const warnings = [];
|
|
4631
5459
|
try {
|
|
4632
|
-
await withMockTmuxFixture('omx-tmux-
|
|
4633
|
-
const statePath = `${logPath}.state`;
|
|
4634
|
-
return `#!/bin/sh
|
|
5460
|
+
await withMockTmuxFixture('omx-tmux-runshell-fallback-', (logPath) => `#!/bin/sh
|
|
4635
5461
|
set -eu
|
|
4636
|
-
printf '%s
|
|
5462
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
4637
5463
|
case "\${1:-}" in
|
|
5464
|
+
-V)
|
|
5465
|
+
echo "tmux 3.4"
|
|
5466
|
+
exit 0
|
|
5467
|
+
;;
|
|
5468
|
+
display-message)
|
|
5469
|
+
case "$*" in
|
|
5470
|
+
*"#{window_width}"*) echo "120" ;;
|
|
5471
|
+
*) echo "leader:0 %1" ;;
|
|
5472
|
+
esac
|
|
5473
|
+
exit 0
|
|
5474
|
+
;;
|
|
4638
5475
|
list-panes)
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
5476
|
+
case "$*" in
|
|
5477
|
+
*"-a -F #{pane_id}"*)
|
|
5478
|
+
printf "%%1\t0\t2000000001\n%%2\t0\t2000000002\n%%3\t0\t2000000003\n"
|
|
5479
|
+
;;
|
|
5480
|
+
*"pane_current_command"*) if [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi ;;
|
|
5481
|
+
*) if [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi ;;
|
|
5482
|
+
esac
|
|
4643
5483
|
exit 0
|
|
4644
5484
|
;;
|
|
4645
5485
|
split-window)
|
|
4646
|
-
: > "${
|
|
4647
|
-
|
|
5486
|
+
: > "${logPath}.worker"
|
|
5487
|
+
case "$*" in
|
|
5488
|
+
*" -h "*) echo "%2" ;;
|
|
5489
|
+
*) echo "%3" ;;
|
|
5490
|
+
esac
|
|
4648
5491
|
exit 0
|
|
4649
5492
|
;;
|
|
4650
|
-
run-shell
|
|
5493
|
+
run-shell)
|
|
5494
|
+
echo "Unsupported tmux compatibility command: run-shell" >&2
|
|
5495
|
+
exit 1
|
|
5496
|
+
;;
|
|
5497
|
+
set-option|resize-pane|select-layout|set-window-option|select-pane|set-hook|send-keys)
|
|
4651
5498
|
exit 0
|
|
4652
5499
|
;;
|
|
4653
5500
|
*)
|
|
4654
5501
|
exit 0
|
|
4655
5502
|
;;
|
|
4656
5503
|
esac
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
const
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
5504
|
+
`, async ({ logPath }) => {
|
|
5505
|
+
const fakeBinDir = join(logPath, '..');
|
|
5506
|
+
const geminiPath = join(fakeBinDir, 'gemini');
|
|
5507
|
+
await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
|
|
5508
|
+
await chmod(geminiPath, 0o755);
|
|
5509
|
+
process.env.TMUX = 'leader-session,stub,0';
|
|
5510
|
+
process.env.TMUX_PANE = '%1';
|
|
5511
|
+
process.env.OMX_TEAM_WORKER_CLI = 'gemini';
|
|
5512
|
+
console.warn = (...args) => { warnings.push(args.map(String).join(' ')); };
|
|
5513
|
+
const session = createTeamSession('Run Shell Fallback', 1, cwd);
|
|
5514
|
+
assert.equal(session.hudPaneId, '%3');
|
|
5515
|
+
assert.match(warnings.join('\n'), /HUD resize/);
|
|
5516
|
+
assert.match(warnings.join('\n'), /Unsupported tmux compatibility command: run-shell/);
|
|
4663
5517
|
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
4664
|
-
|
|
4665
|
-
assert.
|
|
4666
|
-
assert.doesNotMatch(tmuxLog, /kill-pane -t %
|
|
4667
|
-
assert.match(tmuxLog, /list-panes -t %11 -F #\{pane_id\}\t#\{pane_current_command\}\t#\{pane_start_command\}/);
|
|
5518
|
+
assert.match(tmuxLog, /run-shell -b sleep/);
|
|
5519
|
+
assert.doesNotMatch(tmuxLog, /kill-pane -t %2/);
|
|
5520
|
+
assert.doesNotMatch(tmuxLog, /kill-pane -t %3/);
|
|
4668
5521
|
});
|
|
4669
5522
|
}
|
|
4670
5523
|
finally {
|
|
5524
|
+
console.warn = prevWarn;
|
|
5525
|
+
if (typeof prevTmux === 'string')
|
|
5526
|
+
process.env.TMUX = prevTmux;
|
|
5527
|
+
else
|
|
5528
|
+
delete process.env.TMUX;
|
|
5529
|
+
if (typeof prevTmuxPane === 'string')
|
|
5530
|
+
process.env.TMUX_PANE = prevTmuxPane;
|
|
5531
|
+
else
|
|
5532
|
+
delete process.env.TMUX_PANE;
|
|
5533
|
+
if (typeof prevWorkerCli === 'string')
|
|
5534
|
+
process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
|
|
5535
|
+
else
|
|
5536
|
+
delete process.env.OMX_TEAM_WORKER_CLI;
|
|
4671
5537
|
await rm(cwd, { recursive: true, force: true });
|
|
4672
5538
|
}
|
|
4673
5539
|
});
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
5540
|
+
});
|
|
5541
|
+
describe('native Windows HUD reconciliation', () => {
|
|
5542
|
+
it('allows team startup on native Windows when current tmux client is reachable without TMUX env vars', async () => {
|
|
5543
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-win32-no-env-'));
|
|
5544
|
+
const prevTmux = process.env.TMUX;
|
|
5545
|
+
const prevTmuxPane = process.env.TMUX_PANE;
|
|
5546
|
+
const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
5547
|
+
const prevMsystem = process.env.MSYSTEM;
|
|
5548
|
+
const prevOstype = process.env.OSTYPE;
|
|
5549
|
+
const prevWsl = process.env.WSL_DISTRO_NAME;
|
|
5550
|
+
const prevWslInterop = process.env.WSL_INTEROP;
|
|
5551
|
+
const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
4677
5552
|
try {
|
|
4678
|
-
await withMockTmuxFixture('omx-tmux-
|
|
5553
|
+
await withMockTmuxFixture('omx-tmux-win32-no-env-', (logPath) => `#!/bin/sh
|
|
4679
5554
|
set -eu
|
|
4680
5555
|
printf '%s\\n' "$*" >> "${logPath}"
|
|
4681
5556
|
case "\${1:-}" in
|
|
5557
|
+
-V)
|
|
5558
|
+
echo "tmux 3.4"
|
|
5559
|
+
exit 0
|
|
5560
|
+
;;
|
|
4682
5561
|
display-message)
|
|
4683
5562
|
case "$*" in
|
|
4684
|
-
*"#{
|
|
4685
|
-
echo "
|
|
5563
|
+
*"#{window_width}"*)
|
|
5564
|
+
echo "120"
|
|
5565
|
+
;;
|
|
5566
|
+
*)
|
|
5567
|
+
echo "leader:0 %1"
|
|
5568
|
+
;;
|
|
5569
|
+
esac
|
|
5570
|
+
exit 0
|
|
5571
|
+
;;
|
|
5572
|
+
list-panes)
|
|
5573
|
+
case "$*" in
|
|
5574
|
+
*"-a -F #{pane_id}"*)
|
|
5575
|
+
printf "%%1\\t0\\t2000000001\\n%%2\\t0\\t2000000002\\n%%3\\t0\\t2000000003\\n"
|
|
5576
|
+
;;
|
|
5577
|
+
*"pane_current_command"*)
|
|
5578
|
+
if [ -f "${logPath}.hud" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n%%3\\tnode\\t'node omx hud --watch'\\n"; elif [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
|
|
5579
|
+
;;
|
|
5580
|
+
*)
|
|
5581
|
+
if [ -f "${logPath}.hud" ]; then printf "%%1\\n%%2\\n%%3\\n"; elif [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi
|
|
4686
5582
|
;;
|
|
4687
5583
|
esac
|
|
4688
5584
|
exit 0
|
|
4689
5585
|
;;
|
|
4690
5586
|
split-window)
|
|
4691
|
-
|
|
5587
|
+
if [ "$2" = "-h" ]; then : > "${logPath}.worker"; else : > "${logPath}.hud"; fi
|
|
5588
|
+
case "$*" in
|
|
5589
|
+
*" -h "*)
|
|
5590
|
+
echo "%2"
|
|
5591
|
+
;;
|
|
5592
|
+
*)
|
|
5593
|
+
echo "%3"
|
|
5594
|
+
;;
|
|
5595
|
+
esac
|
|
4692
5596
|
exit 0
|
|
4693
5597
|
;;
|
|
4694
|
-
|
|
5598
|
+
resize-pane|select-layout|set-window-option|select-pane|kill-pane|set-hook|run-shell)
|
|
4695
5599
|
exit 0
|
|
4696
5600
|
;;
|
|
4697
5601
|
*)
|
|
@@ -4699,56 +5603,123 @@ case "\${1:-}" in
|
|
|
4699
5603
|
;;
|
|
4700
5604
|
esac
|
|
4701
5605
|
`, async ({ logPath }) => {
|
|
4702
|
-
const
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
5606
|
+
const fakeBinDir = join(logPath, '..');
|
|
5607
|
+
const geminiPath = join(fakeBinDir, 'gemini');
|
|
5608
|
+
await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
|
|
5609
|
+
await chmod(geminiPath, 0o755);
|
|
5610
|
+
delete process.env.TMUX;
|
|
5611
|
+
delete process.env.TMUX_PANE;
|
|
5612
|
+
process.env.OMX_TEAM_WORKER_CLI = 'gemini';
|
|
5613
|
+
delete process.env.MSYSTEM;
|
|
5614
|
+
delete process.env.OSTYPE;
|
|
5615
|
+
delete process.env.WSL_DISTRO_NAME;
|
|
5616
|
+
delete process.env.WSL_INTEROP;
|
|
5617
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
5618
|
+
const session = createTeamSession('Windows Team', 1, cwd);
|
|
5619
|
+
assert.equal(session.name, 'leader:0');
|
|
5620
|
+
assert.equal(session.leaderPaneId, '%1');
|
|
5621
|
+
assert.equal(session.hudPaneId, '%3');
|
|
4706
5622
|
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
4707
|
-
assert.match(tmuxLog, /display-message -p
|
|
4708
|
-
assert.match(tmuxLog,
|
|
4709
|
-
assert.doesNotMatch(tmuxLog,
|
|
4710
|
-
assert.match(tmuxLog,
|
|
5623
|
+
assert.match(tmuxLog, /display-message -p #\{session_name\}:#\{window_index\} #\{pane_id\}/);
|
|
5624
|
+
assert.match(tmuxLog, /powershell\.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -EncodedCommand/);
|
|
5625
|
+
assert.doesNotMatch(tmuxLog, /\/bin\/sh -lc/);
|
|
5626
|
+
assert.match(tmuxLog, new RegExp(`resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
|
|
4711
5627
|
});
|
|
4712
5628
|
}
|
|
4713
5629
|
finally {
|
|
4714
|
-
|
|
4715
|
-
|
|
5630
|
+
if (origPlatform)
|
|
5631
|
+
Object.defineProperty(process, 'platform', origPlatform);
|
|
5632
|
+
if (typeof prevTmux === 'string')
|
|
5633
|
+
process.env.TMUX = prevTmux;
|
|
5634
|
+
else
|
|
5635
|
+
delete process.env.TMUX;
|
|
5636
|
+
if (typeof prevTmuxPane === 'string')
|
|
5637
|
+
process.env.TMUX_PANE = prevTmuxPane;
|
|
5638
|
+
else
|
|
5639
|
+
delete process.env.TMUX_PANE;
|
|
5640
|
+
if (typeof prevWorkerCli === 'string')
|
|
5641
|
+
process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
|
|
5642
|
+
else
|
|
5643
|
+
delete process.env.OMX_TEAM_WORKER_CLI;
|
|
5644
|
+
if (typeof prevMsystem === 'string')
|
|
5645
|
+
process.env.MSYSTEM = prevMsystem;
|
|
5646
|
+
else
|
|
5647
|
+
delete process.env.MSYSTEM;
|
|
5648
|
+
if (typeof prevOstype === 'string')
|
|
5649
|
+
process.env.OSTYPE = prevOstype;
|
|
5650
|
+
else
|
|
5651
|
+
delete process.env.OSTYPE;
|
|
5652
|
+
if (typeof prevWsl === 'string')
|
|
5653
|
+
process.env.WSL_DISTRO_NAME = prevWsl;
|
|
5654
|
+
else
|
|
5655
|
+
delete process.env.WSL_DISTRO_NAME;
|
|
5656
|
+
if (typeof prevWslInterop === 'string')
|
|
5657
|
+
process.env.WSL_INTEROP = prevWslInterop;
|
|
5658
|
+
else
|
|
5659
|
+
delete process.env.WSL_INTEROP;
|
|
5660
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4716
5661
|
}
|
|
4717
5662
|
});
|
|
4718
|
-
it('
|
|
4719
|
-
const
|
|
4720
|
-
const
|
|
4721
|
-
|
|
5663
|
+
it('avoids nested tmux run-shell hooks during team HUD startup on native Windows', async () => {
|
|
5664
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-win32-hud-'));
|
|
5665
|
+
const prevTmux = process.env.TMUX;
|
|
5666
|
+
const prevTmuxPane = process.env.TMUX_PANE;
|
|
5667
|
+
const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
5668
|
+
const prevMsystem = process.env.MSYSTEM;
|
|
5669
|
+
const prevOstype = process.env.OSTYPE;
|
|
5670
|
+
const prevWsl = process.env.WSL_DISTRO_NAME;
|
|
5671
|
+
const prevWslInterop = process.env.WSL_INTEROP;
|
|
5672
|
+
const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
4722
5673
|
try {
|
|
4723
|
-
await withMockTmuxFixture('omx-tmux-
|
|
5674
|
+
await withMockTmuxFixture('omx-tmux-win32-hud-reconcile-', (logPath) => `#!/bin/sh
|
|
4724
5675
|
set -eu
|
|
4725
5676
|
printf '%s\\n' "$*" >> "${logPath}"
|
|
4726
5677
|
case "\${1:-}" in
|
|
5678
|
+
-V)
|
|
5679
|
+
echo "tmux 3.4"
|
|
5680
|
+
exit 0
|
|
5681
|
+
;;
|
|
4727
5682
|
display-message)
|
|
4728
5683
|
case "$*" in
|
|
4729
|
-
*"#{
|
|
4730
|
-
echo "
|
|
5684
|
+
*"#{window_width}"*)
|
|
5685
|
+
echo "120"
|
|
5686
|
+
;;
|
|
5687
|
+
*)
|
|
5688
|
+
echo "leader:0 %1"
|
|
4731
5689
|
;;
|
|
4732
5690
|
esac
|
|
4733
5691
|
exit 0
|
|
4734
5692
|
;;
|
|
4735
|
-
|
|
5693
|
+
list-panes)
|
|
4736
5694
|
case "$*" in
|
|
4737
|
-
*"-
|
|
4738
|
-
|
|
4739
|
-
exit 0
|
|
5695
|
+
*"-a -F #{pane_id}"*)
|
|
5696
|
+
printf "%%1\\t0\\t2000000001\\n%%2\\t0\\t2000000002\\n%%3\\t0\\t2000000003\\n"
|
|
4740
5697
|
;;
|
|
4741
|
-
*"
|
|
4742
|
-
|
|
4743
|
-
exit 1
|
|
5698
|
+
*"pane_current_command"*)
|
|
5699
|
+
if [ -f "${logPath}.hud" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n%%3\\tnode\\t'node omx hud --watch'\\n"; elif [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
|
|
4744
5700
|
;;
|
|
4745
5701
|
*)
|
|
4746
|
-
|
|
4747
|
-
exit 1
|
|
5702
|
+
if [ -f "${logPath}.hud" ]; then printf "%%1\\n%%2\\n%%3\\n"; elif [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi
|
|
4748
5703
|
;;
|
|
4749
5704
|
esac
|
|
5705
|
+
exit 0
|
|
4750
5706
|
;;
|
|
4751
|
-
|
|
5707
|
+
split-window)
|
|
5708
|
+
if [ "$2" = "-h" ]; then : > "${logPath}.worker"; else : > "${logPath}.hud"; fi
|
|
5709
|
+
case "$*" in
|
|
5710
|
+
*" -h "*)
|
|
5711
|
+
echo "%2"
|
|
5712
|
+
;;
|
|
5713
|
+
*)
|
|
5714
|
+
echo "%3"
|
|
5715
|
+
;;
|
|
5716
|
+
esac
|
|
5717
|
+
exit 0
|
|
5718
|
+
;;
|
|
5719
|
+
resize-pane|select-layout|set-window-option|select-pane|kill-pane)
|
|
5720
|
+
exit 0
|
|
5721
|
+
;;
|
|
5722
|
+
set-hook|run-shell)
|
|
4752
5723
|
exit 0
|
|
4753
5724
|
;;
|
|
4754
5725
|
*)
|
|
@@ -4756,71 +5727,126 @@ case "\${1:-}" in
|
|
|
4756
5727
|
;;
|
|
4757
5728
|
esac
|
|
4758
5729
|
`, async ({ logPath }) => {
|
|
4759
|
-
const
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
5730
|
+
const fakeBinDir = join(logPath, '..');
|
|
5731
|
+
const geminiPath = join(fakeBinDir, 'gemini');
|
|
5732
|
+
await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
|
|
5733
|
+
await chmod(geminiPath, 0o755);
|
|
5734
|
+
process.env.TMUX = 'leader-session,stub,0';
|
|
5735
|
+
process.env.TMUX_PANE = '%1';
|
|
5736
|
+
process.env.OMX_TEAM_WORKER_CLI = 'gemini';
|
|
5737
|
+
delete process.env.MSYSTEM;
|
|
5738
|
+
delete process.env.OSTYPE;
|
|
5739
|
+
delete process.env.WSL_DISTRO_NAME;
|
|
5740
|
+
delete process.env.WSL_INTEROP;
|
|
5741
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
5742
|
+
const session = createTeamSession('Windows Team', 1, cwd);
|
|
5743
|
+
assert.equal(session.hudPaneId, '%3');
|
|
5744
|
+
assert.equal(session.resizeHookName, null);
|
|
5745
|
+
assert.equal(session.resizeHookTarget, null);
|
|
4763
5746
|
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
4764
|
-
assert.match(tmuxLog,
|
|
4765
|
-
assert.
|
|
4766
|
-
assert.doesNotMatch(tmuxLog,
|
|
4767
|
-
assert.
|
|
5747
|
+
assert.match(tmuxLog, new RegExp(`resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
|
|
5748
|
+
assert.doesNotMatch(tmuxLog, /set-hook -w /);
|
|
5749
|
+
assert.doesNotMatch(tmuxLog, /window-resized\[/);
|
|
5750
|
+
assert.doesNotMatch(tmuxLog, /set-hook -t leader:0 client-attached\[\d+\]/);
|
|
5751
|
+
assert.doesNotMatch(tmuxLog, /run-shell -b sleep \d+; tmux resize-pane -t %3 -y \d+ >/);
|
|
5752
|
+
assert.doesNotMatch(tmuxLog, /run-shell tmux resize-pane -t %3 -y \d+ >/);
|
|
4768
5753
|
});
|
|
4769
5754
|
}
|
|
4770
5755
|
finally {
|
|
4771
|
-
|
|
5756
|
+
if (origPlatform)
|
|
5757
|
+
Object.defineProperty(process, 'platform', origPlatform);
|
|
5758
|
+
if (typeof prevTmux === 'string')
|
|
5759
|
+
process.env.TMUX = prevTmux;
|
|
5760
|
+
else
|
|
5761
|
+
delete process.env.TMUX;
|
|
5762
|
+
if (typeof prevTmuxPane === 'string')
|
|
5763
|
+
process.env.TMUX_PANE = prevTmuxPane;
|
|
5764
|
+
else
|
|
5765
|
+
delete process.env.TMUX_PANE;
|
|
5766
|
+
if (typeof prevWorkerCli === 'string')
|
|
5767
|
+
process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
|
|
5768
|
+
else
|
|
5769
|
+
delete process.env.OMX_TEAM_WORKER_CLI;
|
|
5770
|
+
if (typeof prevMsystem === 'string')
|
|
5771
|
+
process.env.MSYSTEM = prevMsystem;
|
|
5772
|
+
else
|
|
5773
|
+
delete process.env.MSYSTEM;
|
|
5774
|
+
if (typeof prevOstype === 'string')
|
|
5775
|
+
process.env.OSTYPE = prevOstype;
|
|
5776
|
+
else
|
|
5777
|
+
delete process.env.OSTYPE;
|
|
5778
|
+
if (typeof prevWsl === 'string')
|
|
5779
|
+
process.env.WSL_DISTRO_NAME = prevWsl;
|
|
5780
|
+
else
|
|
5781
|
+
delete process.env.WSL_DISTRO_NAME;
|
|
5782
|
+
if (typeof prevWslInterop === 'string')
|
|
5783
|
+
process.env.WSL_INTEROP = prevWslInterop;
|
|
5784
|
+
else
|
|
5785
|
+
delete process.env.WSL_INTEROP;
|
|
5786
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4772
5787
|
}
|
|
4773
5788
|
});
|
|
4774
|
-
it('
|
|
4775
|
-
const
|
|
4776
|
-
const
|
|
4777
|
-
const
|
|
4778
|
-
const
|
|
4779
|
-
const
|
|
4780
|
-
const
|
|
4781
|
-
const
|
|
4782
|
-
const
|
|
4783
|
-
const
|
|
4784
|
-
const statCalls = [];
|
|
5789
|
+
it('rejects synthetic worker and HUD pane ids that never materialize on native Windows', async () => {
|
|
5790
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-team-win32-synthetic-pane-'));
|
|
5791
|
+
const prevTmux = process.env.TMUX;
|
|
5792
|
+
const prevTmuxPane = process.env.TMUX_PANE;
|
|
5793
|
+
const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
|
|
5794
|
+
const prevMsystem = process.env.MSYSTEM;
|
|
5795
|
+
const prevOstype = process.env.OSTYPE;
|
|
5796
|
+
const prevWsl = process.env.WSL_DISTRO_NAME;
|
|
5797
|
+
const prevWslInterop = process.env.WSL_INTEROP;
|
|
5798
|
+
const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
4785
5799
|
try {
|
|
4786
|
-
|
|
4787
|
-
await writeFile(cygpathPath, '#!/bin/sh\nprintf "%s\\n" "$2"\n');
|
|
4788
|
-
await chmod(cygpathPath, 0o755);
|
|
4789
|
-
process.env.PATH = `${fakeCygpathDir}:${previousPath ?? ''}`;
|
|
4790
|
-
process.env.MSYSTEM = 'MINGW64';
|
|
4791
|
-
process.env.OSTYPE = 'msys';
|
|
4792
|
-
delete process.env.WSL_DISTRO_NAME;
|
|
4793
|
-
delete process.env.WSL_INTEROP;
|
|
4794
|
-
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
4795
|
-
await withMockTmuxFixture('omx-tmux-msys-live-cwd-standalone-hud-', (logPath) => `#!/bin/sh
|
|
5800
|
+
await withMockTmuxFixture('omx-tmux-win32-synthetic-pane-', (logPath) => `#!/bin/sh
|
|
4796
5801
|
set -eu
|
|
4797
5802
|
printf '%s\\n' "$*" >> "${logPath}"
|
|
4798
5803
|
case "\${1:-}" in
|
|
5804
|
+
-V)
|
|
5805
|
+
echo "tmux 3.3.2"
|
|
5806
|
+
exit 0
|
|
5807
|
+
;;
|
|
4799
5808
|
display-message)
|
|
4800
5809
|
case "$*" in
|
|
4801
|
-
*"#{
|
|
4802
|
-
echo "
|
|
5810
|
+
*"#{window_width}"*)
|
|
5811
|
+
echo "120"
|
|
5812
|
+
;;
|
|
5813
|
+
*)
|
|
5814
|
+
echo "leader:0 %1"
|
|
4803
5815
|
;;
|
|
4804
5816
|
esac
|
|
4805
5817
|
exit 0
|
|
4806
5818
|
;;
|
|
4807
|
-
|
|
5819
|
+
list-panes)
|
|
4808
5820
|
case "$*" in
|
|
4809
|
-
*"-
|
|
4810
|
-
|
|
4811
|
-
|
|
5821
|
+
*"-a -F #{pane_id}"*)
|
|
5822
|
+
printf "%%1\t0\t2000000001\n"
|
|
5823
|
+
if [ ! -f "${logPath}.killed-%2" ]; then printf "%%2\t0\t2000000002\n"; fi
|
|
4812
5824
|
;;
|
|
4813
|
-
*"
|
|
4814
|
-
|
|
4815
|
-
exit 1
|
|
5825
|
+
*"pane_current_command"*)
|
|
5826
|
+
printf "%%1\\tnode\\t'codex'\\n"
|
|
4816
5827
|
;;
|
|
4817
5828
|
*)
|
|
4818
|
-
|
|
4819
|
-
exit 1
|
|
5829
|
+
printf "%%1\\n"
|
|
4820
5830
|
;;
|
|
4821
5831
|
esac
|
|
5832
|
+
exit 0
|
|
4822
5833
|
;;
|
|
4823
|
-
|
|
5834
|
+
split-window)
|
|
5835
|
+
case "$*" in
|
|
5836
|
+
*" -h "*)
|
|
5837
|
+
echo "%2"
|
|
5838
|
+
;;
|
|
5839
|
+
*)
|
|
5840
|
+
echo "%3"
|
|
5841
|
+
;;
|
|
5842
|
+
esac
|
|
5843
|
+
exit 0
|
|
5844
|
+
;;
|
|
5845
|
+
kill-pane)
|
|
5846
|
+
: > "${logPath}.killed-$3"
|
|
5847
|
+
exit 0
|
|
5848
|
+
;;
|
|
5849
|
+
select-layout|set-window-option|select-pane|resize-pane|set-hook|run-shell)
|
|
4824
5850
|
exit 0
|
|
4825
5851
|
;;
|
|
4826
5852
|
*)
|
|
@@ -4828,99 +5854,86 @@ case "\${1:-}" in
|
|
|
4828
5854
|
;;
|
|
4829
5855
|
esac
|
|
4830
5856
|
`, async ({ logPath }) => {
|
|
4831
|
-
const
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
5857
|
+
const fakeBinDir = join(logPath, '..');
|
|
5858
|
+
const geminiPath = join(fakeBinDir, 'gemini');
|
|
5859
|
+
const powershellExePath = join(fakeBinDir, 'powershell.exe');
|
|
5860
|
+
await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
|
|
5861
|
+
await chmod(geminiPath, 0o755);
|
|
5862
|
+
await writeFile(powershellExePath, '');
|
|
5863
|
+
process.env.TMUX = 'leader-session,stub,0';
|
|
5864
|
+
process.env.TMUX_PANE = '%1';
|
|
5865
|
+
process.env.OMX_TEAM_WORKER_CLI = 'gemini';
|
|
5866
|
+
delete process.env.MSYSTEM;
|
|
5867
|
+
delete process.env.OSTYPE;
|
|
5868
|
+
delete process.env.WSL_DISTRO_NAME;
|
|
5869
|
+
delete process.env.WSL_INTEROP;
|
|
5870
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
5871
|
+
assert.throws(() => createTeamSession('Windows Team', 1, cwd), /worker pane 1 did not remain present/);
|
|
4841
5872
|
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
4842
|
-
|
|
4843
|
-
assert.
|
|
4844
|
-
assert.
|
|
4845
|
-
assert.match(tmuxLog, /select-pane -t %11/);
|
|
5873
|
+
const listPaneCalls = tmuxLog.match(/list-panes -t leader:0 -F #\{pane_id\}\t#\{pane_current_command\}\t#\{pane_start_command\}/g) || [];
|
|
5874
|
+
assert.ok(listPaneCalls.length >= 2, tmuxLog);
|
|
5875
|
+
assert.match(tmuxLog, /kill-pane -t %2/);
|
|
4846
5876
|
});
|
|
4847
5877
|
}
|
|
4848
5878
|
finally {
|
|
4849
|
-
if (
|
|
4850
|
-
Object.defineProperty(process, 'platform',
|
|
4851
|
-
if (typeof
|
|
4852
|
-
process.env.
|
|
5879
|
+
if (origPlatform)
|
|
5880
|
+
Object.defineProperty(process, 'platform', origPlatform);
|
|
5881
|
+
if (typeof prevTmux === 'string')
|
|
5882
|
+
process.env.TMUX = prevTmux;
|
|
4853
5883
|
else
|
|
4854
|
-
delete process.env.
|
|
4855
|
-
if (typeof
|
|
4856
|
-
process.env.
|
|
5884
|
+
delete process.env.TMUX;
|
|
5885
|
+
if (typeof prevTmuxPane === 'string')
|
|
5886
|
+
process.env.TMUX_PANE = prevTmuxPane;
|
|
5887
|
+
else
|
|
5888
|
+
delete process.env.TMUX_PANE;
|
|
5889
|
+
if (typeof prevWorkerCli === 'string')
|
|
5890
|
+
process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
|
|
5891
|
+
else
|
|
5892
|
+
delete process.env.OMX_TEAM_WORKER_CLI;
|
|
5893
|
+
if (typeof prevMsystem === 'string')
|
|
5894
|
+
process.env.MSYSTEM = prevMsystem;
|
|
4857
5895
|
else
|
|
4858
5896
|
delete process.env.MSYSTEM;
|
|
4859
|
-
if (typeof
|
|
4860
|
-
process.env.OSTYPE =
|
|
5897
|
+
if (typeof prevOstype === 'string')
|
|
5898
|
+
process.env.OSTYPE = prevOstype;
|
|
4861
5899
|
else
|
|
4862
5900
|
delete process.env.OSTYPE;
|
|
4863
|
-
if (typeof
|
|
4864
|
-
process.env.WSL_DISTRO_NAME =
|
|
5901
|
+
if (typeof prevWsl === 'string')
|
|
5902
|
+
process.env.WSL_DISTRO_NAME = prevWsl;
|
|
4865
5903
|
else
|
|
4866
5904
|
delete process.env.WSL_DISTRO_NAME;
|
|
4867
|
-
if (typeof
|
|
4868
|
-
process.env.WSL_INTEROP =
|
|
5905
|
+
if (typeof prevWslInterop === 'string')
|
|
5906
|
+
process.env.WSL_INTEROP = prevWslInterop;
|
|
4869
5907
|
else
|
|
4870
5908
|
delete process.env.WSL_INTEROP;
|
|
4871
|
-
await rm(
|
|
5909
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4872
5910
|
}
|
|
4873
5911
|
});
|
|
4874
|
-
it('
|
|
4875
|
-
const
|
|
4876
|
-
const
|
|
4877
|
-
const
|
|
4878
|
-
const
|
|
4879
|
-
const
|
|
4880
|
-
const
|
|
4881
|
-
const
|
|
4882
|
-
const previousWslInterop = process.env.WSL_INTEROP;
|
|
4883
|
-
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
4884
|
-
const statCalls = [];
|
|
5912
|
+
it('restores standalone HUD panes with direct resize on native Windows', async () => {
|
|
5913
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-win32-hud-'));
|
|
5914
|
+
const prevLeaderNodePath = process.env.OMX_LEADER_NODE_PATH;
|
|
5915
|
+
const prevMsystem = process.env.MSYSTEM;
|
|
5916
|
+
const prevOstype = process.env.OSTYPE;
|
|
5917
|
+
const prevWsl = process.env.WSL_DISTRO_NAME;
|
|
5918
|
+
const prevWslInterop = process.env.WSL_INTEROP;
|
|
5919
|
+
const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
4885
5920
|
try {
|
|
4886
|
-
|
|
4887
|
-
await writeFile(cygpathPath, '#!/bin/sh\nprintf "%s\\n" "$2"\n');
|
|
4888
|
-
await chmod(cygpathPath, 0o755);
|
|
4889
|
-
process.env.PATH = `${fakeCygpathDir}:${previousPath ?? ''}`;
|
|
4890
|
-
process.env.MSYSTEM = 'MINGW64';
|
|
4891
|
-
process.env.OSTYPE = 'msys';
|
|
4892
|
-
delete process.env.WSL_DISTRO_NAME;
|
|
4893
|
-
delete process.env.WSL_INTEROP;
|
|
4894
|
-
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
4895
|
-
await withMockTmuxFixture('omx-tmux-msys-deleted-live-cwd-standalone-hud-', (logPath) => `#!/bin/sh
|
|
5921
|
+
await withMockTmuxFixture('omx-tmux-win32-standalone-hud-', (logPath) => `#!/bin/sh
|
|
4896
5922
|
set -eu
|
|
4897
5923
|
printf '%s\\n' "$*" >> "${logPath}"
|
|
4898
5924
|
case "\${1:-}" in
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
*"#{pane_current_path}"*)
|
|
4902
|
-
echo "${liveLeaderCwd}"
|
|
4903
|
-
;;
|
|
4904
|
-
esac
|
|
5925
|
+
list-panes)
|
|
5926
|
+
if [ "$2" = "-a" ]; then printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000044\\n'; fi
|
|
4905
5927
|
exit 0
|
|
4906
5928
|
;;
|
|
4907
5929
|
split-window)
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
echo "deleted live cwd cannot be used" >&2
|
|
4911
|
-
exit 1
|
|
4912
|
-
;;
|
|
4913
|
-
*"-c ${fallbackCwd} "*)
|
|
4914
|
-
echo "%44"
|
|
4915
|
-
exit 0
|
|
4916
|
-
;;
|
|
4917
|
-
*)
|
|
4918
|
-
echo "unexpected cwd: $*" >&2
|
|
4919
|
-
exit 1
|
|
4920
|
-
;;
|
|
4921
|
-
esac
|
|
5930
|
+
echo "%44"
|
|
5931
|
+
exit 0
|
|
4922
5932
|
;;
|
|
4923
|
-
|
|
5933
|
+
resize-pane|select-pane)
|
|
5934
|
+
exit 0
|
|
5935
|
+
;;
|
|
5936
|
+
set-hook|run-shell)
|
|
4924
5937
|
exit 0
|
|
4925
5938
|
;;
|
|
4926
5939
|
*)
|
|
@@ -4928,110 +5941,310 @@ case "\${1:-}" in
|
|
|
4928
5941
|
;;
|
|
4929
5942
|
esac
|
|
4930
5943
|
`, async ({ logPath }) => {
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
5944
|
+
delete process.env.MSYSTEM;
|
|
5945
|
+
delete process.env.OSTYPE;
|
|
5946
|
+
delete process.env.WSL_DISTRO_NAME;
|
|
5947
|
+
delete process.env.WSL_INTEROP;
|
|
5948
|
+
process.env.OMX_LEADER_NODE_PATH = 'C:\\Program Files\\nodejs\\node.exe';
|
|
5949
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
5950
|
+
const paneId = restoreStandaloneHudPane('%11', cwd);
|
|
4938
5951
|
assert.equal(paneId, '%44');
|
|
4939
|
-
assert.equal(statCalls.includes(liveLeaderCwd), false);
|
|
4940
|
-
assert.equal(statCalls.includes(fallbackCwd), true);
|
|
4941
5952
|
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
assert.ok(liveAttemptIndex >= 0, 'deleted MSYS live cwd should still be attempted');
|
|
4945
|
-
assert.ok(fallbackAttemptIndex > liveAttemptIndex, 'fallback should be attempted after live split failure');
|
|
5953
|
+
assert.match(tmuxLog, /'C:\\Program Files\\nodejs\\node\.exe'/);
|
|
5954
|
+
assert.match(tmuxLog, new RegExp(`resize-pane -t %44 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
|
|
4946
5955
|
assert.match(tmuxLog, /select-pane -t %11/);
|
|
5956
|
+
const commands = tmuxLog.trim().split('\n').filter(Boolean);
|
|
5957
|
+
for (const [index, command] of commands.entries()) {
|
|
5958
|
+
if (/^(split-window .* -t %11|resize-pane -t %44|select-pane -t %11)( |$)/.test(command)) {
|
|
5959
|
+
assert.match(commands[index - 1] ?? '', /^list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}$/, `fresh global proof must immediately precede ${command}`);
|
|
5960
|
+
}
|
|
5961
|
+
}
|
|
5962
|
+
assert.doesNotMatch(tmuxLog, /run-shell -b sleep \d+; tmux resize-pane -t %44 -y \d+ >/);
|
|
5963
|
+
assert.doesNotMatch(tmuxLog, /run-shell tmux resize-pane -t %44 -y \d+ >/);
|
|
5964
|
+
assert.doesNotMatch(tmuxLog, /set-hook -t /);
|
|
4947
5965
|
});
|
|
4948
5966
|
}
|
|
4949
5967
|
finally {
|
|
4950
|
-
if (
|
|
4951
|
-
Object.defineProperty(process, 'platform',
|
|
4952
|
-
if (typeof
|
|
4953
|
-
process.env.
|
|
5968
|
+
if (origPlatform)
|
|
5969
|
+
Object.defineProperty(process, 'platform', origPlatform);
|
|
5970
|
+
if (typeof prevLeaderNodePath === 'string')
|
|
5971
|
+
process.env.OMX_LEADER_NODE_PATH = prevLeaderNodePath;
|
|
4954
5972
|
else
|
|
4955
|
-
delete process.env.
|
|
4956
|
-
if (typeof
|
|
4957
|
-
process.env.MSYSTEM =
|
|
5973
|
+
delete process.env.OMX_LEADER_NODE_PATH;
|
|
5974
|
+
if (typeof prevMsystem === 'string')
|
|
5975
|
+
process.env.MSYSTEM = prevMsystem;
|
|
4958
5976
|
else
|
|
4959
5977
|
delete process.env.MSYSTEM;
|
|
4960
|
-
if (typeof
|
|
4961
|
-
process.env.OSTYPE =
|
|
5978
|
+
if (typeof prevOstype === 'string')
|
|
5979
|
+
process.env.OSTYPE = prevOstype;
|
|
4962
5980
|
else
|
|
4963
5981
|
delete process.env.OSTYPE;
|
|
4964
|
-
if (typeof
|
|
4965
|
-
process.env.WSL_DISTRO_NAME =
|
|
5982
|
+
if (typeof prevWsl === 'string')
|
|
5983
|
+
process.env.WSL_DISTRO_NAME = prevWsl;
|
|
4966
5984
|
else
|
|
4967
5985
|
delete process.env.WSL_DISTRO_NAME;
|
|
4968
|
-
if (typeof
|
|
4969
|
-
process.env.WSL_INTEROP =
|
|
5986
|
+
if (typeof prevWslInterop === 'string')
|
|
5987
|
+
process.env.WSL_INTEROP = prevWslInterop;
|
|
4970
5988
|
else
|
|
4971
5989
|
delete process.env.WSL_INTEROP;
|
|
4972
|
-
await rm(
|
|
5990
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4973
5991
|
}
|
|
4974
5992
|
});
|
|
4975
|
-
it('
|
|
4976
|
-
const cwd = await mkdtemp(join(tmpdir(), 'omx-
|
|
4977
|
-
const
|
|
4978
|
-
const
|
|
4979
|
-
const
|
|
4980
|
-
const previousArgv = process.argv;
|
|
5993
|
+
it('rejects restored HUD debt roots outside the canonical direct Team root before splitting', async () => {
|
|
5994
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-restored-hud-debt-root-'));
|
|
5995
|
+
const canonicalTeamsRoot = join(cwd, '.omx', 'state', 'team');
|
|
5996
|
+
const externalRoot = await mkdtemp(join(tmpdir(), 'omx-restored-hud-debt-external-'));
|
|
5997
|
+
const symlinkRoot = join(canonicalTeamsRoot, 'linked-team');
|
|
4981
5998
|
try {
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
5999
|
+
await mkdir(canonicalTeamsRoot, { recursive: true });
|
|
6000
|
+
await symlink(externalRoot, symlinkRoot);
|
|
6001
|
+
const invalidRoots = [
|
|
6002
|
+
join(cwd, '.omx', 'state', 'foreign'),
|
|
6003
|
+
join(canonicalTeamsRoot, 'alpha', '..', '..', 'foreign'),
|
|
6004
|
+
join(canonicalTeamsRoot, 'alpha', 'nested'),
|
|
6005
|
+
symlinkRoot,
|
|
6006
|
+
];
|
|
6007
|
+
for (const stateRoot of invalidRoots) {
|
|
6008
|
+
assert.throws(() => restoreStandaloneHudPane('%11', cwd, { stateRoot }), /restored_hud_cleanup_debt_state_root_invalid/);
|
|
6009
|
+
}
|
|
6010
|
+
}
|
|
6011
|
+
finally {
|
|
6012
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6013
|
+
await rm(externalRoot, { recursive: true, force: true });
|
|
6014
|
+
}
|
|
6015
|
+
});
|
|
6016
|
+
it('does not suppress a non-Windows parent-directory fsync failure for restored HUD debt', async () => {
|
|
6017
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-restored-hud-debt-fsync-'));
|
|
6018
|
+
const originalFsyncSync = fs.fsyncSync;
|
|
6019
|
+
try {
|
|
6020
|
+
await withMockTmuxFixture('omx-restored-hud-debt-fsync-', (logPath) => `#!/bin/sh
|
|
6021
|
+
set -eu
|
|
6022
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
6023
|
+
case "$1" in
|
|
6024
|
+
list-panes)
|
|
6025
|
+
if [ "$2" = "-a" ]; then
|
|
6026
|
+
printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000044\\n'
|
|
6027
|
+
else
|
|
6028
|
+
printf '%%11\\tzsh\\tzsh\\n'
|
|
6029
|
+
fi
|
|
6030
|
+
;;
|
|
6031
|
+
split-window) printf '%%44\\n' ;;
|
|
6032
|
+
*) exit 0 ;;
|
|
6033
|
+
esac
|
|
6034
|
+
`, async ({ logPath }) => {
|
|
6035
|
+
let fsyncCalls = 0;
|
|
6036
|
+
assert.throws(() => withMockedFsyncSync((descriptor) => {
|
|
6037
|
+
fsyncCalls += 1;
|
|
6038
|
+
if (fsyncCalls === 2) {
|
|
6039
|
+
const error = new Error('parent fsync unavailable');
|
|
6040
|
+
error.code = 'EINVAL';
|
|
6041
|
+
throw error;
|
|
6042
|
+
}
|
|
6043
|
+
originalFsyncSync(descriptor);
|
|
6044
|
+
}, () => restoreStandaloneHudPane('%11', cwd)), /parent fsync unavailable/);
|
|
6045
|
+
assert.equal(fsyncCalls, 2);
|
|
6046
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
6047
|
+
assert.match(commands, /split-window/);
|
|
6048
|
+
assert.doesNotMatch(commands, /resize-pane|select-pane/);
|
|
6049
|
+
});
|
|
6050
|
+
}
|
|
6051
|
+
finally {
|
|
6052
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6053
|
+
}
|
|
6054
|
+
});
|
|
6055
|
+
it('reuses an existing standalone HUD pane across repeated restore calls', async () => {
|
|
6056
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-reuse-hud-'));
|
|
6057
|
+
try {
|
|
6058
|
+
await withMockTmuxFixture('omx-tmux-reuse-standalone-hud-', (logPath) => {
|
|
6059
|
+
const statePath = `${logPath}.state`;
|
|
6060
|
+
return `#!/bin/sh
|
|
4987
6061
|
set -eu
|
|
4988
6062
|
printf '%s\\n' "$*" >> "${logPath}"
|
|
4989
6063
|
case "\${1:-}" in
|
|
6064
|
+
list-panes)
|
|
6065
|
+
if [ "$2" = "-a" ]; then
|
|
6066
|
+
printf '%%11\t0\t2000000011\n'
|
|
6067
|
+
if [ -f "${statePath}" ]; then printf '%%44\t0\t2000000044\n'; fi
|
|
6068
|
+
else
|
|
6069
|
+
printf '%%11\tzsh\tzsh\n'
|
|
6070
|
+
if [ -f "${statePath}" ]; then
|
|
6071
|
+
printf "%%44\tnode\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%%11' /node /omx.js hud --watch\n"
|
|
6072
|
+
fi
|
|
6073
|
+
fi
|
|
6074
|
+
exit 0
|
|
6075
|
+
;;
|
|
4990
6076
|
split-window)
|
|
6077
|
+
: > "${statePath}"
|
|
4991
6078
|
echo "%44"
|
|
4992
6079
|
exit 0
|
|
4993
6080
|
;;
|
|
4994
|
-
run-shell|select-pane|resize-pane|set-hook)
|
|
6081
|
+
run-shell|select-pane|resize-pane|set-hook|kill-pane)
|
|
4995
6082
|
exit 0
|
|
4996
6083
|
;;
|
|
6084
|
+
show-option)
|
|
6085
|
+
if [ "$5" = "%11" ] && [ "$6" = "@omx_team_pane_owner_id" ]; then
|
|
6086
|
+
printf 'team:restore-replay\n'
|
|
6087
|
+
else
|
|
6088
|
+
exit 1
|
|
6089
|
+
fi
|
|
6090
|
+
;;
|
|
4997
6091
|
*)
|
|
4998
6092
|
exit 0
|
|
4999
6093
|
;;
|
|
5000
6094
|
esac
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
6095
|
+
`;
|
|
6096
|
+
}, async ({ logPath }) => {
|
|
6097
|
+
const firstPaneId = restoreStandaloneHudPane('%11', cwd, {
|
|
6098
|
+
expectedLeaderPanePid: 2000000011,
|
|
6099
|
+
expectedLeaderPaneOwnerId: 'team:restore-replay',
|
|
6100
|
+
});
|
|
6101
|
+
assert.equal(firstPaneId, '%44');
|
|
6102
|
+
const debtPath = join(cwd, '.omx', 'state', '.restored-hud-cleanup-debt.json');
|
|
6103
|
+
const crashWindowDebt = JSON.parse(await readFile(debtPath, 'utf-8'));
|
|
6104
|
+
assert.deepEqual(crashWindowDebt, {
|
|
6105
|
+
schema_version: 1,
|
|
6106
|
+
operation: 'restored_hud_cleanup',
|
|
6107
|
+
pane_id: '%44',
|
|
6108
|
+
pane_pid: 2000000044,
|
|
6109
|
+
leader_pane_id: '%11',
|
|
6110
|
+
leader_pane_pid: 2000000011,
|
|
6111
|
+
leader_pane_owner_id: 'team:restore-replay',
|
|
6112
|
+
hud_owner_leader_pane_id: '%11',
|
|
6113
|
+
});
|
|
6114
|
+
// The split may have survived a crash immediately before the config
|
|
6115
|
+
// transaction; finalization is deliberately explicit and post-commit.
|
|
6116
|
+
finalizeRestoredHudCleanupDebtSync(cwd, '%44', 2000000044);
|
|
6117
|
+
await assert.rejects(() => readFile(debtPath, 'utf-8'));
|
|
6118
|
+
const secondPaneId = restoreStandaloneHudPane('%11', cwd);
|
|
6119
|
+
assert.equal(firstPaneId, '%44');
|
|
6120
|
+
assert.equal(secondPaneId, '%44');
|
|
5007
6121
|
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
5008
|
-
|
|
5009
|
-
assert.
|
|
5010
|
-
assert.
|
|
6122
|
+
const splitCount = (tmuxLog.match(/(^|\n)split-window /g) ?? []).length;
|
|
6123
|
+
assert.equal(splitCount, 1);
|
|
6124
|
+
assert.doesNotMatch(tmuxLog, /kill-pane -t %44/);
|
|
6125
|
+
assert.match(tmuxLog, /list-panes -t %11 -F #\{pane_id\}\t#\{pane_current_command\}\t#\{pane_start_command\}/);
|
|
5011
6126
|
});
|
|
5012
6127
|
}
|
|
5013
6128
|
finally {
|
|
5014
|
-
process.argv = previousArgv;
|
|
5015
|
-
if (typeof previousEntryPath === 'string')
|
|
5016
|
-
process.env[OMX_ENTRY_PATH_ENV] = previousEntryPath;
|
|
5017
|
-
else
|
|
5018
|
-
delete process.env[OMX_ENTRY_PATH_ENV];
|
|
5019
|
-
if (typeof previousStartupCwd === 'string')
|
|
5020
|
-
process.env[OMX_STARTUP_CWD_ENV] = previousStartupCwd;
|
|
5021
|
-
else
|
|
5022
|
-
delete process.env[OMX_STARTUP_CWD_ENV];
|
|
5023
6129
|
await rm(cwd, { recursive: true, force: true });
|
|
5024
|
-
await rm(startupCwd, { recursive: true, force: true });
|
|
5025
6130
|
}
|
|
5026
6131
|
});
|
|
5027
|
-
it('
|
|
5028
|
-
const cwd = await mkdtemp(join(tmpdir(), 'omx-
|
|
5029
|
-
const previousArgv = process.argv;
|
|
6132
|
+
it('retains restored-HUD debt for a same-ID/PID non-HUD replacement after leader authorization', async () => {
|
|
6133
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-restored-hud-final-command-change-'));
|
|
5030
6134
|
try {
|
|
5031
|
-
await withMockTmuxFixture('omx-tmux-
|
|
6135
|
+
await withMockTmuxFixture('omx-tmux-restored-hud-final-command-change-', (logPath) => `#!/bin/sh
|
|
6136
|
+
set -eu
|
|
6137
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
6138
|
+
case "\${1:-}" in
|
|
6139
|
+
list-panes)
|
|
6140
|
+
if [ "$2" = '-a' ]; then
|
|
6141
|
+
printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000044\\n'
|
|
6142
|
+
elif [ -f "${logPath}.leader-authorized" ]; then
|
|
6143
|
+
printf "%%11\\tzsh\\tzsh\\n%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%%other' /node /omx.js hud --watch\\n"
|
|
6144
|
+
else
|
|
6145
|
+
printf "%%11\\tzsh\\tzsh\\n%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%%11' /node /omx.js hud --watch\\n"
|
|
6146
|
+
fi
|
|
6147
|
+
;;
|
|
6148
|
+
show-option)
|
|
6149
|
+
: > "${logPath}.leader-authorized"
|
|
6150
|
+
echo 'team:restore-final-command-change'
|
|
6151
|
+
;;
|
|
6152
|
+
esac
|
|
6153
|
+
`, async ({ logPath }) => {
|
|
6154
|
+
const debtPath = join(cwd, '.omx', 'state', '.restored-hud-cleanup-debt.json');
|
|
6155
|
+
await mkdir(dirname(debtPath), { recursive: true });
|
|
6156
|
+
await writeFile(debtPath, `${JSON.stringify({
|
|
6157
|
+
schema_version: 1,
|
|
6158
|
+
operation: 'restored_hud_cleanup',
|
|
6159
|
+
pane_id: '%44',
|
|
6160
|
+
pane_pid: 2000000044,
|
|
6161
|
+
leader_pane_id: '%11',
|
|
6162
|
+
leader_pane_pid: 2000000011,
|
|
6163
|
+
leader_pane_owner_id: 'team:restore-final-command-change',
|
|
6164
|
+
hud_owner_leader_pane_id: '%11',
|
|
6165
|
+
})}\n`);
|
|
6166
|
+
assert.throws(() => finalizeRestoredHudCleanupDebtSync(cwd, '%44', 2000000044), /restored_hud_cleanup_debt_unresolved:%44/);
|
|
6167
|
+
await readFile(debtPath, 'utf-8');
|
|
6168
|
+
const commands = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
|
|
6169
|
+
const topologyIndex = commands.findIndex((command) => command.startsWith('list-panes -t %11 -F '));
|
|
6170
|
+
const ownerIndex = commands.findIndex((command) => command === 'show-option -qv -p -t %11 @omx_team_pane_owner_id');
|
|
6171
|
+
assert.ok(ownerIndex >= 0 && topologyIndex > ownerIndex);
|
|
6172
|
+
assert.equal(commands.slice(topologyIndex + 1).filter((command) => command.startsWith('list-panes -a ')).length, 0);
|
|
6173
|
+
});
|
|
6174
|
+
}
|
|
6175
|
+
finally {
|
|
6176
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6177
|
+
}
|
|
6178
|
+
});
|
|
6179
|
+
it('replays only a pinned restored HUD with continuous leader owner and HUD identity', async () => {
|
|
6180
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-restored-hud-replay-'));
|
|
6181
|
+
try {
|
|
6182
|
+
await withMockTmuxFixture('omx-restored-hud-replay-', (logPath) => {
|
|
6183
|
+
const statePath = `${logPath}.killed`;
|
|
6184
|
+
return `#!/bin/sh
|
|
6185
|
+
set -eu
|
|
6186
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
6187
|
+
case "\${1:-}" in
|
|
6188
|
+
list-panes)
|
|
6189
|
+
if [ "$2" = "-a" ]; then
|
|
6190
|
+
printf '%%11\\t0\\t2000000011\\n'
|
|
6191
|
+
if [ ! -f "${statePath}" ]; then printf '%%44\\t0\\t2000000044\\n'; fi
|
|
6192
|
+
else
|
|
6193
|
+
printf '%%11\\tzsh\\tzsh\\n'
|
|
6194
|
+
if [ ! -f "${statePath}" ]; then printf "%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%%11' /node /omx.js hud --watch\\n"; fi
|
|
6195
|
+
fi
|
|
6196
|
+
;;
|
|
6197
|
+
show-option)
|
|
6198
|
+
if [ "$5" = "%11" ] && [ "$6" = "@omx_team_pane_owner_id" ]; then
|
|
6199
|
+
printf 'team:restore-replay\n'
|
|
6200
|
+
else
|
|
6201
|
+
exit 1
|
|
6202
|
+
fi
|
|
6203
|
+
;;
|
|
6204
|
+
kill-pane) : > "${statePath}" ;;
|
|
6205
|
+
esac
|
|
6206
|
+
`;
|
|
6207
|
+
}, async ({ logPath }) => {
|
|
6208
|
+
const debtPath = join(cwd, '.omx', 'state', '.restored-hud-cleanup-debt.json');
|
|
6209
|
+
await mkdir(dirname(debtPath), { recursive: true });
|
|
6210
|
+
await writeFile(debtPath, `${JSON.stringify({
|
|
6211
|
+
schema_version: 1,
|
|
6212
|
+
operation: 'restored_hud_cleanup',
|
|
6213
|
+
pane_id: '%44',
|
|
6214
|
+
pane_pid: 2000000044,
|
|
6215
|
+
leader_pane_id: '%11',
|
|
6216
|
+
leader_pane_pid: 2000000011,
|
|
6217
|
+
leader_pane_owner_id: 'team:restore-replay',
|
|
6218
|
+
hud_owner_leader_pane_id: '%11',
|
|
6219
|
+
})}\n`);
|
|
6220
|
+
reconcileRestoredHudCleanupDebtSync(cwd);
|
|
6221
|
+
await assert.rejects(() => readFile(debtPath, 'utf-8'));
|
|
6222
|
+
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
6223
|
+
assert.match(tmuxLog, /kill-pane -t %44/);
|
|
6224
|
+
assert.match(tmuxLog, /list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}\nlist-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}\nshow-option -qv -p -t %11 @omx_team_pane_owner_id\nlist-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}\nlist-panes -t %11 -F #\{pane_id\}\t#\{pane_current_command\}\t#\{pane_start_command\}/);
|
|
6225
|
+
assert.match(tmuxLog, /list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}\nlist-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}\nshow-option -qv -p -t %11 @omx_team_pane_owner_id\nlist-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}\nlist-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}\nkill-pane -t %44\nlist-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}/);
|
|
6226
|
+
});
|
|
6227
|
+
}
|
|
6228
|
+
finally {
|
|
6229
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6230
|
+
}
|
|
6231
|
+
});
|
|
6232
|
+
it('restores standalone HUD panes from the live leader pane cwd with explicit session ownership', async () => {
|
|
6233
|
+
const teamLaunchCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-team-launch-cwd-'));
|
|
6234
|
+
const leaderPaneCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-leader-pane-cwd-'));
|
|
6235
|
+
try {
|
|
6236
|
+
await withMockTmuxFixture('omx-tmux-leader-cwd-standalone-hud-', (logPath) => `#!/bin/sh
|
|
5032
6237
|
set -eu
|
|
5033
6238
|
printf '%s\\n' "$*" >> "${logPath}"
|
|
5034
6239
|
case "\${1:-}" in
|
|
6240
|
+
display-message)
|
|
6241
|
+
case "$*" in
|
|
6242
|
+
*"#{pane_current_path}"*)
|
|
6243
|
+
echo "${leaderPaneCwd}"
|
|
6244
|
+
;;
|
|
6245
|
+
esac
|
|
6246
|
+
exit 0
|
|
6247
|
+
;;
|
|
5035
6248
|
split-window)
|
|
5036
6249
|
echo "%44"
|
|
5037
6250
|
exit 0
|
|
@@ -5044,159 +6257,991 @@ case "\${1:-}" in
|
|
|
5044
6257
|
;;
|
|
5045
6258
|
esac
|
|
5046
6259
|
`, async ({ logPath }) => {
|
|
5047
|
-
|
|
5048
|
-
|
|
6260
|
+
const paneId = restoreStandaloneHudPane('%11', teamLaunchCwd, {
|
|
6261
|
+
sessionId: 'current-session-for-hud',
|
|
6262
|
+
});
|
|
5049
6263
|
assert.equal(paneId, '%44');
|
|
5050
6264
|
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
5051
|
-
assert.match(tmuxLog, /
|
|
5052
|
-
assert.
|
|
5053
|
-
assert.
|
|
6265
|
+
assert.match(tmuxLog, /display-message -p -t %11 #\{pane_current_path\}/);
|
|
6266
|
+
assert.match(tmuxLog, new RegExp(`split-window -v -l ${HUD_TMUX_TEAM_HEIGHT_LINES} -t %11 -d -P -F #\\{pane_id\\} -c ${escapeRegExp(leaderPaneCwd)} `));
|
|
6267
|
+
assert.doesNotMatch(tmuxLog, new RegExp(`split-window .* -c ${escapeRegExp(teamLaunchCwd)} `));
|
|
6268
|
+
assert.match(tmuxLog, /exec env OMX_SESSION_ID='current-session-for-hud' OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%11' .*hud --watch/);
|
|
5054
6269
|
});
|
|
5055
6270
|
}
|
|
5056
6271
|
finally {
|
|
5057
|
-
|
|
5058
|
-
await rm(
|
|
6272
|
+
await rm(teamLaunchCwd, { recursive: true, force: true });
|
|
6273
|
+
await rm(leaderPaneCwd, { recursive: true, force: true });
|
|
5059
6274
|
}
|
|
5060
6275
|
});
|
|
5061
|
-
it('
|
|
5062
|
-
const
|
|
5063
|
-
const
|
|
6276
|
+
it('falls back to the team launch cwd when the live leader pane cwd is unusable', async () => {
|
|
6277
|
+
const teamLaunchCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-team-launch-cwd-fallback-'));
|
|
6278
|
+
const deletedLeaderPaneCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-deleted-leader-pane-cwd-'));
|
|
6279
|
+
await rm(deletedLeaderPaneCwd, { recursive: true, force: true });
|
|
5064
6280
|
try {
|
|
5065
|
-
await withMockTmuxFixture('omx-tmux-
|
|
6281
|
+
await withMockTmuxFixture('omx-tmux-deleted-leader-cwd-standalone-hud-', (logPath) => `#!/bin/sh
|
|
5066
6282
|
set -eu
|
|
5067
6283
|
printf '%s\\n' "$*" >> "${logPath}"
|
|
5068
6284
|
case "\${1:-}" in
|
|
6285
|
+
display-message)
|
|
6286
|
+
case "$*" in
|
|
6287
|
+
*"#{pane_current_path}"*)
|
|
6288
|
+
echo "${deletedLeaderPaneCwd}"
|
|
6289
|
+
;;
|
|
6290
|
+
esac
|
|
6291
|
+
exit 0
|
|
6292
|
+
;;
|
|
5069
6293
|
split-window)
|
|
5070
|
-
|
|
6294
|
+
case "$*" in
|
|
6295
|
+
*"-c ${teamLaunchCwd} "*)
|
|
6296
|
+
echo "%44"
|
|
6297
|
+
exit 0
|
|
6298
|
+
;;
|
|
6299
|
+
*"-c ${deletedLeaderPaneCwd} "*)
|
|
6300
|
+
echo "deleted cwd should not be used" >&2
|
|
6301
|
+
exit 1
|
|
6302
|
+
;;
|
|
6303
|
+
*)
|
|
6304
|
+
echo "unexpected cwd: $*" >&2
|
|
6305
|
+
exit 1
|
|
6306
|
+
;;
|
|
6307
|
+
esac
|
|
6308
|
+
;;
|
|
6309
|
+
run-shell|select-pane|resize-pane|set-hook)
|
|
6310
|
+
exit 0
|
|
6311
|
+
;;
|
|
6312
|
+
*)
|
|
6313
|
+
exit 0
|
|
6314
|
+
;;
|
|
6315
|
+
esac
|
|
6316
|
+
`, async ({ logPath }) => {
|
|
6317
|
+
const paneId = restoreStandaloneHudPane('%11', teamLaunchCwd, {
|
|
6318
|
+
sessionId: 'current-session-for-hud',
|
|
6319
|
+
});
|
|
6320
|
+
assert.equal(paneId, '%44');
|
|
6321
|
+
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
6322
|
+
assert.match(tmuxLog, /display-message -p -t %11 #\{pane_current_path\}/);
|
|
6323
|
+
assert.match(tmuxLog, new RegExp(`split-window -v -l ${HUD_TMUX_TEAM_HEIGHT_LINES} -t %11 -d -P -F #\\{pane_id\\} -c ${escapeRegExp(teamLaunchCwd)} `));
|
|
6324
|
+
assert.doesNotMatch(tmuxLog, new RegExp(`split-window .* -c ${escapeRegExp(deletedLeaderPaneCwd)} `));
|
|
6325
|
+
assert.match(tmuxLog, /select-pane -t %11/);
|
|
6326
|
+
});
|
|
6327
|
+
}
|
|
6328
|
+
finally {
|
|
6329
|
+
await rm(teamLaunchCwd, { recursive: true, force: true });
|
|
6330
|
+
}
|
|
6331
|
+
});
|
|
6332
|
+
it('keeps MSYS drive-style live leader cwd candidates without raw stat prefiltering', async () => {
|
|
6333
|
+
const liveLeaderCwd = '/c/live';
|
|
6334
|
+
const fallbackCwd = 'C:\\fallback';
|
|
6335
|
+
const fakeCygpathDir = await mkdtemp(join(tmpdir(), 'omx-msys-live-cygpath-'));
|
|
6336
|
+
const previousPath = process.env.PATH;
|
|
6337
|
+
const previousMsystem = process.env.MSYSTEM;
|
|
6338
|
+
const previousOstype = process.env.OSTYPE;
|
|
6339
|
+
const previousWsl = process.env.WSL_DISTRO_NAME;
|
|
6340
|
+
const previousWslInterop = process.env.WSL_INTEROP;
|
|
6341
|
+
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
6342
|
+
const statCalls = [];
|
|
6343
|
+
try {
|
|
6344
|
+
const cygpathPath = join(fakeCygpathDir, 'cygpath');
|
|
6345
|
+
await writeFile(cygpathPath, '#!/bin/sh\nprintf "%s\\n" "$2"\n');
|
|
6346
|
+
await chmod(cygpathPath, 0o755);
|
|
6347
|
+
process.env.PATH = `${fakeCygpathDir}:${previousPath ?? ''}`;
|
|
6348
|
+
process.env.MSYSTEM = 'MINGW64';
|
|
6349
|
+
process.env.OSTYPE = 'msys';
|
|
6350
|
+
delete process.env.WSL_DISTRO_NAME;
|
|
6351
|
+
delete process.env.WSL_INTEROP;
|
|
6352
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
6353
|
+
await withMockTmuxFixture('omx-tmux-msys-live-cwd-standalone-hud-', (logPath) => `#!/bin/sh
|
|
6354
|
+
set -eu
|
|
6355
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
6356
|
+
case "\${1:-}" in
|
|
6357
|
+
display-message)
|
|
6358
|
+
case "$*" in
|
|
6359
|
+
*"#{pane_current_path}"*)
|
|
6360
|
+
echo "${liveLeaderCwd}"
|
|
6361
|
+
;;
|
|
6362
|
+
esac
|
|
6363
|
+
exit 0
|
|
6364
|
+
;;
|
|
6365
|
+
split-window)
|
|
6366
|
+
case "$*" in
|
|
6367
|
+
*"-c ${liveLeaderCwd} "*)
|
|
6368
|
+
echo "%44"
|
|
6369
|
+
exit 0
|
|
6370
|
+
;;
|
|
6371
|
+
*"-c ${fallbackCwd} "*)
|
|
6372
|
+
echo "fallback cwd should not be used when MSYS live cwd succeeds" >&2
|
|
6373
|
+
exit 1
|
|
6374
|
+
;;
|
|
6375
|
+
*)
|
|
6376
|
+
echo "unexpected cwd: $*" >&2
|
|
6377
|
+
exit 1
|
|
6378
|
+
;;
|
|
6379
|
+
esac
|
|
6380
|
+
;;
|
|
6381
|
+
run-shell|select-pane|resize-pane|set-hook)
|
|
6382
|
+
exit 0
|
|
6383
|
+
;;
|
|
6384
|
+
*)
|
|
5071
6385
|
exit 0
|
|
5072
6386
|
;;
|
|
5073
|
-
|
|
5074
|
-
|
|
6387
|
+
esac
|
|
6388
|
+
`, async ({ logPath }) => {
|
|
6389
|
+
const paneId = withMockedStatSync(((pathLike) => {
|
|
6390
|
+
const value = String(pathLike);
|
|
6391
|
+
statCalls.push(value);
|
|
6392
|
+
if (value === fallbackCwd)
|
|
6393
|
+
return { isDirectory: () => true };
|
|
6394
|
+
return { isDirectory: () => false };
|
|
6395
|
+
}), () => restoreStandaloneHudPane('%11', fallbackCwd, { sessionId: 'current-session-for-hud' }));
|
|
6396
|
+
assert.equal(paneId, '%44');
|
|
6397
|
+
assert.equal(statCalls.includes(liveLeaderCwd), false);
|
|
6398
|
+
assert.equal(statCalls.includes(fallbackCwd), true);
|
|
6399
|
+
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
6400
|
+
assert.match(tmuxLog, /display-message -p -t %11 #\{pane_current_path\}/);
|
|
6401
|
+
assert.match(tmuxLog, new RegExp(`split-window -v -l ${HUD_TMUX_TEAM_HEIGHT_LINES} -t %11 -d -P -F #\\{pane_id\\} -c ${escapeRegExp(liveLeaderCwd)} `));
|
|
6402
|
+
assert.doesNotMatch(tmuxLog, new RegExp(`split-window .* -c ${escapeRegExp(fallbackCwd)} `));
|
|
6403
|
+
assert.match(tmuxLog, /select-pane -t %11/);
|
|
6404
|
+
});
|
|
6405
|
+
}
|
|
6406
|
+
finally {
|
|
6407
|
+
if (originalPlatform)
|
|
6408
|
+
Object.defineProperty(process, 'platform', originalPlatform);
|
|
6409
|
+
if (typeof previousPath === 'string')
|
|
6410
|
+
process.env.PATH = previousPath;
|
|
6411
|
+
else
|
|
6412
|
+
delete process.env.PATH;
|
|
6413
|
+
if (typeof previousMsystem === 'string')
|
|
6414
|
+
process.env.MSYSTEM = previousMsystem;
|
|
6415
|
+
else
|
|
6416
|
+
delete process.env.MSYSTEM;
|
|
6417
|
+
if (typeof previousOstype === 'string')
|
|
6418
|
+
process.env.OSTYPE = previousOstype;
|
|
6419
|
+
else
|
|
6420
|
+
delete process.env.OSTYPE;
|
|
6421
|
+
if (typeof previousWsl === 'string')
|
|
6422
|
+
process.env.WSL_DISTRO_NAME = previousWsl;
|
|
6423
|
+
else
|
|
6424
|
+
delete process.env.WSL_DISTRO_NAME;
|
|
6425
|
+
if (typeof previousWslInterop === 'string')
|
|
6426
|
+
process.env.WSL_INTEROP = previousWslInterop;
|
|
6427
|
+
else
|
|
6428
|
+
delete process.env.WSL_INTEROP;
|
|
6429
|
+
await rm(fakeCygpathDir, { recursive: true, force: true });
|
|
6430
|
+
}
|
|
6431
|
+
});
|
|
6432
|
+
it('falls back after an unusable MSYS drive-style live leader cwd split attempt fails', async () => {
|
|
6433
|
+
const liveLeaderCwd = '/c/deleted-live';
|
|
6434
|
+
const fallbackCwd = 'C:\\fallback';
|
|
6435
|
+
const fakeCygpathDir = await mkdtemp(join(tmpdir(), 'omx-msys-deleted-live-cygpath-'));
|
|
6436
|
+
const previousPath = process.env.PATH;
|
|
6437
|
+
const previousMsystem = process.env.MSYSTEM;
|
|
6438
|
+
const previousOstype = process.env.OSTYPE;
|
|
6439
|
+
const previousWsl = process.env.WSL_DISTRO_NAME;
|
|
6440
|
+
const previousWslInterop = process.env.WSL_INTEROP;
|
|
6441
|
+
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
6442
|
+
const statCalls = [];
|
|
6443
|
+
try {
|
|
6444
|
+
const cygpathPath = join(fakeCygpathDir, 'cygpath');
|
|
6445
|
+
await writeFile(cygpathPath, '#!/bin/sh\nprintf "%s\\n" "$2"\n');
|
|
6446
|
+
await chmod(cygpathPath, 0o755);
|
|
6447
|
+
process.env.PATH = `${fakeCygpathDir}:${previousPath ?? ''}`;
|
|
6448
|
+
process.env.MSYSTEM = 'MINGW64';
|
|
6449
|
+
process.env.OSTYPE = 'msys';
|
|
6450
|
+
delete process.env.WSL_DISTRO_NAME;
|
|
6451
|
+
delete process.env.WSL_INTEROP;
|
|
6452
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
6453
|
+
await withMockTmuxFixture('omx-tmux-msys-deleted-live-cwd-standalone-hud-', (logPath) => `#!/bin/sh
|
|
6454
|
+
set -eu
|
|
6455
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
6456
|
+
case "\${1:-}" in
|
|
6457
|
+
display-message)
|
|
6458
|
+
case "$*" in
|
|
6459
|
+
*"#{pane_current_path}"*)
|
|
6460
|
+
echo "${liveLeaderCwd}"
|
|
6461
|
+
;;
|
|
6462
|
+
esac
|
|
6463
|
+
exit 0
|
|
6464
|
+
;;
|
|
6465
|
+
split-window)
|
|
6466
|
+
case "$*" in
|
|
6467
|
+
*"-c ${liveLeaderCwd} "*)
|
|
6468
|
+
echo "deleted live cwd cannot be used" >&2
|
|
6469
|
+
exit 1
|
|
6470
|
+
;;
|
|
6471
|
+
*"-c ${fallbackCwd} "*)
|
|
6472
|
+
echo "%44"
|
|
6473
|
+
exit 0
|
|
6474
|
+
;;
|
|
6475
|
+
*)
|
|
6476
|
+
echo "unexpected cwd: $*" >&2
|
|
6477
|
+
exit 1
|
|
6478
|
+
;;
|
|
6479
|
+
esac
|
|
6480
|
+
;;
|
|
6481
|
+
run-shell|select-pane|resize-pane|set-hook)
|
|
6482
|
+
exit 0
|
|
6483
|
+
;;
|
|
6484
|
+
*)
|
|
6485
|
+
exit 0
|
|
6486
|
+
;;
|
|
6487
|
+
esac
|
|
6488
|
+
`, async ({ logPath }) => {
|
|
6489
|
+
const paneId = withMockedStatSync(((pathLike) => {
|
|
6490
|
+
const value = String(pathLike);
|
|
6491
|
+
statCalls.push(value);
|
|
6492
|
+
if (value === fallbackCwd)
|
|
6493
|
+
return { isDirectory: () => true };
|
|
6494
|
+
return { isDirectory: () => false };
|
|
6495
|
+
}), () => restoreStandaloneHudPane('%11', fallbackCwd, { sessionId: 'current-session-for-hud' }));
|
|
6496
|
+
assert.equal(paneId, '%44');
|
|
6497
|
+
assert.equal(statCalls.includes(liveLeaderCwd), false);
|
|
6498
|
+
assert.equal(statCalls.includes(fallbackCwd), true);
|
|
6499
|
+
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
6500
|
+
const liveAttemptIndex = tmuxLog.indexOf(`-c ${liveLeaderCwd} `);
|
|
6501
|
+
const fallbackAttemptIndex = tmuxLog.indexOf(`-c ${fallbackCwd} `);
|
|
6502
|
+
assert.ok(liveAttemptIndex >= 0, 'deleted MSYS live cwd should still be attempted');
|
|
6503
|
+
assert.ok(fallbackAttemptIndex > liveAttemptIndex, 'fallback should be attempted after live split failure');
|
|
6504
|
+
assert.match(tmuxLog, /select-pane -t %11/);
|
|
6505
|
+
});
|
|
6506
|
+
}
|
|
6507
|
+
finally {
|
|
6508
|
+
if (originalPlatform)
|
|
6509
|
+
Object.defineProperty(process, 'platform', originalPlatform);
|
|
6510
|
+
if (typeof previousPath === 'string')
|
|
6511
|
+
process.env.PATH = previousPath;
|
|
6512
|
+
else
|
|
6513
|
+
delete process.env.PATH;
|
|
6514
|
+
if (typeof previousMsystem === 'string')
|
|
6515
|
+
process.env.MSYSTEM = previousMsystem;
|
|
6516
|
+
else
|
|
6517
|
+
delete process.env.MSYSTEM;
|
|
6518
|
+
if (typeof previousOstype === 'string')
|
|
6519
|
+
process.env.OSTYPE = previousOstype;
|
|
6520
|
+
else
|
|
6521
|
+
delete process.env.OSTYPE;
|
|
6522
|
+
if (typeof previousWsl === 'string')
|
|
6523
|
+
process.env.WSL_DISTRO_NAME = previousWsl;
|
|
6524
|
+
else
|
|
6525
|
+
delete process.env.WSL_DISTRO_NAME;
|
|
6526
|
+
if (typeof previousWslInterop === 'string')
|
|
6527
|
+
process.env.WSL_INTEROP = previousWslInterop;
|
|
6528
|
+
else
|
|
6529
|
+
delete process.env.WSL_INTEROP;
|
|
6530
|
+
await rm(fakeCygpathDir, { recursive: true, force: true });
|
|
6531
|
+
}
|
|
6532
|
+
});
|
|
6533
|
+
it('restores standalone HUD panes with an absolute OMX entry path after cwd drift', async () => {
|
|
6534
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-relative-hud-'));
|
|
6535
|
+
const startupCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-relative-start-'));
|
|
6536
|
+
const previousEntryPath = process.env[OMX_ENTRY_PATH_ENV];
|
|
6537
|
+
const previousStartupCwd = process.env[OMX_STARTUP_CWD_ENV];
|
|
6538
|
+
const previousArgv = process.argv;
|
|
6539
|
+
try {
|
|
6540
|
+
const launcherDir = join(startupCwd, 'dist', 'cli');
|
|
6541
|
+
const launcherPath = join(launcherDir, 'omx.js');
|
|
6542
|
+
await mkdir(launcherDir, { recursive: true });
|
|
6543
|
+
await writeFile(launcherPath, '#!/usr/bin/env node\n');
|
|
6544
|
+
await withMockTmuxFixture('omx-tmux-relative-standalone-hud-', (logPath) => `#!/bin/sh
|
|
6545
|
+
set -eu
|
|
6546
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
6547
|
+
case "\${1:-}" in
|
|
6548
|
+
split-window)
|
|
6549
|
+
echo "%44"
|
|
6550
|
+
exit 0
|
|
6551
|
+
;;
|
|
6552
|
+
run-shell|select-pane|resize-pane|set-hook)
|
|
6553
|
+
exit 0
|
|
6554
|
+
;;
|
|
6555
|
+
*)
|
|
6556
|
+
exit 0
|
|
6557
|
+
;;
|
|
6558
|
+
esac
|
|
6559
|
+
`, async ({ logPath }) => {
|
|
6560
|
+
delete process.env[OMX_ENTRY_PATH_ENV];
|
|
6561
|
+
process.env[OMX_STARTUP_CWD_ENV] = startupCwd;
|
|
6562
|
+
process.argv = [previousArgv[0] || 'node', 'dist/cli/omx.js'];
|
|
6563
|
+
const paneId = restoreStandaloneHudPane('%11', cwd);
|
|
6564
|
+
assert.equal(paneId, '%44');
|
|
6565
|
+
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
6566
|
+
assert.match(tmuxLog, new RegExp(escapeRegExp(launcherPath)));
|
|
6567
|
+
assert.doesNotMatch(tmuxLog, /'dist\/cli\/omx\.js' hud --watch/);
|
|
6568
|
+
assert.match(tmuxLog, /exec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%11' .*hud --watch/);
|
|
6569
|
+
});
|
|
6570
|
+
}
|
|
6571
|
+
finally {
|
|
6572
|
+
process.argv = previousArgv;
|
|
6573
|
+
if (typeof previousEntryPath === 'string')
|
|
6574
|
+
process.env[OMX_ENTRY_PATH_ENV] = previousEntryPath;
|
|
6575
|
+
else
|
|
6576
|
+
delete process.env[OMX_ENTRY_PATH_ENV];
|
|
6577
|
+
if (typeof previousStartupCwd === 'string')
|
|
6578
|
+
process.env[OMX_STARTUP_CWD_ENV] = previousStartupCwd;
|
|
6579
|
+
else
|
|
6580
|
+
delete process.env[OMX_STARTUP_CWD_ENV];
|
|
6581
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6582
|
+
await rm(startupCwd, { recursive: true, force: true });
|
|
6583
|
+
}
|
|
6584
|
+
});
|
|
6585
|
+
it('restores standalone HUD panes with the packaged CLI entry when argv1 is not the OMX CLI', async () => {
|
|
6586
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-noncli-hud-'));
|
|
6587
|
+
const previousArgv = process.argv;
|
|
6588
|
+
try {
|
|
6589
|
+
await withMockTmuxFixture('omx-tmux-noncli-standalone-hud-', (logPath) => `#!/bin/sh
|
|
6590
|
+
set -eu
|
|
6591
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
6592
|
+
case "\${1:-}" in
|
|
6593
|
+
split-window)
|
|
6594
|
+
echo "%44"
|
|
6595
|
+
exit 0
|
|
6596
|
+
;;
|
|
6597
|
+
run-shell|select-pane|resize-pane|set-hook)
|
|
6598
|
+
exit 0
|
|
6599
|
+
;;
|
|
6600
|
+
*)
|
|
6601
|
+
exit 0
|
|
6602
|
+
;;
|
|
6603
|
+
esac
|
|
6604
|
+
`, async ({ logPath }) => {
|
|
6605
|
+
process.argv = [previousArgv[0] || 'node', '/tmp/codex-host-binary'];
|
|
6606
|
+
const paneId = restoreStandaloneHudPane('%11', cwd);
|
|
6607
|
+
assert.equal(paneId, '%44');
|
|
6608
|
+
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
6609
|
+
assert.match(tmuxLog, /dist\/cli\/omx\.js' hud --watch/);
|
|
6610
|
+
assert.doesNotMatch(tmuxLog, /\/tmp\/codex-host-binary' hud --watch/);
|
|
6611
|
+
assert.match(tmuxLog, /exec env OMX_TMUX_HUD_OWNER=1 .*hud --watch/);
|
|
6612
|
+
});
|
|
6613
|
+
}
|
|
6614
|
+
finally {
|
|
6615
|
+
process.argv = previousArgv;
|
|
6616
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6617
|
+
}
|
|
6618
|
+
});
|
|
6619
|
+
it('restores standalone HUD panes with OMX_ROOT forwarded and shell-escaped', async () => {
|
|
6620
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-root-hud-'));
|
|
6621
|
+
const previousOmxRoot = process.env.OMX_ROOT;
|
|
6622
|
+
try {
|
|
6623
|
+
await withMockTmuxFixture('omx-tmux-root-standalone-hud-', (logPath) => `#!/bin/sh
|
|
6624
|
+
set -eu
|
|
6625
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
6626
|
+
case "\${1:-}" in
|
|
6627
|
+
split-window)
|
|
6628
|
+
echo "%44"
|
|
6629
|
+
exit 0
|
|
6630
|
+
;;
|
|
6631
|
+
run-shell|select-pane|resize-pane|set-hook)
|
|
6632
|
+
exit 0
|
|
6633
|
+
;;
|
|
6634
|
+
*)
|
|
6635
|
+
exit 0
|
|
6636
|
+
;;
|
|
6637
|
+
esac
|
|
6638
|
+
`, async ({ logPath }) => {
|
|
6639
|
+
process.env.OMX_ROOT = "/tmp/boxed root/it's/$(literal)";
|
|
6640
|
+
const paneId = restoreStandaloneHudPane('%11', cwd);
|
|
6641
|
+
assert.equal(paneId, '%44');
|
|
6642
|
+
const tmuxLog = await readFile(logPath, 'utf-8');
|
|
6643
|
+
assert.match(tmuxLog, /exec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%11' OMX_ROOT='\/tmp\/boxed root\/it'\\''s\/\$\(literal\)' .*hud --watch/);
|
|
6644
|
+
});
|
|
6645
|
+
}
|
|
6646
|
+
finally {
|
|
6647
|
+
if (typeof previousOmxRoot === 'string')
|
|
6648
|
+
process.env.OMX_ROOT = previousOmxRoot;
|
|
6649
|
+
else
|
|
6650
|
+
delete process.env.OMX_ROOT;
|
|
6651
|
+
await rm(cwd, { recursive: true, force: true });
|
|
6652
|
+
}
|
|
6653
|
+
});
|
|
6654
|
+
});
|
|
6655
|
+
describe('dismissTrustPromptIfPresent capture shape', () => {
|
|
6656
|
+
it('uses visible capture-pane argv without tail flags', async () => {
|
|
6657
|
+
const previousAutoTrust = process.env.OMX_TEAM_AUTO_TRUST;
|
|
6658
|
+
delete process.env.OMX_TEAM_AUTO_TRUST;
|
|
6659
|
+
try {
|
|
6660
|
+
await withMockTmuxFixture('omx-tmux-dismiss-trust-visible-capture-', (logPath) => `#!/bin/sh
|
|
6661
|
+
set -eu
|
|
6662
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
6663
|
+
case "$1" in
|
|
6664
|
+
capture-pane)
|
|
6665
|
+
cat <<'EOF'
|
|
6666
|
+
Do you trust the contents of this directory?
|
|
6667
|
+
Press enter to continue
|
|
6668
|
+
EOF
|
|
6669
|
+
exit 0
|
|
6670
|
+
;;
|
|
6671
|
+
send-keys)
|
|
6672
|
+
exit 0
|
|
6673
|
+
;;
|
|
6674
|
+
*)
|
|
6675
|
+
exit 0
|
|
6676
|
+
;;
|
|
6677
|
+
esac
|
|
6678
|
+
`, async ({ logPath }) => {
|
|
6679
|
+
assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), true);
|
|
6680
|
+
const log = await readFile(logPath, 'utf-8');
|
|
6681
|
+
assert.match(log, /capture-pane -t omx-team-x:1 -p/);
|
|
6682
|
+
assert.doesNotMatch(log, /capture-pane -t omx-team-x:1 -p -S/);
|
|
6683
|
+
});
|
|
6684
|
+
}
|
|
6685
|
+
finally {
|
|
6686
|
+
if (typeof previousAutoTrust === 'string')
|
|
6687
|
+
process.env.OMX_TEAM_AUTO_TRUST = previousAutoTrust;
|
|
6688
|
+
else
|
|
6689
|
+
delete process.env.OMX_TEAM_AUTO_TRUST;
|
|
6690
|
+
}
|
|
6691
|
+
});
|
|
6692
|
+
it('does not send trust controls to a pane ID reused after capture', async () => {
|
|
6693
|
+
const previousAutoTrust = process.env.OMX_TEAM_AUTO_TRUST;
|
|
6694
|
+
delete process.env.OMX_TEAM_AUTO_TRUST;
|
|
6695
|
+
try {
|
|
6696
|
+
await withMockTmuxFixture('omx-tmux-dismiss-trust-pid-reuse-', (logPath) => `#!/bin/sh
|
|
6697
|
+
set -eu
|
|
6698
|
+
state_dir="$(dirname "${logPath}")"
|
|
6699
|
+
proof_count_file="$state_dir/proof-count"
|
|
6700
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
6701
|
+
case "$1" in
|
|
6702
|
+
list-panes)
|
|
6703
|
+
count=0
|
|
6704
|
+
if [ -f "$proof_count_file" ]; then count=$(cat "$proof_count_file"); fi
|
|
6705
|
+
count=$((count + 1))
|
|
6706
|
+
printf '%s' "$count" > "$proof_count_file"
|
|
6707
|
+
if [ "$count" -eq 1 ]; then
|
|
6708
|
+
printf '%%9\t0\t2000000001\n'
|
|
6709
|
+
elif [ "$count" -le 2 ]; then
|
|
6710
|
+
printf '%%9\t0\t2000000001\n'
|
|
6711
|
+
else
|
|
6712
|
+
printf '%%9\t0\t2000000002\n'
|
|
6713
|
+
fi
|
|
6714
|
+
;;
|
|
6715
|
+
show-option)
|
|
6716
|
+
printf 'team:test\n'
|
|
6717
|
+
;;
|
|
6718
|
+
capture-pane)
|
|
6719
|
+
cat <<'EOF'
|
|
6720
|
+
Do you trust the contents of this directory?
|
|
6721
|
+
Press enter to continue
|
|
6722
|
+
EOF
|
|
6723
|
+
;;
|
|
6724
|
+
-V) exit 0 ;;
|
|
6725
|
+
send-keys) exit 1 ;;
|
|
6726
|
+
*) exit 1 ;;
|
|
6727
|
+
esac
|
|
6728
|
+
`, async ({ logPath }) => {
|
|
6729
|
+
assert.throws(() => dismissTrustPromptIfPresent('ignored-session', 1, '%9', 2000000001, 'team:test', '%10'), /tmux pane identity changed: %9/);
|
|
6730
|
+
const log = await readFile(logPath, 'utf-8');
|
|
6731
|
+
assert.match(log, /capture-pane -t %9 -p/);
|
|
6732
|
+
assert.doesNotMatch(log, /send-keys -t %9/);
|
|
6733
|
+
});
|
|
6734
|
+
}
|
|
6735
|
+
finally {
|
|
6736
|
+
if (typeof previousAutoTrust === 'string')
|
|
6737
|
+
process.env.OMX_TEAM_AUTO_TRUST = previousAutoTrust;
|
|
6738
|
+
else
|
|
6739
|
+
delete process.env.OMX_TEAM_AUTO_TRUST;
|
|
6740
|
+
}
|
|
6741
|
+
});
|
|
6742
|
+
it('fails closed without emitting trust controls when an explicit pane lacks a PID', async () => {
|
|
6743
|
+
await withMockTmuxFixture('omx-tmux-dismiss-trust-missing-pid-', (logPath) => `#!/bin/sh
|
|
6744
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
6745
|
+
[ "$1" = "-V" ] && exit 0
|
|
6746
|
+
exit 1
|
|
6747
|
+
`, async ({ logPath }) => {
|
|
6748
|
+
assert.equal(dismissTrustPromptIfPresent('ignored-session', 1, '%9'), false);
|
|
6749
|
+
const log = await readFile(logPath, 'utf-8');
|
|
6750
|
+
assert.doesNotMatch(log, /capture-pane|send-keys|list-panes/);
|
|
6751
|
+
});
|
|
6752
|
+
});
|
|
6753
|
+
});
|
|
6754
|
+
describe('dismissTrustPromptIfPresent', () => {
|
|
6755
|
+
it('returns false when tmux is unavailable', () => {
|
|
6756
|
+
withEmptyPath(() => {
|
|
6757
|
+
assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), false);
|
|
6758
|
+
});
|
|
6759
|
+
});
|
|
6760
|
+
it('returns false when OMX_TEAM_AUTO_TRUST is disabled', () => {
|
|
6761
|
+
const prev = process.env.OMX_TEAM_AUTO_TRUST;
|
|
6762
|
+
process.env.OMX_TEAM_AUTO_TRUST = '0';
|
|
6763
|
+
try {
|
|
6764
|
+
assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), false);
|
|
6765
|
+
}
|
|
6766
|
+
finally {
|
|
6767
|
+
if (typeof prev === 'string')
|
|
6768
|
+
process.env.OMX_TEAM_AUTO_TRUST = prev;
|
|
6769
|
+
else
|
|
6770
|
+
delete process.env.OMX_TEAM_AUTO_TRUST;
|
|
6771
|
+
}
|
|
6772
|
+
});
|
|
6773
|
+
it('returns false when OMX_TEAM_AUTO_TRUST is unset (auto-trust enabled) but tmux unavailable', () => {
|
|
6774
|
+
const prev = process.env.OMX_TEAM_AUTO_TRUST;
|
|
6775
|
+
delete process.env.OMX_TEAM_AUTO_TRUST;
|
|
6776
|
+
try {
|
|
6777
|
+
withEmptyPath(() => {
|
|
6778
|
+
assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), false);
|
|
6779
|
+
});
|
|
6780
|
+
}
|
|
6781
|
+
finally {
|
|
6782
|
+
if (typeof prev === 'string')
|
|
6783
|
+
process.env.OMX_TEAM_AUTO_TRUST = prev;
|
|
6784
|
+
}
|
|
6785
|
+
});
|
|
6786
|
+
});
|
|
6787
|
+
describe('isWorkerAlive', () => {
|
|
6788
|
+
it('does not require pane_current_command to match "codex"', () => {
|
|
6789
|
+
// This was a real failure mode: tmux reports pane_current_command=node for the Codex TUI,
|
|
6790
|
+
// which caused workers to be treated as dead and the leader to clean up state too early.
|
|
6791
|
+
withEmptyPath(() => {
|
|
6792
|
+
assert.equal(isWorkerAlive('omx-team-x', 1), false);
|
|
6793
|
+
});
|
|
6794
|
+
});
|
|
6795
|
+
it('treats blank persisted pane IDs as absent and uses the compatibility target', async () => {
|
|
6796
|
+
await withMockTmuxFixture('omx-blank-pane-fallback-', () => `#!/bin/sh
|
|
6797
|
+
if [ "$1" = "list-panes" ] && [ "$2" = "-t" ]; then printf '0 %s\n' "$PPID"; exit 0; fi
|
|
6798
|
+
exit 1
|
|
6799
|
+
`, async () => {
|
|
6800
|
+
assert.equal(isWorkerAlive('compat-session', 1, ''), true);
|
|
6801
|
+
assert.equal(isWorkerAlive('compat-session', 1, ' '), true);
|
|
6802
|
+
});
|
|
6803
|
+
});
|
|
6804
|
+
it('treats EPERM liveness probes as unknown and emits no async kill controls', async () => {
|
|
6805
|
+
await withMockTmuxFixture('omx-worker-liveness-eperm-', (logPath) => `#!/bin/sh
|
|
6806
|
+
set -eu
|
|
6807
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
6808
|
+
case "$1" in
|
|
6809
|
+
list-panes)
|
|
6810
|
+
if [ "$2" = "-a" ]; then printf '%%77\t0\t%s\n' "$PPID"; exit 0; fi
|
|
6811
|
+
if [ "$2" = "-t" ]; then printf '0 %s\n' "$PPID"; exit 0; fi
|
|
6812
|
+
exit 1
|
|
6813
|
+
;;
|
|
6814
|
+
show-option) printf 'team:liveness\n'; exit 0 ;;
|
|
6815
|
+
*) exit 1 ;;
|
|
6816
|
+
esac
|
|
6817
|
+
`, async ({ logPath }) => {
|
|
6818
|
+
const originalProcessKill = process.kill;
|
|
6819
|
+
process.kill = ((pid, signal) => {
|
|
6820
|
+
if (pid === process.pid && signal === 0) {
|
|
6821
|
+
const error = new Error('permission denied');
|
|
6822
|
+
error.code = 'EPERM';
|
|
6823
|
+
throw error;
|
|
6824
|
+
}
|
|
6825
|
+
return originalProcessKill(pid, signal);
|
|
6826
|
+
});
|
|
6827
|
+
try {
|
|
6828
|
+
assert.equal(isWorkerAlive('compat-session', 1), true);
|
|
6829
|
+
assert.equal(isWorkerAlive('ignored-session', 1, '%77', process.pid, 'team:liveness'), true);
|
|
6830
|
+
await killWorker('ignored-session', 1, '%77', undefined, process.pid, 'team:liveness');
|
|
6831
|
+
}
|
|
6832
|
+
finally {
|
|
6833
|
+
process.kill = originalProcessKill;
|
|
6834
|
+
}
|
|
6835
|
+
const log = await readFile(logPath, 'utf-8');
|
|
6836
|
+
assert.doesNotMatch(log, /send-keys -t %77|kill-pane -t %77/);
|
|
6837
|
+
});
|
|
6838
|
+
});
|
|
6839
|
+
it('treats only ESRCH as a gone process', async () => {
|
|
6840
|
+
await withMockTmuxFixture('omx-worker-liveness-esrch-', () => `#!/bin/sh
|
|
6841
|
+
case "$1" in
|
|
6842
|
+
list-panes)
|
|
6843
|
+
if [ "$2" = "-a" ]; then printf '%%77\t0\t%s\n' "$PPID"; exit 0; fi
|
|
6844
|
+
if [ "$2" = "-t" ]; then printf '0 %s\n' "$PPID"; exit 0; fi
|
|
6845
|
+
exit 1
|
|
6846
|
+
;;
|
|
6847
|
+
show-option) printf 'team:liveness\n'; exit 0 ;;
|
|
6848
|
+
*) exit 1 ;;
|
|
6849
|
+
esac
|
|
6850
|
+
`, async () => {
|
|
6851
|
+
const originalProcessKill = process.kill;
|
|
6852
|
+
process.kill = ((pid, signal) => {
|
|
6853
|
+
if (pid === process.pid && signal === 0) {
|
|
6854
|
+
const error = new Error('no such process');
|
|
6855
|
+
error.code = 'ESRCH';
|
|
6856
|
+
throw error;
|
|
6857
|
+
}
|
|
6858
|
+
return originalProcessKill(pid, signal);
|
|
6859
|
+
});
|
|
6860
|
+
try {
|
|
6861
|
+
assert.equal(isWorkerAlive('compat-session', 1), false);
|
|
6862
|
+
assert.equal(isWorkerAlive('ignored-session', 1, '%77', process.pid, 'team:liveness'), false);
|
|
6863
|
+
}
|
|
6864
|
+
finally {
|
|
6865
|
+
process.kill = originalProcessKill;
|
|
6866
|
+
}
|
|
6867
|
+
});
|
|
6868
|
+
});
|
|
6869
|
+
it('uses the exact global row for explicit pane liveness and fails closed for dead rows', async () => {
|
|
6870
|
+
await withMockTmuxFixture('omx-pane-id-liveness-', (logPath) => `#!/bin/sh
|
|
6871
|
+
set -eu
|
|
6872
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
6873
|
+
case "\${1:-}" in
|
|
6874
|
+
list-panes)
|
|
6875
|
+
if [ "$2" = "-a" ]; then
|
|
6876
|
+
printf '%%77\t0\t%s\n%%88\t1\t1\n' "$PPID"
|
|
6877
|
+
exit 0
|
|
6878
|
+
fi
|
|
6879
|
+
exit 1
|
|
6880
|
+
;;
|
|
6881
|
+
show-option)
|
|
6882
|
+
printf 'team:liveness\n'
|
|
6883
|
+
;;
|
|
6884
|
+
*)
|
|
6885
|
+
exit 1
|
|
6886
|
+
;;
|
|
6887
|
+
esac
|
|
6888
|
+
`, async () => {
|
|
6889
|
+
assert.equal(isWorkerAlive('ignored-session', 1, '%77', process.pid, 'team:liveness'), true);
|
|
6890
|
+
assert.equal(isWorkerPaneOpen('ignored-session', 1, '%77', process.pid, 'team:liveness'), true);
|
|
6891
|
+
assert.equal(isWorkerAlive('ignored-session', 2, '%88', 1, 'team:liveness'), false);
|
|
6892
|
+
assert.equal(isWorkerPaneOpen('ignored-session', 2, '%88', 1, 'team:liveness'), false);
|
|
6893
|
+
});
|
|
6894
|
+
});
|
|
6895
|
+
});
|
|
6896
|
+
describe('exact global pane proof contract', () => {
|
|
6897
|
+
it('matches only exact pane IDs and distinguishes live, dead, and absent rows', async () => {
|
|
6898
|
+
const pane13Pid = 13013;
|
|
6899
|
+
const pane130Pid = 130130;
|
|
6900
|
+
await withMockTmuxFixture('omx-exact-pane-rows-', () => `#!/bin/sh
|
|
6901
|
+
set -eu
|
|
6902
|
+
case "$1" in
|
|
6903
|
+
list-panes)
|
|
6904
|
+
printf '%%13\t0\t%s\n%%130\t0\t%s\n%%9\t1\t1\n' "${pane13Pid}" "${pane130Pid}"
|
|
6905
|
+
;;
|
|
6906
|
+
show-option)
|
|
6907
|
+
printf 'team:rows\n'
|
|
6908
|
+
;;
|
|
6909
|
+
*)
|
|
6910
|
+
exit 1
|
|
6911
|
+
;;
|
|
6912
|
+
esac
|
|
6913
|
+
`, async () => {
|
|
6914
|
+
assert.deepEqual(readExactPaneProofSync('%1'), { status: 'gone', paneId: '%1', reason: 'absent' });
|
|
6915
|
+
assert.deepEqual(readExactPaneProofSync('%9'), { status: 'gone', paneId: '%9', reason: 'dead' });
|
|
6916
|
+
assert.deepEqual(readExactPaneProofSync('%404'), { status: 'gone', paneId: '%404', reason: 'absent' });
|
|
6917
|
+
assert.deepEqual(await readExactPaneProof('%13'), {
|
|
6918
|
+
status: 'live',
|
|
6919
|
+
paneId: '%13',
|
|
6920
|
+
pid: pane13Pid,
|
|
6921
|
+
});
|
|
6922
|
+
assert.deepEqual(await readExactPaneProof('%130'), {
|
|
6923
|
+
status: 'live',
|
|
6924
|
+
paneId: '%130',
|
|
6925
|
+
pid: pane130Pid,
|
|
6926
|
+
});
|
|
6927
|
+
assert.equal(getWorkerPanePid('ignored-session', 1, '%13', pane13Pid, 'team:rows'), pane13Pid);
|
|
6928
|
+
assert.equal(getWorkerPanePid('ignored-session', 1, '%130', pane130Pid, 'team:rows'), pane130Pid);
|
|
6929
|
+
});
|
|
6930
|
+
});
|
|
6931
|
+
it('fails closed for command failures, malformed snapshots, and invalid IDs', async () => {
|
|
6932
|
+
await withMockTmuxFixture('omx-exact-pane-malformed-', () => `#!/bin/sh
|
|
6933
|
+
set -eu
|
|
6934
|
+
if [ "$1" = "list-panes" ]; then
|
|
6935
|
+
printf '%%1 0 1\n'
|
|
6936
|
+
fi
|
|
6937
|
+
`, async () => {
|
|
6938
|
+
const malformed = readExactPaneProofSync('%1');
|
|
6939
|
+
assert.equal(malformed.status, 'unavailable');
|
|
6940
|
+
if (malformed.status === 'unavailable') {
|
|
6941
|
+
assert.equal(malformed.reason, 'malformed_snapshot');
|
|
6942
|
+
}
|
|
6943
|
+
const invalid = await readExactPaneProof('not-a-pane');
|
|
6944
|
+
assert.equal(invalid.status, 'unavailable');
|
|
6945
|
+
if (invalid.status === 'unavailable') {
|
|
6946
|
+
assert.equal(invalid.reason, 'invalid_pane_id');
|
|
6947
|
+
}
|
|
6948
|
+
});
|
|
6949
|
+
await withMockTmuxFixture('omx-exact-pane-query-failure-', () => `#!/bin/sh
|
|
6950
|
+
set -eu
|
|
6951
|
+
if [ "$1" = "list-panes" ]; then
|
|
6952
|
+
echo "tmux query failed" >&2
|
|
6953
|
+
exit 1
|
|
6954
|
+
fi
|
|
6955
|
+
exit 1
|
|
6956
|
+
`, async () => {
|
|
6957
|
+
const proof = await readExactPaneProof('%1');
|
|
6958
|
+
assert.equal(proof.status, 'unavailable');
|
|
6959
|
+
if (proof.status === 'unavailable') {
|
|
6960
|
+
assert.equal(proof.reason, 'query_failed');
|
|
6961
|
+
}
|
|
6962
|
+
});
|
|
6963
|
+
});
|
|
6964
|
+
it('treats malformed PIDs in dead and unrelated global rows as malformed snapshots', async () => {
|
|
6965
|
+
const proofStatePath = join(tmpdir(), `omx-exact-pane-invalid-pid-${process.pid}-${Date.now()}`);
|
|
6966
|
+
await withMockTmuxFixture('omx-exact-pane-invalid-pid-', () => `#!/bin/sh
|
|
6967
|
+
set -eu
|
|
6968
|
+
if [ "$1" = "list-panes" ]; then
|
|
6969
|
+
if [ -f "${proofStatePath}" ]; then
|
|
6970
|
+
printf '%%1\t0\t2000000001\n%%99\t0\tnot-a-pid\n'
|
|
6971
|
+
else
|
|
6972
|
+
: > "${proofStatePath}"
|
|
6973
|
+
printf '%%1\t0\t2000000001\n%%99\t1\tnot-a-pid\n'
|
|
6974
|
+
fi
|
|
6975
|
+
fi
|
|
6976
|
+
`, async () => {
|
|
6977
|
+
const deadRow = readExactPaneProofSync('%1');
|
|
6978
|
+
assert.equal(deadRow.status, 'unavailable');
|
|
6979
|
+
if (deadRow.status === 'unavailable')
|
|
6980
|
+
assert.equal(deadRow.reason, 'malformed_snapshot');
|
|
6981
|
+
const unrelatedRow = await readExactPaneProof('%1');
|
|
6982
|
+
assert.equal(unrelatedRow.status, 'unavailable');
|
|
6983
|
+
if (unrelatedRow.status === 'unavailable')
|
|
6984
|
+
assert.equal(unrelatedRow.reason, 'malformed_snapshot');
|
|
6985
|
+
});
|
|
6986
|
+
await rm(proofStatePath, { force: true });
|
|
6987
|
+
});
|
|
6988
|
+
it('does not fall back to a session target or issue effects for an unproven explicit ID', async () => {
|
|
6989
|
+
await withMockTmuxFixture('omx-exact-pane-no-fallback-', (logPath) => `#!/bin/sh
|
|
6990
|
+
set -eu
|
|
6991
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
6992
|
+
case "$1" in
|
|
6993
|
+
list-panes)
|
|
6994
|
+
printf '%%13\t0\t%s\n' "$PPID"
|
|
6995
|
+
;;
|
|
6996
|
+
*)
|
|
6997
|
+
exit 0
|
|
6998
|
+
;;
|
|
6999
|
+
esac
|
|
7000
|
+
`, async ({ logPath }) => {
|
|
7001
|
+
assert.equal(getWorkerPanePid('ignored-session', 1, '%1'), null);
|
|
7002
|
+
assert.equal(isWorkerAlive('ignored-session', 1, '%1'), false);
|
|
7003
|
+
assert.equal(isWorkerPaneOpen('ignored-session', 1, '%1'), false);
|
|
7004
|
+
await assert.rejects(() => sendToWorker('ignored-session', 1, 'check inbox', '%1'), /not proven live/);
|
|
7005
|
+
await killWorker('ignored-session', 1, '%1');
|
|
7006
|
+
killWorkerByPaneId('%1', 1);
|
|
7007
|
+
await killWorkerByPaneIdAsync('%1', 1);
|
|
7008
|
+
const log = fs.existsSync(logPath) ? await readFile(logPath, 'utf-8') : '';
|
|
7009
|
+
assert.doesNotMatch(log, /list-panes -t ignored-session:1/);
|
|
7010
|
+
assert.doesNotMatch(log, /send-keys/);
|
|
7011
|
+
assert.doesNotMatch(log, /kill-pane/);
|
|
7012
|
+
});
|
|
7013
|
+
});
|
|
7014
|
+
it('revalidates an explicit pane before every send effect', async () => {
|
|
7015
|
+
await withMockTmuxFixture('omx-exact-pane-send-revalidation-', (logPath) => `#!/bin/sh
|
|
7016
|
+
set -eu
|
|
7017
|
+
state_dir="$(dirname "${logPath}")"
|
|
7018
|
+
count_file="$state_dir/list-count"
|
|
7019
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
7020
|
+
case "$1" in
|
|
7021
|
+
list-panes)
|
|
7022
|
+
count=0
|
|
7023
|
+
if [ -f "$count_file" ]; then count=$(cat "$count_file"); fi
|
|
7024
|
+
count=$((count + 1))
|
|
7025
|
+
printf '%s' "$count" > "$count_file"
|
|
7026
|
+
if [ "$count" -le 4 ]; then
|
|
7027
|
+
printf '%%9\t0\t%s\n' "$PPID"
|
|
7028
|
+
fi
|
|
7029
|
+
;;
|
|
7030
|
+
show-option)
|
|
7031
|
+
printf 'team:test\n'
|
|
7032
|
+
;;
|
|
7033
|
+
capture-pane)
|
|
7034
|
+
cat <<'EOF'
|
|
7035
|
+
${READY_HELPER_CAPTURE}
|
|
7036
|
+
EOF
|
|
7037
|
+
;;
|
|
7038
|
+
send-keys)
|
|
7039
|
+
;;
|
|
7040
|
+
*)
|
|
7041
|
+
exit 1
|
|
7042
|
+
;;
|
|
7043
|
+
esac
|
|
7044
|
+
`, async ({ logPath }) => {
|
|
7045
|
+
await assert.rejects(() => sendToWorker('ignored-session', 1, 'check inbox', '%9', undefined, process.pid, 'team:test', '%10'), /not proven live/);
|
|
7046
|
+
const commands = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
|
|
7047
|
+
const exactGlobalPaneProof = /^list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}$/;
|
|
7048
|
+
const effects = commands
|
|
7049
|
+
.map((command, index) => ({ command, index }))
|
|
7050
|
+
.filter(({ command }) => /^(send-keys|kill-pane) -t %9\b/.test(command));
|
|
7051
|
+
assert.ok(effects.length > 0, `expected an explicit pane effect in log:\n${commands.join('\n')}`);
|
|
7052
|
+
for (const { command, index } of effects) {
|
|
7053
|
+
assert.match(commands[index - 1] ?? '', exactGlobalPaneProof, `fresh proof must immediately precede ${command}`);
|
|
7054
|
+
}
|
|
7055
|
+
assert.match(commands.join('\n'), /send-keys -t %9 -l -- check inbox/);
|
|
7056
|
+
assert.doesNotMatch(commands.join('\n'), /send-keys -t %9 C-m/);
|
|
7057
|
+
});
|
|
7058
|
+
});
|
|
7059
|
+
it('pins an explicit pane PID through an adaptive retry and rejects later pane-ID reuse', async () => {
|
|
7060
|
+
await withMockTmuxFixture('omx-exact-pane-send-retry-pid-pin-', (logPath) => `#!/bin/sh
|
|
7061
|
+
set -eu
|
|
7062
|
+
state_dir="$(dirname "${logPath}")"
|
|
7063
|
+
retry_file="$state_dir/retry"
|
|
7064
|
+
sent_file="$state_dir/sent"
|
|
7065
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
7066
|
+
case "$1" in
|
|
7067
|
+
list-panes)
|
|
7068
|
+
if [ -f "$retry_file" ]; then
|
|
7069
|
+
printf '%%9\t0\t2000000002\n'
|
|
7070
|
+
|
|
7071
|
+
else
|
|
7072
|
+
printf '%%9\t0\t2000000001\n'
|
|
7073
|
+
|
|
7074
|
+
fi
|
|
7075
|
+
;;
|
|
7076
|
+
capture-pane)
|
|
7077
|
+
if [ -f "$sent_file" ]; then
|
|
7078
|
+
cat <<'EOF'
|
|
7079
|
+
${READY_HELPER_CAPTURE}
|
|
7080
|
+
|
|
7081
|
+
› check inbox
|
|
7082
|
+
EOF
|
|
7083
|
+
else
|
|
7084
|
+
cat <<'EOF'
|
|
7085
|
+
${READY_HELPER_CAPTURE}
|
|
7086
|
+
|
|
7087
|
+
• Running tests (esc to interrupt)
|
|
7088
|
+
EOF
|
|
7089
|
+
fi
|
|
7090
|
+
;;
|
|
7091
|
+
send-keys)
|
|
7092
|
+
case "$*" in
|
|
7093
|
+
*" -l -- check inbox") : > "$sent_file" ;;
|
|
7094
|
+
*" C-u") : > "$retry_file" ;;
|
|
7095
|
+
esac
|
|
5075
7096
|
;;
|
|
5076
|
-
|
|
5077
|
-
|
|
7097
|
+
show-option)
|
|
7098
|
+
printf 'team:test\n'
|
|
5078
7099
|
;;
|
|
7100
|
+
*) exit 1 ;;
|
|
5079
7101
|
esac
|
|
5080
7102
|
`, async ({ logPath }) => {
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
});
|
|
5087
|
-
}
|
|
5088
|
-
finally {
|
|
5089
|
-
if (typeof previousOmxRoot === 'string')
|
|
5090
|
-
process.env.OMX_ROOT = previousOmxRoot;
|
|
5091
|
-
else
|
|
5092
|
-
delete process.env.OMX_ROOT;
|
|
5093
|
-
await rm(cwd, { recursive: true, force: true });
|
|
5094
|
-
}
|
|
7103
|
+
await assert.rejects(() => sendToWorker('ignored-session', 1, 'check inbox', '%9', undefined, 2000000001, 'team:test', '%10'), /tmux pane identity changed: %9/);
|
|
7104
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
7105
|
+
assert.match(commands, /send-keys -t %9 C-u/);
|
|
7106
|
+
assert.equal((commands.match(/send-keys -t %9 -l -- check inbox/g) || []).length, 1, `the retry must not type into a reused pane:\n${commands}`);
|
|
7107
|
+
});
|
|
5095
7108
|
});
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
it('uses visible capture-pane argv without tail flags', async () => {
|
|
5099
|
-
const previousAutoTrust = process.env.OMX_TEAM_AUTO_TRUST;
|
|
5100
|
-
delete process.env.OMX_TEAM_AUTO_TRUST;
|
|
5101
|
-
try {
|
|
5102
|
-
await withMockTmuxFixture('omx-tmux-dismiss-trust-visible-capture-', (logPath) => `#!/bin/sh
|
|
7109
|
+
it('rejects same-ID/PID owner takeover before worker input', async () => {
|
|
7110
|
+
await withMockTmuxFixture('omx-exact-pane-send-owner-takeover-', (logPath) => `#!/bin/sh
|
|
5103
7111
|
set -eu
|
|
7112
|
+
state_dir="$(dirname "${logPath}")"
|
|
7113
|
+
owner_count="$state_dir/owner-count"
|
|
5104
7114
|
printf '%s\n' "$*" >> "${logPath}"
|
|
5105
7115
|
case "$1" in
|
|
7116
|
+
list-panes)
|
|
7117
|
+
printf '%%9\t0\t%s\n' "$PPID"
|
|
7118
|
+
;;
|
|
7119
|
+
show-option)
|
|
7120
|
+
count=0
|
|
7121
|
+
if [ -f "$owner_count" ]; then count=$(cat "$owner_count"); fi
|
|
7122
|
+
count=$((count + 1))
|
|
7123
|
+
printf '%s' "$count" > "$owner_count"
|
|
7124
|
+
if [ "$count" -eq 1 ]; then printf 'team:expected\n'; else printf 'team:foreign\n'; fi
|
|
7125
|
+
;;
|
|
5106
7126
|
capture-pane)
|
|
5107
7127
|
cat <<'EOF'
|
|
5108
|
-
|
|
5109
|
-
Press enter to continue
|
|
7128
|
+
${READY_HELPER_CAPTURE}
|
|
5110
7129
|
EOF
|
|
5111
|
-
exit 0
|
|
5112
7130
|
;;
|
|
5113
7131
|
send-keys)
|
|
5114
|
-
exit 0
|
|
5115
|
-
;;
|
|
5116
|
-
*)
|
|
5117
|
-
exit 0
|
|
5118
7132
|
;;
|
|
7133
|
+
*) exit 1 ;;
|
|
5119
7134
|
esac
|
|
5120
7135
|
`, async ({ logPath }) => {
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
assert.doesNotMatch(log, /capture-pane -t omx-team-x:1 -p -S/);
|
|
5125
|
-
});
|
|
5126
|
-
}
|
|
5127
|
-
finally {
|
|
5128
|
-
if (typeof previousAutoTrust === 'string')
|
|
5129
|
-
process.env.OMX_TEAM_AUTO_TRUST = previousAutoTrust;
|
|
5130
|
-
else
|
|
5131
|
-
delete process.env.OMX_TEAM_AUTO_TRUST;
|
|
5132
|
-
}
|
|
5133
|
-
});
|
|
5134
|
-
});
|
|
5135
|
-
describe('dismissTrustPromptIfPresent', () => {
|
|
5136
|
-
it('returns false when tmux is unavailable', () => {
|
|
5137
|
-
withEmptyPath(() => {
|
|
5138
|
-
assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), false);
|
|
7136
|
+
await assert.rejects(() => sendToWorker('ignored-session', 1, 'check inbox', '%9', undefined, process.pid, 'team:expected', '%10'), /team owner changed: %9/);
|
|
7137
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
7138
|
+
assert.doesNotMatch(commands, /send-keys -t %9/);
|
|
5139
7139
|
});
|
|
5140
7140
|
});
|
|
5141
|
-
it('
|
|
5142
|
-
|
|
5143
|
-
process.env.OMX_TEAM_AUTO_TRUST = '0';
|
|
5144
|
-
try {
|
|
5145
|
-
assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), false);
|
|
5146
|
-
}
|
|
5147
|
-
finally {
|
|
5148
|
-
if (typeof prev === 'string')
|
|
5149
|
-
process.env.OMX_TEAM_AUTO_TRUST = prev;
|
|
5150
|
-
else
|
|
5151
|
-
delete process.env.OMX_TEAM_AUTO_TRUST;
|
|
5152
|
-
}
|
|
5153
|
-
});
|
|
5154
|
-
it('returns false when OMX_TEAM_AUTO_TRUST is unset (auto-trust enabled) but tmux unavailable', () => {
|
|
5155
|
-
const prev = process.env.OMX_TEAM_AUTO_TRUST;
|
|
5156
|
-
delete process.env.OMX_TEAM_AUTO_TRUST;
|
|
5157
|
-
try {
|
|
5158
|
-
withEmptyPath(() => {
|
|
5159
|
-
assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), false);
|
|
5160
|
-
});
|
|
5161
|
-
}
|
|
5162
|
-
finally {
|
|
5163
|
-
if (typeof prev === 'string')
|
|
5164
|
-
process.env.OMX_TEAM_AUTO_TRUST = prev;
|
|
5165
|
-
}
|
|
5166
|
-
});
|
|
5167
|
-
});
|
|
5168
|
-
describe('isWorkerAlive', () => {
|
|
5169
|
-
it('does not require pane_current_command to match "codex"', () => {
|
|
5170
|
-
// This was a real failure mode: tmux reports pane_current_command=node for the Codex TUI,
|
|
5171
|
-
// which caused workers to be treated as dead and the leader to clean up state too early.
|
|
5172
|
-
withEmptyPath(() => {
|
|
5173
|
-
assert.equal(isWorkerAlive('omx-team-x', 1), false);
|
|
5174
|
-
});
|
|
7141
|
+
it('rejects an explicit worker target equal to the canonical HUD pane', async () => {
|
|
7142
|
+
await assert.rejects(() => sendToWorker('ignored-session', 1, 'check inbox', '%9', undefined, process.pid, 'team:expected', '%9'), /HUD target: %9/);
|
|
5175
7143
|
});
|
|
5176
|
-
it('
|
|
5177
|
-
await withMockTmuxFixture('omx-pane-
|
|
7144
|
+
it('revalidates an explicit pane before later kill effects', async () => {
|
|
7145
|
+
await withMockTmuxFixture('omx-exact-pane-kill-revalidation-', (logPath) => `#!/bin/sh
|
|
5178
7146
|
set -eu
|
|
7147
|
+
state_dir="$(dirname "${logPath}")"
|
|
7148
|
+
count_file="$state_dir/list-count"
|
|
5179
7149
|
printf '%s\n' "$*" >> "${logPath}"
|
|
5180
|
-
case "
|
|
5181
|
-
-V)
|
|
5182
|
-
echo "tmux 3.4"
|
|
5183
|
-
exit 0
|
|
5184
|
-
;;
|
|
7150
|
+
case "$1" in
|
|
5185
7151
|
list-panes)
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
7152
|
+
count=0
|
|
7153
|
+
if [ -f "$count_file" ]; then count=$(cat "$count_file"); fi
|
|
7154
|
+
count=$((count + 1))
|
|
7155
|
+
printf '%s' "$count" > "$count_file"
|
|
7156
|
+
if [ "$count" -le 9 ]; then
|
|
7157
|
+
printf '%%9\t0\t%s\n' "$PPID"
|
|
5189
7158
|
fi
|
|
5190
|
-
|
|
7159
|
+
;;
|
|
7160
|
+
show-option) printf 'team:kill-test\n' ;;
|
|
7161
|
+
send-keys|kill-pane)
|
|
5191
7162
|
;;
|
|
5192
7163
|
*)
|
|
5193
7164
|
exit 1
|
|
5194
7165
|
;;
|
|
5195
7166
|
esac
|
|
5196
|
-
`, async () => {
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
7167
|
+
`, async ({ logPath }) => {
|
|
7168
|
+
await killWorker('ignored-session', 1, '%9', undefined, process.pid, 'team:kill-test');
|
|
7169
|
+
const commands = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
|
|
7170
|
+
const exactGlobalPaneProof = /^list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}$/;
|
|
7171
|
+
const effects = commands
|
|
7172
|
+
.map((command, index) => ({ command, index }))
|
|
7173
|
+
.filter(({ command }) => /^(send-keys|kill-pane) -t %9\b/.test(command));
|
|
7174
|
+
assert.ok(effects.length > 0, `expected an explicit pane effect in log:\n${commands.join('\n')}`);
|
|
7175
|
+
for (const { command, index } of effects) {
|
|
7176
|
+
assert.match(commands[index - 1] ?? '', exactGlobalPaneProof, `fresh proof must immediately precede ${command}`);
|
|
7177
|
+
}
|
|
7178
|
+
assert.match(commands.join('\n'), /send-keys -t %9 C-c/);
|
|
7179
|
+
assert.match(commands.join('\n'), /send-keys -t %9 C-d/);
|
|
7180
|
+
assert.doesNotMatch(commands.join('\n'), /kill-pane -t %9/);
|
|
7181
|
+
});
|
|
7182
|
+
});
|
|
7183
|
+
it('pins an explicit pane PID through later control and kill effects', async () => {
|
|
7184
|
+
await withMockTmuxFixture('omx-exact-pane-kill-pid-pin-', (logPath) => `#!/bin/sh
|
|
7185
|
+
set -eu
|
|
7186
|
+
state_dir="$(dirname "${logPath}")"
|
|
7187
|
+
exit_file="$state_dir/exit"
|
|
7188
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
7189
|
+
case "$1" in
|
|
7190
|
+
list-panes)
|
|
7191
|
+
if [ -f "$exit_file" ]; then
|
|
7192
|
+
printf '%%9\t0\t%s\n' "$((PPID + 1))"
|
|
7193
|
+
else
|
|
7194
|
+
printf '%%9\t0\t%s\n' "$PPID"
|
|
7195
|
+
fi
|
|
7196
|
+
;;
|
|
7197
|
+
show-option) printf 'team:kill-test\n' ;;
|
|
7198
|
+
send-keys)
|
|
7199
|
+
case "$*" in
|
|
7200
|
+
*" C-d") : > "$exit_file" ;;
|
|
7201
|
+
esac
|
|
7202
|
+
;;
|
|
7203
|
+
kill-pane) exit 0 ;;
|
|
7204
|
+
*) exit 1 ;;
|
|
7205
|
+
esac
|
|
7206
|
+
`, async ({ logPath }) => {
|
|
7207
|
+
await assert.rejects(() => killWorker('ignored-session', 1, '%9', undefined, process.pid, 'team:kill-test'), /tmux pane identity changed: %9/);
|
|
7208
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
7209
|
+
assert.match(commands, /send-keys -t %9 C-c/);
|
|
7210
|
+
assert.match(commands, /send-keys -t %9 C-d/);
|
|
7211
|
+
assert.doesNotMatch(commands, /kill-pane -t %9/);
|
|
7212
|
+
});
|
|
7213
|
+
});
|
|
7214
|
+
it('uses the pinned liveness proof PID without an unconstrained second proof', async () => {
|
|
7215
|
+
await withMockTmuxFixture('omx-exact-pane-kill-liveness-pid-pin-', (logPath) => `#!/bin/sh
|
|
7216
|
+
set -eu
|
|
7217
|
+
state_dir="$(dirname "${logPath}")"
|
|
7218
|
+
count_file="$state_dir/list-count"
|
|
7219
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
7220
|
+
case "$1" in
|
|
7221
|
+
list-panes)
|
|
7222
|
+
count=0
|
|
7223
|
+
if [ -f "$count_file" ]; then count=$(cat "$count_file"); fi
|
|
7224
|
+
count=$((count + 1))
|
|
7225
|
+
printf '%s' "$count" > "$count_file"
|
|
7226
|
+
if [ -f "$state_dir/killed" ]; then
|
|
7227
|
+
:
|
|
7228
|
+
elif [ "$count" -le 12 ]; then
|
|
7229
|
+
printf '%%9\t0\t%s\n' "$PPID"
|
|
7230
|
+
else
|
|
7231
|
+
printf '%%9\t0\t999999999\n'
|
|
7232
|
+
fi
|
|
7233
|
+
;;
|
|
7234
|
+
show-option) printf 'team:kill-test\n' ;;
|
|
7235
|
+
send-keys) ;;
|
|
7236
|
+
kill-pane) : > "$state_dir/killed" ;;
|
|
7237
|
+
*) exit 1 ;;
|
|
7238
|
+
esac
|
|
7239
|
+
`, async ({ logPath }) => {
|
|
7240
|
+
await killWorker('ignored-session', 1, '%9', undefined, process.pid, 'team:kill-test');
|
|
7241
|
+
const commands = await readFile(logPath, 'utf-8');
|
|
7242
|
+
assert.match(commands, /send-keys -t %9 C-c/);
|
|
7243
|
+
assert.match(commands, /send-keys -t %9 C-d/);
|
|
7244
|
+
assert.equal((commands.match(/list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}/g) || []).length, 13, `liveness and effects must each use the pinned proof around owner authorization:\n${commands}`);
|
|
5200
7245
|
});
|
|
5201
7246
|
});
|
|
5202
7247
|
});
|
|
@@ -5390,30 +7435,66 @@ describe('enableMouseScrolling', () => {
|
|
|
5390
7435
|
}
|
|
5391
7436
|
});
|
|
5392
7437
|
});
|
|
5393
|
-
describe('killWorkerByPaneId
|
|
5394
|
-
it('skips kill when workerPaneId matches leaderPaneId
|
|
5395
|
-
// With empty PATH tmux is unavailable, so any actual kill-pane call would fail.
|
|
5396
|
-
// When the guard fires (paneId === leaderPaneId) the function returns early
|
|
5397
|
-
// without invoking tmux, so no error is thrown regardless of PATH.
|
|
7438
|
+
describe('killWorkerByPaneId exact PID guard', () => {
|
|
7439
|
+
it('skips kill when workerPaneId matches leaderPaneId before tmux is called', () => {
|
|
5398
7440
|
withEmptyPath(() => {
|
|
5399
|
-
assert.doesNotThrow(() => killWorkerByPaneId('%5', '%5'));
|
|
7441
|
+
assert.doesNotThrow(() => killWorkerByPaneId('%5', 5, '%5'));
|
|
5400
7442
|
});
|
|
5401
7443
|
});
|
|
5402
|
-
it('
|
|
5403
|
-
// Different IDs: guard does not fire. tmux is unavailable but kill errors are swallowed internally.
|
|
7444
|
+
it('requires a positive expected PID for an explicit direct pane kill', () => {
|
|
5404
7445
|
withEmptyPath(() => {
|
|
5405
|
-
assert.doesNotThrow(() => killWorkerByPaneId('%5'
|
|
7446
|
+
assert.doesNotThrow(() => killWorkerByPaneId('%5'));
|
|
7447
|
+
assert.doesNotThrow(() => killWorkerByPaneId('%5', 0));
|
|
7448
|
+
assert.doesNotThrow(() => killWorkerByPaneId('%5', -1));
|
|
5406
7449
|
});
|
|
5407
7450
|
});
|
|
5408
|
-
it('
|
|
7451
|
+
it('keeps blank pane IDs as no-effect compatibility values', () => {
|
|
5409
7452
|
withEmptyPath(() => {
|
|
5410
|
-
assert.doesNotThrow(() => killWorkerByPaneId('
|
|
7453
|
+
assert.doesNotThrow(() => killWorkerByPaneId('', 5));
|
|
7454
|
+
assert.doesNotThrow(() => killWorkerByPaneId(' ', 5));
|
|
5411
7455
|
});
|
|
5412
7456
|
});
|
|
5413
|
-
it('
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
7457
|
+
it('does not kill a recycled pane ID when its current PID differs from the frozen PID', async () => {
|
|
7458
|
+
await withMockTmuxFixture('omx-direct-kill-recycled-sync-', (logPath) => `#!/bin/sh
|
|
7459
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
7460
|
+
if [ "$1" = "list-panes" ]; then
|
|
7461
|
+
printf '%%5\t0\t222\n'
|
|
7462
|
+
elif [ "$1" = "show-option" ]; then
|
|
7463
|
+
printf 'team:expected\n'
|
|
7464
|
+
fi
|
|
7465
|
+
`, async ({ logPath }) => {
|
|
7466
|
+
killWorkerByPaneId('%5', 111, undefined, 'team:expected', '%6');
|
|
7467
|
+
const log = await readFile(logPath, 'utf-8');
|
|
7468
|
+
assert.match(log, /list-panes -a/);
|
|
7469
|
+
assert.doesNotMatch(log, /kill-pane -t %5/);
|
|
7470
|
+
});
|
|
7471
|
+
});
|
|
7472
|
+
it('does not kill a recycled pane ID asynchronously when its current PID differs from the frozen PID', async () => {
|
|
7473
|
+
await withMockTmuxFixture('omx-direct-kill-recycled-async-', (logPath) => `#!/bin/sh
|
|
7474
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
7475
|
+
if [ "$1" = "list-panes" ]; then
|
|
7476
|
+
printf '%%5\t0\t222\n'
|
|
7477
|
+
elif [ "$1" = "show-option" ]; then
|
|
7478
|
+
printf 'team:expected\n'
|
|
7479
|
+
fi
|
|
7480
|
+
`, async ({ logPath }) => {
|
|
7481
|
+
await killWorkerByPaneIdAsync('%5', 111, undefined, 'team:expected', '%6');
|
|
7482
|
+
const log = await readFile(logPath, 'utf-8');
|
|
7483
|
+
assert.match(log, /list-panes -a/);
|
|
7484
|
+
assert.doesNotMatch(log, /kill-pane -t %5/);
|
|
7485
|
+
});
|
|
7486
|
+
});
|
|
7487
|
+
it('rejects unbound, HUD, and owner-taken-over direct kill targets without effects', async () => {
|
|
7488
|
+
await withMockTmuxFixture('omx-direct-kill-owner-hud-', (logPath) => `#!/bin/sh
|
|
7489
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
7490
|
+
case "$1" in
|
|
7491
|
+
list-panes) printf '%%5\\t0\\t111\\n' ;;
|
|
7492
|
+
show-option) printf 'team:foreign\\n' ;;
|
|
7493
|
+
esac`, async ({ logPath }) => {
|
|
7494
|
+
killWorkerByPaneId('%5', 111);
|
|
7495
|
+
killWorkerByPaneId('%5', 111, undefined, 'team:expected', '%5');
|
|
7496
|
+
killWorkerByPaneId('%5', 111, undefined, 'team:expected', '%6');
|
|
7497
|
+
assert.doesNotMatch(await readFile(logPath, 'utf8'), /kill-pane/);
|
|
5417
7498
|
});
|
|
5418
7499
|
});
|
|
5419
7500
|
});
|
|
@@ -5496,6 +7577,21 @@ esac
|
|
|
5496
7577
|
assert.doesNotMatch(tmuxLog, /terminal-overrides/);
|
|
5497
7578
|
});
|
|
5498
7579
|
});
|
|
7580
|
+
it('requires a fresh caller authorization before every post-sleep session mouse and copy-mode operation', async () => {
|
|
7581
|
+
await withMockTmuxFixture('omx-tmux-enable-mouse-reproof-', (tmuxLogPath) => `#!/bin/sh
|
|
7582
|
+
printf '%s\n' "$*" >> "${tmuxLogPath}"
|
|
7583
|
+
case "$1" in
|
|
7584
|
+
show-options) printf '%s\n' 'bg=yellow,fg=black,underscore' ;;
|
|
7585
|
+
set-option) exit 0 ;;
|
|
7586
|
+
esac
|
|
7587
|
+
`, async ({ logPath }) => {
|
|
7588
|
+
let authorizations = 0;
|
|
7589
|
+
assert.equal(enableMouseScrolling('omx-team-x', () => { authorizations += 1; }), true);
|
|
7590
|
+
const effects = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
|
|
7591
|
+
assert.equal(authorizations, effects.length);
|
|
7592
|
+
assert.ok(authorizations > 2, 'copy-mode reads and mutations must not share a mouse proof');
|
|
7593
|
+
});
|
|
7594
|
+
});
|
|
5499
7595
|
});
|
|
5500
7596
|
describe('mitigateCopyModeUnderlineArtifacts', () => {
|
|
5501
7597
|
it('best-effort sanitizes copy-mode style options without requiring global tmux changes', async () => {
|
|
@@ -5539,13 +7635,13 @@ describe('killWorker leader pane guard', () => {
|
|
|
5539
7635
|
assert.doesNotThrow(() => killWorker('omx-team-x:0', 1, '%5', '%5'));
|
|
5540
7636
|
});
|
|
5541
7637
|
});
|
|
5542
|
-
it('
|
|
5543
|
-
//
|
|
7638
|
+
it('does not send control keys when a differing explicit pane ID cannot be proven live', () => {
|
|
7639
|
+
// The leader guard does not fire, but global proof failure remains fail-closed.
|
|
5544
7640
|
withEmptyPath(() => {
|
|
5545
7641
|
assert.doesNotThrow(() => killWorker('omx-team-x:0', 1, '%5', '%6'));
|
|
5546
7642
|
});
|
|
5547
7643
|
});
|
|
5548
|
-
it('
|
|
7644
|
+
it('does not send control keys without a leader guard when proof is unavailable', () => {
|
|
5549
7645
|
withEmptyPath(() => {
|
|
5550
7646
|
assert.doesNotThrow(() => killWorker('omx-team-x:0', 1, '%5'));
|
|
5551
7647
|
});
|
|
@@ -5555,13 +7651,25 @@ describe('teardownWorkerPanes shared primitive', () => {
|
|
|
5555
7651
|
it('excludes leader and hud panes in shared pane-kill primitive', async () => {
|
|
5556
7652
|
await withMockTmuxFixture('omx-tmux-teardown-', (logPath) => `#!/bin/sh
|
|
5557
7653
|
set -eu
|
|
5558
|
-
printf '%s
|
|
5559
|
-
|
|
5560
|
-
|
|
7654
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
7655
|
+
case "$1" in
|
|
7656
|
+
list-panes)
|
|
7657
|
+
if [ ! -f "${logPath}.dead" ]; then
|
|
7658
|
+
printf '%%3\t0\t%s\n' "$PPID"
|
|
7659
|
+
fi
|
|
7660
|
+
;;
|
|
7661
|
+
kill-pane)
|
|
7662
|
+
: > "${logPath}.dead"
|
|
7663
|
+
;;
|
|
7664
|
+
*)
|
|
7665
|
+
exit 0
|
|
7666
|
+
;;
|
|
7667
|
+
esac`, async ({ logPath }) => {
|
|
5561
7668
|
const summary = await teardownWorkerPanes(['%1', '%2', '%3'], {
|
|
5562
7669
|
leaderPaneId: '%1',
|
|
5563
7670
|
hudPaneId: '%2',
|
|
5564
7671
|
graceMs: 1,
|
|
7672
|
+
expectedPanePids: { '%3': process.pid },
|
|
5565
7673
|
});
|
|
5566
7674
|
assert.equal(summary.excluded.leader, 1);
|
|
5567
7675
|
assert.equal(summary.excluded.hud, 1);
|
|
@@ -5573,28 +7681,89 @@ exit 0
|
|
|
5573
7681
|
assert.doesNotMatch(log, /kill-pane -t %2/);
|
|
5574
7682
|
});
|
|
5575
7683
|
});
|
|
5576
|
-
it('
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
7684
|
+
it('treats an exit-zero kill as unresolved when the pane ID is freshly reused', async () => {
|
|
7685
|
+
await withMockTmuxFixture('omx-tmux-teardown-reused-pane-', (logPath) => `#!/bin/sh
|
|
7686
|
+
set -eu
|
|
7687
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
7688
|
+
case "$1" in
|
|
7689
|
+
list-panes)
|
|
7690
|
+
if [ -f "${logPath}.killed" ]; then
|
|
7691
|
+
printf '%%77\t0\t7702\n'
|
|
7692
|
+
else
|
|
7693
|
+
printf '%%77\t0\t7701\n'
|
|
7694
|
+
fi
|
|
7695
|
+
;;
|
|
7696
|
+
kill-pane)
|
|
7697
|
+
: > "${logPath}.killed"
|
|
7698
|
+
;;
|
|
7699
|
+
esac`, async ({ logPath }) => {
|
|
7700
|
+
const summary = await teardownWorkerPanes(['%77'], { graceMs: 1, expectedPanePids: { '%77': 7701 } });
|
|
7701
|
+
assert.equal(summary.kill.attempted, 1);
|
|
7702
|
+
assert.equal(summary.kill.succeeded, 0);
|
|
7703
|
+
assert.equal(summary.kill.failed, 1);
|
|
7704
|
+
assert.deepEqual(summary.kill.failedPaneIds, ['%77']);
|
|
7705
|
+
assert.deepEqual(summary.killedPaneIds, []);
|
|
7706
|
+
const commands = (await readFile(logPath, 'utf-8')).trim().split('\n');
|
|
7707
|
+
assert.deepEqual(commands, [
|
|
7708
|
+
'list-panes -a -F #{pane_id}\t#{pane_dead}\t#{pane_pid}',
|
|
7709
|
+
'list-panes -a -F #{pane_id}\t#{pane_dead}\t#{pane_pid}',
|
|
7710
|
+
'kill-pane -t %77',
|
|
7711
|
+
'list-panes -a -F #{pane_id}\t#{pane_dead}\t#{pane_pid}',
|
|
7712
|
+
]);
|
|
7713
|
+
});
|
|
5581
7714
|
});
|
|
5582
|
-
it('
|
|
5583
|
-
await withMockTmuxFixture('omx-tmux-teardown-
|
|
7715
|
+
it('stops at the first unavailable proof without killing a later live candidate', async () => {
|
|
7716
|
+
await withMockTmuxFixture('omx-tmux-teardown-proof-unavailable-', (logPath) => {
|
|
7717
|
+
const proofStatePath = `${logPath}.proof-state`;
|
|
7718
|
+
return `#!/bin/sh
|
|
5584
7719
|
set -eu
|
|
5585
|
-
printf '%s
|
|
5586
|
-
if [ "$1" = "
|
|
5587
|
-
|
|
5588
|
-
|
|
7720
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
7721
|
+
if [ "$1" = "list-panes" ]; then
|
|
7722
|
+
if [ -f "${proofStatePath}" ]; then
|
|
7723
|
+
printf '%%405\t0\t%s\n' "$$"
|
|
7724
|
+
else
|
|
7725
|
+
: > "${proofStatePath}"
|
|
7726
|
+
echo "tmux unavailable" >&2
|
|
7727
|
+
exit 1
|
|
7728
|
+
fi
|
|
5589
7729
|
fi
|
|
5590
7730
|
exit 0
|
|
5591
|
-
|
|
7731
|
+
`;
|
|
7732
|
+
}, async ({ logPath }) => {
|
|
5592
7733
|
const summary = await teardownWorkerPanes(['%404', '%405'], { graceMs: 1 });
|
|
5593
|
-
assert.equal(summary.kill.attempted,
|
|
5594
|
-
assert.
|
|
7734
|
+
assert.equal(summary.kill.attempted, 0);
|
|
7735
|
+
assert.deepEqual(summary.provenGonePaneIds, []);
|
|
7736
|
+
assert.deepEqual(summary.proofUnavailable.map((proof) => proof.paneId), ['%404']);
|
|
7737
|
+
const commands = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
|
|
7738
|
+
assert.deepEqual(commands, ['list-panes -a -F #{pane_id}\t#{pane_dead}\t#{pane_pid}']);
|
|
7739
|
+
});
|
|
7740
|
+
});
|
|
7741
|
+
it('distinguishes proven-gone panes from target command failures', async () => {
|
|
7742
|
+
await withMockTmuxFixture('omx-tmux-teardown-proof-outcomes-', (logPath) => `#!/bin/sh
|
|
7743
|
+
set -eu
|
|
7744
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
7745
|
+
case "$1" in
|
|
7746
|
+
list-panes)
|
|
7747
|
+
printf '%%405\t0\t%s\n' "$PPID"
|
|
7748
|
+
;;
|
|
7749
|
+
kill-pane)
|
|
7750
|
+
echo "target command failed" >&2
|
|
7751
|
+
exit 1
|
|
7752
|
+
;;
|
|
7753
|
+
*)
|
|
7754
|
+
exit 0
|
|
7755
|
+
;;
|
|
7756
|
+
esac
|
|
7757
|
+
`, async ({ logPath }) => {
|
|
7758
|
+
const summary = await teardownWorkerPanes(['%404', '%405'], { graceMs: 1, expectedPanePids: { '%405': process.pid } });
|
|
7759
|
+
assert.equal(summary.kill.attempted, 1);
|
|
7760
|
+
assert.equal(summary.kill.succeeded, 0);
|
|
5595
7761
|
assert.equal(summary.kill.failed, 1);
|
|
7762
|
+
assert.deepEqual(summary.provenGonePaneIds, ['%404']);
|
|
7763
|
+
assert.deepEqual(summary.proofUnavailable, []);
|
|
7764
|
+
assert.deepEqual(summary.kill.failedPaneIds, ['%405']);
|
|
5596
7765
|
const log = await readFile(logPath, 'utf-8');
|
|
5597
|
-
assert.
|
|
7766
|
+
assert.doesNotMatch(log, /kill-pane -t %404/);
|
|
5598
7767
|
assert.match(log, /kill-pane -t %405/);
|
|
5599
7768
|
});
|
|
5600
7769
|
});
|
|
@@ -5604,4 +7773,100 @@ describe('leader mailbox-only boundary', () => {
|
|
|
5604
7773
|
assert.equal('sendToLeaderPane' in tmuxSessionModule, false);
|
|
5605
7774
|
});
|
|
5606
7775
|
});
|
|
7776
|
+
describe('exact pane PID authority regressions', () => {
|
|
7777
|
+
it('ignores an unrelated PID-less pane while requiring the matched target PID', async () => {
|
|
7778
|
+
await withMockTmuxFixture('omx-exact-pane-empty-unrelated-', () => `#!/bin/sh
|
|
7779
|
+
set -eu
|
|
7780
|
+
if [ "$1" = "list-panes" ]; then
|
|
7781
|
+
printf '%%42\t0\t4242\n%%99\t0\t\n'
|
|
7782
|
+
fi
|
|
7783
|
+
`, async () => {
|
|
7784
|
+
const proof = await readExactPaneProof('%42');
|
|
7785
|
+
assert.deepEqual(proof, { status: 'live', paneId: '%42', pid: 4242 });
|
|
7786
|
+
const emptyTarget = await readExactPaneProof('%99');
|
|
7787
|
+
assert.equal(emptyTarget.status, 'unavailable');
|
|
7788
|
+
if (emptyTarget.status === 'unavailable')
|
|
7789
|
+
assert.equal(emptyTarget.reason, 'malformed_snapshot');
|
|
7790
|
+
});
|
|
7791
|
+
});
|
|
7792
|
+
it('retains a live pane as unresolved teardown debt when no positive PID was persisted', async () => {
|
|
7793
|
+
await withMockTmuxFixture('omx-teardown-pid-missing-', (logPath) => `#!/bin/sh
|
|
7794
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
7795
|
+
if [ "$1" = "list-panes" ]; then printf '%%42\\t0\\t4242\\n'; fi`, async ({ logPath }) => {
|
|
7796
|
+
const summary = await teardownWorkerPanes(['%42'], { graceMs: 1 });
|
|
7797
|
+
assert.equal(summary.kill.attempted, 0);
|
|
7798
|
+
assert.deepEqual(summary.proofUnavailable.map((proof) => proof.paneId), ['%42']);
|
|
7799
|
+
assert.doesNotMatch(await readFile(logPath, 'utf8'), /kill-pane/);
|
|
7800
|
+
});
|
|
7801
|
+
});
|
|
7802
|
+
it('does not kill a stale pane ID whose live PID differs from persisted identity', async () => {
|
|
7803
|
+
await withMockTmuxFixture('omx-teardown-pid-changed-', (logPath) => `#!/bin/sh
|
|
7804
|
+
set -eu
|
|
7805
|
+
printf '%s\n' "$*" >> "${logPath}"
|
|
7806
|
+
if [ "$1" = "list-panes" ]; then printf '%%42\t0\t4343\n'; fi
|
|
7807
|
+
`, async ({ logPath }) => {
|
|
7808
|
+
const summary = await teardownWorkerPanes(['%42'], {
|
|
7809
|
+
graceMs: 1,
|
|
7810
|
+
expectedPanePids: { '%42': 4242 },
|
|
7811
|
+
});
|
|
7812
|
+
assert.deepEqual(summary.proofUnavailable.map((proof) => proof.reason), ['pane_pid_changed']);
|
|
7813
|
+
assert.doesNotMatch(await readFile(logPath, 'utf8'), /kill-pane/);
|
|
7814
|
+
});
|
|
7815
|
+
});
|
|
7816
|
+
it('does not kill a replacement pane when owner authorization recycles its ID', async () => {
|
|
7817
|
+
await withMockTmuxFixture('omx-teardown-owner-read-pid-reuse-', (logPath) => `#!/bin/sh
|
|
7818
|
+
set -eu
|
|
7819
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
7820
|
+
case "$1" in
|
|
7821
|
+
list-panes)
|
|
7822
|
+
if [ -f "${logPath}.owner-read" ]; then
|
|
7823
|
+
printf '%%42\\t0\\t4343\\n'
|
|
7824
|
+
else
|
|
7825
|
+
printf '%%42\\t0\\t4242\\n'
|
|
7826
|
+
fi
|
|
7827
|
+
;;
|
|
7828
|
+
show-options)
|
|
7829
|
+
: > "${logPath}.owner-read"
|
|
7830
|
+
echo 'team:owned'
|
|
7831
|
+
;;
|
|
7832
|
+
esac
|
|
7833
|
+
`, async ({ logPath }) => {
|
|
7834
|
+
const summary = await teardownWorkerPanes(['%42'], {
|
|
7835
|
+
graceMs: 1,
|
|
7836
|
+
expectedPanePids: { '%42': 4242 },
|
|
7837
|
+
authorizePaneKill: () => {
|
|
7838
|
+
spawnSync('tmux', ['show-options', '-p', '-t', '%42', '@omx_team_pane_owner_id']);
|
|
7839
|
+
return true;
|
|
7840
|
+
},
|
|
7841
|
+
});
|
|
7842
|
+
assert.deepEqual(summary.proofUnavailable.map((proof) => proof.reason), ['pane_pid_changed']);
|
|
7843
|
+
assert.doesNotMatch(await readFile(logPath, 'utf8'), /kill-pane/);
|
|
7844
|
+
});
|
|
7845
|
+
});
|
|
7846
|
+
it('preserves an empty pane_start_command field but rejects a missing trailing field before startup mutation', async () => {
|
|
7847
|
+
await withMockTmuxFixture('omx-startup-topology-trailing-field-', (logPath) => `#!/bin/sh
|
|
7848
|
+
set -eu
|
|
7849
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
7850
|
+
if [ "$1" = "list-panes" ]; then
|
|
7851
|
+
case "$*" in
|
|
7852
|
+
*"pane_current_command"*) printf '%%11\\tzsh\\t\\n' ;;
|
|
7853
|
+
*) printf '%%11\\n' ;;
|
|
7854
|
+
esac
|
|
7855
|
+
fi
|
|
7856
|
+
`, async ({ logPath }) => {
|
|
7857
|
+
assert.deepEqual(listPaneIds('leader:0'), ['%11']);
|
|
7858
|
+
const log = await readFile(logPath, 'utf8');
|
|
7859
|
+
assert.doesNotMatch(log, /set-option|split-window|resize-pane|select-layout|set-window-option|select-pane|send-keys/);
|
|
7860
|
+
});
|
|
7861
|
+
await withMockTmuxFixture('omx-startup-topology-missing-field-', (logPath) => `#!/bin/sh
|
|
7862
|
+
set -eu
|
|
7863
|
+
printf '%s\\n' "$*" >> "${logPath}"
|
|
7864
|
+
if [ "$1" = "list-panes" ]; then printf '%%11\\tzsh\\n'; fi
|
|
7865
|
+
`, async ({ logPath }) => {
|
|
7866
|
+
assert.deepEqual(listPaneIds('leader:0'), []);
|
|
7867
|
+
const log = await readFile(logPath, 'utf8');
|
|
7868
|
+
assert.doesNotMatch(log, /set-option|split-window|resize-pane|select-layout|set-window-option|select-pane|send-keys/);
|
|
7869
|
+
});
|
|
7870
|
+
});
|
|
7871
|
+
});
|
|
5607
7872
|
//# sourceMappingURL=tmux-session.test.js.map
|