swarph-cli 0.7.3__tar.gz → 0.7.5__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.7.3/src/swarph_cli.egg-info → swarph_cli-0.7.5}/PKG-INFO +1 -1
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/pyproject.toml +1 -1
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/commands/spawn.py +36 -1
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/commands/watchdog.py +44 -2
- {swarph_cli-0.7.3 → swarph_cli-0.7.5/src/swarph_cli.egg-info}/PKG-INFO +1 -1
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_spawn_command.py +89 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_watchdog.py +88 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/LICENSE +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/README.md +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/setup.cfg +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/main.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli.egg-info/SOURCES.txt +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_import_command.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_main.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.7.3 → swarph_cli-0.7.5}/tests/test_smoke_phase_5_5.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.5
|
|
4
4
|
Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.7.0 ships Phase 7 substrate-doc R7 §11.1.7 operator-tooling layer in 5 increments: PR-A `--new-instance` flag (sibling-spawn case) + PR-B auto-suffix on collision (sibling-slot persistence) + PR-C SessionStart hook (closes bare-claude operator-paste gap) + watchdog (stranded-session recovery) + PR-D swarph-shared cell.yaml relocation (cell-yaml schema graduates to swarph-shared 0.3.0 kernel-tier; substrate-doc R7 §11.1.5 (O5) RESOLVED).
|
|
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.7.
|
|
7
|
+
version = "0.7.5"
|
|
8
8
|
description = "The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.7.0 ships Phase 7 substrate-doc R7 §11.1.7 operator-tooling layer in 5 increments: PR-A `--new-instance` flag (sibling-spawn case) + PR-B auto-suffix on collision (sibling-slot persistence) + PR-C SessionStart hook (closes bare-claude operator-paste gap) + watchdog (stranded-session recovery) + PR-D swarph-shared cell.yaml relocation (cell-yaml schema graduates to swarph-shared 0.3.0 kernel-tier; substrate-doc R7 §11.1.5 (O5) RESOLVED)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -27,6 +27,7 @@ import argparse
|
|
|
27
27
|
import os
|
|
28
28
|
import shutil
|
|
29
29
|
import sys
|
|
30
|
+
from pathlib import Path
|
|
30
31
|
from typing import Optional
|
|
31
32
|
|
|
32
33
|
from swarph_cli import __version__
|
|
@@ -200,6 +201,33 @@ def _resolve_cell(args: argparse.Namespace) -> tuple[Cell, Optional[str]]:
|
|
|
200
201
|
return load_cell(path), requested_role
|
|
201
202
|
|
|
202
203
|
|
|
204
|
+
def _session_state_exists(session_id: str) -> bool:
|
|
205
|
+
"""True if Claude Code already has on-disk session state for this UUID.
|
|
206
|
+
|
|
207
|
+
Closes v0.7.4 spawn-bug surfaced 2026-05-14 post-reboot (DM #1255):
|
|
208
|
+
`claude --session-id <UUID>` rejects with "Session ID <UUID> is already
|
|
209
|
+
in use" when session-state files exist on disk, even after reboot
|
|
210
|
+
(files persist; the in-use check is filesystem-based not runtime-lock-
|
|
211
|
+
based). Switching to `claude --resume <UUID>` is the correct semantic
|
|
212
|
+
when the UUID's state already exists.
|
|
213
|
+
|
|
214
|
+
Probes the three filesystem locations Claude Code stores per-session
|
|
215
|
+
state in: ~/.claude/file-history/<UUID>, ~/.claude/session-env/<UUID>,
|
|
216
|
+
and ~/.claude/projects/<project-hash>/<UUID>.jsonl (the latter
|
|
217
|
+
discovered via glob since project-hash varies).
|
|
218
|
+
"""
|
|
219
|
+
claude_dir = Path.home() / ".claude"
|
|
220
|
+
if (claude_dir / "file-history" / session_id).exists():
|
|
221
|
+
return True
|
|
222
|
+
if (claude_dir / "session-env" / session_id).exists():
|
|
223
|
+
return True
|
|
224
|
+
projects_dir = claude_dir / "projects"
|
|
225
|
+
if projects_dir.exists():
|
|
226
|
+
for _ in projects_dir.glob(f"*/{session_id}.jsonl"):
|
|
227
|
+
return True
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
|
|
203
231
|
def _build_claude_argv(
|
|
204
232
|
cell: Cell,
|
|
205
233
|
session_id: str,
|
|
@@ -208,7 +236,14 @@ def _build_claude_argv(
|
|
|
208
236
|
effective_role: Optional[str] = None,
|
|
209
237
|
) -> list[str]:
|
|
210
238
|
name_value = effective_role if effective_role is not None else cell.role
|
|
211
|
-
|
|
239
|
+
# v0.7.5: auto-detect existing session state and switch from --session-id
|
|
240
|
+
# (create-new-with-pinned-UUID semantic) to --resume (attach-to-existing
|
|
241
|
+
# semantic). Both pass the same UUID; the verb determines whether claude
|
|
242
|
+
# treats it as fresh-create vs resume-existing.
|
|
243
|
+
if _session_state_exists(session_id):
|
|
244
|
+
argv: list[str] = ["claude", "--name", name_value, "--resume", session_id]
|
|
245
|
+
else:
|
|
246
|
+
argv = ["claude", "--name", name_value, "--session-id", session_id]
|
|
212
247
|
|
|
213
248
|
if not no_starter:
|
|
214
249
|
starter = read_starter_prompt(cell)
|
|
@@ -66,6 +66,7 @@ import argparse
|
|
|
66
66
|
import json
|
|
67
67
|
import os
|
|
68
68
|
import shlex
|
|
69
|
+
import shutil
|
|
69
70
|
import subprocess
|
|
70
71
|
import sys
|
|
71
72
|
import time
|
|
@@ -622,6 +623,35 @@ _SYSTEMD_UNIT_DIR = Path("/etc/systemd/system")
|
|
|
622
623
|
_SYSTEMD_DEFAULT_DIR = Path("/etc/default")
|
|
623
624
|
_SYSTEMD_UNIT_NAMES = ("swarph-watchdog.service", "swarph-watchdog.timer")
|
|
624
625
|
_SYSTEMD_DEFAULT_NAME = "swarph-watchdog" # /etc/default/swarph-watchdog
|
|
626
|
+
# v0.7.3 bundled template's ExecStart placeholder — substituted at install time
|
|
627
|
+
# by `_resolve_swarph_bin()` to the actual binary path on the install host.
|
|
628
|
+
# Fixes the v0.7.3 hardcode that broke pipx-installed peers (binary at
|
|
629
|
+
# ~/.local/bin/swarph not /usr/local/bin/swarph).
|
|
630
|
+
_SWARPH_BIN_PLACEHOLDER = "/usr/local/bin/swarph"
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _resolve_swarph_bin() -> str:
|
|
634
|
+
"""Resolve the absolute path of the running swarph binary.
|
|
635
|
+
|
|
636
|
+
Resolution order:
|
|
637
|
+
1. ``sys.argv[0]`` if it's an absolute path — most reliable, equals
|
|
638
|
+
the path the user invoked
|
|
639
|
+
2. ``shutil.which(sys.argv[0])`` — bare-name invocation, look up in PATH
|
|
640
|
+
3. ``shutil.which("swarph")`` — generic PATH lookup as fallback
|
|
641
|
+
4. ``/usr/local/bin/swarph`` — last-resort default (matches v0.7.3
|
|
642
|
+
hardcode behavior; no regression if all three above fail)
|
|
643
|
+
|
|
644
|
+
ALWAYS returns an absolute path — systemd ExecStart requires absolute.
|
|
645
|
+
Relative inputs (e.g. ``venv/bin/swarph`` from editable installs) get
|
|
646
|
+
abspath'd against cwd. Never raises.
|
|
647
|
+
"""
|
|
648
|
+
invoked = sys.argv[0] or "swarph"
|
|
649
|
+
if Path(invoked).is_absolute():
|
|
650
|
+
return invoked
|
|
651
|
+
resolved = shutil.which(invoked) or shutil.which("swarph")
|
|
652
|
+
if not resolved:
|
|
653
|
+
return _SWARPH_BIN_PLACEHOLDER
|
|
654
|
+
return os.path.abspath(resolved)
|
|
625
655
|
|
|
626
656
|
|
|
627
657
|
def _bundled_systemd_files() -> dict[str, str]:
|
|
@@ -656,6 +686,18 @@ def run_install_service(args: argparse.Namespace) -> int:
|
|
|
656
686
|
"""
|
|
657
687
|
files = _bundled_systemd_files()
|
|
658
688
|
|
|
689
|
+
# v0.7.4: substitute the bundled service template's ExecStart placeholder
|
|
690
|
+
# with the actual swarph binary path on this host. Fixes the v0.7.3 hardcode
|
|
691
|
+
# that broke pipx-installed peers (binary at ~/.local/bin/swarph not
|
|
692
|
+
# /usr/local/bin/swarph). Pipx is the recommended install path on droplet
|
|
693
|
+
# + lab, so the hardcode bit BOTH peers on first install attempt today.
|
|
694
|
+
swarph_bin = _resolve_swarph_bin()
|
|
695
|
+
service_content = files[_SYSTEMD_UNIT_NAMES[0]].replace(
|
|
696
|
+
f"ExecStart={_SWARPH_BIN_PLACEHOLDER}",
|
|
697
|
+
f"ExecStart={swarph_bin}",
|
|
698
|
+
1,
|
|
699
|
+
)
|
|
700
|
+
|
|
659
701
|
# Template the default file with the requested role
|
|
660
702
|
default_content = files["swarph-watchdog.default"].replace(
|
|
661
703
|
"SWARPH_CELL=lab",
|
|
@@ -664,13 +706,13 @@ def run_install_service(args: argparse.Namespace) -> int:
|
|
|
664
706
|
)
|
|
665
707
|
|
|
666
708
|
targets = [
|
|
667
|
-
(_SYSTEMD_UNIT_DIR / _SYSTEMD_UNIT_NAMES[0],
|
|
709
|
+
(_SYSTEMD_UNIT_DIR / _SYSTEMD_UNIT_NAMES[0], service_content),
|
|
668
710
|
(_SYSTEMD_UNIT_DIR / _SYSTEMD_UNIT_NAMES[1], files[_SYSTEMD_UNIT_NAMES[1]]),
|
|
669
711
|
(_SYSTEMD_DEFAULT_DIR / _SYSTEMD_DEFAULT_NAME, default_content),
|
|
670
712
|
]
|
|
671
713
|
|
|
672
714
|
if args.dry_run:
|
|
673
|
-
print(f"# DRY RUN — cell={args.cell}", file=sys.stderr)
|
|
715
|
+
print(f"# DRY RUN — cell={args.cell} swarph_bin={swarph_bin}", file=sys.stderr)
|
|
674
716
|
for path, content in targets:
|
|
675
717
|
print(f"\n# would write {path}:", file=sys.stderr)
|
|
676
718
|
print(content, file=sys.stderr)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.5
|
|
4
4
|
Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.7.0 ships Phase 7 substrate-doc R7 §11.1.7 operator-tooling layer in 5 increments: PR-A `--new-instance` flag (sibling-spawn case) + PR-B auto-suffix on collision (sibling-slot persistence) + PR-C SessionStart hook (closes bare-claude operator-paste gap) + watchdog (stranded-session recovery) + PR-D swarph-shared cell.yaml relocation (cell-yaml schema graduates to swarph-shared 0.3.0 kernel-tier; substrate-doc R7 §11.1.5 (O5) RESOLVED).
|
|
5
5
|
Author: Pierre Samson, Claude Opus
|
|
6
6
|
License: MIT
|
|
@@ -339,3 +339,92 @@ def test_run_spawn_dry_run_redacts_starter_prompt_in_command(
|
|
|
339
339
|
assert rc == 0
|
|
340
340
|
assert "redacted" not in captured.out # the literal word from the prompt
|
|
341
341
|
assert "starter prompt>" in captured.out # the redaction marker
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
# v0.7.5 — _session_state_exists + --resume on existing session
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
#
|
|
348
|
+
# Closes the bug surfaced 2026-05-14 post-reboot: claude --session-id <UUID>
|
|
349
|
+
# rejects with "Session ID <UUID> is already in use" when on-disk session
|
|
350
|
+
# state exists, even after host reboot (files persist; check is filesystem-
|
|
351
|
+
# based not runtime-lock-based). Fix: detect existing state + switch from
|
|
352
|
+
# --session-id (create-new semantic) to --resume (attach-existing semantic).
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_session_state_exists_false_for_fresh_uuid(tmp_path, monkeypatch):
|
|
356
|
+
"""No filesystem state for the UUID = fresh; _build_claude_argv uses
|
|
357
|
+
--session-id (create-new semantic)."""
|
|
358
|
+
from swarph_cli.commands.spawn import _session_state_exists
|
|
359
|
+
|
|
360
|
+
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
|
361
|
+
fresh_uuid = "00000000-0000-0000-0000-000000000000"
|
|
362
|
+
assert _session_state_exists(fresh_uuid) is False
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def test_session_state_exists_true_when_file_history_present(tmp_path, monkeypatch):
|
|
366
|
+
"""File-history dir alone is enough to flip detection (any one of the
|
|
367
|
+
three location signals triggers)."""
|
|
368
|
+
from swarph_cli.commands.spawn import _session_state_exists
|
|
369
|
+
|
|
370
|
+
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
|
371
|
+
uuid = "a30e406c-8bae-4ea2-8cb2-fb0dff35a6f0"
|
|
372
|
+
(tmp_path / ".claude" / "file-history" / uuid).mkdir(parents=True)
|
|
373
|
+
assert _session_state_exists(uuid) is True
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def test_session_state_exists_true_when_session_env_present(tmp_path, monkeypatch):
|
|
377
|
+
from swarph_cli.commands.spawn import _session_state_exists
|
|
378
|
+
|
|
379
|
+
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
|
380
|
+
uuid = "a30e406c-8bae-4ea2-8cb2-fb0dff35a6f0"
|
|
381
|
+
(tmp_path / ".claude" / "session-env").mkdir(parents=True)
|
|
382
|
+
(tmp_path / ".claude" / "session-env" / uuid).write_text("")
|
|
383
|
+
assert _session_state_exists(uuid) is True
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def test_session_state_exists_true_when_project_jsonl_present(tmp_path, monkeypatch):
|
|
387
|
+
"""Projects path varies by project-hash; glob discovers any match."""
|
|
388
|
+
from swarph_cli.commands.spawn import _session_state_exists
|
|
389
|
+
|
|
390
|
+
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
|
391
|
+
uuid = "a30e406c-8bae-4ea2-8cb2-fb0dff35a6f0"
|
|
392
|
+
proj = tmp_path / ".claude" / "projects" / "-some-project-hash"
|
|
393
|
+
proj.mkdir(parents=True)
|
|
394
|
+
(proj / f"{uuid}.jsonl").write_text("{}\n")
|
|
395
|
+
assert _session_state_exists(uuid) is True
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_build_claude_argv_uses_session_id_when_fresh(fake_cell_yaml, tmp_path, monkeypatch):
|
|
399
|
+
"""No prior session state → --session-id (create-new) verb."""
|
|
400
|
+
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
|
401
|
+
cell = load_cell(fake_cell_yaml)
|
|
402
|
+
argv = _build_claude_argv(
|
|
403
|
+
cell=cell,
|
|
404
|
+
session_id="00000000-0000-0000-0000-000000000000",
|
|
405
|
+
no_starter=True,
|
|
406
|
+
passthrough=[],
|
|
407
|
+
)
|
|
408
|
+
assert "--session-id" in argv
|
|
409
|
+
assert "--resume" not in argv
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_build_claude_argv_uses_resume_when_state_exists(fake_cell_yaml, tmp_path, monkeypatch):
|
|
413
|
+
"""Prior session state exists → --resume (attach-existing) verb.
|
|
414
|
+
|
|
415
|
+
Closes the v0.7.4 spawn-after-reboot rejection class.
|
|
416
|
+
"""
|
|
417
|
+
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
|
418
|
+
uuid = "a30e406c-8bae-4ea2-8cb2-fb0dff35a6f0"
|
|
419
|
+
(tmp_path / ".claude" / "file-history" / uuid).mkdir(parents=True)
|
|
420
|
+
cell = load_cell(fake_cell_yaml)
|
|
421
|
+
argv = _build_claude_argv(
|
|
422
|
+
cell=cell,
|
|
423
|
+
session_id=uuid,
|
|
424
|
+
no_starter=True,
|
|
425
|
+
passthrough=[],
|
|
426
|
+
)
|
|
427
|
+
assert "--resume" in argv
|
|
428
|
+
assert "--session-id" not in argv
|
|
429
|
+
# UUID still passed (just as --resume's value not --session-id's)
|
|
430
|
+
assert uuid in argv
|
|
@@ -624,3 +624,91 @@ def test_bundled_systemd_files_readable():
|
|
|
624
624
|
assert "OnUnitActiveSec=5min" in files["swarph-watchdog.timer"]
|
|
625
625
|
# Default file has the SWARPH_CELL=lab template line
|
|
626
626
|
assert "SWARPH_CELL=lab" in files["swarph-watchdog.default"]
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
# ---------------------------------------------------------------------------
|
|
630
|
+
# v0.7.4 — _resolve_swarph_bin + ExecStart templating
|
|
631
|
+
# ---------------------------------------------------------------------------
|
|
632
|
+
#
|
|
633
|
+
# v0.7.3 shipped a hardcoded ExecStart=/usr/local/bin/swarph that broke
|
|
634
|
+
# pipx-installed peers (lab + drop both hit this on first install attempt
|
|
635
|
+
# 2026-05-14). v0.7.4 resolves the path at install time via _resolve_swarph_bin
|
|
636
|
+
# and substitutes into the bundled service template.
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def test_resolve_swarph_bin_absolute_argv0_wins(monkeypatch):
|
|
640
|
+
"""If sys.argv[0] is absolute, it's the most reliable signal — use it."""
|
|
641
|
+
from swarph_cli.commands.watchdog import _resolve_swarph_bin
|
|
642
|
+
|
|
643
|
+
monkeypatch.setattr("sys.argv", ["/home/ubuntu/.local/bin/swarph", "watchdog"])
|
|
644
|
+
assert _resolve_swarph_bin() == "/home/ubuntu/.local/bin/swarph"
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def test_resolve_swarph_bin_bare_name_resolved_via_path(monkeypatch, tmp_path):
|
|
648
|
+
"""Bare-name argv[0] resolves via PATH."""
|
|
649
|
+
from swarph_cli.commands.watchdog import _resolve_swarph_bin
|
|
650
|
+
|
|
651
|
+
fake_bin = tmp_path / "swarph"
|
|
652
|
+
fake_bin.write_text("#!/bin/sh\nexit 0\n")
|
|
653
|
+
fake_bin.chmod(0o755)
|
|
654
|
+
monkeypatch.setattr("sys.argv", ["swarph", "watchdog"])
|
|
655
|
+
monkeypatch.setattr("shutil.which", lambda name: str(fake_bin) if name == "swarph" else None)
|
|
656
|
+
assert _resolve_swarph_bin() == str(fake_bin)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def test_resolve_swarph_bin_falls_back_to_placeholder_if_unresolvable(monkeypatch):
|
|
660
|
+
"""All resolution paths fail → fall back to /usr/local/bin/swarph
|
|
661
|
+
(v0.7.3 hardcode behavior; no regression vs prior version)."""
|
|
662
|
+
from swarph_cli.commands.watchdog import _resolve_swarph_bin, _SWARPH_BIN_PLACEHOLDER
|
|
663
|
+
|
|
664
|
+
monkeypatch.setattr("sys.argv", ["swarph", "watchdog"])
|
|
665
|
+
monkeypatch.setattr("shutil.which", lambda name: None)
|
|
666
|
+
assert _resolve_swarph_bin() == _SWARPH_BIN_PLACEHOLDER
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def test_install_service_dry_run_substitutes_swarph_bin(isolated_state, capsys, monkeypatch):
|
|
670
|
+
"""Dry-run preview shows resolved swarph path in ExecStart, not hardcoded
|
|
671
|
+
/usr/local/bin/swarph (when the host actually has swarph elsewhere)."""
|
|
672
|
+
pipx_path = "/home/ubuntu/.local/bin/swarph"
|
|
673
|
+
monkeypatch.setattr("sys.argv", [pipx_path, "watchdog"])
|
|
674
|
+
rc = run_watchdog(argv=["--install-service", "--cell", "droplet", "--dry-run"])
|
|
675
|
+
assert rc == 0
|
|
676
|
+
captured = capsys.readouterr()
|
|
677
|
+
# Header surfaces the resolved binary
|
|
678
|
+
assert f"swarph_bin={pipx_path}" in captured.err
|
|
679
|
+
# Service file's ExecStart now points at pipx path, NOT the placeholder
|
|
680
|
+
assert f"ExecStart={pipx_path} watchdog --check" in captured.err
|
|
681
|
+
# And the v0.7.3-hardcoded path no longer appears in the preview
|
|
682
|
+
assert "ExecStart=/usr/local/bin/swarph watchdog --check" not in captured.err
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def test_install_service_dry_run_preserves_default_path_when_absolute(isolated_state, capsys, monkeypatch):
|
|
686
|
+
"""When sys.argv[0] IS /usr/local/bin/swarph (the v0.7.3 default path),
|
|
687
|
+
the substitution still happens but produces the same line — no diff vs
|
|
688
|
+
v0.7.3 for this case."""
|
|
689
|
+
monkeypatch.setattr("sys.argv", ["/usr/local/bin/swarph", "watchdog"])
|
|
690
|
+
rc = run_watchdog(argv=["--install-service", "--cell", "lab", "--dry-run"])
|
|
691
|
+
assert rc == 0
|
|
692
|
+
captured = capsys.readouterr()
|
|
693
|
+
assert "ExecStart=/usr/local/bin/swarph watchdog --check" in captured.err
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def test_resolve_swarph_bin_relative_with_slash_resolves_to_absolute(tmp_path, monkeypatch):
|
|
697
|
+
"""Relative path with slash (e.g. editable install's venv/bin/swarph)
|
|
698
|
+
must be absolutized — systemd ExecStart needs absolute. Regression guard
|
|
699
|
+
for the abspath fix on top of v0.7.4 path-autodetect."""
|
|
700
|
+
from swarph_cli.commands.watchdog import _resolve_swarph_bin
|
|
701
|
+
|
|
702
|
+
fake = tmp_path / "swarph"
|
|
703
|
+
fake.write_text("#!/bin/sh\nexit 0\n")
|
|
704
|
+
fake.chmod(0o755)
|
|
705
|
+
# Simulate `./swarph` from cwd=tmp_path
|
|
706
|
+
monkeypatch.chdir(tmp_path)
|
|
707
|
+
monkeypatch.setattr("sys.argv", ["./swarph", "watchdog"])
|
|
708
|
+
# shutil.which('./swarph') returns the relative path as-is when the
|
|
709
|
+
# input contains a slash. _resolve_swarph_bin must abspath it.
|
|
710
|
+
resolved = _resolve_swarph_bin()
|
|
711
|
+
assert Path(resolved).is_absolute(), (
|
|
712
|
+
f"resolver returned non-absolute path: {resolved!r}"
|
|
713
|
+
)
|
|
714
|
+
assert resolved == str(fake)
|
|
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
|