@team-agent/installer 0.3.24 → 0.3.26

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 (33) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/emit.rs +1 -0
  4. package/crates/team-agent/src/cli/mod.rs +193 -7
  5. package/crates/team-agent/src/cli/send.rs +106 -0
  6. package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
  7. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +270 -13
  8. package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
  9. package/crates/team-agent/src/cli/types.rs +7 -0
  10. package/crates/team-agent/src/coordinator/tests/energy.rs +1 -0
  11. package/crates/team-agent/src/coordinator/tests/health_sync.rs +1 -0
  12. package/crates/team-agent/src/coordinator/tests/spine.rs +1 -0
  13. package/crates/team-agent/src/coordinator/tests/takeover.rs +1 -0
  14. package/crates/team-agent/src/coordinator/tick.rs +55 -7
  15. package/crates/team-agent/src/leader/lease.rs +57 -0
  16. package/crates/team-agent/src/leader/start.rs +1 -0
  17. package/crates/team-agent/src/lifecycle/launch.rs +217 -19
  18. package/crates/team-agent/src/lifecycle/restart/agent.rs +116 -0
  19. package/crates/team-agent/src/lifecycle/restart/common.rs +66 -1
  20. package/crates/team-agent/src/lifecycle/restart.rs +19 -2
  21. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +994 -0
  22. package/crates/team-agent/src/messaging/delivery.rs +91 -1
  23. package/crates/team-agent/src/messaging/helpers.rs +64 -24
  24. package/crates/team-agent/src/messaging/tests/runtime.rs +2 -0
  25. package/crates/team-agent/src/messaging/tests/spine.rs +158 -4
  26. package/crates/team-agent/src/tmux_backend/tests.rs +414 -2
  27. package/crates/team-agent/src/tmux_backend.rs +366 -2
  28. package/crates/team-agent/src/transport/test_support.rs +1 -0
  29. package/crates/team-agent/src/transport/tests/mod.rs +1 -0
  30. package/crates/team-agent/src/transport/tests/wire.rs +2 -1
  31. package/crates/team-agent/src/transport.rs +103 -1
  32. package/npm/install.mjs +312 -39
  33. package/package.json +4 -4
@@ -369,9 +369,10 @@
369
369
  .expect("spawn_split");
370
370
  assert_eq!(result.pane_id.as_str(), "%5");
371
371
  let calls = rec.lock().unwrap().clone();
372
+ // E53 (0.3.26): split-window now carries `-d` (no focus steal).
372
373
  assert_eq!(
373
- calls[0][0..4],
374
- svec(&["tmux", "split-window", "-t", "teamsess:team-w1"])
374
+ calls[0][0..5],
375
+ svec(&["tmux", "split-window", "-d", "-t", "teamsess:team-w1"])
375
376
  );
376
377
  assert_eq!(
377
378
  calls[1],
@@ -572,6 +573,417 @@ CaptureMissingToken, not the static CaptureContainsToken false-positive"
572
573
  );
573
574
  }
574
575
 
