okami.mom/albedo

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

Aly Raffauffix(stream): restore unfinished responses on session attachmenta229862

main
12.8 KiB320 linesraw
1// Malformed server and refresh races need controlled HTTP peers. The real
2// daemon E2E verifies attachment and lifecycle behavior with valid server.
3package daemon
4
5import (
6	"context"
7	"encoding/json"
8	"errors"
9	"fmt"
10	"net"
11	"net/http"
12	"net/http/httptest"
13	"sync/atomic"
14	"testing"
15	"time"
16)
17
18const readyServer = `{"instance_id":"instance-a","protocol":3,"state":"ready","capabilities":{"durable_inputs":1,"session_replay":2,"collection_invalidation":1,"tool_progress":1},"build":"verified-build","digest":"verified-digest","extensions":[],"quota":[],"notices":[]}`
19
20func TestAttachValidatesServerIdentityAndRequiredCapabilities(t *testing.T) {
21	for _, change := range []struct {
22		name, key     string
23		value         any
24		compatibility bool
25	}{
26		{"missing identity", "instance_id", nil, false},
27		{"fractional protocol", "protocol", 3.5, false},
28		{"unsupported protocol", "protocol", 2, true},
29		{"missing capabilities", "capabilities", nil, false},
30		{"unsupported capability revision", "capabilities", map[string]any{}, true},
31		{"draining daemon", "state", "draining", false},
32		{"invalid digest", "digest", 7, false},
33	} {
34		t.Run(change.name, func(t *testing.T) {
35			var resource map[string]any
36			_ = json.Unmarshal([]byte(readyServer), &resource)
37			resource[change.key] = change.value
38			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
39				if r.URL.Path != "/server" {
40					t.Errorf("unexpected route %s", r.URL.Path)
41				}
42				_ = json.NewEncoder(w).Encode(resource)
43			}))
44			defer server.Close()
45			conn, err := Attach(t.Context(), ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port, Token: "token"}, nil)
46			if conn != nil || err == nil {
47				t.Fatalf("invalid server attached: %v %v", conn, err)
48			}
49			if change.compatibility {
50				if _, ok := errors.AsType[*CompatibilityError](err); !ok {
51					t.Fatalf("compatibility cause lost: %v", err)
52				}
53			}
54		})
55	}
56}
57
58func TestOptionalCapabilitiesPreventRequestsWithoutRepeatedServerReads(t *testing.T) {
59	for _, feature := range []struct {
60		name string
61		run  func(context.Context, *Connection) error
62	}{
63		{"context", func(ctx context.Context, conn *Connection) error {
64			_, err := GetContextSnapshot(ctx, conn, "s")
65			return err
66		}},
67		{"catalog", func(ctx context.Context, conn *Connection) error {
68			_, err := GetCapabilityCatalog(ctx, conn, "s")
69			return err
70		}},
71		{"workspace_browsing", func(ctx context.Context, conn *Connection) error {
72			_, err := ListFolders(ctx, conn, "/workspace")
73			return err
74		}},
75		{"host_probes", func(ctx context.Context, conn *Connection) error { _, err := WarmHost(ctx, conn, "target"); return err }},
76		{"provider_auth", func(ctx context.Context, conn *Connection) error { _, err := SignInList(ctx, conn); return err }},
77		{"storage_report", func(ctx context.Context, conn *Connection) error { _, err := GetStorageReport(ctx, conn); return err }},
78	} {
79		t.Run(feature.name, func(t *testing.T) {
80			var serverReads, requests atomic.Int32
81			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
82				if r.URL.Path == "/server" {
83					serverReads.Add(1)
84					_, _ = w.Write([]byte(readyServer))
85					return
86				}
87				requests.Add(1)
88				w.WriteHeader(http.StatusNotFound)
89			}))
90			t.Cleanup(server.Close)
91			conn, err := Attach(t.Context(), ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port, Token: "token"}, nil)
92			if err != nil {
93				t.Fatal(err)
94			}
95			t.Cleanup(conn.HTTPClient().CloseIdleConnections)
96			for range 2 {
97				if _, ok := errors.AsType[*UpgradeRequiredError](feature.run(t.Context(), conn)); !ok {
98					t.Fatal("the missing advertised optional capability was not refused")
99				}
100			}
101			ctx, cancel := context.WithCancel(t.Context())
102			cancel()
103			if err := feature.run(ctx, conn); !errors.Is(err, context.Canceled) {
104				t.Fatalf("caller cancellation was replaced by capability refusal: %v", err)
105			}
106			if serverReads.Load() != 1 || requests.Load() != 0 {
107				t.Fatalf("capability checks issued HTTP requests: server=%d operation=%d", serverReads.Load(), requests.Load())
108			}
109		})
110	}
111}
112
113func TestOptionalRecoveryUsesReplacementEndpointCapabilities(t *testing.T) {
114	var replacementReads, replacementRequests atomic.Int32
115	replacement := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
116		if r.URL.Path == "/server" {
117			replacementReads.Add(1)
118			_, _ = w.Write([]byte(readyServer))
119			return
120		}
121		replacementRequests.Add(1)
122		w.WriteHeader(http.StatusNotFound)
123	}))
124	t.Cleanup(replacement.Close)
125	var originalReads, originalRequests atomic.Int32
126	original := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
127		if r.URL.Path == "/server" {
128			originalReads.Add(1)
129			var resource map[string]any
130			_ = json.Unmarshal([]byte(readyServer), &resource)
131			resource["capabilities"].(map[string]any)["catalog"] = 1
132			_ = json.NewEncoder(w).Encode(resource)
133			return
134		}
135		originalRequests.Add(1)
136		w.Header().Set("WWW-Authenticate", "Bearer")
137		w.WriteHeader(http.StatusUnauthorized)
138		_, _ = w.Write([]byte(`{"type":"about:blank","title":"Unauthorized","status":401,"code":"authentication_required","detail":"expired bearer"}`))
139	}))
140	t.Cleanup(original.Close)
141	next := ConnectionSnapshot{Port: replacement.Listener.Addr().(*net.TCPAddr).Port, Token: "replacement"}
142	conn, err := Attach(t.Context(), ConnectionSnapshot{Port: original.Listener.Addr().(*net.TCPAddr).Port, Token: "original"}, func(context.Context) (ConnectionSnapshot, error) { return next, nil })
143	if err != nil {
144		t.Fatal(err)
145	}
146	t.Cleanup(conn.HTTPClient().CloseIdleConnections)
147	_, err = GetCapabilityCatalog(t.Context(), conn, "s")
148	if _, ok := errors.AsType[*UpgradeRequiredError](err); !ok {
149		t.Fatalf("replacement capability refusal was lost: %v", err)
150	}
151	endpoint := conn.Snapshot()
152	if endpoint.Port != next.Port || endpoint.Token != next.Token || originalReads.Load() != 1 || originalRequests.Load() != 1 || replacementReads.Load() != 1 || replacementRequests.Load() != 0 {
153		t.Fatalf("recovery reused old endpoint capability facts: old=%d/%d new=%d/%d", originalReads.Load(), originalRequests.Load(), replacementReads.Load(), replacementRequests.Load())
154	}
155}
156
157func TestProbeServerPreservesAuthenticationAndCancellationWithoutRediscovery(t *testing.T) {
158	for _, cancelProbe := range []bool{false, true} {
159		t.Run(fmt.Sprint(cancelProbe), func(t *testing.T) {
160			entered := make(chan struct{})
161			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
162				close(entered)
163				if cancelProbe {
164					<-r.Context().Done()
165					return
166				}
167				w.WriteHeader(http.StatusUnauthorized)
168				_, _ = fmt.Fprint(w, `{"type":"about:blank","title":"Unauthorized","status":401,"code":"authentication_required","detail":"invalid bearer"}`)
169			}))
170			defer server.Close()
171			var rediscoveries atomic.Int32
172			conn := NewConnection(ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port, Token: "old"}, func(context.Context) (ConnectionSnapshot, error) {
173				rediscoveries.Add(1)
174				return ConnectionSnapshot{}, errors.New("must not rediscover")
175			})
176			ctx, cancel := context.WithCancel(t.Context())
177			defer cancel()
178			finished := make(chan error, 1)
179			go func() { _, err := ProbeServer(ctx, conn); finished <- err }()
180			select {
181			case <-entered:
182			case <-time.After(time.Second):
183				t.Fatal("server request never started")
184			}
185			if cancelProbe {
186				cancel()
187			}
188			select {
189			case err := <-finished:
190				if cancelProbe {
191					if !errors.Is(err, context.Canceled) {
192						t.Fatalf("cancellation lost: %v", err)
193					}
194				} else {
195					failure, ok := errors.AsType[*APIError](err)
196					if !ok || failure.StatusCode != http.StatusUnauthorized || failure.Code != "authentication_required" {
197						t.Fatalf("authentication lost: %v", err)
198					}
199				}
200			case <-time.After(time.Second):
201				t.Fatal("server request ignored cancellation")
202			}
203			if rediscoveries.Load() != 0 {
204				t.Fatal("server probe attempted rediscovery")
205			}
206		})
207	}
208}
209
210func TestRefreshGateCancellationAndSuccessfulAttachment(t *testing.T) {
211	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
212		if r.URL.Path == "/server" {
213			_, _ = fmt.Fprint(w, readyServer)
214			return
215		}
216		if r.Header.Get("Authorization") != "Bearer new" {
217			t.Errorf("request used stale credential: %s", r.Header.Get("Authorization"))
218		}
219		_, _ = fmt.Fprint(w, `{"ok":true}`)
220	}))
221	defer server.Close()
222	entered, release := make(chan struct{}), make(chan struct{})
223	var calls atomic.Int32
224	candidate := ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port, Token: "new", Version: 99, Build: "unverified"}
225	conn := NewConnection(ConnectionSnapshot{Port: candidate.Port, Token: "old"}, func(ctx context.Context) (ConnectionSnapshot, error) {
226		calls.Add(1)
227		close(entered)
228		select {
229		case <-release:
230			return candidate, nil
231		case <-ctx.Done():
232			return ConnectionSnapshot{}, ctx.Err()
233		}
234	})
235	defer conn.HTTPClient().CloseIdleConnections()
236	finished := make(chan error, 1)
237	go func() { finished <- conn.Refresh(t.Context()) }()
238	<-entered
239	waiting, cancel := context.WithCancel(t.Context())
240	cancel()
241	if err := conn.Refresh(waiting); !errors.Is(err, context.Canceled) {
242		t.Fatalf("refresh wait cancellation lost: %v", err)
243	}
244	close(release)
245	if err := <-finished; err != nil {
246		t.Fatal(err)
247	}
248	if calls.Load() != 1 {
249		t.Fatalf("canceled waiter rediscovered: %d", calls.Load())
250	}
251	endpoint := conn.Snapshot()
252	if endpoint.Version != ProtocolVersion || endpoint.Build != "verified-build" || endpoint.Digest != "verified-digest" {
253		t.Fatalf("attachment trusted unverified discovery: %+v", endpoint)
254	}
255	if _, err := requestBytes(t.Context(), conn, operation{Name: "read", Method: http.MethodGet, Path: "/read", Policy: readRecovery}, responseLimits{bodyBytes: 50 * 1024 * 1024, errorBytes: 64 * 1024}); err != nil {
256		t.Fatal(err)
257	}
258}
259
260func TestOperationSurfacesRefreshFailureWithoutReplaying(t *testing.T) {
261	for _, malformed := range []bool{false, true} {
262		t.Run(fmt.Sprint(malformed), func(t *testing.T) {
263			var operations, rediscoveries atomic.Int32
264			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
265				if r.URL.Path == "/server" {
266					_, _ = fmt.Fprint(w, `{"instance_id":"bad","protocol":3,"capabilities":null}`)
267					return
268				}
269				operations.Add(1)
270				w.WriteHeader(http.StatusUnauthorized)
271				_, _ = fmt.Fprint(w, `{"type":"about:blank","title":"Unauthorized","status":401,"code":"authentication_required","detail":"invalid bearer"}`)
272			}))
273			defer server.Close()
274			cause := errors.New("discovery inaccessible")
275			conn := NewConnection(ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port, Token: "old"}, func(context.Context) (ConnectionSnapshot, error) {
276				rediscoveries.Add(1)
277				if !malformed {
278					return ConnectionSnapshot{}, cause
279				}
280				return ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port, Token: "new"}, nil
281			})
282			_, err := requestBytes(t.Context(), conn, operation{Name: "change", Method: http.MethodPost, Path: "/sessions/s/inputs/i", Policy: authRecovery}, responseLimits{bodyBytes: 50 * 1024 * 1024, errorBytes: 64 * 1024})
283			if malformed {
284				if _, ok := errors.AsType[*ProtocolError](err); !ok {
285					t.Fatalf("server failure hidden: %v", err)
286				}
287			} else if !errors.Is(err, cause) {
288				t.Fatalf("discovery failure hidden: %v", err)
289			}
290			if operations.Load() != 1 || rediscoveries.Load() != 1 || conn.Snapshot().Token != "old" {
291				t.Fatalf("failed refresh changed endpoint or replayed: operations=%d rediscoveries=%d token=%s", operations.Load(), rediscoveries.Load(), conn.Snapshot().Token)
292			}
293		})
294	}
295}
296
297func TestBuildMismatchStaysQuietOnlyForProvenSameness(t *testing.T) {
298	for _, tc := range []struct {
299		running, selected BuildIdentity
300		want              bool
301	}{
302		{running: BuildIdentity{Digest: "a"}, selected: BuildIdentity{Digest: "a"}},
303		{running: BuildIdentity{Build: "same"}, selected: BuildIdentity{Build: "same"}},
304		{running: BuildIdentity{Digest: "a"}, selected: BuildIdentity{Digest: "b"}, want: true},
305		{running: BuildIdentity{Build: "x"}, selected: BuildIdentity{Build: "y"}, want: true},
306		// Digests outrank labels: identical content is one build even when
307		// the labels disagree, and different content is a mismatch even when
308		// the labels agree.
309		{running: BuildIdentity{Build: "old", Digest: "a"}, selected: BuildIdentity{Build: "new", Digest: "a"}},
310		{running: BuildIdentity{Build: "same", Digest: "a"}, selected: BuildIdentity{Build: "same", Digest: "b"}, want: true},
311		// Unprovable: one side of each identity is unknown.
312		{running: BuildIdentity{Digest: "a"}, selected: BuildIdentity{Build: "y"}, want: true},
313		{running: BuildIdentity{Build: "x"}, selected: BuildIdentity{Digest: "b"}, want: true},
314		{want: true},
315	} {
316		if got := BuildMismatch(tc.running, tc.selected); got != tc.want {
317			t.Fatalf("BuildMismatch(%+v, %+v) = %v, want %v", tc.running, tc.selected, got, tc.want)
318		}
319	}
320}