@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/emit.rs +1 -0
- package/crates/team-agent/src/cli/mod.rs +193 -7
- package/crates/team-agent/src/cli/send.rs +106 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +270 -13
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +7 -0
- package/crates/team-agent/src/coordinator/tests/energy.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/spine.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/takeover.rs +1 -0
- package/crates/team-agent/src/coordinator/tick.rs +55 -7
- package/crates/team-agent/src/leader/lease.rs +57 -0
- package/crates/team-agent/src/leader/start.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch.rs +217 -19
- package/crates/team-agent/src/lifecycle/restart/agent.rs +116 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +66 -1
- package/crates/team-agent/src/lifecycle/restart.rs +19 -2
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +994 -0
- package/crates/team-agent/src/messaging/delivery.rs +91 -1
- package/crates/team-agent/src/messaging/helpers.rs +64 -24
- package/crates/team-agent/src/messaging/tests/runtime.rs +2 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +158 -4
- package/crates/team-agent/src/tmux_backend/tests.rs +414 -2
- package/crates/team-agent/src/tmux_backend.rs +366 -2
- package/crates/team-agent/src/transport/test_support.rs +1 -0
- package/crates/team-agent/src/transport/tests/mod.rs +1 -0
- package/crates/team-agent/src/transport/tests/wire.rs +2 -1
- package/crates/team-agent/src/transport.rs +103 -1
- package/npm/install.mjs +312 -39
- 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..
|
|
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(
|