返回 DeepSeek-Reasonix
remote_submit_route_race_test.go
根目录 / desktop / remote_submit_route_race_test.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "net/http/httptest"
7 "os"
8 "path/filepath"
9 "testing"
10 "time"
11
12 "reasonix/internal/config"
13 "reasonix/internal/control"
14 "reasonix/internal/event"
15 "reasonix/internal/serve"
16 )
17
18 // A submit that races a session switch is refused by the serve's
19 // expected-session fence ("active session changed"). The desktop must retry it
20 // against the settled route instead of surfacing a failure the user cannot act
21 // on, and must report a transient outcome once the retry window closes.
22 func TestRemoteSubmitRacingRouteChangeRetriesThenReportsTransient(t *testing.T) {
23 isolateDesktopUserDirs(t)
24 dir := t.TempDir()
25 path := filepath.Join(dir, "remote.jsonl")
26 if err := os.WriteFile(path, nil, 0o600); err != nil {
27 t.Fatal(err)
28 }
29 runner := remoteInboxRunner{started: make(chan string, 4), release: make(chan struct{}, 4)}
30 sink := &remoteInboxSink{states: make(chan event.RuntimeStateSnapshot, 64)}
31 ctrl := control.New(control.Options{SessionDir: dir, SessionPath: path, Runner: runner, Sink: sink})
32 server := httptest.NewServer(operatorServeHandler(serve.New(ctrl, nil, config.ServeConfig{})))
33 defer server.Close()
34 t.Cleanup(func() { closeRemoteTestController(t, ctrl) })
35
36 a, tab := remoteRuntimeTestApp(server.Client())
37 a.ctx = context.Background()
38 tab.base, tab.session.path = server.URL, path
39
40 // The desktop's route is ahead of the serve's foreground (the shape of a
41 // submit that raced a switch): every attempt is fenced.
42 tab.routing.currentPath = "session-id:not-the-current-session"
43 err := a.SubmitRemoteTabWithSubmission(tab.id, "raced message", "route-race-1")
44 var coded interface {
45 error
46 RPCErrorData() map[string]any
47 }
48 if !errors.As(err, &coded) || coded.Error() != "reasonix_error:inbox_target_transient" {
49 t.Fatalf("raced submit = %v, want the transient outcome", err)
50 }
51 if coded.RPCErrorData()["transient"] != true {
52 t.Fatalf("transient outcome carries no transient marker: %v", coded.RPCErrorData())
53 }
54
55 // Once the route settles the same message submits normally.
56 tab.routing.currentPath = path
57 if err := a.SubmitRemoteTabWithSubmission(tab.id, "settled message", "route-race-2"); err != nil {
58 t.Fatalf("settled submit failed: %v", err)
59 }
60 select {
61 case <-runner.started:
62 case <-time.After(5 * time.Second):
63 t.Fatal("the settled submit never started a turn")
64 }
65
66 // Let the previous turn finish so the next case tests the route fence and
67 // not the busy fence.
68 runner.release <- struct{}{}
69 deadline := time.Now().Add(5 * time.Second)
70 for ctrl.Running() {
71 if time.Now().After(deadline) {
72 t.Fatal("turn did not finish before the route-recovery case")
73 }
74 time.Sleep(20 * time.Millisecond)
75 }
76
77 // A route that settles inside the retry window is recovered silently.
78 tab.routing.currentPath = "session-id:not-the-current-session"
79 go func() {
80 time.Sleep(60 * time.Millisecond)
81 a.remoteTabMu.Lock()
82 if current := a.remoteTabs[tab.id]; current != nil {
83 current.routing.currentPath = path
84 }
85 a.remoteTabMu.Unlock()
86 }()
87 if err := a.SubmitRemoteTabWithSubmission(tab.id, "recovered message", "route-race-3"); err != nil {
88 t.Fatalf("submit recovered by a settling route failed: %v", err)
89 }
90 select {
91 case <-runner.started:
92 case <-time.After(5 * time.Second):
93 t.Fatal("the recovered submit never started a turn")
94 }
95 runner.release <- struct{}{}
96 }
97
97 lines GO