@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.
Files changed (46) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +5 -3
  4. package/README.zh.md +4 -3
  5. package/crates/team-agent/src/cli/adapters.rs +15 -1
  6. package/crates/team-agent/src/cli/emit.rs +33 -13
  7. package/crates/team-agent/src/cli/mod.rs +148 -21
  8. package/crates/team-agent/src/cli/status_port/brief.rs +62 -3
  9. package/crates/team-agent/src/cli/tests/base.rs +112 -225
  10. package/crates/team-agent/src/cli/tests/run_delegation.rs +4 -2
  11. package/crates/team-agent/src/cli/tests/status_send.rs +2 -2
  12. package/crates/team-agent/src/compiler/tests.rs +46 -59
  13. package/crates/team-agent/src/leader/tests/byte_findings.rs +1 -14
  14. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +57 -173
  15. package/crates/team-agent/src/lifecycle/launch/pi_mcp_extension.js +226 -0
  16. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +0 -37
  17. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +8 -3
  18. package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -0
  19. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +24 -17
  20. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +22 -9
  21. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_native_mcp_host.mjs +71 -0
  22. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +43 -66
  23. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +71 -392
  24. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +0 -5
  25. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -361
  26. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -16
  27. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +20 -112
  28. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1 -12
  29. package/crates/team-agent/src/lifecycle/tests/pi_native_mcp_capability_red.rs +546 -0
  30. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +6 -2
  31. package/crates/team-agent/src/lifecycle/tests.rs +1 -0
  32. package/crates/team-agent/src/lifecycle/worker_command_context.rs +11 -54
  33. package/crates/team-agent/src/messaging/tests/wave2.rs +2 -302
  34. package/crates/team-agent/src/model/name_similarity.rs +52 -67
  35. package/crates/team-agent/src/model/task_graph.rs +37 -124
  36. package/crates/team-agent/src/model/yaml/tests.rs +46 -165
  37. package/crates/team-agent/src/provider/tests/adapter.rs +0 -20
  38. package/crates/team-agent/src/provider/tests/classify.rs +106 -198
  39. package/crates/team-agent/src/state/projection.rs +15 -0
  40. package/crates/team-agent/src/tmux_backend/tests.rs +315 -501
  41. package/crates/team-agent/src/tmux_backend.rs +110 -0
  42. package/crates/team-agent/src/transport/test_support.rs +35 -0
  43. package/crates/team-agent/src/transport/tests/wire.rs +32 -58
  44. package/crates/team-agent/src/transport.rs +26 -12
  45. package/package.json +4 -4
  46. 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 pasted_signal_never_seen_wrong_id_is_consumed() {
2677
- let marker = "[team-agent-token:msg_fold]";
2678
- let other = "❯ [Pasted text #2 +3 lines]\n";
2679
- assert_eq!(
2680
- super::consumption_from_capture(other, marker, false, Some(super::PasteLatch::HashId(1))),
2681
- Some(true),
2682
- "our #1 gone, a later #2 in composer still means ours submitted"
2683
- );
2684
- }
2685
-
2686
- #[test]
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 pasted_signal_seen_token_still_present_is_not_consumed() {
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
- assert_eq!(
2715
- super::token_sighting(true, true),
2716
- super::TokenSighting::Visible
2717
- );
2718
- assert_eq!(
2719
- super::consumption_from_capture(&still, marker, true, None),
2720
- Some(false)
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 pasted_signal_inject_folded_enter_not_effective_is_unverified() {
2741
- let token_text = "Team Agent message from leader:\nline1\nline2\nline3\nline4\nline5\n\n[team-agent-token:msg_fold_stay]";
2742
- let placeholder = "❯ [Pasted text #4 +6 lines]\n";
2743
- let (be, _rec) = backend_folded(placeholder, placeholder);
2744
- let report = be
2745
- .inject(
2746
- &Target::Pane(PaneId::new("%7")),
2747
- &InjectPayload::Text(token_text.to_string()),
2748
- Key::Enter,
2749
- true,
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
- // 只有 #4 离开 composer;不加 Working,否则 busy 升级会让半三不经正信号就绿。
2765
- let after = "assistant reply\n❯ \n";
2766
- let (be, _rec) = backend_folded(placeholder, after);
2767
- let report = be
2768
- .inject(
2769
- &Target::Pane(PaneId::new("%7")),
2770
- &InjectPayload::Text(token_text.to_string()),
2771
- Key::Enter,
2772
- true,
2773
- )
2774
- .expect("inject");
2775
- assert_eq!(
2776
- report.submit_verification,
2777
- SubmitVerification::EnterSentWithoutPlaceholderCheck,
2778
- "folded + Enter effective (#4 left composer) must be consumed; got {:?}",
2779
- report.submit_verification
2780
- );
2781
- }
2782
-
2783
- #[test]
2784
- fn pasted_signal_inject_short_token_visible_consumed_matches_pre_fix() {
2785
- let token_text = "ping [team-agent-token:msg_short_ok]";
2786
- let (be, _rec) = backend_folded(token_text, "❯ \n");
2787
- let report = be
2788
- .inject(
2789
- &Target::Pane(PaneId::new("%7")),
2790
- &InjectPayload::Text(token_text.to_string()),
2791
- Key::Enter,
2792
- true,
2793
- )
2794
- .expect("inject");
2795
- assert_eq!(
2796
- report.submit_verification,
2797
- SubmitVerification::EnterSentWithoutPlaceholderCheck,
2798
- "short visible token then gone must stay consumed; got {:?}",
2799
- report.submit_verification
2800
- );
2801
- }
2802
-
2803
- #[test]
2804
- fn pasted_signal_inject_short_token_visible_unconsumed_matches_pre_fix() {
2805
- let token_text = "ping [team-agent-token:msg_short_stay]";
2806
- let (be, _rec) = backend_folded(token_text, token_text);
2807
- let report = be
2808
- .inject(
2809
- &Target::Pane(PaneId::new("%7")),
2810
- &InjectPayload::Text(token_text.to_string()),
2811
- Key::Enter,
2812
- true,
2813
- )
2814
- .expect("inject");
2815
- assert_eq!(
2816
- report.submit_verification,
2817
- SubmitVerification::SubmitConsumptionUnverified,
2818
- "short visible token still in composer must stay unverified; got {:?}",
2819
- report.submit_verification
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 gone_with_grok_placeholder_still_in_composer_must_not_be_consumed() {
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 g4_turn_grok_unsubmitted_paste_is_missing() {
3722
- let text = "│ ❯ [Pasted: 42 lines] │";
3723
- assert_eq!(
3724
- observe_turn_from_capture(text, Some(G4_TOKEN)),
3725
- TurnVerification::LeaderNewTurnBoundaryMissing
3726
- );
3727
- }
3728
-
3729
- #[test]
3730
- fn g4_turn_claude_unsubmitted_paste_is_missing() {
3731
- let text = "❯ pasted text #4 +12 lines";
3732
- assert_eq!(
3733
- observe_turn_from_capture(text, Some(G4_TOKEN)),
3734
- TurnVerification::LeaderNewTurnBoundaryMissing
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
- }