12 "github.com/stretchr/testify/require"
13 "golang.org/x/sys/unix"
16 func TestClientEndsWhenTheDaemonEnds(t *testing.T) {
18 listener, err := daemonListenSocket(filepath.Join(dir, socketFileName))
19 require.NoError(t, err)
21 accepted := make(chan net.Conn)
23 conn, err := listener.Accept()
29 conn, err := net.Dial("unix", filepath.Join(dir, socketFileName))
30 require.NoError(t, err)
33 done := make(chan error)
35 done <- terminalSession(t.Context(), conn, in, io.Discard)
38 require.NoError(t, (<-accepted).Close())
39 require.ErrorIs(t, <-done, errTerminalDaemonClosed)
40 require.NoError(t, listener.Close())
43 func TestEventText(t *testing.T) {
44 testCases := []struct {
50 name: "keeps the complete reply text",
58 name: "hides the current terminal user message",
62 Source: terminalSource,
67 name: "marks a user message from another source",
70 Text: "from the phone",
73 text: "[telegram] from the phone",
76 name: "marks the start of a subagent",
81 text: "[GreenOtter] started",
84 name: "keeps the complete subagent report",
87 Text: "[GreenOtter]\nThere are 3 files.\n\nFull chat: zoom(\"GreenOtter\")",
89 Status: subagentFinished,
91 text: "[GreenOtter]\nThere are 3 files.\n\nFull chat: zoom(\"GreenOtter\")",
94 name: "clips a long tool result with its omitted count",
97 Text: strings.Repeat("line\n", 12),
99 text: "[result] " + strings.Repeat("line\n", 9) + "line\n[2 more lines]",
103 for _, tc := range testCases {
104 t.Run(tc.name, func(t *testing.T) {
105 require.Equal(t, tc.text, terminalEventText(tc.event))
110 func TestTerminalEventsKeepMarkdownInNonterminalOutput(t *testing.T) {
111 var out strings.Builder
113 text := "# Heading\n\n**bold** and `code`\nA | B\n--- | ---\nx | y"
114 terminalTestShowEvents(t, &out, Event{
119 Text: "[BlueBird]\n" + text,
122 require.Equal(t, text+"\n[BlueBird]\n"+text+"\n", out.String())
125 func TestTerminalFileOutputKeepsMarkdown(t *testing.T) {
126 out, err := os.CreateTemp(t.TempDir(), "output")
127 require.NoError(t, err)
130 require.NoError(t, out.Close())
133 text := "**plain**\n---\n`code`"
134 terminalTestShowEvents(t, out, Event{
139 require.Zero(t, terminalColumns(out))
141 contents, err := os.ReadFile(out.Name())
142 require.NoError(t, err)
143 require.Equal(t, text+"\n", string(contents))
146 func TestTerminalStylesOnlyRepliesAndWork(t *testing.T) {
147 fd, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY, 0)
148 require.NoError(t, err)
151 require.NoError(t, unix.Close(fd))
154 require.NoError(t, unix.IoctlSetWinsize(fd, unix.TIOCSWINSZ, &unix.Winsize{Col: 12}))
156 out := TerminalTestOutput{fd: fd}
157 require.Equal(t, 12, terminalColumns(&out))
158 terminalTestShowEvents(t, &out,
162 Text: "**first next last**",
166 Text: "[BlueBird]\n# Report",
170 Text: "**raw tool**",
174 Text: "**raw result**",
178 Text: "**raw error**",
182 Text: "**raw notice**",
186 Text: "**raw input**",
191 Text: "**raw agent**",
201 require.Equal(t, "[working]\n\x1b[1mfirst next\x1b[0m\n\x1b[1mlast\x1b[0m\n[BlueBird]\n\x1b[1mReport\x1b[0m\n"+
202 "[tool] **raw tool**\n[result] **raw result**\n[error] **raw error**\n**raw notice**\n[telegram] **raw input**\n"+
203 "[connected to **raw agent**]\n[BlueBird] started\n[working]\n[done]\n> ", out.String())
206 func TestTerminalZeroWidthUsesEightyAndReadsChanges(t *testing.T) {
207 fd, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY, 0)
208 require.NoError(t, err)
211 require.NoError(t, unix.Close(fd))
214 out := TerminalTestOutput{fd: fd}
215 require.NoError(t, unix.IoctlSetWinsize(fd, unix.TIOCSWINSZ, &unix.Winsize{Col: 0}))
216 require.Equal(t, 80, terminalColumns(&out))
217 terminalTestShowEvents(t, &out, Event{
219 Text: strings.Repeat("a", 81),
222 require.Equal(t, strings.Repeat("a", 80)+"\na\n> ", out.String())
223 require.NoError(t, unix.IoctlSetWinsize(fd, unix.TIOCSWINSZ, &unix.Winsize{Col: 40}))
224 require.Equal(t, 40, terminalColumns(&out))
227 func TestTerminalPromptsAfterIdleEventsAndClearsBeforeShownEvents(t *testing.T) {
228 fd, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY, 0)
229 require.NoError(t, err)
232 require.NoError(t, unix.Close(fd))
235 out := TerminalTestOutput{fd: fd}
236 terminalTestShowEvents(t, &out,
243 Source: terminalSource,
246 Event{Kind: "unknown"},
253 Text: "**Idle report**",
271 require.Equal(t, "[connected to Example]\n> "+
272 "\r\x1b[2KIdle notice\n> "+
273 "\r\x1b[2K\x1b[1mIdle report\x1b[0m\n> "+
274 "\r\x1b[2K[working]\nBusy notice\n\x1b[1mAnswer\x1b[0m\n[done]\n> "+
275 "\r\x1b[2K[error] Idle error\n> ", out.String())
278 func TestTerminalNonterminalEventsHaveNoPromptOrClearCommands(t *testing.T) {
279 var out strings.Builder
281 terminalTestShowEvents(t, &out,
306 require.Equal(t, "[connected to Example]\nIdle notice\n[working]\nBusy notice\n**Answer**\n[done]\nIdle again\n", out.String())
309 // TerminalTestOutput captures text while it supplies a test terminal
311 type TerminalTestOutput struct {
316 func (o *TerminalTestOutput) Fd() uintptr {
320 func terminalTestShowEvents(t *testing.T, out io.Writer, events ...Event) {
323 server, client := net.Pipe()
324 done := make(chan error, 1)
326 done <- terminalShowEvents(client, out)
329 encoder := json.NewEncoder(server)
330 for _, event := range events {
331 require.NoError(t, encoder.Encode(event))
334 require.NoError(t, server.Close())
335 require.NoError(t, <-done)
336 require.NoError(t, client.Close())