package tui import ( "errors" "strings" "testing" tea "charm.land/bubbletea/v2" "albedo/cli/internal/daemon" "github.com/charmbracelet/x/ansi" ) func TestParseShellInputIsLiteralHumanPrefix(t *testing.T) { for _, tc := range []struct { input, command string ok bool }{ {"!echo \"foobar\"", "echo \"foobar\"", true}, {"! pwd ", "pwd", true}, {"echo !pwd", "", false}, {"/run", "", false}, {"/run is broken", "", false}, } { got, ok := parseShellInput(tc.input) if got != tc.command || ok != tc.ok { t.Errorf("parseShellInput(%q) = %q,%v; want %q,%v", tc.input, got, ok, tc.command, tc.ok) } } } func TestFailedShellSubmissionRestoresDraftAndStableID(t *testing.T) { conn := daemon.NewConnection(daemon.ConnectionSnapshot{Port: 1}, nil) session := &daemon.Session{ID: "session", Workspace: "/work"} m := NewChatModel(session, daemon.NewChatClient(conn, session.ID)) m.shellIDsByCommand = map[string]string{"echo hi": "01a0fcea-2b00-7000-8000-000000000001"} m.shellSubmitting = true m.TextArea.Reset() m, _ = m.Update(ChatShellSubmitMsg{Err: errTestShell, SessionID: session.ID, Generation: m.Generation, OriginalInput: "!echo hi"}) if m.TextArea.Value() != "!echo hi" { t.Fatalf("draft = %q", m.TextArea.Value()) } if m.shellIDsByCommand["echo hi"] == "" { t.Fatal("uncertain retry ID was discarded") } } func TestShellEventDeduplicatesByCommandIDAndSanitizesOutput(t *testing.T) { conn := daemon.NewConnection(daemon.ConnectionSnapshot{Port: 1}, nil) session := &daemon.Session{ID: "session", Workspace: "/work"} m := NewChatModel(session, daemon.NewChatClient(conn, session.ID)) first := daemon.ShellCommand{ID: "shell-id", Command: "echo hi", Status: "running"} m.handleStreamEvent(daemon.StreamEvent{Type: daemon.EventShellCommand, ShellCommand: &first}) second := first second.Status, second.Output = "completed", "safe\x1b]52;c;clipboard\a\n" second.OutputBytes = int64(len(second.Output)) code := int64(0) second.ExitCode = &code m.handleStreamEvent(daemon.StreamEvent{Type: daemon.EventShellCommand, ShellCommand: &second}) if m.History.Len() != 1 { t.Fatalf("history has %d shell rows", m.History.Len()) } entry := m.History.Entries()[0] rows := strings.Join(m.Renderer.shellCard(entry, 80), "\n") if strings.Contains(rows, "\x1b]52") || strings.Contains(ansi.Strip(rows), "clipboard") { t.Fatalf("terminal control text survived rendering: %q", rows) } } // TestShellDeliveryReducerPreservesAckAndExecutionState catches out-of-order // event/history/PUT snapshots that an E2E backend test cannot deterministically // force through the terminal client's event reducer. func TestShellDeliveryReducerPreservesAckAndExecutionState(t *testing.T) { m := newTestChatModel(t, &daemon.Session{ID: "session", Workspace: "/work"}) pending := daemon.ShellCommand{ID: "shell-id", Command: "echo hi", Status: "completed", Delivery: "pending"} m.handleStreamEvent(daemon.StreamEvent{Type: daemon.EventShellCommand, ShellCommand: &pending}) m.upsertShellCommand(daemon.ShellCommand{ID: pending.ID, Command: pending.Command, Status: "completed", Delivery: "delivered"}) m.Follow = false m.SetSize(100, 24) if !strings.Contains(ansi.Strip(m.View()), "sent to agent") { t.Fatalf("ACK not visible in canonical chat view: %q", ansi.Strip(m.View())) } index := m.History.Find(pending.ID) entry := m.History.Entries()[index] rows := strings.Join(m.Renderer.shellCard(entry, 80), "\n") if !strings.Contains(ansi.Strip(rows), "sent to agent") { t.Fatalf("ACK not visible in shell card: %q", ansi.Strip(rows)) } // An older history/PUT snapshot cannot undo an ACK or resurrect execution. m.upsertShellCommand(daemon.ShellCommand{ID: pending.ID, Command: pending.Command, Status: "running", Delivery: "pending", Output: "stale"}) entry = m.History.Entries()[m.History.Find(pending.ID)] if entry.ShellCommand.Status != "completed" || entry.ShellCommand.Delivery != "delivered" { t.Fatalf("stale snapshot regressed shell state: %#v", entry.ShellCommand) } if strings.Contains(ansi.Strip(strings.Join(m.Renderer.shellCard(entry, 80), "\n")), "running ยท click to cancel") { t.Fatal("stale snapshot restored running/cancel state") } m.handleStreamEvent(daemon.StreamEvent{Type: daemon.EventReset, Reason: "reconnect", ShellCommands: []daemon.ShellCommand{{ID: pending.ID, Command: pending.Command, Status: "completed", Delivery: "delivered"}}}) entry = m.History.Entries()[m.History.Find(pending.ID)] if entry.ShellCommand.Delivery != "delivered" { t.Fatalf("reset lost delivery receipt: %#v", entry.ShellCommand) } } var errTestShell = errors.New("shell rejected") func TestGrowingExpandedShellOutputIsRefreshed(t *testing.T) { conn := daemon.NewConnection(daemon.ConnectionSnapshot{Port: 1}, nil) session := &daemon.Session{ID: "session", Workspace: "/work"} m := NewChatModel(session, daemon.NewChatClient(conn, session.ID)) first := daemon.ShellCommand{ID: "shell-growing", Command: "yes", Status: "running", Output: "a", OutputBytes: 1} m.upsertShellCommand(first) entry := m.History.Entries()[0] entry.ShellOutputLoaded, entry.ShellExpandedOutput = true, "a" m.History.Replace(entry) first.Output, first.OutputBytes = "ab", 2 m.upsertShellCommand(first) entry = m.History.Entries()[0] if entry.ShellOutputLoaded || entry.ShellExpandedOutput != "" { t.Fatalf("stale expansion retained: %#v", entry) } } func TestHumanComposerRoutesBangWithoutCreatingModelTurn(t *testing.T) { conn := daemon.NewConnection(daemon.ConnectionSnapshot{Port: 1}, nil) session := &daemon.Session{ID: "session", Workspace: "/work"} m := NewChatModel(session, daemon.NewChatClient(conn, session.ID)) var commands []tea.Cmd m.submitInput(`!echo "foobar"`, &commands) if !m.shellSubmitting || len(commands) != 1 || len(m.pendingUsers) != 0 { t.Fatalf("shell route state submitting=%v commands=%d model-turns=%d", m.shellSubmitting, len(commands), len(m.pendingUsers)) } if command, ok := parseShellInput(`/run is broken`); ok || command != "" { t.Fatalf("slash prose became shell command: %q", command) } }