@team-agent/installer 0.5.77 → 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/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 +362 -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/snapshot.rs +15 -12
- package/crates/team-agent/src/cli/status_port/store.rs +1 -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/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 +151 -14
- 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 +65 -18
- 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/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_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 +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/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/state/persist.rs +97 -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 +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,16 +1,33 @@
|
|
|
1
1
|
use std::fs::OpenOptions;
|
|
2
|
-
use std::io::
|
|
3
|
-
use std::os::unix::net::{UnixListener, UnixStream};
|
|
2
|
+
use std::io::Write;
|
|
4
3
|
use std::path::{Path, PathBuf};
|
|
5
4
|
use std::process::{Command, Stdio};
|
|
6
|
-
use std::time::Duration;
|
|
5
|
+
use std::time::{Duration, Instant};
|
|
7
6
|
|
|
8
7
|
const CHILD_FLAG: &str = "--team-agent-models-timeout-child";
|
|
9
8
|
const LIST_MODELS_FLAG: &str = "--list-models";
|
|
10
9
|
const RECEIPT_ENV: &str = "TEAM_AGENT_MODELS_TIMEOUT_RECEIPT";
|
|
10
|
+
const STAGE_RECEIPT_ENV: &str = "TEAM_AGENT_MODELS_TIMEOUT_STAGE_RECEIPT";
|
|
11
11
|
const MODE_ENV: &str = "TEAM_AGENT_MODELS_TIMEOUT_MODE";
|
|
12
|
-
const READY_SOCKET_ENV: &str = "TEAM_AGENT_MODELS_TIMEOUT_READY_SOCKET";
|
|
13
12
|
const STDOUT_MARKER: &[u8] = b"__team_agent_models_timeout_descendant_stdout_v1__\n";
|
|
13
|
+
const PARENT_HANDOFF_TIMEOUT: Duration = Duration::from_secs(1);
|
|
14
|
+
|
|
15
|
+
// Stage values are UNIX epoch milliseconds so the test process and helper use
|
|
16
|
+
// one directly comparable clock. Stage writes are best-effort diagnostics and
|
|
17
|
+
// never change the fixture's production-facing exit semantics.
|
|
18
|
+
fn append_stage(key: &str) {
|
|
19
|
+
let Some(path) = std::env::var_os(STAGE_RECEIPT_ENV) else {
|
|
20
|
+
return;
|
|
21
|
+
};
|
|
22
|
+
let Some(milliseconds) = std::time::SystemTime::now()
|
|
23
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
24
|
+
.ok()
|
|
25
|
+
.map(|duration| duration.as_millis())
|
|
26
|
+
else {
|
|
27
|
+
return;
|
|
28
|
+
};
|
|
29
|
+
let _ = append_receipt(Path::new(&path), key, milliseconds);
|
|
30
|
+
}
|
|
14
31
|
|
|
15
32
|
fn receipt_path() -> PathBuf {
|
|
16
33
|
std::env::var_os(RECEIPT_ENV)
|
|
@@ -27,14 +44,29 @@ fn append_receipt(path: &Path, key: &str, value: impl std::fmt::Display) -> Resu
|
|
|
27
44
|
writeln!(receipt, "{key}={value}").map_err(|_| ())
|
|
28
45
|
}
|
|
29
46
|
|
|
47
|
+
fn wait_for_parent_handoff(receipt: &Path) {
|
|
48
|
+
let expected = format!("child_pid={}", std::process::id());
|
|
49
|
+
let deadline = Instant::now() + PARENT_HANDOFF_TIMEOUT;
|
|
50
|
+
loop {
|
|
51
|
+
if std::fs::read_to_string(receipt)
|
|
52
|
+
.ok()
|
|
53
|
+
.is_some_and(|text| text.lines().any(|line| line == expected))
|
|
54
|
+
{
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if Instant::now() >= deadline {
|
|
58
|
+
std::process::exit(5);
|
|
59
|
+
}
|
|
60
|
+
std::thread::sleep(Duration::from_millis(1));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
30
64
|
fn child_main(args: &[String]) {
|
|
31
65
|
if args.len() != 1 || args[0] != CHILD_FLAG {
|
|
32
66
|
std::process::exit(5);
|
|
33
67
|
}
|
|
34
68
|
let receipt = receipt_path();
|
|
35
|
-
|
|
36
|
-
.map(PathBuf::from)
|
|
37
|
-
.unwrap_or_else(|| std::process::exit(5));
|
|
69
|
+
wait_for_parent_handoff(&receipt);
|
|
38
70
|
|
|
39
71
|
let mut stdout = std::io::stdout();
|
|
40
72
|
if stdout.write_all(STDOUT_MARKER).is_err() || stdout.flush().is_err() {
|
|
@@ -48,10 +80,6 @@ fn child_main(args: &[String]) {
|
|
|
48
80
|
std::process::exit(5);
|
|
49
81
|
}
|
|
50
82
|
|
|
51
|
-
let mut ready = UnixStream::connect(socket).unwrap_or_else(|_| std::process::exit(5));
|
|
52
|
-
if ready.write_all(b"1").is_err() {
|
|
53
|
-
std::process::exit(5);
|
|
54
|
-
}
|
|
55
83
|
std::thread::sleep(Duration::from_secs(2));
|
|
56
84
|
}
|
|
57
85
|
|
|
@@ -86,10 +114,8 @@ fn main() {
|
|
|
86
114
|
Ok(_) | Err(std::env::VarError::NotUnicode(_)) => std::process::exit(2),
|
|
87
115
|
}
|
|
88
116
|
|
|
117
|
+
append_stage("parent_entry_ms");
|
|
89
118
|
let receipt = receipt_path();
|
|
90
|
-
let socket = PathBuf::from(format!("{}.sock", receipt.display()));
|
|
91
|
-
let _ = std::fs::remove_file(&socket);
|
|
92
|
-
let listener = UnixListener::bind(&socket).unwrap_or_else(|_| std::process::exit(4));
|
|
93
119
|
if append_receipt(&receipt, "parent_started_once", 1).is_err()
|
|
94
120
|
|| append_receipt(&receipt, "parent_argv_exact", 1).is_err()
|
|
95
121
|
|| append_receipt(&receipt, "parent_pid", std::process::id()).is_err()
|
|
@@ -99,24 +125,20 @@ fn main() {
|
|
|
99
125
|
}
|
|
100
126
|
|
|
101
127
|
let executable = std::env::current_exe().unwrap_or_else(|_| std::process::exit(4));
|
|
128
|
+
append_stage("parent_before_child_spawn_ms");
|
|
102
129
|
let child = Command::new(executable)
|
|
103
130
|
.arg(CHILD_FLAG)
|
|
104
131
|
.env(RECEIPT_ENV, &receipt)
|
|
105
|
-
.env(READY_SOCKET_ENV, &socket)
|
|
106
132
|
.stdin(Stdio::null())
|
|
107
133
|
.stdout(Stdio::inherit())
|
|
108
134
|
.stderr(Stdio::null())
|
|
109
135
|
.spawn()
|
|
110
136
|
.unwrap_or_else(|_| std::process::exit(4));
|
|
111
|
-
|
|
112
|
-
let mut byte = [0_u8; 1];
|
|
113
|
-
if ready.read_exact(&mut byte).is_err() || byte != [b'1'] {
|
|
114
|
-
std::process::exit(4);
|
|
115
|
-
}
|
|
137
|
+
append_stage("parent_after_child_spawn_ms");
|
|
116
138
|
if append_receipt(&receipt, "child_pid", child.id()).is_err() {
|
|
117
139
|
std::process::exit(4);
|
|
118
140
|
}
|
|
119
|
-
|
|
141
|
+
append_stage("parent_before_exit_ms");
|
|
120
142
|
// The parent exits successfully without waiting; the child retains the
|
|
121
143
|
// inherited stdout pipe and self-exits after the real reader deadline.
|
|
122
144
|
}
|
|
@@ -28,10 +28,10 @@ use crate::layout::worker_env::{
|
|
|
28
28
|
use crate::model::enums::{PaneLiveness, Provider};
|
|
29
29
|
use crate::transport::{
|
|
30
30
|
command_basename, normalize_capture, tmux_capture_argv, tmux_query_argv, tmux_send_keys_argv,
|
|
31
|
-
tmux_spawn_argv, tmux_submit_key_name, AttachOutcome,
|
|
32
|
-
InjectPayload, InjectStage, InjectVerification, InputSurfaceProbe, Key,
|
|
33
|
-
|
|
34
|
-
TransportError, TurnVerification, WindowName,
|
|
31
|
+
tmux_spawn_argv, tmux_submit_key_name, AttachOutcome, BusySignalKind, CaptureRange,
|
|
32
|
+
CaptureSampleOutcome, InjectPayload, InjectStage, InjectVerification, InputSurfaceProbe, Key,
|
|
33
|
+
PaneField, PaneId, QueryOutcome, SessionName, SetEnvOutcome, SubmitConsumptionReason,
|
|
34
|
+
SubmitVerification, Target, Transport, TransportError, TurnVerification, WindowName,
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
type RecordedArgv = Arc<Mutex<Vec<Vec<String>>>>;
|
|
@@ -1051,6 +1051,15 @@ fn inject_text_runs_buffer_paste_submit_sequence_and_reports_submit() {
|
|
|
1051
1051
|
SubmitVerification::EnterSentWithoutPlaceholderCheck
|
|
1052
1052
|
);
|
|
1053
1053
|
assert_eq!(report.turn_verification, TurnVerification::NotYetObserved);
|
|
1054
|
+
let diagnostics = report.submit_diagnostics.expect("diagnostics");
|
|
1055
|
+
assert_eq!(
|
|
1056
|
+
diagnostics.consumption_reason,
|
|
1057
|
+
SubmitConsumptionReason::NoMarker
|
|
1058
|
+
);
|
|
1059
|
+
assert_eq!(diagnostics.busy_signal_kind, None);
|
|
1060
|
+
assert_eq!(diagnostics.current_marker_in_bottom_15, None);
|
|
1061
|
+
assert_eq!(diagnostics.paste_identity_in_composer, None);
|
|
1062
|
+
assert_eq!(diagnostics.consumption_from_capture_result, None);
|
|
1054
1063
|
}
|
|
1055
1064
|
|
|
1056
1065
|
#[test]
|
|
@@ -1409,10 +1418,18 @@ fn e46_post_submit_matched_token_without_scroll_is_unverified() {
|
|
|
1409
1418
|
"token still in composer and no busy must report 没开跑, not unknown/started"
|
|
1410
1419
|
);
|
|
1411
1420
|
let diagnostics = report.submit_diagnostics.expect("diagnostics");
|
|
1421
|
+
assert_eq!(diagnostics.busy_signal_kind, None);
|
|
1422
|
+
assert_eq!(diagnostics.current_marker_in_bottom_15, Some(true));
|
|
1423
|
+
assert_eq!(diagnostics.paste_identity_in_composer, None);
|
|
1424
|
+
assert_eq!(diagnostics.consumption_from_capture_result, Some(false));
|
|
1412
1425
|
assert!(
|
|
1413
1426
|
diagnostics.attempts_detail.iter().any(|obs| obs.matched),
|
|
1414
1427
|
"the unverified verdict must still record that paste landed"
|
|
1415
1428
|
);
|
|
1429
|
+
assert_eq!(
|
|
1430
|
+
diagnostics.consumption_reason,
|
|
1431
|
+
SubmitConsumptionReason::Unverified
|
|
1432
|
+
);
|
|
1416
1433
|
}
|
|
1417
1434
|
|
|
1418
1435
|
#[test]
|
|
@@ -1452,6 +1469,56 @@ fn e46_unconsumed_token_with_live_busy_state_is_treated_as_processing() {
|
|
|
1452
1469
|
"busy-state capture should be recorded in attempts_detail: {:?}",
|
|
1453
1470
|
diagnostics.attempts_detail
|
|
1454
1471
|
);
|
|
1472
|
+
assert_eq!(
|
|
1473
|
+
diagnostics.consumption_reason,
|
|
1474
|
+
SubmitConsumptionReason::FallbackBusy
|
|
1475
|
+
);
|
|
1476
|
+
assert_eq!(diagnostics.before_busy_signal_kind, None);
|
|
1477
|
+
assert_eq!(diagnostics.busy_signal_kind, Some(BusySignalKind::Working));
|
|
1478
|
+
assert_eq!(diagnostics.busy_line_from_bottom, Some(0));
|
|
1479
|
+
assert_eq!(diagnostics.current_marker_in_bottom_15, Some(true));
|
|
1480
|
+
assert_eq!(diagnostics.paste_identity_in_composer, None);
|
|
1481
|
+
assert_eq!(diagnostics.consumption_from_capture_result, Some(false));
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
#[test]
|
|
1485
|
+
fn e46_capture_error_clears_previous_consumption_observation() {
|
|
1486
|
+
let token_text = "Team Agent message from leader:\n\nhi\n\n[team-agent-token:msg_capture_error]";
|
|
1487
|
+
let busy_tail = format!("{token_text}\n● Working (1s · esc to interrupt)\n");
|
|
1488
|
+
let visible = ok(token_text);
|
|
1489
|
+
let queued = vec![
|
|
1490
|
+
MockResp::Out(ok("")), // set-buffer
|
|
1491
|
+
MockResp::Out(ok("")), // paste-buffer
|
|
1492
|
+
MockResp::Out(ok("")), // delete-buffer
|
|
1493
|
+
MockResp::Out(visible.clone()), // pasted-content prompt: continue
|
|
1494
|
+
MockResp::Out(visible), // pre-submit token visibility
|
|
1495
|
+
MockResp::Out(ok("")), // Escape
|
|
1496
|
+
MockResp::Out(ok("")), // Enter
|
|
1497
|
+
MockResp::Out(ok(&busy_tail)), // first post-Enter capture: busy
|
|
1498
|
+
// Default capture error forces the next poll/fallback/resend reads to fail.
|
|
1499
|
+
];
|
|
1500
|
+
let (be, _rec) = backend_with(MockResp::Io(std::io::ErrorKind::Other), queued);
|
|
1501
|
+
let report = be
|
|
1502
|
+
.inject(
|
|
1503
|
+
&Target::Pane(PaneId::new("%7")),
|
|
1504
|
+
&InjectPayload::Text(token_text.to_string()),
|
|
1505
|
+
Key::Enter,
|
|
1506
|
+
true,
|
|
1507
|
+
)
|
|
1508
|
+
.expect("inject runs through capture failure");
|
|
1509
|
+
let diagnostics = report.submit_diagnostics.expect("diagnostics");
|
|
1510
|
+
assert_eq!(
|
|
1511
|
+
report.submit_verification,
|
|
1512
|
+
SubmitVerification::SubmitConsumptionUnverified
|
|
1513
|
+
);
|
|
1514
|
+
assert_eq!(diagnostics.consumption_reason, SubmitConsumptionReason::Unverified);
|
|
1515
|
+
assert!(diagnostics.capture_err_count > 0);
|
|
1516
|
+
assert_eq!(diagnostics.before_busy_signal_kind, None);
|
|
1517
|
+
assert_eq!(diagnostics.busy_signal_kind, None);
|
|
1518
|
+
assert_eq!(diagnostics.busy_line_from_bottom, None);
|
|
1519
|
+
assert_eq!(diagnostics.current_marker_in_bottom_15, None);
|
|
1520
|
+
assert_eq!(diagnostics.paste_identity_in_composer, None);
|
|
1521
|
+
assert_eq!(diagnostics.consumption_from_capture_result, None);
|
|
1455
1522
|
}
|
|
1456
1523
|
|
|
1457
1524
|
/// NeverSeen + 无占位符:空 pane 不得再把「token 不在底部」写成 consumed。
|
|
@@ -1521,6 +1588,14 @@ fn e46_inject_text_with_token_consumed_after_enter_keeps_enter_sent_without_plac
|
|
|
1521
1588
|
hold (provider_submit_verification_red.rs:113-159). Got {:?}",
|
|
1522
1589
|
report.submit_verification
|
|
1523
1590
|
);
|
|
1591
|
+
let diagnostics = report.submit_diagnostics.expect("diagnostics");
|
|
1592
|
+
assert_eq!(
|
|
1593
|
+
diagnostics.consumption_reason,
|
|
1594
|
+
SubmitConsumptionReason::ConsumptionFromCapture
|
|
1595
|
+
);
|
|
1596
|
+
assert_eq!(diagnostics.busy_signal_kind, None);
|
|
1597
|
+
assert_eq!(diagnostics.current_marker_in_bottom_15, Some(false));
|
|
1598
|
+
assert_eq!(diagnostics.consumption_from_capture_result, Some(true));
|
|
1524
1599
|
}
|
|
1525
1600
|
|
|
1526
1601
|
/// **E46 RED-3 (resend-to-cap, no double-submit)**: when the FIRST Enter
|
|
@@ -3289,6 +3364,15 @@ fn cursor_single_enter_busy_transcript_placeholder_does_not_retry() {
|
|
|
3289
3364
|
"busy after first Enter is consumption for cursor; got {:?}",
|
|
3290
3365
|
report.submit_verification
|
|
3291
3366
|
);
|
|
3367
|
+
let diagnostics = report.submit_diagnostics.expect("diagnostics");
|
|
3368
|
+
assert_eq!(
|
|
3369
|
+
diagnostics.consumption_reason,
|
|
3370
|
+
SubmitConsumptionReason::CursorPollingBusy
|
|
3371
|
+
);
|
|
3372
|
+
assert_eq!(diagnostics.busy_signal_kind, Some(BusySignalKind::Working));
|
|
3373
|
+
assert!(diagnostics.busy_line_from_bottom.is_some());
|
|
3374
|
+
assert!(diagnostics.current_marker_in_bottom_15.is_some());
|
|
3375
|
+
assert!(diagnostics.consumption_from_capture_result.is_some());
|
|
3292
3376
|
let calls = rec.lock().unwrap().clone();
|
|
3293
3377
|
assert_eq!(
|
|
3294
3378
|
count_submit_enters(&calls),
|
|
@@ -3570,6 +3654,39 @@ fn grok_fold_worker_text_payload_polls_consumption() {
|
|
|
3570
3654
|
);
|
|
3571
3655
|
}
|
|
3572
3656
|
|
|
3657
|
+
#[test]
|
|
3658
|
+
fn provider_busy_signal_match_preserves_category_priority_and_tail_boundary() {
|
|
3659
|
+
let cases = [
|
|
3660
|
+
("working", BusySignalKind::Working),
|
|
3661
|
+
("thinking", BusySignalKind::Thinking),
|
|
3662
|
+
("processing", BusySignalKind::Processing),
|
|
3663
|
+
("esc to interrupt", BusySignalKind::EscToInterrupt),
|
|
3664
|
+
("⠋", BusySignalKind::SpinnerGlyph),
|
|
3665
|
+
("working ⠋", BusySignalKind::Working),
|
|
3666
|
+
];
|
|
3667
|
+
for (text, expected) in cases {
|
|
3668
|
+
let matched = super::provider_busy_signal_match(text).expect("busy match");
|
|
3669
|
+
assert_eq!(matched.kind, expected, "text={text:?}");
|
|
3670
|
+
assert_eq!(matched.line_from_bottom, 0, "text={text:?}");
|
|
3671
|
+
assert!(super::provider_busy_signal_in_tail(text));
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
let inside_bottom_15 = std::iter::once("working".to_string())
|
|
3675
|
+
.chain((0..14).map(|index| format!("tail-{index}")))
|
|
3676
|
+
.collect::<Vec<_>>()
|
|
3677
|
+
.join("\n");
|
|
3678
|
+
let matched = super::provider_busy_signal_match(&inside_bottom_15).expect("15th line");
|
|
3679
|
+
assert_eq!(matched.kind, BusySignalKind::Working);
|
|
3680
|
+
assert_eq!(matched.line_from_bottom, 14);
|
|
3681
|
+
|
|
3682
|
+
let outside_bottom_15 = std::iter::once("working".to_string())
|
|
3683
|
+
.chain((0..15).map(|index| format!("tail-{index}")))
|
|
3684
|
+
.collect::<Vec<_>>()
|
|
3685
|
+
.join("\n");
|
|
3686
|
+
assert!(super::provider_busy_signal_match(&outside_bottom_15).is_none());
|
|
3687
|
+
assert!(!super::provider_busy_signal_in_tail(&outside_bottom_15));
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3573
3690
|
const G4_TOKEN: &str = "[team-agent-token:msg_g4]";
|
|
3574
3691
|
|
|
3575
3692
|
#[test]
|