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.
Files changed (110) hide show
  1. {swarph_cli-0.13.1/src/swarph_cli.egg-info → swarph_cli-0.13.2}/PKG-INFO +1 -1
  2. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/pyproject.toml +1 -1
  3. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/spawn.py +57 -17
  5. {swarph_cli-0.13.1 → swarph_cli-0.13.2/src/swarph_cli.egg-info}/PKG-INFO +1 -1
  6. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_spawn_command.py +7 -0
  7. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_spawn_tmux_session.py +51 -14
  8. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/LICENSE +0 -0
  9. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/README.md +0 -0
  10. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/setup.cfg +0 -0
  11. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/caller.py +0 -0
  12. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/capture/__init__.py +0 -0
  13. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/capture/harden.py +0 -0
  14. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/capture/lineage.py +0 -0
  15. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/capture/liveness.py +0 -0
  16. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/capture/manifest.py +0 -0
  17. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/capture/paths.py +0 -0
  18. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/capture/verify.py +0 -0
  19. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/cell.py +0 -0
  20. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/__init__.py +0 -0
  21. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/add.py +0 -0
  22. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/cell.py +0 -0
  23. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/chat.py +0 -0
  24. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/compress.py +0 -0
  25. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/daemon.py +0 -0
  26. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/hook_output.py +0 -0
  27. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/hooks.py +0 -0
  28. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/import_session.py +0 -0
  29. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/init.py +0 -0
  30. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/install_hook.py +0 -0
  31. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/mcp_server.py +0 -0
  32. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/memory_sync.py +0 -0
  33. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/mesh.py +0 -0
  34. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/onboard.py +0 -0
  35. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/protocol_handler.py +0 -0
  36. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/ratify.py +0 -0
  37. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/security.py +0 -0
  38. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/commands/watchdog.py +0 -0
  39. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/compress/__init__.py +0 -0
  40. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/compress/levers.py +0 -0
  41. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/compress/marker.py +0 -0
  42. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/compress/verify.py +0 -0
  43. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/main.py +0 -0
  44. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/parsers/__init__.py +0 -0
  45. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/parsers/claude.py +0 -0
  46. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  47. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  48. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  49. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/SOURCES.txt +0 -0
  50. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  51. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  52. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/requires.txt +0 -0
  53. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/top_level.txt +0 -0
  54. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_add_security_gate.py +0 -0
  55. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_artifact_add.py +0 -0
  56. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_artifact_hash.py +0 -0
  57. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_artifact_lib.py +0 -0
  58. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_artifact_mcp.py +0 -0
  59. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_artifact_skill.py +0 -0
  60. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_artifact_tool.py +0 -0
  61. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_artifact_uri.py +0 -0
  62. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_capture_lineage.py +0 -0
  63. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_capture_liveness.py +0 -0
  64. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_capture_manifest.py +0 -0
  65. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_capture_paths.py +0 -0
  66. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_capture_security.py +0 -0
  67. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_cell_command_dispatch.py +0 -0
  68. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_cell_harden.py +0 -0
  69. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_cell_loader.py +0 -0
  70. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_cell_verify.py +0 -0
  71. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_chat_command.py +0 -0
  72. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_claude_parser.py +0 -0
  73. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_claude_tmux_template.py +0 -0
  74. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_compress_command.py +0 -0
  75. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_compress_levers.py +0 -0
  76. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_compress_marker.py +0 -0
  77. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_compress_verify.py +0 -0
  78. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_daemon_command.py +0 -0
  79. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_feature_to_uri.py +0 -0
  80. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_hook_output.py +0 -0
  81. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_hooks_add.py +0 -0
  82. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_hooks_bundle.py +0 -0
  83. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_hooks_init.py +0 -0
  84. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_hooks_lifecycle.py +0 -0
  85. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_hooks_merge.py +0 -0
  86. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_import_command.py +0 -0
  87. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_init_command.py +0 -0
  88. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_install_hook.py +0 -0
  89. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_main.py +0 -0
  90. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_mcp_server.py +0 -0
  91. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_memory_sync.py +0 -0
  92. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_mesh_command.py +0 -0
  93. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_mesh_sidecar.py +0 -0
  94. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_onboard_command.py +0 -0
  95. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_protocol_handler.py +0 -0
  96. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_ratify_command.py +0 -0
  97. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_security.py +0 -0
  98. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_session_path_role_gate.py +0 -0
  99. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_smoke_chat.py +0 -0
  100. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_smoke_one_shot.py +0 -0
  101. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_smoke_phase_5_5.py +0 -0
  102. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_spawn_live_pin.py +0 -0
  103. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_spawn_mitosis_lineage.py +0 -0
  104. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_spawn_windows_relaunch.py +0 -0
  105. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_watchdog.py +0 -0
  106. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_watchdog_dm_wake.py +0 -0
  107. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  108. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  109. {swarph_cli-0.13.1 → swarph_cli-0.13.2}/tests/test_watchdog_model_rung.py +0 -0
  110. {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.1
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.1"
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" }
@@ -16,6 +16,6 @@ The architecture splits CLI from substrate so:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- __version__ = "0.13.1"
19
+ __version__ = "0.13.2"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -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
- # the membrane env-scrub + starter injection apply inside the pane; the
983
- # inherited $TMUX (and SWARPH_SPAWN below) stop it from re-deciding.
984
- try:
985
- subprocess.run(
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
- f"swarph spawn: tmux session create failed ({exc}); falling "
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.1
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: `has-session` returns rc 0/1 per
66
- `session_exists`; `new-session` returns rc 0 (or raises `new_session_exc`);
67
- `attach` (Windows path only) returns rc 0. The POSIX attach is os.execv,
68
- mocked to raise `_ExecvReplaced` (simulating its never-returns replace).
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
- return MagicMock(returncode=0 if session_exists else 1)
89
- if "new-session" in cmd and new_session_exc is not None:
90
- raise new_session_exc
91
- return MagicMock(returncode=0)
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 test_create_failure_falls_through(monkeypatch, platform):
243
- # tmux new-session errors => return False so the caller drops to the standard
244
- # launch; never attach a half-created session.
245
- boom = subprocess.CalledProcessError(1, "tmux")
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, new_session_exc=boom)
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