返回 DeepSeek-Reasonix
dormant_tab_restore_test.go
根目录 / desktop / dormant_tab_restore_test.go
1 package main
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9 )
10
11 // writeRemoteOnlyTabsFile persists a layout whose only surface is a remote
12 // shell: the state a single-surface remote user leaves behind on quit.
13 func writeRemoteOnlyTabsFile(t *testing.T) {
14 t.Helper()
15 dir := desktopConfigDir()
16 if err := os.MkdirAll(dir, 0o755); err != nil {
17 t.Fatal(err)
18 }
19 body := `{"tabs":null,"activeTab":"r1","remoteTabs":[{"id":"r1","hostId":"box","workspace":"~/app"}],"remoteTabOrder":["r1"],"tabOrder":["r1"]}`
20 if err := os.WriteFile(filepath.Join(dir, tabsFileName), []byte(body), 0o644); err != nil {
21 t.Fatal(err)
22 }
23 }
24
25 // A remote-only layout restores one dormant workspace tab so local commands
26 // have a target. It must satisfy the base ownership contract on that first
27 // build — a resolvable workspace identity — while never taking the visible
28 // surface from the remote shell it was restored beside.
29 func TestRemoteOnlyRestoreBuildsOwnedDormantTabWithoutTakingTheSurface(t *testing.T) {
30 isolateDesktopUserDirs(t)
31 seedBridgeTestHost(t, "box")
32 writeRemoteOnlyTabsFile(t)
33 a := NewApp()
34 a.ctx = t.Context()
35 a.readyHook = func() {}
36 a.remoteRuntime = &fakeRemoteKernel{}
37 t.Cleanup(func() { a.shutdown(context.Background()) })
38
39 a.restoreOrBuildTabs()
40
41 dormant, ctrl := a.activeOrSingleLocalTab()
42 if dormant == nil {
43 t.Fatal("remote-only restore left local commands without a target tab")
44 }
45 if ctrl != nil {
46 t.Fatal("dormant tab built a runtime at startup")
47 }
48 // 8955c156f resolves ownership by workspace identity, so the tab must
49 // carry one before its first build rather than deriving it later.
50 if dormant.SessionWorkspace.ID == "" || dormant.SessionWorkspace.ID != desktopWorkspaceID("global", globalTabWorkspaceRoot()) {
51 t.Fatalf("dormant workspace identity = %q, want the global workspace id", dormant.SessionWorkspace.ID)
52 }
53 if dormant.Scope != "global" || dormant.WorkspaceRoot != globalTabWorkspaceRoot() {
54 t.Fatalf("dormant scope/root = %q/%q, want the global workspace", dormant.Scope, dormant.WorkspaceRoot)
55 }
56 a.mu.RLock()
57 activeID, tabCount := a.activeTabID, len(a.tabs)
58 a.mu.RUnlock()
59 if activeID != "" || tabCount != 1 {
60 t.Fatalf("restore activeTabID=%q localTabs=%d, want one inactive dormant tab", activeID, tabCount)
61 }
62
63 // The remote shell owns the visible surface; no fallback Global tab may
64 // claim it, before or after the frontend activates the remote surface.
65 a.remoteTabMu.Lock()
66 a.remoteTabLayout.activeID = "r1"
67 a.remoteTabMu.Unlock()
68 for _, meta := range a.ListTabs() {
69 if meta.Remote == nil && meta.Active {
70 t.Fatalf("dormant local tab claimed the visible surface: %+v", meta)
71 }
72 if meta.Remote != nil && !meta.Active {
73 t.Fatalf("remote surface lost the visible surface: %+v", meta)
74 }
75 }
76
77 // Activating the dormant tab is the first demand for a runtime.
78 if err := a.SetActiveTab(dormant.ID); err != nil {
79 t.Fatal(err)
80 }
81 deadline := time.Now().Add(20 * time.Second)
82 for a.controllerForTab(dormant) == nil {
83 if time.Now().After(deadline) {
84 t.Fatal("activating the dormant tab never built a runtime")
85 }
86 time.Sleep(5 * time.Millisecond)
87 }
88 }
89
90 // The dormant tab is resolved before it owns a runtime, so a concurrent
91 // activation can publish one while the open is still waiting for the rebuild
92 // lock. Adopting the runtime the tab owns at that point — not the one read
93 // before the wait — keeps the open from failing with a spurious
94 // "tab runtime changed" error.
95 func TestOpenSessionAdoptsRuntimePublishedWhileWaitingForTheRebuildLock(t *testing.T) {
96 app, tab, ctrl, _ := auditMigratedTab(t)
97 ref, ok := ctrl.SessionRef()
98 if !ok {
99 t.Fatal("fixture controller has no session ref")
100 }
101 // Model the caller that resolved the tab while it was still dormant.
102 app.mu.Lock()
103 tab.Ctrl = nil
104 app.mu.Unlock()
105
106 // Holding turnStartMu parks the open after it takes runtimeRebuildMu, so
107 // the publication below is ordered strictly after the resolve it races.
108 tab.turnStartMu.Lock()
109 done := make(chan error, 1)
110 go func() {
111 _, err := app.resumeCanonicalSessionForTranscript(tab, nil, sessionRoute(ref.SessionID), defaultHistoryPageTurns, false)
112 done <- err
113 }()
114 // The open owns the rebuild lock once TryLock fails; it is then parked on
115 // turnStartMu, strictly after the resolve this publication races.
116 deadline := time.Now().Add(20 * time.Second)
117 for app.runtimeRebuildMu.TryLock() {
118 app.runtimeRebuildMu.Unlock()
119 if time.Now().After(deadline) {
120 tab.turnStartMu.Unlock()
121 t.Fatal("the open never reached the rebuild lock")
122 }
123 time.Sleep(time.Millisecond)
124 }
125 app.mu.Lock()
126 tab.Ctrl = ctrl
127 app.mu.Unlock()
128 tab.turnStartMu.Unlock()
129
130 select {
131 case err := <-done:
132 if err != nil {
133 t.Fatalf("open adopted a stale nil runtime: %v", err)
134 }
135 case <-time.After(30 * time.Second):
136 t.Fatal("open did not finish after the runtime was published")
137 }
138 if got := app.controllerForTab(tab); got != ctrl {
139 t.Fatalf("tab controller after open = %p, want the published runtime %p", got, ctrl)
140 }
141 }
142
142 lines GO