@team-agent/installer 0.5.98 → 0.5.100
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 -163
- package/crates/team-agent/src/cli/diagnose.rs +3 -19
- package/crates/team-agent/src/cli/emit.rs +20 -204
- package/crates/team-agent/src/cli/mod.rs +138 -205
- package/crates/team-agent/src/cli/send/coordinator.rs +2 -2
- package/crates/team-agent/src/cli/spec.rs +41 -67
- package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -186
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +9 -10
- package/crates/team-agent/src/cli/tests/run_delegation.rs +0 -3
- package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -1
- package/crates/team-agent/src/cli/triage.rs +1 -1
- package/crates/team-agent/src/cli/types.rs +0 -55
- package/crates/team-agent/src/compiler/tests.rs +5 -8
- package/crates/team-agent/src/compiler.rs +6 -14
- package/crates/team-agent/src/leader/lease.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +5 -25
- package/crates/team-agent/src/lifecycle/launch/identity.rs +2 -18
- package/crates/team-agent/src/lifecycle/launch/layout.rs +7 -29
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +7 -7
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +12 -49
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +3 -9
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -61
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +2 -15
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +3 -7
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +3 -4
- package/crates/team-agent/src/lifecycle/mod.rs +0 -8
- package/crates/team-agent/src/lifecycle/restart/agent.rs +62 -8
- package/crates/team-agent/src/lifecycle/restart/common.rs +0 -32
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +6 -15
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +1 -2
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +0 -1
- package/crates/team-agent/src/lifecycle/tests/core.rs +0 -213
- package/crates/team-agent/src/lifecycle/tests/issue238_codex_effort_contract.rs +96 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +31 -77
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +4 -8
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +16 -9
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +0 -60
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +3 -4
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests.rs +1 -2
- package/crates/team-agent/src/lifecycle/types.rs +3 -133
- package/crates/team-agent/src/messaging/send.rs +2 -2
- package/crates/team-agent/src/model/enums.rs +21 -71
- package/crates/team-agent/src/model/spec.rs +5 -20
- package/crates/team-agent/src/provider/adapters/codex.rs +2 -2
- package/crates/team-agent/src/provider/types.rs +7 -6
- package/crates/team-agent/src/state/repository.rs +0 -5
- package/examples/team.spec.yaml +0 -1
- package/package.json +4 -4
- package/schemas/team.schema.json +2 -2
- package/skills/team-agent/SKILL.md +1 -1
- package/skills/team-agent/references/recovery-runbook.md +2 -2
- package/skills/team-agent/references/team-agent-operator.md +3 -10
- package/skills/team-agent/references/team-in-team.md +0 -6
- package/crates/team-agent/src/lifecycle/display.rs +0 -550
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +0 -884
- package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +0 -481
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
//! what: 按 team 参数或唯一性选出重启目标
|
|
15
15
|
//! depends:
|
|
16
16
|
//! - crate::lifecycle::lock
|
|
17
|
-
//! - crate::lifecycle::display
|
|
18
17
|
//! - crate::lifecycle::restart::selection
|
|
19
18
|
//! - crate::lifecycle::restart::preflight
|
|
20
19
|
//! - crate::lifecycle::restart::remove
|
|
@@ -337,7 +336,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
337
336
|
.first()
|
|
338
337
|
.cloned()
|
|
339
338
|
.unwrap_or_else(|| "dirty_topology".to_string()),
|
|
340
|
-
error: "restart refused: tmux endpoint/socket topology is inconsistent; run
|
|
339
|
+
error: "restart refused: tmux endpoint/socket topology is inconsistent; run doctor from the intended leader socket before restarting".to_string(),
|
|
341
340
|
issue_ids: topology_issue_ids,
|
|
342
341
|
});
|
|
343
342
|
}
|
|
@@ -1367,14 +1366,6 @@ fn restart_with_selected_team_and_transport(
|
|
|
1367
1366
|
Some(selected.team_key.as_str()),
|
|
1368
1367
|
&state,
|
|
1369
1368
|
);
|
|
1370
|
-
if let Ok(probe) = crate::lifecycle::display::probe_display_capabilities(&selected.run_workspace)
|
|
1371
|
-
{
|
|
1372
|
-
let _ = crate::lifecycle::display::rebuild_adaptive_display_after_rebind(
|
|
1373
|
-
&selected.run_workspace,
|
|
1374
|
-
&session_name,
|
|
1375
|
-
&probe,
|
|
1376
|
-
);
|
|
1377
|
-
}
|
|
1378
1369
|
// 0.3.28 Step 1: topology invariant guard (warn-only). Same pattern as
|
|
1379
1370
|
// `lifecycle::launch::launch_with_transport_in_workspace` — logs to stderr,
|
|
1380
1371
|
// never panics. Hard error path is deferred to Step 10.
|
|
@@ -1813,7 +1804,7 @@ fn restart_readiness_timeout_message(
|
|
|
1813
1804
|
- tmux session created: {session}\n\
|
|
1814
1805
|
- worker pane addressable: {pane}\n\
|
|
1815
1806
|
- coordinator alive: {coordinator}\n\
|
|
1816
|
-
Action: check coordinator log {log}, then inspect this workspace with `team-agent
|
|
1807
|
+
Action: check coordinator log {log}, then inspect this workspace with `team-agent doctor --workspace '{workspace}'`\n\
|
|
1817
1808
|
Log: coordinator_log={log} state={state} pid_file={pid}",
|
|
1818
1809
|
missing = restart_readiness_missing_summary(readiness),
|
|
1819
1810
|
session = yes_no(readiness.session_created),
|
|
@@ -2712,7 +2703,7 @@ fn restart_failure_phase(
|
|
|
2712
2703
|
// 0.5.39 Slice 1 (tmux-server-death-locate §11.1 B): promote
|
|
2713
2704
|
// spawn/readiness errors whose stderr contains "server exited
|
|
2714
2705
|
// unexpectedly" to `tmux_server_crashed`, so downstream event
|
|
2715
|
-
// classification and
|
|
2706
|
+
// classification and doctor next_actions ("team-agent doctor")
|
|
2716
2707
|
// don't misattribute a whole-server death to a per-agent provider
|
|
2717
2708
|
// failure. Restart itself does not recover from server crashes in
|
|
2718
2709
|
// 0.5.39 (§11.1 C is Slice 3, later car); it just classifies clearly.
|
|
@@ -2785,7 +2776,7 @@ fn write_restart_agent_failed_event(
|
|
|
2785
2776
|
"phase": phase,
|
|
2786
2777
|
"error": error,
|
|
2787
2778
|
"action": format!(
|
|
2788
|
-
"inspect worker {} output, then
|
|
2779
|
+
"inspect worker {} output, then reset that worker with `team-agent reset-agent {} --discard-session` or rerun `team-agent restart --allow-fresh`",
|
|
2789
2780
|
decision.agent_id,
|
|
2790
2781
|
decision.agent_id
|
|
2791
2782
|
),
|
|
@@ -3271,7 +3262,7 @@ fn restart_failure_next_actions(failed_agents: &[RestartFailedAgent]) -> Vec<Str
|
|
|
3271
3262
|
.iter()
|
|
3272
3263
|
.map(|failure| {
|
|
3273
3264
|
format!(
|
|
3274
|
-
"inspect worker {} output, then
|
|
3265
|
+
"inspect worker {} output, then reset that worker with `team-agent reset-agent {} --discard-session` or rerun `team-agent restart --allow-fresh`",
|
|
3275
3266
|
failure.agent_id, failure.agent_id
|
|
3276
3267
|
)
|
|
3277
3268
|
})
|
|
@@ -4920,7 +4911,7 @@ tasks:
|
|
|
4920
4911
|
},
|
|
4921
4912
|
std::time::Duration::from_secs(30),
|
|
4922
4913
|
);
|
|
4923
|
-
assert!(message.contains("team-agent
|
|
4914
|
+
assert!(message.contains("team-agent doctor --workspace"));
|
|
4924
4915
|
assert!(message.contains(workspace.to_string_lossy().as_ref()));
|
|
4925
4916
|
assert!(!message.contains("--allow-fresh"));
|
|
4926
4917
|
assert!(!message.contains("restart <agent>"));
|
|
@@ -122,7 +122,7 @@ fn stale_snapshot_cannot_flip_status_or_diagnose_ok_readiness() {
|
|
|
122
122
|
"--json",
|
|
123
123
|
]);
|
|
124
124
|
let diagnose_before = case.run_json(&[
|
|
125
|
-
"
|
|
125
|
+
"doctor",
|
|
126
126
|
"--workspace",
|
|
127
127
|
case.workspace_str(),
|
|
128
128
|
"--team",
|
|
@@ -142,7 +142,7 @@ fn stale_snapshot_cannot_flip_status_or_diagnose_ok_readiness() {
|
|
|
142
142
|
"--json",
|
|
143
143
|
]);
|
|
144
144
|
let diagnose_after = case.run_json(&[
|
|
145
|
-
"
|
|
145
|
+
"doctor",
|
|
146
146
|
"--workspace",
|
|
147
147
|
case.workspace_str(),
|
|
148
148
|
"--team",
|
|
@@ -182,7 +182,7 @@ fn diagnose_missing_profile_is_rc1_ok_false_and_zero_token() {
|
|
|
182
182
|
.unwrap();
|
|
183
183
|
|
|
184
184
|
let output = Command::new(crate::lifecycle::tests::test_binary_path())
|
|
185
|
-
.args(["
|
|
185
|
+
.args(["doctor", team.to_str().unwrap(), "--workspace", ws.to_str().unwrap(), "--json"])
|
|
186
186
|
.current_dir(&team)
|
|
187
187
|
.output()
|
|
188
188
|
.expect("diagnose subprocess should run");
|
|
@@ -184,7 +184,7 @@ fn write_team_docs(workspace: &Path) {
|
|
|
184
184
|
std::fs::write(
|
|
185
185
|
workspace.join("TEAM.md"),
|
|
186
186
|
format!(
|
|
187
|
-
"---\nname: {TEAM_NAME}\nobjective: clone-agent tools preservation\nprovider: fake\
|
|
187
|
+
"---\nname: {TEAM_NAME}\nobjective: clone-agent tools preservation\nprovider: fake\n---\n"
|
|
188
188
|
),
|
|
189
189
|
)
|
|
190
190
|
.expect("TEAM.md");
|
|
@@ -324,7 +324,6 @@ fn clone_ok(case: &Case, source: &str, dest: &str, phase: &str) {
|
|
|
324
324
|
case.ws(),
|
|
325
325
|
"--team",
|
|
326
326
|
TEAM_NAME,
|
|
327
|
-
"--no-display",
|
|
328
327
|
"--json",
|
|
329
328
|
]);
|
|
330
329
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
@@ -211,159 +211,6 @@ fn plan_condition_rejects_out_of_grammar() {
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
// ───────────────────────────────────────────────────────────────────────
|
|
215
|
-
// resolve_display_backend — display/backend.py
|
|
216
|
-
// 默认 adaptive;显式 none 是逃生口。
|
|
217
|
-
// ───────────────────────────────────────────────────────────────────────
|
|
218
|
-
|
|
219
|
-
#[test]
|
|
220
|
-
fn resolve_backend_defaults_to_adaptive_when_none_requested() {
|
|
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);
|
|
229
|
-
assert_eq!(r.backend, DisplayBackend::None);
|
|
230
|
-
assert!(r.non_default, "显式 none 是偏离默认 adaptive 的逃生口");
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
#[test]
|
|
234
|
-
fn resolve_backend_requested_overrides_and_marks_non_default() {
|
|
235
|
-
// 显式 requested 非 adaptive → 采用且 non_default=true(非静默发事件)。
|
|
236
|
-
let r = resolve_display_backend(Some(DisplayBackend::GhosttyWindow), None);
|
|
237
|
-
assert_eq!(r.backend, DisplayBackend::GhosttyWindow);
|
|
238
|
-
assert!(r.non_default);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
#[test]
|
|
242
|
-
fn resolve_backend_recorded_used_when_no_request() {
|
|
243
|
-
// 无 requested 但 state 有 recorded → 复用 recorded(restart 一致性)。
|
|
244
|
-
let r = resolve_display_backend(None, Some(DisplayBackend::GhosttyWorkspace));
|
|
245
|
-
assert_eq!(r.backend, DisplayBackend::GhosttyWorkspace);
|
|
246
|
-
assert!(r.non_default);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// ───────────────────────────────────────────────────────────────────────
|
|
250
|
-
// probe_display_capabilities — display/adaptive.py:31 (C13)
|
|
251
|
-
// 分支只看 probe 结果,NOT cfg!(target_os)。golden 实跑:
|
|
252
|
-
// linux no-tmux : in_tmux=false, adaptive_status=leader_not_in_tmux, reason=leader_not_in_tmux
|
|
253
|
-
// linux in-tmux : in_tmux=true, adaptive_status=available(opened), reason=None, caps both true
|
|
254
|
-
// windows/wsl : in_tmux=false, adaptive_status=not_implemented_this_platform, caps both false
|
|
255
|
-
// blocked 是 typed outcome(DisplayStatus::Blocked + reason),NOT error。
|
|
256
|
-
// 注:RUST 入口签名为 probe_display_capabilities(workspace) —— 它内部读环境/平台;
|
|
257
|
-
// 本测试在干净 CI workspace 上跑,只断言"不是 Err(平台降级是 typed outcome)"
|
|
258
|
-
// 以及 reason 必属封闭集(若有)。
|
|
259
|
-
// ───────────────────────────────────────────────────────────────────────
|
|
260
|
-
|
|
261
|
-
#[test]
|
|
262
|
-
fn probe_never_errors_platform_degradation_is_typed_outcome() {
|
|
263
|
-
// C13:能力性降级绝不走 LifecycleError —— 它是 DisplayStatus::Blocked + reason。
|
|
264
|
-
let ws = temp_ws();
|
|
265
|
-
let probe =
|
|
266
|
-
probe_display_capabilities(&ws).expect("probe 平台降级必须是 typed outcome,不是 Err");
|
|
267
|
-
// 若 blocked,reason 必属 AdaptiveBlockReason 封闭集且与 status 一致。
|
|
268
|
-
match probe.adaptive_status {
|
|
269
|
-
DisplayStatus::Blocked => {
|
|
270
|
-
assert!(probe.reason.is_some(), "blocked 必带 reason(C16 封闭集)")
|
|
271
|
-
}
|
|
272
|
-
DisplayStatus::Opened => assert!(probe.reason.is_none(), "opened 不应带 block reason"),
|
|
273
|
-
DisplayStatus::Stopped => {}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
#[test]
|
|
278
|
-
fn probe_caps_consistent_with_in_tmux() {
|
|
279
|
-
// caps.adaptive_display == (in_tmux && 平台支持);不在 tmux → caps 全 false。
|
|
280
|
-
let ws = temp_ws();
|
|
281
|
-
let probe = probe_display_capabilities(&ws).expect("probe 不应 Err");
|
|
282
|
-
if !probe.in_tmux {
|
|
283
|
-
assert!(!probe.caps.adaptive_display);
|
|
284
|
-
assert!(!probe.caps.tmux_append_windows);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// ───────────────────────────────────────────────────────────────────────
|
|
289
|
-
// AdaptiveBlockReason 封闭集 — ADAPTIVE_BLOCK_REASONS (adaptive.py:21)
|
|
290
|
-
// golden 实跑(6 个,无更多无更少):
|
|
291
|
-
// aggregator_rebuild_failed, leader_not_in_tmux, not_implemented_this_platform,
|
|
292
|
-
// split_failed, window_create_failed, worker_session_missing
|
|
293
|
-
// serde rename = snake_case,JSON 名与 Python 字符串一致。
|
|
294
|
-
// ───────────────────────────────────────────────────────────────────────
|
|
295
|
-
|
|
296
|
-
/// 封闭集 ALL — 一处穷举所有 AdaptiveBlockReason 变体(新增第 7 个变体会编译错此 match,
|
|
297
|
-
/// 把 cardinality 锁成编译期事实)。golden ADAPTIVE_BLOCK_REASONS 恰 6 个(adaptive.py:21)。
|
|
298
|
-
const ALL_BLOCK_REASONS: [AdaptiveBlockReason; 6] = [
|
|
299
|
-
AdaptiveBlockReason::LeaderNotInTmux,
|
|
300
|
-
AdaptiveBlockReason::SplitFailed,
|
|
301
|
-
AdaptiveBlockReason::WindowCreateFailed,
|
|
302
|
-
AdaptiveBlockReason::WorkerSessionMissing,
|
|
303
|
-
AdaptiveBlockReason::NotImplementedThisPlatform,
|
|
304
|
-
AdaptiveBlockReason::AggregatorRebuildFailed,
|
|
305
|
-
];
|
|
306
|
-
|
|
307
|
-
#[test]
|
|
308
|
-
fn adaptive_block_reason_serde_names_match_python_and_set_is_exactly_six() {
|
|
309
|
-
// (a) 每个变体 wire-name 与 Python 字符串一致。
|
|
310
|
-
let expected = [
|
|
311
|
-
"\"leader_not_in_tmux\"",
|
|
312
|
-
"\"split_failed\"",
|
|
313
|
-
"\"window_create_failed\"",
|
|
314
|
-
"\"worker_session_missing\"",
|
|
315
|
-
"\"not_implemented_this_platform\"",
|
|
316
|
-
"\"aggregator_rebuild_failed\"",
|
|
317
|
-
];
|
|
318
|
-
for (variant, want) in ALL_BLOCK_REASONS.iter().zip(expected.iter()) {
|
|
319
|
-
assert_eq!(&serde_json::to_string(variant).unwrap(), want);
|
|
320
|
-
}
|
|
321
|
-
// (b) 封闭集 CARDINALITY == 6(无多无少;rogue 第 7 变体使 ALL_BLOCK_REASONS match 编译失败)。
|
|
322
|
-
let mut names: Vec<String> = ALL_BLOCK_REASONS
|
|
323
|
-
.iter()
|
|
324
|
-
.map(|r| serde_json::to_string(r).unwrap())
|
|
325
|
-
.collect();
|
|
326
|
-
names.sort();
|
|
327
|
-
names.dedup();
|
|
328
|
-
assert_eq!(
|
|
329
|
-
names.len(),
|
|
330
|
-
6,
|
|
331
|
-
"ADAPTIVE_BLOCK_REASONS 恰 6 个,无重复无遗漏"
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
#[test]
|
|
336
|
-
fn adaptive_blocked_out_of_set_reason_bottoms_to_aggregator_rebuild_failed() {
|
|
337
|
-
// adaptive.py 兜底:越界 / aggregator 重建失败 → AggregatorRebuildFailed(documented overflow)。
|
|
338
|
-
// RED:经真实路径 —— probe 在 blocked 平台 open_worker_displays,worker display 必属封闭集;
|
|
339
|
-
// 这里用 probe.reason 越界场景驱动 open_worker_displays 而非静态枚举,捕获 reason 发射回归。
|
|
340
|
-
let ws = temp_ws();
|
|
341
|
-
let probe = DisplayProbe {
|
|
342
|
-
in_tmux: true,
|
|
343
|
-
platform: "linux".to_string(),
|
|
344
|
-
leader_session: Some(sess("leader")),
|
|
345
|
-
leader_pane: None,
|
|
346
|
-
caps: CapsFlags {
|
|
347
|
-
tmux_append_windows: true,
|
|
348
|
-
adaptive_display: true,
|
|
349
|
-
},
|
|
350
|
-
// 模拟 aggregator 重建失败封闭:open 必把 worker display 兜底成 AggregatorRebuildFailed。
|
|
351
|
-
adaptive_status: DisplayStatus::Blocked,
|
|
352
|
-
reason: Some(AdaptiveBlockReason::AggregatorRebuildFailed),
|
|
353
|
-
};
|
|
354
|
-
let rep = open_worker_displays(&ws, &sess("team-a"), DisplayBackend::Adaptive, &probe)
|
|
355
|
-
.expect("C14:显示失败不阻塞 readiness");
|
|
356
|
-
for (id, d) in rep.displays.iter() {
|
|
357
|
-
match d {
|
|
358
|
-
WorkerDisplay::Blocked { reason } => assert!(
|
|
359
|
-
ALL_BLOCK_REASONS.contains(reason),
|
|
360
|
-
"worker {id} 的 block reason 必属封闭集:{reason:?}"
|
|
361
|
-
),
|
|
362
|
-
other => panic!("blocked probe 下 worker {id} 应 Blocked:{other:?}"),
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
214
|
#[test]
|
|
368
215
|
fn start_mode_serde_names_match_python_start_mode_strings() {
|
|
369
216
|
// 低价值 wire-format 守卫:start_mode ∈ {"resumed","fresh","fresh_after_missing_rollout","noop"}。
|
|
@@ -1300,66 +1147,6 @@ impl Drop for EnvVarGuard {
|
|
|
1300
1147
|
}
|
|
1301
1148
|
}
|
|
1302
1149
|
|
|
1303
|
-
// ───────────────────────────────────────────────────────────────────────
|
|
1304
|
-
// open_worker_displays — worker_window.py (C14)
|
|
1305
|
-
// 显示失败不阻塞 readiness:probe 为 blocked 平台时,returns typed Blocked displays,
|
|
1306
|
-
// 绝不 Err。这里用 backend=None(无 worker views)验证至少不 panic 成 Err。
|
|
1307
|
-
// ───────────────────────────────────────────────────────────────────────
|
|
1308
|
-
|
|
1309
|
-
#[test]
|
|
1310
|
-
fn open_worker_displays_blocked_probe_yields_typed_blocked_not_error() {
|
|
1311
|
-
let ws = temp_ws();
|
|
1312
|
-
let probe = DisplayProbe {
|
|
1313
|
-
in_tmux: false,
|
|
1314
|
-
platform: "windows".to_string(),
|
|
1315
|
-
leader_session: None,
|
|
1316
|
-
leader_pane: None,
|
|
1317
|
-
caps: CapsFlags {
|
|
1318
|
-
tmux_append_windows: false,
|
|
1319
|
-
adaptive_display: false,
|
|
1320
|
-
},
|
|
1321
|
-
adaptive_status: DisplayStatus::Blocked,
|
|
1322
|
-
reason: Some(AdaptiveBlockReason::NotImplementedThisPlatform),
|
|
1323
|
-
};
|
|
1324
|
-
let rep = open_worker_displays(&ws, &sess("team-a"), DisplayBackend::Adaptive, &probe)
|
|
1325
|
-
.expect("C14:显示失败不阻塞 readiness —— 不得 Err");
|
|
1326
|
-
// 每个 worker 的 display 在 blocked 平台上应是 Blocked 变体(若有 worker)。
|
|
1327
|
-
for (id, d) in rep.displays.iter() {
|
|
1328
|
-
assert!(
|
|
1329
|
-
matches!(
|
|
1330
|
-
d,
|
|
1331
|
-
WorkerDisplay::Blocked {
|
|
1332
|
-
reason: AdaptiveBlockReason::NotImplementedThisPlatform
|
|
1333
|
-
}
|
|
1334
|
-
),
|
|
1335
|
-
"worker {id} 在 windows 平台应 Blocked(not_implemented):{d:?}"
|
|
1336
|
-
);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
// ───────────────────────────────────────────────────────────────────────
|
|
1341
|
-
// close_team_display_backends — display/close.py (C9 close-by-recorded-backend)
|
|
1342
|
-
// 空 workspace 无 recorded backend 无 session → 空 closed / 空 orphans(不 Err)。
|
|
1343
|
-
// adaptive 只删带 team-tag 的窗口(C2 leader pane 安全)。
|
|
1344
|
-
// ───────────────────────────────────────────────────────────────────────
|
|
1345
|
-
|
|
1346
|
-
#[test]
|
|
1347
|
-
fn close_team_display_empty_workspace_closes_nothing_not_error() {
|
|
1348
|
-
let ws = temp_ws();
|
|
1349
|
-
let rep = close_team_display_backends(&ws, &sess("team-a"))
|
|
1350
|
-
.expect("C9:无 recorded backend 应 Ok(空报告),不是 Err");
|
|
1351
|
-
assert!(
|
|
1352
|
-
rep.closed.is_empty(),
|
|
1353
|
-
"无 recorded backend 不应关任何东西:{:?}",
|
|
1354
|
-
rep.closed
|
|
1355
|
-
);
|
|
1356
|
-
assert!(
|
|
1357
|
-
rep.orphans_cleaned.is_empty(),
|
|
1358
|
-
"空 workspace 无 orphan:{:?}",
|
|
1359
|
-
rep.orphans_cleaned
|
|
1360
|
-
);
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
1150
|
// ───────────────────────────────────────────────────────────────────────
|
|
1364
1151
|
// fork_agent — operations.py:284 (native session fork eligibility)
|
|
1365
1152
|
// 资格门(adapter.supports_session_fork = auth_mode != "compatible_api"):
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
//! Issue #238 lifecycle-side contracts for persisted/recovered effort and Pi
|
|
2
|
+
//! leader argument admission. This module deliberately calls the same
|
|
3
|
+
//! lifecycle helpers used by spawn/restart instead of duplicating resolution.
|
|
4
|
+
|
|
5
|
+
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
|
|
6
|
+
|
|
7
|
+
use crate::lifecycle::launch::{
|
|
8
|
+
provider_effort_for_spawn, provider_effort_for_spawn_json, provider_effort_from_raw,
|
|
9
|
+
};
|
|
10
|
+
use crate::lifecycle::launch::pi_mcp::parse_pi_leader_args;
|
|
11
|
+
use crate::model::enums::{Provider, ProviderEffort};
|
|
12
|
+
use crate::model::yaml::Value as Yaml;
|
|
13
|
+
use serde_json::json;
|
|
14
|
+
|
|
15
|
+
fn args(items: &[&str]) -> Vec<String> {
|
|
16
|
+
items.iter().map(|item| (*item).to_string()).collect()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[test]
|
|
20
|
+
fn r6_spawn_and_persisted_recovery_preserve_codex_effort_literals() {
|
|
21
|
+
for raw in ["max", "ultra"] {
|
|
22
|
+
let yaml_agent = Yaml::Map(vec![(
|
|
23
|
+
"effort".to_string(),
|
|
24
|
+
Yaml::Str(raw.to_string()),
|
|
25
|
+
)]);
|
|
26
|
+
let json_agent = json!({"effort": raw});
|
|
27
|
+
|
|
28
|
+
let direct = provider_effort_from_raw(Some(raw), Provider::Codex)
|
|
29
|
+
.expect("direct lifecycle resolution")
|
|
30
|
+
.expect("Codex effort");
|
|
31
|
+
let spawned = provider_effort_for_spawn(&yaml_agent, Provider::Codex)
|
|
32
|
+
.expect("YAML spawn resolution")
|
|
33
|
+
.expect("spawned Codex effort");
|
|
34
|
+
let recovered = provider_effort_for_spawn_json(&json_agent, Provider::Codex)
|
|
35
|
+
.expect("JSON recovery resolution")
|
|
36
|
+
.expect("recovered Codex effort");
|
|
37
|
+
assert_eq!(direct.as_str(), raw);
|
|
38
|
+
assert_eq!(spawned.as_str(), raw);
|
|
39
|
+
assert_eq!(recovered.as_str(), raw);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn r6_unsupported_ultra_is_not_silently_dropped_on_legacy_provider() {
|
|
45
|
+
let error = provider_effort_from_raw(Some("ultra"), Provider::Fake)
|
|
46
|
+
.expect_err("ultra must not be silently ignored")
|
|
47
|
+
.to_string();
|
|
48
|
+
assert!(error.contains("effort 'ultra' is only supported by codex"), "{error}");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn r7_pi_leader_parser_resolves_thinking_by_provider_semantics() {
|
|
53
|
+
let parsed = parse_pi_leader_args(&args(&[
|
|
54
|
+
"--",
|
|
55
|
+
"--model",
|
|
56
|
+
"openai-codex/gpt-5.6-luna",
|
|
57
|
+
"--thinking",
|
|
58
|
+
"max",
|
|
59
|
+
]))
|
|
60
|
+
.expect("Pi max arguments");
|
|
61
|
+
assert_eq!(parsed.model.as_deref(), Some("openai-codex/gpt-5.6-luna"));
|
|
62
|
+
assert_eq!(parsed.effort, Some(ProviderEffort::Max));
|
|
63
|
+
|
|
64
|
+
let error = parse_pi_leader_args(&args(&[
|
|
65
|
+
"--model",
|
|
66
|
+
"openai-codex/gpt-5.6-luna",
|
|
67
|
+
"--thinking",
|
|
68
|
+
"ultra",
|
|
69
|
+
]))
|
|
70
|
+
.expect_err("Pi ultra must be rejected by shared resolver")
|
|
71
|
+
.to_string();
|
|
72
|
+
assert!(error.contains("effort 'ultra' is only supported by codex"), "{error}");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[test]
|
|
76
|
+
fn r7_pi_leader_parser_keeps_boundary_errors_fail_closed() {
|
|
77
|
+
for invalid in [
|
|
78
|
+
args(&["--thinking"]),
|
|
79
|
+
args(&["--unknown", "value"]),
|
|
80
|
+
args(&["--model", "codex"]),
|
|
81
|
+
args(&["--model", "openai-codex/gpt-5.6-luna", "--model", "other/x"]),
|
|
82
|
+
] {
|
|
83
|
+
assert!(parse_pi_leader_args(&invalid).is_err(), "must reject {invalid:?}");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let no_effort = parse_pi_leader_args(&args(&["--model", "openai-codex/gpt-5.6-luna"]))
|
|
87
|
+
.expect("Pi model-only arguments");
|
|
88
|
+
assert_eq!(no_effort.effort, None);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[test]
|
|
92
|
+
fn r7_pi_provider_authority_is_not_model_namespace_authority() {
|
|
93
|
+
let error = provider_effort_from_raw(Some("ultra"), Provider::Pi)
|
|
94
|
+
.expect_err("Pi ultra");
|
|
95
|
+
assert!(error.to_string().contains("effort 'ultra' is only supported by codex"));
|
|
96
|
+
}
|
|
@@ -1284,6 +1284,7 @@ fn lanea_stop_window_present_kills_and_stopped_true() {
|
|
|
1284
1284
|
// touches the display (mark_agent_stopped leaves it as-is) and hardcodes display_closed:false. RED: the
|
|
1285
1285
|
// persisted display.status/pane_title are the in-process observable.
|
|
1286
1286
|
#[test]
|
|
1287
|
+
#[ignore = "legacy external display contract removed; runtime is silent tmux only"]
|
|
1287
1288
|
fn lanea_stop_ghostty_workspace_relabels_slot_to_stopped() {
|
|
1288
1289
|
let ws = lanea_ws_agents(json!({
|
|
1289
1290
|
"alpha": { "status": "running", "provider": "codex", "window": "alpha",
|