@team-agent/installer 0.5.60 → 0.5.62

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 (265) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +49 -3
  4. package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +184 -0
  6. package/crates/team-agent/src/cli/emit.rs +104 -21
  7. package/crates/team-agent/src/cli/helpers.rs +1 -0
  8. package/crates/team-agent/src/cli/leader.rs +14 -2
  9. package/crates/team-agent/src/cli/leaders.rs +2 -0
  10. package/crates/team-agent/src/cli/mod.rs +296 -8
  11. package/crates/team-agent/src/cli/named_address.rs +26 -2
  12. package/crates/team-agent/src/cli/profile.rs +2 -1
  13. package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
  14. package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
  15. package/crates/team-agent/src/cli/send/persist.rs +1 -0
  16. package/crates/team-agent/src/cli/send/presentation.rs +3 -0
  17. package/crates/team-agent/src/cli/send/resolve.rs +206 -1
  18. package/crates/team-agent/src/cli/send.rs +2 -1
  19. package/crates/team-agent/src/cli/spec.rs +5 -1
  20. package/crates/team-agent/src/cli/status.rs +2 -1
  21. package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
  22. package/crates/team-agent/src/cli/status_port/format.rs +1 -0
  23. package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
  24. package/crates/team-agent/src/cli/status_port/store.rs +7 -3
  25. package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
  27. package/crates/team-agent/src/cli/types.rs +36 -2
  28. package/crates/team-agent/src/communication_mode/mod.rs +54 -0
  29. package/crates/team-agent/src/compiler/tests.rs +5 -5
  30. package/crates/team-agent/src/compiler.rs +54 -1
  31. package/crates/team-agent/src/conpty/backend.rs +1 -0
  32. package/crates/team-agent/src/conpty/mod.rs +1 -0
  33. package/crates/team-agent/src/coordinator/backoff.rs +5 -8
  34. package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
  35. package/crates/team-agent/src/coordinator/health.rs +1 -0
  36. package/crates/team-agent/src/coordinator/mod.rs +2 -2
  37. package/crates/team-agent/src/coordinator/orphan.rs +4 -116
  38. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  39. package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
  40. package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
  41. package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
  42. package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
  43. package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
  44. package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
  45. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
  46. package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
  47. package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
  48. package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
  49. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
  50. package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
  51. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
  52. package/crates/team-agent/src/coordinator/tick.rs +24 -28
  53. package/crates/team-agent/src/coordinator/types.rs +3 -176
  54. package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
  55. package/crates/team-agent/src/db/message_store.rs +47 -2
  56. package/crates/team-agent/src/db/migration.rs +5 -4
  57. package/crates/team-agent/src/db/mod.rs +1 -0
  58. package/crates/team-agent/src/db/schema.rs +1 -0
  59. package/crates/team-agent/src/diagnose/comms.rs +2 -1
  60. package/crates/team-agent/src/diagnose/mod.rs +1 -0
  61. package/crates/team-agent/src/diagnose/orphans.rs +1 -0
  62. package/crates/team-agent/src/fake_worker.rs +21 -1
  63. package/crates/team-agent/src/layout/manager.rs +3 -7
  64. package/crates/team-agent/src/layout/mod.rs +2 -2
  65. package/crates/team-agent/src/layout/overlay.rs +6 -1
  66. package/crates/team-agent/src/layout/placement.rs +1 -0
  67. package/crates/team-agent/src/layout/recovery.rs +3 -0
  68. package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
  69. package/crates/team-agent/src/layout/sessions.rs +8 -1
  70. package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
  71. package/crates/team-agent/src/layout/worker_env.rs +3 -0
  72. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  73. package/crates/team-agent/src/leader/helpers.rs +1 -0
  74. package/crates/team-agent/src/leader/incident.rs +1 -0
  75. package/crates/team-agent/src/leader/lease.rs +27 -22
  76. package/crates/team-agent/src/leader/mod.rs +2 -3
  77. package/crates/team-agent/src/leader/owner_bind.rs +13 -3
  78. package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
  79. package/crates/team-agent/src/leader/rediscover.rs +4 -17
  80. package/crates/team-agent/src/leader/registry.rs +3 -15
  81. package/crates/team-agent/src/leader/start.rs +1072 -186
  82. package/crates/team-agent/src/leader/takeover.rs +10 -232
  83. package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
  84. package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
  85. package/crates/team-agent/src/leader/tests/identity.rs +71 -59
  86. package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
  87. package/crates/team-agent/src/leader/tests/idle.rs +0 -193
  88. package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
  89. package/crates/team-agent/src/leader/tests/mod.rs +4 -41
  90. package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
  91. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
  92. package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
  93. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
  94. package/crates/team-agent/src/leader/types.rs +1 -28
  95. package/crates/team-agent/src/lib.rs +7 -0
  96. package/crates/team-agent/src/lifecycle/display.rs +7 -1
  97. package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
  98. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
  99. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
  100. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
  101. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
  102. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
  103. package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
  104. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
  105. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
  106. package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -28
  107. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
  108. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
  109. package/crates/team-agent/src/lifecycle/launch.rs +11 -12
  110. package/crates/team-agent/src/lifecycle/mod.rs +2 -1
  111. package/crates/team-agent/src/lifecycle/restart/agent.rs +4 -4
  112. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  113. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
  114. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
  115. package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
  116. package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
  117. package/crates/team-agent/src/lifecycle/restart.rs +13 -11
  118. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
  119. package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
  120. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
  121. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
  122. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
  123. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
  124. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
  125. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
  126. package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
  127. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
  128. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
  129. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
  130. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
  131. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
  132. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
  133. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
  134. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
  135. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
  136. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
  137. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
  138. package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
  139. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
  140. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
  141. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
  142. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
  143. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
  144. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
  145. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
  146. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
  147. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
  148. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
  149. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
  150. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
  151. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
  152. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
  153. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
  154. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
  155. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
  156. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
  157. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
  158. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
  159. package/crates/team-agent/src/lifecycle/tests.rs +59 -0
  160. package/crates/team-agent/src/lifecycle/types.rs +0 -9
  161. package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
  162. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  163. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
  164. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
  165. package/crates/team-agent/src/mcp_server/mod.rs +1 -0
  166. package/crates/team-agent/src/mcp_server/normalize.rs +5 -3
  167. package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
  168. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  169. package/crates/team-agent/src/mcp_server/tools.rs +101 -66
  170. package/crates/team-agent/src/mcp_server/types.rs +6 -18
  171. package/crates/team-agent/src/mcp_server/wire.rs +32 -26
  172. package/crates/team-agent/src/messaging/activity.rs +1 -0
  173. package/crates/team-agent/src/messaging/address.rs +1 -0
  174. package/crates/team-agent/src/messaging/delivery.rs +109 -41
  175. package/crates/team-agent/src/messaging/helpers.rs +2 -1
  176. package/crates/team-agent/src/messaging/leader_channel.rs +34 -10
  177. package/crates/team-agent/src/messaging/leader_receiver.rs +42 -24
  178. package/crates/team-agent/src/messaging/mod.rs +10 -14
  179. package/crates/team-agent/src/messaging/peers.rs +2 -0
  180. package/crates/team-agent/src/messaging/persist.rs +3 -1
  181. package/crates/team-agent/src/messaging/presentation.rs +111 -1
  182. package/crates/team-agent/src/messaging/results.rs +166 -17
  183. package/crates/team-agent/src/messaging/scheduler.rs +1 -0
  184. package/crates/team-agent/src/messaging/selftest.rs +1 -1
  185. package/crates/team-agent/src/messaging/send.rs +135 -89
  186. package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
  187. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
  188. package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
  189. package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
  190. package/crates/team-agent/src/messaging/types.rs +7 -2
  191. package/crates/team-agent/src/messaging/watchers.rs +24 -13
  192. package/crates/team-agent/src/model/enums.rs +1 -0
  193. package/crates/team-agent/src/model/errors.rs +1 -0
  194. package/crates/team-agent/src/model/ids.rs +1 -0
  195. package/crates/team-agent/src/model/mod.rs +2 -0
  196. package/crates/team-agent/src/model/name_similarity.rs +1 -0
  197. package/crates/team-agent/src/model/pane_authority_refusal.rs +359 -0
  198. package/crates/team-agent/src/model/paths.rs +1 -0
  199. package/crates/team-agent/src/model/permissions.rs +3 -2
  200. package/crates/team-agent/src/model/routing.rs +1 -0
  201. package/crates/team-agent/src/model/spec.rs +17 -0
  202. package/crates/team-agent/src/model/task_graph.rs +1 -0
  203. package/crates/team-agent/src/model/yaml/tests.rs +1 -0
  204. package/crates/team-agent/src/model/yaml.rs +1 -0
  205. package/crates/team-agent/src/packaging/install.rs +9 -147
  206. package/crates/team-agent/src/packaging/migrate.rs +2 -0
  207. package/crates/team-agent/src/packaging/mod.rs +1 -0
  208. package/crates/team-agent/src/packaging/repair.rs +2 -0
  209. package/crates/team-agent/src/packaging/tests.rs +29 -218
  210. package/crates/team-agent/src/packaging/types.rs +8 -15
  211. package/crates/team-agent/src/platform/argv.rs +4 -0
  212. package/crates/team-agent/src/platform/errors.rs +10 -1
  213. package/crates/team-agent/src/platform/file_lock.rs +4 -157
  214. package/crates/team-agent/src/platform/mod.rs +0 -61
  215. package/crates/team-agent/src/platform/process.rs +1 -0
  216. package/crates/team-agent/src/provider/adapter.rs +1 -0
  217. package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
  218. package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
  219. package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
  220. package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
  221. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
  222. package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
  223. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  224. package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
  225. package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
  226. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
  227. package/crates/team-agent/src/provider/classify.rs +1 -0
  228. package/crates/team-agent/src/provider/faults.rs +1 -26
  229. package/crates/team-agent/src/provider/helpers.rs +1 -0
  230. package/crates/team-agent/src/provider/mod.rs +1 -1
  231. package/crates/team-agent/src/provider/session/capture.rs +41 -22
  232. package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
  233. package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
  234. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
  235. package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
  236. package/crates/team-agent/src/provider/session/mod.rs +2 -4
  237. package/crates/team-agent/src/provider/session/resume.rs +2 -209
  238. package/crates/team-agent/src/provider/session_scan/claude.rs +93 -3
  239. package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
  240. package/crates/team-agent/src/provider/session_scan/common.rs +24 -7
  241. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  242. package/crates/team-agent/src/provider/session_scan.rs +6 -42
  243. package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
  244. package/crates/team-agent/src/provider/types.rs +1 -0
  245. package/crates/team-agent/src/provider/wire.rs +1 -0
  246. package/crates/team-agent/src/state/identity.rs +16 -5
  247. package/crates/team-agent/src/state/identity_keys.rs +1 -0
  248. package/crates/team-agent/src/state/mod.rs +2 -0
  249. package/crates/team-agent/src/state/owner_gate.rs +5 -0
  250. package/crates/team-agent/src/state/ownership.rs +10 -4
  251. package/crates/team-agent/src/state/paths.rs +12 -0
  252. package/crates/team-agent/src/state/persist.rs +9 -3
  253. package/crates/team-agent/src/state/projection.rs +15 -2
  254. package/crates/team-agent/src/state/repository/intent.rs +1 -0
  255. package/crates/team-agent/src/state/repository/tests.rs +1 -0
  256. package/crates/team-agent/src/state/repository.rs +7 -0
  257. package/crates/team-agent/src/state/selector.rs +1 -0
  258. package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
  259. package/crates/team-agent/src/tmux_backend.rs +63 -0
  260. package/crates/team-agent/src/topology.rs +3 -0
  261. package/crates/team-agent/src/transport.rs +15 -0
  262. package/package.json +4 -4
  263. package/skills/team-agent/command-coverage.json +379 -0
  264. package/crates/team-agent/src/leader/inject.rs +0 -33
  265. package/crates/team-agent/src/provider/command.rs +0 -80
