okami.mom/albedo

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

Aly Raffauffeat(api)!: migrate daemon and clients to protocol 361c43ca

main
5.9 KiB169 linesraw
1// Hanging refusal bodies and failures after stream delivery require controlled
2// peers; real daemon events cannot reliably force these transport boundaries.
3package daemon
4
5import (
6	"context"
7	"errors"
8	"io"
9	"net"
10	"net/http"
11	"net/http/httptest"
12	"sync/atomic"
13	"testing"
14	"time"
15)
16
17type streamCaller struct {
18	start func(context.Context, *Connection, func() error) error
19	name  string
20}
21
22func streamCallers() []streamCaller {
23	return []streamCaller{
24		{name: "agents", start: func(ctx context.Context, conn *Connection, deliver func() error) error {
25			return StreamAgents(ctx, conn, func(_ []AgentEvent) error { return deliver() })
26		}},
27		{name: "chat", start: func(ctx context.Context, conn *Connection, deliver func() error) error {
28			return NewChatClient(conn, "test").Stream(ctx, 0, func(event StreamEvent) error {
29				if event.Type == EventText {
30					return deliver()
31				}
32				return nil
33			})
34		}},
35	}
36}
37
38func TestStreamAuthenticationRecoveryBeforeDelivery(t *testing.T) {
39	for _, caller := range streamCallers() {
40		t.Run(caller.name, func(t *testing.T) {
41			for _, test := range []struct {
42				name, token string
43				marked      bool
44				requests    int32
45			}{
46				{name: "changed credentials", token: "new", marked: true, requests: 2},
47				{name: "unchanged credentials", token: "old", marked: true, requests: 1},
48				{name: "unmarked refusal", token: "new", requests: 1},
49			} {
50				t.Run(test.name, func(t *testing.T) {
51					var requests atomic.Int32
52					var deliveries atomic.Int32
53					server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
54						if request.URL.Path == "/server" {
55							_, _ = writer.Write([]byte(readyServer))
56							return
57						}
58						if requests.Add(1) == 1 {
59							if test.marked {
60								writer.Header().Set("WWW-Authenticate", "Bearer")
61								writer.WriteHeader(http.StatusUnauthorized)
62							} else {
63								writer.WriteHeader(http.StatusForbidden)
64							}
65							if caller.name == "chat" {
66								if test.marked {
67									_, _ = writer.Write([]byte(`{"code":"authentication_required"}`))
68								} else {
69									_, _ = writer.Write([]byte(`{"detail":"denied"}`))
70								}
71								return
72							}
73							// The refusal marker allows closing the unfinished
74							// body must cancel this handler without waiting for more bytes.
75							writer.(http.Flusher).Flush()
76							<-request.Context().Done()
77							return
78						}
79						if request.Header.Get("Authorization") != "Bearer new" {
80							t.Error("stream reused old credentials")
81						}
82						writer.Header().Set("Content-Type", "text/event-stream")
83						writeInitialStream(writer, caller.name)
84					}))
85					defer server.Close()
86
87					snapshot := ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port, Token: test.token, Version: ProtocolVersion}
88					replacement := snapshot
89					snapshot.Token = "old"
90					conn := NewConnection(snapshot, func(context.Context) (ConnectionSnapshot, error) { return replacement, nil })
91					defer conn.HTTPClient().CloseIdleConnections()
92					ctx, cancel := context.WithTimeout(context.Background(), time.Second)
93					defer cancel()
94					streamErr := caller.start(ctx, conn, func() error { deliveries.Add(1); return nil })
95					if errors.Is(streamErr, context.DeadlineExceeded) {
96						t.Fatalf("stream waited for unfinished refusal body: %v", streamErr)
97					}
98					if requests.Load() != test.requests {
99						t.Fatalf("got %d requests, expected %d", requests.Load(), test.requests)
100					}
101					if test.requests == 2 {
102						if streamErr != nil || deliveries.Load() != 1 {
103							t.Fatalf("stream failed recovery: %v deliveries=%d", streamErr, deliveries.Load())
104						}
105					} else {
106						failure, ok := errors.AsType[*APIError](streamErr)
107						expectedStatus := http.StatusForbidden
108						if test.marked {
109							expectedStatus = http.StatusUnauthorized
110						}
111						if !ok || failure.StatusCode != expectedStatus || deliveries.Load() != 0 {
112							t.Fatalf("refusal changed: %v deliveries=%d", streamErr, deliveries.Load())
113						}
114					}
115				})
116			}
117		})
118	}
119}
120
121func TestAcceptedStreamIsNeverReplayed(t *testing.T) {
122	for _, caller := range streamCallers() {
123		t.Run(caller.name, func(t *testing.T) {
124			for _, failure := range []string{"truncated body", "callback error"} {
125				t.Run(failure, func(t *testing.T) {
126					var requests atomic.Int32
127					var deliveries atomic.Int32
128					callbackFailure := errors.New("consumer stopped")
129					server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
130						if request.URL.Path == "/server" {
131							_, _ = writer.Write([]byte(readyServer))
132							return
133						}
134						requests.Add(1)
135						if failure == "truncated body" {
136							writer.Header().Set("Content-Length", "1000000")
137						}
138						writer.Header().Set("Content-Type", "text/event-stream")
139						writeInitialStream(writer, caller.name)
140					}))
141					defer server.Close()
142					snapshot := ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port, Token: "token", Version: ProtocolVersion}
143
144					replacement := snapshot
145					conn := NewConnection(snapshot, func(context.Context) (ConnectionSnapshot, error) { return replacement, nil })
146					defer conn.HTTPClient().CloseIdleConnections()
147					ctx, cancel := context.WithTimeout(context.Background(), time.Second)
148					defer cancel()
149					streamErr := caller.start(ctx, conn, func() error {
150						deliveries.Add(1)
151						if failure == "callback error" {
152							return callbackFailure
153						}
154						return nil
155					})
156					if failure == "callback error" && !errors.Is(streamErr, callbackFailure) {
157						t.Fatalf("callback failure lost: %v", streamErr)
158					}
159					if failure == "truncated body" && !errors.Is(streamErr, io.ErrUnexpectedEOF) {
160						t.Fatalf("stream truncation lost: %v", streamErr)
161					}
162					if requests.Load() != 1 || deliveries.Load() != 1 {
163						t.Fatalf("accepted stream replayed: requests=%d deliveries=%d", requests.Load(), deliveries.Load())
164					}
165				})
166			}
167		})
168	}
169}