Files
2026-07-13 13:00:08 +08:00

88 lines
3.4 KiB
Go

package agent
import (
"strings"
"testing"
"reasonix/internal/event"
"reasonix/internal/provider"
)
// TestTextSinkReproducesInlineOutput pins the byte-exact rendering of a turn's
// event stream with no markdown renderer (the piped / chat-bridge path).
func TestTextSinkReproducesInlineOutput(t *testing.T) {
var b strings.Builder
s := NewTextSink(&b, nil, 80)
s.Emit(event.Event{Kind: event.TurnStarted})
s.Emit(event.Event{Kind: event.Reasoning, Text: "let me think"})
s.Emit(event.Event{Kind: event.Text, Text: "Hello"})
s.Emit(event.Event{Kind: event.Message, Text: "Hello", Reasoning: "let me think"})
s.Emit(event.Event{Kind: event.Usage, Usage: &provider.Usage{
PromptTokens: 1000, CompletionTokens: 200, TotalTokens: 1200,
CacheHitTokens: 900, CacheMissTokens: 100,
}})
s.Emit(event.Event{Kind: event.ToolDispatch, Tool: event.Tool{Name: "read_file", Args: `{"path":"a"}`}})
s.Emit(event.Event{Kind: event.ToolResult, Tool: event.Tool{Name: "read_file", Output: "contents"}})
s.Emit(event.Event{Kind: event.ToolResult, Tool: event.Tool{Name: "bash", Err: "blocked by permission policy"}})
s.Emit(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "tool output truncated: 5 of 100 bytes elided"})
s.Emit(event.Event{Kind: event.Notice, Level: event.LevelWarn, Text: "response truncated: hit max output tokens"})
want := "\x1b[2m ▎ thinking\x1b[0m\n" + // reasoning header
"Hello" + // answer delta
"\n" + // Message close (no renderer)
" · 1200 tok · in 1000 (900 cached / 100 new) · out 200\n" + // usage
" -> read_file {\"path\":\"a\"}\n" + // tool dispatch
// successful read_file result is silent
" ⊘ bash blocked by permission policy\n" + // blocked result
" · tool output truncated: 5 of 100 bytes elided\n" + // info notice
" ! response truncated: hit max output tokens\n" // warn notice
if got := b.String(); got != want {
t.Errorf("TextSink output mismatch:\n got: %q\nwant: %q", got, want)
}
}
func TestTextSinkCanShowReasoningInVerboseMode(t *testing.T) {
var b strings.Builder
s := NewTextSink(&b, nil, 80)
s.SetShowReasoning(true)
s.Emit(event.Event{Kind: event.TurnStarted})
s.Emit(event.Event{Kind: event.Reasoning, Text: "let me think"})
s.Emit(event.Event{Kind: event.Text, Text: "Hello"})
s.Emit(event.Event{Kind: event.Message, Text: "Hello", Reasoning: "let me think"})
want := "\x1b[2m ▎ thinking\x1b[0m\n" +
"\x1b[2mlet me think\x1b[0m" +
"\n" +
"Hello\n"
if got := b.String(); got != want {
t.Errorf("verbose TextSink output mismatch:\n got: %q\nwant: %q", got, want)
}
}
// fakeRenderer returns a fixed string so the redraw escape sequence is testable
// without a real markdown library.
type fakeRenderer struct{}
func (fakeRenderer) Render(string) string { return "RENDERED" }
// TestTextSinkRedrawsWithRenderer pins the cursor-control redraw: with a
// renderer wired in, a completed text stream is moved over and replaced by the
// styled output. "ab\ncd" occupies one extra row (one newline), so the cursor
// moves up 1 and clears to end of screen.
func TestTextSinkRedrawsWithRenderer(t *testing.T) {
var b strings.Builder
s := NewTextSink(&b, fakeRenderer{}, 80)
s.Emit(event.Event{Kind: event.TurnStarted})
s.Emit(event.Event{Kind: event.Text, Text: "ab\ncd"})
s.Emit(event.Event{Kind: event.Message, Text: "ab\ncd"})
want := "ab\ncd" + "\r\033[1A\033[0J" + "RENDERED"
if got := b.String(); got != want {
t.Errorf("redraw output mismatch:\n got: %q\nwant: %q", got, want)
}
}