@team-agent/installer 0.5.77 → 0.5.80
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 +161 -29
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +24 -8
- package/crates/team-agent/src/cli/mod.rs +543 -98
- package/crates/team-agent/src/cli/models.rs +393 -32
- package/crates/team-agent/src/cli/named_address.rs +19 -5
- package/crates/team-agent/src/cli/profile.rs +35 -4
- package/crates/team-agent/src/cli/spec.rs +7 -2
- package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
- package/crates/team-agent/src/cli/status_port/runtime.rs +3 -1
- package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
- package/crates/team-agent/src/cli/status_port/store.rs +1 -1
- package/crates/team-agent/src/cli/status_port/tests.rs +37 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +122 -2
- package/crates/team-agent/src/leader/lease.rs +492 -33
- package/crates/team-agent/src/leader/owner_bind.rs +40 -12
- package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
- package/crates/team-agent/src/leader/rediscover.rs +36 -6
- package/crates/team-agent/src/leader/start.rs +197 -88
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +86 -23
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
- package/crates/team-agent/src/lifecycle/launch.rs +30 -7
- package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
- package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
- package/crates/team-agent/src/lifecycle/restart.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt +9 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +71 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +140 -11
- package/crates/team-agent/src/messaging/delivery.rs +221 -80
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +127 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/provider/session_scan/common/tests.rs +204 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +24 -2
- package/crates/team-agent/src/state/abnormal_watch/tests/mod.rs +248 -0
- package/crates/team-agent/src/state/abnormal_watch.rs +144 -0
- package/crates/team-agent/src/state/mod.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +102 -4
- package/crates/team-agent/src/state/projection.rs +7 -1
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
- package/crates/team-agent/src/tmux_backend/tests.rs +149 -4
- package/crates/team-agent/src/tmux_backend.rs +239 -127
- package/crates/team-agent/src/transport.rs +78 -0
- package/crates/team-agent/src/transport_factory.rs +53 -0
- package/package.json +4 -5
- package/skills/team-agent/SKILL.md +45 -17
- package/skills/team-agent/command-coverage.json +4 -29
- package/npm/check-skill-links.mjs +0 -88
- package/skills/team-agent/references/team-agent-operator.md +0 -672
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
#[path = "../../../tests/support/hermetic.rs"]
|
|
2
|
+
mod hermetic_guard;
|
|
3
|
+
|
|
1
4
|
use super::*;
|
|
2
5
|
use crate::transport::test_support::OfflineTransport;
|
|
3
6
|
use serde_json::json;
|
|
4
7
|
use serial_test::serial;
|
|
5
8
|
|
|
9
|
+
fn enter_hermetic(tag: &str) -> hermetic_guard::HermeticTestEnv {
|
|
10
|
+
let env = hermetic_guard::HermeticTestEnv::enter(tag);
|
|
11
|
+
env.scrub_tmux();
|
|
12
|
+
env.assert_no_real_tmux();
|
|
13
|
+
env
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
fn launch_source() -> String {
|
|
7
17
|
let src = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/lifecycle");
|
|
8
18
|
let mut files = vec![src.join("launch.rs")];
|
|
@@ -26,9 +36,6 @@ fn launch_source() -> String {
|
|
|
26
36
|
.collect()
|
|
27
37
|
}
|
|
28
38
|
|
|
29
|
-
#[allow(dead_code)]
|
|
30
|
-
struct HermeticTestEnv;
|
|
31
|
-
|
|
32
39
|
const QS_TEAM_MD: &str =
|
|
33
40
|
"---\nname: quickteam\nobjective: Quick start.\nprovider: codex\n---\n\nQuick-start team.\n";
|
|
34
41
|
pub(super) const QS_VALID_ROLE: &str = "---\nname: implementer\nrole: Implementation Engineer\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nImplement bounded tasks.\n";
|
|
@@ -163,7 +170,9 @@ fn unowned_running_ws() -> PathBuf {
|
|
|
163
170
|
// hardcoded "no role docs found" PreflightBlocked. Golden: quick_start.py → _compile_team_dir_spec
|
|
164
171
|
// writes spec_path=team_dir/team.spec.yaml BEFORE launch.
|
|
165
172
|
#[test]
|
|
173
|
+
#[serial(env)]
|
|
166
174
|
fn quick_start_compiles_real_spec_to_team_spec_yaml() {
|
|
175
|
+
let _hermetic = enter_hermetic("launch-spawn-compile-spec");
|
|
167
176
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
168
177
|
let transport = OfflineTransport::new();
|
|
169
178
|
let result = quick_start_with_transport(&team, None, true, None, &transport);
|
|
@@ -196,7 +205,9 @@ fn quick_start_compiles_real_spec_to_team_spec_yaml() {
|
|
|
196
205
|
}
|
|
197
206
|
|
|
198
207
|
#[test]
|
|
208
|
+
#[serial(env)]
|
|
199
209
|
fn quick_start_without_team_id_uses_compiled_team_name_as_canonical_key() {
|
|
210
|
+
let _hermetic = enter_hermetic("launch-spawn-canonical-team");
|
|
200
211
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
201
212
|
let workspace = crate::model::paths::team_workspace(&team).unwrap();
|
|
202
213
|
let transport = OfflineTransport::new();
|
|
@@ -216,7 +227,120 @@ fn quick_start_without_team_id_uses_compiled_team_name_as_canonical_key() {
|
|
|
216
227
|
}
|
|
217
228
|
|
|
218
229
|
#[test]
|
|
230
|
+
#[serial(env)]
|
|
231
|
+
fn existing_runtime_producer_scopes_canonical_team() {
|
|
232
|
+
let _hermetic = enter_hermetic("launch-spawn-existing-producer");
|
|
233
|
+
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
234
|
+
let transport = OfflineTransport::new();
|
|
235
|
+
let first = quick_start_with_transport(&team, Some("from-name"), true, None, &transport)
|
|
236
|
+
.expect("first quick-start");
|
|
237
|
+
match &first {
|
|
238
|
+
QuickStartReport::Ready { team: canonical, .. } => {
|
|
239
|
+
assert_eq!(canonical, "from-name", "Ready producer must emit canonical team");
|
|
240
|
+
}
|
|
241
|
+
other => panic!("first quick-start must be Ready; got {other:?}"),
|
|
242
|
+
}
|
|
243
|
+
let second = quick_start_with_transport(&team, Some("from-name"), true, None, &transport)
|
|
244
|
+
.expect("second quick-start");
|
|
245
|
+
match second {
|
|
246
|
+
QuickStartReport::ExistingRuntime {
|
|
247
|
+
team: canonical,
|
|
248
|
+
agent_ids,
|
|
249
|
+
..
|
|
250
|
+
} => {
|
|
251
|
+
assert_eq!(canonical.as_deref(), Some("from-name"));
|
|
252
|
+
assert!(
|
|
253
|
+
agent_ids.iter().any(|id| id == "implementer"),
|
|
254
|
+
"ExistingRuntime producer agent_ids={agent_ids:?}"
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
other => panic!("duplicate quick-start must be ExistingRuntime; got {other:?}"),
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#[test]
|
|
262
|
+
#[serial(env)]
|
|
263
|
+
fn existing_runtime_requested_bob_does_not_emit_alice_members_or_attach() {
|
|
264
|
+
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
265
|
+
let workspace = crate::model::paths::team_workspace(&team).unwrap();
|
|
266
|
+
let state = json!({
|
|
267
|
+
"active_team_key": "alice",
|
|
268
|
+
"agents": { "alice": { "window": "alice" } },
|
|
269
|
+
"session_name": "team-alice",
|
|
270
|
+
"tmux_endpoint": "/tmp/shared.sock",
|
|
271
|
+
"teams": {
|
|
272
|
+
"alice": {
|
|
273
|
+
"agents": { "alice": { "window": "alice" } },
|
|
274
|
+
"session_name": "team-alice"
|
|
275
|
+
},
|
|
276
|
+
"bob": {
|
|
277
|
+
"agents": { "bob": { "window": "bob" } },
|
|
278
|
+
"session_name": "team-bob"
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
283
|
+
let report = quick_start_with_transport(&team, Some("bob"), true, None, &OfflineTransport::new())
|
|
284
|
+
.expect("requested bob existing runtime");
|
|
285
|
+
match report {
|
|
286
|
+
QuickStartReport::ExistingRuntime {
|
|
287
|
+
team: canonical,
|
|
288
|
+
agent_ids,
|
|
289
|
+
session_name,
|
|
290
|
+
attach_commands,
|
|
291
|
+
..
|
|
292
|
+
} => {
|
|
293
|
+
assert_eq!(canonical.as_deref(), Some("bob"));
|
|
294
|
+
assert_eq!(agent_ids, vec!["bob".to_string()]);
|
|
295
|
+
assert_eq!(
|
|
296
|
+
session_name.as_ref().map(|session| session.as_str()),
|
|
297
|
+
Some("team-bob")
|
|
298
|
+
);
|
|
299
|
+
assert!(
|
|
300
|
+
attach_commands.iter().any(|command| command.contains("team-bob")),
|
|
301
|
+
"attach must follow bob session; got {attach_commands:?}"
|
|
302
|
+
);
|
|
303
|
+
assert!(
|
|
304
|
+
attach_commands
|
|
305
|
+
.iter()
|
|
306
|
+
.all(|command| !command.contains("alice") && !command.contains("team-alice")),
|
|
307
|
+
"attach must not steer to alice; got {attach_commands:?}"
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
other => panic!("expected ExistingRuntime for bob; got {other:?}"),
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#[test]
|
|
315
|
+
#[serial(env)]
|
|
316
|
+
fn existing_runtime_empty_top_level_agents_still_emits_bob() {
|
|
317
|
+
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
318
|
+
let workspace = crate::model::paths::team_workspace(&team).unwrap();
|
|
319
|
+
let state = json!({
|
|
320
|
+
"active_team_key": "bob",
|
|
321
|
+
"agents": {},
|
|
322
|
+
"teams": {
|
|
323
|
+
"bob": {
|
|
324
|
+
"agents": { "bob": { "window": "bob" } },
|
|
325
|
+
"session_name": "team-bob"
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
330
|
+
let report = quick_start_with_transport(&team, Some("bob"), true, None, &OfflineTransport::new())
|
|
331
|
+
.expect("requested bob with empty top-level agents");
|
|
332
|
+
match report {
|
|
333
|
+
QuickStartReport::ExistingRuntime { agent_ids, .. } => {
|
|
334
|
+
assert_eq!(agent_ids, vec!["bob".to_string()]);
|
|
335
|
+
}
|
|
336
|
+
other => panic!("expected ExistingRuntime; got {other:?}"),
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#[test]
|
|
341
|
+
#[serial(env)]
|
|
219
342
|
fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_collect() {
|
|
343
|
+
let _hermetic = enter_hermetic("launch-spawn-dot-team-workspace");
|
|
220
344
|
let workspace = temp_ws();
|
|
221
345
|
let team = workspace.join(".team").join("current");
|
|
222
346
|
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
@@ -299,7 +423,9 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
|
|
|
299
423
|
}
|
|
300
424
|
|
|
301
425
|
#[test]
|
|
426
|
+
#[serial(env)]
|
|
302
427
|
fn quick_start_default_workspace_compiled_spec_uses_project_root() {
|
|
428
|
+
let _hermetic = enter_hermetic("launch-spawn-default-workspace");
|
|
303
429
|
let workspace = temp_ws();
|
|
304
430
|
std::fs::create_dir_all(workspace.join("agents")).unwrap();
|
|
305
431
|
std::fs::write(workspace.join("TEAM.md"), QS_TEAM_MD).unwrap();
|
|
@@ -375,215 +501,6 @@ fn quick_start_invalid_role_doc_surfaces_real_compile_error() {
|
|
|
375
501
|
);
|
|
376
502
|
}
|
|
377
503
|
|
|
378
|
-
#[test]
|
|
379
|
-
fn quick_start_absent_or_empty_roles_preserves_compile_outcome_without_pi_preflight_mutation() {
|
|
380
|
-
for (label, create_agents, expected) in [
|
|
381
|
-
("absent", false, "missing agents directory"),
|
|
382
|
-
("empty", true, "no role docs found"),
|
|
383
|
-
] {
|
|
384
|
-
let team = temp_ws().join(format!("teamdir-{label}"));
|
|
385
|
-
std::fs::create_dir_all(&team).unwrap();
|
|
386
|
-
std::fs::write(team.join("TEAM.md"), QS_TEAM_MD).unwrap();
|
|
387
|
-
if create_agents {
|
|
388
|
-
std::fs::create_dir(team.join("agents")).unwrap();
|
|
389
|
-
}
|
|
390
|
-
let workspace = crate::model::paths::team_workspace(&team).unwrap();
|
|
391
|
-
let team_md_before = std::fs::read(team.join("TEAM.md")).unwrap();
|
|
392
|
-
let absent_paths = [
|
|
393
|
-
team.join("team.spec.yaml"),
|
|
394
|
-
crate::state::persist::runtime_state_path(&workspace),
|
|
395
|
-
crate::model::paths::runtime_spec_path(&workspace, "quickteam"),
|
|
396
|
-
workspace.join("team_state.md"),
|
|
397
|
-
workspace.join(".team/logs/events.jsonl"),
|
|
398
|
-
workspace.join(".team/runtime/team.db"),
|
|
399
|
-
];
|
|
400
|
-
for path in &absent_paths {
|
|
401
|
-
assert!(!path.exists(), "precondition: {} must be absent", path.display());
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
let transport = OfflineTransport::new();
|
|
405
|
-
let mut calls = 0;
|
|
406
|
-
let mut discover = |_requested: &str| {
|
|
407
|
-
calls += 1;
|
|
408
|
-
Ok(Vec::new())
|
|
409
|
-
};
|
|
410
|
-
let error = quick_start_with_transport_in_workspace_with_display_pi_preflight(
|
|
411
|
-
&workspace,
|
|
412
|
-
&team,
|
|
413
|
-
None,
|
|
414
|
-
true,
|
|
415
|
-
None,
|
|
416
|
-
&transport,
|
|
417
|
-
true,
|
|
418
|
-
&mut discover,
|
|
419
|
-
)
|
|
420
|
-
.expect_err("an empty role input must preserve the existing compile outcome");
|
|
421
|
-
drop(discover);
|
|
422
|
-
assert_eq!(calls, 0, "{label} role input must not invoke Pi discovery");
|
|
423
|
-
match error {
|
|
424
|
-
LifecycleError::Compile(message) => {
|
|
425
|
-
assert!(message.contains(expected), "{label}: {message}");
|
|
426
|
-
}
|
|
427
|
-
other => panic!("{label}: expected existing compile outcome, got {other:?}"),
|
|
428
|
-
}
|
|
429
|
-
assert_eq!(team_md_before, std::fs::read(team.join("TEAM.md")).unwrap());
|
|
430
|
-
for path in &absent_paths {
|
|
431
|
-
assert!(!path.exists(), "{label} role input created {}", path.display());
|
|
432
|
-
}
|
|
433
|
-
assert!(transport.calls().is_empty(), "{label}: no transport mutation");
|
|
434
|
-
assert!(transport.spawn_records().is_empty(), "{label}: no spawn");
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
#[test]
|
|
439
|
-
fn quick_start_injected_pi_preflight_is_typed_and_mutation_free() {
|
|
440
|
-
let role_doc = QS_VALID_ROLE.replace(
|
|
441
|
-
"provider: codex\nmodel: gpt-5.5",
|
|
442
|
-
"provider: pi\nmodel: gpt-5.6-sol",
|
|
443
|
-
);
|
|
444
|
-
let team = quick_start_team_dir(&role_doc);
|
|
445
|
-
let workspace = crate::model::paths::team_workspace(&team).unwrap();
|
|
446
|
-
let role_path = team.join("agents/implementer.md");
|
|
447
|
-
let team_md_path = team.join("TEAM.md");
|
|
448
|
-
let local_spec_path = team.join("team.spec.yaml");
|
|
449
|
-
let runtime_state_path = crate::state::persist::runtime_state_path(&workspace);
|
|
450
|
-
let runtime_spec_path = crate::model::paths::runtime_spec_path(&workspace, "quickteam");
|
|
451
|
-
let pi_paths = crate::lifecycle::launch::pi_mcp::pi_seat_paths(
|
|
452
|
-
&workspace,
|
|
453
|
-
"quickteam",
|
|
454
|
-
"implementer",
|
|
455
|
-
);
|
|
456
|
-
let role_before = std::fs::read(&role_path).unwrap();
|
|
457
|
-
let team_md_before = std::fs::read(&team_md_path).unwrap();
|
|
458
|
-
for path in [
|
|
459
|
-
&local_spec_path,
|
|
460
|
-
&runtime_state_path,
|
|
461
|
-
&runtime_spec_path,
|
|
462
|
-
&pi_paths.wrapper,
|
|
463
|
-
&pi_paths.sessions,
|
|
464
|
-
] {
|
|
465
|
-
assert!(!path.exists(), "precondition: {} must be absent", path.display());
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
let transport = OfflineTransport::new();
|
|
469
|
-
let mut calls = 0;
|
|
470
|
-
let mut discover = |requested: &str| {
|
|
471
|
-
calls += 1;
|
|
472
|
-
assert_eq!(requested, "gpt-5.6-sol");
|
|
473
|
-
Ok(vec!["openai-codex/gpt-5.6-sol".into()])
|
|
474
|
-
};
|
|
475
|
-
let error = quick_start_with_transport_in_workspace_with_display_pi_preflight(
|
|
476
|
-
&workspace,
|
|
477
|
-
&team,
|
|
478
|
-
None,
|
|
479
|
-
true,
|
|
480
|
-
None,
|
|
481
|
-
&transport,
|
|
482
|
-
true,
|
|
483
|
-
&mut discover,
|
|
484
|
-
)
|
|
485
|
-
.expect_err("unqualified Pi role must fail before quick-start mutation");
|
|
486
|
-
drop(discover);
|
|
487
|
-
assert_eq!(calls, 1);
|
|
488
|
-
match error {
|
|
489
|
-
LifecycleError::PiModelPreflight {
|
|
490
|
-
requested,
|
|
491
|
-
candidates,
|
|
492
|
-
action,
|
|
493
|
-
not_ready,
|
|
494
|
-
} => {
|
|
495
|
-
assert_eq!(requested, "gpt-5.6-sol");
|
|
496
|
-
assert_eq!(candidates, vec!["openai-codex/gpt-5.6-sol"]);
|
|
497
|
-
assert_eq!(
|
|
498
|
-
action,
|
|
499
|
-
"copy a candidate into the role, or run `team-agent models --provider pi --search gpt-5.6-sol`"
|
|
500
|
-
);
|
|
501
|
-
assert!(!not_ready);
|
|
502
|
-
}
|
|
503
|
-
other => panic!("expected typed Pi preflight error, got {other:?}"),
|
|
504
|
-
}
|
|
505
|
-
assert_eq!(role_before, std::fs::read(&role_path).unwrap());
|
|
506
|
-
assert_eq!(team_md_before, std::fs::read(&team_md_path).unwrap());
|
|
507
|
-
for path in [
|
|
508
|
-
&local_spec_path,
|
|
509
|
-
&runtime_state_path,
|
|
510
|
-
&runtime_spec_path,
|
|
511
|
-
&pi_paths.wrapper,
|
|
512
|
-
&pi_paths.sessions,
|
|
513
|
-
] {
|
|
514
|
-
assert!(!path.exists(), "quick-start preflight created {}", path.display());
|
|
515
|
-
}
|
|
516
|
-
assert!(transport.calls().is_empty(), "no transport calls before preflight rejection");
|
|
517
|
-
assert!(transport.spawn_records().is_empty());
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
#[test]
|
|
521
|
-
fn quick_start_schema_invalid_pi_role_is_mutation_free() {
|
|
522
|
-
let role_doc = QS_VALID_ROLE
|
|
523
|
-
.replace(
|
|
524
|
-
"provider: codex\nmodel: gpt-5.5",
|
|
525
|
-
"provider: pi\nmodel: openai-codex/gpt-5.6-sol",
|
|
526
|
-
)
|
|
527
|
-
.replace(" - mcp_team", " - fs_read");
|
|
528
|
-
let team = quick_start_team_dir(&role_doc);
|
|
529
|
-
let workspace = crate::model::paths::team_workspace(&team).unwrap();
|
|
530
|
-
let role_path = team.join("agents/implementer.md");
|
|
531
|
-
let team_md_path = team.join("TEAM.md");
|
|
532
|
-
let role_before = std::fs::read(&role_path).unwrap();
|
|
533
|
-
let team_md_before = std::fs::read(&team_md_path).unwrap();
|
|
534
|
-
let pi_paths = crate::lifecycle::launch::pi_mcp::pi_seat_paths(
|
|
535
|
-
&workspace,
|
|
536
|
-
"quickteam",
|
|
537
|
-
"implementer",
|
|
538
|
-
);
|
|
539
|
-
let absent_paths = [
|
|
540
|
-
team.join("team.spec.yaml"),
|
|
541
|
-
crate::state::persist::runtime_state_path(&workspace),
|
|
542
|
-
crate::model::paths::runtime_spec_path(&workspace, "quickteam"),
|
|
543
|
-
workspace.join("team_state.md"),
|
|
544
|
-
workspace.join(".team/logs/events.jsonl"),
|
|
545
|
-
workspace.join(".team/runtime/team.db"),
|
|
546
|
-
pi_paths.wrapper,
|
|
547
|
-
pi_paths.sessions,
|
|
548
|
-
];
|
|
549
|
-
for path in &absent_paths {
|
|
550
|
-
assert!(!path.exists(), "precondition: {} must be absent", path.display());
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
let transport = OfflineTransport::new();
|
|
554
|
-
let mut calls = 0;
|
|
555
|
-
let mut discover = |_requested: &str| {
|
|
556
|
-
calls += 1;
|
|
557
|
-
Ok(Vec::new())
|
|
558
|
-
};
|
|
559
|
-
let error = quick_start_with_transport_in_workspace_with_display_pi_preflight(
|
|
560
|
-
&workspace,
|
|
561
|
-
&team,
|
|
562
|
-
None,
|
|
563
|
-
true,
|
|
564
|
-
None,
|
|
565
|
-
&transport,
|
|
566
|
-
true,
|
|
567
|
-
&mut discover,
|
|
568
|
-
)
|
|
569
|
-
.expect_err("invalid Pi schema must fail before quick-start mutation");
|
|
570
|
-
drop(discover);
|
|
571
|
-
assert_eq!(calls, 0, "qualified models must not invoke discovery");
|
|
572
|
-
match error {
|
|
573
|
-
LifecycleError::Compile(message) => {
|
|
574
|
-
assert!(message.contains("Pi roles require mcp_team"), "{message}");
|
|
575
|
-
}
|
|
576
|
-
other => panic!("expected Pi role schema error, got {other:?}"),
|
|
577
|
-
}
|
|
578
|
-
assert_eq!(role_before, std::fs::read(&role_path).unwrap());
|
|
579
|
-
assert_eq!(team_md_before, std::fs::read(&team_md_path).unwrap());
|
|
580
|
-
for path in &absent_paths {
|
|
581
|
-
assert!(!path.exists(), "schema preflight created {}", path.display());
|
|
582
|
-
}
|
|
583
|
-
assert!(transport.calls().is_empty(), "no transport calls before schema rejection");
|
|
584
|
-
assert!(transport.spawn_records().is_empty());
|
|
585
|
-
}
|
|
586
|
-
|
|
587
504
|
// P0 — launch (dry_run) over a real compiled spec must resolve the REAL route/permission plan
|
|
588
505
|
// (no spawn) — NOT the hardcoded RequirementUnmet stub. Golden: launch/core.py dry_run resolves
|
|
589
506
|
// routing + permissions without starting any process.
|
|
@@ -813,7 +730,9 @@ fn unowned_running_ws_all_paused() -> PathBuf {
|
|
|
813
730
|
// #15 — quick_start seeds runtime state under team_workspace(team_dir) (the PARENT), not inside the
|
|
814
731
|
// team dir, so restart/status can locate it. Golden quick_start.py:35 team_workspace(team_dir).
|
|
815
732
|
#[test]
|
|
733
|
+
#[serial(env)]
|
|
816
734
|
fn spine_quick_start_seeds_state_under_team_workspace_not_team_dir() {
|
|
735
|
+
let _hermetic = enter_hermetic("launch-spawn-spine-workspace");
|
|
817
736
|
let team = quick_start_team_dir(QS_VALID_ROLE); // <base>/teamdir
|
|
818
737
|
let transport = OfflineTransport::new();
|
|
819
738
|
let _ = quick_start_with_transport(&team, None, true, None, &transport);
|
|
@@ -1362,7 +1281,9 @@ fn seed_canonical_team_identity(workspace: &std::path::Path, team_key: &str) {
|
|
|
1362
1281
|
// RED at assertion (NOT a panic). OS-safe: recording transport (no real tmux) + seeded-healthy
|
|
1363
1282
|
// coordinator (start_coordinator AlreadyRunning -> no daemon subprocess).
|
|
1364
1283
|
#[test]
|
|
1284
|
+
#[serial(env)]
|
|
1365
1285
|
fn quick_start_with_transport_spawns_workers_not_dry_run() {
|
|
1286
|
+
let _hermetic = enter_hermetic("launch-spawn-spawn-path");
|
|
1366
1287
|
let team = quick_start_team_dir(QS_VALID_ROLE); // one agent: implementer / provider codex
|
|
1367
1288
|
let workspace = team.parent().expect("team_workspace(team_dir) = parent"); // where start_coordinator runs
|
|
1368
1289
|
seed_healthy_coordinator(workspace);
|
|
@@ -1449,7 +1370,9 @@ fn adaptive_layout_plan_8_workers_is_3_3_2() {
|
|
|
1449
1370
|
}
|
|
1450
1371
|
|
|
1451
1372
|
#[test]
|
|
1373
|
+
#[serial(env)]
|
|
1452
1374
|
fn quick_start_default_adaptive_groups_workers_into_layout_panes() {
|
|
1375
|
+
let _hermetic = enter_hermetic("launch-spawn-adaptive-layout");
|
|
1453
1376
|
let roles = (1..=4)
|
|
1454
1377
|
.map(|i| {
|
|
1455
1378
|
let id = format!("w{i}");
|
|
@@ -1532,7 +1455,9 @@ fn quick_start_tmux_backend_prefers_absolute_tmux_env_endpoint() {
|
|
|
1532
1455
|
}
|
|
1533
1456
|
|
|
1534
1457
|
#[test]
|
|
1458
|
+
#[serial(env)]
|
|
1535
1459
|
fn quick_start_persists_selected_tmux_endpoint_and_attach_commands() {
|
|
1460
|
+
let _hermetic = enter_hermetic("launch-spawn-endpoint-attach");
|
|
1536
1461
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
1537
1462
|
let workspace = team.parent().expect("team_workspace(team_dir) = parent");
|
|
1538
1463
|
seed_healthy_coordinator(workspace);
|
|
@@ -1591,7 +1516,9 @@ fn annotate_runtime_tmux_endpoint_persists_workspace_socket_as_full_path() {
|
|
|
1591
1516
|
}
|
|
1592
1517
|
|
|
1593
1518
|
#[test]
|
|
1519
|
+
#[serial(env)]
|
|
1594
1520
|
fn quick_start_preserves_managed_leader_topology_and_emits_leader_attach_command() {
|
|
1521
|
+
let _hermetic = enter_hermetic("launch-spawn-managed-leader");
|
|
1595
1522
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
1596
1523
|
let workspace = team.parent().expect("team_workspace(team_dir) = parent");
|
|
1597
1524
|
seed_healthy_coordinator(workspace);
|
|
@@ -1683,7 +1610,9 @@ fn attach_window_names_for_state_agents_include_managed_leader_and_layout_window
|
|
|
1683
1610
|
}
|
|
1684
1611
|
|
|
1685
1612
|
#[test]
|
|
1613
|
+
#[serial(env)]
|
|
1686
1614
|
fn quick_start_no_display_keeps_one_window_per_agent() {
|
|
1615
|
+
let _hermetic = enter_hermetic("launch-spawn-no-display");
|
|
1687
1616
|
let roles = ["w1", "w2"]
|
|
1688
1617
|
.into_iter()
|
|
1689
1618
|
.map(|id| (format!("{id}.md"), role_doc(id)))
|
|
@@ -3095,7 +3024,9 @@ fn start_agent_adaptive_restarts_missing_pane_in_existing_layout_window() {
|
|
|
3095
3024
|
// OS-safe: OfflineTransport (zero real spawn).
|
|
3096
3025
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
3097
3026
|
#[test]
|
|
3027
|
+
#[serial(env)]
|
|
3098
3028
|
fn quick_start_seeds_tasks_key_from_compiled_spec() {
|
|
3029
|
+
let _hermetic = enter_hermetic("launch-spawn-tasks-key");
|
|
3099
3030
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
3100
3031
|
let transport = OfflineTransport::new();
|
|
3101
3032
|
let _ = quick_start_with_transport(&team, None, true, None, &transport);
|
|
@@ -3146,6 +3077,7 @@ fn quick_start_seeds_tasks_key_from_compiled_spec() {
|
|
|
3146
3077
|
#[test]
|
|
3147
3078
|
#[serial(env)]
|
|
3148
3079
|
fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
|
|
3080
|
+
let _hermetic = enter_hermetic("launch-spawn-state-seeds");
|
|
3149
3081
|
// Bug 2 owner team-scope: top-level owner triple is dropped when the seeded
|
|
3150
3082
|
// pane is empty; an ambient TMUX_PANE (tests run inside the dev's tmux session)
|
|
3151
3083
|
// would otherwise leak into the caller-identity seed, inflate the seeded owner
|
|
@@ -3245,6 +3177,7 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
|
|
|
3245
3177
|
#[test]
|
|
3246
3178
|
#[serial(env)]
|
|
3247
3179
|
fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
3180
|
+
let _hermetic = enter_hermetic("launch-spawn-running-state");
|
|
3248
3181
|
const FIXED_SPAWNED_AT: &str = "2026-06-04T00:00:00+00:00";
|
|
3249
3182
|
let _clock_guard = EnvVarGuard::set("TEAM_AGENT_TEST_FIXED_SPAWNED_AT", FIXED_SPAWNED_AT);
|
|
3250
3183
|
let team = quick_start_team_dir(QS_VALID_ROLE);
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
#[path = "../../../tests/support/hermetic.rs"]
|
|
2
|
+
mod hermetic_guard;
|
|
3
|
+
|
|
1
4
|
use super::agent_ops::lanea_team_ws;
|
|
2
5
|
use super::lane_ops::{fork_ws, LaneHomeGuard, LaneTransport};
|
|
3
6
|
use super::launch_spawn::{
|
|
@@ -19,11 +22,17 @@ use serde_json::json;
|
|
|
19
22
|
use std::collections::BTreeSet;
|
|
20
23
|
use std::time::Duration;
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
fn enter_hermetic(tag: &str) -> hermetic_guard::HermeticTestEnv {
|
|
26
|
+
let env = hermetic_guard::HermeticTestEnv::enter(tag);
|
|
27
|
+
env.scrub_tmux();
|
|
28
|
+
env.assert_no_real_tmux();
|
|
29
|
+
env
|
|
30
|
+
}
|
|
24
31
|
|
|
25
32
|
#[test]
|
|
33
|
+
#[serial_test::serial(env)]
|
|
26
34
|
fn concurrent_reset_discard_session_serializes() {
|
|
35
|
+
let _hermetic = enter_hermetic("phase-b-concurrent-reset");
|
|
27
36
|
let ids = (1..=6).map(|n| format!("w{n}")).collect::<Vec<_>>();
|
|
28
37
|
let role_docs = ids
|
|
29
38
|
.iter()
|
|
@@ -131,6 +140,7 @@ fn concurrent_reset_discard_session_serializes() {
|
|
|
131
140
|
#[test]
|
|
132
141
|
#[serial_test::serial(env)]
|
|
133
142
|
fn add_fork_remove_share_lifecycle_lock_behavior() {
|
|
143
|
+
let _hermetic = enter_hermetic("phase-b-fork");
|
|
134
144
|
let _home = LaneHomeGuard::enter("phase-b-fork");
|
|
135
145
|
let (add_team, add_workspace, add_role, add_transport) = add_fixture();
|
|
136
146
|
let add_agent = aid("w2");
|
|
@@ -313,7 +323,9 @@ fn add_fork_remove_share_lifecycle_lock_behavior() {
|
|
|
313
323
|
}
|
|
314
324
|
|
|
315
325
|
#[test]
|
|
326
|
+
#[serial_test::serial(env)]
|
|
316
327
|
fn breal_spawn_ownership_mismatch_fails_without_state_pollution() {
|
|
328
|
+
let _hermetic = enter_hermetic("phase-b-spawn-ownership");
|
|
317
329
|
let (workspace, _team) = breal_workspace();
|
|
318
330
|
let transport = BRealTransport::misowned();
|
|
319
331
|
|
|
@@ -425,7 +437,9 @@ fn breal_reset_rehydrates_role_context_from_compiled_spec() {
|
|
|
425
437
|
}
|
|
426
438
|
|
|
427
439
|
#[test]
|
|
440
|
+
#[serial_test::serial(env)]
|
|
428
441
|
fn breal_restart_rehydrates_role_context_from_compiled_spec() {
|
|
442
|
+
let _hermetic = enter_hermetic("phase-b-restart-role-context");
|
|
429
443
|
let (workspace, _team) = breal_one_worker_workspace();
|
|
430
444
|
strip_runtime_command_context(&workspace);
|
|
431
445
|
let transport = BRealTransport::owned();
|
|
@@ -90,6 +90,9 @@ fn hermetic_caller_envs() -> &'static [&'static str] {
|
|
|
90
90
|
"TEAM_AGENT_OWNER_TEAM_ID",
|
|
91
91
|
"TEAM_AGENT_ACTIVE_TEAM",
|
|
92
92
|
"TEAM_AGENT_ID",
|
|
93
|
+
"TEAM_AGENT_CALLER_PROVIDER",
|
|
94
|
+
"TEAM_AGENT_CALLER_PANE_ID",
|
|
95
|
+
"TEAM_AGENT_CALLER_TMUX_ENDPOINT",
|
|
93
96
|
]
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -176,28 +179,40 @@ fn phase_f_golden_events_state_status_zero_drift() {
|
|
|
176
179
|
#[test]
|
|
177
180
|
#[serial_test::serial(env)]
|
|
178
181
|
fn phase_golden_unbound_not_ready_requires_claim_leader() {
|
|
179
|
-
// Unbound recovery guidance belongs to the diagnose/quick-start refusal
|
|
180
|
-
// surface, not to the seven-field status brief.
|
|
181
182
|
let diagnose = run_diagnose_after_quick_start();
|
|
182
183
|
let issues = diagnose
|
|
183
184
|
.get("issues")
|
|
184
185
|
.and_then(Value::as_array)
|
|
185
186
|
.cloned()
|
|
186
187
|
.unwrap_or_default();
|
|
188
|
+
let issue_ids: Vec<&str> = issues
|
|
189
|
+
.iter()
|
|
190
|
+
.filter_map(|issue| issue.as_str())
|
|
191
|
+
.collect();
|
|
192
|
+
assert_eq!(
|
|
193
|
+
issue_ids
|
|
194
|
+
.iter()
|
|
195
|
+
.copied()
|
|
196
|
+
.filter(|id| *id == "leader_registry_index_missing")
|
|
197
|
+
.count(),
|
|
198
|
+
1,
|
|
199
|
+
"tombstone after seeded owner without registry must be exactly index-missing; diagnose={diagnose}"
|
|
200
|
+
);
|
|
187
201
|
assert!(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|| issue.get("id").and_then(Value::as_str) == Some("leader_channel_unbound")
|
|
191
|
-
}),
|
|
192
|
-
"tombstone fixture must still diagnose leader_receiver_unbound; diagnose={diagnose}"
|
|
202
|
+
!issue_ids.contains(&"leader_receiver_not_committed"),
|
|
203
|
+
"parallel not_committed bool must not appear; diagnose={diagnose}"
|
|
193
204
|
);
|
|
194
205
|
let repairs = diagnose
|
|
195
206
|
.get("suggested_repairs")
|
|
196
207
|
.map(Value::to_string)
|
|
197
208
|
.unwrap_or_default();
|
|
198
209
|
assert!(
|
|
199
|
-
repairs.contains("
|
|
200
|
-
"
|
|
210
|
+
repairs.contains("publish the leader registry index"),
|
|
211
|
+
"index-missing repair must publish the index; diagnose={diagnose}"
|
|
212
|
+
);
|
|
213
|
+
assert!(
|
|
214
|
+
!repairs.contains("team-agent claim-leader --confirm"),
|
|
215
|
+
"index-missing must not recommend claim-leader --confirm; diagnose={diagnose}"
|
|
201
216
|
);
|
|
202
217
|
}
|
|
203
218
|
|
|
@@ -490,6 +505,36 @@ fn run_diagnose_after_quick_start() -> Value {
|
|
|
490
505
|
false,
|
|
491
506
|
)
|
|
492
507
|
.expect("tombstone fixture must quick-start");
|
|
508
|
+
let mut state = crate::state::persist::load_runtime_state(&workspace)
|
|
509
|
+
.expect("tombstone fixture must persist runtime state");
|
|
510
|
+
crate::lifecycle::launch::seed_launched_owner_from_caller_with_provider_lookup(
|
|
511
|
+
&mut state,
|
|
512
|
+
crate::state::owner_gate::CallerIdentity {
|
|
513
|
+
pane_id: "%1".to_string(),
|
|
514
|
+
provider: "codex".to_string(),
|
|
515
|
+
machine_fingerprint: "fp".to_string(),
|
|
516
|
+
leader_session_uuid: "uuid-tombstone".to_string(),
|
|
517
|
+
leader_session_uuid_source: "env".to_string(),
|
|
518
|
+
},
|
|
519
|
+
|_| None,
|
|
520
|
+
);
|
|
521
|
+
crate::state::persist::save_runtime_state(&workspace, &state)
|
|
522
|
+
.expect("persist seeded owner without registry");
|
|
523
|
+
if let Some(dir) = crate::leader::registry::registry_dir() {
|
|
524
|
+
if let Ok(entries) = std::fs::read_dir(&dir) {
|
|
525
|
+
for entry in entries.flatten() {
|
|
526
|
+
let _ = std::fs::remove_file(entry.path());
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
assert!(
|
|
531
|
+
state
|
|
532
|
+
.pointer("/teams/teamdir/team_owner/pane_id")
|
|
533
|
+
.or_else(|| state.pointer("/team_owner/pane_id"))
|
|
534
|
+
.and_then(Value::as_str)
|
|
535
|
+
.is_some(),
|
|
536
|
+
"index-missing diagnose requires a persisted seeded owner"
|
|
537
|
+
);
|
|
493
538
|
let diagnose = cmd_diagnose(&DiagnoseArgs {
|
|
494
539
|
workspace,
|
|
495
540
|
json: true,
|