eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3// đŸ¤– End-to-end tests for internal/client's live typed-event + ephemeral
4// transport (liveevents.go) — the plumbing the agents bridge rides
5// (docs/superpowers/specs/2026-08-05-eleven-claude-agents-design.md). Same
6// harness as eleven_client_test.go: real router via newTestServer, two real
7// E2EE members.
8
9import (
10 "bytes"
11 "context"
12 "encoding/json"
13 "net/http"
14 "testing"
15 "time"
16
17 lclient "lchat/internal/client"
18 "lchat/internal/e2ee"
19)
20
21// groupForTest creates a named group containing both members, the way the web
22// composer does, and returns its thread id.
23func groupForTest(t *testing.T, tsURL string, a, b *lclient.Client) string {
24 t.Helper()
25 aUID, err := a.Me()
26 if err != nil {
27 t.Fatal(err)
28 }
29 bUID, err := b.Me()
30 if err != nil {
31 t.Fatal(err)
32 }
33 tk, err := e2ee.GenerateKey()
34 if err != nil {
35 t.Fatal(err)
36 }
37 tkB64 := e2ee.B64uEncode(tk)
38 wrapTo := func(pub string) string {
39 w, err := e2ee.WrapThreadKey(tkB64, pub)
40 if err != nil {
41 t.Fatal(err)
42 }
43 return w
44 }
45 nameBlob, err := e2ee.EncryptJSON(tk, map[string]string{"name": "Agents Test"})
46 if err != nil {
47 t.Fatal(err)
48 }
49 body, _ := json.Marshal(map[string]any{
50 "token": a.Session.Token, "kind": "group", "name_blob": nameBlob,
51 "members": []map[string]string{
52 {"user_id": aUID, "wrapped": wrapTo(a.Session.UserPub)},
53 {"user_id": bUID, "wrapped": wrapTo(b.Session.UserPub)},
54 },
55 })
56 resp, err := http.Post(tsURL+"/api/threads", "application/json", bytes.NewReader(body))
57 if err != nil {
58 t.Fatal(err)
59 }
60 defer resp.Body.Close()
61 var created struct {
62 ThreadID string `json:"thread_id"`
63 }
64 if err := json.NewDecoder(resp.Body).Decode(&created); err != nil || created.ThreadID == "" {
65 t.Fatalf("create thread: %v (id %q)", err, created.ThreadID)
66 }
67 return created.ThreadID
68}
69
70func TestClientTypedEventDelivery(t *testing.T) {
71 ts, _ := newTestServer(t)
72 bridge := joinForTest(t, ts.URL, "Claude")
73 member := joinForTest(t, ts.URL, "Paul")
74 thread := groupForTest(t, ts.URL, bridge, member)
75
76 events := make(chan lclient.Event, 8)
77 msgs := make(chan lclient.Message, 8)
78 ready := make(chan struct{})
79 bridge.OnEvent = func(e lclient.Event) { events <- e }
80 bridge.OnMessage = func(m lclient.Message) { msgs <- m }
81 bridge.OnReady = func() { ready <- struct{}{} }
82 ctx, cancel := context.WithCancel(context.Background())
83 defer cancel()
84 go bridge.Run(ctx)
85 select {
86 case <-ready:
87 case <-time.After(5 * time.Second):
88 t.Fatal("bridge never connected")
89 }
90
91 // A member emits a typed event (a steering decision): it must arrive on
92 // OnEvent with the transport fields injected, and never on OnMessage.
93 if err := member.SendEvent(thread, map[string]any{
94 "type": "session.approval", "id": "s1", "rid": "a1", "state": "allowed",
95 }); err != nil {
96 t.Fatalf("SendEvent: %v", err)
97 }
98 select {
99 case e := <-events:
100 if e.Type != "session.approval" || e.ThreadID != thread || e.ID == 0 {
101 t.Errorf("event: %+v", e)
102 }
103 if e.Payload["name"] != "Paul" || e.Payload["rid"] != "a1" {
104 t.Errorf("payload: %+v", e.Payload)
105 }
106 if _, ok := e.Payload["uid"].(string); !ok {
107 t.Errorf("uid not injected: %+v", e.Payload)
108 }
109 case <-time.After(5 * time.Second):
110 t.Fatal("typed event never arrived on OnEvent")
111 }
112 select {
113 case m := <-msgs:
114 t.Errorf("typed event leaked to OnMessage: %+v", m)
115 default:
116 }
117
118 // A plain chat message still lands on OnMessage, not OnEvent.
119 if err := member.Send(thread, "hello"); err != nil {
120 t.Fatal(err)
121 }
122 select {
123 case m := <-msgs:
124 if m.Text != "hello" {
125 t.Errorf("message: %+v", m)
126 }
127 case <-time.After(5 * time.Second):
128 t.Fatal("chat message never arrived on OnMessage")
129 }
130 select {
131 case e := <-events:
132 t.Errorf("chat message leaked to OnEvent: %+v", e)
133 default:
134 }
135}
136
137func TestClientTypedEventCatchUp(t *testing.T) {
138 ts, _ := newTestServer(t)
139 bridge := joinForTest(t, ts.URL, "Claude")
140 member := joinForTest(t, ts.URL, "Paul")
141 thread := groupForTest(t, ts.URL, bridge, member)
142
143 // Baseline the bridge's cursor on the thread (first connect delivers
144 // nothing historical), then drop the connection.
145 ready := make(chan struct{})
146 events := make(chan lclient.Event, 8)
147 bridge.OnEvent = func(e lclient.Event) { events <- e }
148 bridge.OnReady = func() { ready <- struct{}{} }
149 ctx1, cancel1 := context.WithCancel(context.Background())
150 go bridge.Run(ctx1)
151 select {
152 case <-ready:
153 case <-time.After(5 * time.Second):
154 t.Fatal("bridge never connected")
155 }
156 cancel1()
157
158 // An event lands while the bridge is away; the reconnect's catch-up must
159 // deliver it to OnEvent.
160 if err := member.SendEvent(thread, map[string]any{"type": "session.prompt", "id": "s1", "text": "fix it"}); err != nil {
161 t.Fatal(err)
162 }
163 ctx2, cancel2 := context.WithCancel(context.Background())
164 defer cancel2()
165 go bridge.Run(ctx2)
166 select {
167 case e := <-events:
168 if e.Type != "session.prompt" || e.Payload["text"] != "fix it" {
169 t.Errorf("catch-up event: %+v", e)
170 }
171 case <-time.After(5 * time.Second):
172 t.Fatal("catch-up never delivered the typed event")
173 }
174}
175
176func TestClientEphemeralRoundTrip(t *testing.T) {
177 ts, _ := newTestServer(t)
178 bridge := joinForTest(t, ts.URL, "Claude")
179 member := joinForTest(t, ts.URL, "Paul")
180 thread := groupForTest(t, ts.URL, bridge, member)
181
182 // Both ends live: the bridge sends the tail, the member's client receives
183 // it on OnEphemeral.
184 run := func(c *lclient.Client) (chan lclient.Event, context.CancelFunc) {
185 ev := make(chan lclient.Event, 8)
186 ready := make(chan struct{})
187 c.OnEphemeral = func(e lclient.Event) { ev <- e }
188 c.OnReady = func() { ready <- struct{}{} }
189 ctx, cancel := context.WithCancel(context.Background())
190 go c.Run(ctx)
191 select {
192 case <-ready:
193 case <-time.After(5 * time.Second):
194 t.Fatal("client never connected")
195 }
196 return ev, cancel
197 }
198 memberEv, cancelM := run(member)
199 defer cancelM()
200 _, cancelB := run(bridge)
201 defer cancelB()
202
203 if err := bridge.SendEphemeral(thread, map[string]any{"type": "session.stream", "id": "s1", "text": "partial…"}); err != nil {
204 t.Fatalf("SendEphemeral: %v", err)
205 }
206 select {
207 case e := <-memberEv:
208 if e.Type != "session.stream" || e.ID != 0 || e.Payload["name"] != "Claude" || e.Payload["text"] != "partial…" {
209 t.Errorf("ephemeral: %+v", e)
210 }
211 case <-time.After(5 * time.Second):
212 t.Fatal("ephemeral frame never arrived")
213 }
214
215 // Relay-only means relay-only: nothing was stored.
216 evs, err := member.Events(thread, "session.")
217 if err != nil {
218 t.Fatal(err)
219 }
220 if len(evs) != 0 {
221 t.Errorf("ephemeral frame was stored: %+v", evs)
222 }
223}
224
225func TestClientSendEphemeralRequiresConnection(t *testing.T) {
226 ts, _ := newTestServer(t)
227 bridge := joinForTest(t, ts.URL, "Claude")
228 member := joinForTest(t, ts.URL, "Paul")
229 thread := groupForTest(t, ts.URL, bridge, member)
230 // No Run: an ephemeral frame has no store to land in, so without a live
231 // socket the send must fail loudly rather than vanish.
232 if err := bridge.SendEphemeral(thread, map[string]any{"type": "session.stream"}); err == nil {
233 t.Fatal("SendEphemeral without a connection should error")
234 }
235}
236