返回 DeepSeek-Reasonix
remote_runtime_state_test.go
根目录 / desktop / remote_runtime_state_test.go
1 package main
2
3 import (
4 "encoding/json"
5 "errors"
6 "io"
7 "net/http"
8 "reflect"
9 "strings"
10 "sync"
11 "sync/atomic"
12 "testing"
13 "time"
14
15 "reasonix/internal/event"
16 )
17
18 const runtimeRemoteTestPath = "/sessions/current.jsonl"
19
20 func remoteRuntimeTestSnapshot(epoch string, revision uint64, phase string) event.RuntimeStateSnapshot {
21 return event.RuntimeStateSnapshot{SchemaVersion: 1, RuntimeEpoch: epoch, Revision: revision, Phase: phase,
22 Running: phase == "executing" || phase == "finishing", Cancellable: phase == "executing"}
23 }
24
25 func remoteRuntimeTestApp(client *http.Client) (*App, *remoteTab) {
26 tab := &remoteTab{id: "remote-runtime", state: "ready", gen: 7, selectionRevision: 3,
27 client: client, base: "http://runtime-fixture.invalid", ref: RemoteTabRef{HostID: "fixture-host", Workspace: "/workspace"},
28 session: remoteTabSessionState{name: "current", path: runtimeRemoteTestPath},
29 routing: remoteTabSessionRouting{currentPath: runtimeRemoteTestPath, running: map[string]bool{}},
30 capabilities: map[string]bool{serveCapabilityExecutionV2: true, serveCapabilitySessions: true, serveCapabilitySessionIdentityV1: true, serveCapabilitySessionOwnershipV1: true, "permission-presets-v1": true},
31 }
32 return &App{remoteTabs: map[string]*remoteTab{tab.id: tab}}, tab
33 }
34
35 func remoteRuntimeTestResponse(req *http.Request, code int, body string) *http.Response {
36 return &http.Response{StatusCode: code, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body)), Request: req}
37 }
38
39 func remoteRuntimeTestJSON(t *testing.T, value any) string {
40 t.Helper()
41 raw, err := json.Marshal(value)
42 if err != nil {
43 t.Fatal(err)
44 }
45 return string(raw)
46 }
47
48 func remoteRuntimeTestPayload(t *testing.T, state event.RuntimeStateSnapshot) string {
49 return remoteRuntimeTestJSON(t, map[string]any{"schemaVersion": 1, "sessions": []any{map[string]any{"sessionPath": runtimeRemoteTestPath, "state": state}}})
50 }
51
52 func awaitRemoteRuntimeSync(t *testing.T, result <-chan error) {
53 t.Helper()
54 select {
55 case err := <-result:
56 if err != nil {
57 t.Fatal(err)
58 }
59 case <-time.After(5 * time.Second):
60 t.Fatal("runtime synchronization did not complete")
61 }
62 }
63
64 func TestRemoteRuntimeStateReducerOrdersAndFencesInstances(t *testing.T) {
65 _, tab := remoteRuntimeTestApp(nil)
66 initial := remoteRuntimeTestSnapshot("epoch-a", 3, "executing")
67 if !acceptRemoteRuntimeStateLocked(tab, runtimeRemoteTestPath, initial, true) {
68 t.Fatal("authoritative binding rejected")
69 }
70 hostRevision := tab.runtime.revision
71 for _, state := range []event.RuntimeStateSnapshot{
72 initial, remoteRuntimeTestSnapshot("epoch-a", 2, "idle"),
73 remoteRuntimeTestSnapshot("epoch-a", 3, "idle"), remoteRuntimeTestSnapshot("epoch-b", 9, "idle"),
74 } {
75 if acceptRemoteRuntimeStateLocked(tab, runtimeRemoteTestPath, state, false) {
76 t.Fatalf("accepted duplicate/stale/conflicting/unbound state: %+v", state)
77 }
78 if !reflect.DeepEqual(tab.runtime.snapshot, initial) || tab.runtime.revision != hostRevision {
79 t.Fatalf("rejected state mutated projection: %+v", tab.runtime)
80 }
81 }
82 newer := remoteRuntimeTestSnapshot("epoch-a", 4, "idle")
83 if !acceptRemoteRuntimeStateLocked(tab, runtimeRemoteTestPath, newer, false) || tab.runtime.running {
84 t.Fatal("newer idle did not clear running")
85 }
86 background := remoteRuntimeTestSnapshot("background", 1, "executing")
87 if !acceptRemoteRuntimeStateLocked(tab, "/sessions/background.jsonl", background, true) {
88 t.Fatal("background instance registration failed")
89 }
90 if !reflect.DeepEqual(tab.runtime.snapshot, newer) || !tab.routing.running["/sessions/background.jsonl"] {
91 t.Fatal("background runtime replaced selected session or failed to aggregate")
92 }
93 replacement := remoteRuntimeTestSnapshot("epoch-b", 1, "idle")
94 if !acceptRemoteRuntimeStateLocked(tab, runtimeRemoteTestPath, replacement, true) || !reflect.DeepEqual(tab.runtime.snapshot, replacement) {
95 t.Fatal("authoritative new epoch was rejected")
96 }
97 }
98
99 func TestRemoteRuntimeStateGETCannotOverwriteNewerSSE(t *testing.T) {
100 for _, epochChanged := range []bool{false, true} {
101 name := "same-epoch"
102 if epochChanged {
103 name = "new-epoch"
104 }
105 t.Run(name, func(t *testing.T) {
106 isolateDesktopUserDirs(t)
107 entered, release := make(chan struct{}), make(chan struct{})
108 releaseGET := sync.OnceFunc(func() { close(release) })
109 defer releaseGET()
110 response := remoteRuntimeTestSnapshot("epoch-a", 4, "finishing")
111 if epochChanged {
112 response = remoteRuntimeTestSnapshot("epoch-b", 1, "idle")
113 }
114 body := remoteRuntimeTestPayload(t, response)
115 client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
116 close(entered)
117 <-release
118 return remoteRuntimeTestResponse(req, 200, body), nil
119 })}
120 a, tab := remoteRuntimeTestApp(client)
121 acceptRemoteRuntimeStateLocked(tab, runtimeRemoteTestPath, remoteRuntimeTestSnapshot("epoch-a", 2, "executing"), true)
122 result := make(chan error, 1)
123 go func() { _, err := a.SyncRuntimeState(); result <- err }()
124 select {
125 case <-entered:
126 case <-time.After(5 * time.Second):
127 t.Fatal("GET did not start")
128 }
129 newer := remoteRuntimeTestSnapshot("epoch-a", 5, "idle")
130 frame := json.RawMessage(remoteRuntimeTestJSON(t, map[string]any{"runtimeState": newer}))
131 a.acceptRemoteRuntimeFrame(tab.id, tab.gen, runtimeRemoteTestPath, frame)
132 releaseGET()
133 awaitRemoteRuntimeSync(t, result)
134 if got := tab.runtime.snapshot; !reflect.DeepEqual(got, newer) {
135 t.Fatalf("late GET overwrote newer SSE: got=%+v want=%+v", got, newer)
136 }
137 })
138 }
139 }
140
141 func TestRemoteRuntimeStateGETRejectsChangedSelectionAndGeneration(t *testing.T) {
142 for _, fence := range []string{"generation", "selection", "client", "rehydration"} {
143 t.Run(fence, func(t *testing.T) {
144 isolateDesktopUserDirs(t)
145 entered, release := make(chan struct{}), make(chan struct{})
146 releaseGET := sync.OnceFunc(func() { close(release) })
147 defer releaseGET()
148 body := remoteRuntimeTestPayload(t, remoteRuntimeTestSnapshot("replacement", 9, "executing"))
149 client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
150 close(entered)
151 <-release
152 return remoteRuntimeTestResponse(req, 200, body), nil
153 })}
154 a, tab := remoteRuntimeTestApp(client)
155 initial := remoteRuntimeTestSnapshot("initial", 1, "idle")
156 acceptRemoteRuntimeStateLocked(tab, runtimeRemoteTestPath, initial, true)
157 result := make(chan error, 1)
158 go func() { _, err := a.SyncRuntimeState(); result <- err }()
159 select {
160 case <-entered:
161 case <-time.After(5 * time.Second):
162 t.Fatal("GET did not start")
163 }
164 a.remoteTabMu.Lock()
165 switch fence {
166 case "generation":
167 tab.gen++
168 case "selection":
169 tab.selectionRevision++
170 case "client":
171 tab.client = &http.Client{}
172 case "rehydration":
173 tab.routing.rehydratingPath = "/sessions/next.jsonl"
174 }
175 a.remoteTabMu.Unlock()
176 releaseGET()
177 awaitRemoteRuntimeSync(t, result)
178 if !reflect.DeepEqual(tab.runtime.snapshot, initial) {
179 t.Fatalf("stale %s request changed runtime: %+v", fence, tab.runtime.snapshot)
180 }
181 })
182 }
183 }
184
185 func TestRemoteRuntimeStateSSERejectsOldPumpAndDuplicate(t *testing.T) {
186 a, tab := remoteRuntimeTestApp(nil)
187 initial := remoteRuntimeTestSnapshot("epoch", 2, "executing")
188 acceptRemoteRuntimeStateLocked(tab, runtimeRemoteTestPath, initial, true)
189 var events atomic.Int32
190 a.remoteEventHook = func(string, any) { events.Add(1) }
191 for _, fixture := range []struct {
192 gen uint64
193 state event.RuntimeStateSnapshot
194 }{
195 {tab.gen - 1, remoteRuntimeTestSnapshot("epoch", 3, "idle")}, {tab.gen, initial},
196 } {
197 frame := json.RawMessage(remoteRuntimeTestJSON(t, map[string]any{"runtimeState": fixture.state}))
198 a.acceptRemoteRuntimeFrame(tab.id, fixture.gen, runtimeRemoteTestPath, frame)
199 }
200 if !reflect.DeepEqual(tab.runtime.snapshot, initial) || events.Load() != 0 {
201 t.Fatalf("old/duplicate frame mutated state or notified: state=%+v events=%d", tab.runtime.snapshot, events.Load())
202 }
203 }
204
205 func TestRemoteRuntimeStateLegacy404CachedForConnection(t *testing.T) {
206 isolateDesktopUserDirs(t)
207 var probes, statuses atomic.Int32
208 statusBody := remoteRuntimeTestJSON(t, map[string]any{"sessionPath": runtimeRemoteTestPath, "running": false})
209 client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
210 if req.URL.Path == "/runtime-states" {
211 probes.Add(1)
212 return remoteRuntimeTestResponse(req, 404, "not supported"), nil
213 }
214 if req.URL.Path == "/status" {
215 statuses.Add(1)
216 return remoteRuntimeTestResponse(req, 200, statusBody), nil
217 }
218 return nil, errors.New("unexpected runtime fallback endpoint")
219 })}
220 a, _ := remoteRuntimeTestApp(client)
221 for range 2 {
222 if _, err := a.SyncRuntimeState(); err != nil {
223 t.Fatal(err)
224 }
225 }
226 if probes.Load() != 1 || statuses.Load() != 2 {
227 t.Fatalf("legacy capability fallback count probes=%d statuses=%d", probes.Load(), statuses.Load())
228 }
229 }
230
231 func TestRemoteRuntimeStateReconnectProbesCapabilityAgain(t *testing.T) {
232 isolateDesktopUserDirs(t)
233 var probes atomic.Int32
234 updated := remoteRuntimeTestSnapshot("new-server", 1, "idle")
235 body := remoteRuntimeTestPayload(t, updated)
236 statusBody := remoteRuntimeTestJSON(t, map[string]any{"sessionPath": runtimeRemoteTestPath, "running": false})
237 client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
238 if req.URL.Path == "/runtime-states" {
239 if probes.Add(1) == 1 {
240 return remoteRuntimeTestResponse(req, 404, "old server"), nil
241 }
242 return remoteRuntimeTestResponse(req, 200, body), nil
243 }
244 return remoteRuntimeTestResponse(req, 200, statusBody), nil
245 })}
246 a, tab := remoteRuntimeTestApp(client)
247 if _, err := a.SyncRuntimeState(); err != nil {
248 t.Fatal(err)
249 }
250 a.remoteTabMu.Lock()
251 tab.gen++
252 a.remoteTabMu.Unlock()
253 if _, err := a.SyncRuntimeState(); err != nil {
254 t.Fatal(err)
255 }
256 if probes.Load() != 2 || !reflect.DeepEqual(tab.runtime.snapshot, updated) {
257 t.Fatalf("new connection inherited old capability rejection: probes=%d state=%+v", probes.Load(), tab.runtime.snapshot)
258 }
259 }
260
261 func TestRemoteRuntimeFollowupUnknownPOSTLooksUpReceiptWithoutReplay(t *testing.T) {
262 isolateDesktopUserDirs(t)
263 var posts, lookups atomic.Int32
264 var posted map[string]json.RawMessage
265 receiptBody := remoteRuntimeTestJSON(t, map[string]any{"itemId": "accepted-item", "disposition": "queued"})
266 client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
267 if req.Method == http.MethodPost && req.URL.Path == "/inbox/items" {
268 posts.Add(1)
269 if err := json.NewDecoder(req.Body).Decode(&posted); err != nil {
270 return nil, err
271 }
272 return nil, errors.New("response lost after durable accept")
273 }
274 if req.Method == http.MethodGet && req.URL.Path == "/inbox/receipt" {
275 lookups.Add(1)
276 if req.URL.Query().Get("key") != "stable-key" || req.URL.Query().Get("session") != runtimeRemoteTestPath {
277 return nil, errors.New("receipt lookup lost original session/key")
278 }
279 return remoteRuntimeTestResponse(req, 200, receiptBody), nil
280 }
281 return nil, errors.New("unexpected followup request")
282 })}
283 a, tab := remoteRuntimeTestApp(client)
284 invocations := []InvocationRequest{{Name: "fixture-skill", Kind: "skill", Offset: 0}}
285 receipt, err := a.enqueueRemoteFollowup(tab.id, "rich display", "model input", invocations, "stable-key")
286 if err != nil || receipt.ItemID != "accepted-item" || posts.Load() != 1 || lookups.Load() != 1 {
287 t.Fatalf("unknown write was lost or replayed: receipt=%+v err=%v posts=%d lookups=%d", receipt, err, posts.Load(), lookups.Load())
288 }
289 var display, input, key string
290 var gotInvocations []InvocationRequest
291 _ = json.Unmarshal(posted["display"], &display)
292 _ = json.Unmarshal(posted["input"], &input)
293 _ = json.Unmarshal(posted["idempotencyKey"], &key)
294 _ = json.Unmarshal(posted["invocations"], &gotInvocations)
295 if display != "rich display" || input != "model input" || key != "stable-key" || !reflect.DeepEqual(gotInvocations, invocations) {
296 t.Fatalf("rich followup changed: %s", remoteRuntimeTestJSON(t, posted))
297 }
298 }
299
300 func TestRemoteRuntimeFollowupRejectedPOSTCannotReuseOlderReceipt(t *testing.T) {
301 isolateDesktopUserDirs(t)
302 var lookups atomic.Int32
303 client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
304 if req.Method == http.MethodPost {
305 return remoteRuntimeTestResponse(req, http.StatusConflict, "idempotency conflict"), nil
306 }
307 lookups.Add(1)
308 return remoteRuntimeTestResponse(req, http.StatusOK, `{"itemId":"older-request"}`), nil
309 })}
310 a, tab := remoteRuntimeTestApp(client)
311 receipt, err := a.enqueueRemoteFollowup(tab.id, "changed draft", "changed input", nil, "reused-key")
312 if err == nil || receipt.ItemID != "" || lookups.Load() != 0 {
313 t.Fatalf("definite rejection adopted unrelated receipt: receipt=%+v err=%v lookups=%d", receipt, err, lookups.Load())
314 }
315 }
316
317 func TestRemoteRuntimeStateDisconnectPreservesWorkAndReconnectAdoptsEpoch(t *testing.T) {
318 isolateDesktopUserDirs(t)
319 reconnected := remoteRuntimeTestSnapshot("restarted-server", 1, "idle")
320 body := remoteRuntimeTestPayload(t, reconnected)
321 client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
322 return remoteRuntimeTestResponse(req, 200, body), nil
323 })}
324 a, tab := remoteRuntimeTestApp(client)
325 old := remoteRuntimeTestSnapshot("old-server", 8, "executing")
326 old.BackgroundJobs = 1
327 acceptRemoteRuntimeStateLocked(tab, runtimeRemoteTestPath, old, true)
328 oldGen, base := tab.gen, tab.base
329 if !a.reconnectRemoteTabGeneration(tab.id, oldGen) {
330 t.Fatal("current pump did not enter reconnecting")
331 }
332 disconnected := a.GetRuntimeStateSnapshot()
333 if len(disconnected.Sessions) != 1 || disconnected.Sessions[0].Freshness != "unknown" || !reflect.DeepEqual(disconnected.Sessions[0].State, old) {
334 t.Fatalf("disconnect fabricated completion or trusted stale work: %+v", disconnected)
335 }
336 if _, _, _, err := a.remoteTabCommandTarget(tab.id); err == nil {
337 t.Fatal("disconnected session still accepts commands")
338 }
339 // Complete the authenticated connection generation; the state GET may
340 // establish the new server epoch while old pump frames remain fenced out.
341 a.remoteTabMu.Lock()
342 tab.client, tab.base, tab.state = client, base, "ready"
343 a.remoteTabMu.Unlock()
344 if _, err := a.SyncRuntimeState(); err != nil {
345 t.Fatal(err)
346 }
347 stale := remoteRuntimeTestSnapshot("old-server", 99, "executing")
348 frame := json.RawMessage(remoteRuntimeTestJSON(t, map[string]any{"runtimeState": stale}))
349 a.acceptRemoteRuntimeFrame(tab.id, oldGen, runtimeRemoteTestPath, frame)
350 current := a.GetRuntimeStateSnapshot()
351 if !reflect.DeepEqual(current.Sessions[0].State, reconnected) || current.Sessions[0].Freshness != "synced" {
352 t.Fatalf("reconnect did not converge to new authority: %+v", current)
353 }
354 }
355
356 func TestRemoteRuntimeStateOldSelectionFrameOnlyUpdatesBackground(t *testing.T) {
357 isolateDesktopUserDirs(t)
358 a, tab := remoteRuntimeTestApp(nil)
359 old := remoteRuntimeTestSnapshot("old-selection", 2, "executing")
360 acceptRemoteRuntimeStateLocked(tab, runtimeRemoteTestPath, old, true)
361 nextPath := "/sessions/next.jsonl"
362 next := remoteRuntimeTestSnapshot("new-selection", 1, "executing")
363 a.remoteTabMu.Lock()
364 tab.selectionRevision++
365 commitRemoteTabAttachRoute(tab, nextPath, false)
366 acceptRemoteRuntimeStateLocked(tab, nextPath, next, true)
367 a.remoteTabMu.Unlock()
368 oldCompleted := remoteRuntimeTestSnapshot("old-selection", 3, "idle")
369 frame := json.RawMessage(remoteRuntimeTestJSON(t, map[string]any{"runtimeState": oldCompleted}))
370 a.acceptRemoteRuntimeFrame(tab.id, tab.gen, runtimeRemoteTestPath, frame)
371 if tab.routing.currentPath != nextPath || !reflect.DeepEqual(tab.runtime.snapshot, next) {
372 t.Fatalf("old selection frame changed foreground: path=%q state=%+v", tab.routing.currentPath, tab.runtime.snapshot)
373 }
374 if !reflect.DeepEqual(tab.runtimeStates[runtimeRemoteTestPath], oldCompleted) || tab.routing.running[runtimeRemoteTestPath] {
375 t.Fatal("old selection completion was lost from background aggregation")
376 }
377 }
378
379 func TestRemoteRuntimeStatePublicationOrdersGenerationRetirement(t *testing.T) {
380 isolateDesktopUserDirs(t)
381 a, tab := remoteRuntimeTestApp(&http.Client{})
382 initial := remoteRuntimeTestSnapshot("current", 1, "executing")
383 acceptRemoteRuntimeStateLocked(tab, runtimeRemoteTestPath, initial, true)
384 entered, release := make(chan struct{}), make(chan struct{})
385 unblock := sync.OnceFunc(func() { close(release) })
386 defer unblock()
387 a.remoteEventHook = func(name string, _ any) {
388 if name == "remote-tab:updated" {
389 close(entered)
390 <-release
391 }
392 }
393 finished := make(chan struct{})
394 next := remoteRuntimeTestSnapshot("current", 2, "idle")
395 frame := json.RawMessage(remoteRuntimeTestJSON(t, map[string]any{"runtimeState": next}))
396 go func() { a.acceptRemoteRuntimeFrame(tab.id, 7, runtimeRemoteTestPath, frame); close(finished) }()
397 select {
398 case <-entered:
399 case <-time.After(5 * time.Second):
400 t.Fatal("runtime frame did not reach metadata publication")
401 }
402 attempted, retired := make(chan struct{}), make(chan struct{})
403 go func() { close(attempted); a.reconnectRemoteTabGeneration(tab.id, 7); close(retired) }()
404 <-attempted
405 // Match the existing remote publication regression's interleaving: the
406 // callback is held at a known boundary, while retirement attempts the same
407 // publication fence. This timeout only verifies that it remains blocked.
408 select {
409 case <-retired:
410 t.Fatal("generation retirement overtook in-flight runtime metadata; stale frame can follow the reconnect barrier")
411 case <-time.After(30 * time.Millisecond):
412 }
413 a.remoteTabMu.Lock()
414 intact := tab.gen == 7 && tab.state == "ready"
415 a.remoteTabMu.Unlock()
416 if !intact {
417 t.Fatal("generation changed before prior runtime publication completed")
418 }
419 unblock()
420 select {
421 case <-finished:
422 case <-time.After(5 * time.Second):
423 t.Fatal("runtime publication did not finish")
424 }
425 select {
426 case <-retired:
427 case <-time.After(5 * time.Second):
428 t.Fatal("generation retirement did not finish")
429 }
430 }
431
431 lines GO