返回 DeepSeek-Reasonix
remote_tab_review_fences_test.go
根目录 / desktop / remote_tab_review_fences_test.go
1 package main
2
3 import (
4 "context"
5 "net/http"
6 "net/http/httptest"
7 "testing"
8 "time"
9 )
10
11 func TestNewerTerminalStateOverridesStaleRunningListingRow(t *testing.T) {
12 testTerminalListingRace(t, false, false)
13 }
14
15 func TestNewerTerminalStateRetainsBackgroundRunningListingRow(t *testing.T) {
16 testTerminalListingRace(t, true, false)
17 }
18
19 func TestRecoveredTerminalPathRetriesStaleRunningListingRow(t *testing.T) {
20 testTerminalListingRace(t, false, true)
21 }
22
23 func testTerminalListingRace(t *testing.T, serveStillRunning, recovered bool) {
24 t.Helper()
25 const path = "/sessions/current.jsonl"
26 fs := newFakeServe(t, "s3cret", []serveSessionEntry{{Name: "current", Path: path, Current: true, Running: true}})
27 kernel := &fakeRemoteKernel{
28 statuses: []RemoteConnectionStatusView{{HostID: "box", State: "connected"}},
29 ensureView: RemoteServerView{HostID: "box", State: "ready", LocalURL: fs.server.URL}, ensureToken: "s3cret",
30 }
31 seedBridgeTestHost(t, "box")
32 a := &App{remoteRuntime: kernel}
33 cleanupRemoteTabPumps(t, a)
34 meta := openReadyRemoteTab(t, a, RemoteTabOpenOptions{SessionPath: path})
35 fs.mu.Lock()
36 fs.sessionsStarted, fs.sessionsRelease = make(chan struct{}, 1), make(chan struct{})
37 started, release := fs.sessionsStarted, fs.sessionsRelease
38 fs.mu.Unlock()
39 t.Cleanup(func() { closeTestSignal(release) })
40 type result struct {
41 sessions []RemoteSessionView
42 err error
43 }
44 done := make(chan result, 1)
45 go func() {
46 sessions, err := a.RemoteProjectSessions("box", "~/app")
47 done <- result{sessions, err}
48 }()
49 waitTestSignal(t, started, "session listing did not start")
50 a.remoteTabMu.Lock()
51 gen := a.remoteTabs[meta.ID].gen
52 a.remoteTabMu.Unlock()
53 terminalPath := path
54 if recovered {
55 terminalPath = "/sessions/current-recovery.jsonl"
56 }
57 if routed := a.routeRemoteTabFrame(meta.ID, gen, terminalPath, "turn_done"); routed != !recovered {
58 t.Fatalf("terminal route result = %v, recovered=%v", routed, recovered)
59 }
60 fs.mu.Lock()
61 fs.sessions[0] = serveSessionEntry{Name: "current", Path: terminalPath, Current: true, Running: serveStillRunning}
62 fs.mu.Unlock()
63 closeTestSignal(release)
64 var got result
65 select {
66 case got = <-done:
67 case <-time.After(time.Second):
68 t.Fatal("session listing did not finish")
69 }
70 if got.err != nil {
71 t.Fatal(got.err)
72 }
73 for _, session := range got.sessions {
74 if session.Path == terminalPath && session.Running == serveStillRunning {
75 return
76 }
77 }
78 t.Fatalf("running row did not reconcile to %v: %+v", serveStillRunning, got.sessions)
79 }
80
81 func TestAttachRemoteTabServeRejectsResponseAfterNewerAdoption(t *testing.T) {
82 const responsePath, newerPath = "/sessions/response.jsonl", "/sessions/newer.jsonl"
83 feed := make(chan string, 1)
84 fs := newFakeServe(t, "s3cret", nil)
85 fs.mu.Lock()
86 fs.eventFeed, fs.newSessionPath = feed, responsePath
87 fs.newStarted, fs.newRelease = make(chan struct{}, 1), make(chan struct{})
88 started, release := fs.newStarted, fs.newRelease
89 fs.mu.Unlock()
90 t.Cleanup(func() { closeTestSignal(release) })
91 tab := &remoteTab{id: "remote-1", state: "connecting", routing: remoteTabSessionRouting{running: map[string]bool{}}}
92 a := &App{remoteTabs: map[string]*remoteTab{tab.id: tab}}
93 ctx, cancel := context.WithCancel(t.Context())
94 defer cancel()
95 type result struct {
96 entered bool
97 err error
98 }
99 done := make(chan result, 1)
100 go func() {
101 entered, err := a.attachRemoteTabServe(ctx, tab.id, fs.server.URL, "s3cret", "serve-1", RemoteTabOpenOptions{NewSession: true})
102 done <- result{entered, err}
103 }()
104 waitTestSignal(t, started, "new-session attach did not start")
105 feed <- `{"kind":"session_changed","sessionPath":"/sessions/newer.jsonl","sessionCurrent":true}`
106 waitRemoteTestPath(t, a, tab, newerPath)
107 closeTestSignal(release)
108 select {
109 case got := <-done:
110 if got.err != nil || got.entered {
111 t.Fatalf("rejected attach result = entered %v, err %v", got.entered, got.err)
112 }
113 case <-time.After(time.Second):
114 t.Fatal("new-session attach did not finish")
115 }
116 waitRemoteTestPath(t, a, tab, newerPath)
117 }
118
119 func TestRemoteModelResponseCannotLabelNewerForegroundSession(t *testing.T) {
120 testRemoteModelRouteFence(t, false)
121 }
122
123 func TestRemoteModelResponseLabelsExpectedSessionAfterABARouteChange(t *testing.T) {
124 testRemoteModelRouteFence(t, true)
125 }
126
127 func testRemoteModelRouteFence(t *testing.T, returnToExpected bool) {
128 t.Helper()
129 isolateDesktopUserDirs(t)
130 const oldPath, newerPath = "/sessions/old.jsonl", "/sessions/newer.jsonl"
131 started, release := make(chan struct{}), make(chan struct{})
132 var requestPath string
133 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
134 requestPath = r.Header.Get(expectedSessionPathHeader)
135 close(started)
136 <-release
137 w.WriteHeader(http.StatusNoContent)
138 }))
139 defer server.Close()
140 tab := &remoteTab{
141 id: "remote-1", state: "ready", client: server.Client(), base: server.URL, gen: 7, model: "old-model",
142 routing: remoteTabSessionRouting{currentPath: oldPath, pathRevision: 2, running: map[string]bool{}},
143 }
144 a := &App{remoteTabs: map[string]*remoteTab{tab.id: tab}}
145 done := make(chan error, 1)
146 go func() { done <- a.SetRemoteTabModel(tab.id, "next-model") }()
147 waitTestSignal(t, started, "model request did not start")
148 a.adoptRemoteTabFrameCurrent(tab.id, tab.gen, newerPath, true)
149 if returnToExpected {
150 a.adoptRemoteTabFrameCurrent(tab.id, tab.gen, oldPath, true)
151 }
152 close(release)
153 if err := <-done; err != nil {
154 t.Fatal(err)
155 }
156 a.remoteTabMu.Lock()
157 model, path := tab.model, tab.routing.currentPath
158 a.remoteTabMu.Unlock()
159 wantPath, wantModel := newerPath, "old-model"
160 if returnToExpected {
161 wantPath, wantModel = oldPath, "next-model"
162 }
163 if requestPath != oldPath || path != wantPath || model != wantModel {
164 t.Fatalf("request/model/path = %q/%q/%q, want %q/%q/%q", requestPath, model, path, oldPath, wantModel, wantPath)
165 }
166 }
167
168 func closeTestSignal(ch chan struct{}) {
169 select {
170 case <-ch:
171 default:
172 close(ch)
173 }
174 }
175
176 func waitTestSignal(t *testing.T, ch <-chan struct{}, message string) {
177 t.Helper()
178 select {
179 case <-ch:
180 case <-time.After(time.Second):
181 t.Fatal(message)
182 }
183 }
184
185 func waitRemoteTestPath(t *testing.T, a *App, tab *remoteTab, want string) {
186 t.Helper()
187 deadline := time.Now().Add(time.Second)
188 for {
189 a.remoteTabMu.Lock()
190 path := tab.routing.currentPath
191 a.remoteTabMu.Unlock()
192 if path == want {
193 return
194 }
195 if time.Now().After(deadline) {
196 t.Fatalf("remote path = %q, want %q", path, want)
197 }
198 time.Sleep(time.Millisecond)
199 }
200 }
201
201 lines GO