swarph-cli 0.40.2__tar.gz → 0.40.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 (224) hide show
  1. {swarph_cli-0.40.2/src/swarph_cli.egg-info → swarph_cli-0.40.3}/PKG-INFO +1 -1
  2. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/pyproject.toml +1 -1
  3. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/schedule.py +75 -2
  5. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/gateway/server.py +9 -2
  6. {swarph_cli-0.40.2 → swarph_cli-0.40.3/src/swarph_cli.egg-info}/PKG-INFO +1 -1
  7. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/SOURCES.txt +1 -0
  8. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_brain_ask_command.py +1 -1
  9. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_meta_edge_identity.py +47 -0
  10. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_schedule_command.py +35 -4
  11. swarph_cli-0.40.3/tests/test_schedule_create_contract.py +154 -0
  12. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_watchdog.py +1 -1
  13. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/LICENSE +0 -0
  14. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/README.md +0 -0
  15. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/setup.cfg +0 -0
  16. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/__init__.py +0 -0
  17. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/backends.py +0 -0
  18. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/pack.py +0 -0
  19. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/prices.py +0 -0
  20. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/quality.py +0 -0
  21. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/refresh_prices.py +0 -0
  22. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/runner.py +0 -0
  23. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/validate.py +0 -0
  24. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/caller.py +0 -0
  25. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/capture/__init__.py +0 -0
  26. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/capture/harden.py +0 -0
  27. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/capture/lineage.py +0 -0
  28. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/capture/liveness.py +0 -0
  29. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/capture/manifest.py +0 -0
  30. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/capture/paths.py +0 -0
  31. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/capture/verify.py +0 -0
  32. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/cell.py +0 -0
  33. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/chain_token.py +0 -0
  34. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/__init__.py +0 -0
  35. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/_display.py +0 -0
  36. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/_gateway_client.py +0 -0
  37. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/add.py +0 -0
  38. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/bench.py +0 -0
  39. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/board.py +0 -0
  40. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/board_merge_check.py +0 -0
  41. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/brain.py +0 -0
  42. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/brain_ask.py +0 -0
  43. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/cell.py +0 -0
  44. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
  45. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/channel.py +0 -0
  46. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/chat.py +0 -0
  47. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/codegraph.py +0 -0
  48. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/compress.py +0 -0
  49. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/daemon.py +0 -0
  50. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/event.py +0 -0
  51. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/gateway.py +0 -0
  52. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/group.py +0 -0
  53. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/highlight.py +0 -0
  54. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/hook_output.py +0 -0
  55. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/hooks.py +0 -0
  56. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/import_session.py +0 -0
  57. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/init.py +0 -0
  58. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/install_hook.py +0 -0
  59. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/install_multiplexer.py +0 -0
  60. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/lane.py +0 -0
  61. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/mcp_server.py +0 -0
  62. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/memory.py +0 -0
  63. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/memory_sync.py +0 -0
  64. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/mesh.py +0 -0
  65. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/monitor.py +0 -0
  66. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/okf_links.py +0 -0
  67. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/onboard.py +0 -0
  68. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/protocol_handler.py +0 -0
  69. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/ratify.py +0 -0
  70. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/security.py +0 -0
  71. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/service.py +0 -0
  72. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/spawn.py +0 -0
  73. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/timeline.py +0 -0
  74. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/watchdog.py +0 -0
  75. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/compress/__init__.py +0 -0
  76. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/compress/levers.py +0 -0
  77. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/compress/marker.py +0 -0
  78. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/compress/verify.py +0 -0
  79. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/console_safe.py +0 -0
  80. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/delivery_queue.py +0 -0
  81. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/gateway/__init__.py +0 -0
  82. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/gateway/feature_registry.py +0 -0
  83. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/gateway/lanes_control.py +0 -0
  84. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/gateway/schema.sql +0 -0
  85. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/gateway/services_control.py +0 -0
  86. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/main.py +0 -0
  87. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/multiplexer.py +0 -0
  88. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/parsers/__init__.py +0 -0
  89. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/parsers/claude.py +0 -0
  90. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
  91. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/service/__init__.py +0 -0
  92. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/service/app.py +0 -0
  93. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/service/providers.py +0 -0
  94. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/session_bridge.py +0 -0
  95. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/stall_alert.py +0 -0
  96. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  97. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  98. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  99. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  100. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  101. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/requires.txt +0 -0
  102. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/top_level.txt +0 -0
  103. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_add_security_gate.py +0 -0
  104. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_artifact_add.py +0 -0
  105. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_artifact_hash.py +0 -0
  106. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_artifact_lib.py +0 -0
  107. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_artifact_mcp.py +0 -0
  108. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_artifact_skill.py +0 -0
  109. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_artifact_tool.py +0 -0
  110. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_artifact_uri.py +0 -0
  111. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_bench_backends.py +0 -0
  112. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_bench_command.py +0 -0
  113. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_bench_pack.py +0 -0
  114. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_bench_prices.py +0 -0
  115. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_bench_quality.py +0 -0
  116. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_bench_runner.py +0 -0
  117. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_bench_validate.py +0 -0
  118. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_board_command.py +0 -0
  119. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_board_merge_check.py +0 -0
  120. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_brain_ask_env_diagnosis.py +0 -0
  121. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_brain_command.py +0 -0
  122. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_caller_meta_guard.py +0 -0
  123. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_capture_lineage.py +0 -0
  124. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_capture_liveness.py +0 -0
  125. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_capture_manifest.py +0 -0
  126. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_capture_paths.py +0 -0
  127. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_capture_security.py +0 -0
  128. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_cell_command_dispatch.py +0 -0
  129. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_cell_harden.py +0 -0
  130. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_cell_loader.py +0 -0
  131. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_cell_selfcheck.py +0 -0
  132. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_cell_verify.py +0 -0
  133. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_chain_token.py +0 -0
  134. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_channel_command.py +0 -0
  135. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_channel_post.py +0 -0
  136. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_chat_command.py +0 -0
  137. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_claude_parser.py +0 -0
  138. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_claude_tmux_template.py +0 -0
  139. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_codegraph_command.py +0 -0
  140. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_codegraph_mcp.py +0 -0
  141. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_compress_caller_convention.py +0 -0
  142. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_compress_command.py +0 -0
  143. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_compress_levers.py +0 -0
  144. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_compress_marker.py +0 -0
  145. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_compress_verify.py +0 -0
  146. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_console_encoding_e2e.py +0 -0
  147. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_daemon_command.py +0 -0
  148. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_daemon_console_encoding.py +0 -0
  149. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_daemon_delivery.py +0 -0
  150. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_delivery_queue.py +0 -0
  151. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_display_sanitize.py +0 -0
  152. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_event_emit.py +0 -0
  153. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_feature_to_uri.py +0 -0
  154. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_gateway_brain.py +0 -0
  155. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_gateway_client.py +0 -0
  156. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_gateway_command.py +0 -0
  157. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_group_command.py +0 -0
  158. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_highlight_command.py +0 -0
  159. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_hook_output.py +0 -0
  160. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_hooks_activity_marker.py +0 -0
  161. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_hooks_add.py +0 -0
  162. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_hooks_bundle.py +0 -0
  163. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_hooks_init.py +0 -0
  164. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_hooks_lifecycle.py +0 -0
  165. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_hooks_merge.py +0 -0
  166. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_import_command.py +0 -0
  167. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_init_command.py +0 -0
  168. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_install_hook.py +0 -0
  169. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_install_multiplexer_command.py +0 -0
  170. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_lane_command.py +0 -0
  171. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_main.py +0 -0
  172. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_main_unknown_verb_guard.py +0 -0
  173. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_mcp_server.py +0 -0
  174. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_memory_command.py +0 -0
  175. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_memory_gateway.py +0 -0
  176. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_memory_sync.py +0 -0
  177. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_memory_sync_membrane.py +0 -0
  178. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_mesh_command.py +0 -0
  179. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_mesh_inbox_mark_read.py +0 -0
  180. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_mesh_inbox_sanitize.py +0 -0
  181. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_mesh_sidecar.py +0 -0
  182. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
  183. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
  184. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_monitor_cli.py +0 -0
  185. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_monitor_daemon_collision.py +0 -0
  186. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_monitor_self_name_verification.py +0 -0
  187. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_monitor_state_split.py +0 -0
  188. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_multiplexer.py +0 -0
  189. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_okf_links.py +0 -0
  190. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_onboard_command.py +0 -0
  191. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_packaged_artifacts_exist.py +0 -0
  192. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_protocol_handler.py +0 -0
  193. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_providers_isolation.py +0 -0
  194. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_ratify_command.py +0 -0
  195. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_repl_caller_convention.py +0 -0
  196. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_security.py +0 -0
  197. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_service_app.py +0 -0
  198. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_service_command.py +0 -0
  199. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_service_providers.py +0 -0
  200. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_session_bridge_inject.py +0 -0
  201. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_session_bridge_probe.py +0 -0
  202. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_session_bridge_resolve.py +0 -0
  203. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_session_path_role_gate.py +0 -0
  204. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_shipped_units_match_the_cli.py +0 -0
  205. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_smoke_chat.py +0 -0
  206. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_smoke_one_shot.py +0 -0
  207. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_smoke_phase_5_5.py +0 -0
  208. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_spawn_base_hoist.py +0 -0
  209. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_spawn_command.py +0 -0
  210. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_spawn_live_pin.py +0 -0
  211. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_spawn_mitosis_lineage.py +0 -0
  212. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_spawn_tmux_session.py +0 -0
  213. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_spawn_windows_relaunch.py +0 -0
  214. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_stall_alert.py +0 -0
  215. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_timeline_command.py +0 -0
  216. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_token_file_one_parser.py +0 -0
  217. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_version_consistency.py +0 -0
  218. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_watchdog_dm_wake.py +0 -0
  219. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  220. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  221. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_watchdog_emit_health.py +0 -0
  222. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_watchdog_install_flags.py +0 -0
  223. {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_watchdog_model_rung.py +0 -0
  224. {swarph_cli-0.40.2 → swarph_cli-0.40.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.40.2
3
+ Version: 0.40.3
4
4
  Summary: The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, session `import`, and a stranded-session `watchdog`.
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.40.2"
7
+ version = "0.40.3"
8
8
  description = "The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, session `import`, and a stranded-session `watchdog`."
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.40.2"
19
+ __version__ = "0.40.3"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -40,7 +40,13 @@ def _build_parser() -> argparse.ArgumentParser:
40
40
  "--context",
41
41
  action="append",
42
42
  default=[],
43
- help="context reference (repeatable)",
43
+ metavar="KEY=VALUE",
44
+ help="durable context anchor, repeatable. KEY is one of "
45
+ + "|".join(sorted(DURABLE_ANCHOR_KEYS))
46
+ + " (e.g. --context memory=project_x --context repo=swarph-cli). "
47
+ "A raw JSON object is also accepted. REQUIRED: the gateway rejects an "
48
+ "event with no anchors, because an event that fires with no durable "
49
+ "context wakes a cell that cannot reconstruct why.",
44
50
  )
45
51
  create.add_argument(
46
52
  "--min-interval",
@@ -93,14 +99,81 @@ def _ctx(args: argparse.Namespace) -> tuple[str, str, str]:
93
99
  return self_name, token, base
94
100
 
95
101
 
102
+
103
+ # Mirrors mesh-gateway `_DURABLE_ANCHOR_KEYS` (server.py). Kept in sync by
104
+ # test_schedule_create_contract.py, which fails if this drifts from the value the
105
+ # gateway actually enforces — a duplicated constant nobody checks is how the two
106
+ # sides diverged in the first place.
107
+ DURABLE_ANCHOR_KEYS = frozenset({"repo", "memory", "channel", "feature", "file"})
108
+
109
+
110
+ def parse_context_anchor(raw: str) -> dict:
111
+ """Turn one --context value into the ANCHOR DICT the gateway requires.
112
+
113
+ >>> THE BUG THIS FIXES (card #146): the CLI sent `--context` values through as
114
+ RAW STRINGS while the gateway requires each anchor to be a DICT carrying a
115
+ durable key, and requires the list to be NON-EMPTY. So `swarph schedule
116
+ create` COULD NOT SUCCEED — every invocation 400'd, and every real event on
117
+ this mesh had to be armed by hand over the raw API. <<<
118
+
119
+ That is why dated reminders kept living in DM threads instead of the scheduler:
120
+ the verb that makes a date fire was itself broken, so the cheapest path was
121
+ always to tell a peer and hope.
122
+
123
+ Accepts `key=value` for the five durable kinds, or a raw JSON object for
124
+ anything the shorthand cannot express. Raises ValueError with the valid keys
125
+ listed, LOCALLY — a 400 from the gateway names the rule but not the input that
126
+ broke it, and the user is holding the input.
127
+ """
128
+ raw = (raw or "").strip()
129
+ if not raw:
130
+ raise ValueError("empty --context value")
131
+ if raw.startswith("{"):
132
+ try:
133
+ anchor = json.loads(raw)
134
+ except json.JSONDecodeError as exc:
135
+ raise ValueError(f"--context {raw!r} looks like JSON but does not parse: {exc}") from exc
136
+ if not isinstance(anchor, dict):
137
+ raise ValueError(f"--context JSON must be an object, got {type(anchor).__name__}")
138
+ if not (set(anchor) & DURABLE_ANCHOR_KEYS):
139
+ raise ValueError(
140
+ f"--context {raw!r} has no durable key; one of "
141
+ f"{sorted(DURABLE_ANCHOR_KEYS)} is required")
142
+ return anchor
143
+ key, sep, value = raw.partition("=")
144
+ key, value = key.strip(), value.strip()
145
+ if not sep or not value:
146
+ raise ValueError(
147
+ f"--context {raw!r} must be KEY=VALUE (e.g. memory=project_x); "
148
+ f"KEY is one of {sorted(DURABLE_ANCHOR_KEYS)}")
149
+ if key not in DURABLE_ANCHOR_KEYS:
150
+ raise ValueError(
151
+ f"--context key {key!r} is not durable; use one of {sorted(DURABLE_ANCHOR_KEYS)}. "
152
+ f"An anchor must name something that survives compaction — a /tmp path or a "
153
+ f"session id is exactly what the gateway rejects.")
154
+ return {key: value}
155
+
156
+
96
157
  def _run_create(args: argparse.Namespace) -> int:
97
158
  self_name, token, base = _ctx(args)
159
+ if not args.context:
160
+ print("swarph schedule create: --context is REQUIRED (repeatable).\n"
161
+ " The gateway refuses an event with no durable anchor: an event that\n"
162
+ " fires with no context wakes a cell that cannot reconstruct why it woke.\n"
163
+ f" Use KEY=VALUE with KEY in {sorted(DURABLE_ANCHOR_KEYS)},\n"
164
+ " e.g. --context memory=project_graduation_register", file=sys.stderr)
165
+ return 2
166
+ try:
167
+ anchors = [parse_context_anchor(c) for c in args.context]
168
+ except ValueError as exc:
169
+ print(f"swarph schedule create: {exc}", file=sys.stderr)
170
+ return 2
98
171
  body = {
99
172
  "name": args.name,
100
173
  "trigger_type": args.trigger,
101
174
  "target_cell": args.target,
102
175
  "task": args.task,
103
- "context_ref": args.context,
176
+ "context_ref": anchors,
104
177
  "created_by": self_name,
105
178
  }
106
179
  if args.cron is not None:
@@ -2437,8 +2437,15 @@ def _is_operator(auth: AuthContext) -> bool:
2437
2437
  membership/broadcast authority of the FUTURE POST surface is likewise REAL
2438
2438
  only post-C5 (a cell can already create an announce channel + own
2439
2439
  allow_broadcast=1 on it). Mitigation now: pre-seed the reserved names via the
2440
- operator at deploy so the PK-409 protects the canonical names meanwhile."""
2441
- return auth.peer is None
2440
+ operator at deploy so the PK-409 protects the canonical names meanwhile.
2441
+
2442
+ META-EDGE (B1/B2): a verified SSO login (kind='user_identity') and a
2443
+ cell-join key (kind='cell_join') BOTH carry peer=None, so a bare
2444
+ `peer is None` check made every OAuth login a full operator — an
2445
+ OAuth-driven operator factory. The genuine operator is the shared/commander
2446
+ token ALONE, which is the only context with BOTH peer=None AND kind=None.
2447
+ A Meta-Edge context is owner-scoped, never operator."""
2448
+ return auth.peer is None and auth.kind is None
2442
2449
 
2443
2450
 
2444
2451
  class ChannelCreateRequest(BaseModel):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.40.2
3
+ Version: 0.40.3
4
4
  Summary: The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, session `import`, and a stranded-session `watchdog`.
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
@@ -189,6 +189,7 @@ tests/test_providers_isolation.py
189
189
  tests/test_ratify_command.py
190
190
  tests/test_repl_caller_convention.py
191
191
  tests/test_schedule_command.py
192
+ tests/test_schedule_create_contract.py
192
193
  tests/test_security.py
193
194
  tests/test_service_app.py
194
195
  tests/test_service_command.py
@@ -17,7 +17,7 @@ from swarph_cli.commands import brain_ask as ba
17
17
 
18
18
  def test_version_is_0_33_0():
19
19
  import swarph_cli
20
- assert swarph_cli.__version__ == "0.40.2"
20
+ assert swarph_cli.__version__ == "0.40.3"
21
21
 
22
22
 
23
23
  # --- request / response plumbing -------------------------------------------
@@ -331,3 +331,50 @@ def test_shared_token_register_list_auth_unchanged(gw, client):
331
331
  with pytest.raises(gw.HTTPException) as e:
332
332
  gw._authorize("Bearer totally-wrong-token")
333
333
  assert e.value.status_code == 401
334
+
335
+
336
+ # ══ 7. BLOCKER-B — a Meta-Edge login is NOT an operator ════════════════════
337
+ #
338
+ # The shipped _is_operator was `return auth.peer is None`. Both the SSO
339
+ # identity context AND the cell-join context carry peer=None (only the genuine
340
+ # shared/commander token does AND has kind=None), so every OAuth login resolved
341
+ # to a full operator — an OAuth-driven operator factory. The genuine operator is
342
+ # the shared token alone: peer is None AND kind is None.
343
+
344
+ def test_is_operator_only_for_shared_token_not_meta_edge(gw):
345
+ """Unit: the precise close. Shared token = operator; user_identity and
346
+ cell_join (both peer=None) are NOT; a per-peer cell is NOT."""
347
+ shared = gw.AuthContext(peer=None, regime="shared_token", key_generation=None)
348
+ identity = gw.AuthContext(kind="user_identity", user="u-123",
349
+ provider="github", login="octocat")
350
+ join = gw.AuthContext(kind="cell_join", owner="u-123")
351
+ cell = gw.AuthContext(peer="some-cell", regime="peer_token", key_generation=1)
352
+
353
+ assert gw._is_operator(shared) is True
354
+ assert gw._is_operator(identity) is False # was True → the operator factory
355
+ assert gw._is_operator(join) is False # the cell-join hole, closed too
356
+ assert gw._is_operator(cell) is False
357
+
358
+
359
+ def test_user_identity_denied_at_operator_gated_route(gw, client, sign):
360
+ """Wiring: an SSO identity token is 403'd at an operator-gated route
361
+ (announce-channel create, B5). Pre-fix this created the channel (200) —
362
+ the catch must be COMMITTED, not just present on the helper."""
363
+ tok = sign(_base_claims()) # a valid user_identity token (purpose absent)
364
+ r = client.post(
365
+ "/channels",
366
+ json={"name": "sso-cannot-make-this", "kind": "announce",
367
+ "created_by": "u-123"},
368
+ headers=_bearer(tok),
369
+ )
370
+ assert r.status_code == 403, r.text
371
+ assert "operator-gated" in r.text
372
+
373
+ # control: the shared/commander token IS the operator and succeeds.
374
+ r2 = client.post(
375
+ "/channels",
376
+ json={"name": "operator-can", "kind": "announce",
377
+ "created_by": "commander"},
378
+ headers=_bearer(SHARED_TOKEN),
379
+ )
380
+ assert r2.status_code == 200, r2.text
@@ -67,8 +67,8 @@ def test_schedule_create_posts_event(monkeypatch):
67
67
  "--target", "c2",
68
68
  "--task", "summarize the day",
69
69
  "--cron", "0 9 * * *",
70
- "--context", "ref1",
71
- "--context", "ref2",
70
+ "--context", "memory=proj_x",
71
+ "--context", "repo=swarph-cli",
72
72
  "--out-channel", "general",
73
73
  "--min-interval", "300",
74
74
  ])
