//go:build unix // Shell endpoint URL construction belongs to the Go client. Backend-only E2E // tests missed a missing leading slash that made every TUI shell request fail. package e2e import ( "os" "path/filepath" "strings" "testing" "time" "albedo/cli/internal/daemon" "albedo/cli/internal/tui" tea "charm.land/bubbletea/v2" ) func TestTUIShellCommandLifecycleThroughDaemon(t *testing.T) { profile := providerRoute(t, echoReply) t.Parallel() workspace := t.TempDir() // Even an assertion failure must let an admitted command exit. t.Cleanup(func() { _ = os.WriteFile(filepath.Join(workspace, "release"), nil, 0o600) }) session := daemonSession(t, newSession(t, workspace)) d := driveTUI(t, &session) t.Cleanup(func() { d.App.Chat.Close() }) client := daemon.NewChatClient(conn(t), session.ID) d.Dispatch(tui.ChatStatusPollMsg{SessionID: session.ID, Generation: d.App.Chat.Generation}) d.App.Chat.TextArea.SetValue("!printf 'tui-shell-transport\\n'; while [ ! -f release ]; do sleep 0.01; done") reply := d.Key(tea.KeyEnter) submitted, ok := reply.(tui.ChatShellSubmitMsg) if !ok || submitted.Err != nil { t.Fatalf("composer shell submission failed: %v (%#v)\n%s", submitted.Err, reply, d.View()) } d.Dispatch(submitted) id := submitted.Command.ID readUntil := func(want string) daemon.ShellCommand { t.Helper() deadline := time.Now().Add(10 * time.Second) for { command, err := client.GetShellCommand(t.Context(), id) if err != nil { t.Fatal(err) } if command.Status == want && strings.Contains(command.Output, "tui-shell-transport") { return command } if time.Now().After(deadline) { t.Fatalf("shell never reached %s with output: %#v", want, command) } time.Sleep(20 * time.Millisecond) } } running := readUntil("running") d.Update(tui.ChatStreamEventMsg{SessionID: session.ID, Generation: d.App.Chat.Generation, Event: daemon.StreamEvent{Type: daemon.EventShellCommand, ShellCommand: &running}}) d.Dispatch(tea.KeyPressMsg{Code: 'o', Mod: tea.ModCtrl | tea.ModShift}) if entry := d.App.Chat.History.Entries()[0]; !entry.ShellOutputLoaded || entry.ShellExpandedOutput != "tui-shell-transport\n" { t.Fatalf("shell output shortcut did not expand the captured preview: %#v", entry) } page, err := client.GetShellCommandOutput(t.Context(), id, 0, 65536) if err != nil || page.Text != "tui-shell-transport\n" { t.Fatalf("shell output page = %#v, err = %v", page, err) } reply = d.Send(tea.KeyPressMsg{Code: 'x', Mod: tea.ModCtrl | tea.ModShift}) cancelled, ok := reply.(tui.ChatShellCancelMsg) if !ok || cancelled.Err != nil { t.Fatalf("shell cancel shortcut failed: %#v", reply) } d.Dispatch(cancelled) stopped := readUntil("cancelled") d.Dispatch(tui.ChatShellCancelMsg{Command: stopped, SessionID: session.ID, Generation: d.App.Chat.Generation}) if view := d.View(); !strings.Contains(view, "tui-shell-transport") || !strings.Contains(view, "queued for agent") || strings.Contains(view, "not confirmed") { t.Fatalf("shell result did not reach the TUI:\n%s", view) } if d.App.Chat.TextArea.Value() != "" || len(d.App.Chat.History.Entries()) != 1 { t.Fatalf("shell submission restored a draft or duplicated its receipt:\n%s", d.View()) } if got := len(suite.provider.requests(profile)); got != 0 { t.Fatalf("an idle shell command started %d model requests", got) } }