返回 DeepSeek-Reasonix
background_runtime_test.go
根目录 / desktop / background_runtime_test.go
1 package main
2
3 import (
4 "os"
5 "path/filepath"
6 "testing"
7
8 "reasonix/internal/control"
9 "reasonix/internal/jobs"
10 "reasonix/internal/workspacelease"
11 )
12
13 type backgroundRuntimeController struct {
14 stubSessionAPI
15 status control.RuntimeStatus
16 jobs []jobs.View
17 lease workspacelease.State
18 leaseKeys []string
19 cancelled []string
20 turnCancel int
21 }
22
23 func (c *backgroundRuntimeController) RuntimeStatus() control.RuntimeStatus { return c.status }
24 func (c *backgroundRuntimeController) Jobs() []jobs.View {
25 return append([]jobs.View(nil), c.jobs...)
26 }
27 func (c *backgroundRuntimeController) CancelJob(id string) bool {
28 for _, job := range c.jobs {
29 if job.ID == id {
30 c.cancelled = append(c.cancelled, id)
31 return true
32 }
33 }
34 return false
35 }
36 func (c *backgroundRuntimeController) Cancel() { c.turnCancel++ }
37 func (c *backgroundRuntimeController) WorkspaceLeaseState() workspacelease.State {
38 state := c.lease
39 state.HeldKeys = append([]string(nil), c.leaseKeys...)
40 return state
41 }
42 func (c *backgroundRuntimeController) WorkspaceLeaseHeldKeys() []string {
43 return append([]string(nil), c.leaseKeys...)
44 }
45
46 func TestBackgroundRuntimeAPIsKeepDetachedJobsActionable(t *testing.T) {
47 targetCtrl := &backgroundRuntimeController{
48 status: control.RuntimeStatus{BackgroundJobs: 1},
49 jobs: []jobs.View{{ID: "job-target", Kind: "bash", Label: "target tests", Status: "running", StartedAt: 1}},
50 }
51 focusedCtrl := &backgroundRuntimeController{
52 status: control.RuntimeStatus{BackgroundJobs: 1},
53 jobs: []jobs.View{{ID: "job-focused", Kind: "bash", Label: "focused tests", Status: "running", StartedAt: 2}},
54 }
55 target := &WorkspaceTab{ID: "target", TopicTitle: "Detached task", Ctrl: targetCtrl}
56 focused := &WorkspaceTab{ID: "focused", TopicTitle: "Focused task", Ctrl: focusedCtrl}
57 app := &App{
58 tabs: map[string]*WorkspaceTab{"focused": focused},
59 tabOrder: []string{"focused"},
60 activeTabID: "focused",
61 detachedSessions: map[string]*WorkspaceTab{"session-target": target},
62 }
63
64 work := app.ActiveWorkForTab("target")
65 if len(work.Jobs) != 1 || work.Jobs[0].ID != "job-target" {
66 t.Fatalf("ActiveWorkForTab(target) = %+v, want target job", work)
67 }
68 result, err := app.CancelJobsForTab("target", []string{"job-target", "job-target", "missing"})
69 if err != nil {
70 t.Fatalf("CancelJobsForTab: %v", err)
71 }
72 if len(result.Cancelled) != 1 || result.Cancelled[0] != "job-target" || len(result.NotRunning) != 1 || result.NotRunning[0] != "missing" {
73 t.Fatalf("CancelJobsForTab result = %+v", result)
74 }
75 if len(targetCtrl.cancelled) != 1 || len(focusedCtrl.cancelled) != 0 {
76 t.Fatalf("cancellation routing target=%v focused=%v", targetCtrl.cancelled, focusedCtrl.cancelled)
77 }
78
79 runtimes := app.BackgroundRuntimes()
80 if len(runtimes) != 2 {
81 t.Fatalf("BackgroundRuntimes = %+v, want visible and detached", runtimes)
82 }
83 var foundDetached bool
84 for _, runtime := range runtimes {
85 if runtime.TabID == "target" {
86 foundDetached = runtime.Detached && runtime.Title == "Detached task" && len(runtime.Jobs) == 1
87 }
88 }
89 if !foundDetached {
90 t.Fatalf("detached runtime missing from %+v", runtimes)
91 }
92 }
93
94 func TestCancelJobForLocalRuntimeByExplicitTab(t *testing.T) {
95 localCtrl := &backgroundRuntimeController{
96 status: control.RuntimeStatus{BackgroundJobs: 1},
97 jobs: []jobs.View{{ID: "job-local", Kind: "bash", Status: "running"}},
98 }
99 app := &App{
100 tabs: map[string]*WorkspaceTab{"local": {ID: "local", Ctrl: localCtrl}},
101 tabOrder: []string{"local"},
102 activeTabID: "local",
103 }
104
105 listed := app.JobsForTab("local")
106 if len(listed) != 1 || listed[0].ID != "job-local" {
107 t.Fatalf("JobsForTab(local) = %+v, want local job", listed)
108 }
109 cancelled, err := app.CancelJobForTab("local", "job-local")
110 if err != nil {
111 t.Fatalf("CancelJobForTab(local): %v", err)
112 }
113 if !cancelled || len(localCtrl.cancelled) != 1 || localCtrl.cancelled[0] != "job-local" {
114 t.Fatalf("local cancellation = %t, calls=%v; want local job cancelled", cancelled, localCtrl.cancelled)
115 }
116 }
117
118 func TestWorkspaceConflictIdentifiesExactLocalWriterWithoutIdentity(t *testing.T) {
119 root := t.TempDir()
120 waiterCtrl := &backgroundRuntimeController{
121 status: control.RuntimeStatus{Running: true},
122 lease: workspacelease.State{Waiting: true},
123 }
124 ownerCtrl := &backgroundRuntimeController{
125 status: control.RuntimeStatus{BackgroundJobs: 1},
126 jobs: []jobs.View{{ID: "owner-job", Kind: "bash", Status: "running"}},
127 lease: workspacelease.State{Acquired: true},
128 }
129 app := &App{
130 tabs: map[string]*WorkspaceTab{
131 "waiter": {ID: "waiter", WorkspaceRoot: root, Ctrl: waiterCtrl},
132 "owner": {ID: "owner", TopicTitle: "Writing task", WorkspaceRoot: root, Ctrl: ownerCtrl},
133 },
134 tabOrder: []string{"waiter", "owner"},
135 activeTabID: "waiter",
136 }
137
138 conflict := app.WorkspaceConflictForTab("waiter")
139 if conflict.State != "local" || conflict.OwnerTabID != "owner" || conflict.OwnerTitle != "Writing task" || !conflict.CanReveal {
140 t.Fatalf("WorkspaceConflictForTab = %+v, want exact local owner", conflict)
141 }
142 if len(conflict.OwnerWork.Jobs) != 1 || conflict.OwnerWork.Jobs[0].ID != "owner-job" {
143 t.Fatalf("owner work = %+v, want sanitized active work", conflict.OwnerWork)
144 }
145 }
146
147 func TestWorkspaceConflictMatchesTheWaitingFile(t *testing.T) {
148 root := t.TempDir()
149 aPath := filepath.Join(root, "a.go")
150 bPath := filepath.Join(root, "b.go")
151 waiterCtrl := &backgroundRuntimeController{lease: workspacelease.State{
152 Waiting: true, Scope: "file", Label: "a.go", WaitingKeys: []string{aPath},
153 }}
154 wrongCtrl := &backgroundRuntimeController{
155 lease: workspacelease.State{Acquired: true, Scope: "file", Label: "b.go"}, leaseKeys: []string{bPath},
156 }
157 rightCtrl := &backgroundRuntimeController{
158 lease: workspacelease.State{Acquired: true, Scope: "file", Label: "a.go"}, leaseKeys: []string{aPath},
159 }
160 app := &App{
161 tabs: map[string]*WorkspaceTab{
162 "waiter": {ID: "waiter", WorkspaceRoot: root, Ctrl: waiterCtrl},
163 "wrong": {ID: "wrong", WorkspaceRoot: root, Ctrl: wrongCtrl},
164 "right": {ID: "right", WorkspaceRoot: root, Ctrl: rightCtrl},
165 },
166 tabOrder: []string{"waiter", "wrong", "right"}, activeTabID: "waiter",
167 }
168
169 conflict := app.WorkspaceConflictForTab("waiter")
170 if conflict.State != "local" || conflict.OwnerTabID != "right" {
171 t.Fatalf("WorkspaceConflictForTab = %+v, want right file owner", conflict)
172 }
173 }
174
175 func TestWorkspaceConflictMatchesWaitingFileAcrossNestedWorkspaceRoots(t *testing.T) {
176 parent := t.TempDir()
177 nested := filepath.Join(parent, "nested")
178 if err := os.MkdirAll(filepath.Join(nested, ".git"), 0o700); err != nil {
179 t.Fatal(err)
180 }
181 path := filepath.Join(nested, "shared.go")
182 waiterCtrl := &backgroundRuntimeController{lease: workspacelease.State{
183 Waiting: true, Scope: "file", Label: "shared.go", WaitingKeys: []string{path},
184 }}
185 ownerCtrl := &backgroundRuntimeController{
186 lease: workspacelease.State{Acquired: true, Scope: "file", Label: "shared.go"},
187 leaseKeys: []string{path},
188 }
189 app := &App{
190 tabs: map[string]*WorkspaceTab{
191 "waiter": {ID: "waiter", WorkspaceRoot: parent, Ctrl: waiterCtrl},
192 "owner": {ID: "owner", WorkspaceRoot: nested, Ctrl: ownerCtrl},
193 },
194 tabOrder: []string{"waiter", "owner"}, activeTabID: "waiter",
195 }
196
197 conflict := app.WorkspaceConflictForTab("waiter")
198 if conflict.State != "local" || conflict.OwnerTabID != "owner" {
199 t.Fatalf("WorkspaceConflictForTab = %+v, want nested local owner", conflict)
200 }
201 }
202
203 func TestWorkspaceConflictAllowsAcquiredOwnerToWaitForAnotherFile(t *testing.T) {
204 root := t.TempDir()
205 aPath := filepath.Join(root, "a.go")
206 waiterCtrl := &backgroundRuntimeController{lease: workspacelease.State{
207 Acquired: true, Waiting: true, Scope: "file", Label: "a.go", WaitingKeys: []string{aPath},
208 }}
209 ownerCtrl := &backgroundRuntimeController{
210 lease: workspacelease.State{Acquired: true, Scope: "file", Label: "a.go"}, leaseKeys: []string{aPath},
211 }
212 app := &App{
213 tabs: map[string]*WorkspaceTab{
214 "waiter": {ID: "waiter", WorkspaceRoot: root, Ctrl: waiterCtrl},
215 "owner": {ID: "owner", WorkspaceRoot: root, Ctrl: ownerCtrl},
216 },
217 tabOrder: []string{"waiter", "owner"}, activeTabID: "waiter",
218 }
219
220 conflict := app.WorkspaceConflictForTab("waiter")
221 if conflict.State != "local" || conflict.OwnerTabID != "owner" {
222 t.Fatalf("WorkspaceConflictForTab = %+v, want local owner", conflict)
223 }
224 }
225
225 lines GO