@team-agent/installer 0.5.61 → 0.5.63

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 (250) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -2
  4. package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +1 -0
  6. package/crates/team-agent/src/cli/emit.rs +41 -7
  7. package/crates/team-agent/src/cli/helpers.rs +1 -0
  8. package/crates/team-agent/src/cli/leader.rs +6 -1
  9. package/crates/team-agent/src/cli/leaders.rs +2 -0
  10. package/crates/team-agent/src/cli/mod.rs +33 -3
  11. package/crates/team-agent/src/cli/named_address.rs +8 -1
  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 +1 -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 +1 -0
  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/tests/verb_profile.rs +38 -0
  25. package/crates/team-agent/src/cli/types.rs +3 -2
  26. package/crates/team-agent/src/communication_mode/mod.rs +1 -0
  27. package/crates/team-agent/src/compiler.rs +1 -0
  28. package/crates/team-agent/src/conpty/backend.rs +1 -0
  29. package/crates/team-agent/src/conpty/mod.rs +1 -0
  30. package/crates/team-agent/src/coordinator/backoff.rs +5 -8
  31. package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
  32. package/crates/team-agent/src/coordinator/health.rs +1 -0
  33. package/crates/team-agent/src/coordinator/mod.rs +2 -2
  34. package/crates/team-agent/src/coordinator/orphan.rs +4 -116
  35. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  36. package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
  37. package/crates/team-agent/src/coordinator/steps/abnormal.rs +15 -866
  38. package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
  39. package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
  40. package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
  41. package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
  42. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
  43. package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
  44. package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
  45. package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +139 -375
  46. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
  47. package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -49
  49. package/crates/team-agent/src/coordinator/tick.rs +23 -40
  50. package/crates/team-agent/src/coordinator/types.rs +3 -176
  51. package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
  52. package/crates/team-agent/src/db/message_store.rs +2 -1
  53. package/crates/team-agent/src/db/migration.rs +5 -4
  54. package/crates/team-agent/src/db/mod.rs +1 -0
  55. package/crates/team-agent/src/db/schema.rs +1 -0
  56. package/crates/team-agent/src/diagnose/comms.rs +2 -1
  57. package/crates/team-agent/src/diagnose/mod.rs +1 -0
  58. package/crates/team-agent/src/diagnose/orphans.rs +1 -0
  59. package/crates/team-agent/src/layout/manager.rs +3 -7
  60. package/crates/team-agent/src/layout/mod.rs +2 -2
  61. package/crates/team-agent/src/layout/overlay.rs +6 -1
  62. package/crates/team-agent/src/layout/placement.rs +1 -0
  63. package/crates/team-agent/src/layout/recovery.rs +3 -0
  64. package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
  65. package/crates/team-agent/src/layout/sessions.rs +8 -1
  66. package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
  67. package/crates/team-agent/src/layout/worker_env.rs +3 -0
  68. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  69. package/crates/team-agent/src/leader/helpers.rs +1 -0
  70. package/crates/team-agent/src/leader/incident.rs +1 -0
  71. package/crates/team-agent/src/leader/lease.rs +27 -22
  72. package/crates/team-agent/src/leader/mod.rs +2 -3
  73. package/crates/team-agent/src/leader/owner_bind.rs +13 -3
  74. package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
  75. package/crates/team-agent/src/leader/rediscover.rs +4 -17
  76. package/crates/team-agent/src/leader/registry.rs +3 -15
  77. package/crates/team-agent/src/leader/start.rs +373 -125
  78. package/crates/team-agent/src/leader/takeover.rs +10 -232
  79. package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
  80. package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
  81. package/crates/team-agent/src/leader/tests/identity.rs +7 -2
  82. package/crates/team-agent/src/leader/tests/idle.rs +0 -193
  83. package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
  84. package/crates/team-agent/src/leader/tests/mod.rs +4 -41
  85. package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
  86. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
  87. package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
  88. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
  89. package/crates/team-agent/src/leader/types.rs +1 -28
  90. package/crates/team-agent/src/lib.rs +3 -0
  91. package/crates/team-agent/src/lifecycle/display.rs +7 -1
  92. package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
  93. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
  95. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
  96. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
  97. package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
  99. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
  100. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -27
  101. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
  102. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
  103. package/crates/team-agent/src/lifecycle/launch.rs +11 -12
  104. package/crates/team-agent/src/lifecycle/mod.rs +2 -1
  105. package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -66
  106. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
  107. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
  108. package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
  109. package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
  110. package/crates/team-agent/src/lifecycle/restart.rs +13 -15
  111. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
  112. package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
  113. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
  114. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
  115. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
  116. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
  117. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
  118. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
  119. package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
  120. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
  121. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
  122. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
  123. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
  124. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
  125. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
  126. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
  127. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
  128. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
  129. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
  130. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
  131. package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
  132. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
  133. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
  134. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
  135. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
  136. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
  137. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
  138. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
  139. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
  140. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
  141. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
  142. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
  143. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
  144. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
  145. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
  146. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
  147. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
  148. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
  149. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
  150. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
  151. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
  152. package/crates/team-agent/src/lifecycle/tests.rs +59 -0
  153. package/crates/team-agent/src/lifecycle/types.rs +0 -9
  154. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  155. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
  156. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
  157. package/crates/team-agent/src/mcp_server/mod.rs +1 -0
  158. package/crates/team-agent/src/mcp_server/normalize.rs +4 -3
  159. package/crates/team-agent/src/mcp_server/tools.rs +1 -0
  160. package/crates/team-agent/src/mcp_server/types.rs +1 -18
  161. package/crates/team-agent/src/mcp_server/wire.rs +5 -5
  162. package/crates/team-agent/src/messaging/activity.rs +1 -0
  163. package/crates/team-agent/src/messaging/address.rs +1 -0
  164. package/crates/team-agent/src/messaging/delivery.rs +4 -3
  165. package/crates/team-agent/src/messaging/helpers.rs +2 -1
  166. package/crates/team-agent/src/messaging/leader_channel.rs +1 -0
  167. package/crates/team-agent/src/messaging/leader_receiver.rs +2 -1
  168. package/crates/team-agent/src/messaging/mod.rs +10 -14
  169. package/crates/team-agent/src/messaging/peers.rs +2 -0
  170. package/crates/team-agent/src/messaging/persist.rs +3 -1
  171. package/crates/team-agent/src/messaging/presentation.rs +2 -1
  172. package/crates/team-agent/src/messaging/results.rs +1 -0
  173. package/crates/team-agent/src/messaging/scheduler.rs +1 -0
  174. package/crates/team-agent/src/messaging/selftest.rs +1 -1
  175. package/crates/team-agent/src/messaging/send.rs +41 -8
  176. package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
  177. package/crates/team-agent/src/messaging/types.rs +2 -2
  178. package/crates/team-agent/src/messaging/watchers.rs +19 -13
  179. package/crates/team-agent/src/model/enums.rs +1 -0
  180. package/crates/team-agent/src/model/errors.rs +1 -0
  181. package/crates/team-agent/src/model/ids.rs +1 -0
  182. package/crates/team-agent/src/model/mod.rs +1 -0
  183. package/crates/team-agent/src/model/name_similarity.rs +1 -0
  184. package/crates/team-agent/src/model/pane_authority_refusal.rs +1 -0
  185. package/crates/team-agent/src/model/paths.rs +1 -0
  186. package/crates/team-agent/src/model/permissions.rs +3 -2
  187. package/crates/team-agent/src/model/routing.rs +1 -0
  188. package/crates/team-agent/src/model/spec.rs +1 -0
  189. package/crates/team-agent/src/model/task_graph.rs +1 -0
  190. package/crates/team-agent/src/model/yaml/tests.rs +1 -0
  191. package/crates/team-agent/src/model/yaml.rs +1 -0
  192. package/crates/team-agent/src/packaging/install.rs +9 -147
  193. package/crates/team-agent/src/packaging/migrate.rs +2 -0
  194. package/crates/team-agent/src/packaging/mod.rs +1 -0
  195. package/crates/team-agent/src/packaging/repair.rs +2 -0
  196. package/crates/team-agent/src/packaging/tests.rs +29 -218
  197. package/crates/team-agent/src/packaging/types.rs +8 -15
  198. package/crates/team-agent/src/platform/argv.rs +4 -0
  199. package/crates/team-agent/src/platform/errors.rs +10 -1
  200. package/crates/team-agent/src/platform/file_lock.rs +4 -157
  201. package/crates/team-agent/src/platform/mod.rs +0 -61
  202. package/crates/team-agent/src/platform/process.rs +1 -0
  203. package/crates/team-agent/src/provider/adapter.rs +1 -0
  204. package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
  205. package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
  206. package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
  207. package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
  208. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
  209. package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
  210. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  211. package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
  212. package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
  213. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
  214. package/crates/team-agent/src/provider/classify.rs +1 -0
  215. package/crates/team-agent/src/provider/faults.rs +1 -26
  216. package/crates/team-agent/src/provider/helpers.rs +1 -0
  217. package/crates/team-agent/src/provider/mod.rs +1 -1
  218. package/crates/team-agent/src/provider/session/capture.rs +1 -4
  219. package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
  220. package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
  221. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
  222. package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
  223. package/crates/team-agent/src/provider/session/mod.rs +2 -4
  224. package/crates/team-agent/src/provider/session/resume.rs +2 -209
  225. package/crates/team-agent/src/provider/session_scan/claude.rs +1 -0
  226. package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
  227. package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
  228. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  229. package/crates/team-agent/src/provider/session_scan.rs +1 -42
  230. package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
  231. package/crates/team-agent/src/provider/types.rs +1 -0
  232. package/crates/team-agent/src/provider/wire.rs +1 -0
  233. package/crates/team-agent/src/state/identity.rs +16 -5
  234. package/crates/team-agent/src/state/identity_keys.rs +1 -0
  235. package/crates/team-agent/src/state/mod.rs +2 -0
  236. package/crates/team-agent/src/state/owner_gate.rs +5 -0
  237. package/crates/team-agent/src/state/ownership.rs +10 -4
  238. package/crates/team-agent/src/state/paths.rs +12 -0
  239. package/crates/team-agent/src/state/persist.rs +9 -3
  240. package/crates/team-agent/src/state/projection.rs +15 -2
  241. package/crates/team-agent/src/state/repository/intent.rs +1 -0
  242. package/crates/team-agent/src/state/repository/tests.rs +1 -0
  243. package/crates/team-agent/src/state/repository.rs +7 -13
  244. package/crates/team-agent/src/state/selector.rs +1 -0
  245. package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
  246. package/crates/team-agent/src/tmux_backend.rs +63 -0
  247. package/crates/team-agent/src/transport.rs +15 -0
  248. package/package.json +4 -4
  249. package/crates/team-agent/src/leader/inject.rs +0 -33
  250. package/crates/team-agent/src/provider/command.rs +0 -80