@@ -80,28 +80,59 @@ def test_schedule_create_posts_event(monkeypatch):
80
80
  assert body["target_cell"] == "c2"
81
81
  assert body["task"] == "summarize the day"
82
82
  assert body["created_by"] == "c1"
83
- assert body["context_ref"] == ["ref1", "ref2"]
84
83
  assert body["cron"] == "0 9 * * *"
85
84
  assert body["out_channel"] == "general"
86
85
  assert body["min_interval_sec"] == 300
86
+ # Card #146: anchors must reach the gateway as DICTS. This assertion is the
87
+ # one the original test was missing — it asserted the POST happened, never
88
+ # that the BODY was one the gateway accepts.
89
+ assert body["context_ref"] == [{"memory": "proj_x"}, {"repo": "swarph-cli"}]
87
90
 
88
91
 
89
92
  def test_schedule_create_minimal_omits_optionals(monkeypatch):
93
+ """>>> THIS TEST USED TO ASSERT THE BUG (card #146). <<<
94
+
95
+ It ran `create` with NO --context, asserted rc == 0, and asserted
96
+ `body["context_ref"] == []`. The gateway REQUIRES a non-empty context_ref, so the
97
+ behaviour this test pinned as correct was the behaviour that made every real
98
+ `schedule create` fail with a 400.
99
+
100
+ It stayed green for months because it MOCKS THE POST: it verified that the CLI
101
+ sends what the CLI sends. A mocked transport cannot fail a contract it never
102
+ talks to — the same shape as a fixture building a card the real system cannot
103
+ produce.
104
+
105
+ Now: the optionals are still omitted, but --context is supplied because it is
106
+ genuinely required, and the no-context case is asserted to FAIL LOCALLY below.
107
+ """
90
108
  seen = _capture_post(monkeypatch)
