swarph-cli 0.13.1__tar.gz → 0.13.2__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.
- {swarph_cli-0.13.1/src/swarph_cli.egg-info → swarph_cli-0.13.2}/PKG-INFO +1 -1
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/pyproject.toml +1 -1
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/spawn.py +57 -17
- {swarph_cli-0.13.1 → swarph_cli-0.13.2/src/swarph_cli.egg-info}/PKG-INFO +1 -1
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_spawn_command.py +7 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_spawn_tmux_session.py +51 -14
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/LICENSE +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/README.md +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/setup.cfg +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/hooks.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/mcp_server.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/mesh.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/main.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/SOURCES.txt +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_import_command.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_init_command.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_main.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_security.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_watchdog.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_watchdog_model_rung.py +0 -0
- {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_watchdog_stale_peers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.2
|
|
4
4
|
Summary: The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider `swarph spawn` (claude/codex/antigravity per cell.provider via a ProviderMembrane + subprocess billing-scrub), interactive `swarph init`, `swarph mesh` (send/inbox/register) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.5: foolproof Windows auto-relaunch (genuine-WT detection via process ancestry, not WT_SESSION).
|
|
5
5
|
Author: Pierre Samson, Claude Opus
|
|
6
6
|
License: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "swarph-cli"
|
|
7
|
-
version = "0.13.
|
|
7
|
+
version = "0.13.2"
|
|
8
8
|
description = "The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider `swarph spawn` (claude/codex/antigravity per cell.provider via a ProviderMembrane + subprocess billing-scrub), interactive `swarph init`, `swarph mesh` (send/inbox/register) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.5: foolproof Windows auto-relaunch (genuine-WT detection via process ancestry, not WT_SESSION)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -28,6 +28,7 @@ import os
|
|
|
28
28
|
import shutil
|
|
29
29
|
import subprocess
|
|
30
30
|
import sys
|
|
31
|
+
import time
|
|
31
32
|
from pathlib import Path
|
|
32
33
|
from typing import Optional
|
|
33
34
|
from urllib.parse import quote
|
|
@@ -914,6 +915,54 @@ def _tmux_has_session(tmux: str, name: str) -> bool:
|
|
|
914
915
|
return result.returncode == 0
|
|
915
916
|
|
|
916
917
|
|
|
918
|
+
def _tmux_create_session(tmux: str, name: str, cwd: Path) -> bool:
|
|
919
|
+
"""Create the durable detached session that runs ``swarph spawn <name>``.
|
|
920
|
+
|
|
921
|
+
Robust against a STALE session registration. When a multiplexer server dies
|
|
922
|
+
uncleanly (operator closed the owning console, ``kill``, a crash) it can leave
|
|
923
|
+
the session NAME registered with no live server behind it. On the native-Windows
|
|
924
|
+
psmux build (``marlocarlo.psmux``) this poisons re-creation: ``new-session``
|
|
925
|
+
becomes a SILENT no-op — it returns rc=0 but creates nothing — so the name can
|
|
926
|
+
never come back. That is the exact failure that strands a peer's cell ("the
|
|
927
|
+
session got killed and won't return"; verified on workstation-lc 2026-06-18).
|
|
928
|
+
Real tmux on Linux/mac does not hit this, and the extra steps are a harmless
|
|
929
|
+
no-op there (the create succeeds first try and the loop breaks immediately).
|
|
930
|
+
|
|
931
|
+
So: clear any stale registration first (``kill-session`` — a no-op if the name
|
|
932
|
+
is truly absent), then create. Because psmux clears the stale lock
|
|
933
|
+
ASYNCHRONOUSLY (~1s observed), do NOT trust the create's exit code (rc=0 even on
|
|
934
|
+
the silent no-op): VERIFY with ``has-session`` and retry until it actually
|
|
935
|
+
materialises. Returns True once the session exists, False if it never appears.
|
|
936
|
+
"""
|
|
937
|
+
create_cmd = [
|
|
938
|
+
tmux, "new-session", "-d", "-s", name,
|
|
939
|
+
"-c", str(cwd), "-e", "SWARPH_SPAWN=1",
|
|
940
|
+
"swarph", "spawn", name,
|
|
941
|
+
]
|
|
942
|
+
# Clear a stale (server-less) registration; harmless if the name is truly absent.
|
|
943
|
+
try:
|
|
944
|
+
subprocess.run(
|
|
945
|
+
[tmux, "kill-session", "-t", f"={name}"],
|
|
946
|
+
stdout=subprocess.DEVNULL,
|
|
947
|
+
stderr=subprocess.DEVNULL,
|
|
948
|
+
)
|
|
949
|
+
except OSError:
|
|
950
|
+
return False
|
|
951
|
+
for attempt in range(6):
|
|
952
|
+
try:
|
|
953
|
+
subprocess.run(
|
|
954
|
+
create_cmd,
|
|
955
|
+
stdout=subprocess.DEVNULL,
|
|
956
|
+
stderr=subprocess.PIPE,
|
|
957
|
+
)
|
|
958
|
+
except OSError:
|
|
959
|
+
return False
|
|
960
|
+
if _tmux_has_session(tmux, name):
|
|
961
|
+
return True
|
|
962
|
+
time.sleep(0.4) # let psmux's async stale-lock clear catch up, then retry
|
|
963
|
+
return False
|
|
964
|
+
|
|
965
|
+
|
|
917
966
|
def _launch_via_tmux(
|
|
918
967
|
binary: str, argv: list[str], cwd: Path, session_name: str,
|
|
919
968
|
) -> bool:
|
|
@@ -978,24 +1027,15 @@ def _launch_via_tmux(
|
|
|
978
1027
|
interactive = sys.stdout.isatty()
|
|
979
1028
|
|
|
980
1029
|
if not _tmux_has_session(tmux, session_name):
|
|
981
|
-
# Create the durable session detached. The command re-enters spawn so
|
|
982
|
-
#
|
|
983
|
-
#
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
tmux, "new-session", "-d", "-s", session_name,
|
|
988
|
-
"-c", str(cwd), "-e", "SWARPH_SPAWN=1",
|
|
989
|
-
"swarph", "spawn", session_name,
|
|
990
|
-
],
|
|
991
|
-
check=True,
|
|
992
|
-
stdout=subprocess.DEVNULL,
|
|
993
|
-
stderr=subprocess.PIPE,
|
|
994
|
-
)
|
|
995
|
-
except (OSError, subprocess.CalledProcessError) as exc:
|
|
1030
|
+
# Create the durable session detached. The command re-enters spawn so the
|
|
1031
|
+
# membrane env-scrub + starter injection apply inside the pane; the inherited
|
|
1032
|
+
# $TMUX (and SWARPH_SPAWN) stop it from re-deciding. _tmux_create_session
|
|
1033
|
+
# also clears a stale (server-less) registration that would otherwise make
|
|
1034
|
+
# psmux's new-session a silent no-op and strand the cell.
|
|
1035
|
+
if not _tmux_create_session(tmux, session_name, cwd):
|
|
996
1036
|
print(
|
|
997
|
-
|
|
998
|
-
"back to the standard launch.",
|
|
1037
|
+
"swarph spawn: tmux session create failed (stale registration "
|
|
1038
|
+
"could not be cleared?); falling back to the standard launch.",
|
|
999
1039
|
file=sys.stderr,
|
|
1000
1040
|
)
|
|
1001
1041
|
return False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.2
|
|
4
4
|
Summary: The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider `swarph spawn` (claude/codex/antigravity per cell.provider via a ProviderMembrane + subprocess billing-scrub), interactive `swarph init`, `swarph mesh` (send/inbox/register) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.5: foolproof Windows auto-relaunch (genuine-WT detection via process ancestry, not WT_SESSION).
|
|
5
5
|
Author: Pierre Samson, Claude Opus
|
|
6
6
|
License: MIT
|
|
@@ -1323,6 +1323,13 @@ def test_run_spawn_grok_assisted_memory_no_double_system_prompt(
|
|
|
1323
1323
|
monkeypatch.setattr("shutil.which", lambda name: "/bin/fake-grok")
|
|
1324
1324
|
# neutralize the named-tmux launch so we reach launch()/execve in-test
|
|
1325
1325
|
monkeypatch.setattr(spawn, "_launch_via_tmux", lambda *a, **k: False)
|
|
1326
|
+
# This asserts on the composed argv (no doubled --system-prompt-override),
|
|
1327
|
+
# which is platform-independent. Pin to a POSIX platform so launch() takes the
|
|
1328
|
+
# os.execve branch we mock below — otherwise on Windows it takes the
|
|
1329
|
+
# subprocess.run branch (v0.12.1 pane-collapse fix), the execve mock never
|
|
1330
|
+
# fires, and the real /bin/fake-grok exec fails (WinError 2). Matches the
|
|
1331
|
+
# platform-pinning convention of the launch tests above.
|
|
1332
|
+
monkeypatch.setattr(spawn.sys, "platform", "linux")
|
|
1326
1333
|
|
|
1327
1334
|
exec_args = []
|
|
1328
1335
|
monkeypatch.setattr("os.execve", lambda p, a, e: exec_args.append((p, a, e)))
|
|
@@ -59,19 +59,27 @@ class _FakeStdout:
|
|
|
59
59
|
|
|
60
60
|
def _drive(monkeypatch, *, platform="win32", tmux=TMUX, wt=WT, in_tmux=None,
|
|
61
61
|
spawn_marker=None, isatty=True, session_exists=False,
|
|
62
|
-
new_session_exc=None):
|
|
62
|
+
new_session_exc=None, create_succeeds=True, create_succeeds_after=1):
|
|
63
63
|
"""Drive `_launch_via_tmux` with tmux/subprocess/execv mocked.
|
|
64
64
|
|
|
65
|
-
`run` dispatches on the subcommand
|
|
66
|
-
|
|
67
|
-
`
|
|
68
|
-
|
|
65
|
+
`run` dispatches on the subcommand and is STATEFUL to model reality after the
|
|
66
|
+
stale-registration fix: `has-session` returns rc 0 once the session exists —
|
|
67
|
+
either pre-existing (`session_exists`) OR after a `new-session` "takes".
|
|
68
|
+
`_tmux_create_session` clears a stale reg (`kill-session`, rc 0 here) then
|
|
69
|
+
creates + VERIFIES via `has-session`, retrying because psmux clears the stale
|
|
70
|
+
lock asynchronously. `create_succeeds_after` models that delay: the session
|
|
71
|
+
only materialises on the Nth `new-session` call (1 = first try, the happy
|
|
72
|
+
path). `create_succeeds=False` models a create that never materialises (e.g. a
|
|
73
|
+
stale lock that won't clear) → the loop exhausts and returns False.
|
|
74
|
+
`new-session` raises `new_session_exc` when set. `attach` (Windows) returns rc
|
|
75
|
+
0; POSIX attach is os.execv, mocked to raise `_ExecvReplaced`.
|
|
69
76
|
|
|
70
77
|
Returns `(result, run, execv)`. `result` is the function's bool, or the
|
|
71
78
|
`_EXECV` sentinel when the POSIX execv attach took over.
|
|
72
79
|
"""
|
|
73
80
|
monkeypatch.setattr(spawn.sys, "platform", platform)
|
|
74
81
|
monkeypatch.setattr(spawn.sys, "stdout", _FakeStdout(isatty))
|
|
82
|
+
monkeypatch.setattr(spawn.time, "sleep", lambda *_a, **_k: None) # no real wait
|
|
75
83
|
for var, val in (("TMUX", in_tmux), ("SWARPH_SPAWN", spawn_marker)):
|
|
76
84
|
if val is None:
|
|
77
85
|
monkeypatch.delenv(var, raising=False)
|
|
@@ -83,12 +91,20 @@ def _drive(monkeypatch, *, platform="win32", tmux=TMUX, wt=WT, in_tmux=None,
|
|
|
83
91
|
|
|
84
92
|
monkeypatch.setattr(spawn.shutil, "which", _which)
|
|
85
93
|
|
|
94
|
+
state = {"new_calls": 0, "created": False}
|
|
95
|
+
|
|
86
96
|
def _run(cmd, **kwargs):
|
|
87
97
|
if "has-session" in cmd:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
exists = session_exists or state["created"]
|
|
99
|
+
return MagicMock(returncode=0 if exists else 1)
|
|
100
|
+
if "new-session" in cmd:
|
|
101
|
+
if new_session_exc is not None:
|
|
102
|
+
raise new_session_exc
|
|
103
|
+
state["new_calls"] += 1
|
|
104
|
+
if create_succeeds and state["new_calls"] >= create_succeeds_after:
|
|
105
|
+
state["created"] = True
|
|
106
|
+
return MagicMock(returncode=0)
|
|
107
|
+
return MagicMock(returncode=0) # kill-session, attach, ...
|
|
92
108
|
|
|
93
109
|
run = MagicMock(side_effect=_run)
|
|
94
110
|
monkeypatch.setattr(spawn.subprocess, "run", run)
|
|
@@ -108,6 +124,11 @@ def _cmds(run):
|
|
|
108
124
|
return [c.args[0] for c in run.call_args_list]
|
|
109
125
|
|
|
110
126
|
|
|
127
|
+
def _killed_stale(run):
|
|
128
|
+
"""True if the stale-registration clear (`kill-session`) was issued."""
|
|
129
|
+
return any("kill-session" in c for c in _cmds(run))
|
|
130
|
+
|
|
131
|
+
|
|
111
132
|
def _attached_via_run(run):
|
|
112
133
|
"""True if the Windows blocking-subprocess.run attach fired."""
|
|
113
134
|
return [TMUX, "attach", "-t", SESSION] in _cmds(run)
|
|
@@ -239,17 +260,33 @@ def test_existing_session_headless_is_a_noop_handoff(monkeypatch, platform):
|
|
|
239
260
|
|
|
240
261
|
|
|
241
262
|
@pytest.mark.parametrize("platform", ["win32", "linux"])
|
|
242
|
-
def
|
|
243
|
-
#
|
|
244
|
-
#
|
|
245
|
-
|
|
263
|
+
def test_create_never_materialises_falls_through(monkeypatch, platform):
|
|
264
|
+
# The session never appears (e.g. a psmux stale registration that won't clear:
|
|
265
|
+
# new-session returns rc 0 but creates nothing). The verify+retry loop exhausts
|
|
266
|
+
# => return False so the caller drops to the standard launch; never attach a
|
|
267
|
+
# phantom session. We also issued the stale-clear kill-session first.
|
|
246
268
|
r, run, execv = _drive(monkeypatch, platform=platform,
|
|
247
|
-
session_exists=False,
|
|
269
|
+
session_exists=False, create_succeeds=False)
|
|
248
270
|
assert r is False
|
|
271
|
+
assert _killed_stale(run)
|
|
249
272
|
assert not _attached_via_run(run)
|
|
250
273
|
execv.assert_not_called()
|
|
251
274
|
|
|
252
275
|
|
|
276
|
+
@pytest.mark.parametrize("platform", ["win32", "linux", "darwin"])
|
|
277
|
+
def test_stale_registration_cleared_then_create_retried(monkeypatch, platform):
|
|
278
|
+
# The poisoned-name recovery path: kill-session clears the stale reg, but psmux
|
|
279
|
+
# clears the lock async so the FIRST new-session is a silent no-op; the session
|
|
280
|
+
# only materialises on the 2nd attempt. The verify loop must retry and succeed.
|
|
281
|
+
r, run, execv = _drive(monkeypatch, platform=platform, session_exists=False,
|
|
282
|
+
isatty=False, create_succeeds_after=2)
|
|
283
|
+
assert r is True # headless: created, claimed launch
|
|
284
|
+
assert _killed_stale(run) # stale reg cleared first
|
|
285
|
+
new_calls = [c for c in _cmds(run) if "new-session" in c]
|
|
286
|
+
assert len(new_calls) >= 2 # retried past the silent no-op
|
|
287
|
+
execv.assert_not_called()
|
|
288
|
+
|
|
289
|
+
|
|
253
290
|
@pytest.mark.parametrize("platform", ["win32", "linux"])
|
|
254
291
|
def test_create_oserror_falls_through(monkeypatch, platform):
|
|
255
292
|
r, run, execv = _drive(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|