okami.mom/albedo

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

Aly Raffaufperf(tui): reuse prepared startup dataed54422

main
6.1 KiB129 linesraw
1// A confirmed model switch must survive its following cap failure. Controlled
2// partial responses and stale deliveries cannot be produced deterministically
3// by the healthy daemon, so these tests isolate the UI state transition.
4package tui
5
6import (
7	"albedo/cli/internal/testwire"
8	"encoding/json"
9	"errors"
10	"net/http"
11	"testing"
12
13	"albedo/cli/internal/config"
14	"albedo/cli/internal/daemon"
15)
16
17func TestModelChangeRetainsConfirmedSelectionWhenCapFails(t *testing.T) {
18	for _, status := range []int{http.StatusBadRequest, http.StatusOK} {
19		t.Run(http.StatusText(status), func(t *testing.T) {
20			requests := 0
21			conn := commandTestConnection(t, func(w http.ResponseWriter, _ *http.Request) {
22				requests++
23				if requests == 1 {
24					writeSessionChange(w, "session", "confirmed", "provider")
25					return
26				}
27				if requests == 2 {
28					_ = json.NewEncoder(w).Encode(testwire.SettingsChange("providers", testwire.Settings()["providers"]))
29					return
30				}
31				w.WriteHeader(status)
32				_, _ = w.Write([]byte(`{}`))
33			})
34			session := &daemon.Session{ID: "session", Model: "old", Provider: "old-provider", Effort: "high", ETag: "\"session-a\""}
35			app := NewAppModel(conn, Bootstrap{Settings: daemon.Settings{Profiles: config.Profiles{ETag: "\"providers-a\"", Providers: map[string]config.Settings{"provider": {Protocol: "responses"}}}}}, session, "", false, nil)
36			app.SettingsETags = map[string]string{"models": "\"models-a\""}
37			t.Cleanup(app.Chat.Close)
38			app.State, app.ModelPicker.Saving, app.ModelGen = AppStateModelPicker, true, 1
39			raise := true
40			msg := app.changeModelCmd("requested", "provider", "", &raise, app.ModelGen, "provider/cap")().(modelChangedMsg)
41			if msg.Err == nil || msg.Selection == nil {
42				t.Fatalf("lost confirmed switch or following failure: %+v", msg)
43			}
44			if status == http.StatusOK {
45				var uncertain *daemon.UncertainOutcomeError
46				var protocol *daemon.ProtocolError
47				if !errors.As(msg.Err, &uncertain) || !errors.As(msg.Err, &protocol) {
48					t.Fatalf("invalid cap response lost typed uncertainty: %v", msg.Err)
49				}
50			}
51			updated, cmd := app.Update(msg)
52			app = updated.(*AppModel)
53			if cmd != nil || requests != 3 {
54				t.Fatalf("cap failure caused another operation: requests=%d command=%v", requests, cmd != nil)
55			}
56			if app.ActiveSession.Model != "confirmed" || app.Chat.Model != "confirmed" || app.ActiveSession.Provider != "provider" || app.Chat.Provider != "provider" || app.ActiveSession.Protocol != "responses" || app.ActiveSession.Effort != "" || app.Chat.Effort != "" {
57				t.Fatalf("did not apply confirmed switch: %+v", app.ActiveSession)
58			}
59			if app.State != AppStateChat || app.ModelPicker.Saving || len(app.Chat.Notices) != 1 {
60				t.Fatal("confirmed switch did not settle picker and surface the cap failure")
61			}
62		})
63	}
64}
65
66func TestModelChangeFailureDoesNotApplySelectionOrRaiseCap(t *testing.T) {
67	for _, status := range []int{http.StatusBadRequest, http.StatusOK} {
68		t.Run(http.StatusText(status), func(t *testing.T) {
69			requests := 0
70			conn := commandTestConnection(t, func(w http.ResponseWriter, _ *http.Request) {
71				requests++
72				w.WriteHeader(status)
73				_, _ = w.Write([]byte(`{}`))
74			})
75			session := &daemon.Session{ID: "session", Model: "old", Provider: "provider", Effort: "low", ETag: "\"session-a\""}
76			app := NewAppModel(conn, Bootstrap{Settings: daemon.Settings{Profiles: config.Profiles{ETag: "\"providers-a\"", Providers: map[string]config.Settings{"provider": {Protocol: "responses"}}}}}, session, "", false, nil)
77			app.SettingsETags = map[string]string{"models": "\"models-a\""}
78			t.Cleanup(app.Chat.Close)
79			app.State, app.ModelPicker.Saving, app.ModelGen = AppStateModelPicker, true, 1
80			raise := true
81			msg := app.changeModelCmd("requested", "provider", "", &raise, app.ModelGen, "provider/cap")().(modelChangedMsg)
82			updated, cmd := app.Update(msg)
83			app = updated.(*AppModel)
84			if msg.Err == nil || msg.Selection != nil || requests != 1 || cmd != nil {
85				t.Fatalf("failed switch proceeded to cap or invented confirmation: %+v requests=%d", msg, requests)
86			}
87			if app.ActiveSession.Model != "old" || app.Chat.Model != "old" || app.ActiveSession.Effort != "low" || app.Chat.Effort != "low" || app.State != AppStateModelPicker || app.ModelPicker.Saving || app.ModelPicker.Error == "" {
88				t.Fatal("failed switch changed confirmed state or failed to settle picker")
89			}
90		})
91	}
92}
93
94func TestModelChangeIgnoresStaleSessionOrGeneration(t *testing.T) {
95	for _, scenario := range []struct {
96		name, sessionID string
97		generation      int
98	}{
99		{"session", "other", 1},
100		{"generation", "session", 0},
101	} {
102		t.Run(scenario.name, func(t *testing.T) {
103			app := NewAppModel(daemon.NewConnection(daemon.ConnectionSnapshot{Port: 1}, nil), Bootstrap{Settings: daemon.Settings{Profiles: config.Profiles{}}}, &daemon.Session{ID: "session", Model: "old"}, "", false, nil)
104			t.Cleanup(app.Chat.Close)
105			app.State, app.ModelPicker.Saving, app.ModelGen = AppStateModelPicker, true, 1
106			updated, cmd := app.Update(modelChangedMsg{
107				Selection: &daemon.ModelSelection{Model: "stale"},
108				Err:       errors.New("stale failure"), SessionID: scenario.sessionID, Gen: scenario.generation,
109			})
110			app = updated.(*AppModel)
111			if cmd != nil || app.ActiveSession.Model != "old" || app.Chat.Model != "old" || app.State != AppStateModelPicker || !app.ModelPicker.Saving || app.ModelPicker.Error != "" || len(app.Chat.Notices) != 0 {
112				t.Fatal("stale result changed current session, picker, or notices")
113			}
114		})
115	}
116}
117
118func TestModelChangePreservesScreenAfterLeavingPicker(t *testing.T) {
119	app := NewAppModel(daemon.NewConnection(daemon.ConnectionSnapshot{Port: 1}, nil), Bootstrap{Settings: daemon.Settings{Profiles: config.Profiles{}}}, &daemon.Session{ID: "session", Model: "old"}, "", false, nil)
120	t.Cleanup(app.Chat.Close)
121	app.State, app.ModelGen = AppStateSessionPicker, 1
122	updated, cmd := app.Update(modelChangedMsg{
123		Selection: &daemon.ModelSelection{Model: "confirmed"}, SessionID: "session", Gen: 1,
124	})
125	app = updated.(*AppModel)
126	if cmd != nil || app.ActiveSession.Model != "confirmed" || app.Chat.Model != "confirmed" || app.State != AppStateSessionPicker {
127		t.Fatal("confirmed switch failed to update its session or changed an unrelated screen")
128	}
129}