返回 DeepSeek-Reasonix
tab_scoped_actions_test.go
根目录 / desktop / tab_scoped_actions_test.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "reasonix/internal/config"
11 "reasonix/internal/control"
12 "reasonix/internal/provider"
13 )
14
15 type tabScopedActionController struct {
16 control.SessionAPI
17 history []provider.Message
18 newSessionCalls int
19 clearSessionCalls int
20 rewindCalls int
21 compactCalls int
22 forkCalls int
23 summarizeFrom int
24 summarizeUpTo int
25 }
26
27 func newTabScopedActionController() *tabScopedActionController {
28 return &tabScopedActionController{history: []provider.Message{
29 {Role: provider.RoleSystem, Content: "system"},
30 {Role: provider.RoleUser, Content: "keep this tab distinct"},
31 }}
32 }
33
34 func (c *tabScopedActionController) RuntimeStatus() control.RuntimeStatus {
35 return control.RuntimeStatus{}
36 }
37 func (c *tabScopedActionController) PlanMode() bool { return false }
38 func (c *tabScopedActionController) AutoApproveTools() bool { return false }
39 func (c *tabScopedActionController) Goal() string { return "" }
40 func (c *tabScopedActionController) ToolApprovalMode() string {
41 return control.ToolApprovalAsk
42 }
43 func (c *tabScopedActionController) History() []provider.Message {
44 return append([]provider.Message(nil), c.history...)
45 }
46 func (c *tabScopedActionController) WorkspaceRoot() string { return "" }
47 func (c *tabScopedActionController) SessionDir() string { return "" }
48 func (c *tabScopedActionController) SessionPath() string { return "" }
49 func (c *tabScopedActionController) Snapshot() error { return nil }
50 func (c *tabScopedActionController) SetDisplayRecorder(func(content, display string)) {
51 }
52 func (c *tabScopedActionController) NewSession() error {
53 c.newSessionCalls++
54 c.history = []provider.Message{{Role: provider.RoleSystem, Content: "system"}}
55 return nil
56 }
57 func (c *tabScopedActionController) ClearSession() error {
58 c.clearSessionCalls++
59 c.history = []provider.Message{{Role: provider.RoleSystem, Content: "system"}}
60 return nil
61 }
62 func (c *tabScopedActionController) Rewind(_ int, _ control.RewindScope) error {
63 c.rewindCalls++
64 return nil
65 }
66 func (c *tabScopedActionController) Compact(_ context.Context, _ string) error {
67 c.compactCalls++
68 return nil
69 }
70 func (c *tabScopedActionController) ForkSession(_ int, _ string) (string, error) {
71 c.forkCalls++
72 return "", errors.New("stop after selecting source controller")
73 }
74 func (c *tabScopedActionController) SummarizeFrom(_ context.Context, _ int) error {
75 c.summarizeFrom++
76 return nil
77 }
78 func (c *tabScopedActionController) SummarizeUpTo(_ context.Context, _ int) error {
79 c.summarizeUpTo++
80 return nil
81 }
82
83 type blockingForkTabController struct {
84 *tabScopedActionController
85 path string
86 started chan struct{}
87 release chan struct{}
88 }
89
90 func (c *blockingForkTabController) ForkSession(_ int, _ string) (string, error) {
91 close(c.started)
92 <-c.release
93 return c.path, nil
94 }
95
96 func TestTabScopedSessionActionsIgnoreFocusedTab(t *testing.T) {
97 isolateDesktopUserDirs(t)
98 targetCtrl := newTabScopedActionController()
99 focusedCtrl := newTabScopedActionController()
100 app := &App{
101 tabs: map[string]*WorkspaceTab{
102 "target": {ID: "target", Scope: "global", Ready: true, Ctrl: targetCtrl, disabledMCP: map[string]ServerView{}},
103 "focused": {ID: "focused", Scope: "global", Ready: true, Ctrl: focusedCtrl, disabledMCP: map[string]ServerView{}},
104 },
105 tabOrder: []string{"target", "focused"},
106 activeTabID: "focused",
107 }
108
109 if err := app.NewSessionForTab("target"); err != nil {
110 t.Fatalf("NewSessionForTab: %v", err)
111 }
112 // Restore content so ClearSession exercises its non-blank path too.
113 targetCtrl.history = []provider.Message{{Role: provider.RoleSystem, Content: "system"}, {Role: provider.RoleUser, Content: "clear me"}}
114 if err := app.ClearSessionForTab("target"); err != nil {
115 t.Fatalf("ClearSessionForTab: %v", err)
116 }
117 if err := app.RewindForTab("target", 1, "conversation"); err != nil {
118 t.Fatalf("RewindForTab: %v", err)
119 }
120 if err := app.CompactForTab("target"); err != nil {
121 t.Fatalf("CompactForTab: %v", err)
122 }
123 if _, err := app.ForkForTab("target", 1); err == nil || err.Error() != "stop after selecting source controller" {
124 t.Fatalf("ForkForTab error = %v, want source-controller sentinel", err)
125 }
126 if err := app.SummarizeFromForTab("target", 1); err != nil {
127 t.Fatalf("SummarizeFromForTab: %v", err)
128 }
129 if err := app.SummarizeUpToForTab("target", 1); err != nil {
130 t.Fatalf("SummarizeUpToForTab: %v", err)
131 }
132
133 if targetCtrl.newSessionCalls != 1 || targetCtrl.clearSessionCalls != 1 || targetCtrl.rewindCalls != 1 || targetCtrl.compactCalls != 1 || targetCtrl.forkCalls != 1 || targetCtrl.summarizeFrom != 1 || targetCtrl.summarizeUpTo != 1 {
134 t.Fatalf("target calls = new:%d clear:%d rewind:%d compact:%d fork:%d from:%d upto:%d, want all 1",
135 targetCtrl.newSessionCalls, targetCtrl.clearSessionCalls, targetCtrl.rewindCalls, targetCtrl.compactCalls, targetCtrl.forkCalls, targetCtrl.summarizeFrom, targetCtrl.summarizeUpTo)
136 }
137 if focusedCtrl.newSessionCalls != 0 || focusedCtrl.clearSessionCalls != 0 || focusedCtrl.rewindCalls != 0 || focusedCtrl.compactCalls != 0 || focusedCtrl.forkCalls != 0 || focusedCtrl.summarizeFrom != 0 || focusedCtrl.summarizeUpTo != 0 {
138 t.Fatalf("focused tab received scoped action: %+v", focusedCtrl)
139 }
140 if app.activeTabID != "focused" {
141 t.Fatalf("active tab = %q, want focused", app.activeTabID)
142 }
143 }
144
145 func TestForkForTabDoesNotOverrideLaterActiveTab(t *testing.T) {
146 isolateDesktopUserDirs(t)
147 ctrl := &blockingForkTabController{
148 tabScopedActionController: newTabScopedActionController(),
149 path: filepath.Join(config.SessionDir(), "fork.jsonl"),
150 started: make(chan struct{}),
151 release: make(chan struct{}),
152 }
153 app := NewApp()
154 app.setTestCtrl(ctrl, "")
155 app.tabs["test"].TopicTitle = "Source topic"
156 app.tabs["other"] = &WorkspaceTab{ID: "other", Scope: "global", Ready: true, disabledMCP: map[string]ServerView{}}
157 app.tabOrder = []string{"test", "other"}
158
159 type forkResult struct {
160 meta TabMeta
161 err error
162 }
163 result := make(chan forkResult, 1)
164 go func() {
165 meta, err := app.ForkForTab("test", 1)
166 result <- forkResult{meta: meta, err: err}
167 }()
168
169 select {
170 case <-ctrl.started:
171 case <-time.After(2 * time.Second):
172 t.Fatal("timed out waiting for ForkSession")
173 }
174 if err := app.SetActiveTab("other"); err != nil {
175 t.Fatalf("SetActiveTab(other): %v", err)
176 }
177 close(ctrl.release)
178
179 var got forkResult
180 select {
181 case got = <-result:
182 case <-time.After(5 * time.Second):
183 t.Fatal("timed out waiting for ForkForTab")
184 }
185 if got.err != nil {
186 t.Fatalf("ForkForTab: %v", got.err)
187 }
188 if got.meta.ID == "" || got.meta.ID == "test" {
189 t.Fatalf("fork tab ID = %q, want a fresh tab", got.meta.ID)
190 }
191 if got.meta.Active {
192 t.Fatalf("fork meta active = true, want false after later tab switch")
193 }
194 if app.activeTabID != "other" {
195 t.Fatalf("active tab = %q, want later selection %q", app.activeTabID, "other")
196 }
197 }
198
198 lines GO