91
109
  rc = sc.run_schedule([
92
110
  "create", "ev",
93
111
  "--trigger", "event",
94
112
  "--target", "c2",
95
113
  "--task", "do thing",
114
+ "--context", "memory=proj_x",
96
115
  ])
97
116
  assert rc == 0
98
117
  body = seen["body"]
99
- assert body["context_ref"] == []
118
+ assert body["context_ref"] == [{"memory": "proj_x"}]
100
119
  assert "cron" not in body
101
120
  assert "out_channel" not in body
102
121
  assert "min_interval_sec" not in body
103
122
 
104
123
 
124
+ def test_schedule_create_without_context_fails_LOCALLY_and_never_posts(monkeypatch):
125
+ """The replacement for what the old minimal test asserted. An event with no
126
+ durable anchor is refused BEFORE the network, so the user gets the reason and
127
+ the input rather than a server 400 they must reverse-engineer."""
128
+ seen = _capture_post(monkeypatch)
129
+ rc = sc.run_schedule([
130
+ "create", "ev", "--trigger", "event", "--target", "c2", "--task", "do thing",
131
+ ])
132
+ assert rc == 2
133
+ assert not seen, "a request with no anchors must never reach the gateway"
134
+
135
+
105
136
  def test_schedule_enable_posts(monkeypatch):
