@team-agent/installer 0.3.13 → 0.3.15
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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +1 -0
- package/crates/team-agent/src/cli/emit.rs +92 -11
- package/crates/team-agent/src/cli/mod.rs +228 -85
- package/crates/team-agent/src/cli/send.rs +252 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +159 -2
- package/crates/team-agent/src/cli/types.rs +30 -1
- package/crates/team-agent/src/compiler/tests.rs +2 -2
- package/crates/team-agent/src/compiler.rs +1 -1
- package/crates/team-agent/src/fake_worker.rs +32 -145
- package/crates/team-agent/src/leader/start.rs +279 -4
- package/crates/team-agent/src/lifecycle/display.rs +3 -3
- package/crates/team-agent/src/lifecycle/launch.rs +692 -92
- package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
- package/crates/team-agent/src/lifecycle/restart/common.rs +88 -42
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +197 -33
- package/crates/team-agent/src/lifecycle/restart/selection.rs +9 -6
- package/crates/team-agent/src/lifecycle/tests/core.rs +195 -50
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +934 -72
- package/crates/team-agent/src/lifecycle/types.rs +5 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +153 -3
- package/crates/team-agent/src/messaging/leader_receiver.rs +276 -43
- package/crates/team-agent/src/messaging/mod.rs +6 -3
- package/crates/team-agent/src/messaging/results.rs +240 -158
- package/crates/team-agent/src/messaging/send.rs +3 -2
- package/crates/team-agent/src/messaging/tests/e23.rs +35 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +6 -2
- package/crates/team-agent/src/messaging/tests/runtime.rs +9 -9
- package/crates/team-agent/src/os_probe.rs +11 -0
- package/crates/team-agent/src/state/persist.rs +6 -0
- package/crates/team-agent/src/tmux_backend.rs +90 -0
- package/crates/team-agent/src/transport/test_support.rs +46 -1
- package/crates/team-agent/src/transport.rs +31 -0
- package/package.json +4 -4
- package/skills/team-agent/references/recovery-runbook.md +277 -0
|
@@ -12,34 +12,58 @@ use crate::transport::PaneLiveness;
|
|
|
12
12
|
#[test]
|
|
13
13
|
fn classify_first_send_at_none_is_absent_not_corrupt() {
|
|
14
14
|
// None / 缺失 = 从未交互,可丢弃 fresh —— 不能误判成 corrupt。
|
|
15
|
-
assert_eq!(
|
|
15
|
+
assert_eq!(
|
|
16
|
+
classify_first_send_at(&json!(null)),
|
|
17
|
+
FirstSendAtState::Absent
|
|
18
|
+
);
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
#[test]
|
|
19
22
|
fn classify_first_send_at_empty_string_is_corrupt_not_absent() {
|
|
20
23
|
// 陷阱核心:Python truthiness 会把 "" 当 falsey/absent;契约要求 corrupt。
|
|
21
|
-
assert_eq!(
|
|
24
|
+
assert_eq!(
|
|
25
|
+
classify_first_send_at(&json!("")),
|
|
26
|
+
FirstSendAtState::Corrupt
|
|
27
|
+
);
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
#[test]
|
|
25
31
|
fn classify_first_send_at_zero_and_false_are_corrupt() {
|
|
26
32
|
// 0 / False 非 str → corrupt(绝不靠 bool/int truthiness 当 absent)。
|
|
27
33
|
assert_eq!(classify_first_send_at(&json!(0)), FirstSendAtState::Corrupt);
|
|
28
|
-
assert_eq!(
|
|
29
|
-
|
|
34
|
+
assert_eq!(
|
|
35
|
+
classify_first_send_at(&json!(false)),
|
|
36
|
+
FirstSendAtState::Corrupt
|
|
37
|
+
);
|
|
38
|
+
assert_eq!(
|
|
39
|
+
classify_first_send_at(&json!(123)),
|
|
40
|
+
FirstSendAtState::Corrupt
|
|
41
|
+
);
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
#[test]
|
|
33
45
|
fn classify_first_send_at_literal_null_string_is_corrupt() {
|
|
34
46
|
// 字面量字符串 "null"(非 ISO)→ corrupt,而 JSON null → absent(上面已测)。
|
|
35
|
-
assert_eq!(
|
|
36
|
-
|
|
47
|
+
assert_eq!(
|
|
48
|
+
classify_first_send_at(&json!("null")),
|
|
49
|
+
FirstSendAtState::Corrupt
|
|
50
|
+
);
|
|
51
|
+
assert_eq!(
|
|
52
|
+
classify_first_send_at(&json!("not-a-date")),
|
|
53
|
+
FirstSendAtState::Corrupt
|
|
54
|
+
);
|
|
37
55
|
}
|
|
38
56
|
|
|
39
57
|
#[test]
|
|
40
58
|
fn classify_first_send_at_non_string_containers_are_corrupt() {
|
|
41
|
-
assert_eq!(
|
|
42
|
-
|
|
59
|
+
assert_eq!(
|
|
60
|
+
classify_first_send_at(&json!([])),
|
|
61
|
+
FirstSendAtState::Corrupt
|
|
62
|
+
);
|
|
63
|
+
assert_eq!(
|
|
64
|
+
classify_first_send_at(&json!({})),
|
|
65
|
+
FirstSendAtState::Corrupt
|
|
66
|
+
);
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
#[test]
|
|
@@ -178,7 +202,10 @@ fn plan_condition_rejects_out_of_grammar() {
|
|
|
178
202
|
"report_result.s == bar",
|
|
179
203
|
] {
|
|
180
204
|
assert!(
|
|
181
|
-
matches!(
|
|
205
|
+
matches!(
|
|
206
|
+
PlanCondition::parse(bad),
|
|
207
|
+
Err(LifecycleError::InvalidPlan(_))
|
|
208
|
+
),
|
|
182
209
|
"expected InvalidPlan for {bad:?}"
|
|
183
210
|
);
|
|
184
211
|
}
|
|
@@ -186,14 +213,21 @@ fn plan_condition_rejects_out_of_grammar() {
|
|
|
186
213
|
|
|
187
214
|
// ───────────────────────────────────────────────────────────────────────
|
|
188
215
|
// resolve_display_backend — display/backend.py
|
|
189
|
-
// 默认 none
|
|
216
|
+
// 默认 adaptive;显式 none 是逃生口。
|
|
190
217
|
// ───────────────────────────────────────────────────────────────────────
|
|
191
218
|
|
|
192
219
|
#[test]
|
|
193
|
-
fn
|
|
220
|
+
fn resolve_backend_defaults_to_adaptive_when_none_requested() {
|
|
194
221
|
let r = resolve_display_backend(None, None);
|
|
222
|
+
assert_eq!(r.backend, DisplayBackend::Adaptive);
|
|
223
|
+
assert!(!r.non_default, "默认 adaptive 不应标记 non_default");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[test]
|
|
227
|
+
fn resolve_backend_explicit_none_is_escape_hatch() {
|
|
228
|
+
let r = resolve_display_backend(Some(DisplayBackend::None), None);
|
|
195
229
|
assert_eq!(r.backend, DisplayBackend::None);
|
|
196
|
-
assert!(
|
|
230
|
+
assert!(r.non_default, "显式 none 是偏离默认 adaptive 的逃生口");
|
|
197
231
|
}
|
|
198
232
|
|
|
199
233
|
#[test]
|
|
@@ -228,17 +262,14 @@ fn resolve_backend_recorded_used_when_no_request() {
|
|
|
228
262
|
fn probe_never_errors_platform_degradation_is_typed_outcome() {
|
|
229
263
|
// C13:能力性降级绝不走 LifecycleError —— 它是 DisplayStatus::Blocked + reason。
|
|
230
264
|
let ws = temp_ws();
|
|
231
|
-
let probe =
|
|
265
|
+
let probe =
|
|
266
|
+
probe_display_capabilities(&ws).expect("probe 平台降级必须是 typed outcome,不是 Err");
|
|
232
267
|
// 若 blocked,reason 必属 AdaptiveBlockReason 封闭集且与 status 一致。
|
|
233
268
|
match probe.adaptive_status {
|
|
234
|
-
DisplayStatus::Blocked =>
|
|
235
|
-
probe.reason.is_some(),
|
|
236
|
-
|
|
237
|
-
),
|
|
238
|
-
DisplayStatus::Opened => assert!(
|
|
239
|
-
probe.reason.is_none(),
|
|
240
|
-
"opened 不应带 block reason"
|
|
241
|
-
),
|
|
269
|
+
DisplayStatus::Blocked => {
|
|
270
|
+
assert!(probe.reason.is_some(), "blocked 必带 reason(C16 封闭集)")
|
|
271
|
+
}
|
|
272
|
+
DisplayStatus::Opened => assert!(probe.reason.is_none(), "opened 不应带 block reason"),
|
|
242
273
|
DisplayStatus::Stopped => {}
|
|
243
274
|
}
|
|
244
275
|
}
|
|
@@ -294,7 +325,11 @@ fn adaptive_block_reason_serde_names_match_python_and_set_is_exactly_six() {
|
|
|
294
325
|
.collect();
|
|
295
326
|
names.sort();
|
|
296
327
|
names.dedup();
|
|
297
|
-
assert_eq!(
|
|
328
|
+
assert_eq!(
|
|
329
|
+
names.len(),
|
|
330
|
+
6,
|
|
331
|
+
"ADAPTIVE_BLOCK_REASONS 恰 6 个,无重复无遗漏"
|
|
332
|
+
);
|
|
298
333
|
}
|
|
299
334
|
|
|
300
335
|
#[test]
|
|
@@ -332,8 +367,14 @@ fn adaptive_blocked_out_of_set_reason_bottoms_to_aggregator_rebuild_failed() {
|
|
|
332
367
|
#[test]
|
|
333
368
|
fn start_mode_serde_names_match_python_start_mode_strings() {
|
|
334
369
|
// 低价值 wire-format 守卫:start_mode ∈ {"resumed","fresh","fresh_after_missing_rollout","noop"}。
|
|
335
|
-
assert_eq!(
|
|
336
|
-
|
|
370
|
+
assert_eq!(
|
|
371
|
+
serde_json::to_string(&StartMode::Resumed).unwrap(),
|
|
372
|
+
"\"resumed\""
|
|
373
|
+
);
|
|
374
|
+
assert_eq!(
|
|
375
|
+
serde_json::to_string(&StartMode::Fresh).unwrap(),
|
|
376
|
+
"\"fresh\""
|
|
377
|
+
);
|
|
337
378
|
assert_eq!(
|
|
338
379
|
serde_json::to_string(&StartMode::FreshAfterMissingRollout).unwrap(),
|
|
339
380
|
"\"fresh_after_missing_rollout\""
|
|
@@ -368,7 +409,13 @@ fn decide_start_mode_codex_missing_rollout_with_allow_fresh_is_fresh_after_missi
|
|
|
368
409
|
);
|
|
369
410
|
// rollout 路径存在但文件已不在,同样命中。
|
|
370
411
|
assert_eq!(
|
|
371
|
-
decide_start_mode(
|
|
412
|
+
decide_start_mode(
|
|
413
|
+
"codex",
|
|
414
|
+
Some(&sid("s1")),
|
|
415
|
+
Some(&rp("/gone.jsonl")),
|
|
416
|
+
false,
|
|
417
|
+
true
|
|
418
|
+
),
|
|
372
419
|
StartMode::FreshAfterMissingRollout
|
|
373
420
|
);
|
|
374
421
|
}
|
|
@@ -385,7 +432,13 @@ fn decide_start_mode_codex_missing_rollout_without_allow_fresh_refuses() {
|
|
|
385
432
|
#[test]
|
|
386
433
|
fn decide_start_mode_codex_rollout_present_is_resumed_regardless_of_fresh() {
|
|
387
434
|
assert_eq!(
|
|
388
|
-
decide_start_mode(
|
|
435
|
+
decide_start_mode(
|
|
436
|
+
"codex",
|
|
437
|
+
Some(&sid("s1")),
|
|
438
|
+
Some(&rp("/r.jsonl")),
|
|
439
|
+
true,
|
|
440
|
+
false
|
|
441
|
+
),
|
|
389
442
|
StartMode::Resumed
|
|
390
443
|
);
|
|
391
444
|
assert_eq!(
|
|
@@ -431,15 +484,37 @@ fn decide_start_mode_checks_backing_for_all_resumable_providers() {
|
|
|
431
484
|
);
|
|
432
485
|
}
|
|
433
486
|
|
|
487
|
+
#[test]
|
|
488
|
+
fn decide_start_mode_refuses_session_id_for_nonresumable_providers() {
|
|
489
|
+
for provider in ["fake", "gemini_cli"] {
|
|
490
|
+
assert_eq!(
|
|
491
|
+
decide_start_mode(provider, Some(&sid("s1")), None, true, false),
|
|
492
|
+
StartMode::Noop,
|
|
493
|
+
"{provider} has a persisted session id but caps.resume=false, so it must not resume"
|
|
494
|
+
);
|
|
495
|
+
assert_eq!(
|
|
496
|
+
decide_start_mode(provider, Some(&sid("s1")), None, true, true),
|
|
497
|
+
StartMode::FreshAfterMissingRollout,
|
|
498
|
+
"{provider} may only fresh-start a persisted session id under explicit allow_fresh"
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
434
503
|
#[test]
|
|
435
504
|
fn resume_decision_serde_names_match_python() {
|
|
436
505
|
// 低价值 wire-format 守卫:_emit_resume_decisions: "resume"|"fresh_start"|"refuse"。
|
|
437
|
-
assert_eq!(
|
|
506
|
+
assert_eq!(
|
|
507
|
+
serde_json::to_string(&ResumeDecision::Resume).unwrap(),
|
|
508
|
+
"\"resume\""
|
|
509
|
+
);
|
|
438
510
|
assert_eq!(
|
|
439
511
|
serde_json::to_string(&ResumeDecision::FreshStart).unwrap(),
|
|
440
512
|
"\"fresh_start\""
|
|
441
513
|
);
|
|
442
|
-
assert_eq!(
|
|
514
|
+
assert_eq!(
|
|
515
|
+
serde_json::to_string(&ResumeDecision::Refuse).unwrap(),
|
|
516
|
+
"\"refuse\""
|
|
517
|
+
);
|
|
443
518
|
}
|
|
444
519
|
|
|
445
520
|
// ───────────────────────────────────────────────────────────────────────
|
|
@@ -472,7 +547,11 @@ fn classify_restart_plan_interacted_unresumable_no_allow_fresh_yields_refuse() {
|
|
|
472
547
|
.expect("纯验证不应 Err(资源型失败才走 LifecycleError)");
|
|
473
548
|
assert!(plan.corrupt_entries.is_empty(), "valid ISO 不应判 corrupt");
|
|
474
549
|
// 恰一条决策(每非 paused worker 一条),且为 Refuse。
|
|
475
|
-
assert_eq!(
|
|
550
|
+
assert_eq!(
|
|
551
|
+
plan.decisions.len(),
|
|
552
|
+
1,
|
|
553
|
+
"每非 paused worker 恰一条 resume_decision"
|
|
554
|
+
);
|
|
476
555
|
assert_eq!(plan.decisions[0].agent_id, aid("w1"));
|
|
477
556
|
assert_eq!(plan.decisions[0].decision, ResumeDecision::Refuse);
|
|
478
557
|
// unresumable 收口该 worker,reason 精确。
|
|
@@ -481,6 +560,30 @@ fn classify_restart_plan_interacted_unresumable_no_allow_fresh_yields_refuse() {
|
|
|
481
560
|
assert_eq!(plan.unresumable[0].reason, "no_persisted_session_id");
|
|
482
561
|
}
|
|
483
562
|
|
|
563
|
+
#[test]
|
|
564
|
+
fn classify_restart_plan_fake_stale_session_refuses_without_allow_fresh() {
|
|
565
|
+
let state = json!({
|
|
566
|
+
"session_name": "team-a",
|
|
567
|
+
"agents": {
|
|
568
|
+
"w1": {
|
|
569
|
+
"provider": "fake",
|
|
570
|
+
"first_send_at": "2026-05-27T10:00:00+00:00",
|
|
571
|
+
"session_id": "stale-fake-session"
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
let plan = classify_restart_plan(&state, false).expect("pure validation should not Err");
|
|
576
|
+
assert_eq!(plan.decisions.len(), 1);
|
|
577
|
+
assert_eq!(plan.decisions[0].decision, ResumeDecision::Refuse);
|
|
578
|
+
assert_eq!(plan.decisions[0].restart_mode, StartMode::Noop);
|
|
579
|
+
assert_eq!(plan.unresumable.len(), 1);
|
|
580
|
+
assert_eq!(plan.unresumable[0].reason, "session_unresumable");
|
|
581
|
+
assert_eq!(
|
|
582
|
+
plan.unresumable[0].session_id.as_ref().map(|s| s.as_str()),
|
|
583
|
+
Some("stale-fake-session")
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
484
587
|
#[test]
|
|
485
588
|
fn classify_restart_plan_interacted_unresumable_with_allow_fresh_yields_fresh_start_not_refuse() {
|
|
486
589
|
// 同一 worker,allow_fresh=true → FreshStart(可丢 context),unresumable 为空(无 atomic refusal)。
|
|
@@ -517,7 +620,11 @@ fn classify_restart_plan_never_interacted_null_session_refuses_not_fresh() {
|
|
|
517
620
|
ResumeDecision::Refuse,
|
|
518
621
|
"null-session 自启动 worker 默认必须 Refuse,不许静默 fresh"
|
|
519
622
|
);
|
|
520
|
-
assert_eq!(
|
|
623
|
+
assert_eq!(
|
|
624
|
+
plan.unresumable.len(),
|
|
625
|
+
1,
|
|
626
|
+
"Refuse 必入 unresumable(诚实出口)"
|
|
627
|
+
);
|
|
521
628
|
assert_eq!(plan.unresumable[0].reason, "no_persisted_session_id");
|
|
522
629
|
}
|
|
523
630
|
|
|
@@ -632,8 +739,10 @@ fn remove_agent_without_from_spec_is_refused_confirm() {
|
|
|
632
739
|
Ok(RemoveAgentOutcome::RefusedFromSpecConfirm { agent_id }) => {
|
|
633
740
|
assert_eq!(agent_id, aid("w1"));
|
|
634
741
|
}
|
|
635
|
-
// 若先撞 owner / 缺 state 门也可接受(门在 confirm 之前)。
|
|
636
|
-
Err(LifecycleError::OwnerRefused(_))
|
|
742
|
+
// 若先撞 owner / 缺 state / runtime-missing 门也可接受(门在 confirm 之前)。
|
|
743
|
+
Err(LifecycleError::OwnerRefused(_))
|
|
744
|
+
| Err(LifecycleError::RequirementUnmet(_))
|
|
745
|
+
| Err(LifecycleError::TeamSelect(_)) => {}
|
|
637
746
|
other => panic!("from_spec=false 应 RefusedFromSpecConfirm 或更上游门:{other:?}"),
|
|
638
747
|
}
|
|
639
748
|
}
|
|
@@ -756,10 +865,13 @@ fn restart_candidates_empty_workspace_is_empty_list() {
|
|
|
756
865
|
fn save_snapshot_writes_atomic_state_json_under_teams_dir() {
|
|
757
866
|
let ws = temp_ws();
|
|
758
867
|
let state = json!({"session_name": "team-alpha", "agents": {}});
|
|
759
|
-
let path =
|
|
760
|
-
.expect("bug-084:写路径返 Result,正常态须 Ok");
|
|
868
|
+
let path =
|
|
869
|
+
save_team_runtime_snapshot(&ws, &state).expect("bug-084:写路径返 Result,正常态须 Ok");
|
|
761
870
|
// 末段必为 state.json,且落在 runtime/teams/<safe session> 下。
|
|
762
|
-
assert_eq!(
|
|
871
|
+
assert_eq!(
|
|
872
|
+
path.file_name().and_then(|s| s.to_str()),
|
|
873
|
+
Some("state.json")
|
|
874
|
+
);
|
|
763
875
|
let s = path.to_string_lossy();
|
|
764
876
|
assert!(
|
|
765
877
|
s.contains("teams") && s.contains("team-alpha"),
|
|
@@ -884,7 +996,9 @@ fn leader_pane_env_invalid_format_writes_warning_event() {
|
|
|
884
996
|
validate_active_leader_pane_env_with_workspace(&transport, Some(&ws))
|
|
885
997
|
.expect("invalid format is warning-only");
|
|
886
998
|
|
|
887
|
-
let events = crate::event_log::EventLog::new(&ws)
|
|
999
|
+
let events = crate::event_log::EventLog::new(&ws)
|
|
1000
|
+
.tail(0)
|
|
1001
|
+
.expect("events");
|
|
888
1002
|
let event = events
|
|
889
1003
|
.iter()
|
|
890
1004
|
.find(|event| {
|
|
@@ -947,8 +1061,8 @@ fn leader_pane_env_cross_socket_live_wins() {
|
|
|
947
1061
|
#[test]
|
|
948
1062
|
fn leader_pane_env_cross_socket_dead_rejects_without_live() {
|
|
949
1063
|
let pane = crate::transport::PaneId::new("%404");
|
|
950
|
-
let absent =
|
|
951
|
-
.with_pane_presence("%404", false);
|
|
1064
|
+
let absent =
|
|
1065
|
+
crate::transport::test_support::OfflineTransport::new().with_pane_presence("%404", false);
|
|
952
1066
|
let dead = crate::transport::test_support::OfflineTransport::new()
|
|
953
1067
|
.with_liveness("%404", PaneLiveness::Dead);
|
|
954
1068
|
|
|
@@ -963,8 +1077,8 @@ fn leader_pane_env_cross_socket_dead_rejects_without_live() {
|
|
|
963
1077
|
#[test]
|
|
964
1078
|
fn leader_pane_env_cross_socket_absent_when_reachable_servers_confirm_missing() {
|
|
965
1079
|
let pane = crate::transport::PaneId::new("%9999");
|
|
966
|
-
let absent =
|
|
967
|
-
.with_pane_presence("%9999", false);
|
|
1080
|
+
let absent =
|
|
1081
|
+
crate::transport::test_support::OfflineTransport::new().with_pane_presence("%9999", false);
|
|
968
1082
|
let unknown = crate::transport::test_support::OfflineTransport::new()
|
|
969
1083
|
.with_default_liveness(PaneLiveness::Unknown);
|
|
970
1084
|
|
|
@@ -1008,13 +1122,17 @@ fn mcp_auto_approval_env_marks_leader_bypass_namespace_only() {
|
|
|
1008
1122
|
|
|
1009
1123
|
apply_mcp_auto_approval_env(&mut env, &safety);
|
|
1010
1124
|
|
|
1011
|
-
assert_eq!(
|
|
1125
|
+
assert_eq!(
|
|
1126
|
+
env.get("TEAM_AGENT_LEADER_BYPASS").map(String::as_str),
|
|
1127
|
+
Some("1")
|
|
1128
|
+
);
|
|
1012
1129
|
assert_eq!(
|
|
1013
1130
|
env.get("TEAM_AGENT_MCP_AUTO_APPROVE").map(String::as_str),
|
|
1014
1131
|
Some("team_orchestrator")
|
|
1015
1132
|
);
|
|
1016
1133
|
assert_eq!(
|
|
1017
|
-
env.get("TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE")
|
|
1134
|
+
env.get("TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE")
|
|
1135
|
+
.map(String::as_str),
|
|
1018
1136
|
Some("leader_bypass")
|
|
1019
1137
|
);
|
|
1020
1138
|
assert_eq!(
|
|
@@ -1030,7 +1148,10 @@ fn mcp_auto_approval_env_clears_when_leader_is_restricted() {
|
|
|
1030
1148
|
"TEAM_AGENT_MCP_AUTO_APPROVE".to_string(),
|
|
1031
1149
|
"team_orchestrator".to_string(),
|
|
1032
1150
|
),
|
|
1033
|
-
(
|
|
1151
|
+
(
|
|
1152
|
+
"TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE".to_string(),
|
|
1153
|
+
"leader_bypass".to_string(),
|
|
1154
|
+
),
|
|
1034
1155
|
]);
|
|
1035
1156
|
let safety = DangerousApproval {
|
|
1036
1157
|
enabled: false,
|
|
@@ -1045,7 +1166,10 @@ fn mcp_auto_approval_env_clears_when_leader_is_restricted() {
|
|
|
1045
1166
|
|
|
1046
1167
|
apply_mcp_auto_approval_env(&mut env, &safety);
|
|
1047
1168
|
|
|
1048
|
-
assert_eq!(
|
|
1169
|
+
assert_eq!(
|
|
1170
|
+
env.get("TEAM_AGENT_LEADER_BYPASS").map(String::as_str),
|
|
1171
|
+
Some("0")
|
|
1172
|
+
);
|
|
1049
1173
|
assert!(
|
|
1050
1174
|
!env.contains_key("TEAM_AGENT_MCP_AUTO_APPROVE"),
|
|
1051
1175
|
"restricted leader must not leave MCP auto-approval env behind: {env:?}"
|
|
@@ -1113,7 +1237,12 @@ fn open_worker_displays_blocked_probe_yields_typed_blocked_not_error() {
|
|
|
1113
1237
|
// 每个 worker 的 display 在 blocked 平台上应是 Blocked 变体(若有 worker)。
|
|
1114
1238
|
for (id, d) in rep.displays.iter() {
|
|
1115
1239
|
assert!(
|
|
1116
|
-
matches!(
|
|
1240
|
+
matches!(
|
|
1241
|
+
d,
|
|
1242
|
+
WorkerDisplay::Blocked {
|
|
1243
|
+
reason: AdaptiveBlockReason::NotImplementedThisPlatform
|
|
1244
|
+
}
|
|
1245
|
+
),
|
|
1117
1246
|
"worker {id} 在 windows 平台应 Blocked(not_implemented):{d:?}"
|
|
1118
1247
|
);
|
|
1119
1248
|
}
|
|
@@ -1130,7 +1259,11 @@ fn close_team_display_empty_workspace_closes_nothing_not_error() {
|
|
|
1130
1259
|
let ws = temp_ws();
|
|
1131
1260
|
let rep = close_team_display_backends(&ws, &sess("team-a"))
|
|
1132
1261
|
.expect("C9:无 recorded backend 应 Ok(空报告),不是 Err");
|
|
1133
|
-
assert!(
|
|
1262
|
+
assert!(
|
|
1263
|
+
rep.closed.is_empty(),
|
|
1264
|
+
"无 recorded backend 不应关任何东西:{:?}",
|
|
1265
|
+
rep.closed
|
|
1266
|
+
);
|
|
1134
1267
|
assert!(
|
|
1135
1268
|
rep.orphans_cleaned.is_empty(),
|
|
1136
1269
|
"空 workspace 无 orphan:{:?}",
|
|
@@ -1175,8 +1308,14 @@ fn fork_agent_on_unowned_workspace_does_not_silently_fork() {
|
|
|
1175
1308
|
#[test]
|
|
1176
1309
|
fn add_agent_event_name_constants_match_python_lifecycle_strings() {
|
|
1177
1310
|
// 锁死发射事件名(顺序被 porter 实现后的事件流锁死;此处锁名)。
|
|
1178
|
-
assert_eq!(
|
|
1179
|
-
|
|
1311
|
+
assert_eq!(
|
|
1312
|
+
event_names::ADD_STEP_COMPLETED,
|
|
1313
|
+
"lifecycle.add_step_completed"
|
|
1314
|
+
);
|
|
1315
|
+
assert_eq!(
|
|
1316
|
+
event_names::ADD_STEP_ROLLED_BACK,
|
|
1317
|
+
"lifecycle.add_step_rolled_back"
|
|
1318
|
+
);
|
|
1180
1319
|
assert_eq!(event_names::ADD_FAILED, "lifecycle.add_failed");
|
|
1181
1320
|
}
|
|
1182
1321
|
|
|
@@ -1237,7 +1376,10 @@ fn halt_plan_unknown_id_is_not_found_error() {
|
|
|
1237
1376
|
// 未持久化的 plan → "plan not found"(typed/Err),不幂等成 Halted。
|
|
1238
1377
|
match halt_plan(&ws, &pid, "user_requested") {
|
|
1239
1378
|
Err(LifecycleError::InvalidPlan(msg)) | Err(LifecycleError::TeamSelect(msg)) => {
|
|
1240
|
-
assert!(
|
|
1379
|
+
assert!(
|
|
1380
|
+
msg.contains("not found") || msg.contains("ghost-plan"),
|
|
1381
|
+
"got: {msg}"
|
|
1382
|
+
);
|
|
1241
1383
|
}
|
|
1242
1384
|
Ok(PlanProgress::Halted { .. }) => {
|
|
1243
1385
|
panic!("未找到的 plan 不得返回 Halted(应 not-found)")
|
|
@@ -1253,7 +1395,10 @@ fn plan_status_unknown_id_is_not_found() {
|
|
|
1253
1395
|
// 读未持久化 plan → not-found error(Rust 入口签名 Result<PlanState,_>)。
|
|
1254
1396
|
match plan_status(&ws, &pid) {
|
|
1255
1397
|
Err(LifecycleError::InvalidPlan(msg)) | Err(LifecycleError::TeamSelect(msg)) => {
|
|
1256
|
-
assert!(
|
|
1398
|
+
assert!(
|
|
1399
|
+
msg.contains("not found") || msg.contains("ghost-plan"),
|
|
1400
|
+
"got: {msg}"
|
|
1401
|
+
);
|
|
1257
1402
|
}
|
|
1258
1403
|
Ok(st) => panic!("未持久化 plan 不得返回 PlanState:{st:?}"),
|
|
1259
1404
|
other => panic!("意外结果:{other:?}"),
|