@team-agent/installer 0.5.75 → 0.5.78
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/README.md +9 -0
- package/README.zh.md +8 -0
- package/crates/team-agent/src/cli/adapters.rs +184 -125
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +59 -15
- package/crates/team-agent/src/cli/mod.rs +600 -134
- package/crates/team-agent/src/cli/models.rs +485 -44
- 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/brief.rs +1137 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
- 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.rs +8 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
- package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
- 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/leader/lease.rs +538 -41
- 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 +151 -14
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
- package/crates/team-agent/src/leader/types.rs +4 -0
- 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 +138 -14
- 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 +1002 -65
- 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/b0_reader_hideone_audit_contract.rs +45 -14
- 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 -3
- 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/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 +4 -3
- 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 +69 -28
- 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/upgrade_compat_0211_red.rs +22 -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 +12 -0
- package/crates/team-agent/src/mcp_server/wire.rs +140 -11
- package/crates/team-agent/src/messaging/delivery.rs +67 -8
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
- package/crates/team-agent/src/messaging/watchers.rs +48 -4
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/state/paths.rs +17 -2
- package/crates/team-agent/src/state/persist.rs +97 -1
- package/crates/team-agent/src/state/projection.rs +18 -2
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/state/selector.rs +23 -3
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +144 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
- package/crates/team-agent/src/tmux_backend.rs +208 -118
- package/crates/team-agent/src/transport.rs +72 -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,6 +1,8 @@
|
|
|
1
1
|
//! Read-only Pi model catalog discovery for `team-agent models`.
|
|
2
2
|
use super::{CliError, CmdOutput, CmdResult, ExitCode, ModelsArgs};
|
|
3
3
|
use serde_json::{json, Value};
|
|
4
|
+
#[cfg(test)]
|
|
5
|
+
use std::collections::BTreeMap;
|
|
4
6
|
use std::path::Path;
|
|
5
7
|
use std::process::Command;
|
|
6
8
|
use std::time::Duration;
|
|
@@ -134,8 +136,15 @@ fn run_catalog(program: &Path, timeout: Duration, max_bytes: u64) -> Result<Vec<
|
|
|
134
136
|
#[cfg(test)]
|
|
135
137
|
mod tests {
|
|
136
138
|
use super::*;
|
|
139
|
+
#[cfg(unix)]
|
|
140
|
+
use std::io::Write;
|
|
141
|
+
#[cfg(unix)]
|
|
142
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
137
143
|
use std::time::Instant;
|
|
138
144
|
|
|
145
|
+
#[cfg(unix)]
|
|
146
|
+
static FIXTURE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
|
147
|
+
|
|
139
148
|
#[cfg(unix)]
|
|
140
149
|
#[test]
|
|
141
150
|
fn real_dispatcher_child_helper() {
|
|
@@ -242,12 +251,13 @@ mod tests {
|
|
|
242
251
|
#[cfg(unix)]
|
|
243
252
|
fn fixture(body: &str) -> std::path::PathBuf {
|
|
244
253
|
use std::os::unix::fs::PermissionsExt;
|
|
254
|
+
let sequence = FIXTURE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
|
|
245
255
|
let root = std::env::current_exe()
|
|
246
256
|
.unwrap()
|
|
247
257
|
.parent()
|
|
248
258
|
.unwrap()
|
|
249
259
|
.join(format!(
|
|
250
|
-
"team-agent-pi-fixture-{}-{}",
|
|
260
|
+
"team-agent-pi-fixture-{}-{}-{sequence}",
|
|
251
261
|
std::process::id(),
|
|
252
262
|
std::time::SystemTime::now()
|
|
253
263
|
.duration_since(std::time::UNIX_EPOCH)
|
|
@@ -267,20 +277,209 @@ mod tests {
|
|
|
267
277
|
path
|
|
268
278
|
}
|
|
269
279
|
|
|
280
|
+
#[cfg(unix)]
|
|
281
|
+
fn fixture_with_sidecars(body: &str) -> std::path::PathBuf {
|
|
282
|
+
let path = fixture(body);
|
|
283
|
+
let script = std::fs::read_to_string(&path).unwrap();
|
|
284
|
+
let script = script.strip_prefix("#!/bin/sh\n").unwrap();
|
|
285
|
+
let script = format!(
|
|
286
|
+
"#!/bin/sh\nprintf '1\\n' > \"$0.entered\" || :\n{script}body_status=$?\nprintf '1\\n' > \"$0.completed\" || :\nexit \"$body_status\"\n"
|
|
287
|
+
);
|
|
288
|
+
std::fs::write(&path, script).unwrap();
|
|
289
|
+
path
|
|
290
|
+
}
|
|
291
|
+
|
|
270
292
|
#[cfg(unix)]
|
|
271
293
|
fn cleanup_fixture(path: &Path) {
|
|
272
294
|
let _ = std::fs::remove_dir_all(path.parent().unwrap());
|
|
273
295
|
}
|
|
274
296
|
|
|
297
|
+
#[cfg(unix)]
|
|
298
|
+
fn native_timeout_fixture() -> std::path::PathBuf {
|
|
299
|
+
let path = std::path::PathBuf::from(env!("TEAM_AGENT_MODELS_TIMEOUT_FIXTURE"));
|
|
300
|
+
assert!(path.is_file(), "prebuilt models timeout fixture is missing");
|
|
301
|
+
path
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#[cfg(unix)]
|
|
305
|
+
fn native_timeout_receipt() -> std::path::PathBuf {
|
|
306
|
+
let stamp = std::time::SystemTime::now()
|
|
307
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
308
|
+
.unwrap()
|
|
309
|
+
.as_nanos();
|
|
310
|
+
std::path::PathBuf::from("/tmp").join(format!(
|
|
311
|
+
"team-agent-models-timeout-{}-{stamp}.receipt",
|
|
312
|
+
std::process::id()
|
|
313
|
+
))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#[cfg(unix)]
|
|
317
|
+
fn native_timeout_stage_receipt(receipt: &Path) -> std::path::PathBuf {
|
|
318
|
+
receipt.with_extension("stage")
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#[cfg(unix)]
|
|
322
|
+
fn parse_native_timeout_receipt(path: &Path) -> Option<BTreeMap<String, String>> {
|
|
323
|
+
let mut fields = BTreeMap::new();
|
|
324
|
+
for line in std::fs::read_to_string(path).ok()?.lines() {
|
|
325
|
+
let (key, value) = line.split_once('=')?;
|
|
326
|
+
if fields.insert(key.to_string(), value.to_string()).is_some() {
|
|
327
|
+
return None;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
let keys = fields.keys().map(String::as_str).collect::<Vec<_>>();
|
|
331
|
+
(keys
|
|
332
|
+
== vec![
|
|
333
|
+
"child_argv_exact",
|
|
334
|
+
"child_pid",
|
|
335
|
+
"child_spawn_attempts",
|
|
336
|
+
"descendant_pid",
|
|
337
|
+
"descendant_started",
|
|
338
|
+
"descendant_stdout_inherited",
|
|
339
|
+
"parent_argv_exact",
|
|
340
|
+
"parent_pid",
|
|
341
|
+
"parent_started_once",
|
|
342
|
+
])
|
|
343
|
+
.then_some(fields)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
#[cfg(unix)]
|
|
347
|
+
fn wait_native_timeout_receipt(path: &Path) -> BTreeMap<String, String> {
|
|
348
|
+
let deadline = Instant::now() + Duration::from_millis(500);
|
|
349
|
+
loop {
|
|
350
|
+
if let Some(fields) = parse_native_timeout_receipt(path) {
|
|
351
|
+
return fields;
|
|
352
|
+
}
|
|
353
|
+
if Instant::now() >= deadline {
|
|
354
|
+
panic!("native timeout receipt did not become complete after runner return");
|
|
355
|
+
}
|
|
356
|
+
std::thread::sleep(Duration::from_millis(1));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#[cfg(unix)]
|
|
361
|
+
fn with_native_mode<T>(
|
|
362
|
+
mode: &str,
|
|
363
|
+
action: impl FnOnce() -> T,
|
|
364
|
+
) -> (T, crate::lifecycle::launch::pi_mcp::PiCatalogTestObservation) {
|
|
365
|
+
let receipt = native_timeout_receipt();
|
|
366
|
+
assert!(!receipt.exists());
|
|
367
|
+
let result = crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
|
|
368
|
+
&receipt,
|
|
369
|
+
Some(mode),
|
|
370
|
+
action,
|
|
371
|
+
);
|
|
372
|
+
assert!(!receipt.exists());
|
|
373
|
+
let _ = std::fs::remove_file(format!("{}.sock", receipt.display()));
|
|
374
|
+
result
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#[cfg(unix)]
|
|
378
|
+
fn safe_fixture_sidecar(path: &Path, expected: &str) -> &'static str {
|
|
379
|
+
match std::fs::read_to_string(path) {
|
|
380
|
+
Ok(text) if text == expected => "present_expected",
|
|
381
|
+
Ok(_) => "present_unexpected",
|
|
382
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => "missing",
|
|
383
|
+
Err(_) => "unavailable",
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
#[cfg(unix)]
|
|
388
|
+
fn safe_runner_fixture_observation(path: &Path) -> String {
|
|
389
|
+
format!(
|
|
390
|
+
"fixture_entered={} fixture_completed={} fixture_count={}",
|
|
391
|
+
safe_fixture_sidecar(&path.with_extension("entered"), "1\n"),
|
|
392
|
+
safe_fixture_sidecar(&path.with_extension("completed"), "1\n"),
|
|
393
|
+
safe_fixture_sidecar(&path.with_extension("count"), "1:--list-models\n"),
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
#[cfg(unix)]
|
|
398
|
+
fn safe_runner_observation(
|
|
399
|
+
elapsed: Duration,
|
|
400
|
+
observation: &crate::lifecycle::launch::pi_mcp::PiCatalogTestObservation,
|
|
401
|
+
) -> String {
|
|
402
|
+
format!(
|
|
403
|
+
"elapsed_ms={} spawn_count={} argv={:?} parent_pid={:?} parent_exit_success={:?} parent_exit_code={:?} reader_timeout={}",
|
|
404
|
+
elapsed.as_millis(),
|
|
405
|
+
observation.spawn_count,
|
|
406
|
+
observation.argv,
|
|
407
|
+
observation.parent_pid,
|
|
408
|
+
observation.parent_exit_success,
|
|
409
|
+
observation.parent_exit_code,
|
|
410
|
+
observation.reader_timeout,
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#[cfg(unix)]
|
|
415
|
+
fn safe_models_result_class(result: &CmdResult) -> &'static str {
|
|
416
|
+
let CmdOutput::Human(text) = &result.output else {
|
|
417
|
+
return "non_human_output";
|
|
418
|
+
};
|
|
419
|
+
let Ok(value) = serde_json::from_str::<Value>(text) else {
|
|
420
|
+
return "non_json_output";
|
|
421
|
+
};
|
|
422
|
+
match value.get("ok").and_then(Value::as_bool) {
|
|
423
|
+
Some(true) => "success_json",
|
|
424
|
+
Some(false) => match value.get("error").and_then(Value::as_str) {
|
|
425
|
+
Some(error) if error.contains("unavailable") => "runner_unavailable",
|
|
426
|
+
Some(error) if error.contains("timed out") => "runner_timeout",
|
|
427
|
+
Some(error) if error.contains("command failed") => "runner_exit",
|
|
428
|
+
Some(error) if error.contains("could not be read") => "reader_error",
|
|
429
|
+
Some(error) if error.contains("exceeds") => "reader_oversize",
|
|
430
|
+
Some(error) if error.contains("catalog") => "parser_catalog_error",
|
|
431
|
+
Some(_) => "failure_json",
|
|
432
|
+
None => "failure_json_missing_error",
|
|
433
|
+
},
|
|
434
|
+
None => "json_missing_ok",
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#[cfg(unix)]
|
|
439
|
+
fn safe_models_observation(
|
|
440
|
+
elapsed: Duration,
|
|
441
|
+
result: &CmdResult,
|
|
442
|
+
observation: &crate::lifecycle::launch::pi_mcp::PiCatalogTestObservation,
|
|
443
|
+
) -> String {
|
|
444
|
+
format!(
|
|
445
|
+
"class={} elapsed_ms={} parent_pid={:?} parent_exit_success={:?} parent_exit_code={:?} reader_timeout={}",
|
|
446
|
+
safe_models_result_class(result),
|
|
447
|
+
elapsed.as_millis(),
|
|
448
|
+
observation.parent_pid,
|
|
449
|
+
observation.parent_exit_success,
|
|
450
|
+
observation.parent_exit_code,
|
|
451
|
+
observation.reader_timeout,
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
275
455
|
#[cfg(unix)]
|
|
276
456
|
#[test]
|
|
277
457
|
fn runner_invokes_exact_argv_once_and_drains() {
|
|
278
|
-
let path =
|
|
458
|
+
let path = fixture_with_sidecars(
|
|
279
459
|
"test \"$1\" = --list-models && echo provider model && echo openai-codex gpt-5.6-sol",
|
|
280
460
|
);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
461
|
+
let receipt = native_timeout_receipt();
|
|
462
|
+
assert!(!receipt.exists());
|
|
463
|
+
let started = Instant::now();
|
|
464
|
+
let (result, observation) =
|
|
465
|
+
crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
|
|
466
|
+
&receipt,
|
|
467
|
+
None,
|
|
468
|
+
|| run_catalog(&path, Duration::from_secs(1), 1024),
|
|
469
|
+
);
|
|
470
|
+
let elapsed = started.elapsed();
|
|
471
|
+
let fixture_observation = safe_runner_fixture_observation(&path);
|
|
472
|
+
assert!(
|
|
473
|
+
result
|
|
474
|
+
.as_ref()
|
|
475
|
+
.is_ok_and(|bytes| bytes.starts_with(b"provider model")),
|
|
476
|
+
"runner observation: {}; {}",
|
|
477
|
+
safe_runner_observation(elapsed, &observation),
|
|
478
|
+
fixture_observation
|
|
479
|
+
);
|
|
480
|
+
let _ = result.unwrap();
|
|
481
|
+
let _ = std::fs::remove_file(&receipt);
|
|
482
|
+
let _ = std::fs::remove_file(format!("{}.sock", receipt.display()));
|
|
284
483
|
assert_eq!(
|
|
285
484
|
std::fs::read_to_string(path.with_extension("count")).unwrap(),
|
|
286
485
|
"1:--list-models\n"
|
|
@@ -288,15 +487,174 @@ mod tests {
|
|
|
288
487
|
cleanup_fixture(&path);
|
|
289
488
|
}
|
|
290
489
|
|
|
490
|
+
#[cfg(unix)]
|
|
491
|
+
const NATIVE_STAGE_KEYS: [&str; 5] = [
|
|
492
|
+
"parent_entry_ms",
|
|
493
|
+
"parent_before_child_spawn_ms",
|
|
494
|
+
"parent_after_child_spawn_ms",
|
|
495
|
+
"parent_before_exit_ms",
|
|
496
|
+
"test_spawn_return_ms",
|
|
497
|
+
];
|
|
498
|
+
|
|
499
|
+
#[cfg(unix)]
|
|
500
|
+
// Match the native helper's UNIX epoch millisecond clock exactly.
|
|
501
|
+
fn append_native_stage(path: &Path, key: &str) {
|
|
502
|
+
if !NATIVE_STAGE_KEYS.contains(&key) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
let Some(milliseconds) = std::time::SystemTime::now()
|
|
506
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
507
|
+
.ok()
|
|
508
|
+
.map(|duration| duration.as_millis())
|
|
509
|
+
else {
|
|
510
|
+
return;
|
|
511
|
+
};
|
|
512
|
+
let Ok(mut file) = std::fs::OpenOptions::new()
|
|
513
|
+
.create(true)
|
|
514
|
+
.append(true)
|
|
515
|
+
.open(path)
|
|
516
|
+
else {
|
|
517
|
+
return;
|
|
518
|
+
};
|
|
519
|
+
let _ = writeln!(file, "{key}={milliseconds}");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
#[cfg(unix)]
|
|
523
|
+
fn safe_native_stage(path: &Path) -> String {
|
|
524
|
+
let (read_state, text) = match std::fs::read_to_string(path) {
|
|
525
|
+
Ok(text) => ("ok", Some(text)),
|
|
526
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => ("missing", None),
|
|
527
|
+
Err(_) => ("unavailable", None),
|
|
528
|
+
};
|
|
529
|
+
let mut fields = BTreeMap::new();
|
|
530
|
+
if let Some(ref text) = text {
|
|
531
|
+
for line in text.lines() {
|
|
532
|
+
let Some((key, value)) = line.split_once('=') else {
|
|
533
|
+
continue;
|
|
534
|
+
};
|
|
535
|
+
if NATIVE_STAGE_KEYS.contains(&key)
|
|
536
|
+
&& value.len() <= 20
|
|
537
|
+
&& value.bytes().all(|byte| byte.is_ascii_digit())
|
|
538
|
+
{
|
|
539
|
+
fields.entry(key).or_insert(value);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
let field = |key: &str| fields.get(key).copied().unwrap_or("unknown");
|
|
544
|
+
format!(
|
|
545
|
+
"stage_read={read_state} parent_entry_ms={} parent_before_child_spawn_ms={} parent_after_child_spawn_ms={} parent_before_exit_ms={} test_spawn_return_ms={}",
|
|
546
|
+
field("parent_entry_ms"),
|
|
547
|
+
field("parent_before_child_spawn_ms"),
|
|
548
|
+
field("parent_after_child_spawn_ms"),
|
|
549
|
+
field("parent_before_exit_ms"),
|
|
550
|
+
field("test_spawn_return_ms"),
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
#[cfg(unix)]
|
|
555
|
+
fn wait_for_native_parent_exit(
|
|
556
|
+
child: &mut std::process::Child,
|
|
557
|
+
stage: &Path,
|
|
558
|
+
) -> std::process::ExitStatus {
|
|
559
|
+
let deadline = Instant::now() + Duration::from_millis(500);
|
|
560
|
+
loop {
|
|
561
|
+
match child.try_wait() {
|
|
562
|
+
Ok(Some(status)) => return status,
|
|
563
|
+
Ok(None) if Instant::now() < deadline => {
|
|
564
|
+
std::thread::sleep(Duration::from_millis(1));
|
|
565
|
+
}
|
|
566
|
+
Ok(None) => {
|
|
567
|
+
let before_kill = safe_native_stage(stage);
|
|
568
|
+
let _ = child.kill();
|
|
569
|
+
let _ = child.wait();
|
|
570
|
+
let after_wait = safe_native_stage(stage);
|
|
571
|
+
panic!(
|
|
572
|
+
"native parent did not exit before test setup deadline; stage_before_kill={before_kill}; stage_after_wait={after_wait}"
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
Err(_) => {
|
|
576
|
+
let before_kill = safe_native_stage(stage);
|
|
577
|
+
let _ = child.kill();
|
|
578
|
+
let _ = child.wait();
|
|
579
|
+
let after_wait = safe_native_stage(stage);
|
|
580
|
+
panic!(
|
|
581
|
+
"native parent status could not be observed; stage_before_kill={before_kill}; stage_after_wait={after_wait}"
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/// Read/drain seam only: public `run_catalog` spawn coverage remains in the
|
|
589
|
+
/// normal success and fail-closed tests.
|
|
291
590
|
#[cfg(unix)]
|
|
292
591
|
#[test]
|
|
293
|
-
fn
|
|
294
|
-
let path =
|
|
592
|
+
fn reader_deadline_is_bounded_after_parent_exit_with_descendant_pipe() {
|
|
593
|
+
let path = native_timeout_fixture();
|
|
594
|
+
let receipt = native_timeout_receipt();
|
|
595
|
+
let stage = native_timeout_stage_receipt(&receipt);
|
|
596
|
+
assert!(!receipt.exists());
|
|
597
|
+
assert!(!stage.exists());
|
|
598
|
+
let mut parent = std::process::Command::new(&path)
|
|
599
|
+
.arg("--list-models")
|
|
600
|
+
.env("TEAM_AGENT_MODELS_TIMEOUT_RECEIPT", &receipt)
|
|
601
|
+
.env("TEAM_AGENT_MODELS_TIMEOUT_STAGE_RECEIPT", &stage)
|
|
602
|
+
.env("TEAM_AGENT_MODELS_TIMEOUT_MODE", "descendant")
|
|
603
|
+
.stdin(std::process::Stdio::null())
|
|
604
|
+
.stdout(std::process::Stdio::piped())
|
|
605
|
+
.stderr(std::process::Stdio::null())
|
|
606
|
+
.spawn()
|
|
607
|
+
.unwrap();
|
|
608
|
+
append_native_stage(&stage, "test_spawn_return_ms");
|
|
609
|
+
let parent_pid = parent.id();
|
|
610
|
+
let parent_status = wait_for_native_parent_exit(&mut parent, &stage);
|
|
611
|
+
let stdout = parent.stdout.take().expect("native parent stdout pipe");
|
|
295
612
|
let started = Instant::now();
|
|
296
|
-
let result
|
|
297
|
-
|
|
613
|
+
let (result, observation) =
|
|
614
|
+
crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
|
|
615
|
+
&receipt,
|
|
616
|
+
Some("descendant"),
|
|
617
|
+
|| {
|
|
618
|
+
crate::lifecycle::launch::pi_mcp::run_pi_catalog_after_parent_exit_for_test(
|
|
619
|
+
stdout,
|
|
620
|
+
parent_pid,
|
|
621
|
+
parent_status,
|
|
622
|
+
Duration::from_millis(40),
|
|
623
|
+
1024,
|
|
624
|
+
)
|
|
625
|
+
},
|
|
626
|
+
);
|
|
627
|
+
let elapsed = started.elapsed();
|
|
628
|
+
assert!(
|
|
629
|
+
elapsed < Duration::from_millis(500),
|
|
630
|
+
"runner observation: {}",
|
|
631
|
+
safe_runner_observation(elapsed, &observation)
|
|
632
|
+
);
|
|
298
633
|
assert!(result.unwrap_err().contains("timed out"));
|
|
299
|
-
|
|
634
|
+
assert_eq!(
|
|
635
|
+
observation.parent_exit_success,
|
|
636
|
+
Some(true),
|
|
637
|
+
"runner observation: {}",
|
|
638
|
+
safe_runner_observation(elapsed, &observation)
|
|
639
|
+
);
|
|
640
|
+
assert_eq!(observation.parent_exit_code, Some(0));
|
|
641
|
+
assert!(observation.reader_timeout);
|
|
642
|
+
|
|
643
|
+
let fields = wait_native_timeout_receipt(&receipt);
|
|
644
|
+
assert_eq!(fields["parent_started_once"], "1");
|
|
645
|
+
assert_eq!(fields["parent_argv_exact"], "1");
|
|
646
|
+
assert_eq!(fields["child_spawn_attempts"], "1");
|
|
647
|
+
assert_eq!(fields["child_argv_exact"], "1");
|
|
648
|
+
assert_eq!(fields["descendant_started"], "1");
|
|
649
|
+
assert_eq!(fields["descendant_stdout_inherited"], "1");
|
|
650
|
+
assert_eq!(
|
|
651
|
+
fields["parent_pid"],
|
|
652
|
+
observation.parent_pid.unwrap().to_string()
|
|
653
|
+
);
|
|
654
|
+
assert_eq!(fields["child_pid"], fields["descendant_pid"]);
|
|
655
|
+
let _ = std::fs::remove_file(&receipt);
|
|
656
|
+
let _ = std::fs::remove_file(&stage);
|
|
657
|
+
let _ = std::fs::remove_file(format!("{}.sock", receipt.display()));
|
|
300
658
|
}
|
|
301
659
|
|
|
302
660
|
#[cfg(unix)]
|
|
@@ -310,16 +668,72 @@ mod tests {
|
|
|
310
668
|
search: Some("sol".into()),
|
|
311
669
|
json: true,
|
|
312
670
|
};
|
|
313
|
-
let
|
|
314
|
-
|
|
315
|
-
|
|
671
|
+
let receipt = native_timeout_receipt();
|
|
672
|
+
assert!(!receipt.exists());
|
|
673
|
+
let started = Instant::now();
|
|
674
|
+
let (result, observation) =
|
|
675
|
+
crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
|
|
676
|
+
&receipt,
|
|
677
|
+
None,
|
|
678
|
+
|| cmd_models_with(&args, &path, Duration::from_secs(1), 1024),
|
|
679
|
+
);
|
|
680
|
+
let elapsed = started.elapsed();
|
|
681
|
+
let result = match result {
|
|
682
|
+
Ok(result) => result,
|
|
683
|
+
Err(_) => panic!(
|
|
684
|
+
"command boundary returned a CLI error; class=cli_error elapsed_ms={} parent_pid={:?} parent_exit_success={:?} parent_exit_code={:?} reader_timeout={}",
|
|
685
|
+
elapsed.as_millis(),
|
|
686
|
+
observation.parent_pid,
|
|
687
|
+
observation.parent_exit_success,
|
|
688
|
+
observation.parent_exit_code,
|
|
689
|
+
observation.reader_timeout,
|
|
690
|
+
),
|
|
316
691
|
};
|
|
317
|
-
let
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
692
|
+
let CmdOutput::Human(text) = &result.output else {
|
|
693
|
+
panic!(
|
|
694
|
+
"expected JSON projection; {}",
|
|
695
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
696
|
+
)
|
|
697
|
+
};
|
|
698
|
+
let value: Value = serde_json::from_str(text).unwrap_or_else(|_| {
|
|
699
|
+
panic!(
|
|
700
|
+
"expected valid JSON projection; {}",
|
|
701
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
702
|
+
)
|
|
703
|
+
});
|
|
704
|
+
assert_eq!(
|
|
705
|
+
value["schema_version"],
|
|
706
|
+
"models.v1",
|
|
707
|
+
"{}",
|
|
708
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
709
|
+
);
|
|
710
|
+
assert_eq!(
|
|
711
|
+
value["models"][0]["role_model"],
|
|
712
|
+
"openai-codex/gpt-5.6-sol",
|
|
713
|
+
"{}",
|
|
714
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
715
|
+
);
|
|
716
|
+
assert_eq!(
|
|
717
|
+
value["models"][0]["current"],
|
|
718
|
+
false,
|
|
719
|
+
"{}",
|
|
720
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
721
|
+
);
|
|
722
|
+
assert_eq!(
|
|
723
|
+
value["auth"],
|
|
724
|
+
"ok",
|
|
725
|
+
"{}",
|
|
726
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
727
|
+
);
|
|
728
|
+
let model_count = value["models"].as_array().map(|models| models.len());
|
|
729
|
+
assert_eq!(
|
|
730
|
+
model_count,
|
|
731
|
+
Some(1),
|
|
732
|
+
"{}",
|
|
733
|
+
safe_models_observation(elapsed, &result, &observation)
|
|
734
|
+
);
|
|
735
|
+
let _ = std::fs::remove_file(&receipt);
|
|
736
|
+
let _ = std::fs::remove_file(format!("{}.sock", receipt.display()));
|
|
323
737
|
let human = cmd_models_with(
|
|
324
738
|
&ModelsArgs {
|
|
325
739
|
provider: "pi".into(),
|
|
@@ -352,35 +766,65 @@ mod tests {
|
|
|
352
766
|
#[cfg(unix)]
|
|
353
767
|
#[test]
|
|
354
768
|
fn runner_fails_closed_for_nonzero_oversize_timeout_and_unavailable() {
|
|
355
|
-
let fail =
|
|
769
|
+
let fail = native_timeout_fixture();
|
|
770
|
+
let started = Instant::now();
|
|
771
|
+
let (failed_result, fail_observation) = with_native_mode("exit7", || {
|
|
772
|
+
run_catalog(&fail, Duration::from_secs(1), 1024)
|
|
773
|
+
});
|
|
774
|
+
let elapsed = started.elapsed();
|
|
356
775
|
assert_eq!(
|
|
357
|
-
|
|
358
|
-
"Pi model catalog command failed"
|
|
776
|
+
failed_result.unwrap_err(),
|
|
777
|
+
"Pi model catalog command failed",
|
|
778
|
+
"runner observation: {}",
|
|
779
|
+
safe_runner_observation(elapsed, &fail_observation)
|
|
359
780
|
);
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
781
|
+
assert_eq!(fail_observation.spawn_count, 1);
|
|
782
|
+
assert_eq!(fail_observation.argv, vec!["--list-models"]);
|
|
783
|
+
assert_eq!(fail_observation.parent_exit_success, Some(false));
|
|
784
|
+
assert_eq!(fail_observation.parent_exit_code, Some(7));
|
|
785
|
+
assert!(!fail_observation.reader_timeout);
|
|
786
|
+
let (failed, failed_observation) = with_native_mode("exit7", || {
|
|
787
|
+
cmd_models_with(
|
|
788
|
+
&ModelsArgs {
|
|
789
|
+
provider: "pi".into(),
|
|
790
|
+
search: None,
|
|
791
|
+
json: false,
|
|
792
|
+
},
|
|
793
|
+
&fail,
|
|
794
|
+
Duration::from_secs(1),
|
|
795
|
+
1024,
|
|
796
|
+
)
|
|
797
|
+
});
|
|
798
|
+
let failed = failed.unwrap();
|
|
799
|
+
assert_eq!(failed_observation.spawn_count, 1);
|
|
800
|
+
assert_eq!(failed_observation.argv, vec!["--list-models"]);
|
|
801
|
+
assert_eq!(failed_observation.parent_exit_success, Some(false));
|
|
802
|
+
assert_eq!(failed_observation.parent_exit_code, Some(7));
|
|
803
|
+
assert!(!failed_observation.reader_timeout);
|
|
371
804
|
let CmdOutput::Human(text) = failed.output else {
|
|
372
805
|
panic!("expected failure projection")
|
|
373
806
|
};
|
|
374
807
|
assert_eq!(failed.exit, ExitCode::Error);
|
|
375
808
|
assert!(!text.contains("sensitive-token"));
|
|
376
|
-
let big =
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
809
|
+
let big = native_timeout_fixture();
|
|
810
|
+
let (big_result, big_observation) = with_native_mode("oversize", || {
|
|
811
|
+
run_catalog(&big, Duration::from_secs(1), 8)
|
|
812
|
+
});
|
|
813
|
+
assert!(big_result.unwrap_err().contains("bounded output"));
|
|
814
|
+
assert_eq!(big_observation.spawn_count, 1);
|
|
815
|
+
assert_eq!(big_observation.argv, vec!["--list-models"]);
|
|
816
|
+
assert_eq!(big_observation.parent_exit_success, Some(true));
|
|
817
|
+
assert_eq!(big_observation.parent_exit_code, Some(0));
|
|
818
|
+
assert!(!big_observation.reader_timeout);
|
|
819
|
+
let slow = native_timeout_fixture();
|
|
820
|
+
let (slow_result, slow_observation) = with_native_mode("sleep", || {
|
|
821
|
+
run_catalog(&slow, Duration::from_millis(20), 1024)
|
|
822
|
+
});
|
|
823
|
+
assert!(slow_result.unwrap_err().contains("timed out"));
|
|
824
|
+
assert_eq!(slow_observation.spawn_count, 1);
|
|
825
|
+
assert_eq!(slow_observation.argv, vec!["--list-models"]);
|
|
826
|
+
assert_eq!(slow_observation.parent_exit_success, None);
|
|
827
|
+
assert!(!slow_observation.reader_timeout);
|
|
384
828
|
assert!(run_catalog(
|
|
385
829
|
std::path::Path::new("/does/not/exist"),
|
|
386
830
|
Duration::from_secs(1),
|
|
@@ -388,9 +832,6 @@ mod tests {
|
|
|
388
832
|
)
|
|
389
833
|
.unwrap_err()
|
|
390
834
|
.contains("unavailable"));
|
|
391
|
-
for path in [fail, big, slow] {
|
|
392
|
-
cleanup_fixture(&path);
|
|
393
|
-
}
|
|
394
835
|
}
|
|
395
836
|
|
|
396
837
|
#[test]
|
|
@@ -694,9 +694,23 @@ fn resolve_worker(
|
|
|
694
694
|
}
|
|
695
695
|
};
|
|
696
696
|
let matches = matching_session_window(&targets, session, window);
|
|
697
|
-
match matches.len() {
|
|
698
|
-
1 =>
|
|
699
|
-
|
|
697
|
+
let selected_pane = match matches.len() {
|
|
698
|
+
1 => matches.first().copied(),
|
|
699
|
+
n if n > 1 => {
|
|
700
|
+
let state_pane_id =
|
|
701
|
+
string_field(agent_entry, "pane_id").filter(|pane_id| !pane_id.is_empty());
|
|
702
|
+
let mut state_matches = matches.iter().copied().filter(|pane| {
|
|
703
|
+
state_pane_id.is_some_and(|pane_id| pane.pane_id.as_str() == pane_id)
|
|
704
|
+
});
|
|
705
|
+
match (state_matches.next(), state_matches.next()) {
|
|
706
|
+
(Some(pane), None) => Some(pane),
|
|
707
|
+
_ => None,
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
_ => None,
|
|
711
|
+
};
|
|
712
|
+
match selected_pane {
|
|
713
|
+
Some(pane) => {
|
|
700
714
|
let state_pane_id = string_field(agent_entry, "pane_id").map(str::to_string);
|
|
701
715
|
let state_pane_stale = state_pane_id
|
|
702
716
|
.as_deref()
|
|
@@ -727,7 +741,7 @@ fn resolve_worker(
|
|
|
727
741
|
warning,
|
|
728
742
|
})
|
|
729
743
|
}
|
|
730
|
-
|
|
744
|
+
None if matches.is_empty() => Ok(resolved_worker_without_live(
|
|
731
745
|
sender_workspace,
|
|
732
746
|
target_workspace,
|
|
733
747
|
team,
|
|
@@ -738,7 +752,7 @@ fn resolve_worker(
|
|
|
738
752
|
window,
|
|
739
753
|
transport.tmux_endpoint(),
|
|
740
754
|
)),
|
|
741
|
-
|
|
755
|
+
None => Err(name_ambiguous(
|
|
742
756
|
"multiple live panes match the same session/window",
|
|
743
757
|
matches
|
|
744
758
|
.into_iter()
|