返回 DeepSeek-Reasonix
remote_tab_title_fence_test.go
根目录 / desktop / remote_tab_title_fence_test.go
1 package main
2
3 import (
4 "testing"
5 "time"
6
7 "reasonix/internal/config"
8 )
9
10 func TestRemoteTabTitleRefreshRejectsDifferentServeCurrent(t *testing.T) {
11 fs := newFakeServe(t, "s3cret", []serveSessionEntry{
12 {Name: "a", Path: "/a.jsonl", Title: "Title A", Current: true},
13 {Name: "b", Path: "/b.jsonl", Title: "Title B"},
14 })
15 kernel := &fakeRemoteKernel{
16 statuses: []RemoteConnectionStatusView{{HostID: "box", State: "connected"}},
17 ensureView: RemoteServerView{HostID: "box", State: "ready", LocalURL: fs.server.URL}, ensureToken: "s3cret",
18 }
19 seedBridgeTestHost(t, "box")
20 a := &App{remoteRuntime: kernel}
21 cleanupRemoteTabPumps(t, a)
22 meta := openReadyRemoteTab(t, a, RemoteTabOpenOptions{SessionName: "a", SessionPath: "/a.jsonl", SessionTitle: "Selected A"})
23 fs.mu.Lock()
24 fs.sessions[0].Current = false
25 fs.sessions[1].Current = true
26 fs.mu.Unlock()
27
28 a.refreshRemoteTabTitle(meta.ID)
29
30 a.remoteTabMu.Lock()
31 tab := a.remoteTabs[meta.ID]
32 name, sessionPath, route, title := tab.session.name, tab.session.path, tab.routing.currentPath, tab.topicTitle
33 a.remoteTabMu.Unlock()
34 if name != "a" || sessionPath != "/a.jsonl" || route != "/a.jsonl" || title != "Selected A" {
35 t.Fatalf("different Serve current replaced title-refresh target: %q/%q/%q/%q", name, sessionPath, route, title)
36 }
37 }
38
39 func TestRemoteTabTitleRefreshKeepsBlankPreferencesAfterReconnect(t *testing.T) {
40 fs := newFakeServe(t, "s3cret", nil)
41 kernel := &fakeRemoteKernel{
42 statuses: []RemoteConnectionStatusView{{HostID: "box", State: "connected"}},
43 ensureView: RemoteServerView{HostID: "box", State: "ready", LocalURL: fs.server.URL}, ensureToken: "s3cret",
44 }
45 seedBridgeTestHost(t, "box")
46 a := &App{remoteRuntime: kernel}
47 cleanupRemoteTabPumps(t, a)
48 meta := openReadyRemoteTab(t, a, RemoteTabOpenOptions{NewSession: true})
49 if err := setRemoteSessionTitleOverride("box", "~/app", "", "Unsaved title"); err != nil {
50 t.Fatal(err)
51 }
52 if err := setRemoteSessionPinned("box", "~/app", "", true); err != nil {
53 t.Fatal(err)
54 }
55
56 fs.mu.Lock()
57 fs.sessions = []serveSessionEntry{{Name: "stale", Path: "/stale.jsonl", Title: "Serve title", Current: true}}
58 fs.sessionsStarted = make(chan struct{}, 1)
59 fs.sessionsRelease = make(chan struct{})
60 started, release := fs.sessionsStarted, fs.sessionsRelease
61 fs.mu.Unlock()
62 done := make(chan struct{})
63 go func() { a.refreshRemoteTabTitle(meta.ID); close(done) }()
64 select {
65 case <-started:
66 case <-time.After(time.Second):
67 t.Fatal("title refresh did not start")
68 }
69 a.remoteTabMu.Lock()
70 a.remoteTabs[meta.ID].gen++
71 a.remoteTabMu.Unlock()
72 close(release)
73 select {
74 case <-done:
75 case <-time.After(time.Second):
76 t.Fatal("title refresh did not return")
77 }
78
79 if got := remoteSessionTitleOverride("box", "~/app", ""); got != "Unsaved title" {
80 t.Fatalf("blank title after stale response = %q", got)
81 }
82 if !remoteSessionPinned("box", "~/app", "") {
83 t.Fatal("blank pin was removed by a stale response")
84 }
85 if got := remoteSessionTitleOverride("box", "~/app", "stale"); got != "" || remoteSessionPinned("box", "~/app", "stale") {
86 t.Fatalf("stale response received blank preferences: title=%q pinned=%v", got, remoteSessionPinned("box", "~/app", "stale"))
87 }
88 }
89
90 func TestRemoteTabTitleRefreshMigratesPreferencesWithoutHoldingRegistryLock(t *testing.T) {
91 fs := newFakeServe(t, "s3cret", nil)
92 kernel := &fakeRemoteKernel{
93 statuses: []RemoteConnectionStatusView{{HostID: "box", State: "connected"}},
94 ensureView: RemoteServerView{HostID: "box", State: "ready", LocalURL: fs.server.URL}, ensureToken: "s3cret",
95 }
96 seedBridgeTestHost(t, "box")
97 a := &App{remoteRuntime: kernel}
98 cleanupRemoteTabPumps(t, a)
99 meta := openReadyRemoteTab(t, a, RemoteTabOpenOptions{NewSession: true})
100 if err := setRemoteSessionTitleOverride("box", "~/app", "", "Unsaved title"); err != nil {
101 t.Fatal(err)
102 }
103
104 fs.mu.Lock()
105 fs.sessions = []serveSessionEntry{{Name: "fresh", Path: "/fresh.jsonl", Title: "Serve title", Current: true}}
106 fs.sessionsStarted = make(chan struct{}, 1)
107 fs.sessionsRelease = make(chan struct{})
108 started, release := fs.sessionsStarted, fs.sessionsRelease
109 fs.mu.Unlock()
110 done := make(chan struct{})
111 go func() { a.refreshRemoteTabTitle(meta.ID); close(done) }()
112 select {
113 case <-started:
114 case <-time.After(time.Second):
115 t.Fatal("title refresh did not start")
116 }
117
118 unlockPrefs, err := config.LockConfigFileEdits(remotePrefsPath())
119 if err != nil {
120 t.Fatal(err)
121 }
122 close(release)
123 deadline := time.Now().Add(time.Second)
124 for remotePrefsMu.TryLock() {
125 remotePrefsMu.Unlock()
126 if time.Now().After(deadline) {
127 unlockPrefs()
128 t.Fatal("title refresh did not enter preference migration")
129 }
130 time.Sleep(time.Millisecond)
131 }
132
133 registryAvailable := make(chan bool, 1)
134 go func() {
135 a.remoteTabMu.Lock()
136 registered := a.remoteTabs[meta.ID] != nil
137 a.remoteTabMu.Unlock()
138 registryAvailable <- registered
139 }()
140 select {
141 case registered := <-registryAvailable:
142 if !registered {
143 unlockPrefs()
144 t.Fatal("remote tab disappeared during preference migration")
145 }
146 case <-time.After(250 * time.Millisecond):
147 unlockPrefs()
148 <-registryAvailable
149 t.Fatal("preference migration held the global remote tab registry lock")
150 }
151 unlockPrefs()
152 select {
153 case <-done:
154 case <-time.After(time.Second):
155 t.Fatal("title refresh did not finish after preference lock release")
156 }
157
158 a.remoteTabMu.Lock()
159 tab := a.remoteTabs[meta.ID]
160 name, path, title := tab.session.name, tab.session.path, tab.topicTitle
161 a.remoteTabMu.Unlock()
162 if name != "fresh" || path != "/fresh.jsonl" || title != "Unsaved title" {
163 t.Fatalf("materialized identity/title = %q/%q/%q", name, path, title)
164 }
165 }
166
166 lines GO