@team-agent/installer 0.2.11 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1077 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1141 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +436 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1063 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +487 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +685 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +388 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +537 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +582 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +656 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +90 -106
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -503
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -91
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1243
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -144
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -693
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. 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,159 @@
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
+ // ════════════════════════════════════════════════════════════════════════
72
+ // handle_mcp — JSON-RPC routing (server.py:46-91)
73
+ // ════════════════════════════════════════════════════════════════════════
74
+ #[test]
75
+ fn handle_mcp_initialize_echoes_protocol_and_serverinfo() {
76
+ let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
77
+ let resp = handle_mcp(&tools, &json!({
78
+ "jsonrpc": "2.0", "id": 1, "method": "initialize",
79
+ "params": {"protocolVersion": "X"}
80
+ })).unwrap().expect("initialize yields a frame");
81
+ assert_eq!(resp.jsonrpc, "2.0");
82
+ assert_eq!(resp.id, RpcId::Int(1));
83
+ let result = resp.result.unwrap();
84
+ assert_eq!(result["protocolVersion"], json!("X"));
85
+ assert_eq!(result["serverInfo"]["name"], json!("team_orchestrator"));
86
+ assert_eq!(result["serverInfo"]["version"], json!("0.1.4"));
87
+ assert_eq!(result["capabilities"], json!({"tools": {}}));
88
+ }
89
+
90
+ #[test]
91
+ fn handle_mcp_initialize_defaults_protocol_version() {
92
+ let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
93
+ let resp = handle_mcp(&tools, &json!({
94
+ "jsonrpc": "2.0", "id": "abc", "method": "initialize"
95
+ })).unwrap().unwrap();
96
+ assert_eq!(resp.id, RpcId::Str("abc".to_string()));
97
+ assert_eq!(resp.result.unwrap()["protocolVersion"], json!("2024-11-05"));
98
+ }
99
+
100
+ #[test]
101
+ fn handle_mcp_notifications_return_none_no_frame() {
102
+ // 铁律: notifications/* MUST NOT emit a frame (would corrupt stdout stream).
103
+ let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
104
+ let resp = handle_mcp(&tools, &json!({
105
+ "jsonrpc": "2.0", "method": "notifications/initialized"
106
+ })).unwrap();
107
+ assert!(resp.is_none(), "notifications/* → None (loop continues)");
108
+ }
109
+
110
+ #[test]
111
+ fn handle_mcp_unknown_method_is_minus_32601() {
112
+ let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
113
+ let resp = handle_mcp(&tools, &json!({
114
+ "jsonrpc": "2.0", "id": 7, "method": "foo/bar"
115
+ })).unwrap().unwrap();
116
+ assert!(resp.result.is_none());
117
+ let err = resp.error.unwrap();
118
+ assert_eq!(err.code, -32601);
119
+ assert_eq!(err.message, "unknown method 'foo/bar'"); // exact Python repr w/ quotes
120
+ }
121
+
122
+ #[test]
123
+ fn handle_mcp_unknown_tool_call_is_error_with_envelope_text() {
124
+ // tools/call with unknown tool → isError:true, content[0].text == json.dumps(envelope)
125
+ let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
126
+ let resp = handle_mcp(&tools, &json!({
127
+ "jsonrpc": "2.0", "id": 9, "method": "tools/call",
128
+ "params": {"name": "nope", "arguments": {}}
129
+ })).unwrap().unwrap();
130
+ let result = resp.result.unwrap();
131
+ assert_eq!(result["isError"], json!(true));
132
+ let text = result["content"][0]["text"].as_str().unwrap();
133
+ // the text is a JSON-encoded error envelope with redundant keys
134
+ let env: Value = serde_json::from_str(text).unwrap();
135
+ assert_eq!(env["ok"], json!(false));
136
+ assert_eq!(env["reason"], json!("unknown_tool"));
137
+ assert_eq!(env["error_code"], json!("unknown_tool"));
138
+ assert_eq!(env["exc_type"], json!("UnknownTool"));
139
+ assert_eq!(env["message"], json!("unknown tool 'nope'"));
140
+ assert_eq!(env["error"], json!("unknown tool 'nope'"));
141
+ assert_eq!(result["content"][0]["type"], json!("text"));
142
+ }
143
+
144
+ // ════════════════════════════════════════════════════════════════════════
145
+ // dispatch — unknown tool → Err(UnknownTool) (server.py:43)
146
+ // ════════════════════════════════════════════════════════════════════════
147
+ #[test]
148
+ fn dispatch_unknown_tool_returns_unknown_tool_error() {
149
+ let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
150
+ let r = dispatch(&tools, &json!({"tool": "nope"}));
151
+ let err = r.expect_err("unknown tool ⇒ Err");
152
+ assert_eq!(err.reason, ToolErrorReason::UnknownTool);
153
+ assert_eq!(err.exc_type, "UnknownTool");
154
+ assert_eq!(err.message, "unknown tool 'nope'");
155
+ }
156
+
157
+ // ════════════════════════════════════════════════════════════════════════
158
+ // requires_ack_for_target — leader-only → false (tools.py:16)
159
+ // ════════════════════════════════════════════════════════════════════════
@@ -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");