@@ -0,0 +1,928 @@
1
+ //! #249 Python 0.2.11 -> Rust 0.3.x local upgrade compatibility contracts.
2
+ //!
3
+ //! These are T0/T1 fixture-driven checks only: no provider subscription, no live
4
+ //! upgrade. They pin the state key, DB, delivery, restart, and MCP config seams
5
+ //! that must survive a Python 0.2.11 runtime handoff.
6
+
7
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
8
+
9
+ #[path = "../../../tests/support/hermetic.rs"]
10
+ mod hermetic_guard;
11
+ #[allow(dead_code)]
12
+ fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
13
+
14
+ use std::collections::BTreeMap;
15
+ use std::path::{Path, PathBuf};
16
+ use std::sync::atomic::{AtomicU64, Ordering};
17
+ use std::sync::Mutex;
18
+
19
+ use rusqlite::params;
20
+ use serde_json::{json, Value};
21
+ use serial_test::serial;
22
+ use team_agent::cli::{
23
+ cmd_collect_for_team, cmd_status_for_team, CmdOutput, CollectArgs, StatusArgs,
24
+ };
25
+ use team_agent::db::schema::{initialize_schema, open_db, table_layout, SCHEMA_VERSION};
26
+ use team_agent::event_log::EventLog;
27
+ use team_agent::lifecycle::restart::classify_restart_plan;
28
+ use team_agent::lifecycle::{restart_with_transport, ResumeDecision, StartMode};
29
+ use team_agent::message_store::MessageStore;
30
+ use team_agent::messaging::delivery::deliver_pending_message;
31
+ use team_agent::state::persist::save_runtime_state;
32
+ use team_agent::state::selector::{resolve_active_team, SelectorMode};
33
+ use team_agent::transport::{
34
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
35
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
36
+ SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
37
+ TurnVerification, WindowName,
38
+ };
39
+
40
+ const TEAM_KEY: &str = "upgrade-key";
41
+ const LEGACY_NAME: &str = "legacy-name";
42
+ const SIBLING_KEY: &str = "sibling-key";
43
+
44
+ #[test]
45
+ #[serial(env)]
46
+ fn upgrade_0211_state_team_key_runtime_key_not_spec_name() {
47
+ let fixture = UpgradeFixture::new("state-key");
48
+ fixture.write_spec(TEAM_KEY, LEGACY_NAME);
49
+ fixture.seed_dual_team_state(TEAM_KEY);
50
+
51
+ let selected = resolve_active_team(
52
+ &fixture.workspace,
53
+ Some(TEAM_KEY),
54
+ SelectorMode::RuntimeOnly,
55
+ )
56
+ .expect("selector must accept the runtime team key");
57
+ assert_eq!(
58
+ selected.team_key, TEAM_KEY,
59
+ "runtime team key comes from team_dir/spec_path state key, not spec.name"
60
+ );
61
+ assert_eq!(selected.state["active_team_key"], json!(TEAM_KEY));
62
+ assert!(selected.state["agents"].get("upgrade_worker").is_some());
63
+
64
+ let status = json_output(cmd_status_for_team(
65
+ &StatusArgs {
66
+ agent: None,
67
+ workspace: fixture.workspace.clone(),
68
+ detail: true,
69
+ summary: false,
70
+ json: true,
71
+ team: None,
72
+ },
73
+ Some(TEAM_KEY),
74
+ ));
75
+ assert!(
76
+ status.pointer("/agents/upgrade_worker").is_some(),
77
+ "status --team upgrade-key must project the runtime-key team; status={status}"
78
+ );
79
+ assert!(
80
+ status.pointer("/agents/sibling_worker").is_none(),
81
+ "status --team upgrade-key must not collapse to sibling/spec-name state; status={status}"
82
+ );
83
+ }
84
+
85
+ #[test]
86
+ #[serial(env)]
87
+ fn upgrade_0211_db_schema_v3_reads_and_preserves_rows() {
88
+ let fixture = UpgradeFixture::new("db-v3");
89
+ fixture.seed_python_v3_db();
90
+ let before = fixture.table_counts(&["messages", "results", "agent_health"]);
91
+
92
+ let store = MessageStore::open(&fixture.workspace).expect("Rust must open Python schema v3 DB");
93
+ let after = fixture.table_counts(&["messages", "results", "agent_health"]);
94
+
95
+ assert_eq!(before, after, "opening Python schema v3 must not drop rows");
96
+ assert_eq!(
97
+ pragma_user_version(store.db_path()),
98
+ SCHEMA_VERSION,
99
+ "schema version must remain readable as v3"
100
+ );
101
+ }
102
+
103
+ #[test]
104
+ #[serial(env)]
105
+ fn upgrade_legacy_owner_team_id_delivery_projects_state_team() {
106
+ let fixture = UpgradeFixture::new("delivery-runtime-key");
107
+ fixture.seed_dual_team_state(SIBLING_KEY);
108
+ let store = MessageStore::open(&fixture.workspace).unwrap();
109
+ let message_id = store
110
+ .create_message(None, "leader", "w1", "ping", None, false, Some(TEAM_KEY))
111
+ .unwrap();
112
+ let transport = RecordingTransport::new();
113
+ let state = team_agent::state::persist::load_runtime_state(&fixture.workspace).unwrap();
114
+
115
+ let outcome = deliver_pending_message(
116
+ &fixture.workspace,
117
+ &store,
118
+ &transport,
119
+ &message_id,
120
+ &EventLog::new(&fixture.workspace),
121
+ &state,
122
+ )
123
+ .expect("delivery should run");
124
+
125
+ assert!(
126
+ outcome.ok,
127
+ "runtime-key owner row should deliver; outcome={outcome:?}"
128
+ );
129
+ assert_eq!(
130
+ transport.injected_windows(),
131
+ vec![(format!("team-{TEAM_KEY}"), "upgrade-window".to_string())],
132
+ "owner_team_id=upgrade-key must project state.teams[upgrade-key], not active sibling"
133
+ );
134
+ }
135
+
136
+ #[test]
137
+ #[serial(env)]
138
+ fn upgrade_spec_name_owner_team_id_projects_without_read_side_migration() {
139
+ let fixture = UpgradeFixture::new("delivery-legacy-name");
140
+ fixture.seed_dual_team_state(SIBLING_KEY);
141
+ let store = MessageStore::open(&fixture.workspace).unwrap();
142
+ let message_id = store
143
+ .create_message(None, "leader", "w1", "ping", None, false, Some(LEGACY_NAME))
144
+ .unwrap();
145
+ let transport = RecordingTransport::new();
146
+ let state = team_agent::state::persist::load_runtime_state(&fixture.workspace).unwrap();
147
+
148
+ let outcome = deliver_pending_message(
149
+ &fixture.workspace,
150
+ &store,
151
+ &transport,
152
+ &message_id,
153
+ &EventLog::new(&fixture.workspace),
154
+ &state,
155
+ )
156
+ .expect("delivery should return an explicit outcome");
157
+ let owner_after = owner_team_id_for_message(&fixture.workspace, &message_id);
158
+ let status_after = message_status(&fixture.workspace, &message_id);
159
+
160
+ assert!(
161
+ outcome.ok,
162
+ "legacy alias must still project to canonical state; outcome={outcome:?}"
163
+ );
164
+ assert_eq!(
165
+ owner_after.as_deref(),
166
+ Some(LEGACY_NAME),
167
+ "U0-B: read-side compatibility must not UPDATE owner_team_id rows; owner_after={owner_after:?}"
168
+ );
169
+ assert_eq!(
170
+ status_after.as_deref(),
171
+ Some("delivered"),
172
+ "legacy alias delivery should be explicit, not silently dropped; status_after={status_after:?}"
173
+ );
174
+ assert_eq!(
175
+ transport.injected_windows(),
176
+ vec![(format!("team-{TEAM_KEY}"), "upgrade-window".to_string())],
177
+ "owner_team_id=legacy-name must project to upgrade-key without mutating the DB row"
178
+ );
179
+ assert!(
180
+ events_text(&fixture.workspace).contains("owner_team_id.compatibility_alias_detected"),
181
+ "legacy alias projection must leave a read-only audit event; events={}",
182
+ events_text(&fixture.workspace)
183
+ );
184
+ }
185
+
186
+ #[test]
187
+ #[serial(env)]
188
+ fn upgrade_status_collect_scope_by_selected_team_key() {
189
+ let fixture = UpgradeFixture::new("status-collect-scope");
190
+ fixture.write_spec(TEAM_KEY, LEGACY_NAME);
191
+ fixture.seed_dual_team_state(TEAM_KEY);
192
+ fixture.seed_python_v3_db();
193
+ fixture.seed_result(
194
+ "res-upgrade",
195
+ TEAM_KEY,
196
+ "task-upgrade",
197
+ "upgrade_worker",
198
+ "success",
199
+ );
200
+ fixture.seed_result(
201
+ "res-sibling",
202
+ SIBLING_KEY,
203
+ "task-sibling",
204
+ "sibling_worker",
205
+ "success",
206
+ );
207
+
208
+ let status = json_output(cmd_status_for_team(
209
+ &StatusArgs {
210
+ agent: None,
211
+ workspace: fixture.workspace.clone(),
212
+ detail: true,
213
+ summary: false,
214
+ json: true,
215
+ team: None,
216
+ },
217
+ Some(TEAM_KEY),
218
+ ));
219
+ assert_eq!(
220
+ status.pointer("/results/total").and_then(Value::as_i64),
221
+ Some(2),
222
+ "status --team upgrade-key must count only upgrade-key results and exclude sibling rows; status={status}"
223
+ );
224
+ assert_eq!(
225
+ status.pointer("/messages/accepted").and_then(Value::as_i64),
226
+ Some(1),
227
+ "status --team upgrade-key must count only upgrade-key messages; status={status}"
228
+ );
229
+
230
+ let collect = json_output(cmd_collect_for_team(
231
+ &CollectArgs {
232
+ workspace: fixture.workspace.clone(),
233
+ result_file: None,
234
+ json: true,
235
+ team: None,
236
+ },
237
+ Some(TEAM_KEY),
238
+ ));
239
+ let empty = Vec::new();
240
+ let collected_ids = collect
241
+ .get("collected_results")
242
+ .and_then(Value::as_array)
243
+ .unwrap_or(&empty)
244
+ .iter()
245
+ .filter_map(|item| item.get("result_id").and_then(Value::as_str))
246
+ .collect::<Vec<_>>();
247
+ assert!(
248
+ collected_ids.contains(&"res-existing-upgrade")
249
+ && collected_ids.contains(&"res-upgrade")
250
+ && !collected_ids.contains(&"res-sibling"),
251
+ "collect --team upgrade-key must collect only selected runtime-key rows; collect={collect}"
252
+ );
253
+ }
254
+
255
+ #[test]
256
+ fn upgrade_restart_plan_uses_persisted_session_id() {
257
+ let dir = tmp_dir("restart-plan-rollout");
258
+ let rollout = dir.join("rollout.jsonl");
259
+ std::fs::write(&rollout, "{}\n").unwrap();
260
+ let state = json!({
261
+ "agents": {
262
+ "w1": {
263
+ "status": "running",
264
+ "provider": "codex",
265
+ "session_id": "sess-upgrade",
266
+ "rollout_path": rollout.to_string_lossy(),
267
+ "first_send_at": "2026-06-07T01:00:00+00:00"
268
+ }
269
+ }
270
+ });
271
+
272
+ let plan = classify_restart_plan(&state, false).expect("restart plan");
273
+ assert_eq!(plan.decisions[0].decision, ResumeDecision::Resume);
274
+ assert_eq!(plan.decisions[0].restart_mode, StartMode::Resumed);
275
+ assert_eq!(
276
+ plan.decisions[0].session_id.as_ref().map(|s| s.as_str()),
277
+ Some("sess-upgrade")
278
+ );
279
+ }
280
+
281
+ /// 0.4.7 partial-resume: a worker that has NEVER been captured
282
+ /// (no session_id AND first_send_at is Absent) is structurally non-resumable.
283
+ /// restart without --allow-fresh must auto-fresh it (no context to lose),
284
+ /// instead of refusing and blocking the other roles. This prevents 1
285
+ /// never-captured role from connecting 6 other resumable roles.
286
+ #[test]
287
+ fn restart_auto_fresh_never_captured_worker_without_allow_fresh() {
288
+ let state = json!({
289
+ "agents": {
290
+ "never_captured": {
291
+ "status": "running",
292
+ "provider": "claude",
293
+ "session_id": null,
294
+ "first_send_at": null
295
+ }
296
+ }
297
+ });
298
+
299
+ let plan = classify_restart_plan(&state, false).expect("restart plan");
300
+ assert_eq!(
301
+ plan.decisions[0].decision,
302
+ ResumeDecision::FreshStart,
303
+ "0.4.7: never-captured worker (no session_id + no first_send_at) \
304
+ must auto-fresh without --allow-fresh; no context to lose"
305
+ );
306
+ assert_eq!(plan.decisions[0].restart_mode, StartMode::Fresh);
307
+ assert!(
308
+ plan.unresumable.is_empty(),
309
+ "0.4.7: never-captured worker is not unresumable (no context to fail)"
310
+ );
311
+ }
312
+
313
+ /// 0.4.7 partial-resume guard: a worker that HAS first_send_at but no
314
+ /// session_id is the "received message but session not captured" bug
315
+ /// state — restart without --allow-fresh must STILL Refuse, never
316
+ /// silently fresh (architect rule: 绝不静默 fresh). This is what the
317
+ /// existing `upgrade_restart_refuses_interacted_worker_without_session_id`
318
+ /// test pins; this test pins the SAME guard for Claude (the partial-resume
319
+ /// change must not weaken it for any provider).
320
+ #[test]
321
+ fn restart_refuses_interacted_claude_worker_without_session_id_partial_resume_preserves_guard() {
322
+ let state = json!({
323
+ "agents": {
324
+ "interacted_no_sid": {
325
+ "status": "running",
326
+ "provider": "claude",
327
+ "session_id": null,
328
+ "first_send_at": "2026-06-26T01:00:00+00:00"
329
+ }
330
+ }
331
+ });
332
+
333
+ let plan = classify_restart_plan(&state, false).expect("restart plan");
334
+ assert_eq!(
335
+ plan.decisions[0].decision,
336
+ ResumeDecision::Refuse,
337
+ "0.4.7: interacted worker with no session_id must STILL refuse \
338
+ without --allow-fresh (architect rule: never silently drop context)"
339
+ );
340
+ }
341
+
342
+ #[test]
343
+ fn upgrade_restart_refuses_interacted_worker_without_session_id() {
344
+ let state = json!({
345
+ "agents": {
346
+ "w1": {
347
+ "status": "running",
348
+ "provider": "codex",
349
+ "session_id": null,
350
+ "first_send_at": "2026-06-07T01:00:00+00:00"
351
+ }
352
+ }
353
+ });
354
+
355
+ let refused = classify_restart_plan(&state, false).expect("restart plan");
356
+ assert_eq!(refused.decisions[0].decision, ResumeDecision::Refuse);
357
+ assert_eq!(refused.unresumable[0].reason, "no_persisted_session_id");
358
+
359
+ let allowed = classify_restart_plan(&state, true).expect("restart plan with allow_fresh");
360
+ assert_eq!(allowed.decisions[0].decision, ResumeDecision::FreshStart);
361
+ assert!(
362
+ allowed.unresumable.is_empty(),
363
+ "allow_fresh is the explicit opt-in to fresh restart for interacted workers"
364
+ );
365
+ }
366
+
367
+ #[test]
368
+ #[serial(env)]
369
+ fn upgrade_rust_mcp_config_uses_current_binary_and_runtime_key() {
370
+ let fixture = UpgradeFixture::new("restart-mcp");
371
+ fixture.write_spec(TEAM_KEY, LEGACY_NAME);
372
+ fixture.seed_restartable_state();
373
+ fixture.seed_healthy_coordinator();
374
+ let transport = RecordingTransport::new().with_session_present(false);
375
+
376
+ let report = restart_with_transport(&fixture.team_dir, false, Some(TEAM_KEY), &transport)
377
+ .expect("restart should spawn resumable worker");
378
+ let spawn = transport.single_spawn();
379
+ let command_line = spawn.command_line();
380
+ let current_exe = std::env::current_exe()
381
+ .unwrap()
382
+ .to_string_lossy()
383
+ .to_string();
384
+
385
+ assert!(
386
+ format!("{report:?}").contains("Resumed"),
387
+ "restart report must show resume path for persisted session_id; report={report:?}"
388
+ );
389
+ assert!(
390
+ command_line.contains(&current_exe),
391
+ "restarted worker MCP command must use the current Rust candidate binary path; current_exe={current_exe} command_line={command_line:?}"
392
+ );
393
+ assert!(
394
+ command_line.contains("TEAM_AGENT_OWNER_TEAM_ID=upgrade-key")
395
+ && command_line.contains("TEAM_AGENT_OWNER_TEAM_ID=\"upgrade-key\""),
396
+ "spawn env and MCP config must both use runtime team key upgrade-key; command_line={command_line:?}"
397
+ );
398
+ assert!(
399
+ spawn
400
+ .argv
401
+ .windows(2)
402
+ .any(|pair| pair[0] == "codex" && pair[1] == "resume")
403
+ && spawn.argv.iter().any(|arg| arg == "sess-upgrade"),
404
+ "restart must resume persisted provider session, not fresh start; command_line={command_line:?}"
405
+ );
406
+ }
407
+
408
+ #[test]
409
+ #[serial(env)]
410
+ fn upgrade_db_layout_rebuild_preserves_owner_team_id() {
411
+ let fixture = UpgradeFixture::new("db-rebuild-owner");
412
+ fixture.seed_legacy_drift_db_owner_last();
413
+
414
+ let before = distinct_owner_ids(&fixture.db_path(), "messages");
415
+ let store = MessageStore::open(&fixture.workspace).expect("open drifted legacy DB");
416
+ let after = distinct_owner_ids(store.db_path(), "messages");
417
+
418
+ assert_eq!(before, vec![TEAM_KEY.to_string()]);
419
+ assert_eq!(
420
+ after, before,
421
+ "layout rebuild must preserve owner_team_id values"
422
+ );
423
+ assert_eq!(
424
+ table_layout(&open_db(store.db_path()).unwrap(), "messages").unwrap()[..2],
425
+ ["message_id".to_string(), "owner_team_id".to_string()],
426
+ "messages layout must rebuild to canonical owner_team_id position"
427
+ );
428
+ }
429
+
430
+ struct UpgradeFixture {
431
+ workspace: PathBuf,
432
+ team_dir: PathBuf,
433
+ }
434
+
435
+ impl UpgradeFixture {
436
+ fn new(tag: &str) -> Self {
437
+ let root = tmp_dir(tag);
438
+ let workspace = root.join("ws");
439
+ let team_dir = workspace.join(TEAM_KEY);
440
+ std::fs::create_dir_all(&team_dir).unwrap();
441
+ std::fs::create_dir_all(team_agent::model::paths::runtime_dir(&workspace)).unwrap();
442
+ Self {
443
+ workspace,
444
+ team_dir,
445
+ }
446
+ }
447
+
448
+ fn write_spec(&self, team_key: &str, spec_name: &str) {
449
+ std::fs::write(
450
+ self.team_dir.join("TEAM.md"),
451
+ format!(
452
+ "---\nname: {spec_name}\nobjective: upgrade compat\nprovider: codex\n---\n\nTeam.\n"
453
+ ),
454
+ )
455
+ .unwrap();
456
+ std::fs::create_dir_all(self.team_dir.join("agents")).unwrap();
457
+ std::fs::write(
458
+ self.team_dir.join("agents").join("upgrade_worker.md"),
459
+ "---\nname: upgrade_worker\nrole: Upgrade Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nWorker.\n",
460
+ )
461
+ .unwrap();
462
+ let spec = team_agent::compiler::compile_team(&self.team_dir).unwrap();
463
+ assert_eq!(team_key, TEAM_KEY, "fixture uses constant runtime key");
464
+ std::fs::write(
465
+ self.team_dir.join("team.spec.yaml"),
466
+ team_agent::model::yaml::dumps(&spec),
467
+ )
468
+ .unwrap();
469
+ }
470
+
471
+ fn seed_dual_team_state(&self, active: &str) {
472
+ save_runtime_state(&self.workspace, &self.dual_team_state(active)).unwrap();
473
+ }
474
+
475
+ fn seed_restartable_state(&self) {
476
+ std::fs::write(self.workspace.join("rollout.jsonl"), "{}\n").unwrap();
477
+ save_runtime_state(
478
+ &self.workspace,
479
+ &json!({
480
+ "active_team_key": TEAM_KEY,
481
+ "team_dir": self.team_dir.to_string_lossy(),
482
+ "spec_path": self.team_dir.join("team.spec.yaml").to_string_lossy(),
483
+ "session_name": format!("team-{TEAM_KEY}"),
484
+ "agents": {
485
+ "upgrade_worker": {
486
+ "status": "running",
487
+ "provider": "codex",
488
+ "role": "Upgrade Worker",
489
+ "tools": ["mcp_team"],
490
+ "window": "upgrade_worker",
491
+ "owner_team_id": TEAM_KEY,
492
+ "session_id": "sess-upgrade",
493
+ "first_send_at": "2026-06-07T01:00:00+00:00",
494
+ "rollout_path": self.workspace.join("rollout.jsonl").to_string_lossy(),
495
+ "spawn_cwd": self.team_dir.to_string_lossy()
496
+ }
497
+ },
498
+ "tasks": [],
499
+ "teams": {
500
+ TEAM_KEY: {
501
+ "status": "alive",
502
+ "team_dir": self.team_dir.to_string_lossy(),
503
+ "spec_path": self.team_dir.join("team.spec.yaml").to_string_lossy(),
504
+ "session_name": format!("team-{TEAM_KEY}"),
505
+ "agents": {
506
+ "upgrade_worker": {
507
+ "status": "running",
508
+ "provider": "codex",
509
+ "role": "Upgrade Worker",
510
+ "tools": ["mcp_team"],
511
+ "window": "upgrade_worker",
512
+ "owner_team_id": TEAM_KEY,
513
+ "session_id": "sess-upgrade",
514
+ "first_send_at": "2026-06-07T01:00:00+00:00",
515
+ "rollout_path": self.workspace.join("rollout.jsonl").to_string_lossy(),
516
+ "spawn_cwd": self.team_dir.to_string_lossy()
517
+ }
518
+ },
519
+ "tasks": []
520
+ }
521
+ }
522
+ }),
523
+ )
524
+ .unwrap();
525
+ }
526
+
527
+ fn dual_team_state(&self, active: &str) -> Value {
528
+ let upgrade = json!({
529
+ "status": "alive",
530
+ "team_dir": self.team_dir.to_string_lossy(),
531
+ "spec_path": self.team_dir.join("team.spec.yaml").to_string_lossy(),
532
+ "session_name": format!("team-{TEAM_KEY}"),
533
+ "team": {"name": LEGACY_NAME},
534
+ "agents": {
535
+ "upgrade_worker": {"status": "running", "provider": "fake", "window": "upgrade-window", "owner_team_id": TEAM_KEY},
536
+ "w1": {"status": "running", "provider": "fake", "window": "upgrade-window", "owner_team_id": TEAM_KEY}
537
+ },
538
+ "tasks": [{"id": "task-upgrade", "assignee": "upgrade_worker", "status": "pending"}]
539
+ });
540
+ let sibling = json!({
541
+ "status": "alive",
542
+ "team_dir": self.workspace.join(SIBLING_KEY).to_string_lossy(),
543
+ "spec_path": self.workspace.join(SIBLING_KEY).join("team.spec.yaml").to_string_lossy(),
544
+ "session_name": format!("team-{SIBLING_KEY}"),
545
+ "agents": {
546
+ "sibling_worker": {"status": "running", "provider": "fake", "window": "sibling-window", "owner_team_id": SIBLING_KEY},
547
+ "w1": {"status": "running", "provider": "fake", "window": "sibling-window", "owner_team_id": SIBLING_KEY}
548
+ },
549
+ "tasks": [{"id": "task-sibling", "assignee": "sibling_worker", "status": "pending"}]
550
+ });
551
+ let active_state = if active == TEAM_KEY {
552
+ upgrade.clone()
553
+ } else {
554
+ sibling.clone()
555
+ };
556
+ let mut state = active_state;
557
+ let obj = state.as_object_mut().unwrap();
558
+ obj.insert("active_team_key".to_string(), json!(active));
559
+ obj.insert(
560
+ "teams".to_string(),
561
+ json!({TEAM_KEY: upgrade, SIBLING_KEY: sibling}),
562
+ );
563
+ state
564
+ }
565
+
566
+ fn db_path(&self) -> PathBuf {
567
+ self.workspace.join(".team").join("runtime").join("team.db")
568
+ }
569
+
570
+ fn seed_python_v3_db(&self) {
571
+ std::fs::create_dir_all(self.db_path().parent().unwrap()).unwrap();
572
+ let conn = open_db(&self.db_path()).unwrap();
573
+ initialize_schema(&conn, Some(&self.db_path())).unwrap();
574
+ conn.execute_batch(&format!("pragma user_version = {SCHEMA_VERSION};"))
575
+ .unwrap();
576
+ conn.execute(
577
+ "insert into messages(message_id, owner_team_id, sender, recipient, requires_ack, status, content, artifact_refs, created_at, updated_at, delivery_attempts)
578
+ values ('msg-upgrade', ?1, 'leader', 'upgrade_worker', 0, 'accepted', 'hello', '[]', '2026-06-07T00:00:00+00:00', '2026-06-07T00:00:00+00:00', 0),
579
+ ('msg-sibling', ?2, 'leader', 'sibling_worker', 0, 'accepted', 'hello', '[]', '2026-06-07T00:00:01+00:00', '2026-06-07T00:00:01+00:00', 0)",
580
+ params![TEAM_KEY, SIBLING_KEY],
581
+ )
582
+ .unwrap();
583
+ self.seed_result(
584
+ "res-existing-upgrade",
585
+ TEAM_KEY,
586
+ "task-upgrade",
587
+ "upgrade_worker",
588
+ "success",
589
+ );
590
+ conn.execute(
591
+ "insert or replace into agent_health(owner_team_id, agent_id, status, updated_at)
592
+ values (?1, 'upgrade_worker', 'running', '2026-06-07T00:00:00+00:00'),
593
+ (?2, 'sibling_worker', 'running', '2026-06-07T00:00:00+00:00')",
594
+ params![TEAM_KEY, SIBLING_KEY],
595
+ )
596
+ .unwrap();
597
+ }
598
+
599
+ fn seed_result(
600
+ &self,
601
+ result_id: &str,
602
+ owner_team_id: &str,
603
+ task_id: &str,
604
+ agent_id: &str,
605
+ status: &str,
606
+ ) {
607
+ let conn = open_db(&self.db_path()).unwrap();
608
+ let envelope = json!({
609
+ "schema_version": "result_envelope_v1",
610
+ "result_id": result_id,
611
+ "task_id": task_id,
612
+ "agent_id": agent_id,
613
+ "status": status,
614
+ "summary": "done",
615
+ "changes": [], "tests": [], "risks": [], "artifacts": [], "next_actions": []
616
+ });
617
+ conn.execute(
618
+ "insert or replace into results(result_id, owner_team_id, task_id, agent_id, envelope, status, created_at)
619
+ values (?1, ?2, ?3, ?4, ?5, ?6, '2026-06-07T00:00:00+00:00')",
620
+ params![result_id, owner_team_id, task_id, agent_id, envelope.to_string(), status],
621
+ )
622
+ .unwrap();
623
+ }
624
+
625
+ fn seed_legacy_drift_db_owner_last(&self) {
626
+ std::fs::create_dir_all(self.db_path().parent().unwrap()).unwrap();
627
+ let conn = rusqlite::Connection::open(self.db_path()).unwrap();
628
+ conn.execute_batch(
629
+ "pragma user_version=1;
630
+ create table messages (message_id text primary key, task_id text, sender text, recipient text, reply_to text, requires_ack integer, status text, content text, artifact_refs text, created_at text, updated_at text, delivered_at text, acknowledged_at text, error text, delivery_attempts integer not null default 0, owner_team_id text);
631
+ create table results (result_id text primary key, task_id text not null, agent_id text not null, envelope text not null, status text not null, created_at text not null, owner_team_id text);
632
+ insert into messages(message_id, status, owner_team_id) values ('msg-drift','accepted','upgrade-key');
633
+ insert into results(result_id, task_id, agent_id, envelope, status, created_at, owner_team_id) values ('res-drift','task-upgrade','upgrade_worker','{}','success','2026-06-07T00:00:00+00:00','upgrade-key');",
634
+ )
635
+ .unwrap();
636
+ }
637
+
638
+ fn seed_healthy_coordinator(&self) {
639
+ let workspace = team_agent::coordinator::WorkspacePath::new(self.workspace.clone());
640
+ std::fs::create_dir_all(team_agent::model::paths::runtime_dir(workspace.as_path()))
641
+ .unwrap();
642
+ let _ = MessageStore::open(workspace.as_path()).unwrap();
643
+ let pid = team_agent::coordinator::Pid::new(std::process::id());
644
+ team_agent::coordinator::write_coordinator_metadata(
645
+ &workspace,
646
+ pid,
647
+ team_agent::coordinator::MetadataSource::Boot,
648
+ )
649
+ .unwrap();
650
+ std::fs::write(
651
+ team_agent::coordinator::coordinator_pid_path(&workspace),
652
+ pid.to_string(),
653
+ )
654
+ .unwrap();
655
+ }
656
+
657
+ fn table_counts(&self, tables: &[&str]) -> BTreeMap<String, i64> {
658
+ let conn = open_db(&self.db_path()).unwrap();
659
+ tables
660
+ .iter()
661
+ .map(|table| {
662
+ let count = conn
663
+ .query_row(&format!("select count(*) from {table}"), [], |row| {
664
+ row.get(0)
665
+ })
666
+ .unwrap();
667
+ ((*table).to_string(), count)
668
+ })
669
+ .collect()
670
+ }
671
+ }
672
+
673
+ #[derive(Debug, Clone)]
674
+ struct RecordedSpawn {
675
+ argv: Vec<String>,
676
+ env: BTreeMap<String, String>,
677
+ }
678
+
679
+ impl RecordedSpawn {
680
+ fn command_line(&self) -> String {
681
+ let mut parts = self
682
+ .env
683
+ .iter()
684
+ .map(|(key, value)| format!("{key}={value}"))
685
+ .collect::<Vec<_>>();
686
+ parts.extend(self.argv.iter().cloned());
687
+ parts.join(" ")
688
+ }
689
+ }
690
+
691
+ #[derive(Debug, Default)]
692
+ struct RecordingTransport {
693
+ spawns: Mutex<Vec<RecordedSpawn>>,
694
+ injected: Mutex<Vec<Target>>,
695
+ session_present: bool,
696
+ }
697
+
698
+ impl RecordingTransport {
699
+ fn new() -> Self {
700
+ Self::default()
701
+ }
702
+
703
+ fn with_session_present(mut self, present: bool) -> Self {
704
+ self.session_present = present;
705
+ self
706
+ }
707
+
708
+ fn single_spawn(&self) -> RecordedSpawn {
709
+ let spawns = self.spawns.lock().unwrap();
710
+ assert_eq!(spawns.len(), 1, "expected one spawn; spawns={spawns:?}");
711
+ spawns[0].clone()
712
+ }
713
+
714
+ fn injected_windows(&self) -> Vec<(String, String)> {
715
+ self.injected
716
+ .lock()
717
+ .unwrap()
718
+ .iter()
719
+ .filter_map(|target| match target {
720
+ Target::SessionWindow { session, window } => {
721
+ Some((session.as_str().to_string(), window.as_str().to_string()))
722
+ }
723
+ Target::Pane(pane) => Some(("pane".to_string(), pane.as_str().to_string())),
724
+ })
725
+ .collect()
726
+ }
727
+
728
+ fn spawn_result(
729
+ &self,
730
+ session: &SessionName,
731
+ window: &WindowName,
732
+ argv: &[String],
733
+ env: &BTreeMap<String, String>,
734
+ ) -> SpawnResult {
735
+ let mut spawns = self.spawns.lock().unwrap();
736
+ spawns.push(RecordedSpawn {
737
+ argv: argv.to_vec(),
738
+ env: env.clone(),
739
+ });
740
+ SpawnResult {
741
+ pane_id: PaneId::new(format!("%{}", spawns.len())),
742
+ session: session.clone(),
743
+ window: window.clone(),
744
+ child_pid: None,
745
+ }
746
+ }
747
+ }
748
+
749
+ impl Transport for RecordingTransport {
750
+ fn kind(&self) -> BackendKind {
751
+ BackendKind::Tmux
752
+ }
753
+
754
+ fn spawn_first(
755
+ &self,
756
+ session: &SessionName,
757
+ window: &WindowName,
758
+ argv: &[String],
759
+ _cwd: &Path,
760
+ env: &BTreeMap<String, String>,
761
+ ) -> Result<SpawnResult, TransportError> {
762
+ Ok(self.spawn_result(session, window, argv, env))
763
+ }
764
+
765
+ fn spawn_into(
766
+ &self,
767
+ session: &SessionName,
768
+ window: &WindowName,
769
+ argv: &[String],
770
+ _cwd: &Path,
771
+ env: &BTreeMap<String, String>,
772
+ ) -> Result<SpawnResult, TransportError> {
773
+ Ok(self.spawn_result(session, window, argv, env))
774
+ }
775
+
776
+ fn inject(
777
+ &self,
778
+ target: &Target,
779
+ _payload: &InjectPayload,
780
+ _submit: Key,
781
+ _bracketed: bool,
782
+ ) -> Result<InjectReport, TransportError> {
783
+ self.injected.lock().unwrap().push(target.clone());
784
+ Ok(InjectReport {
785
+ stage_reached: InjectStage::Submit,
786
+ inject_verification: InjectVerification::CaptureContainsToken,
787
+ submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
788
+ turn_verification: TurnVerification::NotYetObserved,
789
+ attempts: 1,
790
+ submit_diagnostics: None,
791
+ })
792
+ }
793
+
794
+ fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
795
+ Ok(())
796
+ }
797
+
798
+ fn capture(
799
+ &self,
800
+ _target: &Target,
801
+ range: CaptureRange,
802
+ ) -> Result<CapturedText, TransportError> {
803
+ Ok(CapturedText {
804
+ text: String::new(),
805
+ range,
806
+ })
807
+ }
808
+
809
+ fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
810
+ match field {
811
+ PaneField::PaneWidth => Ok(Some("120".to_string())),
812
+ _ => Ok(None),
813
+ }
814
+ }
815
+
816
+ fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
817
+ Ok(PaneLiveness::Live)
818
+ }
819
+
820
+ fn has_pane(&self, pane: &PaneId) -> Result<Option<bool>, TransportError> {
821
+ let spawns = self.spawns.lock().unwrap();
822
+ Ok(Some(
823
+ spawns
824
+ .iter()
825
+ .enumerate()
826
+ .any(|(idx, _)| pane.as_str() == format!("%{}", idx + 1)),
827
+ ))
828
+ }
829
+
830
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
831
+ Ok(Vec::new())
832
+ }
833
+
834
+ fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
835
+ Ok(self.session_present || !self.spawns.lock().unwrap().is_empty())
836
+ }
837
+
838
+ fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
839
+ Ok(Vec::new())
840
+ }
841
+
842
+ fn set_session_env(
843
+ &self,
844
+ _session: &SessionName,
845
+ _key: &str,
846
+ _value: &str,
847
+ ) -> Result<SetEnvOutcome, TransportError> {
848
+ Ok(SetEnvOutcome::Applied)
849
+ }
850
+
851
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
852
+ Ok(())
853
+ }
854
+
855
+ fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
856
+ Ok(())
857
+ }
858
+
859
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
860
+ Ok(AttachOutcome::Attached)
861
+ }
862
+ }
863
+
864
+ fn json_output(result: Result<team_agent::cli::CmdResult, team_agent::cli::CliError>) -> Value {
865
+ match result.expect("CLI command should return a result").output {
866
+ CmdOutput::Json(value) => value,
867
+ other => panic!("expected JSON output, got {other:?}"),
868
+ }
869
+ }
870
+
871
+ fn owner_team_id_for_message(workspace: &Path, message_id: &str) -> Option<String> {
872
+ let store = MessageStore::open(workspace).unwrap();
873
+ let conn = open_db(store.db_path()).unwrap();
874
+ conn.query_row(
875
+ "select owner_team_id from messages where message_id=?1",
876
+ params![message_id],
877
+ |row| row.get(0),
878
+ )
879
+ .unwrap()
880
+ }
881
+
882
+ fn message_status(workspace: &Path, message_id: &str) -> Option<String> {
883
+ let store = MessageStore::open(workspace).unwrap();
884
+ let conn = open_db(store.db_path()).unwrap();
885
+ conn.query_row(
886
+ "select status from messages where message_id=?1",
887
+ params![message_id],
888
+ |row| row.get(0),
889
+ )
890
+ .unwrap()
891
+ }
892
+
893
+ fn events_text(workspace: &Path) -> String {
894
+ std::fs::read_to_string(workspace.join(".team").join("logs").join("events.jsonl"))
895
+ .unwrap_or_default()
896
+ }
897
+
898
+ fn distinct_owner_ids(db_path: &Path, table: &str) -> Vec<String> {
899
+ let conn = open_db(db_path).unwrap();
900
+ let mut stmt = conn
901
+ .prepare(&format!(
902
+ "select distinct owner_team_id from {table} where owner_team_id is not null order by owner_team_id"
903
+ ))
904
+ .unwrap();
905
+ stmt.query_map([], |row| row.get::<_, String>(0))
906
+ .unwrap()
907
+ .collect::<Result<Vec<_>, _>>()
908
+ .unwrap()
909
+ }
910
+
911
+ fn pragma_user_version(db_path: &Path) -> i64 {
912
+ open_db(db_path)
913
+ .unwrap()
914
+ .query_row("pragma user_version", [], |row| row.get(0))
915
+ .unwrap()
916
+ }
917
+
918
+ fn tmp_dir(tag: &str) -> PathBuf {
919
+ static N: AtomicU64 = AtomicU64::new(0);
920
+ let dir = std::env::temp_dir().join(format!(
921
+ "ta-rs-upgrade-compat-{tag}-{}-{}",
922
+ std::process::id(),
923
+ N.fetch_add(1, Ordering::Relaxed)
924
+ ));
925
+ let _ = std::fs::remove_dir_all(&dir);
926
+ std::fs::create_dir_all(&dir).unwrap();
927
+ std::fs::canonicalize(dir).unwrap()
928
+ }