okami.mom/albedo

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

nekomimi.pettest(e2e): run session-scoped Go scenarios in parallel5b3f8be

main
6.1 KiB162 linesraw
1//go:build unix
2
3// The agents view must replace stale membership and previews after a lost feed.
4package e2e
5
6import (
7	"context"
8	"encoding/json"
9	"fmt"
10	"net"
11	"net/http"
12	"net/http/httptest"
13	"net/http/httputil"
14	"net/url"
15	"strings"
16	"sync/atomic"
17	"testing"
18	"time"
19
20	"albedo/cli/internal/daemon"
21	"albedo/cli/internal/testwire"
22	"albedo/cli/internal/tui"
23	tea "charm.land/bubbletea/v2"
24)
25
26func TestTUIAgentsOverflowRefreshesMembershipAndPreviews(t *testing.T) {
27	entered, release := make(chan struct{}), make(chan struct{})
28	defer close(release)
29	profile := providerRoute(t, func(request map[string]any) string {
30		if lastUserText(request) == "held agent refresh turn" {
31			close(entered)
32			<-release
33		}
34		return echoReply(request)
35	})
36	t.Parallel()
37	root := daemonSession(t, newSession(t, t.TempDir()))
38	create := func(name string) daemon.Session {
39		t.Helper()
40		child, err := daemon.CreateSession(t.Context(), conn(t), daemon.CreateSessionRequest{Kind: "child", ParentID: root.ID, Address: name, Name: name, Task: "finish child setup"})
41		if err != nil {
42			t.Fatal(err)
43		}
44		waitIdle(t, child.ID, profile, 1)
45		return child
46	}
47	removed := create("removed")
48	retained := create("old-name")
49	waitIdle(t, root.ID, profile, 2)
50	if _, err := daemon.NewChatClient(conn(t), root.ID).Send(t.Context(), "INITIAL_PREVIEW_SEED", nil); err != nil {
51		t.Fatal(err)
52	}
53	waitIdle(t, root.ID, profile, 3)
54
55	destination, err := url.Parse(conn(t).BaseURL())
56	if err != nil {
57		t.Fatal(err)
58	}
59	forward := httputil.NewSingleHostReverseProxy(destination)
60	direct := forward.Director
61	forward.Director = func(r *http.Request) { direct(r); r.Host = destination.Host }
62	forward.FlushInterval = -1
63	var streams atomic.Int32
64	seedReady := make(chan struct{}, 1)
65	fragments, gap := make(chan struct{}), make(chan struct{})
66	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
67		if r.URL.Path == "/sessions" && r.Header.Get("Accept") == "text/event-stream" && streams.Add(1) == 1 {
68			w.Header().Set("Content-Type", "text/event-stream")
69			_, _ = fmt.Fprint(w, "data: {\"events\":[{\"type\":\"ready\",\"data\":{\"instance_id\":\"controlled-agent-stream\"}},{\"type\":\"reset\",\"data\":{\"reason\":\"initial\"}}]}\n\n")
70			w.(http.Flusher).Flush()
71			select {
72			case <-fragments:
73			case <-r.Context().Done():
74				return
75			}
76			events, _ := json.Marshal(map[string]any{"events": []map[string]any{
77				{"type": "activity", "data": map[string]any{"session_id": root.ID, "cursor": map[string]any{"generation": testwire.GenerationA, "sequence": 1}, "status": map[string]any{"phase": "running", "run_id": "old-run", "interrupt_requested": false, "blocking_reason": nil}, "current_progress": []map[string]any{{"call_id": "old-run:1:0", "tool_call_id": nil, "name": "python", "phase": "generating", "intent": "", "preview": map[string]any{"offset_scalars": 0, "text": "OLD_PARTIAL_CODE", "complete": false}}}, "activity": map[string]any{"lines": []map[string]any{{"kind": "assistant", "text": "OLD_PARTIAL_TEXT", "truncated": false}}, "output_scalars": 0, "output_utf8_bytes": 0, "observed_at": "2026-10-03T00:00:00Z", "latest_input": nil, "latest_answer": nil}}},
78			}})
79			_, _ = fmt.Fprintf(w, "data: %s\n\n", events)
80			w.(http.Flusher).Flush()
81			select {
82			case <-gap:
83				_, _ = fmt.Fprint(w, "data: {\"events\":[{\"type\":\"overflow\",\"data\":{\"reason\":\"subscriber_overflow\"}}]}\n\n")
84				w.(http.Flusher).Flush()
85			case <-r.Context().Done():
86			}
87			return
88		}
89		forward.ServeHTTP(w, r)
90		if r.URL.Path == "/sessions/"+root.ID && r.URL.Query().Get("tail") != "" {
91			select {
92			case seedReady <- struct{}{}:
93			default:
94			}
95		}
96	}))
97	t.Cleanup(server.Close)
98	snapshot := conn(t).Snapshot()
99	snapshot.Port = server.Listener.Addr().(*net.TCPAddr).Port
100	connection, err := daemon.Attach(t.Context(), snapshot, nil)
101	if err != nil {
102		t.Fatal(err)
103	}
104	t.Cleanup(connection.HTTPClient().CloseIdleConnections)
105	driver := driveTUIWithConnection(t, &root, connection)
106	driver.Update(tea.WindowSizeMsg{Width: 140, Height: 50})
107	t.Cleanup(driver.App.Chat.Close)
108	t.Cleanup(func() { driver.App.Agents.Close() })
109	ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second)
110	defer cancel()
111	inbox := make(chan tea.Msg, 64)
112	streamMessages(ctx, driver.Update(tui.ChatOpenAgentsMsg{}), inbox)
113	pump := func(condition func() bool) {
114		t.Helper()
115		for !condition() {
116			select {
117			case message := <-inbox:
118				streamMessages(ctx, driver.Update(message), inbox)
119			case <-ctx.Done():
120				t.Fatalf("agent view did not recover:\n%s", driver.View())
121			}
122		}
123	}
124	pump(func() bool { return strings.Contains(driver.View(), "INITIAL_PREVIEW_SEED") })
125	select {
126	case <-seedReady:
127	case <-ctx.Done():
128		t.Fatal("initial preview request never completed")
129	}
130	close(fragments)
131	pump(func() bool { return strings.Contains(driver.View(), "OLD_PARTIAL_CODE") })
132	if !strings.Contains(driver.View(), "removed") || !strings.Contains(driver.View(), "old-name") {
133		t.Fatalf("initial membership did not load:\n%s", driver.View())
134	}
135	if _, err := daemon.RenameSession(t.Context(), conn(t), retained.ID, "renamed", sessionCondition(t, conn(t), retained.ID).ETag); err != nil {
136		t.Fatal(err)
137	}
138	if _, err := daemon.DeleteSession(t.Context(), conn(t), removed.ID, false, sessionCondition(t, conn(t), removed.ID)); err != nil {
139		t.Fatal(err)
140	}
141	create("new-agent")
142	if _, err := daemon.NewChatClient(conn(t), root.ID).Send(t.Context(), "held agent refresh turn", nil); err != nil {
143		t.Fatal(err)
144	}
145	select {
146	case <-entered:
147	case <-ctx.Done():
148		t.Fatal("held model turn did not start")
149	}
150	for _, character := range "draft survives refresh" {
151		driver.Update(tea.KeyPressMsg{Code: character, Text: string(character)})
152	}
153	close(gap)
154	pump(func() bool {
155		view := driver.View()
156		return strings.Contains(view, "renamed") && strings.Contains(view, "new-agent") && strings.Contains(view, "1 running") && !strings.Contains(view, "○ removed") && !strings.Contains(view, "○ old-name")
157	})
158	view := driver.View()
159	if strings.Contains(view, "OLD_PARTIAL") || !strings.Contains(view, "draft survives refresh") {
160		t.Fatalf("refresh retained stale fragments or discarded the input draft:\n%s", view)
161	}
162}