@team-agent/installer 0.5.66 → 0.5.68

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 (164) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -0
  4. package/crates/team-agent/src/cli/adapters.rs +5 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +82 -0
  6. package/crates/team-agent/src/cli/emit.rs +3 -3
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +21 -7
  9. package/crates/team-agent/src/cli/leaders.rs +125 -1
  10. package/crates/team-agent/src/cli/mod.rs +20 -0
  11. package/crates/team-agent/src/cli/send/presentation.rs +7 -1
  12. package/crates/team-agent/src/cli/spec.rs +2 -0
  13. package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
  14. package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
  15. package/crates/team-agent/src/cli/tests/base.rs +26 -3
  16. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  17. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  18. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  19. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  20. package/crates/team-agent/src/compiler.rs +59 -0
  21. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  22. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  23. package/crates/team-agent/src/coordinator/health.rs +173 -0
  24. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  25. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  26. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  27. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  29. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  30. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  31. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  32. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  33. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  34. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  35. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  36. package/crates/team-agent/src/coordinator/types.rs +49 -0
  37. package/crates/team-agent/src/db/message_store.rs +39 -5
  38. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  39. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  40. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  41. package/crates/team-agent/src/leader/registry.rs +65 -0
  42. package/crates/team-agent/src/leader/start.rs +920 -63
  43. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  44. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  45. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -12
  46. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +62 -0
  47. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +59 -0
  48. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  49. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  50. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  51. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -456
  52. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  53. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  54. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  55. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  56. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  57. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  58. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  59. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  60. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  61. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  62. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  63. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  64. package/crates/team-agent/src/lifecycle/launch/spawn.rs +62 -2
  65. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +139 -0
  66. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
  67. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  68. package/crates/team-agent/src/lifecycle/launch.rs +99 -29
  69. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  70. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  71. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  72. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  73. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  74. package/crates/team-agent/src/lifecycle/restart/agent.rs +124 -0
  75. package/crates/team-agent/src/lifecycle/restart/common.rs +369 -11
  76. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  77. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  78. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
  79. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  80. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  81. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  82. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  83. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
  84. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  85. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
  86. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
  88. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  89. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  90. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  91. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  92. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  93. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  94. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  95. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  96. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  97. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
  98. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
  99. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
  100. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
  103. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +2 -0
  104. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
  105. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
  106. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  107. package/crates/team-agent/src/lifecycle/tests.rs +23 -13
  108. package/crates/team-agent/src/lifecycle/types.rs +61 -0
  109. package/crates/team-agent/src/lifecycle/worker_command_context.rs +85 -11
  110. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  111. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  112. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  113. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  114. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  115. package/crates/team-agent/src/messaging/results.rs +4 -0
  116. package/crates/team-agent/src/messaging/send.rs +34 -0
  117. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  118. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  119. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  121. package/crates/team-agent/src/messaging/types.rs +4 -1
  122. package/crates/team-agent/src/model/enums.rs +14 -5
  123. package/crates/team-agent/src/model/permissions.rs +3 -0
  124. package/crates/team-agent/src/os_probe.rs +73 -2
  125. package/crates/team-agent/src/provider/adapter.rs +231 -4
  126. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  127. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  128. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  129. package/crates/team-agent/src/provider/bypass_flags.rs +46 -2
  130. package/crates/team-agent/src/provider/classify.rs +4 -2
  131. package/crates/team-agent/src/provider/faults.rs +2 -1
  132. package/crates/team-agent/src/provider/mod.rs +11 -0
  133. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  134. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  135. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  136. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  137. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  138. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  139. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  140. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  141. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  142. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  143. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  144. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  145. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  146. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  147. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  148. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  149. package/crates/team-agent/src/provider/types.rs +4 -2
  150. package/crates/team-agent/src/provider/wire.rs +23 -1
  151. package/crates/team-agent/src/state/persist.rs +301 -8
  152. package/crates/team-agent/src/state/repository.rs +5 -0
  153. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  154. package/crates/team-agent/src/tmux_backend.rs +1064 -75
  155. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  156. package/crates/team-agent/src/transport.rs +248 -1
  157. package/npm/install.mjs +129 -73
  158. package/package.json +4 -4
  159. package/skills/team-agent/SKILL.md +33 -238
  160. package/skills/team-agent/command-coverage.json +31 -0
  161. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  162. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
  163. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  164. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
