@team-agent/installer 0.5.105 → 0.5.107
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 +5 -3
- package/README.zh.md +4 -3
- package/crates/team-agent/src/cli/adapters.rs +15 -1
- package/crates/team-agent/src/cli/emit.rs +33 -13
- package/crates/team-agent/src/cli/mod.rs +148 -21
- package/crates/team-agent/src/cli/status_port/brief.rs +62 -3
- package/crates/team-agent/src/cli/tests/base.rs +112 -225
- package/crates/team-agent/src/cli/tests/run_delegation.rs +4 -2
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -2
- package/crates/team-agent/src/compiler/tests.rs +46 -59
- package/crates/team-agent/src/leader/tests/byte_findings.rs +1 -14
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +57 -173
- package/crates/team-agent/src/lifecycle/launch/pi_mcp_extension.js +226 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +0 -37
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +8 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +24 -17
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +22 -9
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_native_mcp_host.mjs +71 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +43 -66
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +71 -392
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +0 -5
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -361
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -16
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +20 -112
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1 -12
- package/crates/team-agent/src/lifecycle/tests/pi_native_mcp_capability_red.rs +546 -0
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +6 -2
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +11 -54
- package/crates/team-agent/src/messaging/tests/wave2.rs +2 -302
- package/crates/team-agent/src/model/name_similarity.rs +52 -67
- package/crates/team-agent/src/model/task_graph.rs +37 -124
- package/crates/team-agent/src/model/yaml/tests.rs +46 -165
- package/crates/team-agent/src/provider/tests/adapter.rs +0 -20
- package/crates/team-agent/src/provider/tests/classify.rs +106 -198
- package/crates/team-agent/src/state/projection.rs +15 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +315 -501
- package/crates/team-agent/src/tmux_backend.rs +110 -0
- package/crates/team-agent/src/transport/test_support.rs +35 -0
- package/crates/team-agent/src/transport/tests/wire.rs +32 -58
- package/crates/team-agent/src/transport.rs +26 -12
- package/package.json +4 -4
- package/skills/team-agent/references/recovery-runbook.md +2 -2
|
@@ -355,6 +355,21 @@ fn backend_with(default: MockResp, queued: Vec<MockResp>) -> (TmuxBackend, Recor
|
|
|
355
355
|
(TmuxBackend::with_runner(Box::new(runner)), recorded)
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
#[test]
|
|
359
|
+
fn pane_dead_observation_requires_native_boolean_receipt() {
|
|
360
|
+
for (text, expected) in [("1\n", Some(true)), ("0\n", Some(false)), ("", None), ("dead", None), ("2", None)] {
|
|
361
|
+
let (backend, recorded) = backend_with(MockResp::Out(ok(text)), Vec::new());
|
|
362
|
+
assert_eq!(backend.pane_is_dead(&PaneId::new("%47")).unwrap(), expected);
|
|
363
|
+
assert_eq!(recorded.lock().unwrap()[0], svec(&[
|
|
364
|
+
"tmux", "display-message", "-p", "-t", "%47", "#{pane_dead}",
|
|
365
|
+
]));
|
|
366
|
+
}
|
|
367
|
+
let (backend, _) = backend_with(MockResp::Out(fail(1, "pane unavailable")), Vec::new());
|
|
368
|
+
assert_eq!(backend.pane_is_dead(&PaneId::new("%47")).unwrap(), None);
|
|
369
|
+
let (backend, _) = backend_with(MockResp::Io(std::io::ErrorKind::TimedOut), Vec::new());
|
|
370
|
+
assert!(backend.pane_is_dead(&PaneId::new("%47")).is_err());
|
|
371
|
+
}
|
|
372
|
+
|
|
358
373
|
#[test]
|
|
359
374
|
fn provider_exit_receipt_accepts_only_shell_exit_status() {
|
|
360
375
|
for (text, expected) in [
|
|
@@ -2202,6 +2217,128 @@ fn kill_session_and_kill_window_argv() {
|
|
|
2202
2217
|
);
|
|
2203
2218
|
}
|
|
2204
2219
|
|
|
2220
|
+
#[test]
|
|
2221
|
+
fn stop_session_preservation_creates_processless_anchor_by_exact_pane_id() {
|
|
2222
|
+
let (be, rec) = backend_with(
|
|
2223
|
+
MockResp::Out(ok("")),
|
|
2224
|
+
vec![MockResp::Out(ok("%7\tworker\n")), MockResp::Out(ok("%8\n"))],
|
|
2225
|
+
);
|
|
2226
|
+
be.preserve_session_before_stop(
|
|
2227
|
+
&SessionName::new("sess"),
|
|
2228
|
+
&[Target::Pane(PaneId::new("%7"))],
|
|
2229
|
+
).unwrap();
|
|
2230
|
+
let calls = rec.lock().unwrap();
|
|
2231
|
+
assert_eq!(calls.len(), 4);
|
|
2232
|
+
assert_eq!(calls[0], svec(&[
|
|
2233
|
+
"tmux", "list-panes", "-s", "-t", "=sess", "-F", "#{pane_id}\t#{window_name}",
|
|
2234
|
+
]));
|
|
2235
|
+
assert_eq!(calls[1], tmux_spawn_argv(
|
|
2236
|
+
&SessionName::new("=sess"), &WindowName::new("[team-agent keepalive]"),
|
|
2237
|
+
"exec sleep 86400", false,
|
|
2238
|
+
));
|
|
2239
|
+
assert_eq!(calls[2], svec(&[
|
|
2240
|
+
"tmux", "set-window-option", "-t", "%8", "remain-on-exit", "on",
|
|
2241
|
+
]));
|
|
2242
|
+
assert_eq!(calls[3], svec(&["tmux", "respawn-pane", "-k", "-t", "%8", "exit 0"]));
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
#[test]
|
|
2246
|
+
fn stop_session_preservation_skips_surviving_workers_leaders_and_anchors() {
|
|
2247
|
+
for survivor in ["other-worker", "leader", "[team-agent keepalive]", "worker"] {
|
|
2248
|
+
let (be, rec) = backend_with(
|
|
2249
|
+
MockResp::Out(ok(&format!("%7\tworker\n%8\t{survivor}\n"))), vec![],
|
|
2250
|
+
);
|
|
2251
|
+
be.preserve_session_before_stop(
|
|
2252
|
+
&SessionName::new("sess"), &[Target::Pane(PaneId::new("%7"))],
|
|
2253
|
+
).unwrap();
|
|
2254
|
+
assert_eq!(rec.lock().unwrap().len(), 1, "survivor={survivor}");
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
#[test]
|
|
2259
|
+
fn stop_session_preservation_covers_duplicate_panes_and_legacy_window_targets() {
|
|
2260
|
+
for targets in [
|
|
2261
|
+
vec![Target::Pane(PaneId::new("%7")), Target::Pane(PaneId::new("%8"))],
|
|
2262
|
+
vec![Target::SessionWindow { session: SessionName::new("sess"), window: WindowName::new("worker") }],
|
|
2263
|
+
] {
|
|
2264
|
+
let (be, rec) = backend_with(MockResp::Out(ok("")), vec![
|
|
2265
|
+
MockResp::Out(ok("%7\tworker\n%8\tworker\n")), MockResp::Out(ok("%9\n")),
|
|
2266
|
+
]);
|
|
2267
|
+
be.preserve_session_before_stop(&SessionName::new("sess"), &targets).unwrap();
|
|
2268
|
+
assert_eq!(rec.lock().unwrap().len(), 4);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
#[test]
|
|
2273
|
+
fn stop_session_preservation_refuses_unknown_listing_and_rolls_back_failed_anchor() {
|
|
2274
|
+
for response in [fail(1, "listing failed"), ok(""), ok("malformed")] {
|
|
2275
|
+
let (be, rec) = backend_with(MockResp::Out(response), vec![]);
|
|
2276
|
+
assert!(be.preserve_session_before_stop(
|
|
2277
|
+
&SessionName::new("sess"), &[Target::Pane(PaneId::new("%7"))],
|
|
2278
|
+
).is_err());
|
|
2279
|
+
assert_eq!(rec.lock().unwrap().len(), 1);
|
|
2280
|
+
}
|
|
2281
|
+
for failed_step in ["new-window", "set-window-option", "respawn-pane"] {
|
|
2282
|
+
let mut queued = vec![MockResp::Out(ok("%7\tworker\n"))];
|
|
2283
|
+
if failed_step != "new-window" { queued.push(MockResp::Out(ok("%8\n"))); }
|
|
2284
|
+
if failed_step == "respawn-pane" { queued.push(MockResp::Out(ok(""))); }
|
|
2285
|
+
queued.push(MockResp::Out(fail(1, "anchor failed")));
|
|
2286
|
+
let (be, rec) = backend_with(MockResp::Out(ok("")), queued);
|
|
2287
|
+
assert!(be.preserve_session_before_stop(
|
|
2288
|
+
&SessionName::new("sess"), &[Target::Pane(PaneId::new("%7"))],
|
|
2289
|
+
).is_err());
|
|
2290
|
+
let calls = rec.lock().unwrap();
|
|
2291
|
+
assert!(!calls.iter().any(|argv| argv.get(1).is_some_and(|op| op == "kill-window")));
|
|
2292
|
+
if failed_step != "new-window" {
|
|
2293
|
+
assert_eq!(calls.last().unwrap(), &svec(&["tmux", "kill-pane", "-t", "%8"]));
|
|
2294
|
+
}
|
|
2295
|
+
assert!(!calls.iter().any(|argv| argv.get(1).is_some_and(|op| op == "kill-pane") && argv.contains(&"%7".to_string())));
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
#[test]
|
|
2300
|
+
#[ignore = "isolated native tmux: executed explicitly on the remote test host"]
|
|
2301
|
+
fn stop_session_preservation_native_tmux_stop_start_and_shutdown() {
|
|
2302
|
+
let tmp = std::env::temp_dir().join(format!(
|
|
2303
|
+
"ta-preserve-{}-{}", std::process::id(), chrono::Utc::now().timestamp_nanos_opt().unwrap(),
|
|
2304
|
+
));
|
|
2305
|
+
std::fs::create_dir(&tmp).unwrap();
|
|
2306
|
+
let endpoint = tmp.join("tmux.sock");
|
|
2307
|
+
let be = TmuxBackend::with_runner_for_tmux_endpoint(Box::new(RealCommandRunner), endpoint.to_str().unwrap());
|
|
2308
|
+
let session = SessionName::new("preserve-test");
|
|
2309
|
+
struct Cleanup<'a>(&'a TmuxBackend, &'a SessionName, &'a Path);
|
|
2310
|
+
impl Drop for Cleanup<'_> {
|
|
2311
|
+
fn drop(&mut self) {
|
|
2312
|
+
let _ = self.0.kill_session(self.1);
|
|
2313
|
+
let _ = std::fs::remove_dir_all(self.2);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
let _cleanup = Cleanup(&be, &session, &tmp);
|
|
2317
|
+
let argv = svec(&["sleep", "86400"]);
|
|
2318
|
+
let env = BTreeMap::new();
|
|
2319
|
+
let mut worker = be.spawn_first(&session, &WindowName::new("worker"), &argv, &tmp, &env).unwrap();
|
|
2320
|
+
for _ in 0..3 {
|
|
2321
|
+
be.preserve_session_before_stop(&session, &[Target::Pane(worker.pane_id.clone())]).unwrap();
|
|
2322
|
+
be.kill_pane(&worker.pane_id).unwrap();
|
|
2323
|
+
assert!(be.has_session(&session).unwrap());
|
|
2324
|
+
assert_eq!(be.has_pane(&worker.pane_id).unwrap(), Some(false));
|
|
2325
|
+
assert_eq!(be.list_windows(&session).unwrap(), vec![WindowName::new("[team-agent keepalive]")]);
|
|
2326
|
+
let anchor = be.list_targets().unwrap().into_iter().find(|pane| pane.session == session).unwrap();
|
|
2327
|
+
let deadline = Instant::now() + Duration::from_secs(2);
|
|
2328
|
+
loop {
|
|
2329
|
+
let dead = be.runner.run(&be.tmux_argv(&svec(&[
|
|
2330
|
+
"tmux", "display-message", "-p", "-t", anchor.pane_id.as_str(), "#{pane_dead}",
|
|
2331
|
+
]))).unwrap();
|
|
2332
|
+
if dead.success && dead.stdout.trim() == "1" { break; }
|
|
2333
|
+
assert!(Instant::now() < deadline, "keepalive must have no running process: {dead:?}");
|
|
2334
|
+
std::thread::sleep(Duration::from_millis(20));
|
|
2335
|
+
}
|
|
2336
|
+
worker = be.spawn_into(&session, &WindowName::new("worker"), &argv, &tmp, &env).unwrap();
|
|
2337
|
+
}
|
|
2338
|
+
be.kill_session(&session).unwrap();
|
|
2339
|
+
assert!(!be.has_session(&session).unwrap());
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2205
2342
|
// ── 8. ERROR MAPPING: non-zero tmux exit -> TransportError::Subprocess; runner io::Error -> Err ──
|
|
2206
2343
|
#[test]
|
|
2207
2344
|
fn error_paths_map_to_transport_error_not_panic() {
|
|
@@ -2641,84 +2778,68 @@ fn pasted_signal_unfixed_predicate_treats_folded_placeholder_as_consumed() {
|
|
|
2641
2778
|
unfixed_vacuous_consumed(folded, marker),
|
|
2642
2779
|
"causal: pre-fix !token_in_bottom_n is vacuously true on a folded composer"
|
|
2643
2780
|
);
|
|
2644
|
-
assert_eq!(
|
|
2645
|
-
super::consumption_from_capture(folded, marker, false, Some(super::PasteLatch::HashId(1))),
|
|
2646
|
-
Some(false),
|
|
2647
|
-
"post-fix NeverSeen + #1 still in composer must NOT be consumed"
|
|
2648
|
-
);
|
|
2649
|
-
}
|
|
2650
|
-
|
|
2651
|
-
#[test]
|
|
2652
|
-
fn pasted_signal_never_seen_without_placeholder_is_not_consumed() {
|
|
2653
|
-
let marker = "[team-agent-token:msg_empty]";
|
|
2654
|
-
assert_eq!(
|
|
2655
|
-
super::token_sighting(false, false),
|
|
2656
|
-
super::TokenSighting::NeverSeen
|
|
2657
|
-
);
|
|
2658
|
-
assert_eq!(
|
|
2659
|
-
super::consumption_from_capture("", marker, false, None),
|
|
2660
|
-
Some(false)
|
|
2661
|
-
);
|
|
2662
|
-
}
|
|
2663
|
-
|
|
2664
|
-
#[test]
|
|
2665
|
-
fn pasted_signal_never_seen_placeholder_id_gone_is_consumed() {
|
|
2666
|
-
let marker = "[team-agent-token:msg_fold]";
|
|
2667
|
-
let after = "assistant reply\n❯ \n";
|
|
2668
|
-
assert_eq!(
|
|
2669
|
-
super::consumption_from_capture(after, marker, false, Some(super::PasteLatch::HashId(7))),
|
|
2670
|
-
Some(true),
|
|
2671
|
-
"NeverSeen but latched #7 left composer ⇒ consumed (positive signal)"
|
|
2672
|
-
);
|
|
2673
2781
|
}
|
|
2674
2782
|
|
|
2675
2783
|
#[test]
|
|
2676
|
-
fn
|
|
2677
|
-
|
|
2678
|
-
let
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
"
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
fn pasted_signal_never_seen_no_id_cannot_claim_consumed() {
|
|
2688
|
-
let marker = "[team-agent-token:msg_fold]";
|
|
2689
|
-
let gone = "❯ \n";
|
|
2690
|
-
assert_eq!(
|
|
2691
|
-
super::consumption_from_capture(gone, marker, false, None),
|
|
2692
|
-
Some(false),
|
|
2693
|
-
"no #N latched: disappearance is unverified, not consumed"
|
|
2694
|
-
);
|
|
2695
|
-
}
|
|
2696
|
-
|
|
2697
|
-
#[test]
|
|
2698
|
-
fn pasted_signal_seen_token_then_gone_is_consumed() {
|
|
2699
|
-
let marker = "[team-agent-token:msg_short]";
|
|
2700
|
-
assert_eq!(
|
|
2701
|
-
super::token_sighting(false, true),
|
|
2702
|
-
super::TokenSighting::Gone
|
|
2703
|
-
);
|
|
2704
|
-
assert_eq!(
|
|
2705
|
-
super::consumption_from_capture("❯ \n", marker, true, None),
|
|
2706
|
-
Some(true)
|
|
2707
|
-
);
|
|
2784
|
+
fn pasted_signal_token_sighting_table() {
|
|
2785
|
+
use super::TokenSighting::{Gone, NeverSeen, Visible};
|
|
2786
|
+
let cases = [
|
|
2787
|
+
("never_seen", false, false, NeverSeen),
|
|
2788
|
+
("gone", false, true, Gone),
|
|
2789
|
+
("visible", true, true, Visible),
|
|
2790
|
+
("gone_grok", false, true, Gone),
|
|
2791
|
+
];
|
|
2792
|
+
for (id, visible, seen, expected) in cases {
|
|
2793
|
+
assert_eq!(super::token_sighting(visible, seen), expected, "{id}");
|
|
2794
|
+
}
|
|
2708
2795
|
}
|
|
2709
2796
|
|
|
2710
2797
|
#[test]
|
|
2711
|
-
fn
|
|
2798
|
+
fn pasted_signal_consumption_table() {
|
|
2799
|
+
use super::PasteLatch::{GrokLineCount, HashId};
|
|
2800
|
+
// Input: capture, marker, previously seen, tracked paste. Expected is Some(bool),
|
|
2801
|
+
// not a truthiness check: None must still fail these consumption contracts.
|
|
2802
|
+
type Input<'a> = (&'a str, &'a str, bool, Option<super::PasteLatch>);
|
|
2712
2803
|
let marker = "[team-agent-token:msg_short]";
|
|
2713
2804
|
let still = format!("❯ ping {marker}\n");
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2805
|
+
let cases: &[(&str, Input<'_>, bool, &str)] = &[
|
|
2806
|
+
("folded_hash_stays",
|
|
2807
|
+
("❯ [Pasted text #1 +8 lines]\n", "[team-agent-token:msg_fold]", false, Some(HashId(1))), false,
|
|
2808
|
+
"post-fix NeverSeen + #1 still in composer must NOT be consumed"),
|
|
2809
|
+
("never_seen_empty",
|
|
2810
|
+
("", "[team-agent-token:msg_empty]", false, None), false, ""),
|
|
2811
|
+
("hash_id_gone",
|
|
2812
|
+
("assistant reply\n❯ \n", "[team-agent-token:msg_fold]", false, Some(HashId(7))), true,
|
|
2813
|
+
"NeverSeen but latched #7 left composer ⇒ consumed (positive signal)"),
|
|
2814
|
+
("hash_id_replaced",
|
|
2815
|
+
("❯ [Pasted text #2 +3 lines]\n", "[team-agent-token:msg_fold]", false, Some(HashId(1))), true,
|
|
2816
|
+
"our #1 gone, a later #2 in composer still means ours submitted"),
|
|
2817
|
+
("no_hash_latch",
|
|
2818
|
+
("❯ \n", "[team-agent-token:msg_fold]", false, None), false,
|
|
2819
|
+
"no #N latched: disappearance is unverified, not consumed"),
|
|
2820
|
+
("seen_token_gone",
|
|
2821
|
+
("❯ \n", "[team-agent-token:msg_short]", true, None), true, ""),
|
|
2822
|
+
("seen_token_visible",
|
|
2823
|
+
(&still, "[team-agent-token:msg_short]", true, None), false, ""),
|
|
2824
|
+
("grok_lines_stay",
|
|
2825
|
+
(GROK_INCIDENT_LINE, "[team-agent-token:msg_grok]", false, Some(GrokLineCount(42))), false, ""),
|
|
2826
|
+
("grok_lines_gone",
|
|
2827
|
+
("Waiting for response\n❯ \n", "[team-agent-token:msg_grok]", false, Some(GrokLineCount(42))), true, ""),
|
|
2828
|
+
("grok_lines_mismatch",
|
|
2829
|
+
("│ ❯ [Pasted: 10 lines] │", "[team-agent-token:msg_grok]", false, Some(GrokLineCount(42))), false,
|
|
2830
|
+
"different N must not claim the latched paste consumed"),
|
|
2831
|
+
// 悬案B:Gone 仍有 grok 占位符;修前 Gone 无条件 Some(true)。
|
|
2832
|
+
("gone_grok_lines_stay",
|
|
2833
|
+
("some transcript line\n> [Pasted: 27 lines]\nEnter:send Esc:cancel\n", "[team-agent-token:deadbeef]", true, Some(GrokLineCount(27))), false,
|
|
2834
|
+
"Gone must re-check PasteLatch: placeholder still in composer must not be consumed, got {got:?}"),
|
|
2835
|
+
("gone_grok_kb_stay",
|
|
2836
|
+
("│ ❯ [Pasted: 13 KB] │\n Enter:send\n", "[team-agent-token:kbform]", true, Some(GrokLineCount(13))), false,
|
|
2837
|
+
"Gone + [Pasted: 13 KB] still in composer must not be consumed, got {got:?}"),
|
|
2838
|
+
];
|
|
2839
|
+
for &(id, (text, marker, seen, tracked), expected, note) in cases {
|
|
2840
|
+
let got = super::consumption_from_capture(text, marker, seen, tracked);
|
|
2841
|
+
assert_eq!(got, Some(expected), "{id}: {note}; got={got:?}");
|
|
2842
|
+
}
|
|
2722
2843
|
}
|
|
2723
2844
|
|
|
2724
2845
|
#[test]
|
|
@@ -2737,87 +2858,115 @@ fn pasted_signal_marker_some_observation_records_composer_placeholder() {
|
|
|
2737
2858
|
}
|
|
2738
2859
|
|
|
2739
2860
|
#[test]
|
|
2740
|
-
fn
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
.
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
.expect("inject");
|
|
2752
|
-
assert_eq!(
|
|
2753
|
-
report.submit_verification,
|
|
2754
|
-
SubmitVerification::SubmitConsumptionUnverified,
|
|
2755
|
-
"folded + Enter not effective (#4 stays) must be unverified; got {:?}",
|
|
2756
|
-
report.submit_verification
|
|
2757
|
-
);
|
|
2758
|
-
}
|
|
2759
|
-
|
|
2760
|
-
#[test]
|
|
2761
|
-
fn pasted_signal_inject_folded_enter_effective_is_consumed() {
|
|
2762
|
-
let token_text = "Team Agent message from leader:\nline1\nline2\nline3\nline4\nline5\n\n[team-agent-token:msg_fold_go]";
|
|
2861
|
+
fn pasted_signal_submit_verification_table() {
|
|
2862
|
+
use SubmitVerification::{EnterSentWithoutPlaceholderCheck, SubmitConsumptionUnverified};
|
|
2863
|
+
struct Case {
|
|
2864
|
+
id: &'static str,
|
|
2865
|
+
// Payload, pre-submit capture, post-submit capture, clear-after-N-enters.
|
|
2866
|
+
// None uses FoldedPasteRunner; Some(N) uses NthEnterClearRunner.
|
|
2867
|
+
input: (String, String, String, Option<u32>),
|
|
2868
|
+
// Verification, Enter count, paste-buffer count. None preserves no assertion.
|
|
2869
|
+
expected: (SubmitVerification, Option<usize>, Option<usize>),
|
|
2870
|
+
notes: [&'static str; 3],
|
|
2871
|
+
}
|
|
2763
2872
|
let placeholder = "❯ [Pasted text #4 +6 lines]\n";
|
|
2764
|
-
|
|
2765
|
-
let
|
|
2766
|
-
let
|
|
2767
|
-
let
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
#
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2873
|
+
let short_ok = "ping [team-agent-token:msg_short_ok]";
|
|
2874
|
+
let short_stay = "ping [team-agent-token:msg_short_stay]";
|
|
2875
|
+
let marker = "[team-agent-token:msg_wrap_ok]";
|
|
2876
|
+
let wrapped_payload = format!("Team Agent message from leader:\nline1\n\n{marker}");
|
|
2877
|
+
let wrapped = wrapped_identity_with_skin(marker, "> ");
|
|
2878
|
+
let marker = "[team-agent-token:msg_cap_lock]";
|
|
2879
|
+
let cap_lock_payload = format!("Team Agent message from leader:\nline1\n\n{marker}");
|
|
2880
|
+
let cap_lock = wrapped_identity_with_skin(marker, "ready.");
|
|
2881
|
+
let marker = "[team-agent-token:msg_busy_nodup]";
|
|
2882
|
+
let busy_payload = format!("Team Agent message from leader:\nline1 composer> ❯\n\n{marker}");
|
|
2883
|
+
let busy = "STARTED\nsleep holds pty\ncomposer>\n❯\n>\n";
|
|
2884
|
+
let marker = "[team-agent-token:msg_payload_glyph]";
|
|
2885
|
+
let glyphs_payload = format!("Team Agent message from leader:\ncomposer> ❯\n\n{marker}");
|
|
2886
|
+
let glyphs = "output\ncomposer> leftover from payload\n❯ quoted\n";
|
|
2887
|
+
let mut cases = vec![
|
|
2888
|
+
Case { id: "folded_stays",
|
|
2889
|
+
input: ("Team Agent message from leader:\nline1\nline2\nline3\nline4\nline5\n\n[team-agent-token:msg_fold_stay]".into(), placeholder.into(), placeholder.into(), None),
|
|
2890
|
+
expected: (SubmitConsumptionUnverified, None, None),
|
|
2891
|
+
notes: ["folded + Enter not effective (#4 stays) must be unverified; got {:?}", "", ""] },
|
|
2892
|
+
// 只有 #4 离开 composer;不加 Working,否则 busy 升级会让半三不经正信号就绿。
|
|
2893
|
+
Case { id: "folded_gone",
|
|
2894
|
+
input: ("Team Agent message from leader:\nline1\nline2\nline3\nline4\nline5\n\n[team-agent-token:msg_fold_go]".into(), placeholder.into(), "assistant reply\n❯ \n".into(), None),
|
|
2895
|
+
expected: (EnterSentWithoutPlaceholderCheck, None, None),
|
|
2896
|
+
notes: ["folded + Enter effective (#4 left composer) must be consumed; got {:?}", "", ""] },
|
|
2897
|
+
Case { id: "short_gone",
|
|
2898
|
+
input: (short_ok.into(), short_ok.into(), "❯ \n".into(), None),
|
|
2899
|
+
expected: (EnterSentWithoutPlaceholderCheck, None, None),
|
|
2900
|
+
notes: ["short visible token then gone must stay consumed; got {:?}", "", ""] },
|
|
2901
|
+
Case { id: "short_visible",
|
|
2902
|
+
input: (short_stay.into(), short_stay.into(), short_stay.into(), None),
|
|
2903
|
+
expected: (SubmitConsumptionUnverified, None, None),
|
|
2904
|
+
notes: ["short visible token still in composer must stay unverified; got {:?}", "", ""] },
|
|
2905
|
+
Case { id: "grok_stays",
|
|
2906
|
+
input: ("Team Agent message from leader:\nline1\n\n[team-agent-token:msg_grok_stay]".into(), GROK_INCIDENT_LINE.into(), GROK_INCIDENT_LINE.into(), None),
|
|
2907
|
+
expected: (SubmitConsumptionUnverified, Some(3), None),
|
|
2908
|
+
notes: ["", "same grok N still in composer must retry Enter up to cap=3; Unverified B must not add a 4th; invert wrap-gap guard must turn this red; calls={calls:?}", ""] },
|
|
2909
|
+
Case { id: "grok_gone",
|
|
2910
|
+
input: ("Team Agent message from leader:\nline1\n\n[team-agent-token:msg_grok_go]".into(), GROK_INCIDENT_LINE.into(), "Waiting for response\n❯ \n".into(), None),
|
|
2911
|
+
expected: (EnterSentWithoutPlaceholderCheck, Some(1), None),
|
|
2912
|
+
notes: ["", "fold left composer after one Enter; no empty-composer hammer; calls={calls:?}", ""] },
|
|
2913
|
+
Case { id: "wrapped_resend_consumes",
|
|
2914
|
+
input: (wrapped_payload, wrapped, "> \n".into(), Some(2)),
|
|
2915
|
+
expected: (EnterSentWithoutPlaceholderCheck, Some(2), Some(1)),
|
|
2916
|
+
notes: ["Unverified + wrapped identity must resend one C-m then consume; got {:?}", "exactly one extra C-m after Unverified; calls={calls:?}", "must not re-paste; paste-buffer count={pastes}"] },
|
|
2917
|
+
Case { id: "identity_absent",
|
|
2918
|
+
input: (busy_payload, busy.into(), busy.into(), Some(99)),
|
|
2919
|
+
expected: (SubmitConsumptionUnverified, Some(1), None),
|
|
2920
|
+
notes: ["identity absent must stay Unverified, no extra Enter; got {:?}", "no this-paste identity ⇒ no Unverified resend; commenting the identity gate must turn this red (2+ C-m); calls={calls:?}", ""] },
|
|
2921
|
+
Case { id: "identity_resend_cap",
|
|
2922
|
+
input: (cap_lock_payload, cap_lock.clone(), cap_lock, Some(u32::MAX)),
|
|
2923
|
+
expected: (SubmitConsumptionUnverified, Some(2), Some(1)),
|
|
2924
|
+
notes: ["never-clear identity must not fake-consume; got {:?}", "cap must be exactly one extra C-m (1 inject + 1 resend); raising UNVERIFIED_COMPOSER_RESEND_MAX must turn this red; calls={calls:?}", "must not re-paste; paste-buffer count={pastes}"] },
|
|
2925
|
+
Case { id: "payload_glyphs",
|
|
2926
|
+
input: (glyphs_payload, glyphs.into(), glyphs.into(), Some(99)),
|
|
2927
|
+
expected: (SubmitConsumptionUnverified, Some(1), None),
|
|
2928
|
+
notes: ["", "glyphs in payload must not cause extra C-m when token identity is gone; calls={calls:?}", ""] },
|
|
2929
|
+
];
|
|
2930
|
+
for (id, skin) in [
|
|
2931
|
+
("skin_arrow", "❯ "),
|
|
2932
|
+
("skin_greater_than", "> "),
|
|
2933
|
+
("skin_ready", "ready."),
|
|
2934
|
+
] {
|
|
2935
|
+
let marker = format!("[team-agent-token:msg_skin_{}]", skin.chars().next().unwrap() as u32);
|
|
2936
|
+
cases.push(Case {
|
|
2937
|
+
id,
|
|
2938
|
+
input: (format!("Team Agent message from leader:\nline1\n\n{marker}"),
|
|
2939
|
+
wrapped_identity_with_skin(&marker, skin), format!("{skin}\n"), Some(2)),
|
|
2940
|
+
expected: (EnterSentWithoutPlaceholderCheck, Some(2), None),
|
|
2941
|
+
notes: ["skin {skin:?} must consume after one extra C-m", "skin {skin:?} extra C-m; got {n}", ""],
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
for case in cases {
|
|
2945
|
+
let (payload, pre, post, clear_after) = case.input;
|
|
2946
|
+
let (be, rec) = match clear_after {
|
|
2947
|
+
Some(n) => backend_clears_after_n_enters(&pre, &post, n),
|
|
2948
|
+
None => backend_folded(&pre, &post),
|
|
2949
|
+
};
|
|
2950
|
+
let report = be.inject(
|
|
2951
|
+
&Target::Pane(PaneId::new("%7")), &InjectPayload::Text(payload), Key::Enter, true,
|
|
2952
|
+
).expect("inject");
|
|
2953
|
+
let (verification, enters, pastes) = case.expected;
|
|
2954
|
+
assert_eq!(report.submit_verification, verification,
|
|
2955
|
+
"{}: {}; pre={pre:?}", case.id, case.notes[0]);
|
|
2956
|
+
if enters.is_some() || pastes.is_some() {
|
|
2957
|
+
let calls = rec.lock().unwrap().clone();
|
|
2958
|
+
if let Some(enters) = enters {
|
|
2959
|
+
assert_eq!(count_submit_enters(&calls), enters,
|
|
2960
|
+
"{}: {}; calls={calls:?}", case.id, case.notes[1]);
|
|
2961
|
+
}
|
|
2962
|
+
if let Some(pastes) = pastes {
|
|
2963
|
+
let actual = calls.iter()
|
|
2964
|
+
.filter(|argv| argv.get(1).map(String::as_str) == Some("paste-buffer"))
|
|
2965
|
+
.count();
|
|
2966
|
+
assert_eq!(actual, pastes, "{}: {}; calls={calls:?}", case.id, case.notes[2]);
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2821
2970
|
}
|
|
2822
2971
|
|
|
2823
2972
|
fn count_submit_enters(calls: &[Vec<String>]) -> usize {
|
|
@@ -3278,87 +3427,16 @@ fn grok_fold_three_tui_literals_lock() {
|
|
|
3278
3427
|
assert_eq!(codex.id, Some(2));
|
|
3279
3428
|
}
|
|
3280
3429
|
|
|
3281
|
-
#[test]
|
|
3282
|
-
fn grok_fold_same_line_count_still_unconsumed() {
|
|
3283
|
-
let marker = "[team-agent-token:msg_grok]";
|
|
3284
|
-
assert_eq!(
|
|
3285
|
-
super::consumption_from_capture(
|
|
3286
|
-
GROK_INCIDENT_LINE,
|
|
3287
|
-
marker,
|
|
3288
|
-
false,
|
|
3289
|
-
Some(super::PasteLatch::GrokLineCount(42))
|
|
3290
|
-
),
|
|
3291
|
-
Some(false)
|
|
3292
|
-
);
|
|
3293
|
-
}
|
|
3294
|
-
|
|
3295
|
-
#[test]
|
|
3296
|
-
fn grok_fold_line_count_gone_is_consumed() {
|
|
3297
|
-
let marker = "[team-agent-token:msg_grok]";
|
|
3298
|
-
assert_eq!(
|
|
3299
|
-
super::consumption_from_capture(
|
|
3300
|
-
"Waiting for response\n❯ \n",
|
|
3301
|
-
marker,
|
|
3302
|
-
false,
|
|
3303
|
-
Some(super::PasteLatch::GrokLineCount(42))
|
|
3304
|
-
),
|
|
3305
|
-
Some(true)
|
|
3306
|
-
);
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
3430
|
#[test]
|
|
3310
3431
|
fn grok_fold_different_n_is_not_same_paste() {
|
|
3311
3432
|
let marker = "[team-agent-token:msg_grok]";
|
|
3312
3433
|
let other = "│ ❯ [Pasted: 10 lines] │";
|
|
3313
|
-
assert_eq!(
|
|
3314
|
-
super::consumption_from_capture(
|
|
3315
|
-
other,
|
|
3316
|
-
marker,
|
|
3317
|
-
false,
|
|
3318
|
-
Some(super::PasteLatch::GrokLineCount(42))
|
|
3319
|
-
),
|
|
3320
|
-
Some(false),
|
|
3321
|
-
"different N must not claim the latched paste consumed"
|
|
3322
|
-
);
|
|
3323
3434
|
assert!(
|
|
3324
3435
|
!super::should_resubmit_enter(other, marker, Some(super::PasteLatch::GrokLineCount(42))),
|
|
3325
3436
|
"different N is not the same paste; do not hammer Enter"
|
|
3326
3437
|
);
|
|
3327
3438
|
}
|
|
3328
3439
|
|
|
3329
|
-
#[test]
|
|
3330
|
-
fn grok_fold_no_latch_cannot_claim_consumed() {
|
|
3331
|
-
let marker = "[team-agent-token:msg_grok]";
|
|
3332
|
-
assert_eq!(
|
|
3333
|
-
super::consumption_from_capture("❯ \n", marker, false, None),
|
|
3334
|
-
Some(false)
|
|
3335
|
-
);
|
|
3336
|
-
}
|
|
3337
|
-
|
|
3338
|
-
#[test]
|
|
3339
|
-
fn grok_fold_inject_stays_retries_while_identity_present() {
|
|
3340
|
-
let token_text = "Team Agent message from leader:\nline1\n\n[team-agent-token:msg_grok_stay]";
|
|
3341
|
-
let (be, rec) = backend_folded(GROK_INCIDENT_LINE, GROK_INCIDENT_LINE);
|
|
3342
|
-
let report = be
|
|
3343
|
-
.inject(
|
|
3344
|
-
&Target::Pane(PaneId::new("%7")),
|
|
3345
|
-
&InjectPayload::Text(token_text.to_string()),
|
|
3346
|
-
Key::Enter,
|
|
3347
|
-
true,
|
|
3348
|
-
)
|
|
3349
|
-
.expect("inject");
|
|
3350
|
-
assert_eq!(
|
|
3351
|
-
report.submit_verification,
|
|
3352
|
-
SubmitVerification::SubmitConsumptionUnverified
|
|
3353
|
-
);
|
|
3354
|
-
let calls = rec.lock().unwrap().clone();
|
|
3355
|
-
assert_eq!(
|
|
3356
|
-
count_submit_enters(&calls),
|
|
3357
|
-
3,
|
|
3358
|
-
"same grok N still in composer must retry Enter up to cap=3; Unverified B must not add a 4th; invert wrap-gap guard must turn this red; calls={calls:?}"
|
|
3359
|
-
);
|
|
3360
|
-
}
|
|
3361
|
-
|
|
3362
3440
|
/// 2026-08-21 真机屏尾:折叠占位符还在 transcript,Working 已开。
|
|
3363
3441
|
/// 非 cursor 路径会把 pasted #N 当成未消费而连按 Enter。
|
|
3364
3442
|
const CURSOR_BUSY_AFTER_SUBMIT: &str = "\
|
|
@@ -3469,30 +3547,6 @@ fn cursor_should_resubmit_false_when_busy_even_if_paste_placeholder_visible() {
|
|
|
3469
3547
|
);
|
|
3470
3548
|
}
|
|
3471
3549
|
|
|
3472
|
-
#[test]
|
|
3473
|
-
fn grok_fold_inject_leaves_consumed_one_enter() {
|
|
3474
|
-
let token_text = "Team Agent message from leader:\nline1\n\n[team-agent-token:msg_grok_go]";
|
|
3475
|
-
let (be, rec) = backend_folded(GROK_INCIDENT_LINE, "Waiting for response\n❯ \n");
|
|
3476
|
-
let report = be
|
|
3477
|
-
.inject(
|
|
3478
|
-
&Target::Pane(PaneId::new("%7")),
|
|
3479
|
-
&InjectPayload::Text(token_text.to_string()),
|
|
3480
|
-
Key::Enter,
|
|
3481
|
-
true,
|
|
3482
|
-
)
|
|
3483
|
-
.expect("inject");
|
|
3484
|
-
assert_eq!(
|
|
3485
|
-
report.submit_verification,
|
|
3486
|
-
SubmitVerification::EnterSentWithoutPlaceholderCheck
|
|
3487
|
-
);
|
|
3488
|
-
let calls = rec.lock().unwrap().clone();
|
|
3489
|
-
assert_eq!(
|
|
3490
|
-
count_submit_enters(&calls),
|
|
3491
|
-
1,
|
|
3492
|
-
"fold left composer after one Enter; no empty-composer hammer; calls={calls:?}"
|
|
3493
|
-
);
|
|
3494
|
-
}
|
|
3495
|
-
|
|
3496
3550
|
#[test]
|
|
3497
3551
|
fn grok_fold_unverified_without_identity_one_enter() {
|
|
3498
3552
|
let token_text = "Team Agent message from leader:\nhi\n\n[team-agent-token:msg_grok_empty]";
|
|
@@ -3563,57 +3617,12 @@ fn grok_fold_does_not_enter_before_fold_on_raw_tall_paste() {
|
|
|
3563
3617
|
assert_eq!(count_submit_enters(&calls), 1);
|
|
3564
3618
|
}
|
|
3565
3619
|
|
|
3566
|
-
/// 悬案B 主嫌:token 见过又消失(Gone)时 composer 仍有 grok 折叠占位符。
|
|
3567
|
-
/// 修前 Gone 无条件 Some(true)。本测试必须先红。
|
|
3568
3620
|
#[test]
|
|
3569
|
-
fn
|
|
3570
|
-
let cap = "some transcript line\n> [Pasted: 27 lines]\nEnter:send Esc:cancel\n";
|
|
3571
|
-
let marker = "[team-agent-token:deadbeef]";
|
|
3572
|
-
assert_eq!(
|
|
3573
|
-
super::token_sighting(false, true),
|
|
3574
|
-
super::TokenSighting::Gone
|
|
3575
|
-
);
|
|
3576
|
-
let got = super::consumption_from_capture(
|
|
3577
|
-
cap,
|
|
3578
|
-
marker,
|
|
3579
|
-
true,
|
|
3580
|
-
Some(super::PasteLatch::GrokLineCount(27)),
|
|
3581
|
-
);
|
|
3582
|
-
assert_eq!(
|
|
3583
|
-
got,
|
|
3584
|
-
Some(false),
|
|
3585
|
-
"Gone must re-check PasteLatch: placeholder still in composer must not be consumed, got {got:?}"
|
|
3586
|
-
);
|
|
3587
|
-
}
|
|
3588
|
-
|
|
3589
|
-
#[test]
|
|
3590
|
-
fn gone_with_grok_kb_placeholder_still_in_composer_must_not_be_consumed() {
|
|
3621
|
+
fn grok_kb_placeholder_parser_preserves_literal_and_line_count() {
|
|
3591
3622
|
let cap = "│ ❯ [Pasted: 13 KB] │\n Enter:send\n";
|
|
3592
|
-
let marker = "[team-agent-token:kbform]";
|
|
3593
3623
|
let prompt = super::pasted_prompt_in_composer(cap, 15).expect("KB form is grok fold");
|
|
3594
3624
|
assert_eq!(prompt.literal, "pasted:");
|
|
3595
3625
|
assert_eq!(prompt.line_count, Some(13));
|
|
3596
|
-
let got = super::consumption_from_capture(
|
|
3597
|
-
cap,
|
|
3598
|
-
marker,
|
|
3599
|
-
true,
|
|
3600
|
-
Some(super::PasteLatch::GrokLineCount(13)),
|
|
3601
|
-
);
|
|
3602
|
-
assert_eq!(
|
|
3603
|
-
got,
|
|
3604
|
-
Some(false),
|
|
3605
|
-
"Gone + [Pasted: 13 KB] still in composer must not be consumed, got {got:?}"
|
|
3606
|
-
);
|
|
3607
|
-
}
|
|
3608
|
-
|
|
3609
|
-
#[test]
|
|
3610
|
-
fn gone_empty_composer_without_latch_stays_consumed_for_claude() {
|
|
3611
|
-
let marker = "[team-agent-token:msg_short]";
|
|
3612
|
-
assert_eq!(
|
|
3613
|
-
super::consumption_from_capture("❯ \n", marker, true, None),
|
|
3614
|
-
Some(true),
|
|
3615
|
-
"claude short token gone + empty composer + no latch must stay consumed"
|
|
3616
|
-
);
|
|
3617
3626
|
}
|
|
3618
3627
|
|
|
3619
3628
|
/// 样本#5 归因钉:重试只重按 Enter,不得再 load/set/paste-buffer。
|
|
@@ -3718,66 +3727,21 @@ fn provider_busy_signal_match_preserves_category_priority_and_tail_boundary() {
|
|
|
3718
3727
|
const G4_TOKEN: &str = "[team-agent-token:msg_g4]";
|
|
3719
3728
|
|
|
3720
3729
|
#[test]
|
|
3721
|
-
fn
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
}
|
|
3737
|
-
|
|
3738
|
-
#[test]
|
|
3739
|
-
fn g4_turn_cursor_unsubmitted_token_is_missing() {
|
|
3740
|
-
let text = format!("❯ {G4_TOKEN}");
|
|
3741
|
-
assert_eq!(
|
|
3742
|
-
observe_turn_from_capture(&text, Some(G4_TOKEN)),
|
|
3743
|
-
TurnVerification::LeaderNewTurnBoundaryMissing
|
|
3744
|
-
);
|
|
3745
|
-
}
|
|
3746
|
-
|
|
3747
|
-
#[test]
|
|
3748
|
-
fn g4_turn_claude_spinner_is_verified() {
|
|
3749
|
-
let text = "✶ Thinking…\nesc to interrupt";
|
|
3750
|
-
assert_eq!(
|
|
3751
|
-
observe_turn_from_capture(text, Some(G4_TOKEN)),
|
|
3752
|
-
TurnVerification::LeaderNewTurnBoundaryVerified
|
|
3753
|
-
);
|
|
3754
|
-
}
|
|
3755
|
-
|
|
3756
|
-
#[test]
|
|
3757
|
-
fn g4_turn_cursor_processing_is_verified() {
|
|
3758
|
-
let text = "processing\n❯ ";
|
|
3759
|
-
assert_eq!(
|
|
3760
|
-
observe_turn_from_capture(text, Some(G4_TOKEN)),
|
|
3761
|
-
TurnVerification::LeaderNewTurnBoundaryVerified
|
|
3762
|
-
);
|
|
3763
|
-
}
|
|
3764
|
-
|
|
3765
|
-
#[test]
|
|
3766
|
-
fn g4_turn_grok_working_is_verified() {
|
|
3767
|
-
let text = "working\n❯ ";
|
|
3768
|
-
assert_eq!(
|
|
3769
|
-
observe_turn_from_capture(text, Some(G4_TOKEN)),
|
|
3770
|
-
TurnVerification::LeaderNewTurnBoundaryVerified
|
|
3771
|
-
);
|
|
3772
|
-
}
|
|
3773
|
-
|
|
3774
|
-
#[test]
|
|
3775
|
-
fn g4_turn_empty_composer_without_busy_is_unknown() {
|
|
3776
|
-
assert_eq!(
|
|
3777
|
-
observe_turn_from_capture("❯ ", Some(G4_TOKEN)),
|
|
3778
|
-
TurnVerification::NotYetObserved,
|
|
3779
|
-
"empty composer and no busy is 不知道, never 开跑"
|
|
3780
|
-
);
|
|
3730
|
+
fn g4_turn_verification_table() {
|
|
3731
|
+
use TurnVerification::{LeaderNewTurnBoundaryMissing, LeaderNewTurnBoundaryVerified, NotYetObserved};
|
|
3732
|
+
let cursor = format!("❯ {G4_TOKEN}");
|
|
3733
|
+
let cases = [
|
|
3734
|
+
("grok_pending", "│ ❯ [Pasted: 42 lines] │", LeaderNewTurnBoundaryMissing, ""),
|
|
3735
|
+
("claude_pending", "❯ pasted text #4 +12 lines", LeaderNewTurnBoundaryMissing, ""),
|
|
3736
|
+
("cursor_pending", cursor.as_str(), LeaderNewTurnBoundaryMissing, ""),
|
|
3737
|
+
("claude_busy", "✶ Thinking…\nesc to interrupt", LeaderNewTurnBoundaryVerified, ""),
|
|
3738
|
+
("cursor_busy", "processing\n❯ ", LeaderNewTurnBoundaryVerified, ""),
|
|
3739
|
+
("grok_busy", "working\n❯ ", LeaderNewTurnBoundaryVerified, ""),
|
|
3740
|
+
("empty_unknown", "❯ ", NotYetObserved, "empty composer and no busy is 不知道, never 开跑"),
|
|
3741
|
+
];
|
|
3742
|
+
for (id, text, expected, note) in cases {
|
|
3743
|
+
assert_eq!(observe_turn_from_capture(text, Some(G4_TOKEN)), expected, "{id}: {note}");
|
|
3744
|
+
}
|
|
3781
3745
|
}
|
|
3782
3746
|
|
|
3783
3747
|
#[test]
|
|
@@ -4100,153 +4064,3 @@ fn wrap_gap_true_on_joined_token_a_cannot_see() {
|
|
|
4100
4064
|
"B must still cover the wrap gap A cannot see"
|
|
4101
4065
|
);
|
|
4102
4066
|
}
|
|
4103
|
-
|
|
4104
|
-
#[test]
|
|
4105
|
-
fn unverified_wrapped_token_resends_one_enter_then_consumes() {
|
|
4106
|
-
let marker = "[team-agent-token:msg_wrap_ok]";
|
|
4107
|
-
let token_text = format!("Team Agent message from leader:\nline1\n\n{marker}");
|
|
4108
|
-
let pending = wrapped_identity_with_skin(marker, "> ");
|
|
4109
|
-
let cleared = "> \n";
|
|
4110
|
-
let (be, rec) = backend_clears_after_n_enters(&pending, cleared, 2);
|
|
4111
|
-
let report = be
|
|
4112
|
-
.inject(
|
|
4113
|
-
&Target::Pane(PaneId::new("%7")),
|
|
4114
|
-
&InjectPayload::Text(token_text),
|
|
4115
|
-
Key::Enter,
|
|
4116
|
-
true,
|
|
4117
|
-
)
|
|
4118
|
-
.expect("inject");
|
|
4119
|
-
assert_eq!(
|
|
4120
|
-
report.submit_verification,
|
|
4121
|
-
SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
4122
|
-
"Unverified + wrapped identity must resend one C-m then consume; got {:?}",
|
|
4123
|
-
report.submit_verification
|
|
4124
|
-
);
|
|
4125
|
-
let calls = rec.lock().unwrap().clone();
|
|
4126
|
-
assert_eq!(
|
|
4127
|
-
count_submit_enters(&calls),
|
|
4128
|
-
2,
|
|
4129
|
-
"exactly one extra C-m after Unverified; calls={calls:?}"
|
|
4130
|
-
);
|
|
4131
|
-
let pastes = calls
|
|
4132
|
-
.iter()
|
|
4133
|
-
.filter(|argv| argv.get(1).map(String::as_str) == Some("paste-buffer"))
|
|
4134
|
-
.count();
|
|
4135
|
-
assert_eq!(pastes, 1, "must not re-paste; paste-buffer count={pastes}");
|
|
4136
|
-
}
|
|
4137
|
-
|
|
4138
|
-
#[test]
|
|
4139
|
-
fn unverified_resend_skips_when_this_message_identity_absent() {
|
|
4140
|
-
let marker = "[team-agent-token:msg_busy_nodup]";
|
|
4141
|
-
let token_text = format!("Team Agent message from leader:\nline1 composer> ❯\n\n{marker}");
|
|
4142
|
-
let pending = "STARTED\nsleep holds pty\ncomposer>\n❯\n>\n";
|
|
4143
|
-
let (be, rec) = backend_clears_after_n_enters(pending, pending, 99);
|
|
4144
|
-
let report = be
|
|
4145
|
-
.inject(
|
|
4146
|
-
&Target::Pane(PaneId::new("%7")),
|
|
4147
|
-
&InjectPayload::Text(token_text),
|
|
4148
|
-
Key::Enter,
|
|
4149
|
-
true,
|
|
4150
|
-
)
|
|
4151
|
-
.expect("inject");
|
|
4152
|
-
assert_eq!(
|
|
4153
|
-
report.submit_verification,
|
|
4154
|
-
SubmitVerification::SubmitConsumptionUnverified,
|
|
4155
|
-
"identity absent must stay Unverified, no extra Enter; got {:?}",
|
|
4156
|
-
report.submit_verification
|
|
4157
|
-
);
|
|
4158
|
-
let calls = rec.lock().unwrap().clone();
|
|
4159
|
-
assert_eq!(
|
|
4160
|
-
count_submit_enters(&calls),
|
|
4161
|
-
1,
|
|
4162
|
-
"no this-paste identity ⇒ no Unverified resend; commenting the identity gate must turn this red (2+ C-m); calls={calls:?}"
|
|
4163
|
-
);
|
|
4164
|
-
}
|
|
4165
|
-
|
|
4166
|
-
#[test]
|
|
4167
|
-
fn unverified_identity_never_clears_hits_resend_cap_of_one() {
|
|
4168
|
-
let marker = "[team-agent-token:msg_cap_lock]";
|
|
4169
|
-
let token_text = format!("Team Agent message from leader:\nline1\n\n{marker}");
|
|
4170
|
-
let pending = wrapped_identity_with_skin(marker, "ready.");
|
|
4171
|
-
let (be, rec) = backend_clears_after_n_enters(&pending, &pending, u32::MAX);
|
|
4172
|
-
let report = be
|
|
4173
|
-
.inject(
|
|
4174
|
-
&Target::Pane(PaneId::new("%7")),
|
|
4175
|
-
&InjectPayload::Text(token_text),
|
|
4176
|
-
Key::Enter,
|
|
4177
|
-
true,
|
|
4178
|
-
)
|
|
4179
|
-
.expect("inject");
|
|
4180
|
-
assert_eq!(
|
|
4181
|
-
report.submit_verification,
|
|
4182
|
-
SubmitVerification::SubmitConsumptionUnverified,
|
|
4183
|
-
"never-clear identity must not fake-consume; got {:?}",
|
|
4184
|
-
report.submit_verification
|
|
4185
|
-
);
|
|
4186
|
-
let calls = rec.lock().unwrap().clone();
|
|
4187
|
-
assert_eq!(
|
|
4188
|
-
count_submit_enters(&calls),
|
|
4189
|
-
2,
|
|
4190
|
-
"cap must be exactly one extra C-m (1 inject + 1 resend); raising UNVERIFIED_COMPOSER_RESEND_MAX must turn this red; calls={calls:?}"
|
|
4191
|
-
);
|
|
4192
|
-
let pastes = calls
|
|
4193
|
-
.iter()
|
|
4194
|
-
.filter(|argv| argv.get(1).map(String::as_str) == Some("paste-buffer"))
|
|
4195
|
-
.count();
|
|
4196
|
-
assert_eq!(pastes, 1, "must not re-paste; paste-buffer count={pastes}");
|
|
4197
|
-
}
|
|
4198
|
-
|
|
4199
|
-
#[test]
|
|
4200
|
-
fn unverified_wrapped_identity_resends_on_each_of_three_skins() {
|
|
4201
|
-
for skin in ["❯ ", "> ", "ready."] {
|
|
4202
|
-
let marker = format!(
|
|
4203
|
-
"[team-agent-token:msg_skin_{}]",
|
|
4204
|
-
skin.chars().next().unwrap() as u32
|
|
4205
|
-
);
|
|
4206
|
-
let token_text = format!("Team Agent message from leader:\nline1\n\n{marker}");
|
|
4207
|
-
let pending = wrapped_identity_with_skin(&marker, skin);
|
|
4208
|
-
let cleared = format!("{skin}\n");
|
|
4209
|
-
let (be, rec) = backend_clears_after_n_enters(&pending, &cleared, 2);
|
|
4210
|
-
let report = be
|
|
4211
|
-
.inject(
|
|
4212
|
-
&Target::Pane(PaneId::new("%7")),
|
|
4213
|
-
&InjectPayload::Text(token_text),
|
|
4214
|
-
Key::Enter,
|
|
4215
|
-
true,
|
|
4216
|
-
)
|
|
4217
|
-
.expect("inject");
|
|
4218
|
-
assert_eq!(
|
|
4219
|
-
report.submit_verification,
|
|
4220
|
-
SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
4221
|
-
"skin {skin:?} must consume after one extra C-m"
|
|
4222
|
-
);
|
|
4223
|
-
let n = count_submit_enters(&rec.lock().unwrap().clone());
|
|
4224
|
-
assert_eq!(n, 2, "skin {skin:?} extra C-m; got {n}");
|
|
4225
|
-
}
|
|
4226
|
-
}
|
|
4227
|
-
|
|
4228
|
-
#[test]
|
|
4229
|
-
fn unverified_payload_glyphs_do_not_resend_without_identity() {
|
|
4230
|
-
let marker = "[team-agent-token:msg_payload_glyph]";
|
|
4231
|
-
let token_text = format!("Team Agent message from leader:\ncomposer> ❯\n\n{marker}");
|
|
4232
|
-
let pending = "output\ncomposer> leftover from payload\n❯ quoted\n";
|
|
4233
|
-
let (be, rec) = backend_clears_after_n_enters(pending, pending, 99);
|
|
4234
|
-
let report = be
|
|
4235
|
-
.inject(
|
|
4236
|
-
&Target::Pane(PaneId::new("%7")),
|
|
4237
|
-
&InjectPayload::Text(token_text),
|
|
4238
|
-
Key::Enter,
|
|
4239
|
-
true,
|
|
4240
|
-
)
|
|
4241
|
-
.expect("inject");
|
|
4242
|
-
let calls = rec.lock().unwrap().clone();
|
|
4243
|
-
assert_eq!(
|
|
4244
|
-
count_submit_enters(&calls),
|
|
4245
|
-
1,
|
|
4246
|
-
"glyphs in payload must not cause extra C-m when token identity is gone; calls={calls:?}"
|
|
4247
|
-
);
|
|
4248
|
-
assert_eq!(
|
|
4249
|
-
report.submit_verification,
|
|
4250
|
-
SubmitVerification::SubmitConsumptionUnverified
|
|
4251
|
-
);
|
|
4252
|
-
}
|