X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/d50c22617aafed0d3d87bf4596e933b343a10b60..2124c559da8f164a43a3da987b399b6ac6125035:/terminal_test.go?ds=inline diff --git a/terminal_test.go b/terminal_test.go index d86f88a..7b68edb 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -1,14 +1,16 @@ package main import ( + "encoding/json" "io" "net" + "os" "path/filepath" "strings" "testing" - "time" "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" ) func TestClientEndsWhenTheDaemonEnds(t *testing.T) { @@ -59,6 +61,16 @@ func TestEventText(t *testing.T) { event: Event{Kind: "user", Text: "from the phone", Source: "telegram"}, text: "[telegram] from the phone", }, + { + name: "start of a subagent", + event: Event{Kind: "subagent", Text: "GreenOtter"}, + text: "[GreenOtter] started", + }, + { + name: "report of a subagent whole", + 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: "long result cut", event: Event{Kind: "echo", Text: strings.Repeat("line\n", 12)}, @@ -73,67 +85,225 @@ func TestEventText(t *testing.T) { } } -// TerminalTestClient is a terminal client of a test daemon, with a pipe as its -// input. -type TerminalTestClient struct { - input *io.PipeWriter - done chan error - out *SyncBuffer +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()) } -// Type writes one line to the input of the client. -func (c *TerminalTestClient) Type(line string) { - _, err := io.WriteString(c.input, line+"\n") - if err != nil { - panic(err) - } +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)) } -// Output returns what the client showed. -func (c *TerminalTestClient) Output() string { - return c.out.String() +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()) } -// WaitFor waits until the client showed text. -func (c *TerminalTestClient) WaitFor(t *testing.T, text string) { - t.Helper() +func TestTerminalZeroWidthUsesEightyAndReadsChanges(t *testing.T) { + fd, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY, 0) + require.NoError(t, err) - require.Eventually(t, func() bool { - return strings.Contains(c.Output(), text) - }, 10*time.Second, time.Millisecond, "output so far: %q", c.Output()) + 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)) } -// Close ends the input and waits for the client. -func (c *TerminalTestClient) Close() error { - err := c.input.Close() - if err != nil { - return err - } +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)) + }() - return <-c.done + 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()) } -// terminalStartTestClient connects a client to the daemon at socket. -func terminalStartTestClient(t *testing.T, socket string) *TerminalTestClient { - t.Helper() +func TestTerminalNonterminalEventsHaveNoPromptOrClearCommands(t *testing.T) { + var out strings.Builder - conn, err := net.Dial("unix", socket) - require.NoError(t, err) + 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()) +} - reader, writer := io.Pipe() - client := &TerminalTestClient{ - input: writer, - done: make(chan error, 1), - out: &SyncBuffer{}, - } +// 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() { - client.done <- terminalSession(t.Context(), conn, reader, client.out) + done <- terminalShowEvents(client, out) }() - t.Cleanup(func() { - _ = writer.Close() - }) + encoder := json.NewEncoder(server) + for _, event := range events { + require.NoError(t, encoder.Encode(event)) + } - return client + require.NoError(t, server.Close()) + require.NoError(t, <-done) + require.NoError(t, client.Close()) }