fix(e2e): verify Enter submission in TmuxSession.Send · Entire
fix(e2e): verify Enter submission in TmuxSession.Send
5efe246→main·
pfleidi·2w ago·1 file·+51 added/-18 removed
Droid ingests long pasted prompts over several seconds; the fixed 200ms delay before Enter meant the keypress arrived while the input handler was still processing and got swallowed, leaving the prompt unsubmitted (recurring TestFactory* failures in CI).
Send now waits until the echoed input has fully rendered, snapshots the pre-submit pane for WaitFor's settle guard, then verifies the pane reacted to Enter and retries up to three times. The snapshot is taken before Enter so it can never include response output from a fast agent, which would deadlock WaitFor's change requirement.
Sessions
0da2e3e238eaView transcript
[?
Debug E2E Test Timing IssuesClaude Code·Opus 4.8[1m]·2 steps](/content/gh/entireio/cli/session/58280a70-c670-4b2d-a1c8-80439ef7460d#timeline-0da2e3e238ea/index.html)
Changes
1
e2e/agents
- Mtmux.go+51/-18
63 unmodified lines
}
func (s *TmuxSession) Send(input string) error {
preSend := stableContent(s.Capture())
// Send text and Enter separately — Claude's TUI can swallow Enter
preSendRaw := s.Capture()
// Send text and Enter separately — TUIs (Claude, droid) can swallow Enter
// if it arrives before the input handler finishes processing the text.
// Droid ingests long pasted prompts over several seconds, so wait until
// the echoed input has fully rendered before submitting.
if err := s.SendKeys(input); err != nil {
return err
}
time.Sleep(200 * time.Millisecond)
if err := s.SendKeys("Enter"); err != nil {
return err
}
s.waitForInputIngested(preSendRaw)
// Wait for the terminal to reflect the echoed input, then snapshot.
// This ensures WaitFor compares against post-echo content, preventing
// false matches on prompt characters (e.g. ❯) in the echoed input.
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
time.Sleep(200 * time.Millisecond)
current := stableContent(s.Capture())
if current != preSend {
s.stableAtSend = current
return nil
}
}
// Snapshot the post-echo, pre-submit content. WaitFor requires content to
// change from this snapshot before it can settle, preventing false matches
// on prompt characters (e.g. ❯) in the echoed input. Taken before Enter so
// it can never include response output from a fast agent.
s.stableAtSend = stableContent(s.Capture())
// Verify the pane reacted to Enter; a swallowed Enter leaves the prompt
// sitting unsubmitted in the input box. Retry a couple of times — Enter
// on an already-submitted (empty) input box is a no-op.
for range 3 {
preEnter := s.Capture()
if err := s.SendKeys("Enter"); err != nil {
return err
}
if s.paneChangedFrom(preEnter, 2*time.Second) {
break
}
}
return nil
}
// waitForInputIngested waits until the pane content has changed from preSend
// (the echoed input is visible) and stopped changing between consecutive
// polls (the TUI's input handler has caught up).
func (s *TmuxSession) waitForInputIngested(preSend string) {
deadline := time.Now().Add(15 * time.Second)
last := s.Capture()
for time.Now().Before(deadline) {
time.Sleep(300 * time.Millisecond)
current := s.Capture()
if current == last && current != preSend {
return
}
last = current
}
}
// paneChangedFrom reports whether the pane content diverges from prev within
// the timeout.
func (s *TmuxSession) paneChangedFrom(prev string, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
time.Sleep(200 * time.Millisecond)
if s.Capture() != prev {
return true
}
}
return false
}
// SendKeys sends raw tmux key names without appending Enter.
func (s *TmuxSession) SendKeys(keys ...string) error {
args := append([]string{"send-keys", "-t", s.name}, keys...)