@@ -0,0 +1,250 @@
1
+ //! purpose: grok 席位角色文件必须显式写 model,缺则拒绝启动
2
+ //! contract: 缺 model 的 grok 角色启动失败(错误含 model 与原因);写了 model
3
+ //! 则该值进入 worker argv 的 `--model <值>`;claude 缺 model 行为不变
4
+ //! boundary: 只钉 Provider::Grok 的启动契约;不改 claude/codex/copilot 路径
5
+ //!
6
+ //! 修之前判红(40e91fba):grok 角色缺 model 仍会成功启动(compiler 填
7
+ //! 内建默认 grok-4,argv 带 `--model grok-4`,看起来完全正常)。
8
+
9
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
10
+
11
+ #[path = "../../../tests/support/hermetic.rs"]
12
+ mod hermetic_guard;
13
+ #[allow(dead_code)]
14
+ fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
15
+
16
+ use std::path::{Path, PathBuf};
17
+ use std::sync::atomic::{AtomicU64, Ordering};
18
+
19
+ use serial_test::serial;
20
+ use team_agent::lifecycle::quick_start_with_transport_in_workspace;
21
+ use team_agent::transport::test_support::OfflineTransport;
22
+
23
+ /// 1. grok 角色缺 model ⇒ 启动被拒,错误含 "model" 与「内建默认 / 隐式来源」。
24
+ /// 40e91fba 上会成功启动(有 overlay / 有 spawn)⇒ 本条红。
25
+ #[test]
26
+ #[serial(env)]
27
+ fn grok_role_missing_model_refuses_to_start() {
28
+ let ws = tmp_dir("grok-no-model");
29
+ let home = tmp_dir("grok-no-model-home");
30
+ seed_grok_home(&home, Some(&ws));
31
+ let _guard = HomeGuard::set(&home);
32
+ let team = write_role_team(
33
+ &ws,
34
+ "grokteam",
35
+ "grok_writer",
36
+ "grok",
37
+ None,
38
+ "Grok Writer",
39
+ );
40
+ let transport = OfflineTransport::new();
41
+
42
+ let result = quick_start_with_transport_in_workspace(
43
+ &ws,
44
+ &team,
45
+ None,
46
+ true,
47
+ Some("grokteam"),
48
+ &transport,
49
+ );
50
+
51
+ let err = match result {
52
+ Ok(report) => panic!(
53
+ "grok role without model must refuse to start; a built-in default \
54
+ would silently pick the model; report={report:?}"
55
+ ),
56
+ Err(error) => error.to_string(),
57
+ };
58
+ let lower = err.to_ascii_lowercase();
59
+ assert!(
60
+ lower.contains("model"),
61
+ "error must name the missing field; err={err}"
62
+ );
63
+ assert!(
64
+ lower.contains("built-in") || lower.contains("builtin") || err.contains("内建"),
65
+ "error must name the built-in default the framework would fill; err={err}"
66
+ );
67
+ assert!(
68
+ lower.contains("implicit") || err.contains("隐式"),
69
+ "error must reject every implicit source, not just one fallback; err={err}"
70
+ );
71
+ assert!(
72
+ err.contains("model: grok-4.6") || err.contains("model: grok-4"),
73
+ "error must show how to write the field; err={err}"
74
+ );
75
+ assert!(
76
+ !err.to_ascii_lowercase().contains("worktree") && !err.contains("then retry"),
77
+ "must not promise a remedy this version cannot honor; err={err}"
78
+ );
79
+ assert!(
80
+ transport.spawn_records().is_empty(),
81
+ "refusal must happen before spawn; records={:?}",
82
+ transport.spawn_records()
83
+ );
84
+ assert!(
85
+ !ws.join(".grok").join("config.toml").exists(),
86
+ "must refuse before writing grok overlay; leftover .grok/config.toml means the seat started"
87
+ );
88
+ }
89
+
90
+ /// 2. grok 角色写了 model ⇒ 正常启动,且该值真的进 argv(`--model <值>` 相邻)。
91
+ #[test]
92
+ #[serial(env)]
93
+ fn grok_role_explicit_model_reaches_argv() {
94
+ let ws = tmp_dir("grok-with-model");
95
+ let home = tmp_dir("grok-with-model-home");
96
+ seed_grok_home(&home, Some(&ws));
97
+ let _guard = HomeGuard::set(&home);
98
+ let team = write_role_team(
99
+ &ws,
100
+ "grokteam",
101
+ "grok_writer",
102
+ "grok",
103
+ Some("grok-4.6"),
104
+ "Grok Writer",
105
+ );
106
+ let transport = OfflineTransport::new();
107
+
108
+ quick_start_with_transport_in_workspace(
109
+ &ws,
110
+ &team,
111
+ None,
112
+ true,
113
+ Some("grokteam"),
114
+ &transport,
115
+ )
116
+ .expect("grok role with explicit model must start");
117
+
118
+ let argv = first_spawn_argv(&transport);
119
+ assert!(
120
+ argv_contains_adjacent(&argv, &["--model", "grok-4.6"]),
121
+ "declared model must appear as adjacent `--model grok-4.6` in worker argv; argv={argv:?}"
122
+ );
123
+ }
124
+
125
+ /// 3. claude 角色缺 model ⇒ 行为不变(仍能启动,不被 grok 的硬性 model 要求误伤)。
126
+ #[test]
127
+ fn claude_role_missing_model_still_starts() {
128
+ let ws = tmp_dir("claude-no-model");
129
+ let team = write_role_team(
130
+ &ws,
131
+ "claudeteam",
132
+ "clauder",
133
+ "claude",
134
+ None,
135
+ "Claude Worker",
136
+ );
137
+ let transport = OfflineTransport::new();
138
+
139
+ let result = quick_start_with_transport_in_workspace(
140
+ &ws,
141
+ &team,
142
+ None,
143
+ true,
144
+ Some("claudeteam"),
145
+ &transport,
146
+ );
147
+ assert!(
148
+ result.is_ok(),
149
+ "claude role without model must keep starting; grok-only requirement leaked; err={result:?}"
150
+ );
151
+ let argv = first_spawn_argv(&transport);
152
+ assert!(
153
+ !argv.is_empty(),
154
+ "claude seat must still spawn; argv={argv:?}"
155
+ );
156
+ }
157
+
158
+ fn first_spawn_argv(transport: &OfflineTransport) -> Vec<String> {
159
+ let records = transport.spawn_records();
160
+ assert!(
161
+ !records.is_empty(),
162
+ "expected a worker spawn so argv can be inspected; records={records:?}"
163
+ );
164
+ records[0].1.clone()
165
+ }
166
+
167
+ fn argv_contains_adjacent(hay: &[String], needle: &[&str]) -> bool {
168
+ if needle.is_empty() {
169
+ return true;
170
+ }
171
+ hay.windows(needle.len())
172
+ .any(|window| window.iter().map(String::as_str).eq(needle.iter().copied()))
173
+ }
174
+
175
+ fn write_role_team(
176
+ ws: &Path,
177
+ team_key: &str,
178
+ agent_id: &str,
179
+ provider: &str,
180
+ model: Option<&str>,
181
+ role: &str,
182
+ ) -> PathBuf {
183
+ let team = ws.join(team_key);
184
+ std::fs::create_dir_all(team.join("agents")).unwrap();
185
+ std::fs::write(
186
+ team.join("TEAM.md"),
187
+ format!(
188
+ "---\nname: {team_key}\nobjective: explicit-model contract.\nprovider: {provider}\ndangerously_skip_permissions: false\n---\n\nTeam.\n"
189
+ ),
190
+ )
191
+ .unwrap();
192
+ let model_line = match model {
193
+ Some(value) => format!("model: {value}\n"),
194
+ None => String::new(),
195
+ };
196
+ std::fs::write(
197
+ team.join("agents").join(format!("{agent_id}.md")),
198
+ format!(
199
+ "---\nname: {agent_id}\nrole: {role}\nprovider: {provider}\n{model_line}auth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nWorker.\n"
200
+ ),
201
+ )
202
+ .unwrap();
203
+ team
204
+ }
205
+
206
+ fn seed_grok_home(home: &Path, trusted_cwd: Option<&Path>) {
207
+ let grok = home.join(".grok");
208
+ std::fs::create_dir_all(&grok).unwrap();
209
+ std::fs::write(grok.join("auth.json"), r#"{"test":"ok"}"#).unwrap();
210
+ if let Some(cwd) = trusted_cwd {
211
+ std::fs::write(
212
+ grok.join("trusted_folders.toml"),
213
+ format!("[folders.\"{}\"]\ntrusted = true\n", cwd.display()),
214
+ )
215
+ .unwrap();
216
+ }
217
+ }
218
+
219
+ struct HomeGuard {
220
+ prev: Option<String>,
221
+ }
222
+
223
+ impl HomeGuard {
224
+ fn set(home: &Path) -> Self {
225
+ let prev = std::env::var("HOME").ok();
226
+ std::env::set_var("HOME", home);
227
+ Self { prev }
228
+ }
229
+ }
230
+
231
+ impl Drop for HomeGuard {
232
+ fn drop(&mut self) {
233
+ match &self.prev {
234
+ Some(value) => std::env::set_var("HOME", value),
235
+ None => std::env::remove_var("HOME"),
236
+ }
237
+ }
238
+ }
239
+
240
+ fn tmp_dir(tag: &str) -> PathBuf {
241
+ static N: AtomicU64 = AtomicU64::new(0);
242
+ let dir = std::env::temp_dir().join(format!(
243
+ "ta-rs-grok-model-{tag}-{}-{}",
244
+ std::process::id(),
245
+ N.fetch_add(1, Ordering::Relaxed)
246
+ ));
247
+ let _ = std::fs::remove_dir_all(&dir);
248
+ std::fs::create_dir_all(&dir).unwrap();
249
+ std::fs::canonicalize(dir).unwrap()
250
+ }
@@ -0,0 +1,293 @@
1
+ //! ---
2
+ //! purpose: grok pending id 入 state,捕获后整队 restart 走 --resume
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: grok-fresh-pending-matches-argv
6
+ //! what: fresh spawn 的 `_pending_session_id` 等于 argv `--session-id`
7
+ //! - name: grok-restart-resume-argv
8
+ //! what: session_id + 磁盘存档齐时 restart 发 `--resume <uuid>`
9
+ //! - name: grok-classify-resume-when-archive-present
10
+ //! what: classify 在存档存在时决策 Resume,缺失时 Refuse 而非静默 fresh
11
+ //! boundary:
12
+ //! - 不改 reset-agent --discard-session
13
+ //! - 不覆盖 claude 三拍
14
+ //! maturity: wired
15
+ //! ---
16
+
17
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
18
+
19
+ #[path = "../../../tests/support/hermetic.rs"]
20
+ mod hermetic_guard;
21
+ #[allow(dead_code)]
22
+ fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
23
+
24
+ use std::path::{Path, PathBuf};
25
+ use std::sync::atomic::{AtomicU64, Ordering};
26
+
27
+ use crate::lifecycle::restart::classify_restart_plan_with_resume_validation;
28
+ use crate::lifecycle::types::ResumeDecision;
29
+ use crate::provider::session_scan::grok::{grok_session_archive_present, grok_session_dir};
30
+ use crate::state::persist::{load_runtime_state, save_runtime_state};
31
+ use serial_test::serial;
32
+ use team_agent::lifecycle::{quick_start_with_transport_in_workspace, restart_with_transport};
33
+ use team_agent::transport::test_support::OfflineTransport;
34
+
35
+ #[test]
36
+ #[serial(env)]
37
+ fn grok_fresh_spawn_binds_pending_session_id_to_argv_session_id() {
38
+ let (ws, _home, _guard, team) = seed_grok("pending");
39
+ let transport = OfflineTransport::new();
40
+ quick_start_with_transport_in_workspace(&ws, &team, None, true, Some("grokteam"), &transport)
41
+ .expect("grok fresh start");
42
+ let argv = first_spawn_argv(&transport);
43
+ let sid = argv_flag(&argv, "--session-id").expect("--session-id on grok fresh argv");
44
+ assert!(
45
+ argv_flag(&argv, "--resume").is_none(),
46
+ "fresh spawn must not --resume; argv={argv:?}"
47
+ );
48
+ let state = load_runtime_state(&ws).expect("state");
49
+ let pending = state
50
+ .pointer("/agents/grok_writer/_pending_session_id")
51
+ .and_then(|v| v.as_str());
52
+ assert_eq!(
53
+ pending,
54
+ Some(sid.as_str()),
55
+ "pending must match argv session-id; agent={:?}",
56
+ state.pointer("/agents/grok_writer")
57
+ );
58
+ assert_eq!(
59
+ state
60
+ .pointer("/agents/grok_writer/session_id")
61
+ .and_then(|v| v.as_str()),
62
+ None,
63
+ "session_id stays null until capture; agent={:?}",
64
+ state.pointer("/agents/grok_writer")
65
+ );
66
+ }
67
+
68
+ #[test]
69
+ #[serial(env)]
70
+ fn grok_restart_uses_resume_when_session_captured_and_archive_present() {
71
+ let (ws, home, _guard, team) = seed_grok("resume");
72
+ let first = OfflineTransport::new();
73
+ quick_start_with_transport_in_workspace(&ws, &team, None, true, Some("grokteam"), &first)
74
+ .expect("initial grok start");
75
+ let sid = argv_flag(&first_spawn_argv(&first), "--session-id").expect("fresh session-id");
76
+ let dir = grok_session_dir(&ws, &sid).expect("grok session dir");
77
+ std::fs::create_dir_all(&dir).unwrap();
78
+ std::fs::write(dir.join("events.jsonl"), b"").unwrap();
79
+ assert!(grok_session_archive_present(&dir));
80
+ let _ = home;
81
+
82
+ let mut state = load_runtime_state(&ws).expect("load");
83
+ {
84
+ let agent = state
85
+ .pointer_mut("/agents/grok_writer")
86
+ .expect("grok_writer")
87
+ .as_object_mut()
88
+ .expect("agent object");
89
+ agent.insert("session_id".to_string(), serde_json::json!(sid.clone()));
90
+ agent.insert(
91
+ "rollout_path".to_string(),
92
+ serde_json::json!(dir.to_string_lossy()),
93
+ );
94
+ agent.insert(
95
+ "spawn_cwd".to_string(),
96
+ serde_json::json!(ws.to_string_lossy()),
97
+ );
98
+ agent.insert("capture_state".to_string(), serde_json::json!("captured"));
99
+ }
100
+ save_runtime_state(&ws, &state).expect("save captured");
101
+
102
+ seed_healthy_coordinator(&ws);
103
+
104
+ let restart = OfflineTransport::new();
105
+ restart_with_transport(&ws, false, Some("grokteam"), &restart)
106
+ .expect("restart without --allow-fresh must resume grok");
107
+ let argv = first_spawn_argv(&restart);
108
+ assert!(
109
+ argv_contains_adjacent(&argv, &["--resume", sid.as_str()]),
110
+ "captured grok must restart with --resume <uuid>; argv={argv:?}"
111
+ );
112
+ assert!(
113
+ argv_flag(&argv, "--session-id").is_none(),
114
+ "resume argv must not mint a new --session-id; argv={argv:?}"
115
+ );
116
+ }
117
+
118
+ #[test]
119
+ #[serial(env)]
120
+ fn grok_classify_resumes_when_archive_present_and_refuses_when_missing() {
121
+ let base = tmp_dir("classify");
122
+ let home = tmp_dir("classify-home");
123
+ let ws = base.join("ws");
124
+ std::fs::create_dir_all(&ws).unwrap();
125
+ let _guard = HomeGuard::set(&home);
126
+ let sid = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
127
+ let dir = grok_session_dir(&ws, sid).expect("dir");
128
+ std::fs::create_dir_all(&dir).unwrap();
129
+ std::fs::write(dir.join("summary.json"), b"{}").unwrap();
130
+
131
+ let present = serde_json::json!({
132
+ "agents": {
133
+ "w1": {
134
+ "provider": "grok",
135
+ "status": "running",
136
+ "session_id": sid,
137
+ "spawn_cwd": ws.to_string_lossy(),
138
+ "rollout_path": dir.to_string_lossy(),
139
+ }
140
+ }
141
+ });
142
+ let plan = classify_restart_plan_with_resume_validation(Some(&ws), &present, false)
143
+ .expect("classify present");
144
+ assert_eq!(plan.decisions.len(), 1);
145
+ assert_eq!(
146
+ plan.decisions[0].decision,
147
+ ResumeDecision::Resume,
148
+ "archive present must Resume; unresumable={:?}",
149
+ plan.unresumable
150
+ );
151
+
152
+ let missing_sid = "dddddddd-dddd-4ddd-8ddd-dddddddddddd";
153
+ let missing = serde_json::json!({
154
+ "agents": {
155
+ "w1": {
156
+ "provider": "grok",
157
+ "status": "running",
158
+ "session_id": missing_sid,
159
+ "spawn_cwd": ws.to_string_lossy(),
160
+ }
161
+ }
162
+ });
163
+ let refuse = classify_restart_plan_with_resume_validation(Some(&ws), &missing, false)
164
+ .expect("classify missing");
165
+ assert_eq!(refuse.decisions.len(), 1);
166
+ assert_eq!(
167
+ refuse.decisions[0].decision,
168
+ ResumeDecision::Refuse,
169
+ "missing grok archive must Refuse, not silent fresh; got {:?}",
170
+ refuse.decisions[0].decision
171
+ );
172
+ }
173
+
174
+ /// restart 的就绪门要求 `coordinator_health().ok`,而单测里 `start_coordinator`
175
+ /// spawn 的是 `std::env::current_exe()` —— 也就是 libtest 测试二进制本身,它一启动
176
+ /// 就以 `Unrecognized option: 'workspace'` 退出并变成僵尸,`pid_is_running` 的
177
+ /// `ps stat=` 一看到 `Z` 就判 not running。于是就绪门只能靠「抢在死婴被看到之前
178
+ /// 探到它」这个竞态过关,慢机/高负载上必然超时。这里按本仓既有写法(见
179
+ /// `copilot_provider_red.rs` / `restart_build_before_destroy_0540_contract.rs`)
180
+ /// 预先把 pid 文件与 metadata 指向测试进程自己,让 `start_coordinator` 走
181
+ /// `AlreadyRunning` 早返回,竞态被删掉而不是被调宽。
182
+ fn seed_healthy_coordinator(workspace: &Path) {
183
+ let workspace = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
184
+ let pid = crate::coordinator::Pid::new(std::process::id());
185
+ std::fs::create_dir_all(
186
+ crate::coordinator::coordinator_pid_path(&workspace)
187
+ .parent()
188
+ .unwrap(),
189
+ )
190
+ .unwrap();
191
+ crate::coordinator::write_coordinator_metadata(
192
+ &workspace,
193
+ pid,
194
+ crate::coordinator::MetadataSource::Boot,
195
+ )
196
+ .expect("write coordinator metadata");
197
+ std::fs::write(
198
+ crate::coordinator::coordinator_pid_path(&workspace),
199
+ pid.to_string(),
200
+ )
201
+ .expect("write coordinator pid");
202
+ }
203
+
204
+ fn seed_grok(tag: &str) -> (PathBuf, PathBuf, HomeGuard, PathBuf) {
205
+ let ws = tmp_dir(tag);
206
+ let home = tmp_dir(&format!("{tag}-home"));
207
+ seed_grok_home(&home, Some(&ws));
208
+ let guard = HomeGuard::set(&home);
209
+ let team = write_grok_role(&ws);
210
+ (ws, home, guard, team)
211
+ }
212
+
213
+ fn first_spawn_argv(transport: &OfflineTransport) -> Vec<String> {
214
+ let records = transport.spawn_records();
215
+ assert!(
216
+ !records.is_empty(),
217
+ "expected a worker spawn; records={records:?}"
218
+ );
219
+ records[0].1.clone()
220
+ }
221
+
222
+ fn argv_flag(argv: &[String], flag: &str) -> Option<String> {
223
+ argv.windows(2)
224
+ .find(|pair| pair[0] == flag)
225
+ .map(|pair| pair[1].clone())
226
+ }
227
+
228
+ fn argv_contains_adjacent(hay: &[String], needle: &[&str]) -> bool {
229
+ hay.windows(needle.len())
230
+ .any(|window| window.iter().map(String::as_str).eq(needle.iter().copied()))
231
+ }
232
+
233
+ fn write_grok_role(ws: &Path) -> PathBuf {
234
+ let team = ws.join("grokteam");
235
+ std::fs::create_dir_all(team.join("agents")).unwrap();
236
+ std::fs::write(
237
+ team.join("TEAM.md"),
238
+ "---\nname: grokteam\nobjective: grok resume.\nprovider: grok\ndangerously_skip_permissions: false\n---\n\nTeam.\n",
239
+ )
240
+ .unwrap();
241
+ std::fs::write(
242
+ team.join("agents").join("grok_writer.md"),
243
+ "---\nname: grok_writer\nrole: Grok Writer\nprovider: grok\nmodel: grok-4.6\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nWorker.\n",
244
+ )
245
+ .unwrap();
246
+ team
247
+ }
248
+
249
+ fn seed_grok_home(home: &Path, trusted_cwd: Option<&Path>) {
250
+ let grok = home.join(".grok");
251
+ std::fs::create_dir_all(&grok).unwrap();
252
+ std::fs::write(grok.join("auth.json"), r#"{"test":"ok"}"#).unwrap();
253
+ if let Some(cwd) = trusted_cwd {
254
+ std::fs::write(
255
+ grok.join("trusted_folders.toml"),
256
+ format!("[folders.\"{}\"]\ntrusted = true\n", cwd.display()),
257
+ )
258
+ .unwrap();
259
+ }
260
+ }
261
+
262
+ struct HomeGuard {
263
+ prev: Option<String>,
264
+ }
265
+
266
+ impl HomeGuard {
267
+ fn set(home: &Path) -> Self {
268
+ let prev = std::env::var("HOME").ok();
269
+ std::env::set_var("HOME", home);
270
+ Self { prev }
271
+ }
272
+ }
273
+
274
+ impl Drop for HomeGuard {
275
+ fn drop(&mut self) {
276
+ match &self.prev {
277
+ Some(value) => std::env::set_var("HOME", value),
278
+ None => std::env::remove_var("HOME"),
279
+ }
280
+ }
281
+ }
282
+
283
+ fn tmp_dir(tag: &str) -> PathBuf {
284
+ static N: AtomicU64 = AtomicU64::new(0);
285
+ let dir = std::env::temp_dir().join(format!(
286
+ "ta-rs-grok-resume-{tag}-{}-{}",
287
+ std::process::id(),
288
+ N.fetch_add(1, Ordering::Relaxed)
289
+ ));
290
+ let _ = std::fs::remove_dir_all(&dir);
291
+ std::fs::create_dir_all(&dir).unwrap();
292
+ std::fs::canonicalize(dir).unwrap()
293
+ }