返回 DeepSeek-Reasonix
topic_activation_reselect_test.go
根目录 / desktop / topic_activation_reselect_test.go
1 package main
2
3 import (
4 "context"
5 "os"
6 "sync"
7 "testing"
8 "time"
9 )
10
11 func TestTopicActivationReselectsCancelledStartup(t *testing.T) {
12 for _, canonical := range []bool{true, false} {
13 name := "legacy"
14 if canonical {
15 name = "canonical"
16 }
17 t.Run(name, func(t *testing.T) {
18 app, source, target, root, _ := canonicalWorkspaceOpenFixture(t)
19 app.readyHook = func() {}
20 installNoopRuntimeEvents(app, source.sink)
21 t.Cleanup(func() { app.shutdown(context.Background()) })
22 path := sessionRoute(target.Ref().SessionID)
23 if !canonical {
24 saveWorkspace(root)
25 app.registerProjectRoot(root)
26 dir := desktopSessionDir(root)
27 if err := os.MkdirAll(dir, 0o700); err != nil {
28 t.Fatal(err)
29 }
30 path = writeTopicSession(t, dir, "reselect.jsonl", "reselect-topic", "Reselect", root)
31 }
32
33 buildEntered := make(chan struct{}, 4)
34 releaseBuilds := make(chan struct{})
35 unblockBuilds := sync.OnceFunc(func() { close(releaseBuilds) })
36 defer unblockBuilds()
37 app.tabBuildStartHook = func(string) { buildEntered <- struct{}{}; <-releaseBuilds }
38 events := newActivationEventRecorder(app)
39 releaseCompletion := make(chan struct{})
40 unblockCompletion := sync.OnceFunc(func() { close(releaseCompletion) })
41 defer unblockCompletion()
42 // Keep the cancelled target present until the return click. This is
43 // the window between a ready event and the previous surface's prune.
44 app.activationEventHook = func(ev TopicActivationEvent) {
45 events.ch <- ev
46 if ev.RequestID == "away" && ev.Phase == "ready" {
47 <-releaseCompletion
48 }
49 }
50 request := TopicActivationRequest{Scope: "project", WorkspaceRoot: root, TopicID: "reselect-topic", SessionPath: path, RequestID: "first"}
51 first, err := app.StartTopicActivation(request)
52 if err != nil {
53 t.Fatal(err)
54 }
55 select {
56 case <-buildEntered:
57 case <-time.After(5 * time.Second):
58 t.Fatal("target startup did not reach the build gate")
59 }
60 _, err = app.StartTopicActivation(TopicActivationRequest{
61 Scope: source.Scope, WorkspaceRoot: source.WorkspaceRoot,
62 SessionPath: sessionRoute(source.SessionID), RequestID: "away",
63 })
64 if err != nil {
65 t.Fatal(err)
66 }
67 events.waitFor(t, activationEventFor("away", "ready"))
68 request.RequestID = "return"
69 last, err := app.StartTopicActivation(request)
70 if err != nil {
71 t.Fatal(err)
72 }
73 unblockBuilds()
74 unblockCompletion()
75 terminal := events.waitFor(t, func(ev TopicActivationEvent) bool {
76 return ev.RequestID == "return" && (ev.Phase == "ready" || ev.Phase == "failed")
77 })
78 if terminal.Phase != "ready" {
79 t.Fatalf("return click reused an abandoned startup: first=%s last=%s event=%+v", first.TabID, last.TabID, terminal)
80 }
81 app.mu.RLock()
82 visible := app.tabs[app.activeTabID]
83 correct := visible != nil && visible.ID == last.TabID && visible.Ctrl != nil
84 if canonical {
85 correct = correct && visible.SessionID == target.Ref().SessionID
86 }
87 app.mu.RUnlock()
88 if !correct {
89 t.Fatal("reselected startup did not publish the requested session")
90 }
91 })
92 }
93 }
94
94 lines GO