@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.
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 +1204 -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 +1207 -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 +557 -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 +1084 -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 +526 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1101 -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 +237 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +272 -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 +410 -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 +489 -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 +2109 -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 +985 -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 +710 -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 +187 -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 +468 -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 +743 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +329 -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 +553 -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 +578 -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 +659 -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 +765 -0
  172. package/crates/team-agent/src/tmux_backend.rs +810 -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 +118 -112
  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,312 @@
1
+ //! step 14a · mcp_server::normalize — string-alias regularization + result compaction.
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::model::enums::{ChangeKind, ResultStatus, RiskSeverity, TestStatus};
6
+ use crate::model::ids::{AgentId, TaskId};
7
+
8
+ use super::helpers::{items_from_value, normalize_token, text_field, text_of_value};
9
+ use super::types::{
10
+ NormalizedArtifact, NormalizedChange, NormalizedNextAction, NormalizedReportEnvelope,
11
+ NormalizedRisk, NormalizedTest, ToolOk, ToolResult,
12
+ };
13
+
14
+ // ═══════════════════════════════════════════════════════════════════════════
15
+ // NORMALIZE — string-alias regularization onto the step-2 value enums.
16
+ // These are contract-callable: RED tests pass alias strings and assert the enum.
17
+ // ═══════════════════════════════════════════════════════════════════════════
18
+
19
+ /// `_normalize_result_status` (`normalize.py:106-123`): map ok/done/passed/… aliases
20
+ /// onto [`ResultStatus`]; anything unrecognized → `Success`.
21
+ pub fn normalize_result_status(value: Option<&str>) -> ResultStatus {
22
+ match normalize_token(value).as_str() {
23
+ "blocked" | "block" => ResultStatus::Blocked,
24
+ "failed" | "fail" | "error" => ResultStatus::Failed,
25
+ "partial" | "partially_done" => ResultStatus::Partial,
26
+ _ => ResultStatus::Success,
27
+ }
28
+ }
29
+
30
+ /// `_normalize_change_kind` (`normalize.py:145-177`): alias map + description-keyword
31
+ /// inference fallback → [`ChangeKind`]; no match → `Modified`.
32
+ pub fn normalize_change_kind(value: Option<&str>, description: &str) -> ChangeKind {
33
+ match normalize_token(value).as_str() {
34
+ "created" | "create" | "add" | "added" | "new" => ChangeKind::Created,
35
+ "deleted" | "delete" | "remove" | "removed" => ChangeKind::Deleted,
36
+ "observed" | "observe" | "inspected" | "inspect" => ChangeKind::Observed,
37
+ "modified" | "modify" | "updated" | "update" | "changed" | "change" | "edited" | "edit" => {
38
+ ChangeKind::Modified
39
+ }
40
+ _ => {
41
+ let desc = description.to_ascii_lowercase();
42
+ if desc.contains("created") || desc.contains("added") || desc.contains("new file") {
43
+ ChangeKind::Created
44
+ } else if desc.contains("removed") || desc.contains("deleted") {
45
+ ChangeKind::Deleted
46
+ } else if desc.contains("verified") || desc.contains("observed") || desc.contains("inspected") {
47
+ ChangeKind::Observed
48
+ } else {
49
+ ChangeKind::Modified
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ /// `_normalize_test_status` (`normalize.py:199-212`): alias map → [`TestStatus`];
56
+ /// unknown → `NotRun`.
57
+ pub fn normalize_test_status(value: Option<&str>) -> TestStatus {
58
+ match normalize_token(value).as_str() {
59
+ "passed" | "pass" | "ok" | "success" => TestStatus::Passed,
60
+ "failed" | "fail" | "error" => TestStatus::Failed,
61
+ "skipped" | "skip" => TestStatus::Skipped,
62
+ _ => TestStatus::NotRun,
63
+ }
64
+ }
65
+
66
+ /// `severity` regularization (`normalize.py:226-228`): out-of-set → [`RiskSeverity::Low`].
67
+ pub fn normalize_risk_severity(value: Option<&str>) -> RiskSeverity {
68
+ match normalize_token(value).as_str() {
69
+ "medium" => RiskSeverity::Medium,
70
+ "high" => RiskSeverity::High,
71
+ _ => RiskSeverity::Low,
72
+ }
73
+ }
74
+
75
+ /// `_normalize_report_envelope` (`normalize.py:67-80`): the whole-envelope regularizer.
76
+ /// Contracts assert the returned [`NormalizedReportEnvelope`] (status enum, fixed
77
+ /// schema_version, `"manual"`/`"unknown"` fallbacks, normalized child lists).
78
+ pub fn normalize_report_envelope(env: &Value) -> NormalizedReportEnvelope {
79
+ let summary = text_field(env, "summary").unwrap_or_else(|| "completed".to_string());
80
+ let task_id = text_field(env, "task_id").unwrap_or_else(|| "manual".to_string());
81
+ let agent_id = text_field(env, "agent_id").unwrap_or_else(|| "unknown".to_string());
82
+ NormalizedReportEnvelope {
83
+ schema_version: "result_envelope_v1".to_string(),
84
+ task_id: TaskId::new(task_id),
85
+ agent_id: AgentId::new(agent_id),
86
+ status: normalize_result_status(env.get("status").and_then(Value::as_str)),
87
+ summary: summary.clone(),
88
+ changes: normalize_changes(env.get("changes"), &summary),
89
+ tests: normalize_tests(env.get("tests")),
90
+ risks: normalize_risks(env.get("risks")),
91
+ artifacts: normalize_artifacts(env.get("artifacts")),
92
+ next_actions: normalize_next_actions(env.get("next_actions")),
93
+ }
94
+ }
95
+
96
+ /// `_compact_tool_result` (`normalize.py:6-64`): whitelist-key compaction of a
97
+ /// delegate result. ok vs error use different key sets; `fanout_*` status preserves
98
+ /// `deliveries`/`recipients`; `acknowledged_messages` → `acknowledged_count` (len).
99
+ /// An empty ok-compaction yields `{"ok": true}`.
100
+ pub fn compact_tool_result(result: &Value) -> ToolResult {
101
+ let is_error = result.get("ok").and_then(Value::as_bool) == Some(false);
102
+ let mut fields = serde_json::Map::new();
103
+ let keys: &[&str] = if is_error {
104
+ &[
105
+ "ok",
106
+ "status",
107
+ "reason",
108
+ "error",
109
+ "message_id",
110
+ "agent_id",
111
+ "new_agent_id",
112
+ "source_agent_id",
113
+ "role_file_sha",
114
+ "session_id",
115
+ "to",
116
+ "targets",
117
+ "delivered_count",
118
+ "failed_count",
119
+ "fallback_path",
120
+ "suggestion",
121
+ ]
122
+ } else {
123
+ &[
124
+ "ok",
125
+ "status",
126
+ "message_id",
127
+ "to",
128
+ "targets",
129
+ "delivered_count",
130
+ "failed_count",
131
+ "submitted",
132
+ "visible",
133
+ "queued",
134
+ "durably_stored",
135
+ "result_id",
136
+ "task_id",
137
+ "agent_id",
138
+ "new_agent_id",
139
+ "source_agent_id",
140
+ "role_file_sha",
141
+ "session_id",
142
+ "leader_notified",
143
+ "notification_message_id",
144
+ "notification_status",
145
+ "notification_channel",
146
+ "notification_event_id",
147
+ ]
148
+ };
149
+ for key in keys {
150
+ if let Some(value) = result.get(*key) {
151
+ fields.insert((*key).to_string(), value.clone());
152
+ }
153
+ }
154
+ if result
155
+ .get("status")
156
+ .and_then(Value::as_str)
157
+ .is_some_and(|s| s.starts_with("fanout_"))
158
+ {
159
+ for key in ["deliveries", "recipients"] {
160
+ if let Some(value) = result.get(key) {
161
+ fields.insert(key.to_string(), value.clone());
162
+ }
163
+ }
164
+ }
165
+ if !is_error && result.get("acknowledged_messages").is_some() {
166
+ let value = result.get("acknowledged_messages").unwrap_or(&Value::Null);
167
+ let count = value.as_array().map_or(0, Vec::len);
168
+ fields.insert("acknowledged_count".to_string(), Value::from(count));
169
+ }
170
+ if fields.is_empty() {
171
+ fields.insert("ok".to_string(), Value::Bool(true));
172
+ }
173
+ Ok(ToolOk { fields })
174
+ }
175
+
176
+ pub(crate) fn normalize_changes(value: Option<&Value>, envelope_summary: &str) -> Vec<NormalizedChange> {
177
+ items_from_value(value)
178
+ .iter()
179
+ .filter_map(|item| {
180
+ let obj = item.as_object()?;
181
+ let path = obj
182
+ .get("path")
183
+ .or_else(|| obj.get("file"))
184
+ .or_else(|| obj.get("filepath"))
185
+ .or_else(|| obj.get("filename"))
186
+ .and_then(text_of_value)?;
187
+ let description = obj
188
+ .get("description")
189
+ .or_else(|| obj.get("summary"))
190
+ .or_else(|| obj.get("detail"))
191
+ .or_else(|| obj.get("details"))
192
+ .or_else(|| obj.get("message"))
193
+ .and_then(text_of_value)
194
+ .unwrap_or_else(|| envelope_summary.to_string());
195
+ let kind_value = obj
196
+ .get("kind")
197
+ .or_else(|| obj.get("type"))
198
+ .or_else(|| obj.get("action"))
199
+ .and_then(Value::as_str);
200
+ Some(NormalizedChange {
201
+ path,
202
+ kind: normalize_change_kind(kind_value, &description),
203
+ description,
204
+ })
205
+ })
206
+ .collect()
207
+ }
208
+
209
+ pub(crate) fn normalize_tests(value: Option<&Value>) -> Vec<NormalizedTest> {
210
+ items_from_value(value)
211
+ .iter()
212
+ .filter_map(|item| match item {
213
+ Value::Object(obj) => {
214
+ let command = obj
215
+ .get("command")
216
+ .or_else(|| obj.get("cmd"))
217
+ .or_else(|| obj.get("name"))
218
+ .or_else(|| obj.get("test"))
219
+ .and_then(text_of_value)?;
220
+ Some(NormalizedTest {
221
+ command,
222
+ status: normalize_test_status(obj.get("status").and_then(Value::as_str)),
223
+ detail: obj
224
+ .get("detail")
225
+ .or_else(|| obj.get("output"))
226
+ .or_else(|| obj.get("stdout"))
227
+ .or_else(|| obj.get("stderr"))
228
+ .or_else(|| obj.get("summary"))
229
+ .or_else(|| obj.get("message"))
230
+ .and_then(text_of_value),
231
+ })
232
+ }
233
+ scalar => Some(NormalizedTest {
234
+ command: text_of_value(scalar)?,
235
+ status: TestStatus::NotRun,
236
+ detail: None,
237
+ }),
238
+ })
239
+ .collect()
240
+ }
241
+
242
+ pub(crate) fn normalize_risks(value: Option<&Value>) -> Vec<NormalizedRisk> {
243
+ items_from_value(value)
244
+ .iter()
245
+ .filter_map(|item| match item {
246
+ Value::Object(obj) => Some(NormalizedRisk {
247
+ severity: normalize_risk_severity(
248
+ obj.get("severity").or_else(|| obj.get("level")).and_then(Value::as_str),
249
+ ),
250
+ description: obj
251
+ .get("description")
252
+ .or_else(|| obj.get("summary"))
253
+ .or_else(|| obj.get("detail"))
254
+ .or_else(|| obj.get("message"))
255
+ .and_then(text_of_value)?,
256
+ }),
257
+ scalar => Some(NormalizedRisk {
258
+ severity: RiskSeverity::Low,
259
+ description: text_of_value(scalar)?,
260
+ }),
261
+ })
262
+ .collect()
263
+ }
264
+
265
+ pub(crate) fn normalize_artifacts(value: Option<&Value>) -> Vec<NormalizedArtifact> {
266
+ items_from_value(value)
267
+ .iter()
268
+ .filter_map(|item| match item {
269
+ Value::Object(obj) => {
270
+ let path = obj
271
+ .get("path")
272
+ .or_else(|| obj.get("file"))
273
+ .or_else(|| obj.get("filepath"))
274
+ .or_else(|| obj.get("filename"))
275
+ .and_then(text_of_value)?;
276
+ let description = obj
277
+ .get("description")
278
+ .or_else(|| obj.get("summary"))
279
+ .or_else(|| obj.get("detail"))
280
+ .and_then(text_of_value)
281
+ .unwrap_or_else(|| path.clone());
282
+ Some(NormalizedArtifact { path, description })
283
+ }
284
+ scalar => {
285
+ let path = text_of_value(scalar)?;
286
+ Some(NormalizedArtifact {
287
+ path: path.clone(),
288
+ description: path,
289
+ })
290
+ }
291
+ })
292
+ .collect()
293
+ }
294
+
295
+ pub(crate) fn normalize_next_actions(value: Option<&Value>) -> Vec<NormalizedNextAction> {
296
+ items_from_value(value)
297
+ .iter()
298
+ .filter_map(|item| match item {
299
+ Value::Object(obj) => obj
300
+ .get("description")
301
+ .or_else(|| obj.get("summary"))
302
+ .or_else(|| obj.get("action"))
303
+ .or_else(|| obj.get("todo"))
304
+ .or_else(|| obj.get("message"))
305
+ .and_then(text_of_value)
306
+ .map(|description| NormalizedNextAction {
307
+ description,
308
+ }),
309
+ scalar => text_of_value(scalar).map(|description| NormalizedNextAction { description }),
310
+ })
311
+ .collect()
312
+ }
@@ -0,0 +1,283 @@
1
+ #[test]
2
+ fn compact_ok_whitelist_is_exact_23_key_golden_set_and_order() {
3
+ let r = json!({
4
+ "ok": true, "status": "accepted", "message_id": "m1", "to": "alice",
5
+ "targets": ["a", "b"], "delivered_count": 2, "failed_count": 1, "submitted": true,
6
+ "visible": true, "queued": 3, "durably_stored": true, "result_id": "r1",
7
+ "task_id": "t1", "agent_id": "ag1", "new_agent_id": "ag2", "source_agent_id": "ag3",
8
+ "role_file_sha": "sha", "session_id": "s1", "leader_notified": true,
9
+ "notification_message_id": "nm", "notification_status": "sent",
10
+ "notification_channel": "ch", "notification_event_id": "ev",
11
+ // non-whitelist (golden DROPS these — Rust currently INVENTS the first three):
12
+ "delivery_pending": true, "poll_via": "x", "state_file": "/p", "secret": "z"
13
+ });
14
+ let v = serde_json::to_value(compact_tool_result(&r).expect("ok compaction")).unwrap();
15
+ assert_eq!(keys(&v), vec![
16
+ "ok", "status", "message_id", "to", "targets", "delivered_count", "failed_count",
17
+ "submitted", "visible", "queued", "durably_stored", "result_id", "task_id",
18
+ "agent_id", "new_agent_id", "source_agent_id", "role_file_sha", "session_id",
19
+ "leader_notified", "notification_message_id", "notification_status",
20
+ "notification_channel", "notification_event_id",
21
+ ]);
22
+ // The invented Rust keys must NOT appear (golden has no such whitelist entries).
23
+ assert!(v.get("delivery_pending").is_none(), "delivery_pending not in golden ok-whitelist");
24
+ assert!(v.get("poll_via").is_none(), "poll_via not in golden ok-whitelist");
25
+ assert!(v.get("state_file").is_none(), "state_file not in golden ok-whitelist");
26
+ // golden drops everything but ok/status when only the invented keys are present:
27
+ let only_extra = json!({"ok": true, "status": "accepted",
28
+ "delivery_pending": true, "poll_via": "x", "state_file": "/p"});
29
+ let v2 = serde_json::to_value(compact_tool_result(&only_extra).unwrap()).unwrap();
30
+ assert_eq!(s(&v2), r#"{"ok":true,"status":"accepted"}"#);
31
+ }
32
+
33
+ // ── #30/#43/#50 compact error-whitelist: EXACT 16-key golden list + order ───
34
+ // GOLDEN (probe_mcp_red.py ERR-FULL-KEYS): normalize.py:8-25 order, all 16 kept.
35
+ #[test]
36
+ fn compact_error_whitelist_is_exact_16_key_golden_set_and_order() {
37
+ let r = json!({
38
+ "ok": false, "status": "failed", "reason": "boom", "error": "boom detail",
39
+ "message_id": "m1", "agent_id": "ag1", "new_agent_id": "ag2", "source_agent_id": "ag3",
40
+ "role_file_sha": "sha", "session_id": "s1", "to": "alice", "targets": ["a"],
41
+ "delivered_count": 0, "failed_count": 2, "fallback_path": "/fb", "suggestion": "try x",
42
+ // not in error-whitelist → dropped:
43
+ "result_id": "r1", "extra": "drop"
44
+ });
45
+ let v = serde_json::to_value(compact_tool_result(&r).expect("error compaction")).unwrap();
46
+ assert_eq!(keys(&v), vec![
47
+ "ok", "status", "reason", "error", "message_id", "agent_id", "new_agent_id",
48
+ "source_agent_id", "role_file_sha", "session_id", "to", "targets",
49
+ "delivered_count", "failed_count", "fallback_path", "suggestion",
50
+ ]);
51
+ assert!(v.get("result_id").is_none(), "result_id not in golden error-whitelist");
52
+ }
53
+
54
+ // ── #52 acknowledged_count is OK-PATH ONLY (normalize.py:62 inside else) ────
55
+ // GOLDEN (probe_mcp_red.py ACK-ON-ERR): an ok:false result with acknowledged_messages
56
+ // → {"ok":false,"status":"x"} (NO acknowledged_count). Rust adds it unconditionally.
57
+ #[test]
58
+ fn compact_acknowledged_count_not_added_on_error_path() {
59
+ let r = json!({"ok": false, "status": "x", "acknowledged_messages": ["a", "b"]});
60
+ let v = serde_json::to_value(compact_tool_result(&r).expect("error compaction")).unwrap();
61
+ assert!(v.get("acknowledged_count").is_none(),
62
+ "acknowledged_count is added only on the ok branch (normalize.py:62)");
63
+ assert_eq!(s(&v), r#"{"ok":false,"status":"x"}"#);
64
+ }
65
+
66
+ // ── #34/#45/#58 normalize_change_kind: golden alias map + keyword set ───────
67
+ // GOLDEN (probe_mcp_red.py CK ...): verified/verify→modified (NOT observed!);
68
+ // inspected/inspect/observe→observed; edited/edit→modified; desc 'inspected'→observed.
69
+ #[test]
70
+ fn change_kind_golden_alias_and_keyword_set() {
71
+ // verified/verify are NOT in the golden alias map → fall to keyword inference.
72
+ // With empty desc, keyword scan finds no match → Modified (golden), NOT Observed.
73
+ assert_eq!(normalize_change_kind(Some("verified"), ""), ChangeKind::Modified);
74
+ assert_eq!(normalize_change_kind(Some("verify"), ""), ChangeKind::Modified);
75
+ // inspected/inspect ARE in the golden alias map → Observed.
76
+ assert_eq!(normalize_change_kind(Some("inspected"), ""), ChangeKind::Observed);
77
+ assert_eq!(normalize_change_kind(Some("inspect"), ""), ChangeKind::Observed);
78
+ assert_eq!(normalize_change_kind(Some("observe"), ""), ChangeKind::Observed);
79
+ // edited/edit→Modified.
80
+ assert_eq!(normalize_change_kind(Some("edited"), ""), ChangeKind::Modified);
81
+ assert_eq!(normalize_change_kind(Some("edit"), ""), ChangeKind::Modified);
82
+ // description keyword scan for observed includes 'inspected'.
83
+ assert_eq!(normalize_change_kind(None, "inspected it"), ChangeKind::Observed);
84
+ }
85
+
86
+ // ── #40 normalize_result_status: 'partiallydone' (no underscore) → Success ──
87
+ // GOLDEN (probe_mcp_red.py STATUS partiallydone): not in mapping/canonical set → success.
88
+ #[test]
89
+ fn result_status_partiallydone_no_underscore_is_success() {
90
+ assert_eq!(normalize_result_status(Some("partiallydone")), ResultStatus::Success);
91
+ assert_eq!(normalize_result_status(Some("PartiallyDone")), ResultStatus::Success);
92
+ }
93
+
94
+ // ── #35/#46/#53 normalize_changes: full alias set + empty-path SKIP ─────────
95
+ // GOLDEN (probe_mcp_red.py CHG ...): path={path,file,filepath,filename};
96
+ // kind={kind,type,action}; desc={description,summary,detail,details,message};
97
+ // no-path item is SKIPPED (not a phantom path:"").
98
+ #[test]
99
+ fn normalize_changes_full_aliases_and_skip_empty_path() {
100
+ // filepath alias honored; kind 'type-ignored' is not a real alias → keyword
101
+ // inference on desc 'fb' → Modified.
102
+ let v = normalize_changes(Some(&json!([{"filepath": "x.rs", "kind": "type-ignored"}])), "fb");
103
+ assert_eq!(v, vec![NormalizedChange {
104
+ path: "x.rs".to_string(), kind: ChangeKind::Modified, description: "fb".to_string()
105
+ }]);
106
+ // 'type' alias → kind resolution.
107
+ let v2 = normalize_changes(Some(&json!([{"path": "p", "type": "create"}])), "fb");
108
+ assert_eq!(v2[0].kind, ChangeKind::Created);
109
+ // filename + detail aliases.
110
+ let v3 = normalize_changes(Some(&json!([{"filename": "c.rs", "detail": "deet"}])), "fb");
111
+ assert_eq!(v3, vec![NormalizedChange {
112
+ path: "c.rs".to_string(), kind: ChangeKind::Modified, description: "deet".to_string()
113
+ }]);
114
+ // no path at all → SKIPPED (golden returns []).
115
+ assert!(normalize_changes(Some(&json!([{"kind": "created", "description": "d"}])), "fb").is_empty());
116
+ }
117
+
118
+ // ── #35/#46/#54 normalize_tests: full aliases + scalar coercion + skip ──────
119
+ // GOLDEN (probe_mcp_red.py TST ...): command={command,cmd,name,test};
120
+ // detail={detail,output,stdout,stderr,summary,message} (NO 'details');
121
+ // non-dict scalar (incl int) → {command:str(v),status:not_run}; no-command dict SKIPPED;
122
+ // a bare non-list scalar value is wrapped via _items.
123
+ #[test]
124
+ fn normalize_tests_full_aliases_scalar_coerce_and_skip() {
125
+ // name alias + output alias.
126
+ let v = normalize_tests(Some(&json!([{"name": "t", "output": "O"}])));
127
+ assert_eq!(v, vec![NormalizedTest {
128
+ command: "t".to_string(), status: TestStatus::NotRun, detail: Some("O".to_string())
129
+ }]);
130
+ // int scalar coerced to "123".
131
+ let vi = normalize_tests(Some(&json!([123])));
132
+ assert_eq!(vi, vec![NormalizedTest {
133
+ command: "123".to_string(), status: TestStatus::NotRun, detail: None
134
+ }]);
135
+ // dict with no command key → SKIPPED.
136
+ assert!(normalize_tests(Some(&json!([{"status": "pass"}]))).is_empty());
137
+ // non-list scalar value wrapped (_items): "x" → one test.
138
+ let vw = normalize_tests(Some(&json!("x")));
139
+ assert_eq!(vw, vec![NormalizedTest {
140
+ command: "x".to_string(), status: TestStatus::NotRun, detail: None
141
+ }]);
142
+ }
143
+
144
+ // ── #35/#46/#55 normalize_risks: aliases + 'level' + scalar coerce + skip ───
145
+ // GOLDEN (probe_mcp_red.py RISK ...): desc={description,summary,detail,message};
146
+ // severity={severity,level}; int scalar→{severity:low,description:str(v)};
147
+ // no-description dict SKIPPED.
148
+ #[test]
149
+ fn normalize_risks_level_alias_scalar_coerce_and_skip() {
150
+ // 'detail' description alias + 'level' severity alias (HIGH → high).
151
+ let v = normalize_risks(Some(&json!([{"detail": "risky", "level": "HIGH"}])));
152
+ assert_eq!(v, vec![NormalizedRisk {
153
+ severity: RiskSeverity::High, description: "risky".to_string()
154
+ }]);
155
+ // int scalar coerced.
156
+ let vi = normalize_risks(Some(&json!([5])));
157
+ assert_eq!(vi, vec![NormalizedRisk {
158
+ severity: RiskSeverity::Low, description: "5".to_string()
159
+ }]);
160
+ // dict with no description → SKIPPED.
161
+ assert!(normalize_risks(Some(&json!([{"severity": "high"}]))).is_empty());
162
+ }
163
+
164
+ // ── #35/#46/#56 normalize_artifacts: aliases + scalar + skip ────────────────
165
+ // GOLDEN (probe_mcp_red.py ART ...): path={path,file,filepath,filename};
166
+ // desc={description,summary,detail} (default=path); no-path dict SKIPPED.
167
+ #[test]
168
+ fn normalize_artifacts_full_aliases_and_skip() {
169
+ // 'file' path alias + 'detail' description alias.
170
+ let v = normalize_artifacts(Some(&json!([{"file": "a.bin", "detail": "art"}])));
171
+ assert_eq!(v, vec![NormalizedArtifact {
172
+ path: "a.bin".to_string(), description: "art".to_string()
173
+ }]);
174
+ // dict with no path → SKIPPED.
175
+ assert!(normalize_artifacts(Some(&json!([{"description": "d"}]))).is_empty());
176
+ }
177
+
178
+ // ── #35/#46/#57 normalize_next_actions: action/todo/message aliases ─────────
179
+ // GOLDEN (probe_mcp_red.py NA ...): dict desc={description,summary,action,todo,message}.
180
+ #[test]
181
+ fn normalize_next_actions_action_todo_message_aliases() {
182
+ let v = normalize_next_actions(Some(&json!([
183
+ {"action": "a"}, {"todo": "t"}, {"message": "m"}
184
+ ])));
185
+ assert_eq!(v, vec![
186
+ NormalizedNextAction { description: "a".to_string() },
187
+ NormalizedNextAction { description: "t".to_string() },
188
+ NormalizedNextAction { description: "m".to_string() },
189
+ ]);
190
+ }
191
+
192
+ // ── #41 text_field: stringify non-string scalars (Python str()) ─────────────
193
+ // GOLDEN (probe_mcp_red.py ENV-NUMERIC): numeric task_id/agent_id/summary
194
+ // stringify to "123"/"456"/"42" (NOT the manual/unknown/completed fallbacks).
195
+ #[test]
196
+ fn report_envelope_stringifies_numeric_scalar_fields() {
197
+ let env = normalize_report_envelope(&json!({"task_id": 123, "agent_id": 456, "summary": 42}));
198
+ assert_eq!(env.task_id, TaskId::new("123"));
199
+ assert_eq!(env.agent_id, AgentId::new("456"));
200
+ assert_eq!(env.summary, "42");
201
+ }
202
+
203
+ // ── #28 handle_mcp initialize result KEY ORDER (server.py:55-59) ────────────
204
+ // GOLDEN (probe_init evidence): protocolVersion, capabilities, serverInfo.
205
+ // Rust currently emits protocolVersion, serverInfo, capabilities.
206
+ #[test]
207
+ fn handle_mcp_initialize_result_key_order_is_golden() {
208
+ let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
209
+ let resp = handle_mcp(&tools, &json!({
210
+ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "X"}
211
+ })).unwrap().expect("initialize frame");
212
+ let result = resp.result.unwrap();
213
+ assert_eq!(keys(&result), vec!["protocolVersion", "capabilities", "serverInfo"]);
214
+ }
215
+
216
+ // ── #31 tools/call text field: json.dumps DEFAULT separators (spaces) ───────
217
+ // GOLDEN (probe_mcp_red.py DUMPS-DEFAULT): text has a space after ':' and ','.
218
+ // Rust serde_json::to_string is compact (no spaces).
219
+ #[test]
220
+ fn tool_call_text_field_uses_json_dumps_default_spacing() {
221
+ let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
222
+ let resp = handle_mcp(&tools, &json!({
223
+ "jsonrpc": "2.0", "id": 9, "method": "tools/call",
224
+ "params": {"name": "nope", "arguments": {}}
225
+ })).unwrap().unwrap();
226
+ let text = resp.result.unwrap()["content"][0]["text"].as_str().unwrap().to_string();
227
+ // The raw bytes are compared by MCP clients: golden has ": " and ", ".
228
+ assert!(text.contains(r#""ok": false"#), "golden json.dumps puts a space after ':'");
229
+ assert!(text.contains(r#""reason": "unknown_tool""#), "golden has space after ':' and ','");
230
+ assert!(text.starts_with(r#"{"ok": false, "reason": "unknown_tool""#),
231
+ "byte-stable golden prefix; got: {text}");
232
+ }
233
+
234
+ // ── #33/#39 dispatch: empty/missing tool → 'unknown tool None'; method fallback;
235
+ // quote tool → Python repr double-quote switch ───────────────────────────
236
+ // GOLDEN (probe_dispatch_red.py): empty/missing tool message == "unknown tool None"
237
+ // (Python repr of None, not ''); method fallback resolves the tool name; a tool
238
+ // name containing a single quote reprs with DOUBLE quotes.
239
+ #[test]
240
+ fn dispatch_empty_and_missing_tool_message_is_unknown_tool_none() {
241
+ let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
242
+ // empty-string tool is falsy in Python → falls through to method (None).
243
+ let e1 = dispatch(&tools, &json!({"tool": "", "arguments": {}})).expect_err("empty tool ⇒ Err");
244
+ assert_eq!(e1.message, "unknown tool None");
245
+ // no tool, no method at all.
246
+ let e2 = dispatch(&tools, &json!({"arguments": {}})).expect_err("missing tool ⇒ Err");
247
+ assert_eq!(e2.message, "unknown tool None");
248
+ }
249
+
250
+ #[test]
251
+ fn dispatch_falls_back_to_method_key_for_tool_name() {
252
+ // tool absent, method present and unknown → 'unknown tool 'nope'' (the method
253
+ // value, not 'unknown tool None'). Rust currently only falls back to 'name'.
254
+ let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
255
+ let e = dispatch(&tools, &json!({"method": "nope", "params": {}})).expect_err("unknown ⇒ Err");
256
+ assert_eq!(e.message, "unknown tool 'nope'");
257
+ }
258
+
259
+ #[test]
260
+ fn dispatch_unknown_tool_with_quote_uses_python_repr_double_quotes() {
261
+ // GOLDEN QUOTE-TOOL: repr("a'b") switches to double quotes → unknown tool "a'b".
262
+ let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
263
+ let e = dispatch(&tools, &json!({"tool": "a'b", "arguments": {}})).expect_err("unknown ⇒ Err");
264
+ assert_eq!(e.message, "unknown tool \"a'b\"");
265
+ }
266
+
267
+ // ── #37 rpc id echoed VERBATIM: float 1.5 and bigint beyond i64 ─────────────
268
+ // GOLDEN (probe_rpcid_red.py): id 1.5 → echoed 1.5; 99999999999999999999 → verbatim.
269
+ // Rust collapses both to null (RpcId has only Int(i64)/Str/Null).
270
+ #[test]
271
+ fn rpc_id_float_echoed_verbatim() {
272
+ let tools = TeamOrchestratorTools::with_identity(Path::new("/tmp/ws"), None, None);
273
+ let resp = handle_mcp(&tools, &json!({
274
+ "jsonrpc": "2.0", "id": 1.5, "method": "initialize", "params": {}
275
+ })).unwrap().unwrap();
276
+ let v = serde_json::to_value(&resp).unwrap();
277
+ assert_eq!(v["id"], json!(1.5), "float id echoed verbatim, not null");
278
+ }
279
+
280
+ // ── #51 dispatch_tool(SendMessage) WorkerAccepted returned VERBATIM ─────────
281
+ // GOLDEN (probe_sendoutcome evidence + tools.py:176-183): worker-accepted dict is
282
+ // returned DIRECTLY: keys [status, delivery_pending, poll_via, message_id]. Rust
283
+ // dispatch_tool re-runs compact_tool_result, dropping delivery_pending/poll_via.