swarph-cli 0.13.2__tar.gz → 0.13.3__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 (113) hide show
  1. {swarph_cli-0.13.2/src/swarph_cli.egg-info → swarph_cli-0.13.3}/PKG-INFO +1 -1
  2. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/pyproject.toml +1 -1
  3. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/chat.py +9 -5
  4. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/compress/levers.py +7 -1
  5. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/compress/verify.py +6 -1
  6. {swarph_cli-0.13.2 → swarph_cli-0.13.3/src/swarph_cli.egg-info}/PKG-INFO +1 -1
  7. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli.egg-info/SOURCES.txt +3 -0
  8. swarph_cli-0.13.3/tests/test_caller_meta_guard.py +117 -0
  9. swarph_cli-0.13.3/tests/test_compress_caller_convention.py +21 -0
  10. swarph_cli-0.13.3/tests/test_repl_caller_convention.py +43 -0
  11. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/LICENSE +0 -0
  12. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/README.md +0 -0
  13. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/setup.cfg +0 -0
  14. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/__init__.py +0 -0
  15. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/caller.py +0 -0
  16. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/capture/__init__.py +0 -0
  17. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/capture/harden.py +0 -0
  18. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/capture/lineage.py +0 -0
  19. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/capture/liveness.py +0 -0
  20. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/capture/manifest.py +0 -0
  21. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/capture/paths.py +0 -0
  22. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/capture/verify.py +0 -0
  23. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/cell.py +0 -0
  24. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/__init__.py +0 -0
  25. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/add.py +0 -0
  26. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/cell.py +0 -0
  27. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/compress.py +0 -0
  28. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/daemon.py +0 -0
  29. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/hook_output.py +0 -0
  30. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/hooks.py +0 -0
  31. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/import_session.py +0 -0
  32. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/init.py +0 -0
  33. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/install_hook.py +0 -0
  34. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/mcp_server.py +0 -0
  35. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/memory_sync.py +0 -0
  36. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/mesh.py +0 -0
  37. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/onboard.py +0 -0
  38. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/protocol_handler.py +0 -0
  39. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/ratify.py +0 -0
  40. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/security.py +0 -0
  41. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/spawn.py +0 -0
  42. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/commands/watchdog.py +0 -0
  43. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/compress/__init__.py +0 -0
  44. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/compress/marker.py +0 -0
  45. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/main.py +0 -0
  46. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/parsers/__init__.py +0 -0
  47. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/parsers/claude.py +0 -0
  48. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  49. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  50. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  51. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  52. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  53. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli.egg-info/requires.txt +0 -0
  54. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/src/swarph_cli.egg-info/top_level.txt +0 -0
  55. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_add_security_gate.py +0 -0
  56. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_artifact_add.py +0 -0
  57. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_artifact_hash.py +0 -0
  58. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_artifact_lib.py +0 -0
  59. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_artifact_mcp.py +0 -0
  60. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_artifact_skill.py +0 -0
  61. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_artifact_tool.py +0 -0
  62. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_artifact_uri.py +0 -0
  63. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_capture_lineage.py +0 -0
  64. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_capture_liveness.py +0 -0
  65. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_capture_manifest.py +0 -0
  66. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_capture_paths.py +0 -0
  67. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_capture_security.py +0 -0
  68. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_cell_command_dispatch.py +0 -0
  69. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_cell_harden.py +0 -0
  70. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_cell_loader.py +0 -0
  71. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_cell_verify.py +0 -0
  72. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_chat_command.py +0 -0
  73. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_claude_parser.py +0 -0
  74. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_claude_tmux_template.py +0 -0
  75. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_compress_command.py +0 -0
  76. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_compress_levers.py +0 -0
  77. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_compress_marker.py +0 -0
  78. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_compress_verify.py +0 -0
  79. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_daemon_command.py +0 -0
  80. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_feature_to_uri.py +0 -0
  81. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_hook_output.py +0 -0
  82. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_hooks_add.py +0 -0
  83. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_hooks_bundle.py +0 -0
  84. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_hooks_init.py +0 -0
  85. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_hooks_lifecycle.py +0 -0
  86. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_hooks_merge.py +0 -0
  87. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_import_command.py +0 -0
  88. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_init_command.py +0 -0
  89. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_install_hook.py +0 -0
  90. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_main.py +0 -0
  91. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_mcp_server.py +0 -0
  92. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_memory_sync.py +0 -0
  93. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_mesh_command.py +0 -0
  94. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_mesh_sidecar.py +0 -0
  95. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_onboard_command.py +0 -0
  96. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_protocol_handler.py +0 -0
  97. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_ratify_command.py +0 -0
  98. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_security.py +0 -0
  99. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_session_path_role_gate.py +0 -0
  100. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_smoke_chat.py +0 -0
  101. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_smoke_one_shot.py +0 -0
  102. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_smoke_phase_5_5.py +0 -0
  103. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_spawn_command.py +0 -0
  104. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_spawn_live_pin.py +0 -0
  105. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_spawn_mitosis_lineage.py +0 -0
  106. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_spawn_tmux_session.py +0 -0
  107. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_spawn_windows_relaunch.py +0 -0
  108. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_watchdog.py +0 -0
  109. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_watchdog_dm_wake.py +0 -0
  110. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  111. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  112. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/tests/test_watchdog_model_rung.py +0 -0
  113. {swarph_cli-0.13.2 → swarph_cli-0.13.3}/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.2
