@team-agent/installer 0.5.119 → 0.5.120

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 (59) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +46 -1
  4. package/README.zh.md +46 -1
  5. package/crates/team-agent/src/cli/adapters.rs +11 -0
  6. package/crates/team-agent/src/cli/arguments.rs +401 -0
  7. package/crates/team-agent/src/cli/emit.rs +241 -142
  8. package/crates/team-agent/src/cli/intent/runtime.rs +1139 -0
  9. package/crates/team-agent/src/cli/intent/runtime_tests.rs +584 -0
  10. package/crates/team-agent/src/cli/intent/tests.rs +654 -0
  11. package/crates/team-agent/src/cli/intent.rs +642 -0
  12. package/crates/team-agent/src/cli/mod.rs +49 -5
  13. package/crates/team-agent/src/cli/named_address.rs +10 -1
  14. package/crates/team-agent/src/cli/spec.rs +22 -10
  15. package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
  16. package/crates/team-agent/src/cli/tests/base.rs +6 -1
  17. package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
  18. package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
  19. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
  20. package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
  21. package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
  22. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
  23. package/crates/team-agent/src/lifecycle/launch.rs +28 -0
  24. package/crates/team-agent/src/lifecycle/restart.rs +17 -0
  25. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
  26. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
  27. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
  28. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
  29. package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
  30. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
  31. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
  32. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
  33. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
  34. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +4 -2
  35. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
  36. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +4 -3
  37. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
  38. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
  39. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
  40. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
  41. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
  42. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
  43. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
  44. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
  45. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
  46. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
  47. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
  48. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
  49. package/crates/team-agent/src/lifecycle/types.rs +10 -0
  50. package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
  51. package/crates/team-agent/src/tmux_backend.rs +8 -1
  52. package/package.json +4 -4
  53. package/skills/team-agent/SKILL.md +71 -54
  54. package/skills/team-agent/command-coverage.json +283 -13
  55. package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
  56. package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
  57. package/skills/team-agent/references/recovery-runbook.md +61 -212
  58. package/skills/team-agent/references/team-agent-operator.md +181 -48
  59. package/skills/team-agent/references/team-in-team.md +47 -80
