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) {
})
}
}
+
+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())
+}