@team-agent/installer 0.5.41 → 0.5.43

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 (152) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +129 -54
  4. package/crates/team-agent/src/cli/diagnose.rs +1 -2
  5. package/crates/team-agent/src/cli/emit.rs +1 -7
  6. package/crates/team-agent/src/cli/helpers.rs +3 -1
  7. package/crates/team-agent/src/cli/leader.rs +2 -1
  8. package/crates/team-agent/src/cli/mod.rs +27 -7
  9. package/crates/team-agent/src/cli/named_address.rs +14 -5
  10. package/crates/team-agent/src/cli/profile.rs +19 -7
  11. package/crates/team-agent/src/cli/send.rs +9 -3
  12. package/crates/team-agent/src/cli/status.rs +8 -30
  13. package/crates/team-agent/src/cli/status_port.rs +1341 -1272
  14. package/crates/team-agent/src/cli/tests/base.rs +738 -660
  15. package/crates/team-agent/src/cli/tests/compile.rs +45 -18
  16. package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
  21. package/crates/team-agent/src/cli/tests/mod.rs +17 -7
  22. package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
  23. package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
  25. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
  26. package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
  27. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
  28. package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
  29. package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
  30. package/crates/team-agent/src/codex_app_server.rs +2 -5
  31. package/crates/team-agent/src/compiler/tests.rs +139 -33
  32. package/crates/team-agent/src/compiler.rs +55 -22
  33. package/crates/team-agent/src/conpty/backend.rs +23 -33
  34. package/crates/team-agent/src/coordinator/backoff.rs +2 -7
  35. package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
  36. package/crates/team-agent/src/coordinator/health.rs +46 -31
  37. package/crates/team-agent/src/coordinator/mod.rs +3 -3
  38. package/crates/team-agent/src/coordinator/orphan.rs +22 -10
  39. package/crates/team-agent/src/coordinator/steps/abnormal.rs +47 -52
  40. package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
  41. package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
  42. package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
  43. package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
  44. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
  45. package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
  46. package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
  47. package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
  49. package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
  50. package/crates/team-agent/src/coordinator/tick.rs +19 -1
  51. package/crates/team-agent/src/db/message_store.rs +138 -30
  52. package/crates/team-agent/src/db/migration.rs +249 -61
  53. package/crates/team-agent/src/db/schema.rs +303 -82
  54. package/crates/team-agent/src/diagnose/comms.rs +9 -2
  55. package/crates/team-agent/src/diagnose/mod.rs +1 -3
  56. package/crates/team-agent/src/diagnose/orphans.rs +79 -61
  57. package/crates/team-agent/src/event_log.rs +70 -16
  58. package/crates/team-agent/src/layout/manager.rs +15 -4
  59. package/crates/team-agent/src/layout/mod.rs +4 -4
  60. package/crates/team-agent/src/layout/overlay.rs +10 -3
  61. package/crates/team-agent/src/layout/placement.rs +5 -1
  62. package/crates/team-agent/src/layout/recovery.rs +4 -2
  63. package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
  64. package/crates/team-agent/src/layout/sessions.rs +17 -9
  65. package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
  66. package/crates/team-agent/src/layout/worker_env.rs +52 -19
  67. package/crates/team-agent/src/leader/helpers.rs +7 -1
  68. package/crates/team-agent/src/leader/lease.rs +195 -82
  69. package/crates/team-agent/src/leader/owner_bind.rs +55 -22
  70. package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
  71. package/crates/team-agent/src/leader/rediscover.rs +74 -25
  72. package/crates/team-agent/src/leader/start.rs +75 -54
  73. package/crates/team-agent/src/leader/takeover.rs +46 -11
  74. package/crates/team-agent/src/leader/tests/basics.rs +320 -167
  75. package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
  76. package/crates/team-agent/src/leader/tests/identity.rs +428 -356
  77. package/crates/team-agent/src/leader/tests/idle.rs +285 -254
  78. package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
  79. package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
  80. package/crates/team-agent/src/leader/tests/mod.rs +115 -99
  81. package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
  82. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
  83. package/crates/team-agent/src/lib.rs +4 -4
  84. package/crates/team-agent/src/lifecycle/display.rs +7 -3
  85. package/crates/team-agent/src/lifecycle/launch.rs +6 -1
  86. package/crates/team-agent/src/lifecycle/mod.rs +9 -1
  87. package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
  88. package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
  89. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
  90. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
  91. package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
  92. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
  93. package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
  94. package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
  95. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
  96. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
  97. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
  98. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
  99. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
  100. package/crates/team-agent/src/lifecycle/tests.rs +2 -2
  101. package/crates/team-agent/src/main.rs +4 -4
  102. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
  103. package/crates/team-agent/src/mcp_server/mod.rs +11 -2
  104. package/crates/team-agent/src/mcp_server/types.rs +14 -3
  105. package/crates/team-agent/src/mcp_server/wire.rs +230 -68
  106. package/crates/team-agent/src/messaging/delivery.rs +20 -18
  107. package/crates/team-agent/src/messaging/helpers.rs +25 -4
  108. package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
  109. package/crates/team-agent/src/messaging/mod.rs +2 -3
  110. package/crates/team-agent/src/messaging/selftest.rs +46 -26
  111. package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
  112. package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
  113. package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
  114. package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
  115. package/crates/team-agent/src/messaging/trust.rs +25 -2
  116. package/crates/team-agent/src/messaging/watchers.rs +37 -9
  117. package/crates/team-agent/src/model/enums.rs +65 -16
  118. package/crates/team-agent/src/model/ids.rs +12 -3
  119. package/crates/team-agent/src/model/paths.rs +28 -7
  120. package/crates/team-agent/src/model/permissions.rs +176 -33
  121. package/crates/team-agent/src/model/routing.rs +66 -20
  122. package/crates/team-agent/src/model/spec.rs +365 -69
  123. package/crates/team-agent/src/model/task_graph.rs +36 -9
  124. package/crates/team-agent/src/model/yaml/tests.rs +24 -6
  125. package/crates/team-agent/src/model/yaml.rs +7 -6
  126. package/crates/team-agent/src/packaging/install.rs +23 -9
  127. package/crates/team-agent/src/packaging/migrate.rs +5 -7
  128. package/crates/team-agent/src/packaging/mod.rs +9 -1
  129. package/crates/team-agent/src/packaging/repair.rs +13 -6
  130. package/crates/team-agent/src/packaging/tests.rs +63 -16
  131. package/crates/team-agent/src/packaging/types.rs +22 -7
  132. package/crates/team-agent/src/platform/argv.rs +4 -1
  133. package/crates/team-agent/src/platform/file_lock.rs +22 -8
  134. package/crates/team-agent/src/platform/process.rs +21 -22
  135. package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
  136. package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
  137. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
  138. package/crates/team-agent/src/provider/classify.rs +127 -42
  139. package/crates/team-agent/src/provider/faults.rs +17 -5
  140. package/crates/team-agent/src/provider/helpers.rs +6 -5
  141. package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
  142. package/crates/team-agent/src/state/repository.rs +27 -14
  143. package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
  144. package/crates/team-agent/src/tmux_backend.rs +80 -44
  145. package/crates/team-agent/src/topology.rs +27 -20
  146. package/crates/team-agent/src/transport/test_support.rs +20 -23
  147. package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
  148. package/crates/team-agent/src/transport/tests/mod.rs +178 -187
  149. package/crates/team-agent/src/transport/tests/wire.rs +561 -525
  150. package/crates/team-agent/src/transport.rs +12 -17
  151. package/crates/team-agent/src/transport_factory.rs +29 -14
  152. package/package.json +4 -4
