okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffaufperf(tui): reuse prepared startup dataed54422

main
7.4 KiB176 linesraw
1// Delayed command delivery and discarded screen instances cannot be scheduled
2// deterministically through the terminal E2E harness. These tests exercise real
3// API commands and UI updates while controlling only response timing.
4package tui
5
6import (
7	"context"
8	"encoding/json"
9	"errors"
10	"io"
11	"net/http"
12	"strings"
13	"sync"
14	"sync/atomic"
15	"testing"
16
17	"albedo/cli/internal/config"
18	"albedo/cli/internal/daemon"
19	tea "charm.land/bubbletea/v2"
20	"github.com/charmbracelet/x/ansi"
21)
22
23func TestReopenedModelPickerRejectsEarlierCatalogAndCancelsItsRead(t *testing.T) {
24	started, cancelled := make(chan struct{}), make(chan struct{})
25	conn := commandTestConnection(t, func(w http.ResponseWriter, r *http.Request) {
26		if r.URL.Query().Get("endpoint") == "https://old.example/v1" {
27			close(started)
28			<-r.Context().Done()
29			close(cancelled)
30			return
31		}
32		_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{map[string]any{
33			"id": "fresh-model", "label": "Fresh model", "efforts": []any{},
34			"default_context_tokens": nil, "effective_context_tokens": nil, "max_context_tokens": nil,
35			"max_output_tokens": nil, "input_modalities": []any{}, "image_edge": nil,
36			"raised": false, "cap_key": "fresh-model", "cache_policy": map[string]any{"ttl_seconds": nil, "source": nil},
37			"source": "provider", "observed_at": nil,
38		}}, "next": nil})
39	})
40	profiles := config.Profiles{Providers: map[string]config.Settings{"work": {Extension: "openai", BaseURL: "https://old.example/v1"}}}
41	old := NewModelPickerModel(conn, profiles, "", "work", "")
42	t.Cleanup(old.Close)
43	delivered := make(chan tea.Msg, 1)
44	read := old.listCmd(old.catalogs[0])
45	go func() { delivered <- read() }()
46	awaitCommandSignal(t, started)
47	old.Close()
48	awaitCommandSignal(t, cancelled)
49	profiles.Providers["work"] = config.Settings{Extension: "openai", BaseURL: "https://new.example/v1"}
50	current := NewModelPickerModel(conn, profiles, "", "work", "")
51	t.Cleanup(current.Close)
52	current, _ = current.Update(awaitCommandSignal(t, delivered))
53	if !current.catalogs[0].loading {
54		t.Fatal("discarded request settled the replacement picker")
55	}
56	current, _ = current.Update(current.listCmd(current.catalogs[0])())
57	if view := current.View(); !strings.Contains(view, "fresh-model") || current.catalogs[0].failed {
58		t.Fatalf("replacement catalog not delivered: %s", view)
59	}
60}
61
62type heldFolderSource struct {
63	*fakeFolders
64	entered, cancelled chan struct{}
65}
66
67func (source heldFolderSource) List(ctx context.Context, _ string) (daemon.FolderList, error) {
68	close(source.entered)
69	<-ctx.Done()
70	close(source.cancelled)
71	return daemon.FolderList{}, ctx.Err()
72}
73
74func TestReopenedFolderPickerRejectsEarlierBrowseAndStopsItsRead(t *testing.T) {
75	source := heldFolderSource{fakeFolders: &fakeFolders{}, entered: make(chan struct{}), cancelled: make(chan struct{})}
76	old := NewFolderPicker(source, daemon.Session{Workspace: "/workspace"}, nil)
77	t.Cleanup(old.Close)
78	previousGeneration := old.sessionsGeneration
79	old.setQuery("/workspace/")
80	request := old.fetch()
81	delivered := make(chan tea.Msg, 1)
82	go func() { delivered <- request() }()
83	awaitCommandSignal(t, source.entered)
84	old.Close()
85	awaitCommandSignal(t, source.cancelled)
86	current := NewFolderPicker(&fakeFolders{}, daemon.Session{Workspace: "/workspace"}, nil)
87	t.Cleanup(current.Close)
88	current.setQuery("/workspace/")
89	current, _ = current.Update(awaitCommandSignal(t, delivered))
90	current, _ = current.Update(folderListMsg{Gen: previousGeneration, Path: "/workspace", List: daemon.FolderList{Path: "/workspace", Entries: []daemon.FolderEntry{{Name: "obsolete"}}}})
91	if strings.Contains(ansi.Strip(current.View()), "obsolete") || current.listings["/workspace"] != nil {
92		t.Fatal("discarded browse populated the replacement picker")
93	}
94	current, _ = current.Update(folderListMsg{Gen: current.sessionsGeneration, Path: "/workspace", List: daemon.FolderList{Path: "/workspace", Entries: []daemon.FolderEntry{{Name: "fresh"}}}})
95	if !strings.Contains(ansi.Strip(current.View()), "fresh") {
96		t.Fatal("current browse was not delivered")
97	}
98}
99
100func TestClosedLoginCancelsLateStartupWithoutOpeningBrowser(t *testing.T) {
101	started, release := make(chan struct{}), make(chan struct{})
102	var releaseOnce sync.Once
103	releaseProvider := func() { releaseOnce.Do(func() { close(release) }) }
104	t.Cleanup(releaseProvider)
105	cancelled := make(chan string, 1)
106	created := make(chan string, 1)
107	var opened atomic.Bool
108	conn := commandTestConnection(t, func(w http.ResponseWriter, r *http.Request) {
109		id := strings.TrimPrefix(r.URL.Path, "/auth/logins/")
110		if r.Method == http.MethodDelete {
111			cancelled <- id
112			w.WriteHeader(http.StatusNoContent)
113			return
114		}
115		if r.Method != http.MethodPut {
116			t.Errorf("unexpected %s %s", r.Method, r.URL)
117			return
118		}
119		created <- id
120		close(started)
121		<-release
122		w.Header().Set("ETag", `"observed"`)
123		w.WriteHeader(http.StatusCreated)
124		_ = json.NewEncoder(w).Encode(map[string]any{"id": id, "provider": "provider", "url": "https://login.example/authorize", "expires_at": "2099-01-01T00:00:00Z", "state": "waiting", "instructions": nil, "progress": "waiting", "accounts": []any{}, "failure": nil})
125	})
126	app := NewAppModel(conn, Bootstrap{Settings: daemon.Settings{Profiles: config.Profiles{}}}, nil, "", false, func(string) { opened.Store(true) })
127	app.State = AppStateLogin
128	app.Login = NewLoginModel(conn, "", app.openBrowser)
129	app.Login.Provider = "provider"
130	delivered := make(chan tea.Msg, 1)
131	command := app.Login.beginLoginCmd()
132	go func() { delivered <- command() }()
133	awaitCommandSignal(t, started)
134	expectedID := awaitCommandSignal(t, created)
135	app.Login.Close()
136	app.State = AppStateSessionPicker
137	releaseProvider()
138	result := awaitCommandSignal(t, delivered).(signInStartedMsg)
139	updated, cleanup := app.Update(result)
140	app = updated.(*AppModel)
141	if cleanup != nil {
142		t.Fatal("completed cleanup was scheduled a second time")
143	}
144	if id := awaitCommandSignal(t, cancelled); id != expectedID {
145		t.Fatalf("cancelled a different login: %s", id)
146	}
147	if opened.Load() || app.State != AppStateSessionPicker {
148		t.Fatal("late login opened a browser or took over the screen")
149	}
150}
151
152func TestLateProviderMutationKeepsUncertaintyWithoutSelectingReplacementFlow(t *testing.T) {
153	conn := daemon.NewConnection(daemon.ConnectionSnapshot{Port: 1}, nil)
154	app := NewAppModel(conn, Bootstrap{Settings: daemon.Settings{Profiles: config.Profiles{}}}, nil, "", false, nil)
155	previous := NewLoginModel(conn, "", nil)
156	conn.HTTPClient().Transport = catalogTransport(func(*http.Request) (*http.Response, error) { return nil, io.ErrUnexpectedEOF })
157	// Capture the observed validator before creating the mutation command.
158	previous.Profiles.ETag = `"seen"`
159	command := previous.saveProviderCmd("work", config.Settings{Extension: "openai", Protocol: "responses", BaseURL: "https://provider.example/v1", Model: "model"})
160	previous.Close()
161	app.Login = NewLoginModel(conn, "", nil)
162	t.Cleanup(func() { app.Login.Close() })
163	app.State = AppStateLogin
164	result := command().(providerSavedMsg)
165	if _, ok := errors.AsType[*daemon.UncertainOutcomeError](result.Err); !ok {
166		t.Fatalf("missing uncertainty: %v", result.Err)
167	}
168	updated, followup := app.Update(result)
169	app = updated.(*AppModel)
170	if followup != nil || app.Login.Name != "" || app.State != AppStateLogin || len(app.Notices) == 0 {
171		t.Fatal("late mutation was hidden or selected the replacement login flow")
172	}
173	if !strings.Contains(ansi.Strip(app.content()), "may have been accepted") {
174		t.Fatal("uncertain outcome was not visible in the replacement login screen")
175	}
176}