@@ -0,0 +1,584 @@
1
+ #![allow(clippy::unwrap_used, clippy::panic)]
2
+ use super::super::tests::Sandbox;
3
+ use super::*;
4
+
5
+ fn fixture(ids: &[&str]) -> (Sandbox, Intent) {
6
+ let root = Sandbox::new();
7
+ std::fs::create_dir(root.path().join("agents")).unwrap();
8
+ std::fs::write(
9
+ root.path().join("TEAM.md"),
10
+ "---\nname: fixture\nprovider: pi\n---\nFixture\n",
11
+ )
12
+ .unwrap();
13
+ for id in ids {
14
+ write_role(root.path(), id);
15
+ }
16
+ let mut intent = super::super::tests::intent(Verb::Start);
17
+ intent.workspace = root.path().to_owned();
18
+ intent.subject = Subject::Team { source: None };
19
+ intent.team = Some("fixture".into());
20
+ (root, intent)
21
+ }
22
+ fn write_role(root: &Path, id: &str) {
23
+ std::fs::write(root.join("agents").join(format!("{id}.md")),format!("---\nname: {id}\nprovider: pi\nmodel: openai-codex/gpt-6-luna\neffort: max\ndangerously_skip_permissions: true\n---\nFixture\n")).unwrap();
24
+ }
25
+ fn install(root: &Path, ids: &[(&str, &str, bool)]) {
26
+ let agents:serde_json::Map<String,Value>=ids.iter().map(|(id,status,explicit)|((*id).into(),json!({"provider":"pi","status":status,"explicit_stop":explicit,"pane_pid":std::process::id(),"pane_id":format!("%{id}"),"window":id}))).collect();
27
+ let entry = json!({"team_key":"fixture","team_dir":root,"status":"alive","session_name":"team-fixture","agents":agents,"tasks":{}});
28
+ let mut raw = entry.clone();
29
+ raw["active_team_key"] = json!("fixture");
30
+ raw["teams"] = json!({"fixture":entry});
31
+ let state = crate::state::persist::runtime_state_path(root);
32
+ std::fs::create_dir_all(state.parent().unwrap()).unwrap();
33
+ std::fs::write(state, serde_json::to_vec(&raw).unwrap()).unwrap();
34
+ let spec = crate::model::paths::runtime_spec_path(root, "fixture");
35
+ std::fs::create_dir_all(spec.parent().unwrap()).unwrap();
36
+ std::fs::write(
37
+ spec,
38
+ yaml::dumps(&crate::compiler::compile_team(root).unwrap()),
39
+ )
40
+ .unwrap();
41
+ }
42
+ fn actions(plan: &Plan) -> Vec<(&str, Option<&str>)> {
43
+ plan.steps.iter().map(|s| (s.name(), s.agent())).collect()
44
+ }
45
+ #[test]
46
+ #[serial_test::serial(env)]
47
+ fn absent_source_is_one_original_create_not_restart_or_probe() {
48
+ let (root, intent) = fixture(&["one", "two"]);
49
+ let plan = plan_with_observer(intent, |_, _, _| panic!("fresh team probed")).unwrap();
50
+ assert_eq!(actions(&plan), [("quick_start", None)]);
51
+ assert!(!crate::state::persist::runtime_state_path(root.path()).exists());
52
+ }
53
+ #[test]
54
+ #[serial_test::serial(env)]
55
+ fn stopped_team_starts_both_and_mixed_start_leaves_live_peer_untouched() {
56
+ let (root, intent) = fixture(&["one", "two"]);
57
+ install(
58
+ root.path(),
59
+ &[("one", "stopped", true), ("two", "stopped", false)],
60
+ );
61
+ let plan =
62
+ plan_with_observer(intent.clone(), |_, _, _| Ok(Observation::StoppedVerified)).unwrap();
63
+ assert_eq!(
64
+ actions(&plan),
65
+ [("start_agent", Some("one")), ("start_agent", Some("two"))]
66
+ );
67
+ assert_eq!(plan.scope.team.as_deref(), Some("fixture"));
68
+ install(
69
+ root.path(),
70
+ &[("one", "running", false), ("two", "stopped", false)],
71
+ );
72
+ let plan = plan_with_observer(intent, |_, id, _| {
73
+ Ok(if id == "one" {
74
+ Observation::Live
75
+ } else {
76
+ Observation::StoppedVerified
77
+ })
78
+ })
79
+ .unwrap();
80
+ assert_eq!(actions(&plan), [("start_agent", Some("two"))]);
81
+ assert_eq!(plan.skipped[0]["reason"], "already_running");
82
+ }
83
+ #[test]
84
+ #[serial_test::serial(env)]
85
+ fn verified_all_live_is_noop_but_known_drift_and_unknown_refuse() {
86
+ let (root, intent) = fixture(&["one"]);
87
+ install(root.path(), &[("one", "running", false)]);
88
+ assert!(
89
+ plan_with_observer(intent.clone(), |_, _, _| Ok(Observation::Live))
90
+ .unwrap()
91
+ .steps
92
+ .is_empty()
93
+ );
94
+ assert!(plan_with_observer(intent.clone(), |_, _, _| Ok(Observation::Unknown)).is_err());
95
+ let mut patched = intent.clone();
96
+ patched.subject = Subject::Agent(AgentId::new("one"));
97
+ patched.patch.model = Some("new-model".into());
98
+ assert!(plan_with_observer(patched, |_, _, _| Ok(Observation::Live)).is_err());
99
+ let state = crate::state::persist::runtime_state_path(root.path());
100
+ let mut raw: Value = serde_json::from_slice(&std::fs::read(&state).unwrap()).unwrap();
101
+ raw["teams"]["fixture"]["agents"]["one"]["as_launched_dangerously_skip_permissions"] =
102
+ json!(false);
103
+ std::fs::write(state, serde_json::to_vec(&raw).unwrap()).unwrap();
104
+ assert!(plan_with_observer(intent, |_, _, _| Ok(Observation::Live)).is_err());
105
+ }
106
+ #[test]
107
+ #[serial_test::serial(env)]
108
+ fn new_restart_preserves_held_seats_and_shutdown_branch_starts_only_eligible() {
109
+ let (root, mut intent) = fixture(&["one", "two"]);
110
+ install(
111
+ root.path(),
112
+ &[("one", "stopped", true), ("two", "stopped", false)],
113
+ );
114
+ intent.verb = Verb::Restart;
115
+ intent.session = SessionPolicy::Auto;
116
+ let state = crate::state::persist::runtime_state_path(root.path());
117
+ let mut raw: Value = serde_json::from_slice(&std::fs::read(&state).unwrap()).unwrap();
118
+ raw["teams"]["fixture"]["status"] = json!("shutdown");
119
+ std::fs::write(&state, serde_json::to_vec(&raw).unwrap()).unwrap();
120
+ let plan = plan_with_observer(intent.clone(), |_, id, _| {
121
+ assert_ne!(id, "one", "held seat was probed");
122
+ Ok(Observation::StoppedVerified)
123
+ })
124
+ .unwrap();
125
+ assert_eq!(actions(&plan), [("start_agent", Some("two"))]);
126
+ assert_eq!(plan.skipped[0]["reason"], "explicit_stop");
127
+ raw["teams"]["fixture"]["agents"]["two"]["status"] = json!("running");
128
+ std::fs::write(state, serde_json::to_vec(&raw).unwrap()).unwrap();
129
+ let plan = plan_with_observer(intent, |_, _, _| Ok(Observation::Live)).unwrap();
130
+ assert_eq!(actions(&plan), [("restart_team", None)]);
131
+ let Step::RestartTeam(args) = &plan.steps[0] else {
132
+ panic!()
133
+ };
134
+ assert!(args.allow_fresh);
135
+ }
136
+ #[test]
137
+ #[serial_test::serial(env)]
138
+ fn all_paused_no_eligible_and_single_restart_never_unpauses() {
139
+ let (root, mut intent) = fixture(&["one"]);
140
+ install(root.path(), &[("one", "paused", false)]);
141
+ let blocked = plan_with_observer(intent.clone(), |_, _, _| panic!("paused probe")).unwrap();
142
+ assert!(blocked.no_eligible.is_some());
143
+ assert!(blocked.steps.is_empty());
144
+ assert_eq!(blocked.skipped[0]["reason"], "paused");
145
+ intent.verb = Verb::Restart;
146
+ intent.subject = Subject::Agent(AgentId::new("one"));
147
+ assert!(plan_with_observer(intent, |_, _, _| panic!("paused probe")).is_err());
148
+ }
149
+ #[test]
150
+ #[serial_test::serial(env)]
151
+ fn no_eligible_restart_preserves_scoped_held_and_stopped_skip_reasons() {
152
+ for explicit in [false, true] {
153
+ let (root, mut intent) = fixture(&["one"]);
154
+ install(root.path(), &[("one", "stopped", explicit)]);
155
+ intent.verb = Verb::Restart;
156
+ let blocked =
157
+ plan_with_observer(intent, |_, _, _| Ok(Observation::StoppedVerified)).unwrap();
158
+ assert!(blocked.no_eligible.is_some());
159
+ assert!(blocked.steps.is_empty());
160
+ assert_eq!(blocked.scope.team.as_deref(), Some("fixture"));
161
+ assert_eq!(blocked.skipped[0]["agent"], "one");
162
+ assert_eq!(
163
+ blocked.skipped[0]["reason"],
164
+ if explicit {
165
+ "explicit_stop"
166
+ } else {
167
+ "stopped_preserved_by_original_restart"
168
+ }
169
+ );
170
+ }
171
+ }
172
+
173
+ #[test]
174
+ #[serial_test::serial(env)]
175
+ fn role_only_add_and_absent_creation_need_explicit_decisions() {
176
+ let (root, mut intent) = fixture(&["one"]);
177
+ install(root.path(), &[("one", "stopped", false)]);
178
+ write_role(root.path(), "two");
179
+ intent.subject = Subject::Agent(AgentId::new("two"));
180
+ let plan = plan_with_observer(intent.clone(), |_, _, _| panic!("unregistered probe")).unwrap();
181
+ assert_eq!(actions(&plan), [("add_agent", Some("two"))]);
182
+ let Step::AddAgent(args) = &plan.steps[0] else {
183
+ panic!()
184
+ };
185
+ assert_eq!(
186
+ Path::new(&args.role_file),
187
+ root.path().join("agents/two.md")
188
+ );
189
+ intent.subject = Subject::Agent(AgentId::new("typo"));
190
+ assert!(plan_with_observer(intent.clone(), |_, _, _| panic!()).is_err());
191
+ intent.patch.provider = Some("pi".into());
192
+ intent.patch.bypass = Some(true);
193
+ assert!(matches!(
194
+ plan_with_observer(intent, |_, _, _| panic!())
195
+ .unwrap()
196
+ .steps[0],
197
+ Step::AddAgent(_)
198
+ ));
199
+ }
200
+ #[test]
201
+ #[serial_test::serial(env)]
202
+ fn spec_only_state_only_and_missing_spec_do_not_fallback_to_creation() {
203
+ let (root, mut intent) = fixture(&["one", "two"]);
204
+ install(root.path(), &[("one", "stopped", false)]);
205
+ assert!(
206
+ plan_with_observer(intent.clone(), |_, _, _| Ok(Observation::StoppedVerified)).is_err()
207
+ );
208
+ let spec = crate::model::paths::runtime_spec_path(root.path(), "fixture");
209
+ std::fs::remove_file(&spec).unwrap();
210
+ assert!(
211
+ plan_with_observer(intent.clone(), |_, _, _| Ok(Observation::StoppedVerified)).is_err()
212
+ );
213
+ intent.verb = Verb::Restart;
214
+ let blocked = plan_with_observer(intent, |_, _, _| Ok(Observation::StoppedVerified)).unwrap();
215
+ assert!(blocked.no_eligible.is_some());
216
+ assert!(blocked.steps.is_empty());
217
+ assert_eq!(blocked.skipped[0]["agent"], "one");
218
+ }
219
+ #[test]
220
+ #[serial_test::serial(env)]
221
+ fn runtime_files_and_source_are_unchanged_by_dry_planning() {
222
+ let (root, intent) = fixture(&["one"]);
223
+ install(root.path(), &[("one", "stopped", true)]);
224
+ let state = crate::state::persist::runtime_state_path(root.path());
225
+ let before = std::fs::read(&state).unwrap();
226
+ let plan = plan_with_observer(intent, |_, _, _| Ok(Observation::StoppedVerified)).unwrap();
227
+ assert!(!plan.steps.is_empty());
228
+ assert_eq!(std::fs::read(state).unwrap(), before);
229
+ }
230
+ #[test]
231
+ fn discovery_never_prefers_current_over_two_distinct_sources() {
232
+ let root = Sandbox::new();
233
+ for name in ["one", "current"] {
234
+ let path = root.path().join(".team").join(name);
235
+ std::fs::create_dir_all(path.join("agents")).unwrap();
236
+ std::fs::write(path.join("TEAM.md"), "fixture").unwrap();
237
+ }
238
+ assert!(discover(root.path(), None, None).is_err());
239
+ }
240
+ #[test]
241
+ #[serial_test::serial(env)]
242
+ fn explicit_source_and_relative_role_file_are_resolved_from_caller_not_workspace() {
243
+ let (root, _) = fixture(&["one"]);
244
+ let workspace = root.path().join("other");
245
+ std::fs::create_dir(&workspace).unwrap();
246
+ let args = vec![
247
+ "--from".into(),
248
+ ".".into(),
249
+ "--workspace".into(),
250
+ workspace.to_string_lossy().into_owned(),
251
+ "--dry-run".into(),
252
+ "--json".into(),
253
+ ];
254
+ let result = super::super::run("start", &args, root.path())
255
+ .unwrap()
256
+ .unwrap();
257
+ let CmdOutput::Json(value) = result.output else {
258
+ panic!()
259
+ };
260
+ assert_eq!(
261
+ value["scope"]["workspace"],
262
+ workspace.to_string_lossy().as_ref()
263
+ );
264
+ assert!(!workspace.join(".team").exists());
265
+ }
266
+
267
+ #[test]
268
+ #[serial_test::serial(env)]
269
+ fn genuine_bootstrap_uses_original_predicate_but_history_is_not_fresh() {
270
+ let (root, intent) = fixture(&["one"]);
271
+ let state = crate::state::persist::runtime_state_path(root.path());
272
+ std::fs::create_dir_all(state.parent().unwrap()).unwrap();
273
+ let bootstrap = json!({"team_key":"fixture","agents":{},"tasks":[],"session_name":null,"team_owner":{"pane_id":"%0"},"leader_receiver":{"pane_id":"%0","status":"attached"}});
274
+ std::fs::write(&state, serde_json::to_vec(&bootstrap).unwrap()).unwrap();
275
+ assert_eq!(
276
+ actions(&plan_with_observer(intent.clone(), |_, _, _| panic!()).unwrap()),
277
+ [("quick_start", None)]
278
+ );
279
+ let mut history = bootstrap;
280
+ history["tasks"] = json!({"old":{}});
281
+ std::fs::write(&state, serde_json::to_vec(&history).unwrap()).unwrap();
282
+ assert!(plan_with_observer(intent, |_, _, _| panic!()).is_err());
283
+ }
284
+
285
+ #[test]
286
+ #[serial_test::serial(env)]
287
+ fn preserve_rejects_known_missing_backing_before_stop_auto_uses_existing_decision() {
288
+ let (root, _) = fixture(&["one"]);
289
+ install(root.path(), &[("one", "running", false)]);
290
+ let selected =
291
+ resolve_active_team_readonly(root.path(), Some("fixture"), SelectorMode::RuntimeOnly)
292
+ .unwrap();
293
+ let row = json!({"provider":"pi","session_id":"missing-session","rollout_path":root.path().join("missing.jsonl"),"spawn_cwd":root.path()});
294
+ assert!(preflight_start(&selected, "one", &row, SessionPolicy::Preserve).is_err());
295
+ assert!(preflight_start(&selected, "one", &row, SessionPolicy::Auto).is_ok());
296
+ assert!(
297
+ preflight_start(
298
+ &selected,
299
+ "one",
300
+ &json!({"provider":"pi"}),
301
+ SessionPolicy::Preserve
302
+ )
303
+ .is_ok(),
304
+ "first fresh has no captured context to discard"
305
+ );
306
+ for policy in [SessionPolicy::Auto, SessionPolicy::Preserve] {
307
+ assert!(
308
+ preflight_start(&selected, "one", &json!({"provider":"cursor"}), policy).is_err(),
309
+ "unadmitted native resume cannot fall back to legacy/fresh"
310
+ );
311
+ }
312
+ }
313
+
314
+ #[test]
315
+ #[serial_test::serial(env)]
316
+ fn changed_role_and_socket_refuse_in_the_real_readonly_recheck() {
317
+ let (root, intent) = fixture(&["one"]);
318
+ install(root.path(), &[("one", "stopped", false)]);
319
+ let plan = plan_with_observer(intent, |_, _, _| Ok(Observation::StoppedVerified)).unwrap();
320
+ let path = root.path().join("agents/one.md");
321
+ let original = std::fs::read(&path).unwrap();
322
+ std::fs::write(&path, b"changed role").unwrap();
323
+ assert!(recheck(&plan, &plan.steps[0], false).is_err());
324
+ std::fs::write(path, original).unwrap();
325
+ let state = crate::state::persist::runtime_state_path(root.path());
326
+ let mut raw: Value = serde_json::from_slice(&std::fs::read(&state).unwrap()).unwrap();
327
+ raw["teams"]["fixture"]["tmux_socket"] = json!("/different/endpoint");
328
+ std::fs::write(state, serde_json::to_vec(&raw).unwrap()).unwrap();
329
+ assert!(recheck(&plan, &plan.steps[0], false).is_err());
330
+ }
331
+
332
+ #[test]
333
+ #[serial_test::serial(env)]
334
+ fn creation_rejects_an_added_role_before_the_original_create_handler() {
335
+ let (root, intent) = fixture(&["one"]);
336
+ let plan = plan_with_observer(intent, |_, _, _| panic!()).unwrap();
337
+ write_role(root.path(), "two");
338
+ assert!(recheck(&plan, &plan.steps[0], false).is_err());
339
+ assert!(!crate::state::persist::runtime_state_path(root.path()).exists());
340
+ }
341
+
342
+ #[test]
343
+ #[serial_test::serial(env)]
344
+ fn stopped_text_without_physical_identity_is_unknown_and_standalone_source_keeps_its_workspace() {
345
+ let (root, _) = fixture(&["one"]);
346
+ install(root.path(), &[("one", "stopped", false)]);
347
+ let selected =
348
+ resolve_active_team_readonly(root.path(), Some("fixture"), SelectorMode::RuntimeOnly)
349
+ .unwrap();
350
+ assert_eq!(
351
+ observe(
352
+ &selected,
353
+ "one",
354
+ &json!({"status":"stopped","provider":"pi"})
355
+ )
356
+ .unwrap(),
357
+ Observation::Unknown
358
+ );
359
+ assert_eq!(
360
+ observe(
361
+ &selected,
362
+ "one",
363
+ &json!({"status":"stopped","provider":"pi","pane_pid":std::process::id()})
364
+ )
365
+ .unwrap(),
366
+ Observation::Unknown
367
+ );
368
+ let (root, _) = fixture(&["one"]);
369
+ let values = vec![
370
+ "--from".into(),
371
+ root.path().to_string_lossy().into_owned(),
372
+ "--dry-run".into(),
373
+ "--json".into(),
374
+ ];
375
+ let result = super::super::run("start", &values, root.path())
376
+ .unwrap()
377
+ .unwrap();
378
+ let CmdOutput::Json(value) = result.output else {
379
+ panic!()
380
+ };
381
+ assert_eq!(
382
+ value["scope"]["workspace"],
383
+ root.path().to_string_lossy().as_ref()
384
+ );
385
+ }
386
+
387
+ #[test]
388
+ fn declared_provider_aliases_do_not_create_false_drift_or_absorb_native_routes() {
389
+ assert!(!drift(
390
+ &json!({"provider":"agent"}),
391
+ &json!({"provider":"cursor_agent"})
392
+ ));
393
+ assert!(same_provider("claude-code", "claude_code"));
394
+ assert!(!same_provider("cursor", "cursor_agent"));
395
+ assert!(!same_provider("grok_build", "grok"));
396
+ }
397
+
398
+ #[test]
399
+ #[cfg(unix)]
400
+ fn native_noop_compares_all_five_authoritative_launch_settings() {
401
+ use crate::contract_runtime::{config::RoleConfig, prompt::Prompt};
402
+ use team_agent_contract::contract::types::{Digest, ProviderId, SeatId};
403
+ let old = RoleConfig {
404
+ id: SeatId::new("one").unwrap(),
405
+ provider: ProviderId::new("cursor").unwrap(),
406
+ source: PathBuf::from("/fixture/one.md"),
407
+ role: "one".into(),
408
+ model: "original".into(),
409
+ effort: None,
410
+ bypass: true,
411
+ paused: false,
412
+ body: "body".into(),
413
+ prompt: Prompt {
414
+ text: String::new(),
415
+ sha256: Digest([0; 32]),
416
+ sources: vec![],
417
+ },
418
+ };
419
+ assert!(!native_settings_changed(&old, &old.clone()));
420
+ for field in ["provider", "model", "effort", "bypass", "paused"] {
421
+ let mut current = old.clone();
422
+ match field {
423
+ "provider" => current.provider = ProviderId::new("kiro").unwrap(),
424
+ "model" => current.model = "changed".into(),
425
+ "effort" => current.effort = Some("high".into()),
426
+ "bypass" => current.bypass = false,
427
+ _ => current.paused = true,
428
+ }
429
+ assert!(
430
+ native_settings_changed(&old, &current),
431
+ "unexamined launch drift: {field}"
432
+ );
433
+ }
434
+ }
435
+
436
+ #[test]
437
+ fn original_supervised_shell_needs_both_current_native_frame_and_live_child() {
438
+ for shell in ["sh", "bash", "zsh", "fish"] {
439
+ assert!(surface_liveness(shell, true, true));
440
+ assert!(
441
+ !surface_liveness(shell, true, false),
442
+ "stale native screen without child"
443
+ );
444
+ assert!(
445
+ !surface_liveness(shell, false, true),
446
+ "inert tail sleep is not a provider"
447
+ );
448
+ assert!(!surface_liveness(shell, false, false));
449
+ }
450
+ assert!(!surface_liveness("", true, true));
451
+ assert!(surface_liveness("node", true, false));
452
+ assert!(!surface_liveness("node", false, true));
453
+ }
454
+
455
+ #[test]
456
+ fn unchanged_observation_port_rejects_native_screen_followed_by_exit_tail() {
457
+ use crate::model::enums::Provider;
458
+ use crate::transport::native_input::inspect_input;
459
+ let native = "────────────────────────\n\n────────────────────────\ngpt-6-luna · max · 0%/272k · 0% · $0.00\n";
460
+ assert!(inspect_input(Provider::Pi, native, 24).is_some());
461
+ for tail in ["[team-agent worker] pi exited with 1\n", "[team-agent] Provider exited; this pane no longer accepts input. Restart from another pane with the appropriate team-agent start command.\n", "$ "] {
462
+ assert!(inspect_input(Provider::Pi, &format!("{native}{tail}"), 24).is_none());
463
+ }
464
+ }
465
+
466
+ #[test]
467
+ #[cfg(unix)]
468
+ fn native_kernel_exit_can_recover_ready_but_mismatched_liveness_never_noops() {
469
+ use team_agent_contract::{
470
+ host::process::ProcessState as P, orchestration::store::SeatStatus as S,
471
+ };
472
+ for (status, process, root, expected) in [
473
+ (S::Ready, P::Alive, "running", Observation::Live),
474
+ (S::Ready, P::Exited, "running", Observation::StoppedVerified),
475
+ (
476
+ S::Stopped,
477
+ P::Exited,
478
+ "stopped",
479
+ Observation::StoppedVerified,
480
+ ),
481
+ (S::Ready, P::Alive, "stopped", Observation::Unknown),
482
+ (S::Stopped, P::Alive, "running", Observation::Unknown),
483
+ (S::Stopped, P::Exited, "running", Observation::Unknown),
484
+ (S::Ready, P::Replaced, "running", Observation::Unknown),
485
+ (
486
+ S::Ready,
487
+ P::Unknown(team_agent_contract::host::HostError::new(
488
+ "fixture",
489
+ team_agent_contract::host::HostErrorKind::Unknown,
490
+ )),
491
+ "running",
492
+ Observation::Unknown,
493
+ ),
494
+ ] {
495
+ assert_eq!(native_observation(status, process, Some(root)), expected);
496
+ }
497
+ }
498
+
499
+ #[test]
500
+ #[serial_test::serial(env)]
501
+ fn preserve_context_preflight_and_corrupt_metadata_refuse_before_stop() {
502
+ let (root, _) = fixture(&["one"]);
503
+ install(root.path(), &[("one", "running", false)]);
504
+ let selected =
505
+ resolve_active_team_readonly(root.path(), Some("fixture"), SelectorMode::RuntimeOnly)
506
+ .unwrap();
507
+ for signal in [
508
+ json!({"first_send_at":"2026-10-19T00:00:00Z"}),
509
+ json!({"task_prompt_delivered":true}),
510
+ json!({"last_result_at":"2026-10-19T00:00:00Z"}),
511
+ ] {
512
+ let mut row = json!({"provider":"pi"});
513
+ row.as_object_mut()
514
+ .unwrap()
515
+ .extend(signal.as_object().unwrap().clone());
516
+ assert!(preflight_start(&selected, "one", &row, SessionPolicy::Preserve).is_err());
517
+ assert!(preflight_start(&selected, "one", &row, SessionPolicy::Auto).is_ok());
518
+ }
519
+ for field in [
520
+ "session_id",
521
+ "rollout_path",
522
+ "last_result_at",
523
+ "spawn_cwd",
524
+ "first_send_at",
525
+ "task_prompt_delivered",
526
+ ] {
527
+ let mut row = json!({"provider":"pi"});
528
+ row[field] = json!([]);
529
+ for policy in [SessionPolicy::Auto, SessionPolicy::Preserve] {
530
+ assert!(
531
+ preflight_start(&selected, "one", &row, policy).is_err(),
532
+ "corrupt {field}"
533
+ );
534
+ }
535
+ }
536
+ let outside = Sandbox::new();
537
+ assert!(preflight_start(
538
+ &selected,
539
+ "one",
540
+ &json!({"provider":"pi", "spawn_cwd":outside.path()}),
541
+ SessionPolicy::Auto
542
+ )
543
+ .is_err());
544
+ assert!(preflight_start(
545
+ &selected,
546
+ "one",
547
+ &json!({"provider":"pi", "session_id":"captured"}),
548
+ SessionPolicy::Auto
549
+ )
550
+ .is_err());
551
+ }
552
+
553
+ #[test]
554
+ #[serial_test::serial(env)]
555
+ fn pinned_child_scope_never_inherits_parent_runtime_and_spec_only_never_creates() {
556
+ let (parent, _) = fixture(&["one"]);
557
+ install(parent.path(), &[("one", "running", false)]);
558
+ let child = parent.path().join("child");
559
+ std::fs::create_dir(&child).unwrap();
560
+ std::fs::create_dir(child.join("agents")).unwrap();
561
+ std::fs::write(
562
+ child.join("TEAM.md"),
563
+ "---\nname: child\nprovider: pi\n---\n",
564
+ )
565
+ .unwrap();
566
+ write_role(&child, "two");
567
+ let mut intent = super::super::tests::intent(Verb::Start);
568
+ intent.workspace = child.clone();
569
+ intent.team = None;
570
+ intent.subject = Subject::Team {
571
+ source: Some(child.clone()),
572
+ };
573
+ let plan = plan_with_observer(intent.clone(), |_, _, _| {
574
+ panic!("parent must not be selected")
575
+ })
576
+ .unwrap();
577
+ assert_eq!(plan.scope.workspace, child);
578
+ assert_eq!(actions(&plan), [("quick_start", None)]);
579
+ let path = crate::model::paths::runtime_spec_path(&child, "orphan");
580
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
581
+ std::fs::write(path, "agents: []\n").unwrap();
582
+ assert!(plan_with_observer(intent, |_, _, _| panic!()).is_err());
583
+ assert!(!crate::state::persist::runtime_state_path(&child).exists());
584
+ }