@@ -1,392 +1,464 @@
1
1
  use super::*;
2
2
 
3
- // =====================================================================
4
- // 12. leader_identity_context — override / state uuid / derive 三源(unimplemented → RED)
5
- // =====================================================================
3
+ // =====================================================================
4
+ // 12. leader_identity_context — override / state uuid / derive 三源(unimplemented → RED)
5
+ // =====================================================================
6
6
 
7
- // 无 override env、无 state record → derive(machine, ws_abspath, user, team)。
8
- // 现 unimplemented → 调用即 RED;锁住返回 LeaderIdentity 且 source==Derived。
9
- #[test]
10
- #[serial_test::serial(env)]
11
- fn leader_identity_context_derives_when_no_override_no_state() {
12
- let ws = std::env::temp_dir().join(format!("ta_rs_lic_{}", std::process::id()));
13
- std::fs::create_dir_all(&ws).unwrap();
14
- // 空 state(无 team_owner/leader_receiver uuid)。
15
- let state = serde_json::json!({});
16
- let id = leader_identity_context(&ws, None, Some(&state)).unwrap();
17
- // 无 override → source 为 Derived(leader plan 侧;__init__.py:206)。
18
- assert_eq!(id.leader_session_uuid_source, LeaderSessionUuidSource::Derived);
19
- // uuid 是 32 hex(derive 形)。
20
- assert_eq!(id.leader_session_uuid.as_str().len(), 32);
21
- }
7
+ // 无 override env、无 state record → derive(machine, ws_abspath, user, team)。
8
+ // 现 unimplemented → 调用即 RED;锁住返回 LeaderIdentity 且 source==Derived。
9
+ #[test]
10
+ #[serial_test::serial(env)]
11
+ fn leader_identity_context_derives_when_no_override_no_state() {
12
+ let ws = std::env::temp_dir().join(format!("ta_rs_lic_{}", std::process::id()));
13
+ std::fs::create_dir_all(&ws).unwrap();
14
+ // 空 state(无 team_owner/leader_receiver uuid)。
15
+ let state = serde_json::json!({});
16
+ let id = leader_identity_context(&ws, None, Some(&state)).unwrap();
17
+ // 无 override → source 为 Derived(leader plan 侧;__init__.py:206)。
18
+ assert_eq!(
19
+ id.leader_session_uuid_source,
20
+ LeaderSessionUuidSource::Derived
21
+ );
22
+ // uuid 是 32 hex(derive 形)。
23
+ assert_eq!(id.leader_session_uuid.as_str().len(), 32);
24
+ }
22
25
 
23
- // leader_identity:CLI 直出 dict(__init__.py:355-369)。unimplemented → RED。
24
- // 强化:uuid_prefix 必须 == leader_identity_context 派生 uuid 的前 12 hex(绑到真值,
25
- // 而非任意 12 字符);整个 dict 的 machine_fingerprint/os_user/team_id/source 必须与 context 一致。
26
- #[test]
27
- #[serial_test::serial(env)]
28
- fn leader_identity_dict_ties_prefix_and_fields_to_derived_context() {
29
- let ws = std::env::temp_dir().join(format!("ta_rs_lid_{}", std::process::id()));
30
- std::fs::create_dir_all(&ws).unwrap();
31
- // 空 state → context 走 derive(无 override/无 state uuid)。
32
- let ctx = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
33
- let expected_prefix = &ctx.leader_session_uuid.as_str()[..12];
34
- let v = leader_identity(&ws, None).unwrap();
35
- assert_eq!(v["ok"], serde_json::json!(true));
36
- // uuid_prefix 绑到派生真值的前 12 hex(错的 12 字符串会被抓)。
37
- assert_eq!(v["uuid_prefix"].as_str().unwrap(), expected_prefix);
38
- // 其余身份字段与 context 字节一致。
39
- assert_eq!(v["machine_fingerprint"].as_str().unwrap(), ctx.machine_fingerprint);
40
- assert_eq!(v["os_user"].as_str().unwrap(), ctx.os_user);
41
- assert_eq!(v["team_id"].as_str().unwrap(), ctx.team_id.as_str());
42
- // source == 派生侧 "derived"(无 override → 不是 "override"/"env")
43
- assert_eq!(v["source"], serde_json::json!("derived"));
44
- assert_eq!(ctx.leader_session_uuid_source, LeaderSessionUuidSource::Derived);
45
- // CLI 直出形态:current_pane_id / last_seen_at 在无 env/无 receiver 时为 null
46
- assert_eq!(v["last_seen_at"], serde_json::Value::Null);
47
- }
26
+ // leader_identity:CLI 直出 dict(__init__.py:355-369)。unimplemented → RED。
27
+ // 强化:uuid_prefix 必须 == leader_identity_context 派生 uuid 的前 12 hex(绑到真值,
28
+ // 而非任意 12 字符);整个 dict 的 machine_fingerprint/os_user/team_id/source 必须与 context 一致。
29
+ #[test]
30
+ #[serial_test::serial(env)]
31
+ fn leader_identity_dict_ties_prefix_and_fields_to_derived_context() {
32
+ let ws = std::env::temp_dir().join(format!("ta_rs_lid_{}", std::process::id()));
33
+ std::fs::create_dir_all(&ws).unwrap();
34
+ // 空 state → context 走 derive(无 override/无 state uuid)。
35
+ let ctx = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
36
+ let expected_prefix = &ctx.leader_session_uuid.as_str()[..12];
37
+ let v = leader_identity(&ws, None).unwrap();
38
+ assert_eq!(v["ok"], serde_json::json!(true));
39
+ // uuid_prefix 绑到派生真值的前 12 hex(错的 12 字符串会被抓)。
40
+ assert_eq!(v["uuid_prefix"].as_str().unwrap(), expected_prefix);
41
+ // 其余身份字段与 context 字节一致。
42
+ assert_eq!(
43
+ v["machine_fingerprint"].as_str().unwrap(),
44
+ ctx.machine_fingerprint
45
+ );
46
+ assert_eq!(v["os_user"].as_str().unwrap(), ctx.os_user);
47
+ assert_eq!(v["team_id"].as_str().unwrap(), ctx.team_id.as_str());
48
+ // source == 派生侧 "derived"(无 override 不是 "override"/"env")
49
+ assert_eq!(v["source"], serde_json::json!("derived"));
50
+ assert_eq!(
51
+ ctx.leader_session_uuid_source,
52
+ LeaderSessionUuidSource::Derived
53
+ );
54
+ // CLI 直出形态:current_pane_id / last_seen_at 在无 env/无 receiver 时为 null。
55
+ assert_eq!(v["last_seen_at"], serde_json::Value::Null);
56
+ }
48
57
 
