@team-agent/installer 0.2.10 → 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 -83
  203. package/src/team_agent/coordinator/lifecycle.py +0 -363
  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 -200
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -111
  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 -254
  255. package/src/team_agent/messaging/delivery.py +0 -473
  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 -457
  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 -86
  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 -1239
  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 -143
  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 -602
  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,244 @@
1
+ #[test]
2
+ fn result_status_alias_chain() {
3
+ // None / unknown → Success
4
+ assert_eq!(normalize_result_status(None), ResultStatus::Success);
5
+ assert_eq!(normalize_result_status(Some("weird")), ResultStatus::Success);
6
+ // ok/done/complete/completed/passed/pass → success
7
+ for a in ["ok", "DONE", "complete", "completed", "passed", "pass"] {
8
+ assert_eq!(normalize_result_status(Some(a)), ResultStatus::Success, "alias {a}");
9
+ }
10
+ assert_eq!(normalize_result_status(Some("blocked")), ResultStatus::Blocked);
11
+ assert_eq!(normalize_result_status(Some("block")), ResultStatus::Blocked);
12
+ assert_eq!(normalize_result_status(Some("failed")), ResultStatus::Failed);
13
+ assert_eq!(normalize_result_status(Some("fail")), ResultStatus::Failed);
14
+ assert_eq!(normalize_result_status(Some("error")), ResultStatus::Failed);
15
+ assert_eq!(normalize_result_status(Some("partial")), ResultStatus::Partial);
16
+ assert_eq!(normalize_result_status(Some("partially_done")), ResultStatus::Partial);
17
+ // case/space/hyphen fold: " Partially-Done " → partial
18
+ assert_eq!(normalize_result_status(Some(" Partially-Done ")), ResultStatus::Partial);
19
+ }
20
+
21
+ // ════════════════════════════════════════════════════════════════════════
22
+ // normalize_change_kind — alias map + description keyword fallback (normalize.py:145)
23
+ // ════════════════════════════════════════════════════════════════════════
24
+ #[test]
25
+ fn change_kind_alias_and_keyword_inference() {
26
+ // None + empty desc → Modified (final fallback)
27
+ assert_eq!(normalize_change_kind(None, ""), ChangeKind::Modified);
28
+ // exact canonical passes through
29
+ assert_eq!(normalize_change_kind(Some("created"), "whatever"), ChangeKind::Created);
30
+ // alias map
31
+ assert_eq!(normalize_change_kind(Some("add"), ""), ChangeKind::Created);
32
+ assert_eq!(normalize_change_kind(Some("updated"), ""), ChangeKind::Modified);
33
+ // keyword inference from description when value absent
34
+ assert_eq!(normalize_change_kind(None, "created a new file"), ChangeKind::Created);
35
+ assert_eq!(normalize_change_kind(Some(""), "removed the thing"), ChangeKind::Deleted);
36
+ assert_eq!(normalize_change_kind(None, "verified output"), ChangeKind::Observed);
37
+ // unknown value + plain desc → Modified
38
+ assert_eq!(normalize_change_kind(Some("zzz"), "plain text"), ChangeKind::Modified);
39
+ }
40
+
41
+ // ════════════════════════════════════════════════════════════════════════
42
+ // normalize_test_status — alias + unknown→not_run (normalize.py:199)
43
+ // ════════════════════════════════════════════════════════════════════════
44
+ #[test]
45
+ fn test_status_alias_chain() {
46
+ assert_eq!(normalize_test_status(None), TestStatus::NotRun);
47
+ assert_eq!(normalize_test_status(Some("weird")), TestStatus::NotRun);
48
+ for a in ["pass", "OK", "success"] {
49
+ assert_eq!(normalize_test_status(Some(a)), TestStatus::Passed, "alias {a}");
50
+ }
51
+ assert_eq!(normalize_test_status(Some("fail")), TestStatus::Failed);
52
+ assert_eq!(normalize_test_status(Some("error")), TestStatus::Failed);
53
+ assert_eq!(normalize_test_status(Some("notrun")), TestStatus::NotRun);
54
+ assert_eq!(normalize_test_status(Some("skip")), TestStatus::Skipped);
55
+ assert_eq!(normalize_test_status(Some("passed")), TestStatus::Passed);
56
+ }
57
+
58
+ // ════════════════════════════════════════════════════════════════════════
59
+ // normalize_risk_severity — out-of-set → Low (normalize.py:226)
60
+ // ════════════════════════════════════════════════════════════════════════
61
+ #[test]
62
+ fn risk_severity_out_of_set_is_low() {
63
+ assert_eq!(normalize_risk_severity(None), RiskSeverity::Low);
64
+ assert_eq!(normalize_risk_severity(Some("CRITICAL")), RiskSeverity::Low);
65
+ assert_eq!(normalize_risk_severity(Some("low")), RiskSeverity::Low);
66
+ assert_eq!(normalize_risk_severity(Some("medium")), RiskSeverity::Medium);
67
+ assert_eq!(normalize_risk_severity(Some("high")), RiskSeverity::High);
68
+ }
69
+
70
+ // ════════════════════════════════════════════════════════════════════════
71
+ // normalize_report_envelope — fixed schema_version + manual/unknown fallbacks
72
+ // + child-list regularization (normalize.py:67)
73
+ // ════════════════════════════════════════════════════════════════════════
74
+ #[test]
75
+ fn report_envelope_empty_uses_fixed_fallbacks() {
76
+ let env = normalize_report_envelope(&json!({}));
77
+ assert_eq!(env.schema_version, "result_envelope_v1");
78
+ assert_eq!(env.task_id, TaskId::new("manual")); // bug-085: NOT None
79
+ assert_eq!(env.agent_id, AgentId::new("unknown"));
80
+ assert_eq!(env.status, ResultStatus::Success);
81
+ assert_eq!(env.summary, "completed"); // empty summary → "completed"
82
+ assert!(env.changes.is_empty());
83
+ assert!(env.tests.is_empty());
84
+ assert!(env.risks.is_empty());
85
+ assert!(env.artifacts.is_empty());
86
+ assert!(env.next_actions.is_empty());
87
+ }
88
+
89
+ #[test]
90
+ fn report_envelope_regularizes_children_and_blank_ids() {
91
+ // blank summary/task_id/agent_id collapse to fallbacks; status alias "done"→success;
92
+ // change uses file/action/summary aliases + keyword; bare test str → not_run.
93
+ let env = normalize_report_envelope(&json!({
94
+ "summary": " ",
95
+ "status": "done",
96
+ "task_id": " ",
97
+ "agent_id": "",
98
+ "changes": [{"file": "a.rs", "action": "add", "summary": "added a.rs"}],
99
+ "tests": ["cargo test"],
100
+ }));
101
+ assert_eq!(env.summary, "completed");
102
+ assert_eq!(env.task_id, TaskId::new("manual"));
103
+ assert_eq!(env.agent_id, AgentId::new("unknown"));
104
+ assert_eq!(env.status, ResultStatus::Success);
105
+ assert_eq!(env.changes.len(), 1);
106
+ assert_eq!(env.changes[0], NormalizedChange {
107
+ path: "a.rs".to_string(),
108
+ kind: ChangeKind::Created, // "add" alias
109
+ description: "added a.rs".to_string(),
110
+ });
111
+ assert_eq!(env.tests.len(), 1);
112
+ assert_eq!(env.tests[0], NormalizedTest {
113
+ command: "cargo test".to_string(),
114
+ status: TestStatus::NotRun, // bare string default
115
+ detail: None,
116
+ });
117
+ }
118
+
119
+ // ════════════════════════════════════════════════════════════════════════
120
+ // compact_tool_result — ok whitelist + INSERTION ORDER (preserve_order on)
121
+ // Golden: input {delivered_count,ok,message_id,status,to} (scrambled) emits
122
+ // keys in WHITELIST order: ok,status,message_id,to,delivered_count.
123
+ // ════════════════════════════════════════════════════════════════════════
124
+ #[test]
125
+ fn compact_ok_whitelist_drops_unknown_and_orders_by_whitelist() {
126
+ let r = json!({
127
+ "delivered_count": 5, "ok": true, "message_id": "mZ",
128
+ "status": "queued", "to": "x", "secret": "drop_me"
129
+ });
130
+ let ok = compact_tool_result(&r).expect("ok compaction");
131
+ let v = serde_json::to_value(&ok).unwrap();
132
+ // unknown key dropped
133
+ assert!(v.get("secret").is_none());
134
+ // WHITELIST order, not input order
135
+ assert_eq!(keys(&v), vec!["ok", "status", "message_id", "to", "delivered_count"]);
136
+ assert_eq!(s(&v), r#"{"ok":true,"status":"queued","message_id":"mZ","to":"x","delivered_count":5}"#);
137
+ }
138
+
139
+ #[test]
140
+ fn compact_ok_empty_yields_ok_true() {
141
+ // empty compaction → {"ok": true} (normalize.py:64)
142
+ let ok = compact_tool_result(&json!({})).expect("empty→ok:true");
143
+ assert_eq!(s(&serde_json::to_value(&ok).unwrap()), r#"{"ok":true}"#);
144
+ }
145
+
146
+ #[test]
147
+ fn compact_error_whitelist_and_order() {
148
+ // Python `_compact_tool_result` (normalize.py:6-31) does NOT synthesize a
149
+ // _tool_error_result for an ok:false delegate dict — it passes the delegate's
150
+ // OWN string reason/error/status THROUGH the error whitelist, in WHITELIST
151
+ // ORDER, and returns a plain dict that still carries ok:false. So the Rust
152
+ // seam is `Ok(ToolOk)` with ok:false inside (NOT Err(ToolError): the closed
153
+ // ToolErrorReason enum has no "r" variant and cannot byte-faithfully carry an
154
+ // arbitrary delegate reason). The isError flag is derived later from the
155
+ // body's ok:false in handle_mcp, not from this compactor.
156
+ //
157
+ // Golden (re-probed v0.2.11):
158
+ // _compact_tool_result({"error":"e","ok":false,"message_id":"m",
159
+ // "reason":"r","status":"failed"})
160
+ // == {"ok":false,"status":"failed","reason":"r","error":"e","message_id":"m"}
161
+ // keys (whitelist order): ok,status,reason,error,message_id
162
+ let r = json!({"error":"e","ok":false,"message_id":"m","reason":"r","status":"failed"});
163
+ let ok = compact_tool_result(&r)
164
+ .expect("error-path compaction is Ok(ToolOk{ok:false,...}), not Err — mirrors Python");
165
+ let v = serde_json::to_value(&ok).unwrap();
166
+ // ok:false is preserved INSIDE the body (the error whitelist begins with "ok").
167
+ assert_eq!(v.get("ok"), Some(&json!(false)));
168
+ assert_eq!(v.get("status"), Some(&json!("failed")));
169
+ assert_eq!(v.get("reason"), Some(&json!("r"))); // delegate string passed through
170
+ assert_eq!(v.get("error"), Some(&json!("e"))); // delegate string passed through
171
+ assert_eq!(v.get("message_id"), Some(&json!("m")));
172
+ // WHITELIST insertion order (preserve_order), not input order.
173
+ assert_eq!(keys(&v), vec!["ok", "status", "reason", "error", "message_id"]);
174
+ // full byte-stable string — the contract this module exists to lock.
175
+ assert_eq!(s(&v),
176
+ r#"{"ok":false,"status":"failed","reason":"r","error":"e","message_id":"m"}"#);
177
+ }
178
+
179
+ #[test]
180
+ fn compact_fanout_preserves_deliveries_and_recipients() {
181
+ // fanout_* status preserves deliveries/recipients APPENDED after whitelist keys.
182
+ let r = json!({
183
+ "ok": true, "status": "fanout_partial",
184
+ "deliveries": [1, 2], "recipients": ["a", "b"], "delivered_count": 1
185
+ });
186
+ let ok = compact_tool_result(&r).expect("fanout ok");
187
+ let v = serde_json::to_value(&ok).unwrap();
188
+ assert_eq!(keys(&v), vec!["ok", "status", "delivered_count", "deliveries", "recipients"]);
189
+ assert_eq!(s(&v),
190
+ r#"{"ok":true,"status":"fanout_partial","delivered_count":1,"deliveries":[1,2],"recipients":["a","b"]}"#);
191
+ }
192
+
193
+ #[test]
194
+ fn compact_acknowledged_messages_becomes_count() {
195
+ // acknowledged_messages list → acknowledged_count = len
196
+ let ok = compact_tool_result(&json!({
197
+ "ok": true, "status": "collected", "acknowledged_messages": ["a", "b", "c"]
198
+ })).expect("ack ok");
199
+ let v = serde_json::to_value(&ok).unwrap();
200
+ assert_eq!(v.get("acknowledged_count"), Some(&json!(3)));
201
+ assert!(v.get("acknowledged_messages").is_none());
202
+ // None/empty → 0
203
+ let ok0 = compact_tool_result(&json!({"ok": true, "acknowledged_messages": Value::Null}))
204
+ .expect("ack none");
205
+ let v0 = serde_json::to_value(&ok0).unwrap();
206
+ assert_eq!(v0.get("acknowledged_count"), Some(&json!(0)));
207
+ }
208
+
209
+ // ════════════════════════════════════════════════════════════════════════
210
+ // ToolError envelope — REDUNDANT keys are load-bearing (server.py:98-106)
211
+ // reason==error_code AND message==error, byte-for-byte.
212
+ // ════════════════════════════════════════════════════════════════════════
213
+ #[test]
214
+ fn tool_error_envelope_redundant_keys_byte_stable() {
215
+ let te = ToolError::new(ToolErrorReason::UnknownTool, "unknown tool 'foo'", "UnknownTool");
216
+ let env = te.to_envelope();
217
+ // exact golden from server._tool_error_result
218
+ assert_eq!(env.get("ok"), Some(&json!(false)));
219
+ assert_eq!(env.get("reason"), Some(&json!("unknown_tool")));
220
+ assert_eq!(env.get("error_code"), Some(&json!("unknown_tool"))); // == reason
221
+ assert_eq!(env.get("exc_type"), Some(&json!("UnknownTool")));
222
+ assert_eq!(env.get("message"), Some(&json!("unknown tool 'foo'")));
223
+ assert_eq!(env.get("error"), Some(&json!("unknown tool 'foo'"))); // == message
224
+ // full byte-stable order: ok,reason,error_code,exc_type,message,error
225
+ assert_eq!(keys(&env), vec!["ok", "reason", "error_code", "exc_type", "message", "error"]);
226
+ }
227
+
228
+ #[test]
229
+ fn public_exception_message_scrub() {
230
+ // newline→space, trim
231
+ assert_eq!(
232
+ ToolError::public_exception_message(" line1\nline2 ", "ValueError"),
233
+ "line1 line2"
234
+ );
235
+ // empty → exc_type name
236
+ assert_eq!(ToolError::public_exception_message("", "ValueError"), "ValueError");
237
+ // truncate to 200 chars
238
+ let long = "x".repeat(250);
239
+ assert_eq!(ToolError::public_exception_message(&long, "ValueError").len(), 200);
240
+ }
241
+
242
+ // ════════════════════════════════════════════════════════════════════════
243
+ // McpTool / RpcMethod wire mapping
244
+ // ════════════════════════════════════════════════════════════════════════
@@ -0,0 +1,189 @@
1
+ #[test]
2
+ fn dispatch_send_message_worker_accepted_returned_verbatim() {
3
+ let tools = TeamOrchestratorTools::with_identity(
4
+ &unique_ws("dispatch-accepted"),
5
+ Some(AgentId::new("leader")), // legacy single-team bypasses cross-team refusal
6
+ None,
7
+ );
8
+ let ok = dispatch_tool(&tools, McpTool::SendMessage, &json!({
9
+ "to": "worker-1", "content": "do it"
10
+ })).expect("send ok");
11
+ let v = serde_json::to_value(&ok).unwrap();
12
+ assert_eq!(keys(&v), vec!["status", "delivery_pending", "poll_via", "message_id"],
13
+ "worker-accepted dict returned verbatim (NOT re-compacted)");
14
+ assert_eq!(v.get("status"), Some(&json!("accepted")));
15
+ assert_eq!(v.get("delivery_pending"), Some(&json!(true)));
16
+ let mid = v.get("message_id").and_then(Value::as_str).unwrap();
17
+ assert_eq!(v.get("poll_via"), Some(&json!(format!("team-agent inbox {mid}"))));
18
+ }
19
+
20
+ // ── #32/#47 request_human key order ok,message_id,status (no compaction) ────
21
+ // GOLDEN (probe_events_red.py REQUEST_HUMAN-KEYS): ['ok','message_id','status'].
22
+ // Rust compact_tool_result reorders to ok,status,message_id.
23
+ #[test]
24
+ fn request_human_key_order_is_ok_message_id_status() {
25
+ let tools = TeamOrchestratorTools::with_identity(
26
+ &unique_ws("reqhuman-order"),
27
+ Some(AgentId::new("worker-3")),
28
+ None,
29
+ );
30
+ let ok = tools.request_human("need approval", Some("task-1"), None).expect("request_human ok");
31
+ let v = serde_json::to_value(&ok).unwrap();
32
+ assert_eq!(keys(&v), vec!["ok", "message_id", "status"]);
33
+ assert_eq!(v.get("status"), Some(&json!("needs_human")));
34
+ }
35
+
36
+ // ── #32 update_state returns RAW {ok, state_file} (NO compaction) ───────────
37
+ // GOLDEN (tools.py:316-325 + probe_passthrough): update_state is NOT compacted;
38
+ // state_file survives. Rust runs compact_tool_result whose ok-whitelist DROPS
39
+ // state_file (not a golden whitelist key), so the key vanishes.
40
+ #[test]
41
+ fn update_state_state_file_survives_no_compaction() {
42
+ let tools = TeamOrchestratorTools::with_identity(
43
+ &unique_ws("update-state-raw"),
44
+ Some(AgentId::new("leader")),
45
+ None,
46
+ );
47
+ let ok = tools.update_state("note").expect("update_state ok");
48
+ let v = serde_json::to_value(&ok).unwrap();
49
+ assert!(v.get("state_file").and_then(Value::as_str).is_some(),
50
+ "state_file must survive (update_state is not _compact_tool_result'd)");
51
+ assert_eq!(keys(&v), vec!["ok", "state_file"]);
52
+ }
53
+
54
+ // ── #36 report_result setdefault: populated envelope keys WIN over args ─────
55
+ // GOLDEN (probe_setdefault.py): envelope {agent_id:env-agent, task_id:env-task,...}
56
+ // + explicit args agent_id=ARG-agent, task_id=ARG-task → returned dict keeps
57
+ // env-agent / env-task (setdefault). Rust unconditionally insert-overrides.
58
+ #[test]
59
+ fn report_result_setdefault_envelope_wins_over_args() {
60
+ let tools = TeamOrchestratorTools::with_identity(
61
+ &unique_ws("report-setdefault"),
62
+ Some(AgentId::new("env-id")),
63
+ None,
64
+ );
65
+ let ok = tools.report_result(
66
+ Some(&json!({
67
+ "agent_id": "env-agent", "task_id": "env-task",
68
+ "status": "blocked", "summary": "env summary"
69
+ })),
70
+ Some("ARG summary"), ResultStatus::Success,
71
+ None, None, None, None, None,
72
+ Some("ARG-task"), Some("ARG-agent"),
73
+ ).expect("report ok");
74
+ let v = serde_json::to_value(&ok).unwrap();
75
+ // setdefault: the pre-populated envelope values win.
76
+ assert_eq!(v.get("agent_id"), Some(&json!("env-agent")), "envelope agent_id wins (setdefault)");
77
+ assert_eq!(v.get("task_id"), Some(&json!("env-task")), "envelope task_id wins (setdefault)");
78
+ }
79
+
80
+ // ── #44 report_result task_id inference from state (_latest_task_for_assignee)
81
+ // GOLDEN (probe_report evidence): env agent worker-7, state tasks=[{id:t-42,
82
+ // assignee:worker-7,status:pending}], report with NO task_id → task_id "t-42".
83
+ // Rust has no _latest_task_for_assignee; hard-codes "manual".
84
+ #[test]
85
+ fn report_result_infers_task_id_from_latest_assigned_task() {
86
+ let cws = seed_state_ws("report-infer-task", &json!({
87
+ "agents": {}, "active_team_key": null,
88
+ "tasks": [{"id": "t-42", "assignee": "worker-7", "status": "pending"}]
89
+ }));
90
+ let tools = TeamOrchestratorTools::with_identity(&cws, Some(AgentId::new("worker-7")), None);
91
+ let ok = tools.report_result(
92
+ None, Some("done it"), ResultStatus::Success,
93
+ None, None, None, None, None,
94
+ None, None,
95
+ ).expect("report ok");
96
+ let v = serde_json::to_value(&ok).unwrap();
97
+ assert_eq!(v.get("task_id"), Some(&json!("t-42")),
98
+ "task_id inferred from latest non-terminal assigned task, not 'manual'");
99
+ }
100
+
101
+ // ── #42 get_visible_peers reads seeded state (sorted live peers) ────────────
102
+ // GOLDEN (probe_peers.py): teamA agents {worker-z(alive),worker-a(working),
103
+ // worker-dead(DEAD),worker-stopped(Stopped),worker-no-status(dict no status),
104
+ // worker-weird(non-dict)} → peers ["worker-a","worker-no-status","worker-weird",
105
+ // "worker-z"] (sorted, dead/stopped filtered, non-dict & no-status INCLUDED);
106
+ // sender_team_id "teamA", scope team. Rust stub returns empty peers.
107
+ #[test]
108
+ fn get_visible_peers_reads_state_sorted_live_filtered() {
109
+ let cws = seed_state_ws("visible-peers", &json!({
110
+ "agents": {}, "active_team_key": null,
111
+ "teams": {
112
+ "teamA": {"status": "alive", "agents": {
113
+ "worker-z": {"status": "alive"},
114
+ "worker-a": {"status": "working"},
115
+ "worker-dead": {"status": "DEAD"},
116
+ "worker-stopped": {"status": "Stopped"},
117
+ "worker-no-status": {},
118
+ "worker-weird": "not-a-dict"
119
+ }},
120
+ "teamB": {"status": "alive", "agents": {"other-bob": {"status": "alive"}}}
121
+ }
122
+ }));
123
+ let tools = TeamOrchestratorTools::with_identity(
124
+ &cws, Some(AgentId::new("worker-1")), Some(TeamKey::new("teamA")),
125
+ );
126
+ let vp = tools.get_visible_peers().expect("visible peers");
127
+ let got: Vec<&str> = vp.peers.iter().map(AgentId::as_str).collect();
128
+ assert_eq!(got, vec!["worker-a", "worker-no-status", "worker-weird", "worker-z"]);
129
+ assert_eq!(vp.sender_team_id, Some(TeamKey::new("teamA")));
130
+ assert_eq!(vp.scope, Scope::Team);
131
+ }
132
+
133
+ // ── #42 refuse_cross_team_peer ALLOWS a live in-team peer (visible bypass) ──
134
+ // GOLDEN (probe_peers.py): with the same seeded state, refusing worker-a / worker-z
135
+ // / worker-no-status / worker-weird → None (ALLOWED, they are visible peers), while
136
+ // worker-dead / worker-stopped / other-bob → refused. Rust stub refuses ALL of them.
137
+ #[test]
138
+ fn refuse_cross_team_peer_allows_live_in_team_peer() {
139
+ let cws = seed_state_ws("refuse-inteam", &json!({
140
+ "agents": {}, "active_team_key": null,
141
+ "teams": {
142
+ "teamA": {"status": "alive", "agents": {
143
+ "worker-z": {"status": "alive"},
144
+ "worker-a": {"status": "working"},
145
+ "worker-dead": {"status": "DEAD"},
146
+ "worker-no-status": {},
147
+ "worker-weird": "not-a-dict"
148
+ }},
149
+ "teamB": {"status": "alive", "agents": {"other-bob": {"status": "alive"}}}
150
+ }
151
+ }));
152
+ let tools = TeamOrchestratorTools::with_identity(
153
+ &cws, Some(AgentId::new("worker-1")), Some(TeamKey::new("teamA")),
154
+ );
155
+ // live / no-status / non-dict in-team peers are ALLOWED (None).
156
+ assert!(tools.refuse_cross_team_peer(&MessageTarget::Single("worker-a".to_string()), None).is_none(),
157
+ "a live in-team peer must be allowed (visible-peer bypass)");
158
+ assert!(tools.refuse_cross_team_peer(&MessageTarget::Single("worker-z".to_string()), None).is_none());
159
+ assert!(tools.refuse_cross_team_peer(&MessageTarget::Single("worker-no-status".to_string()), None).is_none());
160
+ assert!(tools.refuse_cross_team_peer(&MessageTarget::Single("worker-weird".to_string()), None).is_none());
161
+ // dead / other-team peers are still refused.
162
+ assert!(tools.refuse_cross_team_peer(&MessageTarget::Single("worker-dead".to_string()), None).is_some());
163
+ assert!(tools.refuse_cross_team_peer(&MessageTarget::Single("other-bob".to_string()), None).is_some());
164
+ }
165
+
166
+ // ── #48 refuse_cross_team_peer writes mcp.send_message_refused EventLog ─────
167
+ // GOLDEN (probe_events_red.py): a refusal appends an event with fields
168
+ // event=mcp.send_message_refused, reason=peer_not_in_scope, scope=team,
169
+ // sender_team_id=teamA, hint=<the cross-team hint>. Rust handler writes nothing.
170
+ #[test]
171
+ fn refuse_cross_team_peer_writes_send_message_refused_event() {
172
+ let cws = seed_state_ws("refuse-event", &json!({
173
+ "agents": {}, "active_team_key": null,
174
+ "teams": {"teamA": {"status": "alive", "agents": {"worker-1": {"status": "alive"}}}}
175
+ }));
176
+ let tools = TeamOrchestratorTools::with_identity(
177
+ &cws, Some(AgentId::new("worker-1")), Some(TeamKey::new("teamA")),
178
+ );
179
+ // out-of-scope peer → refusal must emit the audit event.
180
+ let _ = tools.refuse_cross_team_peer(&MessageTarget::Single("other-bob".to_string()), None);
181
+ let events = EventLog::new(&cws).tail(50).expect("read events");
182
+ let refused = events.iter().find(|e| e["event"] == json!("mcp.send_message_refused"))
183
+ .expect("mcp.send_message_refused must be written on refusal");
184
+ assert_eq!(refused["reason"], json!("peer_not_in_scope"));
185
+ assert_eq!(refused["scope"], json!("team"));
186
+ assert_eq!(refused["sender_team_id"], json!("teamA"));
187
+ assert_eq!(refused["hint"],
188
+ json!("the requested peer is not part of your team. pass scope='workspace' to address peers in other teams."));
189
+ }