返回 DeepSeek-Reasonix
goal_delivery_yolo_test.go
根目录 / desktop / goal_delivery_yolo_test.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11
12 "reasonix/internal/agent"
13 "reasonix/internal/boot"
14 "reasonix/internal/config"
15 "reasonix/internal/control"
16 "reasonix/internal/event"
17 "reasonix/internal/evidence"
18 "reasonix/internal/store"
19 )
20
21 func newGoalDeliveryYoloTestApp(t *testing.T, goalStatus string) (*App, *WorkspaceTab, control.SessionAPI, string) {
22 t.Helper()
23 isolateDesktopUserDirs(t)
24 setDesktopTestCredential(t, "GOAL_DELIVERY_KEY", "sk-test")
25 setDesktopTestCredential(t, "GOAL_DELIVERY_ALT_KEY", "sk-test")
26 cfg := config.Default()
27 cfg.DefaultModel = "test/model"
28 cfg.Desktop.ProviderAccess = []string{"test", "alt"}
29 cfg.Providers = []config.ProviderEntry{
30 {
31 Name: "test", Kind: "openai", BaseURL: "https://example.invalid/v1", Model: "model", APIKeyEnv: "GOAL_DELIVERY_KEY",
32 SupportedEfforts: []string{"low", "high"},
33 },
34 {Name: "alt", Kind: "openai", BaseURL: "https://example.invalid/v1", Model: "alt-model", APIKeyEnv: "GOAL_DELIVERY_ALT_KEY"},
35 }
36 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
37 t.Fatalf("save config: %v", err)
38 }
39
40 dir := config.SessionDir()
41 if err := os.MkdirAll(dir, 0o755); err != nil {
42 t.Fatalf("mkdir session dir: %v", err)
43 }
44 path := filepath.Join(dir, "goal-delivery-yolo.jsonl")
45 writeHistoryTestSession(t, path, "continue the delivery")
46 checkpoint := evidence.DeliveryCheckpoint{
47 ScopeID: "goal-test-scope",
48 CriteriaEstablished: true,
49 WorkObserved: true,
50 MutationObserved: true,
51 PendingMutation: true,
52 }
53 state := map[string]any{
54 "goal": "ship the combined mode",
55 "status": goalStatus,
56 "budgetClass": "research",
57 "turnsLimit": 40,
58 "scopeID": checkpoint.ScopeID,
59 "deliveryCheckpoint": checkpoint,
60 }
61 data, err := json.Marshal(state)
62 if err != nil {
63 t.Fatal(err)
64 }
65 if err := os.WriteFile(store.SessionGoalState(path), data, 0o600); err != nil {
66 t.Fatalf("write Goal sidecar: %v", err)
67 }
68
69 loaded, err := agent.LoadSession(path)
70 if err != nil {
71 t.Fatalf("load session: %v", err)
72 }
73 exec := agent.New(nil, nil, loaded, agent.Options{}, event.Discard)
74 oldCtrl := control.New(control.Options{Executor: exec, SessionDir: dir, SessionPath: path, Label: "test/model", Sink: event.Discard})
75 oldCtrl.Resume(loaded, path)
76 oldCtrl.SetToolApprovalMode(control.ToolApprovalYolo)
77
78 app := NewApp()
79 app.ctx = context.Background()
80 app.readyHook = func() {}
81 tab := &WorkspaceTab{
82 ID: "tab_goal_delivery_yolo", Scope: "global", Ready: true,
83 SessionPath: path, model: "test/model",
84 mode: "yolo", toolApprovalMode: control.ToolApprovalYolo,
85 goal: "stale tab goal", Ctrl: oldCtrl,
86 disabledMCP: map[string]ServerView{},
87 }
88 tab.sink = &tabEventSink{tabID: tab.ID, app: app}
89 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
90 app.tabOrder = []string{tab.ID}
91 app.activeTabID = tab.ID
92 if err := tab.ensureSessionLease(path); err != nil {
93 t.Fatalf("ensure session lease: %v", err)
94 }
95 app.mu.Lock()
96 app.newSessionRuntimeLocked(tab, sessionRuntimeKey(path))
97 app.advanceSessionRuntimeEpochLocked(tab)
98 app.mu.Unlock()
99 t.Cleanup(func() {
100 if ctrl := app.controllerForTab(tab); ctrl != nil {
101 ctrl.Close()
102 }
103 tab.releaseSessionLease()
104 })
105 return app, tab, oldCtrl, path
106 }
107
108 func TestGoalDeliveryYoloTokenSwitchPreservesBlockedGoalCheckpoint(t *testing.T) {
109 app, tab, oldCtrl, path := newGoalDeliveryYoloTestApp(t, control.GoalStatusBlocked)
110 if err := app.SetTokenModeForTab(tab.ID, boot.TokenModeDelivery); err != nil {
111 t.Fatalf("SetTokenModeForTab: %v", err)
112 }
113 ctrl := app.controllerForTab(tab)
114 // Role setting switches in place; Goal axes must remain on the same controller.
115 if ctrl == nil || ctrl != oldCtrl {
116 t.Fatal("token-mode/role setting switch must keep the same controller")
117 }
118 if ctrl.GoalStatus() != control.GoalStatusBlocked || ctrl.Goal() != "ship the combined mode" {
119 t.Fatalf("Goal after role switch = (%q, %q), want blocked Goal", ctrl.Goal(), ctrl.GoalStatus())
120 }
121 if ctrl.ToolApprovalMode() != control.ToolApprovalYolo {
122 t.Fatalf("tool approval = %q, want yolo", ctrl.ToolApprovalMode())
123 }
124 if currentTabTokenMode(tab) != boot.TokenModeFull {
125 t.Fatalf("token mode = %q, want full", currentTabTokenMode(tab))
126 }
127 app.mu.RLock()
128 runtimeForPath := app.runtimeBySessionKey[sessionRuntimeKey(path)]
129 runtimeCount := len(app.runtimeBySessionKey)
130 app.mu.RUnlock()
131 if runtimeForPath == nil || runtimeForPath.Owner != tab || runtimeCount != 1 {
132 t.Fatalf("runtime registry after Goal+Delivery switch = owner %p count %d, want tab %p count 1", runtimeForPath, runtimeCount, tab)
133 }
134 var persisted struct {
135 DeliveryCheckpoint evidence.DeliveryCheckpoint `json:"deliveryCheckpoint"`
136 }
137 data, err := os.ReadFile(store.SessionGoalState(path))
138 if err != nil {
139 t.Fatalf("read Goal sidecar: %v", err)
140 }
141 if err := json.Unmarshal(data, &persisted); err != nil {
142 t.Fatalf("decode Goal sidecar: %v", err)
143 }
144 if persisted.DeliveryCheckpoint.ScopeID != "goal-test-scope" || !persisted.DeliveryCheckpoint.PendingMutation {
145 t.Fatalf("persisted checkpoint = %+v", persisted.DeliveryCheckpoint)
146 }
147 if !app.ResumeGoalForTab(tab.ID) || ctrl.GoalStatus() != control.GoalStatusRunning {
148 t.Fatal("blocked Goal did not resume after controller rebuild")
149 }
150 }
151
152 func TestGoalDeliveryYoloTokenSwitchDoesNotReviveCompletedGoal(t *testing.T) {
153 app, tab, _, _ := newGoalDeliveryYoloTestApp(t, control.GoalStatusComplete)
154 if err := app.SetTokenModeForTab(tab.ID, boot.TokenModeDelivery); err != nil {
155 t.Fatalf("SetTokenModeForTab: %v", err)
156 }
157 ctrl := app.controllerForTab(tab)
158 if ctrl.GoalStatus() == control.GoalStatusRunning {
159 t.Fatalf("completed Goal was revived: goal=%q status=%q", ctrl.Goal(), ctrl.GoalStatus())
160 }
161 if app.ResumeGoalForTab(tab.ID) {
162 t.Fatal("completed Goal should not be resumable")
163 }
164 }
165
166 func TestPlanYoloDeliveryRebuildUsesLiveControllerAxes(t *testing.T) {
167 app, tab, oldCtrl, _ := newGoalDeliveryYoloTestApp(t, control.GoalStatusBlocked)
168 oldCtrl.SetPlanMode(true)
169 oldCtrl.SetToolApprovalMode(control.ToolApprovalYolo)
170 // Simulate stale tab metadata: the rebuild must snapshot the admitted
171 // Controller state, not restore these lagging persistence fields.
172 app.mu.Lock()
173 tab.mode = "normal"
174 tab.toolApprovalMode = ""
175 app.mu.Unlock()
176
177 if err := app.SetTokenModeForTab(tab.ID, boot.TokenModeDelivery); err != nil {
178 t.Fatalf("SetTokenModeForTab: %v", err)
179 }
180 ctrl := app.controllerForTab(tab)
181 // Role setting is in-place: live Controller axes are the source of truth.
182 if ctrl == nil || ctrl != oldCtrl {
183 t.Fatal("token-mode/role setting switch must keep the same controller")
184 }
185 if !ctrl.PlanMode() || ctrl.ToolApprovalMode() != control.ToolApprovalYolo {
186 t.Fatalf("axes plan=%v approval=%q, want true/yolo", ctrl.PlanMode(), ctrl.ToolApprovalMode())
187 }
188 if ctrl.GoalStatus() != control.GoalStatusBlocked {
189 t.Fatalf("blocked Goal status = %q, want preserved while Plan is active", ctrl.GoalStatus())
190 }
191 if currentTabTokenMode(tab) != boot.TokenModeFull {
192 t.Fatalf("token mode = %q, want full", currentTabTokenMode(tab))
193 }
194 }
195
196 func TestPlanWinsRunningGoalConflictDuringDeliveryRebuild(t *testing.T) {
197 // Axis conflict resolution runs on rebuild paths (model/effort/settings).
198 // Role setting no longer rebuilds, so exercise a model rebuild here.
199 app, tab, oldCtrl, path := newGoalDeliveryYoloTestApp(t, control.GoalStatusRunning)
200 oldCtrl.SetPlanMode(true)
201 oldCtrl.SetToolApprovalMode(control.ToolApprovalYolo)
202 app.mu.Lock()
203 tab.mode = "plan-yolo"
204 tab.goal = "ship the combined mode"
205 tab.qualityFloor = control.QualityFloorDelivery
206 app.mu.Unlock()
207
208 if err := app.SetModelForTab(tab.ID, "alt/alt-model"); err != nil {
209 t.Fatalf("SetModelForTab: %v", err)
210 }
211 ctrl := app.controllerForTab(tab)
212 if ctrl == nil || ctrl == oldCtrl {
213 t.Fatal("model rebuild did not install a replacement controller")
214 }
215 if !ctrl.PlanMode() || ctrl.ToolApprovalMode() != control.ToolApprovalYolo {
216 t.Fatalf("rebuilt axes plan=%v approval=%q, want true/yolo", ctrl.PlanMode(), ctrl.ToolApprovalMode())
217 }
218 if ctrl.GoalStatus() == control.GoalStatusRunning || strings.TrimSpace(ctrl.Goal()) != "ship the combined mode" {
219 t.Fatalf("Plan/Goal conflict survived rebuild: goal=%q status=%q", ctrl.Goal(), ctrl.GoalStatus())
220 }
221 var persisted struct {
222 Goal string `json:"goal"`
223 Status string `json:"status"`
224 }
225 data, err := os.ReadFile(store.SessionGoalState(path))
226 if err != nil {
227 t.Fatal(err)
228 }
229 if err := json.Unmarshal(data, &persisted); err != nil {
230 t.Fatal(err)
231 }
232 if strings.TrimSpace(persisted.Goal) != "ship the combined mode" {
233 t.Fatalf("conflicting Goal sidecar = %+v, want cleared", persisted)
234 }
235 }
236
237 func TestRunningGoalDeliveryYoloRebuildKeepsUnifiedGoalScope(t *testing.T) {
238 app, tab, oldCtrl, path := newGoalDeliveryYoloTestApp(t, control.GoalStatusRunning)
239 if err := app.SetTokenModeForTab(tab.ID, boot.TokenModeDelivery); err != nil {
240 t.Fatalf("SetTokenModeForTab: %v", err)
241 }
242 ctrl := app.controllerForTab(tab)
243 // In-place role switch: same controller keeps the running Goal identity.
244 if ctrl == nil || ctrl != oldCtrl {
245 t.Fatalf("role switch must keep controller: got %p want %p", ctrl, oldCtrl)
246 }
247 if ctrl.GoalStatus() != control.GoalStatusStopped || ctrl.Goal() != "ship the combined mode" {
248 t.Fatalf("running Goal lost after role switch: goal=%q status=%q", ctrl.Goal(), ctrl.GoalStatus())
249 }
250 if ctrl.ToolApprovalMode() != control.ToolApprovalYolo {
251 t.Fatalf("tool approval = %q, want yolo", ctrl.ToolApprovalMode())
252 }
253 var persisted struct {
254 ScopeID string `json:"scopeID"`
255 AutoResearchTaskID string `json:"autoResearchTaskID"`
256 DeliveryCheckpoint evidence.DeliveryCheckpoint `json:"deliveryCheckpoint"`
257 }
258 data, err := os.ReadFile(store.SessionGoalState(path))
259 if err != nil {
260 t.Fatal(err)
261 }
262 if err := json.Unmarshal(data, &persisted); err != nil {
263 t.Fatal(err)
264 }
265 if persisted.ScopeID != "goal-test-scope" || persisted.AutoResearchTaskID != "" {
266 t.Fatalf("restored Goal identity = %+v", persisted)
267 }
268 if persisted.DeliveryCheckpoint.ScopeID != persisted.ScopeID || !persisted.DeliveryCheckpoint.PendingMutation {
269 t.Fatalf("restored Delivery checkpoint = %+v", persisted.DeliveryCheckpoint)
270 }
271 }
272
273 func TestGoalDeliveryYoloSurvivesEveryControllerRebuildPath(t *testing.T) {
274 for _, tc := range []struct {
275 name string
276 prepare func(*App, *WorkspaceTab)
277 rebuild func(*App, *WorkspaceTab) error
278 // inPlace means the switch keeps the same controller (role setting).
279 inPlace bool
280 }{
281 {
282 name: "settings",
283 prepare: func(app *App, tab *WorkspaceTab) {
284 app.mu.Lock()
285 tab.qualityFloor = control.QualityFloorDelivery
286 app.mu.Unlock()
287 },
288 rebuild: func(app *App, _ *WorkspaceTab) error {
289 return app.rebuildSetting("settings")
290 },
291 },
292 {
293 name: "model",
294 prepare: func(app *App, tab *WorkspaceTab) {
295 app.mu.Lock()
296 tab.qualityFloor = control.QualityFloorDelivery
297 app.mu.Unlock()
298 },
299 rebuild: func(app *App, tab *WorkspaceTab) error {
300 return app.SetModelForTab(tab.ID, "alt/alt-model")
301 },
302 },
303 {
304 name: "effort",
305 prepare: func(app *App, tab *WorkspaceTab) {
306 app.mu.Lock()
307 tab.qualityFloor = control.QualityFloorDelivery
308 app.mu.Unlock()
309 },
310 rebuild: func(app *App, tab *WorkspaceTab) error {
311 return app.SetEffortForTab(tab.ID, "high")
312 },
313 },
314 {
315 name: "token mode",
316 prepare: func(*App, *WorkspaceTab) {},
317 rebuild: func(app *App, tab *WorkspaceTab) error {
318 return app.SetTokenModeForTab(tab.ID, boot.TokenModeDelivery)
319 },
320 inPlace: true,
321 },
322 } {
323 t.Run(tc.name, func(t *testing.T) {
324 app, tab, oldCtrl, path := newGoalDeliveryYoloTestApp(t, control.GoalStatusRunning)
325 tc.prepare(app, tab)
326 if err := tc.rebuild(app, tab); err != nil {
327 t.Fatalf("rebuild: %v", err)
328 }
329
330 ctrl := app.controllerForTab(tab)
331 if ctrl == nil {
332 t.Fatal("controller missing after switch")
333 }
334 if tc.inPlace {
335 if ctrl != oldCtrl {
336 t.Fatal("role setting must keep the same controller")
337 }
338 } else if ctrl == oldCtrl {
339 t.Fatal("rebuild did not install a replacement controller")
340 }
341 if ctrl.PlanMode() || ctrl.GoalStatus() != control.GoalStatusStopped || ctrl.Goal() != "ship the combined mode" {
342 t.Fatalf("collaboration state plan=%v goal=%q status=%q, want running Goal", ctrl.PlanMode(), ctrl.Goal(), ctrl.GoalStatus())
343 }
344 if ctrl.ToolApprovalMode() != control.ToolApprovalYolo || currentTabTokenMode(tab) != boot.TokenModeFull {
345 t.Fatalf("runtime axes approval=%q token=%q, want yolo/full", ctrl.ToolApprovalMode(), currentTabTokenMode(tab))
346 }
347
348 var persisted struct {
349 ScopeID string `json:"scopeID"`
350 AutoResearchTaskID string `json:"autoResearchTaskID"`
351 DeliveryCheckpoint evidence.DeliveryCheckpoint `json:"deliveryCheckpoint"`
352 }
353 data, err := os.ReadFile(store.SessionGoalState(path))
354 if err != nil {
355 t.Fatalf("read Goal sidecar: %v", err)
356 }
357 if err := json.Unmarshal(data, &persisted); err != nil {
358 t.Fatalf("decode Goal sidecar: %v", err)
359 }
360 if persisted.ScopeID != "goal-test-scope" || persisted.AutoResearchTaskID != "" {
361 t.Fatalf("restored Goal identity = %+v", persisted)
362 }
363 if persisted.DeliveryCheckpoint.ScopeID != persisted.ScopeID || !persisted.DeliveryCheckpoint.PendingMutation {
364 t.Fatalf("restored Delivery checkpoint = %+v", persisted.DeliveryCheckpoint)
365 }
366 })
367 }
368 }
369
370 func TestPlanYoloDeliveryOrderConverges(t *testing.T) {
371 for _, tc := range []struct {
372 name string
373 run func(*App, *WorkspaceTab) error
374 }{
375 {
376 name: "plan then yolo then delivery",
377 run: func(app *App, tab *WorkspaceTab) error {
378 app.SetCollaborationModeForTab(tab.ID, "plan")
379 app.SetToolApprovalModeForTab(tab.ID, control.ToolApprovalYolo)
380 return app.SetTokenModeForTab(tab.ID, boot.TokenModeDelivery)
381 },
382 },
383 {
384 name: "delivery then plan then yolo",
385 run: func(app *App, tab *WorkspaceTab) error {
386 if err := app.SetTokenModeForTab(tab.ID, boot.TokenModeDelivery); err != nil {
387 return err
388 }
389 app.SetCollaborationModeForTab(tab.ID, "plan")
390 app.SetToolApprovalModeForTab(tab.ID, control.ToolApprovalYolo)
391 return nil
392 },
393 },
394 } {
395 t.Run(tc.name, func(t *testing.T) {
396 app, tab, _, _ := newGoalDeliveryYoloTestApp(t, control.GoalStatusComplete)
397 app.SetToolApprovalModeForTab(tab.ID, control.ToolApprovalAsk)
398 if err := tc.run(app, tab); err != nil {
399 t.Fatal(err)
400 }
401 ctrl := app.controllerForTab(tab)
402 if !ctrl.PlanMode() || ctrl.ToolApprovalMode() != control.ToolApprovalYolo || currentTabTokenMode(tab) != boot.TokenModeFull {
403 t.Fatalf("final axes plan=%v approval=%q token=%q", ctrl.PlanMode(), ctrl.ToolApprovalMode(), currentTabTokenMode(tab))
404 }
405 })
406 }
407 }
408
409 func TestEveryCollaborationAndTokenModeCombinationConvergesInBothOrders(t *testing.T) {
410 collaborationModes := []string{"normal", "plan", "goal"}
411 orders := []string{"collaboration-first", "token-first"}
412
413 for _, collaborationMode := range collaborationModes {
414 for _, order := range orders {
415 t.Run(collaborationMode+"/"+order, func(t *testing.T) {
416 app, tab, _, path := newGoalDeliveryYoloTestApp(t, control.GoalStatusRunning)
417 app.SetToolApprovalModeForTab(tab.ID, control.ToolApprovalAsk)
418
419 setCollaboration := func() {
420 switch collaborationMode {
421 case "goal":
422 app.SetGoalForTab(tab.ID, "exercise all runtime axes")
423 app.SetCollaborationModeForTab(tab.ID, "goal")
424 case "plan":
425 app.SetCollaborationModeForTab(tab.ID, "plan")
426 default:
427 app.SetGoalForTab(tab.ID, "")
428 app.SetCollaborationModeForTab(tab.ID, "normal")
429 }
430 }
431 setToken := func() {
432 if err := app.SetTokenModeForTab(tab.ID, boot.TokenModeDelivery); err != nil {
433 t.Fatalf("SetTokenModeForTab: %v", err)
434 }
435 }
436 if order == "collaboration-first" {
437 setCollaboration()
438 setToken()
439 } else {
440 setToken()
441 setCollaboration()
442 }
443
444 ctrl := app.controllerForTab(tab)
445 if ctrl == nil {
446 t.Fatal("final controller is nil")
447 }
448 if got := currentTabTokenMode(tab); got != boot.TokenModeFull {
449 t.Fatalf("token mode = %q, want full", got)
450 }
451 if got := ctrl.AgentPreset(); got != boot.AgentPresetStandard {
452 t.Fatalf("controller AgentPreset = %q, want standard", got)
453 }
454 switch collaborationMode {
455 case "goal":
456 if ctrl.PlanMode() || ctrl.GoalStatus() != control.GoalStatusRunning ||
457 ctrl.Goal() != "exercise all runtime axes" {
458 t.Fatalf("Goal axes plan=%v goal=%q status=%q", ctrl.PlanMode(), ctrl.Goal(), ctrl.GoalStatus())
459 }
460 case "plan":
461 if !ctrl.PlanMode() || ctrl.GoalStatus() == control.GoalStatusRunning {
462 t.Fatalf("Plan axes plan=%v goal=%q status=%q", ctrl.PlanMode(), ctrl.Goal(), ctrl.GoalStatus())
463 }
464 default:
465 if ctrl.PlanMode() || ctrl.GoalStatus() == control.GoalStatusRunning {
466 t.Fatalf("Normal axes plan=%v goal=%q status=%q", ctrl.PlanMode(), ctrl.Goal(), ctrl.GoalStatus())
467 }
468 }
469 app.mu.RLock()
470 runtimeForPath := app.runtimeBySessionKey[sessionRuntimeKey(path)]
471 runtimeCount := len(app.runtimeBySessionKey)
472 view := app.sessionRuntimeViewLocked(tab)
473 app.mu.RUnlock()
474 if runtimeForPath == nil || runtimeForPath.Owner != tab || runtimeCount != 1 {
475 t.Fatalf("runtime registry owner=%#v count=%d, want one runtime for tab", runtimeForPath, runtimeCount)
476 }
477 if view.Phase != sessionRuntimeReady || tab.sessionLeaseRuntimeKey() != sessionRuntimeKey(path) {
478 t.Fatalf("runtime phase=%q lease=%q, want ready/%q", view.Phase, tab.sessionLeaseRuntimeKey(), sessionRuntimeKey(path))
479 }
480 })
481 }
482 }
483 }
484
485 func TestGoalAndCollaborationResyncBeforeSendPreserveRunningDeliveryScope(t *testing.T) {
486 app, tab, ctrl, path := newGoalDeliveryYoloTestApp(t, control.GoalStatusRunning)
487 tab.goal = ctrl.Goal()
488 app.SetCollaborationModeForTab(tab.ID, "goal")
489 app.SetGoalForTab(tab.ID, ctrl.Goal())
490
491 var persisted struct {
492 ScopeID string `json:"scopeID"`
493 }
494 data, err := os.ReadFile(store.SessionGoalState(path))
495 if err != nil {
496 t.Fatalf("read Goal sidecar: %v", err)
497 }
498 if err := json.Unmarshal(data, &persisted); err != nil {
499 t.Fatalf("decode Goal sidecar: %v", err)
500 }
501 if persisted.ScopeID != "goal-test-scope" {
502 t.Fatalf("Goal resync replaced delivery scope: got %q", persisted.ScopeID)
503 }
504 }
505
506 func TestRetiredTokenModeSwitchDoesNotWaitForForegroundTurnAdmission(t *testing.T) {
507 // The compatibility call validates and returns without touching turn state.
508 app, tab, oldCtrl, _ := newGoalDeliveryYoloTestApp(t, control.GoalStatusBlocked)
509 tab.turnStartMu.Lock()
510 defer tab.turnStartMu.Unlock()
511 done := make(chan error, 1)
512 go func() {
513 done <- app.SetTokenModeForTab(tab.ID, boot.TokenModeDelivery)
514 }()
515 select {
516 case err := <-done:
517 if err != nil {
518 t.Fatalf("SetTokenModeForTab: %v", err)
519 }
520 case <-time.After(300 * time.Millisecond):
521 t.Fatal("retired SetTokenModeForTab blocked on foreground turn admission")
522 }
523 if app.controllerForTab(tab) != oldCtrl {
524 t.Fatal("SetTokenModeForTab rebuilt the controller")
525 }
526 if got := currentTabTokenMode(tab); got != boot.TokenModeFull {
527 t.Fatalf("token mode = %q, want full", got)
528 }
529 }
530
530 lines GO