okami.mom/albedo

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

Aly Raffaufperf(tui): reuse prepared startup dataed54422

main
11.5 KiB270 linesraw
1// Delayed controlled responses verify that commands retain captured UI inputs.
2package tui
3
4import (
5	"albedo/cli/internal/config"
6	"albedo/cli/internal/daemon"
7	"albedo/cli/internal/testwire"
8	tea "charm.land/bubbletea/v2"
9	"encoding/json"
10	"net"
11	"net/http"
12	"net/http/httptest"
13	"reflect"
14	"sync"
15	"testing"
16	"time"
17)
18
19func commandTestConnection(t *testing.T, handler http.HandlerFunc) *daemon.Connection {
20	t.Helper()
21	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22		if r.URL.Path == "/server" {
23			_, _ = w.Write([]byte(testwire.Server))
24			return
25		}
26		handler(w, r)
27	}))
28	t.Cleanup(server.Close)
29	conn, err := daemon.Attach(t.Context(), daemon.ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port, Token: "token"}, nil)
30	if err != nil {
31		t.Fatal(err)
32	}
33	t.Cleanup(conn.HTTPClient().CloseIdleConnections)
34	return conn
35}
36func awaitCommandSignal[T any](t *testing.T, ch <-chan T) T {
37	t.Helper()
38	select {
39	case value := <-ch:
40		return value
41	case <-time.After(5 * time.Second):
42		t.Fatal("timed out waiting for command")
43		var zero T
44		return zero
45	}
46}
47
48// A partial deletion must keep an undeleted row until an authoritative refresh.
49// A healthy daemon cannot reliably force a teardown failure during this update.
50func TestPartialDeletionKeepsUndeletedSessionAndOutcomeAfterRefresh(t *testing.T) {
51	app := NewAppModel(daemon.NewConnection(daemon.ConnectionSnapshot{Port: 1}, nil), Bootstrap{Settings: daemon.Settings{Profiles: config.Profiles{}}}, nil, "", false, nil)
52	app.State = AppStateSessionPicker
53	app.Sessions = []daemon.Session{{ID: "root"}, {ID: "deleted"}}
54	app.updateSessionPickerItems()
55	result := daemon.DeletionResult{State: "partial", Deleted: 1, Remaining: 1, DeletedIDs: []string{"deleted"}, Message: "Deleted 1 sessions; 1 remain. root: kernel teardown failed"}
56	updated, command := app.Update(sessionDeletedMsg{ID: "root", Result: &result})
57	app = updated.(*AppModel)
58	if command == nil || len(app.Sessions) != 1 || app.Sessions[0].ID != "root" || app.SessionPicker.notice != result.Message {
59		t.Fatalf("partial deletion invented completion: command nil=%v rows=%d id=%q notice=%q wanted=%q", command == nil, len(app.Sessions), app.Sessions[0].ID, app.SessionPicker.notice, result.Message)
60	}
61	updated, _ = app.Update(sessionsLoadedMsg{Gen: app.SessionGen, Sessions: app.Sessions, Notice: result.Message})
62	app = updated.(*AppModel)
63	if app.SessionPicker.notice != result.Message {
64		t.Fatal("refresh erased the partial operation outcome")
65	}
66}
67func TestModelChangeCapturesProviderAndCapBeforeExecution(t *testing.T) {
68	for _, failure := range []string{"", "switch", "default", "cap"} {
69		t.Run(failure, func(t *testing.T) {
70			var paths []string
71			var switchBody map[string]string
72			var capBody map[string]map[string]bool
73			conn := commandTestConnection(t, func(w http.ResponseWriter, r *http.Request) {
74				paths = append(paths, r.URL.String())
75				if r.URL.Path == "/sessions/original" {
76					if r.Header.Get("If-Match") != "\"session-a\"" {
77						t.Errorf("changed session validator %s", r.Header.Get("If-Match"))
78					}
79					_ = json.NewDecoder(r.Body).Decode(&switchBody)
80					if failure == "switch" {
81						w.WriteHeader(400)
82						return
83					}
84					writeSessionChange(w, "original", "resolved", "new")
85					return
86				}
87				group := r.URL.Query().Get("group")
88				if r.URL.Path != "/settings" {
89					t.Errorf("unexpected route %s", r.URL)
90				}
91				if failure == group || failure == "default" && group == "providers" || failure == "cap" && group == "models" {
92					w.WriteHeader(400)
93					return
94				}
95				settings := testwire.Settings()
96				if group == "models" {
97					_ = json.NewDecoder(r.Body).Decode(&capBody)
98				}
99				_ = json.NewEncoder(w).Encode(testwire.SettingsChange(group, settings[group]))
100			})
101			session := &daemon.Session{ID: "original", Provider: "old", ETag: "\"session-a\""}
102			app := &AppModel{Conn: conn, ActiveSession: session, Profiles: config.Profiles{ETag: "\"providers-a\"", Providers: map[string]config.Settings{"new": {Protocol: "responses"}}}, SettingsETags: map[string]string{"models": "\"models-a\""}}
103			raised := true
104			cmd := app.changeModelCmd("chosen", "new", "high", &raised, 7, "provider/cap")
105			session.ID, session.Provider = "changed", "new"
106			raised = false
107			app.Conn, app.ActiveSession = nil, nil
108			msg := cmd().(modelChangedMsg)
109			if msg.SessionID != "original" || msg.Gen != 7 || (msg.Err != nil) != (failure != "") {
110				t.Fatalf("captured result changed: %+v", msg)
111			}
112			if !reflect.DeepEqual(switchBody, map[string]string{"model": "chosen", "provider_profile": "new", "effort": "high"}) {
113				t.Fatalf("switch inputs changed: %v", switchBody)
114			}
115			expected := 1
116			if failure != "switch" {
117				expected = 2
118				if failure != "default" {
119					expected = 3
120				}
121			}
122			if len(paths) != expected {
123				t.Fatalf("wrong partial workflow %v", paths)
124			}
125			if failure == "switch" {
126				if msg.Selection != nil {
127					t.Fatal("failed switch installed")
128				}
129			} else if msg.Selection == nil || msg.Selection.Model != "resolved" {
130				t.Fatal("confirmed switch lost")
131			}
132			if failure == "" && !capBody["raised_caps"]["provider/cap"] {
133				t.Fatalf("cap input changed %v", capBody)
134			}
135		})
136	}
137}
138func TestSessionCommandKeepsSessionWhileRequestIsRunning(t *testing.T) {
139	started, release := make(chan struct{}), make(chan struct{})
140	var once sync.Once
141	releaseRequest := func() { once.Do(func() { close(release) }) }
142	conn := commandTestConnection(t, func(w http.ResponseWriter, r *http.Request) {
143		if r.URL.Path != "/models" || r.URL.Query().Get("model") != "current" {
144			t.Errorf("wrong effort metadata %s", r.URL)
145		}
146		close(started)
147		<-release
148		_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{testwire.Model("current")}, "next": nil})
149	})
150	t.Cleanup(releaseRequest)
151	session := &daemon.Session{ID: "original", Model: "current"}
152	app := &AppModel{Conn: conn, ActiveSession: session}
153	cmd := app.executeCommandCmd("/effort", "", 9)
154	done := make(chan tea.Msg, 1)
155	go func() { done <- cmd() }()
156	awaitCommandSignal(t, started)
157	session.ID = "changed"
158	app.Conn, app.ActiveSession = nil, nil
159	releaseRequest()
160	msg := awaitCommandSignal(t, done).(commandExecutedMsg)
161	if msg.Err != nil || msg.SessionID != "original" || msg.Gen != 9 || !reflect.DeepEqual(msg.Available, []string{"low", "high"}) {
162		t.Fatalf("result used changed app state %+v", msg)
163	}
164}
165func TestStatusAndGlancesUseOneCapturedSessionSnapshot(t *testing.T) {
166	var paths []string
167	conn := commandTestConnection(t, func(w http.ResponseWriter, r *http.Request) {
168		paths = append(paths, r.URL.Path)
169		if r.URL.RawQuery != "tail=0" {
170			t.Errorf("state polling repeated history reads: %s", r.URL)
171		}
172		session := protocolSession("original", generationA, 0)
173		session["kernel"] = map[string]any{"state": "attached", "build": "observed-build", "stale": true, "staleness_reasons": []any{map[string]any{"code": "build_changed", "detail": "A newer kernel is available."}}, "live_job_count": 3, "running_jobs": []any{map[string]any{"id": "job1", "pid": 123, "command": "sleep 10", "started_at": 0}}, "stage": nil, "instance_id": "observed-kernel"}
174		session["glances"] = []any{map[string]any{"extension": "first", "title": "First", "rows": []any{}, "url": "/extensions/first/items"}, map[string]any{"extension": "second", "title": "Second", "rows": []any{}, "url": "/extensions/second/items"}}
175		_ = json.NewEncoder(w).Encode(session)
176	})
177	session := &daemon.Session{ID: "original"}
178	chat := NewChatModel(session, daemon.NewChatClient(conn, session.ID))
179	t.Cleanup(chat.Close)
180	cmd := chat.statusCmd()
181	session.ID = "changed"
182	chat.SessionID = "changed"
183	msg := cmd().(ChatStatusMsg)
184	if !reflect.DeepEqual(paths, []string{"/sessions/original"}) || msg.Err != nil || msg.SessionID != "original" || msg.Status == nil || len(msg.Glances) != 2 {
185		t.Fatalf("glance capture changed %+v %v", msg, paths)
186	}
187	chat.SessionID = "original"
188	chat.Glances = []PageGlance{{Title: "confirmed"}}
189	app := &AppModel{ActiveSession: &daemon.Session{ID: "original"}, Chat: chat}
190	updated, refresh := app.Update(PageViewChangedMsg{})
191	app = updated.(*AppModel)
192	updated, _ = app.Update(msg)
193	app = updated.(*AppModel)
194	if refresh == nil || len(app.Chat.Glances) != 1 || app.Chat.Glances[0].Title != "confirmed" {
195		t.Fatal("an earlier state read overwrote the acknowledged page change")
196	}
197	updated, _ = app.Update(refresh().(ChatStatusMsg))
198	app = updated.(*AppModel)
199	if len(app.Chat.Glances) != 2 {
200		t.Fatal("the state read after the page change was discarded")
201	}
202	phase := daemon.PhaseModel
203	app.Chat.handleStreamEvent(daemon.StreamEvent{Type: "status", Status: &daemon.AgentStatus{Phase: &phase, Running: true}})
204	status := app.Chat.Status
205	if status.Phase == nil || *status.Phase != phase || !status.Running || status.KernelLink != "attached" || !status.KernelStale || status.KernelJobs == nil || *status.KernelJobs != 3 || len(status.RunningJobs) != 1 || status.RunningJobs[0].Command != "sleep 10" || status.KernelBuild == nil || *status.KernelBuild != "observed-build" || status.KernelInstanceID == nil || *status.KernelInstanceID != "observed-kernel" {
206		t.Fatalf("lightweight live status lost the last captured kernel observation: %+v", status)
207	}
208}
209func TestUIPatchCapturesValuesAndObservedValidator(t *testing.T) {
210	for _, sessionID := range []string{"", "original"} {
211		t.Run(sessionID, func(t *testing.T) {
212			var got map[string]any
213			conn := commandTestConnection(t, func(w http.ResponseWriter, r *http.Request) {
214				if r.Method != "PATCH" || r.Header.Get("If-Match") != "\"seen\"" {
215					t.Errorf("conditional edit changed %s %v", r.Method, r.Header)
216				}
217				_ = json.NewDecoder(r.Body).Decode(&got)
218				if sessionID == "" {
219					if r.URL.String() != "/settings?group=ui" {
220						t.Errorf("wrong route %s", r.URL)
221					}
222					_ = json.NewEncoder(w).Encode(testwire.SettingsChange("ui", testwire.Settings()["ui"]))
223				} else {
224					if r.URL.String() != "/sessions/original?view=configuration" {
225						t.Errorf("wrong route %s", r.URL)
226					}
227					writeSessionChange(w, "original", "current", "provider")
228				}
229			})
230			app := &AppModel{Conn: conn}
231			app.UI.ETag = "\"seen\""
232			first, second := true, false
233			patch := daemon.UIPreferencesPatch{Thinking: &first, Tools: &second}
234			if sessionID != "" {
235				patch = daemon.UIPreferencesPatch{Pinned: &first, Archived: &second, ETag: "\"seen\""}
236			}
237			cmd := app.patchUICmd(sessionID, patch, 5)
238			first, second = false, true
239			app.Conn = nil
240			msg := cmd().(uiSavedMsg)
241			if msg.Err != nil || msg.Gen != 5 {
242				t.Fatalf("patch failed %+v", msg)
243			}
244			if sessionID != "" {
245				got = got["preferences"].(map[string]any)
246			}
247			firstKey, secondKey := "thinking", "tools"
248			if sessionID != "" {
249				firstKey, secondKey = "pinned", "archived"
250			}
251			if got[firstKey] != true || got[secondKey] != false {
252				t.Fatalf("captured values changed %v", got)
253			}
254		})
255	}
256}
257func TestPreviewCallbackKeepsConnectionAcrossAppChanges(t *testing.T) {
258	conn := commandTestConnection(t, func(w http.ResponseWriter, r *http.Request) {
259		if r.URL.String() != "/sessions/requested/history?limit=16" {
260			t.Errorf("unexpected preview %s", r.URL)
261		}
262		_ = json.NewEncoder(w).Encode(map[string]any{"high_water": 1, "items": []any{protocolEntry("e", "user", "hello", 1)}, "older": nil, "newer": nil})
263	})
264	app := NewAppModel(conn, Bootstrap{Settings: daemon.Settings{Profiles: config.Profiles{}}}, nil, "", false, nil)
265	app.Conn = nil
266	msg := app.SessionPicker.Fetch("requested")().(SessionPreviewMsg)
267	if msg.Err != nil || msg.ID != "requested" || len(msg.Preview.Items) != 1 || msg.Preview.Items[0].Preview != "hello" {
268		t.Fatalf("preview followed changed app state %+v", msg)
269	}
270}