okami.mom/albedo

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

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

main
6.2 KiB123 linesraw
1package app
2
3// Controlled streams verify turn membership, retries, and cleanup after cancellation.
4import (
5	"albedo/cli/internal/daemon"
6	"albedo/cli/internal/testwire"
7	"context"
8	"encoding/json"
9	"errors"
10	"net"
11	"net/http"
12	"net/http/httptest"
13	"strings"
14	"testing"
15	"time"
16)
17
18func promptTestConnection(server *httptest.Server) *daemon.Connection {
19	return daemon.NewConnection(daemon.ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port}, nil)
20}
21func TestPromptRequiresDurableInputCapabilityBeforeCreatingASession(t *testing.T) {
22	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23		if r.URL.Path != "/server" {
24			t.Errorf("unsupported prompt issued %s", r.URL)
25		}
26		var resource map[string]any
27		_ = json.Unmarshal([]byte(testwire.Server), &resource)
28		resource["capabilities"] = map[string]any{}
29		_ = json.NewEncoder(w).Encode(resource)
30	}))
31	defer server.Close()
32	service := Service{Connect: func(ctx context.Context) (*daemon.Connection, error) {
33		return daemon.Attach(ctx, daemon.ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port, Token: "token"}, nil)
34	}}
35	_, err := service.RunPrompt(t.Context(), PromptOptions{Prompt: "hello"})
36	if _, ok := errors.AsType[*daemon.CompatibilityError](err); !ok {
37		t.Fatalf("missing capability dispatched a submission: %v", err)
38	}
39}
40func promptEntry(id, text, run string, position int64) map[string]any {
41	return map[string]any{"id": id, "position": position, "kind": "assistant", "created_at": nil, "input_id": nil, "turn_id": run, "checkpoint_id": nil, "content_complete": true, "content": []any{map[string]any{"kind": "text", "text": text}}, "tool": nil}
42}
43func TestPromptFollowsCombinedSubmissionThroughRetryAndCompletion(t *testing.T) {
44	submitted := make(chan string, 1)
45	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
46		switch {
47		case r.Method == "PUT" && strings.HasPrefix(r.URL.Path, "/sessions/test/inputs/"):
48			id := strings.TrimPrefix(r.URL.Path, "/sessions/test/inputs/")
49			submitted <- id
50			w.WriteHeader(202)
51			_ = json.NewEncoder(w).Encode(testwire.Input("test", id, "message", "pending"))
52		case r.URL.Path == "/sessions/test":
53			w.Header().Set("Content-Type", "text/event-stream")
54			testwire.WriteBatch(w, map[string]any{"generation": testwire.GenerationA, "cursor": 0, "snapshot": testwire.Session("test", testwire.GenerationA, 0), "events": []any{map[string]any{"type": "reset", "data": map[string]any{"reason": "initial"}}}})
55			w.(http.Flusher).Flush()
56			id := <-submitted
57			input := testwire.Input("test", id, "message", "committed")
58			input["turn"] = map[string]any{"id": "combined", "state": "running", "input_ids": []string{id, "other"}, "input_count": 2, "truncated": false, "started_at": "2026-10-03T00:00:00Z", "ended_at": nil, "outcome": nil}
59			events := []any{testwire.Event("input", 1, map[string]any{"input": input}), testwire.Event("message", 2, map[string]any{"entry": promptEntry("m1", "before retry", "combined", 1)}), testwire.Event("error", 3, map[string]any{"run_id": "combined", "code": "temporary", "message": "temporary failure"}), testwire.Event("retry", 4, map[string]any{"run_id": "combined", "attempt": 2, "reason": map[string]any{"code": "retry", "detail": "retrying"}, "delay_ms": 0}), testwire.Event("message", 5, map[string]any{"entry": promptEntry("m2", "combined final answer", "combined", 2)}), testwire.Event("turn_completed", 6, map[string]any{"run_id": "combined", "state": "completed", "input_ids": []string{id, "other"}, "input_count": 2, "truncated": false})}
60			testwire.WriteBatch(w, map[string]any{"generation": testwire.GenerationA, "cursor": 6, "events": events})
61			w.(http.Flusher).Flush()
62			<-r.Context().Done()
63		default:
64			t.Errorf("unexpected %s %s", r.Method, r.URL)
65			w.WriteHeader(404)
66		}
67	}))
68	defer server.Close()
69	ctx, cancel := context.WithTimeout(t.Context(), time.Second)
70	defer cancel()
71	answer, err := awaitReply(ctx, daemon.NewChatClient(promptTestConnection(server), "test"), "hello")
72	if err != nil || answer != "combined final answer" {
73		t.Fatalf("combined reply: %q %v", answer, err)
74	}
75}
76func TestPromptCancellationReportsSharedWorkAndCleanupFailure(t *testing.T) {
77	for _, fixture := range []struct {
78		name, outcome, want string
79		status              int
80	}{{"shared", "shared_running", "shared turn is still running", 200}, {"cleanup failure", "", "cancellation unconfirmed", 503}} {
81		t.Run(fixture.name, func(t *testing.T) {
82			submitted, cancelled := make(chan struct{}), make(chan struct{})
83			ctx, cancel := context.WithCancel(t.Context())
84			defer cancel()
85			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
86				switch {
87				case r.URL.Path == "/sessions/test":
88					w.Header().Set("Content-Type", "text/event-stream")
89					testwire.WriteBatch(w, map[string]any{"generation": testwire.GenerationA, "cursor": 0, "snapshot": testwire.Session("test", testwire.GenerationA, 0), "events": []any{map[string]any{"type": "reset", "data": map[string]any{"reason": "initial"}}}})
90					w.(http.Flusher).Flush()
91					<-r.Context().Done()
92				case r.Method == "PUT" && strings.HasPrefix(r.URL.Path, "/sessions/test/inputs/"):
93					id := strings.TrimPrefix(r.URL.Path, "/sessions/test/inputs/")
94					w.WriteHeader(202)
95					_ = json.NewEncoder(w).Encode(testwire.Input("test", id, "message", "pending"))
96					close(submitted)
97				case r.Method == "POST" && strings.HasSuffix(r.URL.Path, "/cancel"):
98					if r.Context().Err() != nil {
99						t.Error("cleanup reused cancelled context")
100					}
101					id := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/sessions/test/inputs/"), "/cancel")
102					w.WriteHeader(fixture.status)
103					_ = json.NewEncoder(w).Encode(map[string]any{"input": testwire.Input("test", id, "message", "committed"), "result": fixture.outcome})
104					close(cancelled)
105				default:
106					t.Errorf("unexpected operation %s %s", r.Method, r.URL)
107					w.WriteHeader(404)
108				}
109			}))
110			defer server.Close()
111			go func() { <-submitted; cancel() }()
112			_, err := awaitReply(ctx, daemon.NewChatClient(promptTestConnection(server), "test"), "hello")
113			if err == nil || !strings.Contains(err.Error(), fixture.want) {
114				t.Fatalf("cancellation report %v", err)
115			}
116			select {
117			case <-cancelled:
118			default:
119				t.Error("cleanup not attempted")
120			}
121		})
122	}
123}