49
- // =====================================================================
50
- // 13. leader_start_plan(unimplemented → RED):钉 mode 选择 + leader_env 导出键。
51
- // =====================================================================
58
+ // =====================================================================
59
+ // 13. leader_start_plan(unimplemented → RED):钉 mode 选择 + leader_env 导出键。
60
+ // =====================================================================
52
61
 
53
- // leader_start_plan(__init__.py:82-145)。强化:钉具体 mode + plan 内容,而非 provider 回声。
54
- // 确定性环境:在 TMUX 内 → exec_provider;不在 TMUX(且 tmux 可用)→ new_tmux_session,
55
- // session_name==leader_session_name(Fake,ws),leader_env 携带 5 个 TEAM_AGENT_* 导出键。
56
- // 注:`detached` 在 leader_start_plan 返回值里恒为 false(__init__.py:174 "detached": False);
57
- // 非 tty 的 `-d` 插入发生在 start_leader 调用者层(:74-78),不在本 plan 边界 → 不在此断言 detached。
58
- // unimplemented → RED。
59
- #[test]
60
- #[serial_test::serial(env)]
61
- fn leader_start_plan_pins_mode_and_leader_env() {
62
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
63
- let _e = EnvGuard::apply(&[("TMUX", None), ("TMUX_PANE", None)]);
64
- let ws = std::env::temp_dir().join(format!("ta_rs_lsp_{}", std::process::id()));
65
- std::fs::create_dir_all(&ws).unwrap();
66
- let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None, false).unwrap();
67
- assert_eq!(plan.provider, Provider::Fake);
68
- assert_eq!(plan.mode, LeaderStartMode::ManagedTmuxClient);
69
- // 0.3.28 Step 2: managed mode now uses the dedicated leader session
70
- // (`team-agent-leader-<provider>-<folder>-<sha1[:8]>`), not the worker
71
- // session `team-<team_id>`. Python parity.
72
- let session_name = plan.session_name.as_ref().map(SessionName::as_str).unwrap_or("");
73
- assert!(
74
- session_name.starts_with(crate::layout::sessions::LEADER_SESSION_PREFIX),
75
- "managed mode must use dedicated leader session prefix; got `{session_name}`"
76
- );
77
- // 0.3.28 Step 2: window name = provider wire (`fake`), not `leader`.
78
- assert_eq!(
79
- plan.leader_window.as_ref().map(WindowName::as_str),
80
- Some("fake")
81
- );
82
- assert!(!plan.is_external_leader);
62
+ // leader_start_plan(__init__.py:82-145)。强化:钉具体 mode + plan 内容,而非 provider 回声。
63
+ // 确定性环境:在 TMUX 内 → exec_provider;不在 TMUX(且 tmux 可用)→ new_tmux_session,
64
+ // session_name==leader_session_name(Fake,ws),leader_env 携带 5 个 TEAM_AGENT_* 导出键。
65
+ // 注:`detached` 在 leader_start_plan 返回值里恒为 false(__init__.py:174 "detached": False);
66
+ // 非 tty 的 `-d` 插入发生在 start_leader 调用者层(:74-78),不在本 plan 边界 → 不在此断言 detached。
67
+ // unimplemented → RED。
68
+ #[test]
69
+ #[serial_test::serial(env)]
70
+ fn leader_start_plan_pins_mode_and_leader_env() {
71
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
72
+ let _e = EnvGuard::apply(&[("TMUX", None), ("TMUX_PANE", None)]);
73
+ let ws = std::env::temp_dir().join(format!("ta_rs_lsp_{}", std::process::id()));
74
+ std::fs::create_dir_all(&ws).unwrap();
75
+ let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None, false).unwrap();
76
+ assert_eq!(plan.provider, Provider::Fake);
77
+ assert_eq!(plan.mode, LeaderStartMode::ManagedTmuxClient);
78
+ // 0.3.28 Step 2: managed mode now uses the dedicated leader session
79
+ // (`team-agent-leader-<provider>-<folder>-<sha1[:8]>`), not the worker
80
+ // session `team-<team_id>`. Python parity.
81
+ let session_name = plan
82
+ .session_name
83
+ .as_ref()
84
+ .map(SessionName::as_str)
85
+ .unwrap_or("");
86
+ assert!(
87
+ session_name.starts_with(crate::layout::sessions::LEADER_SESSION_PREFIX),
88
+ "managed mode must use dedicated leader session prefix; got `{session_name}`"
89
+ );
90
+ // 0.3.28 Step 2: window name = provider wire (`fake`), not `leader`.
91
+ assert_eq!(
92
+ plan.leader_window.as_ref().map(WindowName::as_str),
93
+ Some("fake")
94
+ );
95
+ assert!(!plan.is_external_leader);
96
+ assert!(
97
+ plan.argv.iter().any(|arg| arg == "attach-session"),
98
+ "no-tmux managed launch attaches the user client to the leader window: {:?}",
99
+ plan.argv
100
+ );
101
+ // plan 边界 detached 恒 false(`-d` 插入在 start_leader 层,非此处)。
102
+ assert!(!plan.detached, "leader_start_plan 返回值 detached 恒 false");
103
+ // leader_env 携带 5 个 TEAM_AGENT_* 导出键(_leader_provider_env)。
104
+ for key in [
105
+ "TEAM_AGENT_LEADER_PROVIDER",
106
+ "TEAM_AGENT_LEADER_SESSION_UUID",
107
+ "TEAM_AGENT_MACHINE_FINGERPRINT",
108
+ "TEAM_AGENT_WORKSPACE",
109
+ "TEAM_AGENT_TEAM_ID",
110
+ ] {
83
111
  assert!(
84
- plan.argv.iter().any(|arg| arg == "attach-session"),
85
- "no-tmux managed launch attaches the user client to the leader window: {:?}",
86
- plan.argv
87
- );
88
- // plan 边界 detached 恒 false(`-d` 插入在 start_leader 层,非此处)。
89
- assert!(!plan.detached, "leader_start_plan 返回值 detached 恒 false");
90
- // leader_env 携带 5 个 TEAM_AGENT_* 导出键(_leader_provider_env)。
91
- for key in [
92
- "TEAM_AGENT_LEADER_PROVIDER",
93
- "TEAM_AGENT_LEADER_SESSION_UUID",
94
- "TEAM_AGENT_MACHINE_FINGERPRINT",
95
- "TEAM_AGENT_WORKSPACE",
96
- "TEAM_AGENT_TEAM_ID",
97
- ] {
98
- assert!(plan.leader_env.contains_key(key), "leader_env 缺导出键 {key}");
99
- }
100
- assert_eq!(
101
- plan.leader_env.get("TEAM_AGENT_LEADER_PROVIDER").map(String::as_str),
102
- Some("fake")
112
+ plan.leader_env.contains_key(key),
113
+ "leader_env 缺导出键 {key}"
103
114
  );
104
115
  }
