]> git.giacomolli.uk - onechat/blobdiff - terminal_test.go
Select notification targets by stable ID
[onechat] / terminal_test.go
index 91e43d2d84812e966eb8a35d00353449d739e33b..7f83b575dd42f427994dfc5223f1610ed2de023f 100644 (file)
@@ -1,13 +1,16 @@
 package main
 
 import (
+       "encoding/json"
        "io"
        "net"
+       "os"
        "path/filepath"
        "strings"
        "testing"
 
        "github.com/stretchr/testify/require"
+       "golang.org/x/sys/unix"
 )
 
 func TestClientEndsWhenTheDaemonEnds(t *testing.T) {
@@ -44,24 +47,56 @@ func TestEventText(t *testing.T) {
                text  string
        }{
                {
-                       name:  "reply as it is",
-                       event: Event{Kind: "reply", Text: "Done."},
-                       text:  "Done.",
+                       name: "keeps the complete reply text",
+                       event: Event{
+                               Kind: "reply",
+                               Text: "Done.",
+                       },
+                       text: "Done.",
                },
                {
-                       name:  "own user message not shown",
-                       event: Event{Kind: "user", Text: "typed", Source: terminalSource},
-                       text:  "",
+                       name: "hides the current terminal user message",
+                       event: Event{
+                               Kind:   "user",
+                               Text:   "typed",
+                               Source: terminalSource,
+                       },
+                       text: "",
                },
                {
-                       name:  "user message of another source marked",
-                       event: Event{Kind: "user", Text: "from the phone", Source: "telegram"},
-                       text:  "[telegram] from the phone",
+                       name: "marks a user message from another source",
+                       event: Event{
+                               Kind:   "user",
+                               Text:   "from the phone",
+                               Source: "telegram",
+                       },
+                       text: "[telegram] from the phone",
                },
                {
-                       name:  "long result cut",
-                       event: Event{Kind: "echo", Text: strings.Repeat("line\n", 12)},
-                       text:  "[result] " + strings.Repeat("line\n", 9) + "line\n[2 more lines]",
+                       name: "marks the start of a subagent",
+                       event: Event{
+                               Kind: "subagent",
+                               Text: "GreenOtter",
+                       },
+                       text: "[GreenOtter] started",
+               },
+               {
+                       name: "keeps the complete subagent report",
+                       event: Event{
+                               Kind:   "work",
+                               Text:   "[GreenOtter]\nThere are 3 files.\n\nFull chat: zoom(\"GreenOtter\")",
+                               Source: "GreenOtter",
+                               Status: subagentFinished,
+                       },
+                       text: "[GreenOtter]\nThere are 3 files.\n\nFull chat: zoom(\"GreenOtter\")",
+               },
+               {
+                       name: "clips a long tool result with its omitted count",
+                       event: Event{
+                               Kind: "echo",
+                               Text: strings.Repeat("line\n", 12),
+                       },
+                       text: "[result] " + strings.Repeat("line\n", 9) + "line\n[2 more lines]",
                },
        }
 
@@ -71,3 +106,232 @@ func TestEventText(t *testing.T) {
                })
        }
 }
