railmux 0.4.0.dev17__tar.gz → 0.4.0.dev19__tar.gz
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.
- {railmux-0.4.0.dev17/src/railmux.egg-info → railmux-0.4.0.dev19}/PKG-INFO +1 -1
- railmux-0.4.0.dev19/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/cli.py +45 -13
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/tmux_server.py +84 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/app.py +32 -5
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_cli.py +114 -2
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_fullscreen_binding.py +61 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_tmux_server.py +135 -0
- railmux-0.4.0.dev17/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/LICENSE +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/README.md +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/pyproject.toml +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/setup.cfg +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/windows_msys2.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_windows_msys2.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev19"
|
|
@@ -26,6 +26,7 @@ from railmux import tmux_server
|
|
|
26
26
|
from railmux import windows_tmux_lifecycle
|
|
27
27
|
from railmux.system_deps import ensure_tmux_available
|
|
28
28
|
from railmux.ssh_args import AppendSshArgument, ExtendSshArguments
|
|
29
|
+
from railmux.terminal_status import command_status
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
def _show_startup_message(
|
|
@@ -85,6 +86,9 @@ def _run_tmux_client_with_watchdog(
|
|
|
85
86
|
expected_session_id: str | None = None,
|
|
86
87
|
) -> int:
|
|
87
88
|
"""Keep one monitor outside tmux so a frozen server cannot trap the TTY."""
|
|
89
|
+
from railmux.provider_paths import running_in_managed_windows_wrapper
|
|
90
|
+
|
|
91
|
+
managed_windows = running_in_managed_windows_wrapper()
|
|
88
92
|
attributes = None
|
|
89
93
|
if sys.stdin.isatty():
|
|
90
94
|
try:
|
|
@@ -93,7 +97,14 @@ def _run_tmux_client_with_watchdog(
|
|
|
93
97
|
pass
|
|
94
98
|
started_at = time.monotonic()
|
|
95
99
|
try:
|
|
96
|
-
|
|
100
|
+
popen_kwargs: dict[str, object] = {"env": env}
|
|
101
|
+
if managed_windows and expected_target is not None:
|
|
102
|
+
# A fast direct-client rejection is an implementation detail when
|
|
103
|
+
# the server-origin bridge succeeds. Suppress tmux's raw
|
|
104
|
+
# ``open terminal failed`` line and emit one Railmux-owned status
|
|
105
|
+
# below; bridge failures still receive an actionable error.
|
|
106
|
+
popen_kwargs["stderr"] = subprocess.DEVNULL
|
|
107
|
+
process: object = subprocess.Popen(argv, **popen_kwargs)
|
|
97
108
|
except OSError as exc:
|
|
98
109
|
print(f"error: could not start tmux client: {exc}", file=sys.stderr)
|
|
99
110
|
return 2
|
|
@@ -204,10 +215,6 @@ def _run_tmux_client_with_watchdog(
|
|
|
204
215
|
expected_session_id = tmux_server.target_session_id(
|
|
205
216
|
expected_target, "railmux", timeout=0.5)
|
|
206
217
|
|
|
207
|
-
from railmux.provider_paths import running_in_managed_windows_wrapper
|
|
208
|
-
|
|
209
|
-
managed_windows = running_in_managed_windows_wrapper()
|
|
210
|
-
|
|
211
218
|
can_relay = bool(
|
|
212
219
|
current_target == expected_target
|
|
213
220
|
and expected_session_id is not None
|
|
@@ -223,13 +230,7 @@ def _run_tmux_client_with_watchdog(
|
|
|
223
230
|
start_relay_client,
|
|
224
231
|
)
|
|
225
232
|
|
|
226
|
-
print(
|
|
227
|
-
"info: direct tmux attach was unavailable; reconnecting "
|
|
228
|
-
"through the Windows terminal bridge",
|
|
229
|
-
file=sys.stderr,
|
|
230
|
-
)
|
|
231
233
|
try:
|
|
232
|
-
tty.setraw(sys.stdin.fileno())
|
|
233
234
|
relay = start_relay_client(
|
|
234
235
|
target=expected_target,
|
|
235
236
|
session_id=expected_session_id,
|
|
@@ -237,6 +238,15 @@ def _run_tmux_client_with_watchdog(
|
|
|
237
238
|
stdin_fd=sys.stdin.fileno(),
|
|
238
239
|
stdout_fd=sys.stdout.fileno(),
|
|
239
240
|
)
|
|
241
|
+
print(
|
|
242
|
+
command_status(
|
|
243
|
+
"info",
|
|
244
|
+
"connected through the Windows terminal bridge",
|
|
245
|
+
stream=sys.stderr,
|
|
246
|
+
),
|
|
247
|
+
file=sys.stderr,
|
|
248
|
+
)
|
|
249
|
+
tty.setraw(sys.stdin.fileno())
|
|
240
250
|
process = relay
|
|
241
251
|
returncode, watchdog_failed = monitor_client(
|
|
242
252
|
asynchronous_probe=True)
|
|
@@ -296,6 +306,14 @@ def _run_tmux_client_with_watchdog(
|
|
|
296
306
|
),
|
|
297
307
|
consecutive_failures=1,
|
|
298
308
|
)
|
|
309
|
+
if not relay_attempted:
|
|
310
|
+
print(
|
|
311
|
+
"error: direct tmux attach was rejected and the "
|
|
312
|
+
"Windows terminal bridge was unavailable; the "
|
|
313
|
+
"existing workspace was left running; run "
|
|
314
|
+
"'railmux doctor' for diagnostics",
|
|
315
|
+
file=sys.stderr,
|
|
316
|
+
)
|
|
299
317
|
# Routine post-exit removal of a proof-authorized last-session socket
|
|
300
318
|
# is housekeeping. Only pre-launch recovery is user-visible.
|
|
301
319
|
windows_tmux_lifecycle.recover_abandoned_socket()
|
|
@@ -593,14 +611,28 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
593
611
|
if Path(sys.argv[0]).name == "__main__.py"
|
|
594
612
|
else [sys.argv[0]]
|
|
595
613
|
)
|
|
596
|
-
|
|
614
|
+
client_env = tmux_server.exec_environment()
|
|
597
615
|
dedicated_session_id = (
|
|
598
616
|
tmux_server.target_session_id(dedicated_target, "railmux")
|
|
599
617
|
if dedicated_target is not None else None
|
|
600
618
|
)
|
|
619
|
+
if dedicated_target is not None and dedicated_session_id is None:
|
|
620
|
+
from railmux.provider_paths import (
|
|
621
|
+
running_in_managed_windows_wrapper,
|
|
622
|
+
)
|
|
623
|
+
if running_in_managed_windows_wrapper():
|
|
624
|
+
dedicated_session_id = (
|
|
625
|
+
tmux_server.ensure_detached_launcher_session(
|
|
626
|
+
dedicated_target,
|
|
627
|
+
launch_prefix,
|
|
628
|
+
raw_args,
|
|
629
|
+
env=client_env,
|
|
630
|
+
)
|
|
631
|
+
)
|
|
632
|
+
cmd = tmux_server.launcher_argv(launch_prefix, raw_args)
|
|
601
633
|
return _run_tmux_client_with_watchdog(
|
|
602
634
|
cmd,
|
|
603
|
-
|
|
635
|
+
client_env,
|
|
604
636
|
expected_target=dedicated_target,
|
|
605
637
|
expected_session_id=dedicated_session_id,
|
|
606
638
|
)
|
|
@@ -645,3 +645,87 @@ def launcher_argv(
|
|
|
645
645
|
"--inside-tmux",
|
|
646
646
|
*forwarded_args,
|
|
647
647
|
)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
_MANAGED_WINDOWS_SESSION_ENV = (
|
|
651
|
+
"RAILMUX_WINDOWS_RUNTIME",
|
|
652
|
+
"RAILMUX_MSYS2_RUNTIME_ID",
|
|
653
|
+
"RAILMUX_MSYS2_APP_ID",
|
|
654
|
+
)
|
|
655
|
+
_MANAGED_WINDOWS_SESSION_VALUE_RE = re.compile(
|
|
656
|
+
r"[A-Za-z0-9_.-]{1,128}\Z"
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _managed_windows_session_environment_args(
|
|
661
|
+
env: Mapping[str, str] | None,
|
|
662
|
+
) -> list[str]:
|
|
663
|
+
"""Return bounded, non-secret tmux ``new-session -e`` arguments.
|
|
664
|
+
|
|
665
|
+
A detached outer session can be created by an existing dedicated server
|
|
666
|
+
whose inherited environment belongs to an older Railmux app layer. Pass
|
|
667
|
+
only the three independently verified managed-runtime identity hints to
|
|
668
|
+
the new pane; provider credentials and the rest of the caller environment
|
|
669
|
+
must remain outside tmux metadata.
|
|
670
|
+
"""
|
|
671
|
+
if env is None or env.get("RAILMUX_WINDOWS_RUNTIME") != "msys2":
|
|
672
|
+
return []
|
|
673
|
+
result: list[str] = []
|
|
674
|
+
for name in _MANAGED_WINDOWS_SESSION_ENV:
|
|
675
|
+
value = env.get(name)
|
|
676
|
+
if (not isinstance(value, str)
|
|
677
|
+
or _MANAGED_WINDOWS_SESSION_VALUE_RE.fullmatch(value) is None):
|
|
678
|
+
continue
|
|
679
|
+
result.extend(["-e", f"{name}={value}"])
|
|
680
|
+
return result
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def ensure_detached_launcher_session(
|
|
684
|
+
target: TmuxServerTarget,
|
|
685
|
+
launch_prefix: Sequence[str],
|
|
686
|
+
forwarded_args: Sequence[str],
|
|
687
|
+
*,
|
|
688
|
+
env: Mapping[str, str] | None = None,
|
|
689
|
+
timeout: float = 5.0,
|
|
690
|
+
) -> str | None:
|
|
691
|
+
"""Create the outer UI detached on one already-proven server.
|
|
692
|
+
|
|
693
|
+
Managed Windows uses this only when a proven existing server has no outer
|
|
694
|
+
UI, commonly because Soft Quit left provider sessions alive in another
|
|
695
|
+
Terminal Services session. A new tmux client can then reject
|
|
696
|
+
``new-session -A`` before the outer session exists, so the server-origin
|
|
697
|
+
terminal bridge has no immutable session to attach.
|
|
698
|
+
|
|
699
|
+
An exact pre/post server-PID check fences the mutation. A concurrent
|
|
700
|
+
launcher may win the create race; resolving one exact resulting session is
|
|
701
|
+
success regardless of this client's ``new-session`` return code.
|
|
702
|
+
"""
|
|
703
|
+
existing = target_session_id(target, "railmux", timeout=0.5)
|
|
704
|
+
if existing is not None:
|
|
705
|
+
return existing
|
|
706
|
+
if not launch_prefix or not target_is_live(target, timeout=1.0, env=env):
|
|
707
|
+
return None
|
|
708
|
+
try:
|
|
709
|
+
subprocess.run(
|
|
710
|
+
target_argv(
|
|
711
|
+
target,
|
|
712
|
+
"new-session",
|
|
713
|
+
"-d",
|
|
714
|
+
"-s",
|
|
715
|
+
"railmux",
|
|
716
|
+
*_managed_windows_session_environment_args(env),
|
|
717
|
+
*launch_prefix,
|
|
718
|
+
"--inside-tmux",
|
|
719
|
+
*forwarded_args,
|
|
720
|
+
),
|
|
721
|
+
stdout=subprocess.DEVNULL,
|
|
722
|
+
stderr=subprocess.DEVNULL,
|
|
723
|
+
timeout=timeout,
|
|
724
|
+
check=False,
|
|
725
|
+
env=None if env is None else dict(env),
|
|
726
|
+
)
|
|
727
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
728
|
+
return None
|
|
729
|
+
if not target_is_live(target, timeout=1.0, env=env):
|
|
730
|
+
return None
|
|
731
|
+
return target_session_id(target, "railmux", timeout=1.0)
|
|
@@ -902,6 +902,7 @@ class App:
|
|
|
902
902
|
self._projected_target_pane_id: str | None = None
|
|
903
903
|
self._target_toggle_warning_shown = False
|
|
904
904
|
self._status_navigation_warning_shown = False
|
|
905
|
+
self._status_navigation_projected = False
|
|
905
906
|
self._teardown_core_done: bool = False
|
|
906
907
|
self._outer_teardown_done: bool = False
|
|
907
908
|
self._exit_in_progress: bool = False
|
|
@@ -2523,11 +2524,7 @@ class App:
|
|
|
2523
2524
|
"warn",
|
|
2524
2525
|
)
|
|
2525
2526
|
if manager.status_navigation_available:
|
|
2526
|
-
|
|
2527
|
-
# acquired. Repaint once ownership is known so its wide
|
|
2528
|
-
# Mode/Layout ranges (and compact page ranges) are immediately
|
|
2529
|
-
# live rather than waiting for the next unrelated transition.
|
|
2530
|
-
self._apply_tmux_bar(self._tmux_error_bar)
|
|
2527
|
+
self._project_status_navigation()
|
|
2531
2528
|
elif (running_in_windows_wrapper()
|
|
2532
2529
|
and not self._status_navigation_warning_shown):
|
|
2533
2530
|
self._status_navigation_warning_shown = True
|
|
@@ -2538,6 +2535,35 @@ class App:
|
|
|
2538
2535
|
)
|
|
2539
2536
|
self._sync_termux_tap_route()
|
|
2540
2537
|
|
|
2538
|
+
def _project_status_navigation(self) -> None:
|
|
2539
|
+
"""Publish clickable ranges after both lease and status bar exist.
|
|
2540
|
+
|
|
2541
|
+
The Windows worker can finish before ``run`` enables the session-local
|
|
2542
|
+
bar. Its immediate projection is then intentionally a no-op, so the
|
|
2543
|
+
periodic caller retries until the first live bar has received both its
|
|
2544
|
+
left navigation and unchanged right-side Copy range.
|
|
2545
|
+
"""
|
|
2546
|
+
if getattr(self, "_status_navigation_projected", False):
|
|
2547
|
+
return
|
|
2548
|
+
manager = getattr(self, "_tmux_binding_manager", None)
|
|
2549
|
+
if (not getattr(self, "_tmux_status_enabled", False)
|
|
2550
|
+
or getattr(manager, "status_navigation_available", False)
|
|
2551
|
+
is not True):
|
|
2552
|
+
return
|
|
2553
|
+
# The initial bar is painted before the shared binding lease is
|
|
2554
|
+
# acquired. Invalidate its cache so the same visible labels are still
|
|
2555
|
+
# rewritten with the newly available range metadata.
|
|
2556
|
+
self._applied_tmux_bar_state = None
|
|
2557
|
+
self._apply_tmux_bar(self._tmux_error_bar)
|
|
2558
|
+
status_text = getattr(self, "_rendered_status_text", "")
|
|
2559
|
+
if status_text:
|
|
2560
|
+
self._render_status_to_tmux(
|
|
2561
|
+
status_text,
|
|
2562
|
+
getattr(self, "_rendered_status_level", "info"),
|
|
2563
|
+
remember=False,
|
|
2564
|
+
)
|
|
2565
|
+
self._status_navigation_projected = True
|
|
2566
|
+
|
|
2541
2567
|
def _start_windows_tmux_setup(self) -> None:
|
|
2542
2568
|
"""Prepare expensive shared tmux leases without delaying first paint."""
|
|
2543
2569
|
if not running_in_windows_wrapper():
|
|
@@ -11154,6 +11180,7 @@ class App:
|
|
|
11154
11180
|
|
|
11155
11181
|
def _on_tick(self, loop, _user_data) -> None:
|
|
11156
11182
|
self._refresh()
|
|
11183
|
+
self._project_status_navigation()
|
|
11157
11184
|
if self._pending_restore_state is not None:
|
|
11158
11185
|
# A portable Codex preview may have been waiting for the first
|
|
11159
11186
|
# immutable history generation even when no live recovery
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import subprocess
|
|
2
3
|
from unittest.mock import MagicMock, patch
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
@@ -472,6 +473,73 @@ def test_prelaunch_recovery_is_scoped_to_the_dedicated_server(monkeypatch):
|
|
|
472
473
|
assert os.environ["TMUX_PANE"] == "%9"
|
|
473
474
|
|
|
474
475
|
|
|
476
|
+
def test_managed_windows_precreates_missing_outer_session_for_bridge(
|
|
477
|
+
monkeypatch,
|
|
478
|
+
):
|
|
479
|
+
target = TmuxServerTarget("/tmp/tmux-private/railmux", 789)
|
|
480
|
+
monkeypatch.setattr(
|
|
481
|
+
"railmux.cli.tmux_server.discover_target", lambda: target)
|
|
482
|
+
monkeypatch.setattr(
|
|
483
|
+
"railmux.cli.tmux_server.is_current_server", lambda _target: False)
|
|
484
|
+
monkeypatch.setattr(
|
|
485
|
+
"railmux.cli.tmux_server.target_session_id", lambda *_a, **_kw: None)
|
|
486
|
+
monkeypatch.setattr("railmux.cli.sys.argv", ["/opt/railmux/bin/railmux"])
|
|
487
|
+
monkeypatch.setattr(
|
|
488
|
+
"railmux.provider_paths.running_in_managed_windows_wrapper",
|
|
489
|
+
lambda: True,
|
|
490
|
+
)
|
|
491
|
+
prepared = MagicMock(return_value="$9")
|
|
492
|
+
monkeypatch.setattr(
|
|
493
|
+
"railmux.cli.tmux_server.ensure_detached_launcher_session", prepared)
|
|
494
|
+
run_client = MagicMock(return_value=0)
|
|
495
|
+
monkeypatch.setattr(
|
|
496
|
+
"railmux.cli._run_tmux_client_with_watchdog", run_client)
|
|
497
|
+
|
|
498
|
+
assert main(["--project", "/work"]) == 0
|
|
499
|
+
|
|
500
|
+
client_env = tmux_server.exec_environment()
|
|
501
|
+
prepared.assert_called_once_with(
|
|
502
|
+
target,
|
|
503
|
+
["/opt/railmux/bin/railmux"],
|
|
504
|
+
["--project", "/work"],
|
|
505
|
+
env=client_env,
|
|
506
|
+
)
|
|
507
|
+
assert run_client.call_args.kwargs == {
|
|
508
|
+
"expected_target": target,
|
|
509
|
+
"expected_session_id": "$9",
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def test_posix_missing_outer_session_keeps_ordinary_new_session_path(
|
|
514
|
+
monkeypatch,
|
|
515
|
+
):
|
|
516
|
+
target = TmuxServerTarget("/tmp/tmux-private/railmux", 789)
|
|
517
|
+
monkeypatch.setattr(
|
|
518
|
+
"railmux.cli.tmux_server.discover_target", lambda: target)
|
|
519
|
+
monkeypatch.setattr(
|
|
520
|
+
"railmux.cli.tmux_server.is_current_server", lambda _target: False)
|
|
521
|
+
monkeypatch.setattr(
|
|
522
|
+
"railmux.cli.tmux_server.target_session_id", lambda *_a, **_kw: None)
|
|
523
|
+
monkeypatch.setattr(
|
|
524
|
+
"railmux.provider_paths.running_in_managed_windows_wrapper",
|
|
525
|
+
lambda: False,
|
|
526
|
+
)
|
|
527
|
+
prepared = MagicMock()
|
|
528
|
+
monkeypatch.setattr(
|
|
529
|
+
"railmux.cli.tmux_server.ensure_detached_launcher_session", prepared)
|
|
530
|
+
run_client = MagicMock(return_value=0)
|
|
531
|
+
monkeypatch.setattr(
|
|
532
|
+
"railmux.cli._run_tmux_client_with_watchdog", run_client)
|
|
533
|
+
|
|
534
|
+
assert main([]) == 0
|
|
535
|
+
|
|
536
|
+
prepared.assert_not_called()
|
|
537
|
+
assert run_client.call_args.kwargs == {
|
|
538
|
+
"expected_target": target,
|
|
539
|
+
"expected_session_id": None,
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
|
|
475
543
|
def test_local_tmux_watchdog_exits_and_records_after_consecutive_failures(
|
|
476
544
|
monkeypatch, capsys,
|
|
477
545
|
):
|
|
@@ -641,8 +709,8 @@ def test_managed_windows_fast_attach_rejection_uses_one_terminal_bridge(
|
|
|
641
709
|
self.closed = True
|
|
642
710
|
|
|
643
711
|
relay = SuccessfulRelay()
|
|
644
|
-
|
|
645
|
-
|
|
712
|
+
popen = MagicMock(return_value=ExitedClient())
|
|
713
|
+
monkeypatch.setattr("railmux.cli.subprocess.Popen", popen)
|
|
646
714
|
monkeypatch.setattr("railmux.cli.sys.stdin.isatty", lambda: True)
|
|
647
715
|
monkeypatch.setattr("railmux.cli.sys.stdout.isatty", lambda: True)
|
|
648
716
|
monkeypatch.setattr("railmux.cli.sys.stdin.fileno", lambda: 10)
|
|
@@ -682,11 +750,55 @@ def test_managed_windows_fast_attach_rejection_uses_one_terminal_bridge(
|
|
|
682
750
|
assert relay.closed
|
|
683
751
|
recover.assert_called_once_with()
|
|
684
752
|
record.assert_not_called()
|
|
753
|
+
assert popen.call_args.kwargs["stderr"] is subprocess.DEVNULL
|
|
685
754
|
stderr = capsys.readouterr().err
|
|
686
755
|
assert "Windows terminal bridge" in stderr
|
|
756
|
+
assert "direct tmux attach was unavailable" not in stderr
|
|
757
|
+
assert "open terminal failed" not in stderr
|
|
687
758
|
assert "cleaned an abandoned" not in stderr
|
|
688
759
|
|
|
689
760
|
|
|
761
|
+
def test_managed_windows_attach_without_bridge_is_actionable(
|
|
762
|
+
monkeypatch, capsys,
|
|
763
|
+
):
|
|
764
|
+
target = TmuxServerTarget("/tmp/railmux", 77)
|
|
765
|
+
|
|
766
|
+
class ExitedClient:
|
|
767
|
+
returncode = 1
|
|
768
|
+
|
|
769
|
+
def poll(self):
|
|
770
|
+
return self.returncode
|
|
771
|
+
|
|
772
|
+
popen = MagicMock(return_value=ExitedClient())
|
|
773
|
+
monkeypatch.setattr("railmux.cli.subprocess.Popen", popen)
|
|
774
|
+
monkeypatch.setattr("railmux.cli.sys.stdin.isatty", lambda: False)
|
|
775
|
+
monkeypatch.setattr(
|
|
776
|
+
"railmux.cli.tmux_server.discover_target", lambda **_kw: target)
|
|
777
|
+
monkeypatch.setattr(
|
|
778
|
+
"railmux.provider_paths.running_in_managed_windows_wrapper",
|
|
779
|
+
lambda: True,
|
|
780
|
+
)
|
|
781
|
+
record = MagicMock(return_value=True)
|
|
782
|
+
monkeypatch.setattr("railmux.cli.tmux_health.record_incident", record)
|
|
783
|
+
|
|
784
|
+
assert _run_tmux_client_with_watchdog(
|
|
785
|
+
["tmux", "-L", "railmux"],
|
|
786
|
+
{"RAILMUX_WINDOWS_RUNTIME": "msys2"},
|
|
787
|
+
expected_target=target,
|
|
788
|
+
expected_session_id="$7",
|
|
789
|
+
) == 1
|
|
790
|
+
|
|
791
|
+
assert popen.call_args.kwargs["stderr"] is subprocess.DEVNULL
|
|
792
|
+
record.assert_called_once_with(
|
|
793
|
+
component="launcher",
|
|
794
|
+
reason="launcher-attach-rejected",
|
|
795
|
+
consecutive_failures=1,
|
|
796
|
+
)
|
|
797
|
+
stderr = capsys.readouterr().err
|
|
798
|
+
assert "bridge was unavailable" in stderr
|
|
799
|
+
assert "run 'railmux doctor'" in stderr
|
|
800
|
+
|
|
801
|
+
|
|
690
802
|
def test_invalid_config_is_actionable_without_traceback(
|
|
691
803
|
monkeypatch, tmp_path, capsys,
|
|
692
804
|
):
|
|
@@ -108,6 +108,67 @@ def test_windows_unavailable_status_clicks_warn_with_keyboard_fallback(
|
|
|
108
108
|
)
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
def test_finished_status_lease_repaints_left_and_current_copy_range():
|
|
112
|
+
manager = MagicMock()
|
|
113
|
+
manager.target_toggle_available = True
|
|
114
|
+
manager.status_navigation_available = True
|
|
115
|
+
manager.selection_isolation_available = False
|
|
116
|
+
app = _bare_app(
|
|
117
|
+
_tmux_binding_manager=manager,
|
|
118
|
+
_selection_isolation_manager=None,
|
|
119
|
+
_tmux_status_enabled=False,
|
|
120
|
+
_tmux_error_bar=False,
|
|
121
|
+
_applied_tmux_bar_state=("stale",),
|
|
122
|
+
_rendered_status_text="Restored session safely",
|
|
123
|
+
_rendered_status_level="success",
|
|
124
|
+
)
|
|
125
|
+
app._apply_tmux_bar = MagicMock()
|
|
126
|
+
app._render_status_to_tmux = MagicMock()
|
|
127
|
+
app._sync_target_pane_option = MagicMock()
|
|
128
|
+
app._sync_termux_tap_route = MagicMock()
|
|
129
|
+
|
|
130
|
+
app._finish_tmux_bindings(True)
|
|
131
|
+
|
|
132
|
+
app._apply_tmux_bar.assert_not_called()
|
|
133
|
+
app._render_status_to_tmux.assert_not_called()
|
|
134
|
+
assert getattr(app, "_status_navigation_projected", False) is False
|
|
135
|
+
|
|
136
|
+
# The worker may finish before run() enables its session-local status bar.
|
|
137
|
+
# The first periodic reconciliation must project the same remembered text.
|
|
138
|
+
app._tmux_status_enabled = True
|
|
139
|
+
app._project_status_navigation()
|
|
140
|
+
|
|
141
|
+
assert app._applied_tmux_bar_state is None
|
|
142
|
+
assert app._status_navigation_projected is True
|
|
143
|
+
app._apply_tmux_bar.assert_called_once_with(False)
|
|
144
|
+
app._render_status_to_tmux.assert_called_once_with(
|
|
145
|
+
"Restored session safely", "success", remember=False,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_finished_status_lease_does_not_invent_empty_copy_source():
|
|
150
|
+
manager = MagicMock()
|
|
151
|
+
manager.target_toggle_available = True
|
|
152
|
+
manager.status_navigation_available = True
|
|
153
|
+
manager.selection_isolation_available = False
|
|
154
|
+
app = _bare_app(
|
|
155
|
+
_tmux_binding_manager=manager,
|
|
156
|
+
_selection_isolation_manager=None,
|
|
157
|
+
_tmux_status_enabled=True,
|
|
158
|
+
_tmux_error_bar=False,
|
|
159
|
+
_rendered_status_text="",
|
|
160
|
+
)
|
|
161
|
+
app._apply_tmux_bar = MagicMock()
|
|
162
|
+
app._render_status_to_tmux = MagicMock()
|
|
163
|
+
app._sync_target_pane_option = MagicMock()
|
|
164
|
+
app._sync_termux_tap_route = MagicMock()
|
|
165
|
+
|
|
166
|
+
app._finish_tmux_bindings(True)
|
|
167
|
+
|
|
168
|
+
app._apply_tmux_bar.assert_called_once_with(False)
|
|
169
|
+
app._render_status_to_tmux.assert_not_called()
|
|
170
|
+
|
|
171
|
+
|
|
111
172
|
def test_slow_windows_startup_summary_mentions_read_only_cache():
|
|
112
173
|
import time
|
|
113
174
|
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import subprocess
|
|
5
5
|
from types import SimpleNamespace
|
|
6
|
+
from unittest.mock import MagicMock
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
8
9
|
|
|
@@ -37,6 +38,140 @@ def test_launcher_argv_preserves_multi_argument_python_module_prefix():
|
|
|
37
38
|
]
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
def test_detached_launcher_session_is_pinned_before_and_after_create(
|
|
42
|
+
monkeypatch,
|
|
43
|
+
):
|
|
44
|
+
target = tmux_server.TmuxServerTarget("/tmp/private", 44)
|
|
45
|
+
live = []
|
|
46
|
+
session_ids = iter((None, "$7"))
|
|
47
|
+
# Another launcher can win after our initial absence check. tmux then
|
|
48
|
+
# rejects this duplicate create, while the exact resulting session is the
|
|
49
|
+
# safe shared outcome.
|
|
50
|
+
run = SimpleNamespace(returncode=1)
|
|
51
|
+
launched = []
|
|
52
|
+
|
|
53
|
+
def target_is_live(candidate, **kwargs):
|
|
54
|
+
live.append((candidate, kwargs))
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
monkeypatch.setattr(tmux_server, "target_is_live", target_is_live)
|
|
58
|
+
monkeypatch.setattr(
|
|
59
|
+
tmux_server,
|
|
60
|
+
"target_session_id",
|
|
61
|
+
lambda *_args, **_kwargs: next(session_ids),
|
|
62
|
+
)
|
|
63
|
+
monkeypatch.setattr(
|
|
64
|
+
subprocess,
|
|
65
|
+
"run",
|
|
66
|
+
lambda argv, **kwargs: launched.append((argv, kwargs)) or run,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
assert tmux_server.ensure_detached_launcher_session(
|
|
70
|
+
target,
|
|
71
|
+
["/opt/railmux/bin/python", "-m", "railmux"],
|
|
72
|
+
["--mode", "codex"],
|
|
73
|
+
env={"PATH": "/usr/bin"},
|
|
74
|
+
) == "$7"
|
|
75
|
+
|
|
76
|
+
assert len(live) == 2
|
|
77
|
+
assert launched[0][0] == [
|
|
78
|
+
"tmux", "-S", "/tmp/private",
|
|
79
|
+
"new-session", "-d", "-s", "railmux",
|
|
80
|
+
"/opt/railmux/bin/python", "-m", "railmux", "--inside-tmux",
|
|
81
|
+
"--mode", "codex",
|
|
82
|
+
]
|
|
83
|
+
assert launched[0][1]["env"] == {"PATH": "/usr/bin"}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_detached_managed_windows_session_receives_only_runtime_identity(
|
|
87
|
+
monkeypatch,
|
|
88
|
+
):
|
|
89
|
+
target = tmux_server.TmuxServerTarget("/tmp/private", 44)
|
|
90
|
+
session_ids = iter((None, "$7"))
|
|
91
|
+
launched = []
|
|
92
|
+
monkeypatch.setattr(tmux_server, "target_is_live", lambda *_a, **_k: True)
|
|
93
|
+
monkeypatch.setattr(
|
|
94
|
+
tmux_server,
|
|
95
|
+
"target_session_id",
|
|
96
|
+
lambda *_args, **_kwargs: next(session_ids),
|
|
97
|
+
)
|
|
98
|
+
monkeypatch.setattr(
|
|
99
|
+
subprocess,
|
|
100
|
+
"run",
|
|
101
|
+
lambda argv, **kwargs: launched.append((argv, kwargs))
|
|
102
|
+
or SimpleNamespace(returncode=0),
|
|
103
|
+
)
|
|
104
|
+
env = {
|
|
105
|
+
"RAILMUX_WINDOWS_RUNTIME": "msys2",
|
|
106
|
+
"RAILMUX_MSYS2_RUNTIME_ID": "msys2-2026-03-22",
|
|
107
|
+
"RAILMUX_MSYS2_APP_ID": "railmux-0.4.0.dev19",
|
|
108
|
+
"CODEX_API_KEY": "must-not-enter-tmux",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
assert tmux_server.ensure_detached_launcher_session(
|
|
112
|
+
target, ["/opt/railmux/bin/railmux"], [], env=env,
|
|
113
|
+
) == "$7"
|
|
114
|
+
|
|
115
|
+
argv = launched[0][0]
|
|
116
|
+
assert argv == [
|
|
117
|
+
"tmux", "-S", "/tmp/private", "new-session", "-d", "-s",
|
|
118
|
+
"railmux",
|
|
119
|
+
"-e", "RAILMUX_WINDOWS_RUNTIME=msys2",
|
|
120
|
+
"-e", "RAILMUX_MSYS2_RUNTIME_ID=msys2-2026-03-22",
|
|
121
|
+
"-e", "RAILMUX_MSYS2_APP_ID=railmux-0.4.0.dev19",
|
|
122
|
+
"/opt/railmux/bin/railmux", "--inside-tmux",
|
|
123
|
+
]
|
|
124
|
+
assert all("CODEX_API_KEY" not in value for value in argv)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_detached_session_rejects_unbounded_runtime_identity(monkeypatch):
|
|
128
|
+
target = tmux_server.TmuxServerTarget("/tmp/private", 44)
|
|
129
|
+
session_ids = iter((None, "$7"))
|
|
130
|
+
launched = []
|
|
131
|
+
monkeypatch.setattr(tmux_server, "target_is_live", lambda *_a, **_k: True)
|
|
132
|
+
monkeypatch.setattr(
|
|
133
|
+
tmux_server,
|
|
134
|
+
"target_session_id",
|
|
135
|
+
lambda *_args, **_kwargs: next(session_ids),
|
|
136
|
+
)
|
|
137
|
+
monkeypatch.setattr(
|
|
138
|
+
subprocess,
|
|
139
|
+
"run",
|
|
140
|
+
lambda argv, **kwargs: launched.append(argv)
|
|
141
|
+
or SimpleNamespace(returncode=0),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
assert tmux_server.ensure_detached_launcher_session(
|
|
145
|
+
target,
|
|
146
|
+
["railmux"],
|
|
147
|
+
[],
|
|
148
|
+
env={
|
|
149
|
+
"RAILMUX_WINDOWS_RUNTIME": "msys2",
|
|
150
|
+
"RAILMUX_MSYS2_APP_ID": "bad=value",
|
|
151
|
+
},
|
|
152
|
+
) == "$7"
|
|
153
|
+
|
|
154
|
+
assert "RAILMUX_WINDOWS_RUNTIME=msys2" in launched[0]
|
|
155
|
+
assert all("bad=value" not in value for value in launched[0])
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_detached_launcher_session_refuses_changed_server_identity(
|
|
159
|
+
monkeypatch,
|
|
160
|
+
):
|
|
161
|
+
target = tmux_server.TmuxServerTarget("/tmp/private", 44)
|
|
162
|
+
session_id = MagicMock(return_value=None)
|
|
163
|
+
monkeypatch.setattr(tmux_server, "target_session_id", session_id)
|
|
164
|
+
monkeypatch.setattr(
|
|
165
|
+
tmux_server, "target_is_live", MagicMock(side_effect=(True, False)))
|
|
166
|
+
monkeypatch.setattr(
|
|
167
|
+
subprocess, "run", MagicMock(return_value=SimpleNamespace(returncode=0)))
|
|
168
|
+
|
|
169
|
+
assert tmux_server.ensure_detached_launcher_session(
|
|
170
|
+
target, ["railmux"], [],
|
|
171
|
+
) is None
|
|
172
|
+
session_id.assert_called_once_with(target, "railmux", timeout=0.5)
|
|
173
|
+
|
|
174
|
+
|
|
40
175
|
def test_current_socket_parser_allows_commas_in_the_path():
|
|
41
176
|
env = {"TMUX": "/tmp/with,comma/railmux,123,0"}
|
|
42
177
|
assert tmux_server.current_socket_path(env) == "/tmp/with,comma/railmux"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.0.dev17"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|