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.
- {swarph_cli-0.40.2/src/swarph_cli.egg-info → swarph_cli-0.40.3}/PKG-INFO +1 -1
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/pyproject.toml +1 -1
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/schedule.py +75 -2
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/gateway/server.py +9 -2
- {swarph_cli-0.40.2 → swarph_cli-0.40.3/src/swarph_cli.egg-info}/PKG-INFO +1 -1
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/SOURCES.txt +1 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_brain_ask_command.py +1 -1
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_meta_edge_identity.py +47 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_schedule_command.py +35 -4
- swarph_cli-0.40.3/tests/test_schedule_create_contract.py +154 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_watchdog.py +1 -1
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/LICENSE +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/README.md +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/setup.cfg +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/__init__.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/backends.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/pack.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/prices.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/quality.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/refresh_prices.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/runner.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/bench/validate.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/chain_token.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/_display.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/_gateway_client.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/bench.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/board.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/board_merge_check.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/brain.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/brain_ask.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/channel.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/codegraph.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/event.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/gateway.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/group.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/highlight.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/hooks.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/install_multiplexer.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/lane.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/mcp_server.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/memory.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/mesh.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/monitor.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/okf_links.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/service.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/spawn.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/timeline.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/console_safe.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/delivery_queue.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/gateway/__init__.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/gateway/feature_registry.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/gateway/lanes_control.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/gateway/schema.sql +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/gateway/services_control.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/main.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/multiplexer.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/service/__init__.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/service/app.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/service/providers.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/session_bridge.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/stall_alert.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_bench_backends.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_bench_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_bench_pack.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_bench_prices.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_bench_quality.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_bench_runner.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_bench_validate.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_board_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_board_merge_check.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_brain_ask_env_diagnosis.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_brain_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_caller_meta_guard.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_cell_selfcheck.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_chain_token.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_channel_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_channel_post.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_codegraph_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_codegraph_mcp.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_compress_caller_convention.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_console_encoding_e2e.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_daemon_console_encoding.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_daemon_delivery.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_delivery_queue.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_display_sanitize.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_event_emit.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_gateway_brain.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_gateway_client.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_gateway_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_group_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_highlight_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_hooks_activity_marker.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_import_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_init_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_install_multiplexer_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_lane_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_main.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_main_unknown_verb_guard.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_memory_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_memory_gateway.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_memory_sync_membrane.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_mesh_inbox_mark_read.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_mesh_inbox_sanitize.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_monitor_cli.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_monitor_daemon_collision.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_monitor_self_name_verification.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_monitor_state_split.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_multiplexer.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_okf_links.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_packaged_artifacts_exist.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_providers_isolation.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_repl_caller_convention.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_security.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_service_app.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_service_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_service_providers.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_session_bridge_inject.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_session_bridge_probe.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_session_bridge_resolve.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_shipped_units_match_the_cli.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_spawn_base_hoist.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_spawn_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_spawn_tmux_session.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_stall_alert.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_timeline_command.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_token_file_one_parser.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_version_consistency.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_watchdog_emit_health.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_watchdog_install_flags.py +0 -0
- {swarph_cli-0.40.2 → swarph_cli-0.40.3}/tests/test_watchdog_model_rung.py +0 -0
- {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.
|
|
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.
|
|
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" }
|
|
@@ -40,7 +40,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
40
40
|
"--context",
|
|
41
41
|
action="append",
|
|
42
42
|
default=[],
|
|
43
|
-
|
|
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":
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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", "
|
|
71
|
-
"--context", "
|
|
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")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|