@team-agent/installer 0.3.12 → 0.3.14
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 +23 -0
- package/crates/team-agent/src/cli/leader.rs +2 -5
- package/crates/team-agent/src/cli/mod.rs +258 -79
- package/crates/team-agent/src/cli/tests/compile.rs +69 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +68 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +160 -2
- package/crates/team-agent/src/compiler.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +16 -83
- package/crates/team-agent/src/leader/lease.rs +4 -20
- package/crates/team-agent/src/leader/mod.rs +3 -1
- package/crates/team-agent/src/leader/owner_bind.rs +46 -48
- package/crates/team-agent/src/leader/provider_attribution.rs +214 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -3
- package/crates/team-agent/src/leader/rediscover.rs +34 -17
- package/crates/team-agent/src/leader/start.rs +279 -4
- package/crates/team-agent/src/lifecycle/launch.rs +279 -24
- package/crates/team-agent/src/lifecycle/restart/common.rs +50 -40
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +343 -49
- package/crates/team-agent/src/lifecycle/restart/selection.rs +9 -6
- package/crates/team-agent/src/lifecycle/tests/core.rs +181 -45
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +390 -61
- package/crates/team-agent/src/lifecycle/types.rs +28 -0
- package/crates/team-agent/src/messaging/delivery.rs +5 -1
- package/crates/team-agent/src/messaging/helpers.rs +1 -1
- package/crates/team-agent/src/messaging/tests/runtime.rs +14 -0
- package/crates/team-agent/src/provider/adapter.rs +6 -3
- package/crates/team-agent/src/provider/startup_prompt.rs +173 -0
- package/crates/team-agent/src/provider/testdata/copilot-ready-marker.txt +5 -0
- package/crates/team-agent/src/provider/testdata/copilot-trust-prompt.txt +19 -0
- package/crates/team-agent/src/tmux_backend.rs +5 -0
- package/crates/team-agent/src/transport/test_support.rs +22 -7
- package/crates/team-agent/src/transport.rs +4 -0
- package/package.json +4 -4
|
@@ -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
|
}
|
|
@@ -228,17 +255,14 @@ fn resolve_backend_recorded_used_when_no_request() {
|
|
|
228
255
|
fn probe_never_errors_platform_degradation_is_typed_outcome() {
|
|
229
256
|
// C13:能力性降级绝不走 LifecycleError —— 它是 DisplayStatus::Blocked + reason。
|
|
230
257
|
let ws = temp_ws();
|
|
231
|
-
let probe =
|
|
258
|
+
let probe =
|
|
259
|
+
probe_display_capabilities(&ws).expect("probe 平台降级必须是 typed outcome,不是 Err");
|
|
232
260
|
// 若 blocked,reason 必属 AdaptiveBlockReason 封闭集且与 status 一致。
|
|
233
261
|
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
|
-
),
|
|
262
|
+
DisplayStatus::Blocked => {
|
|
263
|
+
assert!(probe.reason.is_some(), "blocked 必带 reason(C16 封闭集)")
|
|
264
|
+
}
|
|
265
|
+
DisplayStatus::Opened => assert!(probe.reason.is_none(), "opened 不应带 block reason"),
|
|
242
266
|
DisplayStatus::Stopped => {}
|
|
243
267
|
}
|
|
244
268
|
}
|
|
@@ -294,7 +318,11 @@ fn adaptive_block_reason_serde_names_match_python_and_set_is_exactly_six() {
|
|
|
294
318
|
.collect();
|
|
295
319
|
names.sort();
|
|
296
320
|
names.dedup();
|
|
297
|
-
assert_eq!(
|
|
321
|
+
assert_eq!(
|
|
322
|
+
names.len(),
|
|
323
|
+
6,
|
|
324
|
+
"ADAPTIVE_BLOCK_REASONS 恰 6 个,无重复无遗漏"
|
|
325
|
+
);
|
|
298
326
|
}
|
|
299
327
|
|
|
300
328
|
#[test]
|
|
@@ -332,8 +360,14 @@ fn adaptive_blocked_out_of_set_reason_bottoms_to_aggregator_rebuild_failed() {
|
|
|
332
360
|
#[test]
|
|
333
361
|
fn start_mode_serde_names_match_python_start_mode_strings() {
|
|
334
362
|
// 低价值 wire-format 守卫:start_mode ∈ {"resumed","fresh","fresh_after_missing_rollout","noop"}。
|
|
335
|
-
assert_eq!(
|
|
336
|
-
|
|
363
|
+
assert_eq!(
|
|
364
|
+
serde_json::to_string(&StartMode::Resumed).unwrap(),
|
|
365
|
+
"\"resumed\""
|
|
366
|
+
);
|
|
367
|
+
assert_eq!(
|
|
368
|
+
serde_json::to_string(&StartMode::Fresh).unwrap(),
|
|
369
|
+
"\"fresh\""
|
|
370
|
+
);
|
|
337
371
|
assert_eq!(
|
|
338
372
|
serde_json::to_string(&StartMode::FreshAfterMissingRollout).unwrap(),
|
|
339
373
|
"\"fresh_after_missing_rollout\""
|
|
@@ -368,7 +402,13 @@ fn decide_start_mode_codex_missing_rollout_with_allow_fresh_is_fresh_after_missi
|
|
|
368
402
|
);
|
|
369
403
|
// rollout 路径存在但文件已不在,同样命中。
|
|
370
404
|
assert_eq!(
|
|
371
|
-
decide_start_mode(
|
|
405
|
+
decide_start_mode(
|
|
406
|
+
"codex",
|
|
407
|
+
Some(&sid("s1")),
|
|
408
|
+
Some(&rp("/gone.jsonl")),
|
|
409
|
+
false,
|
|
410
|
+
true
|
|
411
|
+
),
|
|
372
412
|
StartMode::FreshAfterMissingRollout
|
|
373
413
|
);
|
|
374
414
|
}
|
|
@@ -385,7 +425,13 @@ fn decide_start_mode_codex_missing_rollout_without_allow_fresh_refuses() {
|
|
|
385
425
|
#[test]
|
|
386
426
|
fn decide_start_mode_codex_rollout_present_is_resumed_regardless_of_fresh() {
|
|
387
427
|
assert_eq!(
|
|
388
|
-
decide_start_mode(
|
|
428
|
+
decide_start_mode(
|
|
429
|
+
"codex",
|
|
430
|
+
Some(&sid("s1")),
|
|
431
|
+
Some(&rp("/r.jsonl")),
|
|
432
|
+
true,
|
|
433
|
+
false
|
|
434
|
+
),
|
|
389
435
|
StartMode::Resumed
|
|
390
436
|
);
|
|
391
437
|
assert_eq!(
|
|
@@ -431,15 +477,37 @@ fn decide_start_mode_checks_backing_for_all_resumable_providers() {
|
|
|
431
477
|
);
|
|
432
478
|
}
|
|
433
479
|
|
|
480
|
+
#[test]
|
|
481
|
+
fn decide_start_mode_refuses_session_id_for_nonresumable_providers() {
|
|
482
|
+
for provider in ["fake", "gemini_cli"] {
|
|
483
|
+
assert_eq!(
|
|
484
|
+
decide_start_mode(provider, Some(&sid("s1")), None, true, false),
|
|
485
|
+
StartMode::Noop,
|
|
486
|
+
"{provider} has a persisted session id but caps.resume=false, so it must not resume"
|
|
487
|
+
);
|
|
488
|
+
assert_eq!(
|
|
489
|
+
decide_start_mode(provider, Some(&sid("s1")), None, true, true),
|
|
490
|
+
StartMode::FreshAfterMissingRollout,
|
|
491
|
+
"{provider} may only fresh-start a persisted session id under explicit allow_fresh"
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
434
496
|
#[test]
|
|
435
497
|
fn resume_decision_serde_names_match_python() {
|
|
436
498
|
// 低价值 wire-format 守卫:_emit_resume_decisions: "resume"|"fresh_start"|"refuse"。
|
|
437
|
-
assert_eq!(
|
|
499
|
+
assert_eq!(
|
|
500
|
+
serde_json::to_string(&ResumeDecision::Resume).unwrap(),
|
|
501
|
+
"\"resume\""
|
|
502
|
+
);
|
|
438
503
|
assert_eq!(
|
|
439
504
|
serde_json::to_string(&ResumeDecision::FreshStart).unwrap(),
|
|
440
505
|
"\"fresh_start\""
|
|
441
506
|
);
|
|
442
|
-
assert_eq!(
|
|
507
|
+
assert_eq!(
|
|
508
|
+
serde_json::to_string(&ResumeDecision::Refuse).unwrap(),
|
|
509
|
+
"\"refuse\""
|
|
510
|
+
);
|
|
443
511
|
}
|
|
444
512
|
|
|
445
513
|
// ───────────────────────────────────────────────────────────────────────
|
|
@@ -472,7 +540,11 @@ fn classify_restart_plan_interacted_unresumable_no_allow_fresh_yields_refuse() {
|
|
|
472
540
|
.expect("纯验证不应 Err(资源型失败才走 LifecycleError)");
|
|
473
541
|
assert!(plan.corrupt_entries.is_empty(), "valid ISO 不应判 corrupt");
|
|
474
542
|
// 恰一条决策(每非 paused worker 一条),且为 Refuse。
|
|
475
|
-
assert_eq!(
|
|
543
|
+
assert_eq!(
|
|
544
|
+
plan.decisions.len(),
|
|
545
|
+
1,
|
|
546
|
+
"每非 paused worker 恰一条 resume_decision"
|
|
547
|
+
);
|
|
476
548
|
assert_eq!(plan.decisions[0].agent_id, aid("w1"));
|
|
477
549
|
assert_eq!(plan.decisions[0].decision, ResumeDecision::Refuse);
|
|
478
550
|
// unresumable 收口该 worker,reason 精确。
|
|
@@ -481,6 +553,30 @@ fn classify_restart_plan_interacted_unresumable_no_allow_fresh_yields_refuse() {
|
|
|
481
553
|
assert_eq!(plan.unresumable[0].reason, "no_persisted_session_id");
|
|
482
554
|
}
|
|
483
555
|
|
|
556
|
+
#[test]
|
|
557
|
+
fn classify_restart_plan_fake_stale_session_refuses_without_allow_fresh() {
|
|
558
|
+
let state = json!({
|
|
559
|
+
"session_name": "team-a",
|
|
560
|
+
"agents": {
|
|
561
|
+
"w1": {
|
|
562
|
+
"provider": "fake",
|
|
563
|
+
"first_send_at": "2026-05-27T10:00:00+00:00",
|
|
564
|
+
"session_id": "stale-fake-session"
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
let plan = classify_restart_plan(&state, false).expect("pure validation should not Err");
|
|
569
|
+
assert_eq!(plan.decisions.len(), 1);
|
|
570
|
+
assert_eq!(plan.decisions[0].decision, ResumeDecision::Refuse);
|
|
571
|
+
assert_eq!(plan.decisions[0].restart_mode, StartMode::Noop);
|
|
572
|
+
assert_eq!(plan.unresumable.len(), 1);
|
|
573
|
+
assert_eq!(plan.unresumable[0].reason, "session_unresumable");
|
|
574
|
+
assert_eq!(
|
|
575
|
+
plan.unresumable[0].session_id.as_ref().map(|s| s.as_str()),
|
|
576
|
+
Some("stale-fake-session")
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
484
580
|
#[test]
|
|
485
581
|
fn classify_restart_plan_interacted_unresumable_with_allow_fresh_yields_fresh_start_not_refuse() {
|
|
486
582
|
// 同一 worker,allow_fresh=true → FreshStart(可丢 context),unresumable 为空(无 atomic refusal)。
|
|
@@ -517,7 +613,11 @@ fn classify_restart_plan_never_interacted_null_session_refuses_not_fresh() {
|
|
|
517
613
|
ResumeDecision::Refuse,
|
|
518
614
|
"null-session 自启动 worker 默认必须 Refuse,不许静默 fresh"
|
|
519
615
|
);
|
|
520
|
-
assert_eq!(
|
|
616
|
+
assert_eq!(
|
|
617
|
+
plan.unresumable.len(),
|
|
618
|
+
1,
|
|
619
|
+
"Refuse 必入 unresumable(诚实出口)"
|
|
620
|
+
);
|
|
521
621
|
assert_eq!(plan.unresumable[0].reason, "no_persisted_session_id");
|
|
522
622
|
}
|
|
523
623
|
|
|
@@ -756,10 +856,13 @@ fn restart_candidates_empty_workspace_is_empty_list() {
|
|
|
756
856
|
fn save_snapshot_writes_atomic_state_json_under_teams_dir() {
|
|
757
857
|
let ws = temp_ws();
|
|
758
858
|
let state = json!({"session_name": "team-alpha", "agents": {}});
|
|
759
|
-
let path =
|
|
760
|
-
.expect("bug-084:写路径返 Result,正常态须 Ok");
|
|
859
|
+
let path =
|
|
860
|
+
save_team_runtime_snapshot(&ws, &state).expect("bug-084:写路径返 Result,正常态须 Ok");
|
|
761
861
|
// 末段必为 state.json,且落在 runtime/teams/<safe session> 下。
|
|
762
|
-
assert_eq!(
|
|
862
|
+
assert_eq!(
|
|
863
|
+
path.file_name().and_then(|s| s.to_str()),
|
|
864
|
+
Some("state.json")
|
|
865
|
+
);
|
|
763
866
|
let s = path.to_string_lossy();
|
|
764
867
|
assert!(
|
|
765
868
|
s.contains("teams") && s.contains("team-alpha"),
|
|
@@ -884,7 +987,9 @@ fn leader_pane_env_invalid_format_writes_warning_event() {
|
|
|
884
987
|
validate_active_leader_pane_env_with_workspace(&transport, Some(&ws))
|
|
885
988
|
.expect("invalid format is warning-only");
|
|
886
989
|
|
|
887
|
-
let events = crate::event_log::EventLog::new(&ws)
|
|
990
|
+
let events = crate::event_log::EventLog::new(&ws)
|
|
991
|
+
.tail(0)
|
|
992
|
+
.expect("events");
|
|
888
993
|
let event = events
|
|
889
994
|
.iter()
|
|
890
995
|
.find(|event| {
|
|
@@ -947,8 +1052,8 @@ fn leader_pane_env_cross_socket_live_wins() {
|
|
|
947
1052
|
#[test]
|
|
948
1053
|
fn leader_pane_env_cross_socket_dead_rejects_without_live() {
|
|
949
1054
|
let pane = crate::transport::PaneId::new("%404");
|
|
950
|
-
let absent =
|
|
951
|
-
.with_pane_presence("%404", false);
|
|
1055
|
+
let absent =
|
|
1056
|
+
crate::transport::test_support::OfflineTransport::new().with_pane_presence("%404", false);
|
|
952
1057
|
let dead = crate::transport::test_support::OfflineTransport::new()
|
|
953
1058
|
.with_liveness("%404", PaneLiveness::Dead);
|
|
954
1059
|
|
|
@@ -963,8 +1068,8 @@ fn leader_pane_env_cross_socket_dead_rejects_without_live() {
|
|
|
963
1068
|
#[test]
|
|
964
1069
|
fn leader_pane_env_cross_socket_absent_when_reachable_servers_confirm_missing() {
|
|
965
1070
|
let pane = crate::transport::PaneId::new("%9999");
|
|
966
|
-
let absent =
|
|
967
|
-
.with_pane_presence("%9999", false);
|
|
1071
|
+
let absent =
|
|
1072
|
+
crate::transport::test_support::OfflineTransport::new().with_pane_presence("%9999", false);
|
|
968
1073
|
let unknown = crate::transport::test_support::OfflineTransport::new()
|
|
969
1074
|
.with_default_liveness(PaneLiveness::Unknown);
|
|
970
1075
|
|
|
@@ -1008,13 +1113,17 @@ fn mcp_auto_approval_env_marks_leader_bypass_namespace_only() {
|
|
|
1008
1113
|
|
|
1009
1114
|
apply_mcp_auto_approval_env(&mut env, &safety);
|
|
1010
1115
|
|
|
1011
|
-
assert_eq!(
|
|
1116
|
+
assert_eq!(
|
|
1117
|
+
env.get("TEAM_AGENT_LEADER_BYPASS").map(String::as_str),
|
|
1118
|
+
Some("1")
|
|
1119
|
+
);
|
|
1012
1120
|
assert_eq!(
|
|
1013
1121
|
env.get("TEAM_AGENT_MCP_AUTO_APPROVE").map(String::as_str),
|
|
1014
1122
|
Some("team_orchestrator")
|
|
1015
1123
|
);
|
|
1016
1124
|
assert_eq!(
|
|
1017
|
-
env.get("TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE")
|
|
1125
|
+
env.get("TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE")
|
|
1126
|
+
.map(String::as_str),
|
|
1018
1127
|
Some("leader_bypass")
|
|
1019
1128
|
);
|
|
1020
1129
|
assert_eq!(
|
|
@@ -1030,7 +1139,10 @@ fn mcp_auto_approval_env_clears_when_leader_is_restricted() {
|
|
|
1030
1139
|
"TEAM_AGENT_MCP_AUTO_APPROVE".to_string(),
|
|
1031
1140
|
"team_orchestrator".to_string(),
|
|
1032
1141
|
),
|
|
1033
|
-
(
|
|
1142
|
+
(
|
|
1143
|
+
"TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE".to_string(),
|
|
1144
|
+
"leader_bypass".to_string(),
|
|
1145
|
+
),
|
|
1034
1146
|
]);
|
|
1035
1147
|
let safety = DangerousApproval {
|
|
1036
1148
|
enabled: false,
|
|
@@ -1045,7 +1157,10 @@ fn mcp_auto_approval_env_clears_when_leader_is_restricted() {
|
|
|
1045
1157
|
|
|
1046
1158
|
apply_mcp_auto_approval_env(&mut env, &safety);
|
|
1047
1159
|
|
|
1048
|
-
assert_eq!(
|
|
1160
|
+
assert_eq!(
|
|
1161
|
+
env.get("TEAM_AGENT_LEADER_BYPASS").map(String::as_str),
|
|
1162
|
+
Some("0")
|
|
1163
|
+
);
|
|
1049
1164
|
assert!(
|
|
1050
1165
|
!env.contains_key("TEAM_AGENT_MCP_AUTO_APPROVE"),
|
|
1051
1166
|
"restricted leader must not leave MCP auto-approval env behind: {env:?}"
|
|
@@ -1113,7 +1228,12 @@ fn open_worker_displays_blocked_probe_yields_typed_blocked_not_error() {
|
|
|
1113
1228
|
// 每个 worker 的 display 在 blocked 平台上应是 Blocked 变体(若有 worker)。
|
|
1114
1229
|
for (id, d) in rep.displays.iter() {
|
|
1115
1230
|
assert!(
|
|
1116
|
-
matches!(
|
|
1231
|
+
matches!(
|
|
1232
|
+
d,
|
|
1233
|
+
WorkerDisplay::Blocked {
|
|
1234
|
+
reason: AdaptiveBlockReason::NotImplementedThisPlatform
|
|
1235
|
+
}
|
|
1236
|
+
),
|
|
1117
1237
|
"worker {id} 在 windows 平台应 Blocked(not_implemented):{d:?}"
|
|
1118
1238
|
);
|
|
1119
1239
|
}
|
|
@@ -1130,7 +1250,11 @@ fn close_team_display_empty_workspace_closes_nothing_not_error() {
|
|
|
1130
1250
|
let ws = temp_ws();
|
|
1131
1251
|
let rep = close_team_display_backends(&ws, &sess("team-a"))
|
|
1132
1252
|
.expect("C9:无 recorded backend 应 Ok(空报告),不是 Err");
|
|
1133
|
-
assert!(
|
|
1253
|
+
assert!(
|
|
1254
|
+
rep.closed.is_empty(),
|
|
1255
|
+
"无 recorded backend 不应关任何东西:{:?}",
|
|
1256
|
+
rep.closed
|
|
1257
|
+
);
|
|
1134
1258
|
assert!(
|
|
1135
1259
|
rep.orphans_cleaned.is_empty(),
|
|
1136
1260
|
"空 workspace 无 orphan:{:?}",
|
|
@@ -1175,8 +1299,14 @@ fn fork_agent_on_unowned_workspace_does_not_silently_fork() {
|
|
|
1175
1299
|
#[test]
|
|
1176
1300
|
fn add_agent_event_name_constants_match_python_lifecycle_strings() {
|
|
1177
1301
|
// 锁死发射事件名(顺序被 porter 实现后的事件流锁死;此处锁名)。
|
|
1178
|
-
assert_eq!(
|
|
1179
|
-
|
|
1302
|
+
assert_eq!(
|
|
1303
|
+
event_names::ADD_STEP_COMPLETED,
|
|
1304
|
+
"lifecycle.add_step_completed"
|
|
1305
|
+
);
|
|
1306
|
+
assert_eq!(
|
|
1307
|
+
event_names::ADD_STEP_ROLLED_BACK,
|
|
1308
|
+
"lifecycle.add_step_rolled_back"
|
|
1309
|
+
);
|
|
1180
1310
|
assert_eq!(event_names::ADD_FAILED, "lifecycle.add_failed");
|
|
1181
1311
|
}
|
|
1182
1312
|
|
|
@@ -1237,7 +1367,10 @@ fn halt_plan_unknown_id_is_not_found_error() {
|
|
|
1237
1367
|
// 未持久化的 plan → "plan not found"(typed/Err),不幂等成 Halted。
|
|
1238
1368
|
match halt_plan(&ws, &pid, "user_requested") {
|
|
1239
1369
|
Err(LifecycleError::InvalidPlan(msg)) | Err(LifecycleError::TeamSelect(msg)) => {
|
|
1240
|
-
assert!(
|
|
1370
|
+
assert!(
|
|
1371
|
+
msg.contains("not found") || msg.contains("ghost-plan"),
|
|
1372
|
+
"got: {msg}"
|
|
1373
|
+
);
|
|
1241
1374
|
}
|
|
1242
1375
|
Ok(PlanProgress::Halted { .. }) => {
|
|
1243
1376
|
panic!("未找到的 plan 不得返回 Halted(应 not-found)")
|
|
@@ -1253,7 +1386,10 @@ fn plan_status_unknown_id_is_not_found() {
|
|
|
1253
1386
|
// 读未持久化 plan → not-found error(Rust 入口签名 Result<PlanState,_>)。
|
|
1254
1387
|
match plan_status(&ws, &pid) {
|
|
1255
1388
|
Err(LifecycleError::InvalidPlan(msg)) | Err(LifecycleError::TeamSelect(msg)) => {
|
|
1256
|
-
assert!(
|
|
1389
|
+
assert!(
|
|
1390
|
+
msg.contains("not found") || msg.contains("ghost-plan"),
|
|
1391
|
+
"got: {msg}"
|
|
1392
|
+
);
|
|
1257
1393
|
}
|
|
1258
1394
|
Ok(st) => panic!("未持久化 plan 不得返回 PlanState:{st:?}"),
|
|
1259
1395
|
other => panic!("意外结果:{other:?}"),
|