返回 DeepSeek-Reasonix
extensions_app_test.go
根目录 / desktop / extensions_app_test.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "testing"
7
8 "reasonix/internal/control"
9 )
10
11 // extensionUIController fakes the extension-UI slice of control.SessionAPI:
12 // the Capabilities port methods plus the concrete controller's
13 // SubmitExtensionForm (asserted locally by the binding until the port grows
14 // it).
15 type extensionUIController struct {
16 control.SessionAPI
17 actions []control.ExtensionActionView
18 invokeMessage string
19 invokeErr error
20 submitErr error
21 invokeName string
22 invokeArgs map[string]string
23 submitPluginID string
24 submitSurfaceID string
25 submitValues map[string]any
26 }
27
28 func (c *extensionUIController) ExtensionActions() []control.ExtensionActionView {
29 return append([]control.ExtensionActionView(nil), c.actions...)
30 }
31
32 func (c *extensionUIController) InvokeExtensionAction(_ context.Context, name string, args map[string]string) (string, error) {
33 c.invokeName = name
34 c.invokeArgs = args
35 return c.invokeMessage, c.invokeErr
36 }
37
38 func (c *extensionUIController) SubmitExtensionForm(_ context.Context, pluginID, surfaceID string, values map[string]any) error {
39 c.submitPluginID = pluginID
40 c.submitSurfaceID = surfaceID
41 c.submitValues = values
42 return c.submitErr
43 }
44
45 func newExtensionTestApp(ctrl control.SessionAPI) *App {
46 return &App{
47 tabs: map[string]*WorkspaceTab{
48 "target": {ID: "target", Scope: "global", Ready: true, Ctrl: ctrl, disabledMCP: map[string]ServerView{}},
49 "focused": {ID: "focused", Scope: "global", Ready: true, disabledMCP: map[string]ServerView{}},
50 },
51 tabOrder: []string{"target", "focused"},
52 activeTabID: "focused",
53 }
54 }
55
56 func TestExtensionActionsMapsControlViewsAndRoutesByTab(t *testing.T) {
57 isolateDesktopUserDirs(t)
58 targetCtrl := &extensionUIController{actions: []control.ExtensionActionView{
59 {PluginID: "alpha", ActionID: "act1", Label: "Run the thing", Slash: "/alpha:act1"},
60 {PluginID: "beta", ActionID: "act2", Label: "", Slash: "/beta:act2"},
61 }}
62 app := newExtensionTestApp(targetCtrl)
63
64 views, err := app.ExtensionActions("target")
65 if err != nil {
66 t.Fatalf("ExtensionActions: %v", err)
67 }
68 if len(views) != 2 {
69 t.Fatalf("ExtensionActions = %+v, want 2 views", views)
70 }
71 if views[0] != (ExtensionActionView{Plugin: "alpha", Action: "act1", Slash: "/alpha:act1", Description: "Run the thing"}) {
72 t.Fatalf("view[0] = %+v, want JSON twin of the control view", views[0])
73 }
74 if views[1].Description != "" {
75 t.Fatalf("empty label must stay an omitted description, got %+v", views[1])
76 }
77
78 // Empty tab id resolves the active tab.
79 active, err := app.ExtensionActions("")
80 if err != nil {
81 t.Fatalf("ExtensionActions(active): %v", err)
82 }
83 if len(active) != 0 {
84 t.Fatalf("active tab actions = %+v, want empty (focused tab has none)", active)
85 }
86
87 // Unknown tabs and tabs without a running runtime yield an empty list.
88 for _, tabID := range []string{"missing", "focused-noruntime"} {
89 app.tabs["focused-noruntime"] = &WorkspaceTab{ID: "focused-noruntime", Scope: "global", disabledMCP: map[string]ServerView{}}
90 got, err := app.ExtensionActions(tabID)
91 if err != nil {
92 t.Fatalf("ExtensionActions(%q): %v", tabID, err)
93 }
94 if len(got) != 0 {
95 t.Fatalf("ExtensionActions(%q) = %+v, want empty", tabID, got)
96 }
97 }
98 }
99
100 func TestInvokeExtensionActionForwardsNameAndArgs(t *testing.T) {
101 isolateDesktopUserDirs(t)
102 targetCtrl := &extensionUIController{invokeMessage: "done"}
103 focusedCtrl := &extensionUIController{}
104 app := newExtensionTestApp(targetCtrl)
105
106 message, err := app.InvokeExtensionAction("target", "/alpha:act1", map[string]string{"k": "v"})
107 if err != nil {
108 t.Fatalf("InvokeExtensionAction: %v", err)
109 }
110 if message != "done" {
111 t.Fatalf("message = %q, want done", message)
112 }
113 if targetCtrl.invokeName != "/alpha:act1" || targetCtrl.invokeArgs["k"] != "v" {
114 t.Fatalf("controller saw name=%q args=%+v", targetCtrl.invokeName, targetCtrl.invokeArgs)
115 }
116 if focusedCtrl.invokeName != "" {
117 t.Fatalf("focused tab received scoped invoke: %q", focusedCtrl.invokeName)
118 }
119
120 targetCtrl.invokeErr = errors.New("sidecar said no")
121 if _, err := app.InvokeExtensionAction("target", "/alpha:act1", nil); err == nil || err.Error() != "sidecar said no" {
122 t.Fatalf("controller error must pass through, got %v", err)
123 }
124
125 if _, err := app.InvokeExtensionAction("target", " ", nil); err == nil {
126 t.Fatal("blank action name must fail before reaching the controller")
127 }
128 if _, err := app.InvokeExtensionAction("missing", "/alpha:act1", nil); err == nil {
129 t.Fatal("unknown tab must fail")
130 }
131 if _, err := app.InvokeExtensionAction("focused-noruntime", "/alpha:act1", nil); err == nil {
132 t.Fatal("tab without a running runtime must fail")
133 }
134 }
135
136 func TestSubmitExtensionFormRoutesValues(t *testing.T) {
137 isolateDesktopUserDirs(t)
138 targetCtrl := &extensionUIController{}
139 app := newExtensionTestApp(targetCtrl)
140
141 values := map[string]any{"name": "x", "flags": []any{"a", "b"}, "agree": true}
142 if err := app.SubmitExtensionForm("target", "alpha", "f1", values); err != nil {
143 t.Fatalf("SubmitExtensionForm: %v", err)
144 }
145 if targetCtrl.submitPluginID != "alpha" || targetCtrl.submitSurfaceID != "f1" {
146 t.Fatalf("controller saw %q/%q", targetCtrl.submitPluginID, targetCtrl.submitSurfaceID)
147 }
148 if targetCtrl.submitValues["name"] != "x" || targetCtrl.submitValues["agree"] != true {
149 t.Fatalf("values = %+v", targetCtrl.submitValues)
150 }
151
152 // A dismissal rides the same path as an explicit cancelled value.
153 if err := app.SubmitExtensionForm("target", "alpha", "f1", map[string]any{"cancelled": true}); err != nil {
154 t.Fatalf("SubmitExtensionForm(cancel): %v", err)
155 }
156 if targetCtrl.submitValues["cancelled"] != true {
157 t.Fatalf("cancelled value = %+v", targetCtrl.submitValues)
158 }
159
160 targetCtrl.submitErr = errors.New("not accepted")
161 if err := app.SubmitExtensionForm("target", "alpha", "f1", nil); err == nil || err.Error() != "not accepted" {
162 t.Fatalf("controller error must pass through, got %v", err)
163 }
164
165 if err := app.SubmitExtensionForm("target", "", "f1", nil); err == nil {
166 t.Fatal("blank plugin id must fail before reaching the controller")
167 }
168 if err := app.SubmitExtensionForm("target", "alpha", "", nil); err == nil {
169 t.Fatal("blank surface id must fail before reaching the controller")
170 }
171 if err := app.SubmitExtensionForm("missing", "alpha", "f1", nil); err == nil {
172 t.Fatal("unknown tab must fail")
173 }
174 }
175
176 // SubmitExtensionForm is not on control.SessionAPI yet; a controller that only
177 // satisfies the port must produce a clean error instead of a panic.
178 func TestSubmitExtensionFormWithoutSubmitterFailsCleanly(t *testing.T) {
179 isolateDesktopUserDirs(t)
180 app := newExtensionTestApp(&extensionUIActionsOnlyController{})
181 if err := app.SubmitExtensionForm("target", "alpha", "f1", nil); err == nil {
182 t.Fatal("SubmitExtensionForm without a submitter must fail")
183 }
184 }
185
186 type extensionUIActionsOnlyController struct {
187 control.SessionAPI
188 }
189
190 func (c *extensionUIActionsOnlyController) ExtensionActions() []control.ExtensionActionView {
191 return nil
192 }
193
193 lines GO