@@ -0,0 +1,837 @@
1
+ //! task#8 RED contract: tests that exercise Team Agent product side effects
2
+ //! must run inside one HOME/registry/socket/env hermetic boundary.
3
+ //!
4
+ //! References:
5
+ //! - `.team/artifacts/test-isolation-escape-locate.md` §8 R1-R6.
6
+ //! - Slice A: shared `HermeticTestEnv` is implementation-owned; this file keeps
7
+ //! only the minimal hostile harness needed to express the RED contracts.
8
+ //! - Slice B: restart `auto_attach_leader` is product behavior and must write
9
+ //! the same derived host leader registry entry as explicit binding commands.
10
+
11
+ #![cfg(unix)]
12
+ #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
13
+
14
+ use std::collections::BTreeMap;
15
+ use std::path::{Path, PathBuf};
16
+ use std::process::{Command, Output};
17
+ use std::sync::atomic::{AtomicU64, Ordering};
18
+ use std::sync::{Arc, Mutex};
19
+
20
+ use serde_json::{json, Value};
21
+ use serial_test::serial;
22
+ use team_agent::lifecycle::{restart_with_transport_with_readiness_deadline, RestartReport};
23
+ use team_agent::state::persist::{load_runtime_state, save_runtime_state};
24
+ use team_agent::transport::{
25
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
26
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
27
+ SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
28
+ TurnVerification, WindowName,
29
+ };
30
+
31
+ const CALLER_PANE: &str = "%7";
32
+ const TMUX_SOCKET: &str = "/private/tmp/tmux-501/ta-0515-auto-attach";
33
+ const TEAM_KEY: &str = "ctxteam";
34
+ const WORKER: &str = "alpha";
35
+
36
+ #[test]
37
+ fn r1_claim_takeover_contracts_must_enter_hermetic_home_boundary() {
38
+ let source =
39
+ read_repo_file("crates/team-agent/tests/explicit_claim_takeover_any_live_pane_red.rs");
40
+ assert_contains_all(
41
+ "R1",
42
+ &source,
43
+ &[
44
+ "HermeticTestEnv",
45
+ "assert_real_registry_unchanged",
46
+ "registry_entries",
47
+ "HOME",
48
+ ],
49
+ "ta-rs-bug3 claim/takeover contracts run real CARGO_BIN_EXE_team-agent and binding hooks; they must isolate HOME and prove real ~/.team-agent/leaders is unchanged while hermetic HOME gets the expected entries",
50
+ );
51
+ assert!(
52
+ !source.contains("std::env::temp_dir()"),
53
+ "R1: explicit_claim_takeover_any_live_pane_red.rs must allocate workspaces through HermeticTestEnv, not std::env::temp_dir()"
54
+ );
55
+ }
56
+
57
+ #[test]
58
+ fn r2_bug4_canary_contract_must_not_observe_hostile_workspace_or_real_tmux() {
59
+ let source = read_repo_file(
60
+ "crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs",
61
+ );
62
+ assert_contains_all(
63
+ "R2",
64
+ &source,
65
+ &[
66
+ "HermeticTestEnv",
67
+ "TEAM_AGENT_WORKSPACE",
68
+ "assert_store_under_root",
69
+ "assert_path_under_root",
70
+ "RecordingTransport",
71
+ "BUG4 worker to leader canary",
72
+ ],
73
+ "BUG4 delivery contracts use MessageStore/send/deliver primitives; hostile TEAM_AGENT_* and TMUX inputs must be scrubbed, fixture DB rows must stay under the hermetic root, and delivery must use RecordingTransport only",
74
+ );
75
+ assert!(
76
+ !source.contains("fn temp_ws("),
77
+ "R2: verify_rs031_window_consistency_red.rs must not keep a standalone temp_ws helper; workspace allocation must come from HermeticTestEnv"
78
+ );
79
+ }
80
+
81
+ #[test]
82
+ fn r3_socket_escape_contract_requires_declared_offline_or_recording_transport() {
83
+ let verify = read_repo_file(
84
+ "crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs",
85
+ );
86
+ let quick = read_repo_file(
87
+ "crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs",
88
+ );
89
+ let combined = format!("{verify}\n{quick}");
90
+ assert_contains_all(
91
+ "R3",
92
+ &combined,
93
+ &[
94
+ "HermeticTestEnv",
95
+ "scrub_tmux",
96
+ "RecordingTransport",
97
+ "assert_no_real_tmux",
98
+ ],
99
+ "non-real-machine contracts may set hostile TMUX/TMUX_PANE only through HermeticTestEnv and must prove they did not construct a real TmuxBackend/default transport",
100
+ );
101
+ }
102
+
103
+ #[test]
104
+ fn r4_phase_golden_full_order_must_enter_hermetic_before_workspace_creation() {
105
+ let source = read_repo_file("crates/team-agent/src/lifecycle/tests/phase_golden.rs");
106
+ let hermetic = source.find("HermeticTestEnv").unwrap_or_else(|| {
107
+ panic!("R4: phase_golden must import/enter shared HermeticTestEnv before creating test workspaces")
108
+ });
109
+ let workspace = source.find("two_worker_team_dir").unwrap_or_else(|| {
110
+ panic!("R4 setup: phase_golden still expected to create the two-worker fixture")
111
+ });
112
+ assert!(
113
+ hermetic < workspace,
114
+ "R4: phase_golden must enter HermeticTestEnv before two_worker_team_dir/workspace creation so full --lib order cannot inherit prior env/HOME/socket state"
115
+ );
116
+ assert!(
117
+ !source.contains("const CALLER_IDENTITY_ENVS"),
118
+ "R4: phase_golden must not keep a private caller-identity scrub list that can drift from HermeticTestEnv"
119
+ );
120
+ }
121
+
122
+ #[test]
123
+ #[serial(env)]
124
+ fn r5_restart_auto_attach_registers_isolated_live_leader_entry() {
125
+ let case = RestartAutoAttachCase::new("r5-auto-attach");
126
+ let _process_env = case.enter_process_env();
127
+ case.seed_restartable_workspace();
128
+ seed_healthy_coordinator(&case.workspace);
129
+
130
+ let report = restart_with_transport_with_readiness_deadline(
131
+ &case.workspace,
132
+ true,
133
+ Some(TEAM_KEY),
134
+ &RestartAutoAttachTransport::new(),
135
+ Some(1_000),
136
+ )
137
+ .expect("R5 setup: restart should complete against fake transport");
138
+ assert!(
139
+ matches!(report, RestartReport::Restarted { .. }),
140
+ "R5 setup: restart must succeed before checking registry side effects; report={report:?}"
141
+ );
142
+ let state = load_runtime_state(&case.workspace).expect("read state after restart");
143
+ assert_eq!(
144
+ canonical_leader_receiver(&state)
145
+ .and_then(|receiver| receiver.get("status"))
146
+ .and_then(Value::as_str),
147
+ Some("attached"),
148
+ "R5 setup: restart auto_attach must bind canonical leader_receiver before registry assertion; state={state}"
149
+ );
150
+
151
+ let entries = case.registry_entries();
152
+ assert_eq!(
153
+ entries.len(),
154
+ 1,
155
+ "R5: successful restart auto_attach_leader must write exactly one isolated HOME registry entry; canonical binding exists but entries={entries:?}"
156
+ );
157
+ let entry = &entries[0].1;
158
+ assert_eq!(
159
+ entry.get("source").and_then(Value::as_str),
160
+ Some("restart-auto-attach"),
161
+ "R5: restart auto_attach registry entry must identify source=restart-auto-attach; entry={entry}"
162
+ );
163
+ case.assert_no_tmp_registry_files();
164
+
165
+ let leaders = case.run_cli(["leaders", "--json"]);
166
+ let leaders_json = json_output(&leaders, "R5 leaders --json");
167
+ assert!(
168
+ leader_statuses(&leaders_json).iter().any(|(name, status)| {
169
+ (name == TEAM_KEY || name.ends_with(&format!("/{TEAM_KEY}"))) && status == "LIVE"
170
+ }),
171
+ "R5: leaders --json under isolated HOME must list the restart auto-attached leader as LIVE; output={leaders_json} entry={entry}"
172
+ );
173
+ }
174
+
175
+ #[test]
176
+ fn r6_static_guard_rejects_dangerous_tests_without_hermetic_boundary() {
177
+ let synthetic = r#"
178
+ use team_agent::message_store::MessageStore;
179
+ fn fixture() {
180
+ let _ = env!("CARGO_BIN_EXE_team-agent");
181
+ }
182
+ "#;
183
+ assert!(
184
+ static_guard_offenders([(
185
+ "synthetic_missing_hermetic.rs".to_string(),
186
+ synthetic.to_string()
187
+ )])
188
+ .iter()
189
+ .any(|offender| offender.path == "synthetic_missing_hermetic.rs"),
190
+ "R6 setup: synthetic MessageStore/CARGO_BIN_EXE test without HermeticTestEnv must be rejected"
191
+ );
192
+ let manual_cli_isolation = r#"
193
+ use std::process::Command;
194
+ fn run_cli() {
195
+ let mut command = Command::new(crate::lifecycle::tests::test_binary_path());
196
+ command
197
+ .env("HOME", "/tmp/hermetic-home")
198
+ .current_dir("/tmp/hermetic-workspace");
199
+ let _ = std::env::var_os("TEAM_AGENT_TEST_TMP");
200
+ for key in [
201
+ "TMUX",
202
+ "TMUX_PANE",
203
+ "TEAM_AGENT_LEADER_PANE_ID",
204
+ "TEAM_AGENT_LEADER_SESSION_UUID",
205
+ "TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
206
+ "TEAM_AGENT_LEADER_PROVIDER",
207
+ "TEAM_AGENT_MACHINE_FINGERPRINT",
208
+ "TEAM_AGENT_WORKSPACE",
209
+ "TEAM_AGENT_TEAM_ID",
210
+ "TEAM_AGENT_OWNER_TEAM_ID",
211
+ "TEAM_AGENT_ACTIVE_TEAM",
212
+ "TEAM_AGENT_ID",
213
+ ] {
214
+ command.env_remove(key);
215
+ }
216
+ }
217
+ "#;
218
+ assert!(
219
+ static_guard_offenders([(
220
+ "synthetic_manual_cli_isolation.rs".to_string(),
221
+ manual_cli_isolation.to_string()
222
+ )])
223
+ .is_empty(),
224
+ "R6 setup: CLI contracts that manually isolate HOME/current_dir and scrub TMUX/TEAM_AGENT_* envs should satisfy the equivalent isolation marker"
225
+ );
226
+
227
+ let files = dangerous_test_files();
228
+ let offenders = static_guard_offenders(files);
229
+ assert!(
230
+ offenders.is_empty(),
231
+ "R6: any test importing MessageStore/CARGO_BIN_EXE/send/delivery/quick-start/launch/registry/binding CLI surfaces must enter HermeticTestEnv or declare an equivalent real-machine isolation marker. offenders(first 30)={:#?}",
232
+ offenders.into_iter().take(30).collect::<Vec<_>>()
233
+ );
234
+ }
235
+
236
+ fn assert_contains_all(label: &str, source: &str, needles: &[&str], message: &str) {
237
+ let missing = needles
238
+ .iter()
239
+ .copied()
240
+ .filter(|needle| !source.contains(needle))
241
+ .collect::<Vec<_>>();
242
+ assert!(
243
+ missing.is_empty(),
244
+ "{label}: {message}; missing markers={missing:?}"
245
+ );
246
+ }
247
+
248
+ #[allow(dead_code)]
249
+ #[derive(Debug)]
250
+ struct StaticOffender {
251
+ path: String,
252
+ signals: Vec<&'static str>,
253
+ }
254
+
255
+ fn static_guard_offenders(
256
+ files: impl IntoIterator<Item = (String, String)>,
257
+ ) -> Vec<StaticOffender> {
258
+ files
259
+ .into_iter()
260
+ .filter_map(|(path, source)| {
261
+ let signals = dangerous_signals(&source);
262
+ if signals.is_empty() {
263
+ return None;
264
+ }
265
+ let has_hermetic = source.contains("HermeticTestEnv");
266
+ let has_real_machine_isolation = source.contains("real-machine")
267
+ && source.contains("HOME")
268
+ && source.contains("TMUX")
269
+ && source.contains("TEAM_AGENT_WORKSPACE");
270
+ if has_hermetic || has_real_machine_isolation || has_manual_cli_env_isolation(&source) {
271
+ None
272
+ } else {
273
+ Some(StaticOffender { path, signals })
274
+ }
275
+ })
276
+ .collect()
277
+ }
278
+
279
+ fn has_manual_cli_env_isolation(source: &str) -> bool {
280
+ [
281
+ "CARGO_BIN_EXE_team-agent",
282
+ "TEAM_AGENT_TEST_TMP",
283
+ ".env(\"HOME\"",
284
+ ".current_dir(",
285
+ "command.env_remove(key)",
286
+ "\"TMUX\"",
287
+ "\"TMUX_PANE\"",
288
+ "\"TEAM_AGENT_LEADER_PANE_ID\"",
289
+ "\"TEAM_AGENT_LEADER_SESSION_UUID\"",
290
+ "\"TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE\"",
291
+ "\"TEAM_AGENT_LEADER_PROVIDER\"",
292
+ "\"TEAM_AGENT_MACHINE_FINGERPRINT\"",
293
+ "\"TEAM_AGENT_WORKSPACE\"",
294
+ "\"TEAM_AGENT_TEAM_ID\"",
295
+ "\"TEAM_AGENT_OWNER_TEAM_ID\"",
296
+ "\"TEAM_AGENT_ACTIVE_TEAM\"",
297
+ "\"TEAM_AGENT_ID\"",
298
+ ]
299
+ .into_iter()
300
+ .all(|needle| source.contains(needle))
301
+ }
302
+
303
+ fn dangerous_signals(source: &str) -> Vec<&'static str> {
304
+ [
305
+ "MessageStore",
306
+ "send_message",
307
+ "deliver_pending_message",
308
+ "deliver_pending_messages",
309
+ "quick_start_with_transport",
310
+ "launch_with_transport",
311
+ "CARGO_BIN_EXE_team-agent",
312
+ "leader::registry",
313
+ "claim-leader",
314
+ "takeover",
315
+ "attach-leader",
316
+ ]
317
+ .into_iter()
318
+ .filter(|needle| source.contains(needle))
319
+ .collect()
320
+ }
321
+
322
+ fn dangerous_test_files() -> Vec<(String, String)> {
323
+ let root = repo_root();
324
+ let mut files = Vec::new();
325
+ collect_rs_files(
326
+ &root,
327
+ "crates/team-agent/tests",
328
+ |name| name.ends_with("_red.rs") || name.ends_with("_contract.rs"),
329
+ &mut files,
330
+ );
331
+ collect_rs_files(
332
+ &root,
333
+ "crates/team-agent/src/lifecycle/tests",
334
+ |name| name.ends_with(".rs"),
335
+ &mut files,
336
+ );
337
+ files.sort_by(|a, b| a.0.cmp(&b.0));
338
+ files
339
+ }
340
+
341
+ fn read_repo_file(path: &str) -> String {
342
+ std::fs::read_to_string(repo_root().join(path))
343
+ .unwrap_or_else(|error| panic!("read repo file {path}: {error}"))
344
+ }
345
+
346
+ fn repo_root() -> PathBuf {
347
+ Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
348
+ }
349
+
350
+ fn collect_rs_files(
351
+ root: &Path,
352
+ relative_dir: &'static str,
353
+ keep: impl Fn(&str) -> bool,
354
+ out: &mut Vec<(String, String)>,
355
+ ) {
356
+ let dir = root.join(relative_dir);
357
+ let read_dir = std::fs::read_dir(&dir)
358
+ .unwrap_or_else(|error| panic!("read static guard dir {}: {error}", dir.display()));
359
+ for entry in read_dir.filter_map(Result::ok) {
360
+ let path = entry.path();
361
+ let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
362
+ continue;
363
+ };
364
+ if !keep(name) {
365
+ continue;
366
+ }
367
+ let relative = format!("{relative_dir}/{name}");
368
+ let source = std::fs::read_to_string(&path)
369
+ .unwrap_or_else(|error| panic!("read static guard file {relative}: {error}"));
370
+ out.push((relative, source));
371
+ }
372
+ }
373
+
374
+ struct RestartAutoAttachCase {
375
+ root: PathBuf,
376
+ workspace: PathBuf,
377
+ home: PathBuf,
378
+ fake_bin: PathBuf,
379
+ }
380
+
381
+ impl RestartAutoAttachCase {
382
+ fn new(tag: &str) -> Self {
383
+ let root = tmp_dir(tag);
384
+ let workspace = root.join("teamdir");
385
+ let home = root.join("home");
386
+ std::fs::create_dir_all(&home).expect("create isolated HOME");
387
+ std::fs::create_dir_all(workspace.join("agents")).expect("create agents dir");
388
+ std::fs::write(
389
+ workspace.join("TEAM.md"),
390
+ "---\nname: ctxteam\nobjective: restart auto attach registry contract\nprovider: fake\n---\n\nTeam.\n",
391
+ )
392
+ .expect("write TEAM.md");
393
+ std::fs::write(
394
+ workspace.join("agents").join(format!("{WORKER}.md")),
395
+ format!(
396
+ "---\nname: {WORKER}\nrole: Worker {WORKER}\nprovider: fake\nmodel: fake\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nWorker.\n"
397
+ ),
398
+ )
399
+ .expect("write worker role");
400
+ let spec = team_agent::compiler::compile_team(&workspace).expect("compile team");
401
+ std::fs::write(
402
+ workspace.join("team.spec.yaml"),
403
+ team_agent::model::yaml::dumps(&spec),
404
+ )
405
+ .expect("write team.spec.yaml");
406
+ let fake_bin = fake_tmux_bin(&root, &workspace);
407
+ Self {
408
+ root,
409
+ workspace,
410
+ home,
411
+ fake_bin,
412
+ }
413
+ }
414
+
415
+ fn enter_process_env(&self) -> EnvGuard {
416
+ EnvGuard::set([
417
+ ("HOME", Some(self.home.to_string_lossy().to_string())),
418
+ (
419
+ "PATH",
420
+ Some(format!(
421
+ "{}:{}",
422
+ self.fake_bin.display(),
423
+ std::env::var("PATH").unwrap_or_default()
424
+ )),
425
+ ),
426
+ ("TMUX", Some(format!("{TMUX_SOCKET},12345,0"))),
427
+ ("TMUX_PANE", Some(CALLER_PANE.to_string())),
428
+ ("TEAM_AGENT_LEADER_PROVIDER", Some("codex".to_string())),
429
+ (
430
+ "TEAM_AGENT_MACHINE_FINGERPRINT",
431
+ Some("machine-0515-red".to_string()),
432
+ ),
433
+ ("TEAM_AGENT_LEADER_PANE_ID", None),
434
+ ("TEAM_AGENT_LEADER_SESSION_UUID", None),
435
+ ("TEAM_AGENT_WORKSPACE", None),
436
+ ("TEAM_AGENT_TEAM_ID", None),
437
+ ("TEAM_AGENT_OWNER_TEAM_ID", None),
438
+ ("TEAM_AGENT_ACTIVE_TEAM", None),
439
+ ("TEAM_AGENT_ID", None),
440
+ ])
441
+ }
442
+
443
+ fn seed_restartable_workspace(&self) {
444
+ let worker = json!({
445
+ "status": "stopped",
446
+ "provider": "fake",
447
+ "role": format!("Worker {WORKER}"),
448
+ "tools": ["mcp_team"],
449
+ "window": WORKER,
450
+ "spawn_cwd": self.workspace.to_string_lossy().to_string()
451
+ });
452
+ save_runtime_state(
453
+ &self.workspace,
454
+ &json!({
455
+ "active_team_key": TEAM_KEY,
456
+ "team_key": TEAM_KEY,
457
+ "team_dir": self.workspace.to_string_lossy().to_string(),
458
+ "spec_path": self.workspace.join("team.spec.yaml").to_string_lossy().to_string(),
459
+ "session_name": "team-ctxteam",
460
+ "tmux_endpoint": TMUX_SOCKET,
461
+ "tmux_socket": TMUX_SOCKET,
462
+ "agents": { WORKER: worker.clone() },
463
+ "teams": {
464
+ TEAM_KEY: {
465
+ "active_team_key": TEAM_KEY,
466
+ "team_key": TEAM_KEY,
467
+ "team_dir": self.workspace.to_string_lossy().to_string(),
468
+ "spec_path": self.workspace.join("team.spec.yaml").to_string_lossy().to_string(),
469
+ "session_name": "team-ctxteam",
470
+ "tmux_endpoint": TMUX_SOCKET,
471
+ "tmux_socket": TMUX_SOCKET,
472
+ "agents": { WORKER: worker }
473
+ }
474
+ }
475
+ }),
476
+ )
477
+ .expect("seed runtime state");
478
+ }
479
+
480
+ fn registry_entries(&self) -> Vec<(PathBuf, Value)> {
481
+ registry_entries(&self.home)
482
+ }
483
+
484
+ fn assert_no_tmp_registry_files(&self) {
485
+ let dir = self.home.join(".team-agent/leaders");
486
+ let tmp = std::fs::read_dir(&dir)
487
+ .ok()
488
+ .into_iter()
489
+ .flatten()
490
+ .filter_map(Result::ok)
491
+ .map(|entry| entry.path())
492
+ .filter(|path| {
493
+ path.file_name()
494
+ .and_then(|name| name.to_str())
495
+ .is_some_and(|name| name.contains(".tmp") || name.ends_with(".tmp"))
496
+ })
497
+ .collect::<Vec<_>>();
498
+ assert!(
499
+ tmp.is_empty(),
500
+ "R5: registry temp files must not remain: {tmp:?}"
501
+ );
502
+ }
503
+
504
+ fn run_cli<const N: usize>(&self, args: [&str; N]) -> Output {
505
+ Command::new(crate::lifecycle::tests::test_binary_path())
506
+ .args(args)
507
+ .current_dir(&self.workspace)
508
+ .env("HOME", &self.home)
509
+ .env(
510
+ "PATH",
511
+ format!(
512
+ "{}:{}",
513
+ self.fake_bin.display(),
514
+ std::env::var("PATH").unwrap_or_default()
515
+ ),
516
+ )
517
+ .env("TMUX", format!("{TMUX_SOCKET},12345,0"))
518
+ .env("TMUX_PANE", CALLER_PANE)
519
+ .env_remove("TEAM_AGENT_LEADER_PANE_ID")
520
+ .env_remove("TEAM_AGENT_LEADER_SESSION_UUID")
521
+ .env_remove("TEAM_AGENT_WORKSPACE")
522
+ .env_remove("TEAM_AGENT_TEAM_ID")
523
+ .env_remove("TEAM_AGENT_OWNER_TEAM_ID")
524
+ .env_remove("TEAM_AGENT_ACTIVE_TEAM")
525
+ .env_remove("TEAM_AGENT_ID")
526
+ .output()
527
+ .expect("run team-agent CLI")
528
+ }
529
+ }
530
+
531
+ impl Drop for RestartAutoAttachCase {
532
+ fn drop(&mut self) {
533
+ if std::env::var("TEAM_AGENT_KEEP_TEST_TMP").as_deref() != Ok("1") {
534
+ let _ = std::fs::remove_dir_all(&self.root);
535
+ }
536
+ }
537
+ }
538
+
539
+ #[derive(Clone, Default)]
540
+ struct RestartAutoAttachTransport {
541
+ pane_seq: Arc<Mutex<u64>>,
542
+ }
543
+
544
+ impl RestartAutoAttachTransport {
545
+ fn new() -> Self {
546
+ Self::default()
547
+ }
548
+
549
+ fn spawn_result(&self, session: &SessionName, window: &WindowName) -> SpawnResult {
550
+ let mut pane_seq = self.pane_seq.lock().unwrap();
551
+ let pane = format!("%{}", *pane_seq);
552
+ *pane_seq = pane_seq.saturating_add(1);
553
+ SpawnResult {
554
+ pane_id: PaneId::new(pane),
555
+ session: session.clone(),
556
+ window: window.clone(),
557
+ child_pid: Some(4242),
558
+ }
559
+ }
560
+ }
561
+
562
+ impl Transport for RestartAutoAttachTransport {
563
+ fn kind(&self) -> BackendKind {
564
+ BackendKind::Tmux
565
+ }
566
+
567
+ fn spawn_first(
568
+ &self,
569
+ session: &SessionName,
570
+ window: &WindowName,
571
+ _argv: &[String],
572
+ _cwd: &Path,
573
+ _env: &BTreeMap<String, String>,
574
+ ) -> Result<SpawnResult, TransportError> {
575
+ Ok(self.spawn_result(session, window))
576
+ }
577
+
578
+ fn spawn_into(
579
+ &self,
580
+ session: &SessionName,
581
+ window: &WindowName,
582
+ _argv: &[String],
583
+ _cwd: &Path,
584
+ _env: &BTreeMap<String, String>,
585
+ ) -> Result<SpawnResult, TransportError> {
586
+ Ok(self.spawn_result(session, window))
587
+ }
588
+
589
+ fn inject(
590
+ &self,
591
+ _target: &Target,
592
+ _payload: &InjectPayload,
593
+ _submit: Key,
594
+ _bracketed: bool,
595
+ ) -> Result<InjectReport, TransportError> {
596
+ Ok(inject_report())
597
+ }
598
+
599
+ fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
600
+ Ok(())
601
+ }
602
+
603
+ fn capture(
604
+ &self,
605
+ _target: &Target,
606
+ range: CaptureRange,
607
+ ) -> Result<CapturedText, TransportError> {
608
+ Ok(CapturedText {
609
+ text: String::new(),
610
+ range,
611
+ })
612
+ }
613
+
614
+ fn query(&self, _target: &Target, _field: PaneField) -> Result<Option<String>, TransportError> {
615
+ Ok(Some("fake".to_string()))
616
+ }
617
+
618
+ fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
619
+ Ok(PaneLiveness::Live)
620
+ }
621
+
622
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
623
+ Ok(vec![])
624
+ }
625
+
626
+ fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
627
+ Ok(true)
628
+ }
629
+
630
+ fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
631
+ Ok(vec![WindowName::new(WORKER)])
632
+ }
633
+
634
+ fn set_session_env(
635
+ &self,
636
+ _session: &SessionName,
637
+ _key: &str,
638
+ _value: &str,
639
+ ) -> Result<SetEnvOutcome, TransportError> {
640
+ Ok(SetEnvOutcome::Applied)
641
+ }
642
+
643
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
644
+ Ok(())
645
+ }
646
+
647
+ fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
648
+ Ok(())
649
+ }
650
+
651
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
652
+ Ok(AttachOutcome::Attached)
653
+ }
654
+ }
655
+
656
+ fn inject_report() -> InjectReport {
657
+ InjectReport {
658
+ stage_reached: InjectStage::Submit,
659
+ inject_verification: InjectVerification::NoToken,
660
+ submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
661
+ turn_verification: TurnVerification::NotRequired,
662
+ attempts: 1,
663
+ submit_diagnostics: None,
664
+ }
665
+ }
666
+
667
+ fn fake_tmux_bin(root: &Path, cwd: &Path) -> PathBuf {
668
+ let bin = root.join("fake-bin");
669
+ std::fs::create_dir_all(&bin).expect("create fake bin dir");
670
+ let tmux = bin.join("tmux");
671
+ let line = format!(
672
+ "{CALLER_PANE}\tteam-agent-leader-0515\t0\tleader\t0\t/dev/ttys0515\tcodex\t1\t{}\t1\t0\t{}\n",
673
+ cwd.display(),
674
+ std::process::id()
675
+ );
676
+ let script = format!(
677
+ r#"#!/bin/sh
678
+ case " $* " in
679
+ *" list-panes "*) printf '%s' '{line}'; exit 0 ;;
680
+ *" list-sessions "*) printf '%s\n' 'team-agent-leader-0515: 1 windows'; exit 0 ;;
681
+ *" display-message "*) printf '%s\n' '{pid}'; exit 0 ;;
682
+ *" has-session "*) exit 0 ;;
683
+ *) exit 0 ;;
684
+ esac
685
+ "#,
686
+ line = shell_single_quoted_payload(&line),
687
+ pid = std::process::id()
688
+ );
689
+ std::fs::write(&tmux, script).expect("write fake tmux");
690
+ #[cfg(unix)]
691
+ {
692
+ use std::os::unix::fs::PermissionsExt;
693
+ std::fs::set_permissions(&tmux, std::fs::Permissions::from_mode(0o755))
694
+ .expect("chmod fake tmux");
695
+ }
696
+ bin
697
+ }
698
+
699
+ fn registry_entries(home: &Path) -> Vec<(PathBuf, Value)> {
700
+ let dir = home.join(".team-agent/leaders");
701
+ let Ok(read_dir) = std::fs::read_dir(&dir) else {
702
+ return Vec::new();
703
+ };
704
+ let mut entries = read_dir
705
+ .filter_map(Result::ok)
706
+ .map(|entry| entry.path())
707
+ .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("json"))
708
+ .map(|path| {
709
+ let value = serde_json::from_str::<Value>(
710
+ &std::fs::read_to_string(&path).expect("read registry entry"),
711
+ )
712
+ .expect("parse registry json");
713
+ (path, value)
714
+ })
715
+ .collect::<Vec<_>>();
716
+ entries.sort_by(|a, b| a.0.cmp(&b.0));
717
+ entries
718
+ }
719
+
720
+ fn leader_statuses(value: &Value) -> Vec<(String, String)> {
721
+ value
722
+ .get("leaders")
723
+ .or_else(|| value.get("entries"))
724
+ .and_then(Value::as_array)
725
+ .into_iter()
726
+ .flatten()
727
+ .map(|entry| {
728
+ let name = entry
729
+ .get("name")
730
+ .or_else(|| entry.get("qualified_name"))
731
+ .or_else(|| entry.get("team_key"))
732
+ .and_then(Value::as_str)
733
+ .unwrap_or("")
734
+ .to_string();
735
+ let status = entry
736
+ .get("status")
737
+ .and_then(Value::as_str)
738
+ .unwrap_or("")
739
+ .to_string();
740
+ (name, status)
741
+ })
742
+ .collect()
743
+ }
744
+
745
+ fn canonical_leader_receiver<'a>(state: &'a Value) -> Option<&'a Value> {
746
+ state
747
+ .pointer(&format!("/teams/{TEAM_KEY}/leader_receiver"))
748
+ .or_else(|| state.get("leader_receiver"))
749
+ }
750
+
751
+ fn json_output(output: &Output, label: &str) -> Value {
752
+ let stdout = String::from_utf8_lossy(&output.stdout);
753
+ let stderr = String::from_utf8_lossy(&output.stderr);
754
+ assert!(
755
+ output.status.success(),
756
+ "{label}: expected success; code={:?} stdout={stdout} stderr={stderr}",
757
+ output.status.code()
758
+ );
759
+ serde_json::from_str(stdout.trim()).unwrap_or_else(|error| {
760
+ panic!("{label}: parse JSON failed: {error}; stdout={stdout:?}; stderr={stderr:?}")
761
+ })
762
+ }
763
+
764
+ fn seed_healthy_coordinator(workspace: &Path) {
765
+ let workspace_path = team_agent::coordinator::WorkspacePath::new(workspace.to_path_buf());
766
+ std::fs::create_dir_all(team_agent::model::paths::runtime_dir(workspace)).unwrap();
767
+ let _ = team_agent::message_store::MessageStore::open(workspace).unwrap();
768
+ let pid = team_agent::coordinator::Pid::new(std::process::id());
769
+ team_agent::coordinator::write_coordinator_metadata(
770
+ &workspace_path,
771
+ pid,
772
+ team_agent::coordinator::MetadataSource::Boot,
773
+ )
774
+ .unwrap();
775
+ std::fs::write(
776
+ team_agent::coordinator::coordinator_pid_path(&workspace_path),
777
+ pid.to_string(),
778
+ )
779
+ .unwrap();
780
+ }
781
+
782
+ struct EnvGuard {
783
+ previous: Vec<(&'static str, Option<String>)>,
784
+ }
785
+
786
+ impl EnvGuard {
787
+ fn set<const N: usize>(values: [(&'static str, Option<String>); N]) -> Self {
788
+ let previous = values
789
+ .iter()
790
+ .map(|(key, _)| (*key, std::env::var(key).ok()))
791
+ .collect::<Vec<_>>();
792
+ for (key, value) in values {
793
+ unsafe {
794
+ if let Some(value) = value {
795
+ std::env::set_var(key, value);
796
+ } else {
797
+ std::env::remove_var(key);
798
+ }
799
+ }
800
+ }
801
+ Self { previous }
802
+ }
803
+ }
804
+
805
+ impl Drop for EnvGuard {
806
+ fn drop(&mut self) {
807
+ for (key, value) in self.previous.drain(..).rev() {
808
+ unsafe {
809
+ if let Some(value) = value {
810
+ std::env::set_var(key, value);
811
+ } else {
812
+ std::env::remove_var(key);
813
+ }
814
+ }
815
+ }
816
+ }
817
+ }
818
+
819
+ fn tmp_dir(tag: &str) -> PathBuf {
820
+ static COUNTER: AtomicU64 = AtomicU64::new(0);
821
+ let root = std::env::var_os("TEAM_AGENT_TEST_TMP")
822
+ .map(PathBuf::from)
823
+ .unwrap_or_else(std::env::temp_dir);
824
+ std::fs::create_dir_all(&root).expect("create TEAM_AGENT_TEST_TMP root");
825
+ let dir = root.join(format!(
826
+ "ta-0515-{tag}-{}-{}",
827
+ std::process::id(),
828
+ COUNTER.fetch_add(1, Ordering::Relaxed)
829
+ ));
830
+ let _ = std::fs::remove_dir_all(&dir);
831
+ std::fs::create_dir_all(&dir).expect("create temp dir");
832
+ std::fs::canonicalize(dir).expect("canonicalize temp dir")
833
+ }
834
+
835
+ fn shell_single_quoted_payload(text: &str) -> String {
836
+ text.replace('\'', "'\\''")
837
+ }