@team-agent/installer 0.2.11 → 0.3.1
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.
- package/Cargo.lock +744 -0
- package/Cargo.toml +34 -0
- package/crates/team-agent/Cargo.toml +33 -0
- package/crates/team-agent/src/cli/adapters.rs +1343 -0
- package/crates/team-agent/src/cli/diagnose.rs +554 -0
- package/crates/team-agent/src/cli/emit.rs +1204 -0
- package/crates/team-agent/src/cli/helpers.rs +88 -0
- package/crates/team-agent/src/cli/leader.rs +216 -0
- package/crates/team-agent/src/cli/mod.rs +1207 -0
- package/crates/team-agent/src/cli/profile.rs +306 -0
- package/crates/team-agent/src/cli/send.rs +215 -0
- package/crates/team-agent/src/cli/status.rs +179 -0
- package/crates/team-agent/src/cli/status_port.rs +502 -0
- package/crates/team-agent/src/cli/tests/base.rs +616 -0
- package/crates/team-agent/src/cli/tests/compile.rs +96 -0
- package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
- package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
- package/crates/team-agent/src/cli/tests/mod.rs +97 -0
- package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
- package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
- package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
- package/crates/team-agent/src/cli/types.rs +605 -0
- package/crates/team-agent/src/compiler/tests.rs +701 -0
- package/crates/team-agent/src/compiler.rs +489 -0
- package/crates/team-agent/src/coordinator/backoff.rs +153 -0
- package/crates/team-agent/src/coordinator/health.rs +557 -0
- package/crates/team-agent/src/coordinator/mod.rs +80 -0
- package/crates/team-agent/src/coordinator/orphan.rs +179 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
- package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
- package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
- package/crates/team-agent/src/coordinator/tick.rs +2032 -0
- package/crates/team-agent/src/coordinator/types.rs +584 -0
- package/crates/team-agent/src/db/migration.rs +716 -0
- package/crates/team-agent/src/db/mod.rs +23 -0
- package/crates/team-agent/src/db/schema.rs +378 -0
- package/crates/team-agent/src/event_log.rs +375 -0
- package/crates/team-agent/src/fake_worker.rs +253 -0
- package/crates/team-agent/src/leader/helpers.rs +190 -0
- package/crates/team-agent/src/leader/inject.rs +33 -0
- package/crates/team-agent/src/leader/lease.rs +1084 -0
- package/crates/team-agent/src/leader/mod.rs +99 -0
- package/crates/team-agent/src/leader/owner_bind.rs +292 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +526 -0
- package/crates/team-agent/src/leader/rediscover.rs +1101 -0
- package/crates/team-agent/src/leader/start.rs +273 -0
- package/crates/team-agent/src/leader/takeover.rs +235 -0
- package/crates/team-agent/src/leader/tests/basics.rs +183 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +237 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +272 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +410 -0
- package/crates/team-agent/src/leader/tests/mod.rs +125 -0
- package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
- package/crates/team-agent/src/leader/types.rs +489 -0
- package/crates/team-agent/src/lib.rs +85 -0
- package/crates/team-agent/src/lifecycle/display.rs +228 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
- package/crates/team-agent/src/lifecycle/launch.rs +2109 -0
- package/crates/team-agent/src/lifecycle/mod.rs +62 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
- package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
- package/crates/team-agent/src/lifecycle/restart.rs +76 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
- package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +985 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests.rs +27 -0
- package/crates/team-agent/src/lifecycle/types.rs +710 -0
- package/crates/team-agent/src/main.rs +41 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
- package/crates/team-agent/src/mcp_server/mod.rs +183 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
- package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
- package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
- package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +187 -0
- package/crates/team-agent/src/mcp_server/tests.rs +38 -0
- package/crates/team-agent/src/mcp_server/tools.rs +603 -0
- package/crates/team-agent/src/mcp_server/types.rs +421 -0
- package/crates/team-agent/src/mcp_server/wire.rs +468 -0
- package/crates/team-agent/src/message_store.rs +767 -0
- package/crates/team-agent/src/messaging/activity.rs +433 -0
- package/crates/team-agent/src/messaging/delivery.rs +743 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +329 -0
- package/crates/team-agent/src/messaging/mod.rs +147 -0
- package/crates/team-agent/src/messaging/peers.rs +32 -0
- package/crates/team-agent/src/messaging/results.rs +553 -0
- package/crates/team-agent/src/messaging/scheduler.rs +344 -0
- package/crates/team-agent/src/messaging/selftest.rs +100 -0
- package/crates/team-agent/src/messaging/send.rs +578 -0
- package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
- package/crates/team-agent/src/messaging/trust.rs +192 -0
- package/crates/team-agent/src/messaging/types.rs +355 -0
- package/crates/team-agent/src/messaging/watchers.rs +591 -0
- package/crates/team-agent/src/model/enums.rs +311 -0
- package/crates/team-agent/src/model/errors.rs +17 -0
- package/crates/team-agent/src/model/ids.rs +155 -0
- package/crates/team-agent/src/model/mod.rs +22 -0
- package/crates/team-agent/src/model/paths.rs +228 -0
- package/crates/team-agent/src/model/permissions.rs +567 -0
- package/crates/team-agent/src/model/routing.rs +340 -0
- package/crates/team-agent/src/model/spec.rs +680 -0
- package/crates/team-agent/src/model/task_graph.rs +380 -0
- package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
- package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
- package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
- package/crates/team-agent/src/model/yaml/tests.rs +288 -0
- package/crates/team-agent/src/model/yaml.rs +800 -0
- package/crates/team-agent/src/packaging/install.rs +305 -0
- package/crates/team-agent/src/packaging/migrate.rs +30 -0
- package/crates/team-agent/src/packaging/mod.rs +82 -0
- package/crates/team-agent/src/packaging/repair.rs +24 -0
- package/crates/team-agent/src/packaging/tests.rs +829 -0
- package/crates/team-agent/src/packaging/types.rs +369 -0
- package/crates/team-agent/src/provider/adapter.rs +801 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
- package/crates/team-agent/src/provider/classify.rs +456 -0
- package/crates/team-agent/src/provider/faults.rs +136 -0
- package/crates/team-agent/src/provider/helpers.rs +41 -0
- package/crates/team-agent/src/provider/mod.rs +53 -0
- package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
- package/crates/team-agent/src/provider/tests/classify.rs +240 -0
- package/crates/team-agent/src/provider/tests/faults.rs +120 -0
- package/crates/team-agent/src/provider/tests/idle.rs +208 -0
- package/crates/team-agent/src/provider/tests/wire.rs +213 -0
- package/crates/team-agent/src/provider/tests.rs +31 -0
- package/crates/team-agent/src/provider/types.rs +424 -0
- package/crates/team-agent/src/state/identity.rs +659 -0
- package/crates/team-agent/src/state/mod.rs +58 -0
- package/crates/team-agent/src/state/owner_gate.rs +423 -0
- package/crates/team-agent/src/state/persist.rs +712 -0
- package/crates/team-agent/src/state/projection.rs +657 -0
- package/crates/team-agent/src/state/selector.rs +105 -0
- package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +765 -0
- package/crates/team-agent/src/tmux_backend.rs +810 -0
- package/crates/team-agent/src/transport/test_support.rs +252 -0
- package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
- package/crates/team-agent/src/transport/tests/mod.rs +199 -0
- package/crates/team-agent/src/transport/tests/wire.rs +527 -0
- package/crates/team-agent/src/transport.rs +774 -0
- package/npm/install.mjs +118 -112
- package/package.json +15 -13
- package/crates/team-agent-core/Cargo.toml +0 -12
- package/crates/team-agent-core/src/lib.rs +0 -332
- package/crates/team-agent-core/src/main.rs +0 -152
- package/pyproject.toml +0 -18
- package/scripts/install.py +0 -88
- package/scripts/run_regression_tests.py +0 -83
- package/src/team_agent/__init__.py +0 -3
- package/src/team_agent/__main__.py +0 -5
- package/src/team_agent/_legacy_pane_discovery.py +0 -186
- package/src/team_agent/abnormal_track.py +0 -253
- package/src/team_agent/approvals/__init__.py +0 -65
- package/src/team_agent/approvals/constants.py +0 -6
- package/src/team_agent/approvals/parsing.py +0 -176
- package/src/team_agent/approvals/runtime_prompts.py +0 -171
- package/src/team_agent/approvals/status.py +0 -176
- package/src/team_agent/cli/__init__.py +0 -137
- package/src/team_agent/cli/commands.py +0 -481
- package/src/team_agent/cli/e2e.py +0 -202
- package/src/team_agent/cli/helpers.py +0 -226
- package/src/team_agent/cli/parser.py +0 -540
- package/src/team_agent/compiler.py +0 -334
- package/src/team_agent/coordinator/__init__.py +0 -53
- package/src/team_agent/coordinator/__main__.py +0 -119
- package/src/team_agent/coordinator/lifecycle.py +0 -411
- package/src/team_agent/coordinator/metadata.py +0 -61
- package/src/team_agent/coordinator/paths.py +0 -17
- package/src/team_agent/diagnose/__init__.py +0 -48
- package/src/team_agent/diagnose/checks.py +0 -101
- package/src/team_agent/diagnose/comms.py +0 -213
- package/src/team_agent/diagnose/health.py +0 -241
- package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
- package/src/team_agent/diagnose/preflight.py +0 -194
- package/src/team_agent/diagnose/quick_start.py +0 -324
- package/src/team_agent/display/__init__.py +0 -92
- package/src/team_agent/display/adaptive.py +0 -511
- package/src/team_agent/display/backend.py +0 -46
- package/src/team_agent/display/close.py +0 -154
- package/src/team_agent/display/ghostty.py +0 -77
- package/src/team_agent/display/rebuild.py +0 -102
- package/src/team_agent/display/tiling.py +0 -156
- package/src/team_agent/display/worker_window.py +0 -114
- package/src/team_agent/display/workspace.py +0 -382
- package/src/team_agent/errors.py +0 -10
- package/src/team_agent/events.py +0 -84
- package/src/team_agent/fake_worker.py +0 -80
- package/src/team_agent/idle_predicate.py +0 -218
- package/src/team_agent/idle_takeover.py +0 -59
- package/src/team_agent/idle_takeover_wiring.py +0 -114
- package/src/team_agent/launch/__init__.py +0 -41
- package/src/team_agent/launch/bootstrap.py +0 -85
- package/src/team_agent/launch/config.py +0 -106
- package/src/team_agent/launch/core.py +0 -301
- package/src/team_agent/launch/requirements.py +0 -57
- package/src/team_agent/leader/__init__.py +0 -926
- package/src/team_agent/leader_binding.py +0 -183
- package/src/team_agent/lifecycle/__init__.py +0 -5
- package/src/team_agent/lifecycle/agents.py +0 -278
- package/src/team_agent/lifecycle/operations.py +0 -411
- package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
- package/src/team_agent/lifecycle/start.py +0 -363
- package/src/team_agent/mcp_server/__init__.py +0 -42
- package/src/team_agent/mcp_server/__main__.py +0 -7
- package/src/team_agent/mcp_server/contracts.py +0 -148
- package/src/team_agent/mcp_server/normalize.py +0 -257
- package/src/team_agent/mcp_server/server.py +0 -150
- package/src/team_agent/mcp_server/tools.py +0 -352
- package/src/team_agent/message_store/__init__.py +0 -23
- package/src/team_agent/message_store/agent_health.py +0 -113
- package/src/team_agent/message_store/core.py +0 -497
- package/src/team_agent/message_store/leader_notification_log.py +0 -198
- package/src/team_agent/message_store/result_watchers.py +0 -251
- package/src/team_agent/message_store/schema.py +0 -308
- package/src/team_agent/message_store/schema_migration.py +0 -448
- package/src/team_agent/messaging/__init__.py +0 -1
- package/src/team_agent/messaging/activity_detector.py +0 -262
- package/src/team_agent/messaging/delivery.py +0 -504
- package/src/team_agent/messaging/deps.py +0 -247
- package/src/team_agent/messaging/idle_alerts.py +0 -423
- package/src/team_agent/messaging/internal_delivery.py +0 -46
- package/src/team_agent/messaging/leader.py +0 -497
- package/src/team_agent/messaging/leader_api_errors.py +0 -216
- package/src/team_agent/messaging/leader_panes.py +0 -673
- package/src/team_agent/messaging/owner_bypass.py +0 -29
- package/src/team_agent/messaging/result_delivery.py +0 -539
- package/src/team_agent/messaging/results.py +0 -447
- package/src/team_agent/messaging/scheduler.py +0 -450
- package/src/team_agent/messaging/send.py +0 -532
- package/src/team_agent/messaging/session_drift.py +0 -94
- package/src/team_agent/messaging/tmux_io.py +0 -506
- package/src/team_agent/messaging/tmux_prompt.py +0 -338
- package/src/team_agent/messaging/trust_auto_answer.py +0 -52
- package/src/team_agent/orchestrator/__init__.py +0 -376
- package/src/team_agent/orchestrator/plan.py +0 -122
- package/src/team_agent/orchestrator/state.py +0 -128
- package/src/team_agent/paths.py +0 -45
- package/src/team_agent/permissions.py +0 -123
- package/src/team_agent/profiles/__init__.py +0 -82
- package/src/team_agent/profiles/constants.py +0 -19
- package/src/team_agent/profiles/core.py +0 -407
- package/src/team_agent/profiles/helpers.py +0 -69
- package/src/team_agent/profiles/provider_env.py +0 -188
- package/src/team_agent/profiles/smoke.py +0 -201
- package/src/team_agent/provider_cli/__init__.py +0 -43
- package/src/team_agent/provider_cli/adapter.py +0 -172
- package/src/team_agent/provider_cli/base.py +0 -48
- package/src/team_agent/provider_cli/claude.py +0 -503
- package/src/team_agent/provider_cli/codex.py +0 -336
- package/src/team_agent/provider_cli/copilot.py +0 -8
- package/src/team_agent/provider_cli/fake.py +0 -39
- package/src/team_agent/provider_cli/gemini.py +0 -95
- package/src/team_agent/provider_cli/opencode.py +0 -8
- package/src/team_agent/provider_cli/prompt.py +0 -62
- package/src/team_agent/provider_cli/registry.py +0 -18
- package/src/team_agent/provider_cli/unsupported.py +0 -32
- package/src/team_agent/provider_state/README.md +0 -78
- package/src/team_agent/provider_state/__init__.py +0 -91
- package/src/team_agent/provider_state/claude.py +0 -86
- package/src/team_agent/provider_state/codex.py +0 -84
- package/src/team_agent/provider_state/common.py +0 -207
- package/src/team_agent/provider_state/registry.py +0 -118
- package/src/team_agent/providers.py +0 -163
- package/src/team_agent/quality_gates.py +0 -104
- package/src/team_agent/restart/__init__.py +0 -34
- package/src/team_agent/restart/orchestration.py +0 -554
- package/src/team_agent/restart/selection.py +0 -89
- package/src/team_agent/restart/snapshot.py +0 -70
- package/src/team_agent/routing.py +0 -84
- package/src/team_agent/runtime.py +0 -1243
- package/src/team_agent/rust_core.py +0 -327
- package/src/team_agent/sessions/__init__.py +0 -25
- package/src/team_agent/sessions/capture.py +0 -144
- package/src/team_agent/sessions/inventory.py +0 -44
- package/src/team_agent/sessions/resume.py +0 -135
- package/src/team_agent/simple_yaml.py +0 -236
- package/src/team_agent/spec.py +0 -370
- package/src/team_agent/state.py +0 -693
- package/src/team_agent/status/__init__.py +0 -63
- package/src/team_agent/status/approvals.py +0 -52
- package/src/team_agent/status/compact.py +0 -158
- package/src/team_agent/status/constants.py +0 -18
- package/src/team_agent/status/inbox.py +0 -58
- package/src/team_agent/status/peek.py +0 -117
- package/src/team_agent/status/queries.py +0 -199
- package/src/team_agent/task_graph.py +0 -80
- package/src/team_agent/terminal.py +0 -57
- package/src/team_agent/wake.py +0 -58
- package/src/team_agent/watch/__init__.py +0 -145
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#[test]
|
|
2
|
+
fn requires_ack_for_target_leader_vs_worker() {
|
|
3
|
+
assert!(!requires_ack_for_target(&MessageTarget::Single("leader".to_string())));
|
|
4
|
+
assert!(!requires_ack_for_target(&MessageTarget::Single("Leader".to_string())));
|
|
5
|
+
assert!(requires_ack_for_target(&MessageTarget::Single("alice".to_string())));
|
|
6
|
+
// list: all-leader → false; any non-leader → true
|
|
7
|
+
assert!(!requires_ack_for_target(&MessageTarget::Fanout(vec![
|
|
8
|
+
"leader".to_string(), "Leader".to_string()
|
|
9
|
+
])));
|
|
10
|
+
assert!(requires_ack_for_target(&MessageTarget::Fanout(vec![
|
|
11
|
+
"leader".to_string(), "alice".to_string()
|
|
12
|
+
])));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
16
|
+
// is_worker_recipient — single str not in {"","*","leader","Leader"} (tools.py:22)
|
|
17
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
18
|
+
#[test]
|
|
19
|
+
fn is_worker_recipient_classification() {
|
|
20
|
+
assert!(is_worker_recipient(&MessageTarget::Single("alice".to_string())));
|
|
21
|
+
assert!(!is_worker_recipient(&MessageTarget::Single("".to_string())));
|
|
22
|
+
assert!(!is_worker_recipient(&MessageTarget::Single("leader".to_string())));
|
|
23
|
+
assert!(!is_worker_recipient(&MessageTarget::Single("Leader".to_string())));
|
|
24
|
+
// Broadcast "*" is NOT a worker recipient
|
|
25
|
+
assert!(!is_worker_recipient(&MessageTarget::Broadcast));
|
|
26
|
+
// Fanout list is NOT a worker recipient (not a single str)
|
|
27
|
+
assert!(!is_worker_recipient(&MessageTarget::Fanout(vec!["alice".to_string()])));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
31
|
+
// merge_tasks_by_id — prefer wins, prefer-first insertion order (tools.py:30)
|
|
32
|
+
// Golden: prefer t1(done),t2 + fallback t1(pending),t3,{no id},"notdict"
|
|
33
|
+
// → [t1(done), t2, t3] (t1 from prefer wins; non-dict / no-id dropped)
|
|
34
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
35
|
+
#[test]
|
|
36
|
+
fn merge_tasks_by_id_prefer_wins_no_done_regression() {
|
|
37
|
+
let prefer = vec![
|
|
38
|
+
json!({"id": "t1", "status": "done"}),
|
|
39
|
+
json!({"id": "t2", "status": "pending"}),
|
|
40
|
+
];
|
|
41
|
+
let fallback = vec![
|
|
42
|
+
json!({"id": "t1", "status": "pending"}), // must NOT regress t1
|
|
43
|
+
json!({"id": "t3", "status": "ready"}),
|
|
44
|
+
json!({"no": "id"}), // dropped (no id)
|
|
45
|
+
json!("notdict"), // dropped (not object)
|
|
46
|
+
];
|
|
47
|
+
let merged = merge_tasks_by_id(&prefer, &fallback);
|
|
48
|
+
assert_eq!(merged.len(), 3);
|
|
49
|
+
assert_eq!(merged[0]["id"], json!("t1"));
|
|
50
|
+
assert_eq!(merged[0]["status"], json!("done")); // prefer wins → no regression
|
|
51
|
+
assert_eq!(merged[1]["id"], json!("t2"));
|
|
52
|
+
assert_eq!(merged[2]["id"], json!("t3"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
56
|
+
// SendOutcome::to_value — worker-accepted async envelope (tools.py:177-182)
|
|
57
|
+
// byte-stable: {status:"accepted",delivery_pending:true,
|
|
58
|
+
// poll_via:"team-agent inbox <id>",message_id:<id>}
|
|
59
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
60
|
+
#[test]
|
|
61
|
+
fn send_outcome_worker_accepted_envelope_byte_stable() {
|
|
62
|
+
let outcome = SendOutcome::WorkerAccepted {
|
|
63
|
+
message_id: "42".to_string(),
|
|
64
|
+
poll_via: "team-agent inbox 42".to_string(),
|
|
65
|
+
};
|
|
66
|
+
let v = outcome.to_value();
|
|
67
|
+
assert_eq!(keys(&v), vec!["status", "delivery_pending", "poll_via", "message_id"]);
|
|
68
|
+
assert_eq!(s(&v),
|
|
69
|
+
r#"{"status":"accepted","delivery_pending":true,"poll_via":"team-agent inbox 42","message_id":"42"}"#);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn send_outcome_direct_renders_compact_body() {
|
|
74
|
+
// leader / * / broadcast path → compacted delegate body, not the accepted envelope.
|
|
75
|
+
let ok = ToolOk {
|
|
76
|
+
fields: {
|
|
77
|
+
let mut m = serde_json::Map::new();
|
|
78
|
+
m.insert("ok".to_string(), json!(true));
|
|
79
|
+
m.insert("status".to_string(), json!("queued"));
|
|
80
|
+
m
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
let v = SendOutcome::Direct(ok).to_value();
|
|
84
|
+
assert_eq!(v.get("status"), Some(&json!("queued")));
|
|
85
|
+
assert!(v.get("delivery_pending").is_none(), "Direct is NOT the accepted envelope");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
89
|
+
// CONTROL-PLANE: send_message worker recipient → WorkerAccepted (tools.py:135-183)
|
|
90
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
91
|
+
#[test]
|
|
92
|
+
fn send_message_worker_recipient_returns_accepted_with_poll_hint() {
|
|
93
|
+
// A worker recipient w/ a delivered message_id → async accepted carrying the
|
|
94
|
+
// byte-stable poll hint. Identity anchored on injected env (no candidate scan).
|
|
95
|
+
// golden: a leader WITH owner_team_id on an unseeded ws would hit the C23 cross-team
|
|
96
|
+
// refusal first (worker-1 not in visible peers) -> PeerNotInScope. owner_team_id=None
|
|
97
|
+
// (legacy single-team) bypasses that, isolating the worker-recipient accepted path.
|
|
98
|
+
// The cross-team refusal has its own tests (refuse_cross_team_peer_* / send_message_cross_team_*).
|
|
99
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
100
|
+
&unique_ws("send-worker"),
|
|
101
|
+
Some(AgentId::new("leader")),
|
|
102
|
+
None,
|
|
103
|
+
);
|
|
104
|
+
let outcome = tools.send_message(
|
|
105
|
+
&MessageTarget::Single("worker-1".to_string()),
|
|
106
|
+
"do the thing",
|
|
107
|
+
None, None, None, None,
|
|
108
|
+
);
|
|
109
|
+
match outcome {
|
|
110
|
+
Ok(SendOutcome::WorkerAccepted { message_id, poll_via }) => {
|
|
111
|
+
assert!(!message_id.is_empty());
|
|
112
|
+
assert_eq!(poll_via, format!("team-agent inbox {message_id}"));
|
|
113
|
+
}
|
|
114
|
+
other => panic!("worker recipient must be WorkerAccepted, got {other:?}"),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[test]
|
|
119
|
+
fn send_message_leader_recipient_is_direct_not_accepted() {
|
|
120
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
121
|
+
&unique_ws("send-leader"),
|
|
122
|
+
Some(AgentId::new("worker-1")),
|
|
123
|
+
Some(TeamKey::new("teamA")),
|
|
124
|
+
);
|
|
125
|
+
let outcome = tools.send_message(
|
|
126
|
+
&MessageTarget::Single("leader".to_string()),
|
|
127
|
+
"status update",
|
|
128
|
+
None, None, None, None,
|
|
129
|
+
).expect("leader send ok");
|
|
130
|
+
assert!(matches!(outcome, SendOutcome::Direct(_)),
|
|
131
|
+
"leader recipient → Direct (synchronous), not WorkerAccepted");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
135
|
+
// CROSS-TEAM PRE-REFUSAL (C23) — refuse_cross_team_peer (tools.py:185-213)
|
|
136
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
137
|
+
#[test]
|
|
138
|
+
fn refuse_cross_team_peer_blocks_unknown_peer_without_workspace_scope() {
|
|
139
|
+
// owner_team set, target a peer NOT in scope, scope != workspace → PeerNotInScope.
|
|
140
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
141
|
+
Path::new("/tmp/ws"),
|
|
142
|
+
Some(AgentId::new("worker-1")),
|
|
143
|
+
Some(TeamKey::new("teamA")),
|
|
144
|
+
);
|
|
145
|
+
let refusal = tools.refuse_cross_team_peer(
|
|
146
|
+
&MessageTarget::Single("other-team-bob".to_string()),
|
|
147
|
+
None,
|
|
148
|
+
);
|
|
149
|
+
let te = refusal.expect("cross-team peer must be refused");
|
|
150
|
+
assert_eq!(te.reason, ToolErrorReason::PeerNotInScope);
|
|
151
|
+
// hint preserved in extra (tools.py:208-213 status:"refused" + hint)
|
|
152
|
+
let env = te.to_envelope();
|
|
153
|
+
assert_eq!(env.get("status"), Some(&json!("refused")));
|
|
154
|
+
assert_eq!(env.get("reason"), Some(&json!("peer_not_in_scope")));
|
|
155
|
+
assert_eq!(
|
|
156
|
+
env.get("hint"),
|
|
157
|
+
Some(&json!("the requested peer is not part of your team. pass scope='workspace' to address peers in other teams."))
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[test]
|
|
162
|
+
fn refuse_cross_team_peer_allows_workspace_scope_optin() {
|
|
163
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
164
|
+
Path::new("/tmp/ws"),
|
|
165
|
+
Some(AgentId::new("worker-1")),
|
|
166
|
+
Some(TeamKey::new("teamA")),
|
|
167
|
+
);
|
|
168
|
+
// scope="workspace" → None (allowed to proceed)
|
|
169
|
+
assert!(tools.refuse_cross_team_peer(
|
|
170
|
+
&MessageTarget::Single("other-team-bob".to_string()),
|
|
171
|
+
Some(Scope::Workspace),
|
|
172
|
+
).is_none(), "workspace scope opts in to cross-team addressing");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[test]
|
|
176
|
+
fn refuse_cross_team_peer_allows_leader_broadcast_and_self() {
|
|
177
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
178
|
+
Path::new("/tmp/ws"),
|
|
179
|
+
Some(AgentId::new("worker-1")),
|
|
180
|
+
Some(TeamKey::new("teamA")),
|
|
181
|
+
);
|
|
182
|
+
// leader / "*" / "" / self are never refused (tools.py:190,195)
|
|
183
|
+
assert!(tools.refuse_cross_team_peer(&MessageTarget::Single("leader".to_string()), None).is_none());
|
|
184
|
+
assert!(tools.refuse_cross_team_peer(&MessageTarget::Broadcast, None).is_none());
|
|
185
|
+
assert!(tools.refuse_cross_team_peer(&MessageTarget::Single("worker-1".to_string()), None).is_none());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[test]
|
|
189
|
+
fn refuse_cross_team_peer_no_owner_team_is_legacy_passthrough() {
|
|
190
|
+
// No owner_team_id (legacy single-team) → never refuse (tools.py:192).
|
|
191
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
192
|
+
Path::new("/tmp/ws"),
|
|
193
|
+
Some(AgentId::new("worker-1")),
|
|
194
|
+
None,
|
|
195
|
+
);
|
|
196
|
+
assert!(tools.refuse_cross_team_peer(
|
|
197
|
+
&MessageTarget::Single("anybody".to_string()),
|
|
198
|
+
None,
|
|
199
|
+
).is_none());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[test]
|
|
203
|
+
fn send_message_cross_team_peer_surfaces_peer_not_in_scope_error() {
|
|
204
|
+
// End-to-end: send_message to an out-of-scope peer → Err(ToolError{PeerNotInScope})
|
|
205
|
+
// BEFORE any runtime delivery (server-side guard, no peer-name leak).
|
|
206
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
207
|
+
Path::new("/tmp/ws"),
|
|
208
|
+
Some(AgentId::new("worker-1")),
|
|
209
|
+
Some(TeamKey::new("teamA")),
|
|
210
|
+
);
|
|
211
|
+
let err = tools.send_message(
|
|
212
|
+
&MessageTarget::Single("other-team-bob".to_string()),
|
|
213
|
+
"leak attempt",
|
|
214
|
+
None, None, None, None,
|
|
215
|
+
).expect_err("out-of-scope peer must be refused");
|
|
216
|
+
assert_eq!(err.reason, ToolErrorReason::PeerNotInScope);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
220
|
+
// WORKER-ID INFERENCE FALLBACK (bug-085, C17) — report_result identity.
|
|
221
|
+
// explicit > env > "unknown"; task → "manual". NEVER treat worker as leader.
|
|
222
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#[test]
|
|
2
|
+
fn report_result_infers_agent_from_env_when_not_explicit() {
|
|
3
|
+
// env identity present, no explicit agent_id → envelope.agent_id == env id.
|
|
4
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
5
|
+
&unique_ws("report-env"),
|
|
6
|
+
Some(AgentId::new("worker-7")),
|
|
7
|
+
Some(TeamKey::new("teamA")),
|
|
8
|
+
);
|
|
9
|
+
let ok = tools.report_result(
|
|
10
|
+
None, Some("done it"), ResultStatus::Success,
|
|
11
|
+
None, None, None, None, None,
|
|
12
|
+
None, None, // no explicit task/agent
|
|
13
|
+
).expect("report ok");
|
|
14
|
+
let v = serde_json::to_value(&ok).unwrap();
|
|
15
|
+
// C17/bug-085: env id wins over leader/unknown. agent_id is a guaranteed-present
|
|
16
|
+
// ok-whitelist key (normalize.py:46) — runtime.report_result echoes envelope
|
|
17
|
+
// ["agent_id"], which _infer_agent_id sourced from self.agent_id (the injected
|
|
18
|
+
// TEAM_AGENT_ID). UNCONDITIONAL assert: a vacuous skip here would silently fail
|
|
19
|
+
// to lock the exact invariant this lane exists for.
|
|
20
|
+
assert_eq!(
|
|
21
|
+
v.get("agent_id"),
|
|
22
|
+
Some(&json!("worker-7")),
|
|
23
|
+
"env id wins, never leader/unknown"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[test]
|
|
28
|
+
fn report_result_explicit_agent_overrides_env() {
|
|
29
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
30
|
+
&unique_ws("report-explicit"),
|
|
31
|
+
Some(AgentId::new("worker-7")),
|
|
32
|
+
Some(TeamKey::new("teamA")),
|
|
33
|
+
);
|
|
34
|
+
let ok = tools.report_result(
|
|
35
|
+
None, Some("done"), ResultStatus::Success,
|
|
36
|
+
None, None, None, None, None,
|
|
37
|
+
Some("task-9"), Some("explicit-agent"),
|
|
38
|
+
).expect("report ok");
|
|
39
|
+
let v = serde_json::to_value(&ok).unwrap();
|
|
40
|
+
// explicit > env: both task_id and agent_id are ok-whitelist keys
|
|
41
|
+
// (normalize.py:45-46) echoed by runtime.report_result, so present on success.
|
|
42
|
+
// UNCONDITIONAL asserts — the override must be proven, not silently skipped.
|
|
43
|
+
assert_eq!(
|
|
44
|
+
v.get("agent_id"),
|
|
45
|
+
Some(&json!("explicit-agent")),
|
|
46
|
+
"explicit agent_id overrides env"
|
|
47
|
+
);
|
|
48
|
+
assert_eq!(
|
|
49
|
+
v.get("task_id"),
|
|
50
|
+
Some(&json!("task-9")),
|
|
51
|
+
"explicit task_id flows through"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[test]
|
|
56
|
+
fn report_result_no_env_no_explicit_falls_back_unknown_and_manual() {
|
|
57
|
+
// bug-085: env missing + nothing explicit → agent "unknown", task "manual"
|
|
58
|
+
// (NOT None, NOT "leader"). The envelope-builder is the asserted seam.
|
|
59
|
+
let env = normalize_report_envelope(&json!({"summary": "x"}));
|
|
60
|
+
assert_eq!(env.agent_id, AgentId::new("unknown"));
|
|
61
|
+
assert_eq!(env.task_id, TaskId::new("manual"));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
65
|
+
// CONTROL-PLANE: request_human creates a requires_ack leader message → needs_human
|
|
66
|
+
// (tools.py:342-346). sender = explicit > env > "unknown" (never leader).
|
|
67
|
+
//
|
|
68
|
+
// Post-#230 N31/N32 funnel (cr-approved I-3): request_human routes through the
|
|
69
|
+
// shared leader-delivery primitive (`send_to_leader_receiver`) instead of doing a
|
|
70
|
+
// raw `store.create_message` bypass. Return shape from the caller's perspective is
|
|
71
|
+
// unchanged: `status="needs_human"` + a populated `message_id`. With no leader
|
|
72
|
+
// pane bound in this fixture, the primitive's I-4 rebind_required path STILL
|
|
73
|
+
// persists the message row and returns its `message_id` — audit + rebind replay
|
|
74
|
+
// both depend on it.
|
|
75
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
76
|
+
#[test]
|
|
77
|
+
fn request_human_returns_needs_human_with_message_id() {
|
|
78
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
79
|
+
&unique_ws("request-human"),
|
|
80
|
+
Some(AgentId::new("worker-3")),
|
|
81
|
+
Some(TeamKey::new("teamA")),
|
|
82
|
+
);
|
|
83
|
+
let ok = tools.request_human("need approval", Some("task-1"), None)
|
|
84
|
+
.expect("request_human ok");
|
|
85
|
+
let v = serde_json::to_value(&ok).unwrap();
|
|
86
|
+
assert_eq!(v.get("status"), Some(&json!("needs_human")));
|
|
87
|
+
assert!(v.get("message_id").and_then(Value::as_str).is_some(),
|
|
88
|
+
"request_human must return the created leader message_id (persisted for rebind audit even on I-4 rebind_required)");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
92
|
+
// CONTROL-PLANE: update_state appends a note + returns state_file (tools.py:316-325)
|
|
93
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
94
|
+
#[test]
|
|
95
|
+
fn update_state_returns_ok_and_state_file_path() {
|
|
96
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
97
|
+
&unique_ws("update-state"),
|
|
98
|
+
Some(AgentId::new("leader")),
|
|
99
|
+
Some(TeamKey::new("teamA")),
|
|
100
|
+
);
|
|
101
|
+
let ok = tools.update_state("checkpoint note").expect("update_state ok");
|
|
102
|
+
let v = serde_json::to_value(&ok).unwrap();
|
|
103
|
+
assert_eq!(v.get("ok"), Some(&json!(true)));
|
|
104
|
+
assert!(v.get("state_file").and_then(Value::as_str).is_some(),
|
|
105
|
+
"update_state returns the rewritten team_state.md path");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
109
|
+
// RpcId / RpcResponse byte-stability — null id echoed, error frame shape.
|
|
110
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
111
|
+
#[test]
|
|
112
|
+
fn rpc_response_error_frame_serializes_without_result_key() {
|
|
113
|
+
// server.py: error frames carry NO result key; result frames carry NO error key.
|
|
114
|
+
let frame = RpcResponse {
|
|
115
|
+
jsonrpc: "2.0".to_string(),
|
|
116
|
+
id: RpcId::Int(7),
|
|
117
|
+
result: None,
|
|
118
|
+
error: Some(RpcError { code: -32601, message: "unknown method 'x'".to_string() }),
|
|
119
|
+
};
|
|
120
|
+
let v = serde_json::to_value(&frame).unwrap();
|
|
121
|
+
assert!(v.get("result").is_none(), "error frame omits result");
|
|
122
|
+
assert_eq!(v["error"]["code"], json!(-32601));
|
|
123
|
+
assert_eq!(v["jsonrpc"], json!("2.0"));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#[test]
|
|
127
|
+
fn rpc_id_null_roundtrips() {
|
|
128
|
+
// request.get("id") absent/null → echoed back as null
|
|
129
|
+
let frame = RpcResponse {
|
|
130
|
+
jsonrpc: "2.0".to_string(),
|
|
131
|
+
id: RpcId::Null,
|
|
132
|
+
result: Some(json!({"ok": true})),
|
|
133
|
+
error: None,
|
|
134
|
+
};
|
|
135
|
+
let v = serde_json::to_value(&frame).unwrap();
|
|
136
|
+
assert_eq!(v["id"], Value::Null);
|
|
137
|
+
assert!(v.get("error").is_none(), "result frame omits error");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
141
|
+
// STEP-14 DIVERGENCE-FIX RED TESTS (Phase 1). Each encodes the EXACT Python
|
|
142
|
+
// golden v0.2.11 value (probed via PYTHONPATH=.../src python3) and FAILS against
|
|
143
|
+
// current Rust. The P2 porter greens these. Do NOT weaken existing assertions.
|
|
144
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
145
|
+
|
|
146
|
+
/// Seed `<ws>/.team/runtime/state.json` and return the CANONICAL workspace path
|
|
147
|
+
/// so `with_identity` (which canonicalizes) reads the same file we wrote.
|
|
148
|
+
fn seed_state_ws(tag: &str, state: &Value) -> std::path::PathBuf {
|
|
149
|
+
let ws = unique_ws(tag);
|
|
150
|
+
let cws = std::fs::canonicalize(&ws).unwrap_or(ws);
|
|
151
|
+
let rt = cws.join(".team").join("runtime");
|
|
152
|
+
std::fs::create_dir_all(&rt).unwrap();
|
|
153
|
+
std::fs::write(rt.join("state.json"), serde_json::to_string_pretty(state).unwrap()).unwrap();
|
|
154
|
+
cws
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── #29/#43/#49 compact ok-whitelist: EXACT 23-key golden list + order ──────
|
|
158
|
+
// GOLDEN (probe_mcp_red.py OK-FULL-KEYS): the 23 keys in normalize.py:32-56 order
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#[test]
|
|
2
|
+
fn mcp_tool_wire_names_and_parse_roundtrip() {
|
|
3
|
+
let names = [
|
|
4
|
+
(McpTool::AssignTask, "assign_task"),
|
|
5
|
+
(McpTool::SendMessage, "send_message"),
|
|
6
|
+
(McpTool::ReportResult, "report_result"),
|
|
7
|
+
(McpTool::UpdateState, "update_state"),
|
|
8
|
+
(McpTool::GetTeamStatus, "get_team_status"),
|
|
9
|
+
(McpTool::StopAgent, "stop_agent"),
|
|
10
|
+
(McpTool::ResetAgent, "reset_agent"),
|
|
11
|
+
(McpTool::AddAgent, "add_agent"),
|
|
12
|
+
(McpTool::ForkAgent, "fork_agent"),
|
|
13
|
+
(McpTool::RequestHuman, "request_human"),
|
|
14
|
+
(McpTool::StuckList, "stuck_list"),
|
|
15
|
+
(McpTool::StuckCancel, "stuck_cancel"),
|
|
16
|
+
];
|
|
17
|
+
for (tool, name) in names {
|
|
18
|
+
assert_eq!(tool.wire_name(), name);
|
|
19
|
+
assert_eq!(McpTool::parse(name), Some(tool));
|
|
20
|
+
}
|
|
21
|
+
// unknown → None (server.py:43 maps to UnknownTool)
|
|
22
|
+
assert_eq!(McpTool::parse("nope"), None);
|
|
23
|
+
assert_eq!(McpTool::parse("AssignTask"), None); // case-sensitive snake_case
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[test]
|
|
27
|
+
fn rpc_method_classify() {
|
|
28
|
+
assert_eq!(RpcMethod::classify("initialize"), RpcMethod::Initialize);
|
|
29
|
+
assert_eq!(RpcMethod::classify("tools/list"), RpcMethod::ToolsList);
|
|
30
|
+
assert_eq!(RpcMethod::classify("tools/call"), RpcMethod::ToolsCall);
|
|
31
|
+
// notifications/* → Notification (no reply path)
|
|
32
|
+
assert!(matches!(
|
|
33
|
+
RpcMethod::classify("notifications/initialized"),
|
|
34
|
+
RpcMethod::Notification(_)
|
|
35
|
+
));
|
|
36
|
+
// unknown → Unknown
|
|
37
|
+
assert_eq!(
|
|
38
|
+
RpcMethod::classify("foo/bar"),
|
|
39
|
+
RpcMethod::Unknown("foo/bar".to_string())
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
44
|
+
// tools_contract — TOOLS wire list (contracts.py): 12 tools, exact names+order
|
|
45
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
46
|
+
#[test]
|
|
47
|
+
fn tools_contract_has_twelve_tools_in_order() {
|
|
48
|
+
let tools = tools_contract();
|
|
49
|
+
assert_eq!(tools.len(), 12);
|
|
50
|
+
let got: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
|
|
51
|
+
assert_eq!(got, vec![
|
|
52
|
+
"assign_task", "send_message", "report_result", "update_state",
|
|
53
|
+
"get_team_status", "stop_agent", "reset_agent", "add_agent",
|
|
54
|
+
"fork_agent", "request_human", "stuck_list", "stuck_cancel",
|
|
55
|
+
]);
|
|
56
|
+
// each carries description + inputSchema
|
|
57
|
+
for t in &tools {
|
|
58
|
+
assert!(t.get("description").and_then(Value::as_str).is_some());
|
|
59
|
+
assert!(t.get("inputSchema").is_some());
|
|
60
|
+
}
|
|
61
|
+
// spot-check byte-stable description + schema for send_message
|
|
62
|
+
let send = tools.iter().find(|t| t["name"] == json!("send_message")).unwrap();
|
|
63
|
+
assert_eq!(
|
|
64
|
+
send["description"],
|
|
65
|
+
json!("Send a message to a teammate, the leader, or '*' for all other team members. Provide only target and content; Team Agent fills sender, task id, ack policy, and delivery metadata.")
|
|
66
|
+
);
|
|
67
|
+
assert_eq!(send["inputSchema"]["additionalProperties"], json!(false));
|
|
68
|
+
assert_eq!(send["inputSchema"]["required"], json!(["to", "content"]));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[test]
|
|
72
|
+
fn tools_contract_input_schemas_are_openai_strict_top_level_objects() {
|
|
73
|
+
let forbidden = ["oneOf", "anyOf", "allOf", "enum", "not"];
|
|
74
|
+
for tool in tools_contract() {
|
|
75
|
+
let schema = tool["inputSchema"].as_object().unwrap();
|
|
76
|
+
assert_eq!(schema.get("type"), Some(&json!("object")), "schema must be a top-level object: {tool}");
|
|
77
|
+
for key in forbidden {
|
|
78
|
+
assert!(
|
|
79
|
+
!schema.contains_key(key),
|
|
80
|
+
"OpenAI rejects top-level `{key}` in MCP tool schema: {tool}"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
let properties = schema
|
|
84
|
+
.get("properties")
|
|
85
|
+
.and_then(Value::as_object)
|
|
86
|
+
.unwrap_or_else(|| panic!("schema properties must be an object: {tool}"));
|
|
87
|
+
for required in schema.get("required").and_then(Value::as_array).into_iter().flatten() {
|
|
88
|
+
let Some(name) = required.as_str() else {
|
|
89
|
+
panic!("required entries must be strings: {tool}");
|
|
90
|
+
};
|
|
91
|
+
assert!(
|
|
92
|
+
properties.contains_key(name),
|
|
93
|
+
"required property `{name}` must be declared in properties: {tool}"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
100
|
+
// handle_mcp — JSON-RPC routing (server.py:46-91)
|
|
101
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
102
|
+
#[test]
|
|
103
|
+
fn handle_mcp_initialize_echoes_protocol_and_serverinfo() {
|
|
104
|
+
let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
|
|
105
|
+
let resp = handle_mcp(&tools, &json!({
|
|
106
|
+
"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
|
107
|
+
"params": {"protocolVersion": "X"}
|
|
108
|
+
})).unwrap().expect("initialize yields a frame");
|
|
109
|
+
assert_eq!(resp.jsonrpc, "2.0");
|
|
110
|
+
assert_eq!(resp.id, RpcId::Int(1));
|
|
111
|
+
let result = resp.result.unwrap();
|
|
112
|
+
assert_eq!(result["protocolVersion"], json!("X"));
|
|
113
|
+
assert_eq!(result["serverInfo"]["name"], json!("team_orchestrator"));
|
|
114
|
+
assert_eq!(result["serverInfo"]["version"], json!("0.1.4"));
|
|
115
|
+
assert_eq!(result["capabilities"], json!({"tools": {}}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[test]
|
|
119
|
+
fn handle_mcp_initialize_defaults_protocol_version() {
|
|
120
|
+
let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
|
|
121
|
+
let resp = handle_mcp(&tools, &json!({
|
|
122
|
+
"jsonrpc": "2.0", "id": "abc", "method": "initialize"
|
|
123
|
+
})).unwrap().unwrap();
|
|
124
|
+
assert_eq!(resp.id, RpcId::Str("abc".to_string()));
|
|
125
|
+
assert_eq!(resp.result.unwrap()["protocolVersion"], json!("2024-11-05"));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[test]
|
|
129
|
+
fn handle_mcp_notifications_return_none_no_frame() {
|
|
130
|
+
// 铁律: notifications/* MUST NOT emit a frame (would corrupt stdout stream).
|
|
131
|
+
let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
|
|
132
|
+
let resp = handle_mcp(&tools, &json!({
|
|
133
|
+
"jsonrpc": "2.0", "method": "notifications/initialized"
|
|
134
|
+
})).unwrap();
|
|
135
|
+
assert!(resp.is_none(), "notifications/* → None (loop continues)");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#[test]
|
|
139
|
+
fn handle_mcp_unknown_method_is_minus_32601() {
|
|
140
|
+
let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
|
|
141
|
+
let resp = handle_mcp(&tools, &json!({
|
|
142
|
+
"jsonrpc": "2.0", "id": 7, "method": "foo/bar"
|
|
143
|
+
})).unwrap().unwrap();
|
|
144
|
+
assert!(resp.result.is_none());
|
|
145
|
+
let err = resp.error.unwrap();
|
|
146
|
+
assert_eq!(err.code, -32601);
|
|
147
|
+
assert_eq!(err.message, "unknown method 'foo/bar'"); // exact Python repr w/ quotes
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[test]
|
|
151
|
+
fn handle_mcp_unknown_tool_call_is_error_with_envelope_text() {
|
|
152
|
+
// tools/call with unknown tool → isError:true, content[0].text == json.dumps(envelope)
|
|
153
|
+
let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
|
|
154
|
+
let resp = handle_mcp(&tools, &json!({
|
|
155
|
+
"jsonrpc": "2.0", "id": 9, "method": "tools/call",
|
|
156
|
+
"params": {"name": "nope", "arguments": {}}
|
|
157
|
+
})).unwrap().unwrap();
|
|
158
|
+
let result = resp.result.unwrap();
|
|
159
|
+
assert_eq!(result["isError"], json!(true));
|
|
160
|
+
let text = result["content"][0]["text"].as_str().unwrap();
|
|
161
|
+
// the text is a JSON-encoded error envelope with redundant keys
|
|
162
|
+
let env: Value = serde_json::from_str(text).unwrap();
|
|
163
|
+
assert_eq!(env["ok"], json!(false));
|
|
164
|
+
assert_eq!(env["reason"], json!("unknown_tool"));
|
|
165
|
+
assert_eq!(env["error_code"], json!("unknown_tool"));
|
|
166
|
+
assert_eq!(env["exc_type"], json!("UnknownTool"));
|
|
167
|
+
assert_eq!(env["message"], json!("unknown tool 'nope'"));
|
|
168
|
+
assert_eq!(env["error"], json!("unknown tool 'nope'"));
|
|
169
|
+
assert_eq!(result["content"][0]["type"], json!("text"));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
173
|
+
// dispatch — unknown tool → Err(UnknownTool) (server.py:43)
|
|
174
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
175
|
+
#[test]
|
|
176
|
+
fn dispatch_unknown_tool_returns_unknown_tool_error() {
|
|
177
|
+
let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
|
|
178
|
+
let r = dispatch(&tools, &json!({"tool": "nope"}));
|
|
179
|
+
let err = r.expect_err("unknown tool ⇒ Err");
|
|
180
|
+
assert_eq!(err.reason, ToolErrorReason::UnknownTool);
|
|
181
|
+
assert_eq!(err.exc_type, "UnknownTool");
|
|
182
|
+
assert_eq!(err.message, "unknown tool 'nope'");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
186
|
+
// requires_ack_for_target — leader-only → false (tools.py:16)
|
|
187
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//! step 14a · mcp_server::tests — WAVE-2 RED contracts (Python v0.2.11 golden).
|
|
2
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
3
|
+
|
|
4
|
+
use super::*;
|
|
5
|
+
use serde_json::json;
|
|
6
|
+
|
|
7
|
+
// ── helpers ──────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/// Serialize a serde_json::Value to a string — used to assert byte-stable
|
|
10
|
+
/// key ORDER (preserve_order is enabled workspace-wide).
|
|
11
|
+
fn s(v: &Value) -> String {
|
|
12
|
+
serde_json::to_string(v).unwrap()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// Ordered list of keys as they appear in a JSON object Value.
|
|
16
|
+
fn keys(v: &Value) -> Vec<String> {
|
|
17
|
+
v.as_object().unwrap().keys().cloned().collect()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// A UNIQUE throwaway workspace dir per test (mirrors the state/coordinator idiom):
|
|
21
|
+
/// tests that open the db (MessageStore) or write the filesystem MUST NOT share
|
|
22
|
+
/// `/tmp/ws`, or they flake under parallel cargo (sqlite "database is locked" / NotFound).
|
|
23
|
+
/// Pure-function / dispatch-shape tests that never touch fs/db keep a dummy fixed path.
|
|
24
|
+
fn unique_ws(tag: &str) -> std::path::PathBuf {
|
|
25
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
26
|
+
static N: AtomicU64 = AtomicU64::new(0);
|
|
27
|
+
let n = N.fetch_add(1, Ordering::Relaxed);
|
|
28
|
+
let p = std::env::temp_dir().join(format!("ta-rs-mcp-{tag}-{}-{n}", std::process::id()));
|
|
29
|
+
std::fs::create_dir_all(&p).unwrap();
|
|
30
|
+
p
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
include!("tests/normalize.rs");
|
|
34
|
+
include!("tests/wire.rs");
|
|
35
|
+
include!("tests/send.rs");
|
|
36
|
+
include!("tests/tools.rs");
|
|
37
|
+
include!("tests/golden.rs");
|
|
38
|
+
include!("tests/scoped.rs");
|