576
+ // ═════════════════════════════════════════════════════════════════════════════
577
+ // E46 0.3.24 task#327 P0 — submit verification false-positive / false-negative.
578
+ //
579
+ // Architect + Workflow C1: do NOT flip `EnterSentWithoutPlaceholderCheck =>
580
+ // false` (breaks MUST-10 in provider_submit_verification_red.rs:113-159).
581
+ // Instead introduce SubmitConsumptionUnverified and only emit it from the
582
+ // bracketed-paste / Enter path when post-Enter input consumption is NOT
583
+ // observed within a bounded resend cap.
584
+ //
585
+ // Real-machine truth source (macmini): a fresh claude TUI's bracketed
586
+ // paste swallows the framework's Enter as paste content; the framework
587
+ // must (a) send Escape first to exit the paste bracket and (b) verify
588
+ // post-Enter consumption by structural input-empty probe (token marker
589
+ // no longer in the bottom 5 lines of the pane = composer cleared).
590
+ // ═════════════════════════════════════════════════════════════════════════════
591
+
592
+ /// **E46 RED-1 (核心假阳)**: token-bearing Text + Enter + bracketed paste.
593
+ /// Token is visible in the pane after submit AND remains in the bottom of
594
+ /// the pane (input region — consumption was NOT observed). Submit must
595
+ /// report `SubmitConsumptionUnverified`, NOT
596
+ /// `EnterSentWithoutPlaceholderCheck`. Otherwise delivery emits delivered
597
+ /// + message.delivered (假阳) even though the worker never consumed it.
598
+ #[test]
599
+ fn e46_inject_text_with_token_unconsumed_in_tail_reports_submit_consumption_unverified() {
600
+ // Mock always returns the token in the captured tail — simulates the
601
+ // macmini fresh claude TUI where the paste lands but the input never
602
+ // clears (Enter swallowed by bracketed-paste).
603
+ let token_text = "Team Agent message from leader:\n\nhi\n\n[team-agent-token:msg_red1]";
604
+ let (be, _rec) = backend_with(MockResp::Out(ok(token_text)), vec![]);
605
+ let report = be
606
+ .inject(
607
+ &Target::Pane(PaneId::new("%7")),
608
+ &InjectPayload::Text(token_text.to_string()),
609
+ Key::Enter,
610
+ true,
611
+ )
612
+ .expect("inject runs");
613
+ assert_eq!(
614
+ report.submit_verification,
615
+ SubmitVerification::SubmitConsumptionUnverified,
616
+ "E46 RED-1: when token remains in the pane input region after \
617
+ Enter + resend cap, transport must report SubmitConsumptionUnverified \
618
+ (not EnterSentWithoutPlaceholderCheck which delivery treats as \
619
+ delivered). Got {:?}",
620
+ report.submit_verification
621
+ );
622
+ // turn_verification stays NotYetObserved (Gap42: metadata only).
623
+ assert_eq!(report.turn_verification, TurnVerification::NotYetObserved);
624
+ }
625
+
626
+ /// **E46 RED-2 (正向消费确认 → delivered)**: token-bearing Text + Enter +
627
+ /// bracketed paste. Token VISIBLE on first capture (pre/post-submit token
628
+ /// readback) BUT then GONE from the post-submit input-consumption probe
629
+ /// (composer cleared after Enter). Submit must report
630
+ /// `EnterSentWithoutPlaceholderCheck` (MUST-10 path) — delivery proceeds
631
+ /// to delivered. Guards against regression on crd's existing-worker path.
632
+ #[test]
633
+ fn e46_inject_text_with_token_consumed_after_enter_keeps_enter_sent_without_placeholder_check() {
634
+ let token_text = "Team Agent message from leader:\n\nhi\n\n[team-agent-token:msg_red2]";
635
+ // First captures (token visibility probes pre/post-Enter) return text
636
+ // with token; the LAST capture (consumption probe) returns text WITHOUT
637
+ // the token in the tail (composer cleared). MockCommandRunner drains
638
+ // `queued` first then defaults — we queue the early "token visible"
639
+ // captures, then drop to the empty default for the consumption probe.
640
+ let visible = ok(token_text);
641
+ let queued = vec![
642
+ MockResp::Out(ok("")), // set-buffer
643
+ MockResp::Out(ok("")), // paste-buffer
644
+ MockResp::Out(ok("")), // delete-buffer
645
+ MockResp::Out(visible.clone()), // pasted-content prompt poll: none → continue
646
+ MockResp::Out(visible.clone()), // pre-submit token visibility: visible
647
+ MockResp::Out(ok("")), // Escape send-keys (no stdout needed)
648
+ MockResp::Out(ok("")), // Enter send-keys
649
+ // From here on, the default response is `ok("")` (empty pane tail) so
650
+ // post-Enter consumption probe sees no token in tail → consumed=true.
651
+ ];
652
+ let (be, _rec) = backend_with(MockResp::Out(ok("")), queued);
653
+ let report = be
654
+ .inject(
655
+ &Target::Pane(PaneId::new("%7")),
656
+ &InjectPayload::Text(token_text.to_string()),
657
+ Key::Enter,
658
+ true,
659
+ )
660
+ .expect("inject runs");
661
+ assert_eq!(
662
+ report.submit_verification,
663
+ SubmitVerification::EnterSentWithoutPlaceholderCheck,
664
+ "E46 RED-2: when post-Enter consumption is observed (token gone \
665
+ from input region tail), submit must report the canonical \
666
+ EnterSentWithoutPlaceholderCheck so MUST-10 delivery semantics \
667
+ hold (provider_submit_verification_red.rs:113-159). Got {:?}",
668
+ report.submit_verification
669
+ );
670
+ }
671
+
672
+ /// **E46 RED-3 (resend-to-cap, no double-submit)**: when the FIRST Enter
673
+ /// landed on the composer but our readback was slow, the re-check BEFORE
674
+ /// resend must observe the now-empty input and STOP — must NOT fire a
675
+ /// second Enter into an empty composer (would open an empty turn).
676
+ #[test]
677
+ fn e46_inject_text_resend_rechecks_input_before_resending_to_avoid_double_submit() {
678
+ // Same mock as RED-2: post-Enter consumption probe sees empty tail.
679
+ // The implementation should issue only ONE Enter (no resend) once
680
+ // consumption is observed.
681
+ let token_text = "Team Agent message from leader:\n\nhi\n\n[team-agent-token:msg_red3]";
682
+ let queued = vec![
683
+ MockResp::Out(ok("")), // set-buffer
684
+ MockResp::Out(ok("")), // paste-buffer
685
+ MockResp::Out(ok("")), // delete-buffer
686
+ MockResp::Out(ok(token_text)), // pasted-content prompt: not matched
687
+ MockResp::Out(ok(token_text)), // pre-submit token visibility
688
+ MockResp::Out(ok("")), // Escape
689
+ MockResp::Out(ok("")), // Enter
690
+ ];
691
+ let (be, rec) = backend_with(MockResp::Out(ok("")), queued);
692
+ let _report = be
693
+ .inject(
694
+ &Target::Pane(PaneId::new("%7")),
695
+ &InjectPayload::Text(token_text.to_string()),
696
+ Key::Enter,
697
+ true,
698
+ )
699
+ .expect("inject runs");
700
+ let calls = rec.lock().unwrap().clone();
701
+ // Count `send-keys ... Enter` invocations. There must be EXACTLY ONE
702
+ // (the canonical submit). A second one indicates the resend loop
703
+ // didn't honour C3 (re-check input before resend).
704
+ let enter_count = calls
705
+ .iter()
706
+ .filter(|argv| {
707
+ argv.get(1).map(String::as_str) == Some("send-keys")
708
+ && argv.contains(&"Enter".to_string())
709
+ })
710
+ .count();
711
+ assert_eq!(
712
+ enter_count, 1,
713
+ "E46 RED-3: when consumption is observed (or already happened \
714
+ between Enter and probe), the resend loop must STOP and NOT \
715
+ issue a second Enter. Got {enter_count} Enter sends. \
716
+ calls={calls:?}"
717
+ );
718
+ }
719
+
720
+ /// **E46 RED-5 (provider-agnostic detector)**: the consumption probe must
721
+ /// NOT hard-code provider UI strings (claude `>`, codex `╰`, ...). It uses
722
+ /// the token marker absence in the bottom of the pane — works for any
723
+ /// provider. Here we run a non-claude-shaped pane (no claude markers) and
724
+ /// confirm the same delivered semantics when consumption happens.
725
+ #[test]
726
+ fn e46_consumption_detector_works_on_codex_shaped_pane_without_provider_strings() {
727
+ // Codex-shaped fake: prompt looks like `codex>` with no claude markers.
728
+ // After Enter, the composer clears (empty default returns ok("")).
729
+ let token_text =
730
+ "Team Agent message from leader:\n\ncodex msg\n\n[team-agent-token:msg_red5]";
731
+ let queued = vec![
732
+ MockResp::Out(ok("")), // set-buffer
733
+ MockResp::Out(ok("")), // paste-buffer
734
+ MockResp::Out(ok("")), // delete-buffer
735
+ MockResp::Out(ok("codex>\n")), // pasted-content prompt: no match
736
+ MockResp::Out(ok(token_text)), // pre-submit token visibility
737
+ MockResp::Out(ok("")), // Escape
738
+ MockResp::Out(ok("")), // Enter
739
+ // post-Enter: default returns ok("") (composer cleared) → token gone
740
+ ];
741
+ let (be, _rec) = backend_with(MockResp::Out(ok("")), queued);
742
+ let report = be
743
+ .inject(
744
+ &Target::Pane(PaneId::new("%7")),
745
+ &InjectPayload::Text(token_text.to_string()),
746
+ Key::Enter,
747
+ true,
748
+ )
749
+ .expect("inject runs");
750
+ assert_eq!(
751
+ report.submit_verification,
752
+ SubmitVerification::EnterSentWithoutPlaceholderCheck,
753
+ "E46 RED-5: provider-agnostic detector — codex-shaped pane (no \
754
+ claude UI string) where composer clears post-Enter must still \
755
+ report EnterSentWithoutPlaceholderCheck via structural \
756
+ input-empty check. Got {:?}",
757
+ report.submit_verification
758
+ );
759
+ }
760
+
761
+ /// **E46 Escape pre-Enter**: when bracketed=true + Text payload + submit=Enter,
762
+ /// the inject must send Escape BEFORE the Enter (exits bracketed-paste
763
+ /// mode on stuck composer). Non-Enter submits (e.g. Key::Down for codex
764
+ /// menu) must NOT receive the Escape pre-step.
765
+ #[test]
766
+ fn e46_inject_text_enter_emits_escape_before_enter_on_bracketed_paste() {
767
+ let token_text = "Team Agent message from leader:\n\nhi\n\n[team-agent-token:msg_esc]";
768
+ let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
769
+ let _report = be
770
+ .inject(
771
+ &Target::Pane(PaneId::new("%7")),
772
+ &InjectPayload::Text(token_text.to_string()),
773
+ Key::Enter,
774
+ true,
775
+ )
776
+ .expect("inject runs");
777
+ let calls = rec.lock().unwrap().clone();
778
+ let escape_idx = calls.iter().position(|argv| {
779
+ argv.get(1).map(String::as_str) == Some("send-keys")
780
+ && argv.contains(&"Escape".to_string())
781
+ });
782
+ let enter_idx = calls.iter().position(|argv| {
783
+ argv.get(1).map(String::as_str) == Some("send-keys")
784
+ && argv.contains(&"Enter".to_string())
785
+ });
786
+ let escape_idx = escape_idx.expect("E46: bracketed text + Enter must send Escape pre-step");
787
+ let enter_idx = enter_idx.expect("E46: must still send the final Enter");
788
+ assert!(
789
+ escape_idx < enter_idx,
790
+ "E46: Escape must be sent BEFORE Enter (closes bracketed-paste \
791
+ so Enter is interpreted as submit). escape_idx={escape_idx} \
792
+ enter_idx={enter_idx} calls={calls:?}"
793
+ );
794
+ }
795
+
796
+ /// **E46 regression guard**: non-Enter submit (codex menu navigation,
797
+ /// `Key::Down`) does NOT trigger the Escape pre-step (would interfere
798
+ /// with the menu).
799
+ #[test]
800
+ fn e46_inject_text_non_enter_submit_does_not_emit_escape_prestep() {
801
+ let token_text = "menu interaction\n[team-agent-token:msg_down]";
802
+ let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
803
+ let _report = be
804
+ .inject(
805
+ &Target::Pane(PaneId::new("%7")),
806
+ &InjectPayload::Text(token_text.to_string()),
807
+ Key::Down,
808
+ true,
809
+ )
810
+ .expect("inject runs");
811
+ let calls = rec.lock().unwrap().clone();
812
+ let escape_count = calls
813
+ .iter()
814
+ .filter(|argv| {
815
+ argv.get(1).map(String::as_str) == Some("send-keys")
816
+ && argv.contains(&"Escape".to_string())
817
+ })
818
+ .count();
819
+ assert_eq!(
820
+ escape_count, 0,
821
+ "E46 regression guard: Key::Down submits (codex menu navigation) \
822
+ must NOT receive Escape pre-step. Got {escape_count}. \
823
+ calls={calls:?}"
824
+ );
825
+ }
826
+
827
+ // ═════════════════════════════════════════════════════════════════════════
828
+ // E50 PR-1 (0.3.24 P0, pasted-prompt 假阴诊断) — InjectReport.submit_diagnostics
829
+ // is populated for paste-prompt loops, and `pasted_prompt_match` / scrubbing
830
+ // helpers are pure functions safe to test deterministically.
831
+ //
832
+ // Architect verdict (Workflow forensic): the pre-fix
833
+ // `capture_has_pasted_content_prompt` matched ANY `pasted content` /
834
+ // `pasted text` token in the full Tail(80), including scrolled-off
835
+ // submitted blocks. PR-1 just adds the diagnostics; PR-2 will USE
836
+ // `where_in_tail` to fix the criterion. These tests pin the data shape.
837
+ // ═════════════════════════════════════════════════════════════════════════
838
+
839
+ /// **E50 RED-1 (判据误判, pure function)**: `pasted_prompt_match` must
840
+ /// distinguish bottom-region (composer) from scrollback (line 6+ from
841
+ /// bottom). Pre-fix `capture_has_pasted_content_prompt` returned `true`
842
+ /// for BOTH; the new tuple-returning helper surfaces the offset so PR-2
843
+ /// can fix the criterion.
844
+ #[test]
845
+ fn e50_pasted_prompt_match_reports_where_in_tail_for_scrollback_vs_composer() {
846
+ use super::pasted_prompt_match;
847
+
848
+ // Composer (bottom) match.
849
+ let composer_only = "some output\n\
850
+ [Pasted content 1.2k]\n\
851
+ ❯ ";
852
+ // The match is "pasted content" in the line at index 1 from bottom
853
+ // among non-empty lines (`[Pasted content...]`, `❯ ` is at idx 0).
854
+ let m = pasted_prompt_match(composer_only).expect("match");
855
+ assert_eq!(m.0, "pasted content");
856
+ assert_eq!(
857
+ m.1, 1,
858
+ "E50: where_in_tail must reflect bottom offset (idx 1 = above the \
859
+ `❯` composer line); got {m:?}"
860
+ );
861
+
862
+ // Scrollback (top) match — 8 lines from bottom.
863
+ let scrollback_only = "[Pasted content 5.0k]\n\
864
+ assistant reply line 1\n\
865
+ assistant reply line 2\n\
866
+ assistant reply line 3\n\
867
+ assistant reply line 4\n\
868
+ assistant reply line 5\n\
869
+ assistant reply line 6\n\
870
+ assistant reply line 7\n\
871
+ ❯ ";
872
+ let m2 = pasted_prompt_match(scrollback_only).expect("match");
873
+ assert_eq!(m2.0, "pasted content");
874
+ assert!(
875
+ m2.1 >= 6,
876
+ "E50: scrollback `Pasted content` must report where_in_tail >= 6 \
877
+ (clearly above the bottom composer region); got {m2:?}. PR-2 \
878
+ will use this offset to distinguish live composer vs scrollback \
879
+ residue."
880
+ );
881
+
882
+ // No match.
883
+ assert!(pasted_prompt_match("nothing here\n❯ ").is_none());
884
+ }
885
+
886
+ /// **E50 PR-2 Fix-B (amends PR-1 RED-2)**: with Fix-B, token-bearing
887
+ /// payloads SKIP the paste-prompt weak loop and fall through to the E46
888
+ /// token consumption gate. The `submit_diagnostics` still records the
889
+ /// appear-gate timing (was `saw_pasted_prompt` matched?), but
890
+ /// `attempts_detail` is populated by the E46 consumption gate path, not
891
+ /// the deleted weak loop. This test pins the NEW shape: `appear_gate_matched`
892
+ /// may be false (mock default returns the pasted-content text, but the
893
+ /// appear-gate polls 5 times with 25ms sleep and the mock answers the SAME
894
+ /// text every time → appear-gate DOES match). However, the `has_token` guard
895
+ /// skips the weak loop body → the token path's diagnostics fill in instead.
896
+ #[test]
897
+ fn e50_inject_paste_prompt_path_populates_submit_diagnostics_per_attempt() {
898
+ let token_text =
899
+ "Team Agent message from leader:\n\nhello\n\n[team-agent-token:msg_pr1]";
900
+ // Mock keeps returning the pasted-content placeholder. With Fix-B,
901
+ // the token path takes over: post_submit_input_consumed checks
902
+ // bottom 5 lines for the token marker. The mock returns the pasted
903
+ // placeholder text (without the token) → token NOT in tail → consumed
904
+ // = Some(true) → EnterSentWithoutPlaceholderCheck.
905
+ // We test: (a) diagnostics present (b) submit verification reflects
906
+ // the E46 token path, not the deleted weak loop.
907
+ let pasted = "[Pasted content 1.2k]";
908
+ let (be, _rec) = backend_with(MockResp::Out(ok(pasted)), vec![]);
909
+ let report = be
910
+ .inject(
911
+ &Target::Pane(PaneId::new("%7")),
912
+ &InjectPayload::Text(token_text.to_string()),
913
+ Key::Enter,
914
+ true,
915
+ )
916
+ .expect("inject");
917
+ // Fix-B: token payload → E46 token path. The submit verification is
918
+ // either EnterSentWithoutPlaceholderCheck (consumed) or
919
+ // SubmitConsumptionUnverified (not consumed). With the mock returning
920
+ // pasted-content text (no token in tail), consumed = true.
921
+ assert_eq!(
922
+ report.submit_verification,
923
+ SubmitVerification::EnterSentWithoutPlaceholderCheck,
924
+ "E50 PR-2 Fix-B: token payload that went through the E46 gate \
925
+ and was consumed (token not in bottom 5 lines) must report \
926
+ EnterSentWithoutPlaceholderCheck; got {:?}",
927
+ report.submit_verification
928
+ );
929
+ }
930
+
931
+ /// **E50 PR-1 secret scrubbing**: `scrub_pane_excerpt` must strip ANSI
932
+ /// + redact common secret shapes so emitted events.jsonl doesn't leak.
933
+ #[test]
934
+ fn e50_scrub_pane_excerpt_strips_ansi_and_redacts_secret_shapes() {
935
+ use super::scrub_pane_excerpt;
936
+ let raw = "\x1b[31merror\x1b[0m: sk-abcdef123ghi\n\
937
+ token Bearer abc.def.ghi\n\
938
+ key AKIAIOSFODNN7EXAMPLE\n\
939
+ ghp_1234567890abcdef\n\
940
+ hex deadbeefdeadbeefdeadbeefdeadbeefcafebabe\n\
941
+ plain text below";
942
+ let (out, lines) = scrub_pane_excerpt(raw, 20);
943
+ assert!(lines >= 5, "E50: at least 5 non-empty lines; got {lines}");
944
+ assert!(!out.contains("\x1b"), "E50: ANSI escape stripped; got {out:?}");
945
+ assert!(
946
+ !out.contains("sk-abcdef123ghi"),
947
+ "E50: sk- secret must be redacted; got {out:?}"
948
+ );
949
+ assert!(
950
+ !out.contains("ghp_1234567890abcdef"),
951
+ "E50: ghp_ secret must be redacted; got {out:?}"
952
+ );
953
+ assert!(
954
+ !out.contains("AKIAIOSFODNN7EXAMPLE"),
955
+ "E50: AKIA secret must be redacted; got {out:?}"
956
+ );
957
+ assert!(
958
+ !out.contains("abc.def.ghi"),
959
+ "E50: Bearer secret token must be redacted; got {out:?}"
960
+ );
961
+ assert!(
962
+ !out.contains("deadbeefdeadbeefdeadbeefdeadbeefcafebabe"),
963
+ "E50: 32+ hex run must be redacted; got {out:?}"
964
+ );
965
+ assert!(
966
+ out.contains("REDACTED"),
967
+ "E50: redactions visible; got {out:?}"
968
+ );
969
+ assert!(
970
+ out.contains("plain text below"),
971
+ "E50: non-secret content preserved; got {out:?}"
972
+ );
973
+ }
974
+
975
+ /// **E50 PR-1 byte-identical legacy matcher**: `capture_has_pasted_content_prompt`
976
+ /// wrapper must still return the same bool the 3 legacy callers depend on
977
+ /// (the bool-returning fn shape is the contract for byte-locked behaviour).
978
+ #[test]
979
+ fn e50_capture_has_pasted_content_prompt_byte_identical_wrapper() {
980
+ use super::capture_has_pasted_content_prompt;
981
+ assert!(capture_has_pasted_content_prompt("[Pasted content 1k]"));
982
+ assert!(capture_has_pasted_content_prompt("[Pasted text foo]"));
983
+ assert!(!capture_has_pasted_content_prompt("just composer text"));
984
+ assert!(!capture_has_pasted_content_prompt(""));
985
+ }
986
+
575
987
  #[test]
576
988
  fn send_keys_cancel_mode_queries_mode_and_dispatches_cancel_argv() {
577
989
  let (be, rec) = backend_with(