116
+ assert_eq!(
117
+ plan.leader_env
118
+ .get("TEAM_AGENT_LEADER_PROVIDER")
119
+ .map(String::as_str),
120
+ Some("fake")
121
+ );
122
+ }
105
123
 
106
- #[test]
107
- #[serial_test::serial(env)]
108
- fn leader_start_plan_external_leader_keeps_exec_provider_in_tmux() {
109
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
110
- let _e = EnvGuard::apply(&[("TMUX", Some("/private/tmp/tmux-501/default,88432,187"))]);
111
- let ws = std::env::temp_dir().join(format!("ta_rs_lsp_external_{}", std::process::id()));
112
- std::fs::create_dir_all(&ws).unwrap();
124
+ #[test]
125
+ #[serial_test::serial(env)]
126
+ fn leader_start_plan_external_leader_keeps_exec_provider_in_tmux() {
127
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
128
+ let _e = EnvGuard::apply(&[("TMUX", Some("/private/tmp/tmux-501/default,88432,187"))]);
129
+ let ws = std::env::temp_dir().join(format!("ta_rs_lsp_external_{}", std::process::id()));
130
+ std::fs::create_dir_all(&ws).unwrap();
113
131
 
114
- let provider_args = vec!["--".to_string(), "--model".to_string(), "opus".to_string()];
115
- let plan = leader_start_plan(Provider::Fake, &provider_args, &ws, false, false, None, true).unwrap();
132
+ let provider_args = vec!["--".to_string(), "--model".to_string(), "opus".to_string()];
133
+ let plan = leader_start_plan(
134
+ Provider::Fake,
135
+ &provider_args,
136
+ &ws,
137
+ false,
138
+ false,
139
+ None,
140
+ true,
141
+ )
142
+ .unwrap();
116
143
 
117
- assert_eq!(plan.mode, LeaderStartMode::ExecProvider);
118
- assert!(plan.is_external_leader);
119
- assert_eq!(plan.leader_window, None);
120
- assert_eq!(
121
- plan.argv,
122
- vec!["fake".to_string(), "--model".to_string(), "opus".to_string()]
123
- );
124
- assert_eq!(
125
- plan.provider_argv,
126
- vec!["fake".to_string(), "--model".to_string(), "opus".to_string()]
127
- );
128
- }
144
+ assert_eq!(plan.mode, LeaderStartMode::ExecProvider);
145
+ assert!(plan.is_external_leader);
146
+ assert_eq!(plan.leader_window, None);
147
+ assert_eq!(
148
+ plan.argv,
149
+ vec![
150
+ "fake".to_string(),
151
+ "--model".to_string(),
152
+ "opus".to_string()
153
+ ]
154
+ );
155
+ assert_eq!(
156
+ plan.provider_argv,
157
+ vec![
158
+ "fake".to_string(),
159
+ "--model".to_string(),
160
+ "opus".to_string()
161
+ ]
162
+ );
163
+ }
129
164
 
130
- #[test]
131
- #[serial_test::serial(env)]
132
- fn managed_leader_uses_dedicated_leader_session_independent_of_state_session_name() {
133
- // 0.3.28 Step 2 amendment: the persisted `session_name` is the WORKER
134
- // session, not the leader session. Managed leader now ignores it and
135
- // always uses the dedicated `leader_session_name(provider, workspace)`.
136
- // This test pins the new behaviour and the regression guard that the
137
- // managed argv never gets a `team-team-*` double prefix.
138
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
139
- let _e = EnvGuard::apply(&[("TMUX", None), ("TMUX_PANE", None)]);
140
- let ws = std::env::temp_dir().join(format!("ta_rs_lsp_existing_{}", std::process::id()));
141
- std::fs::create_dir_all(&ws).unwrap();
142
- crate::state::persist::save_runtime_state(
143
- &ws,
144
- &serde_json::json!({"session_name": "team-alpha"}),
145
- )
146
- .unwrap();
165
+ #[test]
166
+ #[serial_test::serial(env)]
167
+ fn managed_leader_uses_dedicated_leader_session_independent_of_state_session_name() {
168
+ // 0.3.28 Step 2 amendment: the persisted `session_name` is the WORKER
169
+ // session, not the leader session. Managed leader now ignores it and
170
+ // always uses the dedicated `leader_session_name(provider, workspace)`.
171
+ // This test pins the new behaviour and the regression guard that the
172
+ // managed argv never gets a `team-team-*` double prefix.
173
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
174
+ let _e = EnvGuard::apply(&[("TMUX", None), ("TMUX_PANE", None)]);
175
+ let ws = std::env::temp_dir().join(format!("ta_rs_lsp_existing_{}", std::process::id()));
176
+ std::fs::create_dir_all(&ws).unwrap();
177
+ crate::state::persist::save_runtime_state(
178
+ &ws,
179
+ &serde_json::json!({"session_name": "team-alpha"}),
180
+ )
181
+ .unwrap();
147
182
 
148
- let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None, false).unwrap();
183
+ let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None, false).unwrap();
149
184
 