+
+func TestTerminalEventsKeepMarkdownInNonterminalOutput(t *testing.T) {
+       var out strings.Builder
+
+       text := "# Heading\n\n**bold** and `code`\nA | B\n--- | ---\nx | y"
+       terminalTestShowEvents(t, &out, Event{
+               Kind: "reply",
+               Text: text,
+       }, Event{
+               Kind: "work",
+               Text: "[BlueBird]\n" + text,
+       })
+
+       require.Equal(t, text+"\n[BlueBird]\n"+text+"\n", out.String())
+}
+
+func TestTerminalFileOutputKeepsMarkdown(t *testing.T) {
+       out, err := os.CreateTemp(t.TempDir(), "output")
+       require.NoError(t, err)
+
+       defer func() {
+               require.NoError(t, out.Close())
+       }()
+
+       text := "**plain**\n---\n`code`"
+       terminalTestShowEvents(t, out, Event{
+               Kind: "reply",
+               Text: text,
+       })
+
+       require.Zero(t, terminalColumns(out))
+
+       contents, err := os.ReadFile(out.Name())
+       require.NoError(t, err)
+       require.Equal(t, text+"\n", string(contents))
+}
+
+func TestTerminalStylesOnlyRepliesAndWork(t *testing.T) {
+       fd, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY, 0)
+       require.NoError(t, err)
+
+       defer func() {
+               require.NoError(t, unix.Close(fd))
+       }()
+
+       require.NoError(t, unix.IoctlSetWinsize(fd, unix.TIOCSWINSZ, &unix.Winsize{Col: 12}))
+
+       out := TerminalTestOutput{fd: fd}
+       require.Equal(t, 12, terminalColumns(&out))
+       terminalTestShowEvents(t, &out,
+               Event{Kind: "busy"},
+               Event{
+                       Kind: "reply",
+                       Text: "**first next last**",
+               },
+               Event{
+                       Kind: "work",
+                       Text: "[BlueBird]\n# Report",
+               },
+               Event{
+                       Kind: "tool",
+                       Text: "**raw tool**",
+               },
+               Event{
+                       Kind: "echo",
+                       Text: "**raw result**",
+               },
+               Event{
+                       Kind: "error",
+                       Text: "**raw error**",
+               },
+               Event{
+                       Kind: "notice",
+                       Text: "**raw notice**",
+               },
+               Event{
+                       Kind:   "user",
+                       Text:   "**raw input**",
+                       Source: "telegram",
+               },
+               Event{
+                       Kind: "ready",
+                       Text: "**raw agent**",
+               },
+               Event{
+                       Kind: "subagent",
+                       Text: "BlueBird",
+               },
+               Event{Kind: "busy"},
+               Event{Kind: "idle"},
+       )
+
+       require.Equal(t, "[working]\n\x1b[1mfirst next\x1b[0m\n\x1b[1mlast\x1b[0m\n[BlueBird]\n\x1b[1mReport\x1b[0m\n"+
+               "[tool] **raw tool**\n[result] **raw result**\n[error] **raw error**\n**raw notice**\n[telegram] **raw input**\n"+
+               "[connected to **raw agent**]\n[BlueBird] started\n[working]\n[done]\n> ", out.String())
+}
+
+func TestTerminalZeroWidthUsesEightyAndReadsChanges(t *testing.T) {
+       fd, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY, 0)
+       require.NoError(t, err)
+
+       defer func() {
+               require.NoError(t, unix.Close(fd))
+       }()
+
+       out := TerminalTestOutput{fd: fd}
+       require.NoError(t, unix.IoctlSetWinsize(fd, unix.TIOCSWINSZ, &unix.Winsize{Col: 0}))
+       require.Equal(t, 80, terminalColumns(&out))
+       terminalTestShowEvents(t, &out, Event{
+               Kind: "reply",
+               Text: strings.Repeat("a", 81),
+       })
+
+       require.Equal(t, strings.Repeat("a", 80)+"\na\n> ", out.String())
+       require.NoError(t, unix.IoctlSetWinsize(fd, unix.TIOCSWINSZ, &unix.Winsize{Col: 40}))
+       require.Equal(t, 40, terminalColumns(&out))
+}
+
+func TestTerminalPromptsAfterIdleEventsAndClearsBeforeShownEvents(t *testing.T) {
+       fd, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY, 0)
+       require.NoError(t, err)
+
+       defer func() {
+               require.NoError(t, unix.Close(fd))
+       }()
+
+       out := TerminalTestOutput{fd: fd}
+       terminalTestShowEvents(t, &out,
+               Event{
+                       Kind: "ready",
+                       Text: "Example",
+               },
+               Event{
+                       Kind:   "user",
+                       Source: terminalSource,
+                       Text:   "hidden",
+               },
+               Event{Kind: "unknown"},
+               Event{
+                       Kind: "notice",
+                       Text: "Idle notice",
+               },
+               Event{
+                       Kind: "work",
+                       Text: "**Idle report**",
+               },
+               Event{Kind: "busy"},
+               Event{
+                       Kind: "notice",
+                       Text: "Busy notice",
+               },
+               Event{
+                       Kind: "reply",
+                       Text: "**Answer**",
+               },
+               Event{Kind: "idle"},
+               Event{
+                       Kind: "error",
+                       Text: "Idle error",
+               },
+       )
+
+       require.Equal(t, "[connected to Example]\n> "+
+               "\r\x1b[2KIdle notice\n> "+
+               "\r\x1b[2K\x1b[1mIdle report\x1b[0m\n> "+
+               "\r\x1b[2K[working]\nBusy notice\n\x1b[1mAnswer\x1b[0m\n[done]\n> "+
+               "\r\x1b[2K[error] Idle error\n> ", out.String())
+}
+
+func TestTerminalNonterminalEventsHaveNoPromptOrClearCommands(t *testing.T) {
+       var out strings.Builder
+
+       terminalTestShowEvents(t, &out,
+               Event{
+                       Kind: "ready",
+                       Text: "Example",
+               },
+               Event{
+                       Kind: "notice",
+                       Text: "Idle notice",
+               },
+               Event{Kind: "busy"},
+               Event{
+                       Kind: "notice",
+                       Text: "Busy notice",
+               },
+               Event{
+                       Kind: "reply",
+                       Text: "**Answer**",
+               },
+               Event{Kind: "idle"},
+               Event{
+                       Kind: "notice",
+                       Text: "Idle again",
+               },
+       )
+
+       require.Equal(t, "[connected to Example]\nIdle notice\n[working]\nBusy notice\n**Answer**\n[done]\nIdle again\n", out.String())
+}
+
+// TerminalTestOutput captures text while it supplies a test terminal
+// descriptor.
+type TerminalTestOutput struct {
+       strings.Builder
+       fd int
+}
+
+func (o *TerminalTestOutput) Fd() uintptr {
+       return uintptr(o.fd)
+}
+
+func terminalTestShowEvents(t *testing.T, out io.Writer, events ...Event) {
+       t.Helper()
+
+       server, client := net.Pipe()
+       done := make(chan error, 1)
+       go func() {
+               done <- terminalShowEvents(client, out)
+       }()
+
+       encoder := json.NewEncoder(server)
+       for _, event := range events {
+               require.NoError(t, encoder.Encode(event))
+       }
+
+       require.NoError(t, server.Close())
+       require.NoError(t, <-done)
+       require.NoError(t, client.Close())
+}