3
+ Version: 0.13.3
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.2"
7
+ version = "0.13.3"
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" }
@@ -35,7 +35,7 @@ import sys
35
35
  from dataclasses import dataclass, field
36
36
  from typing import Optional
37
37
 
38
- from swarph_cli.caller import default_caller
38
+ from swarph_cli.caller import _sanitize_username, default_caller
39
39
 
40
40
 
41
41
  _BANNER = """\
@@ -123,13 +123,17 @@ def _default_repl_caller() -> str:
123
123
  """``cli.repl.<user>`` — distinct from ``cli.oneshot.<user>`` so
124
124
  attribution rows distinguish REPL turns from one-shot calls.
125
125
 
126
- Falls back to the caller-convention default if env-derived user
127
- extraction fails."""
126
+ Reuses ``caller._sanitize_username`` (the SAME sanitizer ``default_caller``
127
+ uses) so the slug is guaranteed convention-valid: leading letter (``u_``
128
+ prefix if not), collapsed underscores, ``unknown`` on empty. The previous
129
+ inline slug omitted those guards, so a leading-digit username produced a
130
+ non-conforming ``cli.repl.3bob`` that crashed SwarphCall at runtime (#83).
131
+ Falls back to the caller-convention default if env-derived user extraction
132
+ fails."""
128
133
  user = os.environ.get("USER") or os.environ.get("LOGNAME")
129
134
  if not user:
130
135
  return default_caller()
131
- user_slug = "".join(c if c.isalnum() else "_" for c in user.lower())
132
- return f"cli.repl.{user_slug}"
136
+ return f"cli.repl.{_sanitize_username(user)}"
133
137
 
134
138
 
135
139
  def _print(msg: str = "", *, file=None) -> None:
@@ -4,6 +4,12 @@ from __future__ import annotations
4
4
  import re
5
5
  from dataclasses import dataclass
6
6
 
7
+ # Caller tag for the shorthand model call. MUST satisfy swarph_shared's
8
+ # caller convention (^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$ — dotted, not
9
+ # hyphenated), or SwarphCall(...) raises ValueError at runtime. See
10
+ # tests/test_compress_caller_convention.py.
11
+ SHORTHAND_CALLER = "swarph.compress"
12
+
7
13
 
8
14
  @dataclass
9
15
  class ArchivalResult:
@@ -41,7 +47,7 @@ async def shorthand(source: str, *, system_prompt: str = SHORTHAND_SYSTEM, chat=
41
47
  (messages, system_prompt, **kw)->resp with .text; defaults to swarph_mesh."""
42
48
  if chat is None:
43
49
  from swarph_mesh import ChatMessage, SwarphCall
44
- sc = SwarphCall(provider="claude", caller="swarph-compress")
50
+ sc = SwarphCall(provider="claude", caller=SHORTHAND_CALLER)
45
51
  async def chat(messages, system_prompt=None, **kw):
46
52
  return await sc.chat(messages=messages, system_prompt=system_prompt, **kw)
47
53
  msgs = [ChatMessage(role="user", content=source)]
@@ -4,6 +4,11 @@ from __future__ import annotations
4
4
  import gzip, re
5
5
  from pathlib import Path
6
6
 
7
+ # Caller tag for the adversarial verify-expand model call. MUST satisfy
8
+ # swarph_shared's dotted caller convention or SwarphCall raises at runtime.
9
+ # See tests/test_compress_caller_convention.py.
10
+ VERIFY_EXPAND_CALLER = "swarph.compress.verify"
11
+
7
12
  _LINK_RE = re.compile(r"\]\(([^)]+)\)")
8
13
 
9
14
 
@@ -67,7 +72,7 @@ async def verify_expand(source: str, compressed: str, *, chat=None) -> bool:
67
72
  fact in source not recoverable from compressed. One dropped fact -> False."""
68
73
  if chat is None:
69
74
  from swarph_mesh import ChatMessage, SwarphCall
70
- sc = SwarphCall(provider="claude", caller="swarph-compress-verify")
75
+ sc = SwarphCall(provider="claude", caller=VERIFY_EXPAND_CALLER)
71
76
  async def chat(messages, system_prompt=None, **kw):
72
77
  return await sc.chat(messages=messages, system_prompt=system_prompt, **kw)
73
78
  msgs = [ChatMessage(role="user", content=f"ORIGINAL:\n{source}\n\nCOMPRESSED:\n{compressed}")]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.13.2
3
+ Version: 0.13.3
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
@@ -55,6 +55,7 @@ tests/test_artifact_mcp.py
55
55
  tests/test_artifact_skill.py
56
56
  tests/test_artifact_tool.py
57
57
  tests/test_artifact_uri.py
58
+ tests/test_caller_meta_guard.py
58
59
  tests/test_capture_lineage.py
59
60
  tests/test_capture_liveness.py
60
61
  tests/test_capture_manifest.py
@@ -67,6 +68,7 @@ tests/test_cell_verify.py
67
68
  tests/test_chat_command.py
68
69
  tests/test_claude_parser.py
69
70
  tests/test_claude_tmux_template.py
71
+ tests/test_compress_caller_convention.py
70
72
  tests/test_compress_command.py
71
73
  tests/test_compress_levers.py
72
74
  tests/test_compress_marker.py
@@ -90,6 +92,7 @@ tests/test_mesh_sidecar.py
90
92
  tests/test_onboard_command.py
91
93
  tests/test_protocol_handler.py
92
94
  tests/test_ratify_command.py
95
+ tests/test_repl_caller_convention.py
93
96
  tests/test_security.py
94
97
  tests/test_session_path_role_gate.py
95
98
  tests/test_smoke_chat.py
@@ -0,0 +1,117 @@
1
+ """Repo-wide caller-convention meta-guard (issue #81).
2
+
3
+ The #80 bug: a hyphenated caller tag (`swarph-compress`) was silently dead at
4
+ runtime because it fails swarph_shared's dotted `role.subrole` convention — and it
5
+ was masked because the levers/verify tests injected a fake chat and never built a
6
+ real SwarphCall. That fix added a per-caller regression test for the two compress
7
+ callers. THIS guard generalizes it: it discovers EVERY static caller tag across the
8
+ whole `swarph_cli` package by import-introspection and validates each, so the bug
9
+ class can't be re-introduced in ANY module without someone remembering to add a
10
+ per-caller test.
11
+
12
+ Design (converged lab+droplet, #81): import-introspection, not AST literal-walking.
13
+ The callers live in named `*_CALLER` constants — `caller=SHORTHAND_CALLER` is an
14
+ ast.Name, not a literal, so a naive literal-walk would miss exactly the callers #80
15
+ hardened. Introspection resolves the value for free and nudges the light convention
16
+ that caller tags live in discoverable `*_CALLER` constants.
17
+
18
+ NOT covered here (separate follow-up, flagged in the PR): the DYNAMIC default-caller
19
+ producers (`default_caller` / `_default_repl_caller`) do not guarantee an
20
+ [a-z]-leading slug, so a leading-digit/empty OS username yields a non-conforming
21
+ `cli.repl.<digit...>` tag — a real robustness bug in the sanitizers, out of scope
22
+ for this static meta-guard.
23
+ """
24
+ import importlib
25
+ import pkgutil
26
+
27
+ import swarph_cli
28
+ from swarph_shared.caller_convention import validate_caller
29
+
30
+
31
+ def _iter_caller_constants():
32
+ """Yield (module, attr, value) for every module-level ``*_CALLER`` str constant
33
+ across the swarph_cli package. Walks the package so a NEW module's caller
34
+ constant is covered automatically. A submodule that can't be imported in this
35
+ env (e.g. an optional dep) is skipped — importability is other tests' concern,
36
+ not this guard's."""
37
+ for mod in pkgutil.walk_packages(swarph_cli.__path__, swarph_cli.__name__ + "."):
38
+ try:
39
+ m = importlib.import_module(mod.name)
40
+ except Exception:
41
+ continue
42
+ for attr in dir(m):
43
+ if attr.endswith("_CALLER"):
44
+ val = getattr(m, attr, None)
45
+ if isinstance(val, str):
46
+ yield mod.name, attr, val
47
+
48
+
49
+ def test_all_caller_constants_conform():
50
+ """Every ``*_CALLER`` constant in the package satisfies the dotted convention."""
51
+ found = list(_iter_caller_constants())
52
+ # Sanity: the compress callers from #80 must be discovered, else the walk is broken
53
+ # (e.g. swarph_cli resolving to a stale install rather than this tree).
54
+ names = {f"{m.rsplit('.', 1)[-1]}.{a}" for m, a, _ in found}
55
+ assert "levers.SHORTHAND_CALLER" in names and "verify.VERIFY_EXPAND_CALLER" in names, (
56
+ f"meta-guard didn't discover the known compress callers; found: {sorted(names)}")
57
+ for modname, attr, val in found:
58
+ try:
59
+ validate_caller(val)
60
+ except ValueError as e:
61
+ raise AssertionError(
62
+ f"caller constant {modname}.{attr} = {val!r} violates the convention "
63
+ f"(dotted role.subrole) — would crash SwarphCall at runtime: {e}")
64
+
65
+
66
+ def test_default_caller_producers_conform_happy_path(monkeypatch):
67
+ """The default-caller producers emit a conforming tag for a normal username.
68
+ Username is injected so this is deterministic regardless of the CI runner's
69
+ real user (the adversarial leading-digit case is a flagged follow-up, not here)."""
70
+ monkeypatch.setenv("USER", "alice")
71
+ monkeypatch.setenv("LOGNAME", "alice")
72
+ from swarph_cli.main import default_caller
73
+ from swarph_cli.commands.chat import _default_repl_caller
74
+ for fn in (default_caller, _default_repl_caller):
75
+ val = fn()
76
+ try:
77
+ validate_caller(val)
78
+ except ValueError as e:
79
+ raise AssertionError(
80
+ f"{fn.__module__}.{fn.__name__}() -> {val!r} violates the convention: {e}")
81
+
82
+
83
+ def test_guard_catches_a_planted_bad_caller(monkeypatch):
84
+ """PIN the catch-property — a guard's catch must be COMMITTED, not remembered
85
+ (the very #80 lesson this PR generalizes; droplet review). Plant a hyphenated
86
+ `*_CALLER` on a real, discovered module and assert the guard both DISCOVERS it
87
+ and REJECTS it. monkeypatch auto-removes the plant so the real guard test stays
88
+ clean regardless of order."""
89
+ import pytest
90
+
91
+ import swarph_cli.compress.levers as lev
92
+ monkeypatch.setattr(lev, "PLANTED_BAD_CALLER", "swarph-bad-hyphen", raising=False)
93
+
94
+ # 1) introspection DISCOVERS the planted constant
95
+ found = {f"{m}.{a}": v for m, a, v in _iter_caller_constants()}
96
+ assert found.get("swarph_cli.compress.levers.PLANTED_BAD_CALLER") == "swarph-bad-hyphen", (
97
+ "the walk failed to discover a planted *_CALLER — discovery is broken")
98
+
99
+ # 2) the guard CATCHES it (fails loud, names the offending constant)
100
+ with pytest.raises(AssertionError, match="violates the convention"):
101
+ test_all_caller_constants_conform()
102
+
103
+
104
+ def test_no_modules_skipped_by_the_walk():
105
+ """The guard resiliently SKIPS an unimportable submodule (so an unrelated optional-
106
+ dep import error can't break it) — but a silent skip could hide a bad caller in a
107
+ module that's importable in prod yet not in this env. Assert NONE are skipped here,
108
+ so the walk is exhaustive; if this ever fails in CI, run with full extras."""
109
+ skipped = []
110
+ for mod in pkgutil.walk_packages(swarph_cli.__path__, swarph_cli.__name__ + "."):
111
+ try:
112
+ importlib.import_module(mod.name)
113
+ except Exception as e: # noqa: BLE001 — we want to surface ANY import failure
114
+ skipped.append((mod.name, repr(e)))
115
+ assert not skipped, (
116
+ f"caller meta-guard walk skipped modules (a bad caller in one could ship "
117
+ f"uncaught): {skipped}")
@@ -0,0 +1,21 @@
1
+ """Regression: the compress model-call caller tags MUST satisfy swarph_shared's
2
+ caller convention (dotted `role.subrole`, not hyphenated). The existing
3
+ levers/verify tests inject a fake `chat` and never construct a real SwarphCall,
4
+ so they passed while the production path crashed at SwarphCall(...) on a
5
+ hyphenated caller ('swarph-compress'). This test checks the tags directly so
6
+ the break can't silently regress again."""
7
+ from swarph_shared.caller_convention import validate_caller
8
+
9
+ from swarph_cli.compress.levers import SHORTHAND_CALLER
10
+ from swarph_cli.compress.verify import VERIFY_EXPAND_CALLER
11
+
12
+
13
+ def test_shorthand_caller_matches_convention():
14
+ # raises ValueError if it doesn't match ^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$
15
+ validate_caller(SHORTHAND_CALLER)
16
+ assert SHORTHAND_CALLER == "swarph.compress"
17
+
18
+
19
+ def test_verify_expand_caller_matches_convention():
20
+ validate_caller(VERIFY_EXPAND_CALLER)
21
+ assert VERIFY_EXPAND_CALLER == "swarph.compress.verify"
@@ -0,0 +1,43 @@
1
+ """Regression (#83): `_default_repl_caller` must emit a convention-valid caller for
2
+ ANY OS username — including the adversarial cases the old inline slug logic broke on.
3
+
4
+ #80 fixed a hyphenated STATIC caller constant. This is the same crash-class on the
5
+ DYNAMIC repl-default path: `_default_repl_caller` rolled its own slug
6
+ (`"".join(c if c.isalnum() else "_" ...)`) that — unlike `caller._sanitize_username`
7
+ which `default_caller` uses — did NOT guarantee a leading letter, collapse underscores,
8
+ or fall back on empty. So a leading-digit username produced `cli.repl.3bob`, which
9
+ fails `^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$` and would crash SwarphCall at runtime.
10
+ Fix: reuse `_sanitize_username` (single sanitizer, no divergence).
11
+ """
12
+ import pytest
13
+
14
+ from swarph_shared.caller_convention import validate_caller
15
+
16
+ from swarph_cli.commands.chat import _default_repl_caller
17
+
18
+
19
+ @pytest.mark.parametrize("username", [
20
+ "3bob", # leading digit — the reported crash
21
+ "9", # all-digit
22
+ "!!!", # all-special -> empty slug -> must fall back, not emit `cli.repl.`
23
+ "ALICE", # uppercase
24
+ "bob.smith", # dot (would otherwise split the segment)
25
+ "a-b-c", # hyphens (the #80 char)
26
+ "_", # underscore-only -> empty after strip
27
+ " ", # whitespace-only
28
+ ])
29
+ def test_repl_caller_conforms_for_adversarial_usernames(monkeypatch, username):
30
+ """The repl default caller validates clean for every adversarial username."""
31
+ monkeypatch.setenv("USER", username)
32
+ monkeypatch.delenv("LOGNAME", raising=False)
33
+ # raises ValueError if the produced tag violates the convention
34
+ validate_caller(_default_repl_caller())
35
+
36
+
37
+ def test_repl_caller_stays_distinct_from_oneshot(monkeypatch):
38
+ """Still `cli.repl.*` (not `cli.oneshot.*`) — the separate producer exists so
39
+ attribution distinguishes REPL turns from one-shot calls; the fix must not
40
+ collapse them."""
41
+ monkeypatch.setenv("USER", "alice")
42
+ monkeypatch.delenv("LOGNAME", raising=False)
43
+ assert _default_repl_caller().startswith("cli.repl.")
File without changes
File without changes
File without changes