150
- assert_eq!(plan.mode, LeaderStartMode::ManagedTmuxClient);
151
- let session_name = plan.session_name.as_ref().map(SessionName::as_str).unwrap_or("");
152
- assert!(
153
- session_name.starts_with(crate::layout::sessions::LEADER_SESSION_PREFIX),
154
- "managed mode uses dedicated leader session prefix regardless of \
185
+ assert_eq!(plan.mode, LeaderStartMode::ManagedTmuxClient);
186
+ let session_name = plan
187
+ .session_name
188
+ .as_ref()
189
+ .map(SessionName::as_str)
190
+ .unwrap_or("");
191
+ assert!(
192
+ session_name.starts_with(crate::layout::sessions::LEADER_SESSION_PREFIX),
193
+ "managed mode uses dedicated leader session prefix regardless of \
155
194
  persisted worker session_name; got `{session_name}`"
156
- );
157
- assert!(
158
- !plan.argv.iter().any(|arg| arg.contains("team-team-alpha")),
159
- "managed session name must not gain a second team- prefix: {:?}",
160
- plan.argv
161
- );
162
- }
195
+ );
196
+ assert!(
197
+ !plan.argv.iter().any(|arg| arg.contains("team-team-alpha")),
198
+ "managed session name must not gain a second team- prefix: {:?}",
199
+ plan.argv
200
+ );
201
+ }
163
202
 
164
- #[test]
165
- #[serial_test::serial(env)]
166
- fn in_tmux_default_leader_runs_provider_in_current_pane() {
167
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
168
- let ws = std::env::temp_dir().join(format!("ta_rs_lsp_switch_{}", std::process::id()));
169
- std::fs::create_dir_all(&ws).unwrap();
170
- let socket = crate::tmux_backend::socket_name_for_workspace(&ws);
171
- let endpoint = format!("/private/tmp/tmux-501/{socket},88432,187");
172
- let _e = EnvGuard::apply(&[("TMUX", Some(&endpoint)), ("TMUX_PANE", Some("%7"))]);
203
+ #[test]
204
+ #[serial_test::serial(env)]
205
+ fn in_tmux_default_leader_runs_provider_in_current_pane() {
206
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
207
+ let ws = std::env::temp_dir().join(format!("ta_rs_lsp_switch_{}", std::process::id()));
208
+ std::fs::create_dir_all(&ws).unwrap();
209
+ let socket = crate::tmux_backend::socket_name_for_workspace(&ws);
210
+ let endpoint = format!("/private/tmp/tmux-501/{socket},88432,187");
211
+ let _e = EnvGuard::apply(&[("TMUX", Some(&endpoint)), ("TMUX_PANE", Some("%7"))]);
173
212
 
174
- let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None, false).unwrap();
213
+ let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None, false).unwrap();
175
214
 
176
- assert_eq!(plan.mode, LeaderStartMode::ExecProvider);
177
- assert!(!plan.is_external_leader);
178
- assert!(plan.session_name.is_none());
179
- assert_eq!(plan.leader_window, None);
180
- assert_eq!(plan.argv, vec!["fake".to_string()]);
181
- assert!(
182
- !plan.argv.iter().any(|arg| arg == "switch-client" || arg == "attach-session"),
183
- "in-tmux default launch must not create or attach a background leader session: {:?}",
184
- plan.argv
185
- );
186
- }
215
+ assert_eq!(plan.mode, LeaderStartMode::ExecProvider);
216
+ assert!(!plan.is_external_leader);
217
+ assert!(plan.session_name.is_none());
218
+ assert_eq!(plan.leader_window, None);
219
+ assert_eq!(plan.argv, vec!["fake".to_string()]);
220
+ assert!(
221
+ !plan
222
+ .argv
223
+ .iter()
224
+ .any(|arg| arg == "switch-client" || arg == "attach-session"),
225
+ "in-tmux default launch must not create or attach a background leader session: {:?}",
226
+ plan.argv
227
+ );
228
+ }
187
229
 
188
- #[test]
189
- #[serial_test::serial(env)]
190
- fn in_tmux_default_leader_does_not_refuse_different_tmux_server() {
191
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
192
- let _e = EnvGuard::apply(&[
193
- ("TMUX", Some("/private/tmp/tmux-501/default,88432,187")),
194
- ("TMUX_PANE", Some("%9")),
195
- ]);
196
- let ws = std::env::temp_dir().join(format!("ta_rs_lsp_refuse_{}", std::process::id()));
197
- std::fs::create_dir_all(&ws).unwrap();
230
+ #[test]
231
+ #[serial_test::serial(env)]
232
+ fn in_tmux_default_leader_does_not_refuse_different_tmux_server() {
233
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
234
+ let _e = EnvGuard::apply(&[
235
+ ("TMUX", Some("/private/tmp/tmux-501/default,88432,187")),
236
+ ("TMUX_PANE", Some("%9")),
237
+ ]);
238
+ let ws = std::env::temp_dir().join(format!("ta_rs_lsp_refuse_{}", std::process::id()));
239
+ std::fs::create_dir_all(&ws).unwrap();
198
240
 
199
- let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None, false).unwrap();
241
+ let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None, false).unwrap();
200
242
 
201
- assert_eq!(plan.mode, LeaderStartMode::ExecProvider);
202
- assert!(!plan.is_external_leader);
203
- assert!(plan.session_name.is_none());
204
- assert_eq!(plan.argv, vec!["fake".to_string()]);
205
- }
243
+ assert_eq!(plan.mode, LeaderStartMode::ExecProvider);
244
+ assert!(!plan.is_external_leader);
245
+ assert!(plan.session_name.is_none());
246
+ assert_eq!(plan.argv, vec!["fake".to_string()]);
247
+ }
206
248
 