106
137
  seen = _capture_post(monkeypatch)
107
138
  rc = sc.run_schedule(["enable", "daily-digest"])
@@ -0,0 +1,154 @@
1
+ """Card #146 — `swarph schedule create` could never succeed.
2
+
3
+ >>> THE CLI SENT --context VALUES AS RAW STRINGS. THE GATEWAY REQUIRES EACH ANCHOR TO
4
+ BE A DICT CARRYING A DURABLE KEY, AND REQUIRES THE LIST TO BE NON-EMPTY. SO EVERY
5
+ INVOCATION 400'd, UNCONDITIONALLY. <<<
6
+
7
+ The cost was not the broken verb. It was that the verb which makes a DATE FIRE was
8
+ itself broken, so arming a reminder always cost a hand-written raw-API call — and the
9
+ cheapest path was therefore to tell a peer in a DM and hope. Board cards #129/#168
10
+ (stale dependency gates) and the graduation register are downstream of exactly this:
11
+ dates lived in conversations because the mechanism for dates did not work.
12
+
13
+ This file pins the CONTRACT, not the implementation:
14
+ · the parser produces the dict shape the gateway accepts
15
+ · a non-durable / malformed anchor fails LOCALLY, naming the valid keys
16
+ · an EMPTY context list fails locally rather than as a server 400
17
+ · the CLI's copy of the durable-key set MATCHES THE GATEWAY'S — the drift check,
18
+ because a duplicated constant nobody compares is how the two sides diverged
19
+ """
20
+ import json
21
+ import re
22
+ from pathlib import Path
23
+
24
+ import pytest
25
+
26
+ from swarph_cli.commands.schedule import DURABLE_ANCHOR_KEYS, parse_context_anchor
27
+
28
+
29
+ # ── the shape the gateway actually wants ────────────────────────────────────
30
+
31
+ @pytest.mark.parametrize("raw,want", [
32
+ ("memory=project_graduation_register", {"memory": "project_graduation_register"}),
33
+ ("repo=swarph-cli", {"repo": "swarph-cli"}),
34
+ ("channel=System", {"channel": "System"}),
35
+ ("feature=scheduler", {"feature": "scheduler"}),
36
+ ("file=docs/PLAN.md", {"file": "docs/PLAN.md"}),
37
+ ('{"memory": "x"}', {"memory": "x"}),
38
+ ])
39
+ def test_shorthand_and_json_both_produce_anchor_dicts(raw, want):
40
+ assert parse_context_anchor(raw) == want
41
+
42
+
43
+ def test_the_old_behaviour_is_exactly_what_the_gateway_rejects():
44
+ """>>> THE REGRESSION PIN. The pre-fix CLI passed the raw string through. <<<
45
+
46
+ The gateway's check is `not isinstance(anchor, dict) or not (set(anchor) &
47
+ DURABLE_ANCHOR_KEYS)`, so a bare string failed the FIRST clause. If someone
48
+ "simplifies" the parser back to a pass-through, this fails instead of the verb
49
+ silently 400-ing again for another two months.
50
+ """
51
+ got = parse_context_anchor("memory=x")
52
+ assert isinstance(got, dict), "a raw string is what made this verb unusable"
53
+ assert set(got) & DURABLE_ANCHOR_KEYS
54
+
55
+
56
+ # ── failing LOCALLY, with the input the user is holding ─────────────────────
57
+
58
+ @pytest.mark.parametrize("bad", [
59
+ "project_x", # the natural thing to type — no key at all
60
+ "session=abc", # a real key name, but not a durable one
61
+ "memory=", # key with no value
62
+ "", # empty
63
+ "{not json}", # looks like JSON, is not
64
+ '["memory"]', # JSON, but not an object
65
+ '{"note": "x"}', # object, but no durable key
66
+ ])
67
+ def test_bad_anchors_raise_locally_and_name_the_valid_keys(bad):
68
+ with pytest.raises(ValueError) as e:
69
+ parse_context_anchor(bad)
70
+ msg = str(e.value)
71
+ assert bad[:20] in msg or "durable" in msg or "empty" in msg, msg
72
+
73
+
74
+ def test_the_error_lists_the_actual_keys_not_a_vague_rule():
75
+ """A 400 from the gateway names the RULE; it does not name the INPUT that broke
76
+ it, and the user is the one holding the input. The local error must do both."""
77
+ with pytest.raises(ValueError) as e:
78
+ parse_context_anchor("session=abc")
79
+ msg = str(e.value)
80
+ assert "session" in msg, "the error must quote the offending key"
81
+ for k in ("memory", "repo", "channel", "feature", "file"):
82
+ assert k in msg, f"the error must list {k!r} as a valid alternative"
83
+
84
+
85
+ # ── the drift check: the two sides must agree ───────────────────────────────
86
+
87
+ # >>> THIS IS THE DEPLOYED TREE, NOT A CHECKOUT — AND THAT IS DELIBERATE. <<<
88
+ # Measured 2026-07-30: the deployed gateway (6e3e01a) is NOT origin/main (b10c3ad).
89
+ # Reading the deployed path validates the CLI against THE CONTRACT CURRENTLY SERVING
90
+ # rather than the one that happens to be merged — the artifact-vs-source distinction
91
+ # that cost us the 0.40.0 release, applied on purpose this time.
92
+ #
93
+ # CONSEQUENCE A FUTURE READER MUST HAVE (droplet, PR #161 review): a green here means
94
+ # "the CLI satisfies the gateway RUNNING RIGHT NOW", never "the CLI satisfies main".
95
+ # So the next time someone `git pull`s the gateway, THESE TESTS CAN GO RED WITH NO CLI
96
+ # CHANGE AT ALL.
97
+ # >>> A RED HERE MEANS THE CONTRACT MOVED. It is the drift check working, not a CLI
98
+ # regression. Check the gateway diff before you touch this package. <<<
99
+ GATEWAY = Path("/home/ubuntu/mesh-gateway/server.py")
100
+
101
+
102
+ @pytest.mark.skipif(not GATEWAY.exists(), reason="mesh-gateway source not on this box")
103
+ def test_the_cli_durable_keys_match_the_gateway_the_client_talks_to():
104
+ """>>> THE CHECK THAT WOULD HAVE CAUGHT #146 AT BIRTH. <<<
105
+
106
+ Two codebases, one contract, and no test comparing them — so the CLI could send a
107
+ shape the gateway had never accepted and nothing said a word until a human tried
108
+ to arm a real event and got a 400 they had to reverse-engineer.
109
+
110
+ Skipped where mesh-gateway is absent (most CI runners), and SKIPPED MEANS NOT
111
+ VERIFIED, not verified-OK — pytest counts and prints it. On the lab host, where
112
+ both live, it runs and is authoritative.
113
+ """
114
+ src = GATEWAY.read_text(encoding="utf-8")
115
+ m = re.search(r"_DURABLE_ANCHOR_KEYS\s*=\s*frozenset\(\{([^}]*)\}\)", src)
116
+ assert m, "could not locate _DURABLE_ANCHOR_KEYS in the gateway — the drift check is blind"
117
+ gateway_keys = set(re.findall(r'"([a-z_]+)"', m.group(1)))
118
+ assert gateway_keys == set(DURABLE_ANCHOR_KEYS), (
119
+ f"CLI and gateway disagree on durable anchor keys: "
120
+ f"cli={sorted(DURABLE_ANCHOR_KEYS)} gateway={sorted(gateway_keys)}")
121
+
122
+
123
+ @pytest.mark.skipif(not GATEWAY.exists(), reason="mesh-gateway source not on this box")
124
+ def test_anchors_pass_the_GATEWAYS_OWN_validator_not_a_local_copy():
125
+ """>>> THE END-TO-END CHECK, AND THE ONLY ONE THAT ACTUALLY PROVES THE FIX. <<<
126
+
127
+ A live POST cannot prove it: the gateway checks AUTHZ (server.py ~3707) BEFORE it
128
+ validates the payload (~3728), so a non-operator caller gets an identical 403 for
129
+ a well-formed and a malformed body. The measurement returns the same value whether
130
+ the fix works or not — which is exactly the proxy this card's whole family is about,
131
+ and it means the CLI's correctness is UNTESTABLE end-to-end by most callers.
132
+
133
+ So instead of a mock or a local re-implementation, this executes the DEPLOYED
134
+ gateway's own `_anchor_is_durable` against what the parser actually emits. Real
135
+ function, real contract, no server and no token.
136
+ """
137
+ import re as _re
138
+ src = GATEWAY.read_text(encoding="utf-8")
139
+ ns = {}
140
+ keys = _re.search(r"_DURABLE_ANCHOR_KEYS\s*=\s*frozenset\(\{[^}]*\}\)", src)
141
+ fn = _re.search(r"def _anchor_is_durable\(anchor: dict\) -> bool:.*?\n return True\n", src, _re.S)
142
+ assert keys and fn, "could not extract the gateway validator — this check is blind"
143
+ exec(keys.group(0), ns)
144
+ exec(fn.group(0), ns)
145
+ is_durable = ns["_anchor_is_durable"]
146
+
147
+ for raw in ("memory=project_x", "repo=swarph-cli", "channel=System",
148
+ "feature=scheduler", "file=docs/PLAN.md", '{"memory": "x"}'):
149
+ assert is_durable(parse_context_anchor(raw)), f"gateway would reject {raw!r}"
150
+
151
+ # and the pre-fix behaviour must still be rejected by that same validator
152
+ assert not is_durable("memory=x"), (
153
+ "a bare string is what the unpatched CLI sent; if the gateway now accepts it, "
154
+ "this test is measuring the wrong contract")
@@ -1043,4 +1043,4 @@ def test_parser_defaults_process_name_claude_liveness_cmd_none():
1043
1043
 
1044
1044
  def test_version_is_0_33_0():
1045
1045
  import swarph_cli
1046
- assert swarph_cli.__version__ == "0.40.2"
1046
+ assert swarph_cli.__version__ == "0.40.3"
File without changes
File without changes
File without changes