Skip to content

Commit 26d03e9

Browse files
committed
style(screentracker): use Given/When/Then comments in writeStabilize tests
Restructure test comments to follow Cucumber-style Given/When/Then pattern for clarity. Also fix send-no-echo-agent-reacts assertion to scan for the user message instead of assuming it's the last message in the conversation (the snapshot loop may append an agent message after Send returns).
1 parent fcc12e4 commit 26d03e9

File tree

1 file changed

+129
-127
lines changed

1 file changed

+129
-127
lines changed

lib/screentracker/pty_conversation_test.go

Lines changed: 129 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -450,137 +450,139 @@ func TestMessages(t *testing.T) {
450450
assert.ErrorIs(t, c.Send(st.MessagePartText{Content: ""}), st.ErrMessageValidationEmpty)
451451
})
452452

453-
t.Run("send-no-echo-agent-reacts", func(t *testing.T) {
454-
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
455-
t.Cleanup(cancel)
456-
457-
agent := &testAgent{screen: "prompt"}
458-
// Agent doesn't echo input text, but reacts to carriage return.
459-
agent.onWrite = func(data []byte) {
460-
if string(data) == "\r" {
461-
agent.screen = "processing..."
453+
t.Run("send-no-echo-agent-reacts", func(t *testing.T) {
454+
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
455+
t.Cleanup(cancel)
456+
457+
// Given: an agent that doesn't echo typed input but
458+
// reacts to carriage return by updating the screen.
459+
agent := &testAgent{screen: "prompt"}
460+
agent.onWrite = func(data []byte) {
461+
if string(data) == "\r" {
462+
agent.screen = "processing..."
463+
}
462464
}
463-
}
464-
mClock := quartz.NewMock(t)
465-
mClock.Set(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
466-
cfg := st.PTYConversationConfig{
467-
Clock: mClock,
468-
AgentIO: agent,
469-
SnapshotInterval: interval,
470-
ScreenStabilityLength: 200 * time.Millisecond,
471-
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
472-
}
473-
c := st.NewPTY(ctx, cfg, &testEmitter{})
474-
c.Start(ctx)
475-
476-
// Stabilize: fill snapshot buffer so status becomes stable.
477-
advanceFor(ctx, t, mClock, interval*threshold)
478-
479-
// Send and advance. Phase 1 times out (2s, non-fatal),
480-
// Phase 2 writes \r → onWrite changes screen → succeeds.
481-
sendAndAdvance(ctx, t, c, mClock, st.MessagePartText{Content: "hello"})
482-
483-
// User message was recorded.
484-
msgs := c.Messages()
485-
require.True(t, len(msgs) >= 2)
486-
assert.Equal(t, st.ConversationRoleUser, msgs[len(msgs)-1].Role)
487-
assert.Equal(t, "hello", msgs[len(msgs)-1].Message)
488-
})
489-
490-
t.Run("send-no-echo-no-react", func(t *testing.T) {
491-
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
492-
t.Cleanup(cancel)
493-
494-
agent := &testAgent{screen: "prompt"}
495-
// Agent is completely unresponsive: no echo, no reaction.
496-
agent.onWrite = func(data []byte) {}
497-
mClock := quartz.NewMock(t)
498-
mClock.Set(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
499-
cfg := st.PTYConversationConfig{
500-
Clock: mClock,
501-
AgentIO: agent,
502-
SnapshotInterval: interval,
503-
ScreenStabilityLength: 200 * time.Millisecond,
504-
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
505-
}
506-
c := st.NewPTY(ctx, cfg, &testEmitter{})
507-
c.Start(ctx)
508-
509-
// Stabilize.
510-
advanceFor(ctx, t, mClock, interval*threshold)
511-
512-
// Send in a goroutine; can't use sendAndAdvance because it
513-
// calls require.NoError internally.
514-
var sendErr error
515-
var sendDone atomic.Bool
516-
go func() {
517-
sendErr = c.Send(st.MessagePartText{Content: "hello"})
518-
sendDone.Store(true)
519-
}()
520-
advanceUntil(ctx, t, mClock, func() bool { return sendDone.Load() })
521-
522-
require.Error(t, sendErr)
523-
assert.Contains(t, sendErr.Error(), "failed to wait for processing to start")
524-
})
525-
526-
t.Run("send-tui-selection-esc-cancels", func(t *testing.T) {
527-
// Documents a known limitation: when a TUI agent shows a
528-
// selection prompt, sending a user message wraps it in
529-
// bracketed paste. The ESC (\x1b) in the paste-start
530-
// sequence cancels the selection widget. The user's intended
531-
// choice never reaches the selection handler.
532-
//
533-
// For selection prompts, callers should use MessageTypeRaw
534-
// to send raw keystrokes directly to the PTY.
535-
//
536-
// See lib/httpapi/claude.go for the full formatClaudeCodeMessage
537-
// format; this test focuses on the ESC invariant only.
538-
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
539-
t.Cleanup(cancel)
540-
541-
agent := &testAgent{screen: "selection prompt"}
542-
selectionCancelled := false
543-
agent.onWrite = func(data []byte) {
544-
// Simulate TUI selection widget: ESC cancels the
545-
// selection, changing the screen.
546-
if bytes.Contains(data, []byte("\x1b")) {
547-
selectionCancelled = true
548-
agent.screen = "selection cancelled"
549-
} else if string(data) == "\r" {
550-
agent.screen = "post-cancel"
465+
mClock := quartz.NewMock(t)
466+
mClock.Set(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
467+
cfg := st.PTYConversationConfig{
468+
Clock: mClock,
469+
AgentIO: agent,
470+
SnapshotInterval: interval,
471+
ScreenStabilityLength: 200 * time.Millisecond,
472+
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
551473
}
552-
}
553-
mClock := quartz.NewMock(t)
554-
mClock.Set(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
555-
cfg := st.PTYConversationConfig{
556-
Clock: mClock,
557-
AgentIO: agent,
558-
SnapshotInterval: interval,
559-
ScreenStabilityLength: 200 * time.Millisecond,
560-
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
561-
}
562-
c := st.NewPTY(ctx, cfg, &testEmitter{})
563-
c.Start(ctx)
474+
c := st.NewPTY(ctx, cfg, &testEmitter{})
475+
c.Start(ctx)
476+
advanceFor(ctx, t, mClock, interval*threshold)
564477

565-
// Stabilize.
566-
advanceFor(ctx, t, mClock, interval*threshold)
478+
// When: a message is sent. Phase 1 times out (no echo),
479+
// Phase 2 writes \r and the agent reacts.
480+
sendAndAdvance(ctx, t, c, mClock, st.MessagePartText{Content: "hello"})
567481

568-
// Send using bracketed paste, which contains ESC. The
569-
// test focuses on the ESC invariant — any input wrapped
570-
// in bracketed paste will trigger this.
571-
sendAndAdvance(ctx, t, c, mClock,
572-
st.MessagePartText{Content: "\x1b[200~", Hidden: true},
573-
st.MessagePartText{Content: "2"},
574-
st.MessagePartText{Content: "\x1b[201~", Hidden: true},
575-
)
576-
577-
// The send succeeded, but the selection was cancelled by
578-
// ESC — not option "2" being selected.
579-
assert.True(t, selectionCancelled,
580-
"ESC in bracketed paste cancels TUI selection prompts; "+
581-
"use MessageTypeRaw for selection prompts instead")
582-
})
583-
}
482+
// Then: Send succeeds and the user message is recorded.
483+
msgs := c.Messages()
484+
require.True(t, len(msgs) >= 2)
485+
var foundUserMsg bool
486+
for _, msg := range msgs {
487+
if msg.Role == st.ConversationRoleUser && msg.Message == "hello" {
488+
foundUserMsg = true
489+
break
490+
}
491+
}
492+
assert.True(t, foundUserMsg, "expected user message 'hello' in conversation")
493+
})
494+
t.Run("send-no-echo-no-react", func(t *testing.T) {
495+
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
496+
t.Cleanup(cancel)
497+
498+
// Given: an agent that is completely unresponsive — it
499+
// neither echoes input nor reacts to carriage return.
500+
agent := &testAgent{screen: "prompt"}
501+
agent.onWrite = func(data []byte) {}
502+
mClock := quartz.NewMock(t)
503+
mClock.Set(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
504+
cfg := st.PTYConversationConfig{
505+
Clock: mClock,
506+
AgentIO: agent,
507+
SnapshotInterval: interval,
508+
ScreenStabilityLength: 200 * time.Millisecond,
509+
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
510+
}
511+
c := st.NewPTY(ctx, cfg, &testEmitter{})
512+
c.Start(ctx)
513+
advanceFor(ctx, t, mClock, interval*threshold)
514+
515+
// When: a message is sent. Both Phase 1 (echo) and
516+
// Phase 2 (processing) time out.
517+
// Note: can't use sendAndAdvance here because it calls
518+
// require.NoError internally.
519+
var sendErr error
520+
var sendDone atomic.Bool
521+
go func() {
522+
sendErr = c.Send(st.MessagePartText{Content: "hello"})
523+
sendDone.Store(true)
524+
}()
525+
advanceUntil(ctx, t, mClock, func() bool { return sendDone.Load() })
526+
527+
// Then: Send fails with a Phase 2 error (not Phase 1).
528+
require.Error(t, sendErr)
529+
assert.Contains(t, sendErr.Error(), "failed to wait for processing to start")
530+
})
531+
t.Run("send-tui-selection-esc-cancels", func(t *testing.T) {
532+
// Documents a known limitation: when a TUI agent shows a
533+
// selection prompt, sending a user message wraps it in
534+
// bracketed paste. The ESC (\x1b) in the paste-start
535+
// sequence cancels the selection widget. The user's
536+
// intended choice never reaches the selection handler.
537+
// For selection prompts, callers should use
538+
// MessageTypeRaw to send raw keystrokes directly.
539+
//
540+
// See lib/httpapi/claude.go formatClaudeCodeMessage for
541+
// the full format; this test focuses on the ESC
542+
// invariant only.
543+
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
544+
t.Cleanup(cancel)
545+
546+
// Given: a TUI agent showing a selection prompt where
547+
// ESC cancels the selection and changes the screen.
548+
agent := &testAgent{screen: "selection prompt"}
549+
selectionCancelled := false
550+
agent.onWrite = func(data []byte) {
551+
if bytes.Contains(data, []byte("\x1b")) {
552+
selectionCancelled = true
553+
agent.screen = "selection cancelled"
554+
} else if string(data) == "\r" {
555+
agent.screen = "post-cancel"
556+
}
557+
}
558+
mClock := quartz.NewMock(t)
559+
mClock.Set(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
560+
cfg := st.PTYConversationConfig{
561+
Clock: mClock,
562+
AgentIO: agent,
563+
SnapshotInterval: interval,
564+
ScreenStabilityLength: 200 * time.Millisecond,
565+
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
566+
}
567+
c := st.NewPTY(ctx, cfg, &testEmitter{})
568+
c.Start(ctx)
569+
advanceFor(ctx, t, mClock, interval*threshold)
570+
571+
// When: a message is sent using bracketed paste, which
572+
// contains ESC in the start sequence (\x1b[200~).
573+
sendAndAdvance(ctx, t, c, mClock,
574+
st.MessagePartText{Content: "\x1b[200~", Hidden: true},
575+
st.MessagePartText{Content: "2"},
576+
st.MessagePartText{Content: "\x1b[201~", Hidden: true},
577+
)
578+
579+
// Then: Send succeeds, but the selection was cancelled
580+
// by ESC — option "2" was never delivered to the
581+
// selection handler.
582+
assert.True(t, selectionCancelled,
583+
"ESC in bracketed paste cancels TUI selection prompts; "+
584+
"use MessageTypeRaw for selection prompts instead")
585+
})}
584586

585587
func TestStatePersistence(t *testing.T) {
586588
t.Run("SaveState creates file with correct structure", func(t *testing.T) {

0 commit comments

Comments
 (0)