207
- #[test]
208
- #[serial_test::serial(env)]
209
- fn copilot_leader_env_disables_terminal_title_only_for_copilot() {
210
- let ws = std::env::temp_dir().join(format!("ta_rs_lsp_copilot_{}", std::process::id()));
211
- std::fs::create_dir_all(&ws).unwrap();
212
- let identity = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
249
+ #[test]
250
+ #[serial_test::serial(env)]
251
+ fn copilot_leader_env_disables_terminal_title_only_for_copilot() {
252
+ let ws = std::env::temp_dir().join(format!("ta_rs_lsp_copilot_{}", std::process::id()));
253
+ std::fs::create_dir_all(&ws).unwrap();
254
+ let identity = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
213
255
 
214
- let copilot = leader_env_for_identity(Provider::Copilot, &identity);
215
- assert_eq!(
216
- copilot.get("COPILOT_DISABLE_TERMINAL_TITLE").map(String::as_str),
217
- Some("1")
218
- );
256
+ let copilot = leader_env_for_identity(Provider::Copilot, &identity);
257
+ assert_eq!(
258
+ copilot
259
+ .get("COPILOT_DISABLE_TERMINAL_TITLE")
260
+ .map(String::as_str),
261
+ Some("1")
262
+ );
219
263
 
220
- for provider in [Provider::Codex, Provider::ClaudeCode] {
221
- let leader_env = leader_env_for_identity(provider, &identity);
222
- assert!(
223
- !leader_env.contains_key("COPILOT_DISABLE_TERMINAL_TITLE"),
224
- "{provider:?} leader env must not include copilot title override"
225
- );
226
- }
264
+ for provider in [Provider::Codex, Provider::ClaudeCode] {
265
+ let leader_env = leader_env_for_identity(provider, &identity);
266
+ assert!(
267
+ !leader_env.contains_key("COPILOT_DISABLE_TERMINAL_TITLE"),
268
+ "{provider:?} leader env must not include copilot title override"
269
+ );
227
270
  }
271
+ }
228
272
 
229
- // ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model findings) ═══════════════
230
- // Lock CORRECT Python v0.2.11 leader-identity behavior the contracts missed.
231
- // Golden re-probed via /tmp/probe_p2_leader.py vs team-agent-public @ 439bef8
232
- // (leader/__init__.py:_leader_identity_context / _identity_* / _detect_dual_state_divergence).
273
+ // ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model findings) ═══════════════
274
+ // Lock CORRECT Python v0.2.11 leader-identity behavior the contracts missed.
275
+ // Golden re-probed via /tmp/probe_p2_leader.py vs team-agent-public @ 439bef8
276
+ // (leader/__init__.py:_leader_identity_context / _identity_* / _detect_dual_state_divergence).
233
277
 
234
- #[test]
235
- #[serial_test::serial(env)]
236
- fn p2_leader_state_uuid_source_is_derived_not_env() {
237
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
238
- let _e = EnvGuard::apply(&[
239
- ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
240
- ("TEAM_AGENT_LEADER_SESSION_UUID", None),
241
- ]);
242
- let ws = p2_temp_ws("src");
243
- let state = serde_json::json!({"team_owner": {"leader_session_uuid": "STATEUUID123"}});
244
- let id = leader_identity_context(&ws, None, Some(&state)).unwrap();
245
- assert_eq!(id.leader_session_uuid_source, LeaderSessionUuidSource::Derived);
246
- assert_eq!(id.leader_session_uuid.as_str(), "STATEUUID123");
247
- }
278
+ #[test]
279
+ #[serial_test::serial(env)]
280
+ fn p2_leader_state_uuid_source_is_derived_not_env() {
281
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
282
+ let _e = EnvGuard::apply(&[
283
+ ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
284
+ ("TEAM_AGENT_LEADER_SESSION_UUID", None),
285
+ ]);
286
+ let ws = p2_temp_ws("src");
287
+ let state = serde_json::json!({"team_owner": {"leader_session_uuid": "STATEUUID123"}});
288
+ let id = leader_identity_context(&ws, None, Some(&state)).unwrap();
289
+ assert_eq!(
290
+ id.leader_session_uuid_source,
291
+ LeaderSessionUuidSource::Derived
292
+ );
293
+ assert_eq!(id.leader_session_uuid.as_str(), "STATEUUID123");
294
+ }
248
295
 
249
- // P1 — operator override env var is TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE (the
250
- // _OVERRIDE suffix), per leader/__init__.py:197 — NOT the injected child-env var.
251
- #[test]
252
- #[serial_test::serial(env)]
253
- fn p2_leader_override_reads_override_suffixed_env_var() {
254
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
255
- let _e = EnvGuard::apply(&[
256
- ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", Some("OVERRIDE_X")),
257
- ("TEAM_AGENT_LEADER_SESSION_UUID", None),
258
- ]);
259
- let ws = p2_temp_ws("ovr");
260
- let id = leader_identity_context(&ws, None, None).unwrap();
261
- assert_eq!(id.leader_session_uuid_source, LeaderSessionUuidSource::Override);
262
- assert_eq!(id.leader_session_uuid.as_str(), "OVERRIDE_X");
263
- }
296
+ // P1 — operator override env var is TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE (the
297
+ // _OVERRIDE suffix), per leader/__init__.py:197 — NOT the injected child-env var.
298
+ #[test]
299
+ #[serial_test::serial(env)]
300
+ fn p2_leader_override_reads_override_suffixed_env_var() {
301
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
302
+ let _e = EnvGuard::apply(&[
303
+ (
304
+ "TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
305
+ Some("OVERRIDE_X"),
306
+ ),
307
+ ("TEAM_AGENT_LEADER_SESSION_UUID", None),
308
+ ]);
309
+ let ws = p2_temp_ws("ovr");
310
+ let id = leader_identity_context(&ws, None, None).unwrap();
311
+ assert_eq!(
312
+ id.leader_session_uuid_source,
313
+ LeaderSessionUuidSource::Override
314
+ );
315
+ assert_eq!(id.leader_session_uuid.as_str(), "OVERRIDE_X");
316
+ }
264
317
 
265
- // P1 — derived inputs read state: machine_fingerprint = state team_owner record first
266
- // (_identity_machine_fingerprint); team_id = team_state_key(state) from session_name
267
- // (default 'current', not a hardcoded 'default').
268
- #[test]
269
- #[serial_test::serial(env)]
270
- fn p2_leader_derived_inputs_read_state_record() {
271
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
272
- let _e = EnvGuard::apply(&[("TEAM_AGENT_MACHINE_FINGERPRINT", None)]);
273
- let ws = p2_temp_ws("der");
274
- let state = serde_json::json!({
275
- "team_owner": {"machine_fingerprint": "RECORDED-FP-FROM-STATE"},
276
- "session_name": "team-agent-myteam"
277
- });
278
- let id = leader_identity_context(&ws, None, Some(&state)).unwrap();
279
- assert_eq!(id.machine_fingerprint, "RECORDED-FP-FROM-STATE", "state record fp beats env/hostname");
280
- assert_eq!(id.team_id.as_str(), "team-agent-myteam", "team_id from state.session_name");
281
- }
318
+ // P1 — derived inputs read state: machine_fingerprint = state team_owner record first
319
+ // (_identity_machine_fingerprint); team_id = team_state_key(state) from session_name
320
+ // (default 'current', not a hardcoded 'default').
321
+ #[test]
322
+ #[serial_test::serial(env)]
323
+ fn p2_leader_derived_inputs_read_state_record() {
324
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
325
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_MACHINE_FINGERPRINT", None)]);
326
+ let ws = p2_temp_ws("der");
327
+ let state = serde_json::json!({
328
+ "team_owner": {"machine_fingerprint": "RECORDED-FP-FROM-STATE"},
329
+ "session_name": "team-agent-myteam"
330
+ });
331
+ let id = leader_identity_context(&ws, None, Some(&state)).unwrap();
332
+ assert_eq!(
333
+ id.machine_fingerprint, "RECORDED-FP-FROM-STATE",
334
+ "state record fp beats env/hostname"
335
+ );
336
+ assert_eq!(
337
+ id.team_id.as_str(),
338
+ "team-agent-myteam",
339
+ "team_id from state.session_name"
340
+ );
341
+ }
282
342
 
283
- // P1 — os_user fallback chain = USER or USERNAME or '' (_identity_os_user), NOT
284
- // USER or LOGNAME or 'unknown'. (USERNAME is the 2nd choice; empty-string fallback.)
285
- #[test]
286
- #[serial_test::serial(env)]
287
- fn p2_leader_os_user_honors_username_then_empty() {
288
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
289
- let _e = EnvGuard::apply(&[
290
- ("USER", None),
291
- ("LOGNAME", None),
292
- ("USERNAME", Some("winuser")),
293
- ]);
294
- let ws = p2_temp_ws("usr");
295
- let id = leader_identity_context(&ws, None, None).unwrap();
296
- assert_eq!(id.os_user, "winuser", "USERNAME is the second choice (not LOGNAME)");
297
- }
343
+ // P1 — os_user fallback chain = USER or USERNAME or '' (_identity_os_user), NOT
344
+ // USER or LOGNAME or 'unknown'. (USERNAME is the 2nd choice; empty-string fallback.)
345
+ #[test]
346
+ #[serial_test::serial(env)]
347
+ fn p2_leader_os_user_honors_username_then_empty() {
348
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
349
+ let _e = EnvGuard::apply(&[
350
+ ("USER", None),
351
+ ("LOGNAME", None),
352
+ ("USERNAME", Some("winuser")),
353
+ ]);
354
+ let ws = p2_temp_ws("usr");
355
+ let id = leader_identity_context(&ws, None, None).unwrap();
356
+ assert_eq!(
357
+ id.os_user, "winuser",
358
+ "USERNAME is the second choice (not LOGNAME)"
359
+ );
360
+ }
298
361
 
299
- // P1 — detect_dual_state_divergence must catch an owner leader_session_uuid split even
300
- // when panes + epoch are identical (leader/__init__.py:574).
301
- #[test]
302
- #[serial_test::serial(env)]
303
- fn p2_leader_detect_divergence_catches_owner_uuid_split() {
304
- let ws = p2_temp_ws("div");
305
- let snap_dir = crate::model::paths::runtime_dir(&ws).join("teams").join("sess1");
306
- std::fs::create_dir_all(&snap_dir).unwrap();
307
- let snap = serde_json::json!({
308
- "session_name":"sess1",
309
- "team_owner":{"pane_id":"%1","leader_session_uuid":"UUID_B","owner_epoch":5},
310
- "leader_receiver":{"pane_id":"%1"}
311
- });
312
- std::fs::write(snap_dir.join("state.json"), serde_json::to_string(&snap).unwrap()).unwrap();
313
- let state = serde_json::json!({
314
- "session_name":"sess1",
315
- "team_owner":{"pane_id":"%1","leader_session_uuid":"UUID_A","owner_epoch":5},
316
- "leader_receiver":{"pane_id":"%1"}
317
- });
318
- let div = detect_dual_state_divergence(&ws, &state).unwrap();
319
- assert!(div.is_some(), "owner uuid split (A vs B) with matching panes/epoch must be detected");
320
- }
362
+ // P1 — detect_dual_state_divergence must catch an owner leader_session_uuid split even
363
+ // when panes + epoch are identical (leader/__init__.py:574).
364
+ #[test]
365
+ #[serial_test::serial(env)]
366
+ fn p2_leader_detect_divergence_catches_owner_uuid_split() {
367
+ let ws = p2_temp_ws("div");
368
+ let snap_dir = crate::model::paths::runtime_dir(&ws)
369
+ .join("teams")
370
+ .join("sess1");
371
+ std::fs::create_dir_all(&snap_dir).unwrap();
372
+ let snap = serde_json::json!({
373
+ "session_name":"sess1",
374
+ "team_owner":{"pane_id":"%1","leader_session_uuid":"UUID_B","owner_epoch":5},
375
+ "leader_receiver":{"pane_id":"%1"}
376
+ });
377
+ std::fs::write(
378
+ snap_dir.join("state.json"),
379
+ serde_json::to_string(&snap).unwrap(),
380
+ )
381
+ .unwrap();
382
+ let state = serde_json::json!({
383
+ "session_name":"sess1",
384
+ "team_owner":{"pane_id":"%1","leader_session_uuid":"UUID_A","owner_epoch":5},
385
+ "leader_receiver":{"pane_id":"%1"}
386
+ });
387
+ let div = detect_dual_state_divergence(&ws, &state).unwrap();
388
+ assert!(
389
+ div.is_some(),
390
+ "owner uuid split (A vs B) with matching panes/epoch must be detected"
391
+ );
392
+ }
321
393
 
322
- // ═══════════════════════════════════════════════════════════════════════
323
- // 14. WAVE-2 Lane B CONTRACT PASS — CLI-handler-facing byte-parity for the
324
- // three verbs (claim-leader / takeover / identity) + their core lease
325
- // machinery (_claim_lease_no_incident outcomes / _lease_refused shapes).
326
- //
327
- // GOLDEN (re-probed @ team-agent-public, leader/__init__.py +
328
- // runtime.py:721/791). Each test labels RED|LOCK honestly:
329
- // RED = drives an unimplemented!() body (claim_lease_no_incident /
330
- // attach_leader_to_state) → panics today = correct RED-first.
331
- // LOCK = drives an already-implemented stub/path → green today; pins
332
- // the golden so a future port cannot regress it.
333
- // Deferred to later adversarial rounds (Lane-A style): the ambiguous-
334
- // incident claim arm (no_caller_pane / caller_not_candidate / dry_run /
335
- // lost_race) which needs a seeded event-log incident + the broadcast
336
- // requeue cross-lane; the strict-uuid attach refusal string (needs a
337
- // live pane resolver). Those are #[ignore]/NOTE seams below.
338
- // ═══════════════════════════════════════════════════════════════════════
394
+ // ═══════════════════════════════════════════════════════════════════════
395
+ // 14. WAVE-2 Lane B CONTRACT PASS — CLI-handler-facing byte-parity for the
396
+ // three verbs (claim-leader / takeover / identity) + their core lease
397
+ // machinery (_claim_lease_no_incident outcomes / _lease_refused shapes).
398
+ //
399
+ // GOLDEN (re-probed @ team-agent-public, leader/__init__.py +
400
+ // runtime.py:721/791). Each test labels RED|LOCK honestly:
401
+ // RED = drives an unimplemented!() body (claim_lease_no_incident /
402
+ // attach_leader_to_state) → panics today = correct RED-first.
403
+ // LOCK = drives an already-implemented stub/path → green today; pins
404
+ // the golden so a future port cannot regress it.
405
+ // Deferred to later adversarial rounds (Lane-A style): the ambiguous-
406
+ // incident claim arm (no_caller_pane / caller_not_candidate / dry_run /
407
+ // lost_race) which needs a seeded event-log incident + the broadcast
408
+ // requeue cross-lane; the strict-uuid attach refusal string (needs a
409
+ // live pane resolver). Those are #[ignore]/NOTE seams below.
410
+ // ═══════════════════════════════════════════════════════════════════════
339
411
 
340
- // ── 14a. _claim_lease_no_incident OUTCOMES (golden __init__.py:598) ──────
341
- //
342
- // claim_lease_no_incident is unimplemented!() → every test here PANICS today
343
- // ═══════════════════════════════════════════════════════════════════════
344
- // unit-0 (Stage 0) characterization tests
345
- //
346
- // Pin two invariants that unit-1/3/4 will refactor behind typed
347
- // identity helpers. If the constant or the prefix shape changes, the
348
- // refactor must also update these tests.
349
- // ═══════════════════════════════════════════════════════════════════════
412
+ // ── 14a. _claim_lease_no_incident OUTCOMES (golden __init__.py:598) ──────
413
+ //
414
+ // claim_lease_no_incident is unimplemented!() → every test here PANICS today
415
+ // ═══════════════════════════════════════════════════════════════════════
416
+ // unit-0 (Stage 0) characterization tests
417
+ //
418
+ // Pin two invariants that unit-1/3/4 will refactor behind typed
419
+ // identity helpers. If the constant or the prefix shape changes, the
420
+ // refactor must also update these tests.
421
+ // ═══════════════════════════════════════════════════════════════════════
350
422
 
351
- #[test]
352
- fn unit0_leader_session_prefix_constant_is_stable() {
353
- // Pinned: the literal leader session prefix that gates "is this a
354
- // leader session" decisions everywhere in the runtime.
355
- assert_eq!(
356
- crate::leader::start::LEADER_SESSION_PREFIX,
357
- "team-agent-leader-"
358
- );
359
- // The layout module re-exports the same value (kept in sync via
360
- // const re-export).
361
- assert_eq!(
362
- crate::layout::sessions::LEADER_SESSION_PREFIX,
363
- crate::leader::start::LEADER_SESSION_PREFIX
364
- );
365
- }
423
+ #[test]
424
+ fn unit0_leader_session_prefix_constant_is_stable() {
425
+ // Pinned: the literal leader session prefix that gates "is this a
426
+ // leader session" decisions everywhere in the runtime.
427
+ assert_eq!(
428
+ crate::leader::start::LEADER_SESSION_PREFIX,
429
+ "team-agent-leader-"
430
+ );
431
+ // The layout module re-exports the same value (kept in sync via
432
+ // const re-export).
433
+ assert_eq!(
434
+ crate::layout::sessions::LEADER_SESSION_PREFIX,
435
+ crate::leader::start::LEADER_SESSION_PREFIX
436
+ );
437
+ }
366
438
 
367
- #[test]
368
- fn unit0_leader_prefixed_name_must_never_be_taken_as_worker_session() {
369
- // Pinned invariant fed into unit-1 typed identity: any session
370
- // name starting with `LEADER_SESSION_PREFIX` is a leader launcher
371
- // session, never a worker session. unit-1 will wrap this rule in
372
- // typed constructors (`WorkerSession::new` rejects the prefix,
373
- // `LeaderLauncherSession::new` requires it).
374
- let leader = "team-agent-leader-claude-x-aaaaaaaaaaaa";
375
- let worker = "team-real-worker";
376
- let prefix = crate::leader::start::LEADER_SESSION_PREFIX;
377
- assert!(leader.starts_with(prefix));
378
- assert!(!worker.starts_with(prefix));
379
- // Symmetry: the runtime today uses this exact check
380
- // (cli/mod.rs:261 `starts_with(LEADER_SESSION_PREFIX)`) to spare
381
- // leader sessions during shutdown.
382
- let leader_name = crate::transport::SessionName::new(leader);
383
- let worker_name = crate::transport::SessionName::new(worker);
384
- assert!(
385
- crate::layout::sessions::is_leader_session(&leader_name),
386
- "is_leader_session must accept a leader-prefixed session",
387
- );
388
- assert!(
389
- !crate::layout::sessions::is_leader_session(&worker_name),
390
- "is_leader_session must reject a non-prefixed session",
391
- );
392
- }
439
+ #[test]
440
+ fn unit0_leader_prefixed_name_must_never_be_taken_as_worker_session() {
441
+ // Pinned invariant fed into unit-1 typed identity: any session
442
+ // name starting with `LEADER_SESSION_PREFIX` is a leader launcher
443
+ // session, never a worker session. unit-1 will wrap this rule in
444
+ // typed constructors (`WorkerSession::new` rejects the prefix,
445
+ // `LeaderLauncherSession::new` requires it).
446
+ let leader = "team-agent-leader-claude-x-aaaaaaaaaaaa";
447
+ let worker = "team-real-worker";
448
+ let prefix = crate::leader::start::LEADER_SESSION_PREFIX;
449
+ assert!(leader.starts_with(prefix));
450
+ assert!(!worker.starts_with(prefix));
451
+ // Symmetry: the runtime today uses this exact check
452
+ // (cli/mod.rs:261 `starts_with(LEADER_SESSION_PREFIX)`) to spare
453
+ // leader sessions during shutdown.
454
+ let leader_name = crate::transport::SessionName::new(leader);
455
+ let worker_name = crate::transport::SessionName::new(worker);
456
+ assert!(
457
+ crate::layout::sessions::is_leader_session(&leader_name),
458
+ "is_leader_session must accept a leader-prefixed session",
459
+ );
460
+ assert!(
461
+ !crate::layout::sessions::is_leader_session(&worker_name),
462
+ "is_leader_session must reject a non-prefixed session",
463
+ );
464
+ }