返回 DeepSeek-Reasonix
extension_ui_test.go
根目录 / internal / control / extension_ui_test.go
1 package control
2
3 import (
4 "context"
5 "strings"
6 "testing"
7
8 "reasonix/internal/event"
9 "reasonix/internal/extension/protocol"
10 "reasonix/internal/extension/uihub"
11 )
12
13 // fakeExtensionClient is the uihub.ActionClient double for the controller
14 // port tests.
15 type fakeExtensionClient struct {
16 actionResult protocol.UIActionResult
17 submitResult protocol.UISubmitResult
18 gotAction *protocol.UIActionParams
19 gotSubmit *protocol.UISubmitParams
20 }
21
22 func (f *fakeExtensionClient) UIAction(_ context.Context, p protocol.UIActionParams) (protocol.UIActionResult, error) {
23 f.gotAction = &p
24 return f.actionResult, nil
25 }
26
27 func (f *fakeExtensionClient) UISubmit(_ context.Context, p protocol.UISubmitParams) (protocol.UISubmitResult, error) {
28 f.gotSubmit = &p
29 return f.submitResult, nil
30 }
31
32 func newExtensionUIController(t *testing.T, client uihub.ActionClient) (*Controller, *uihub.Hub) {
33 t.Helper()
34 c := New(Options{Sink: event.Discard})
35 hub := uihub.New(uihub.Options{
36 SessionID: "sess-1", Generation: 3,
37 Resolve: func(string) uihub.ActionClient { return client },
38 })
39 if err := hub.RegisterActions("alpha", []protocol.UIActionDecl{{ActionID: "act1", Label: "Act one"}}); err != nil {
40 t.Fatalf("RegisterActions: %v", err)
41 }
42 c.SetExtensionUI(hub)
43 return c, hub
44 }
45
46 func TestExtensionUIPortNilHub(t *testing.T) {
47 c := New(Options{Sink: event.Discard})
48 if got := c.ExtensionActions(); len(got) != 0 {
49 t.Fatalf("ExtensionActions = %+v, want empty without a hub", got)
50 }
51 if _, err := c.InvokeExtensionAction(context.Background(), "/alpha:act1", nil); err == nil {
52 t.Fatal("InvokeExtensionAction succeeded without a hub")
53 }
54 if err := c.SubmitExtensionForm(context.Background(), "alpha", "f1", nil); err == nil {
55 t.Fatal("SubmitExtensionForm succeeded without a hub")
56 }
57 }
58
59 func TestExtensionUIPortEnumeratesAndInvokes(t *testing.T) {
60 client := &fakeExtensionClient{
61 actionResult: protocol.UIActionResult{Accepted: true, Message: "done"},
62 submitResult: protocol.UISubmitResult{Accepted: true},
63 }
64 c, hub := newExtensionUIController(t, client)
65
66 actions := c.ExtensionActions()
67 if len(actions) != 1 {
68 t.Fatalf("ExtensionActions = %+v", actions)
69 }
70 if actions[0].Slash != "/alpha:act1" || actions[0].Label != "Act one" || actions[0].PluginID != "alpha" {
71 t.Fatalf("action view = %+v", actions[0])
72 }
73
74 message, err := c.InvokeExtensionAction(context.Background(), "/alpha:act1", map[string]string{"k": "v"})
75 if err != nil {
76 t.Fatalf("InvokeExtensionAction: %v", err)
77 }
78 if message != "done" {
79 t.Fatalf("message = %q", message)
80 }
81 if client.gotAction == nil || client.gotAction.ActionID != "act1" ||
82 client.gotAction.SessionID != hub.SessionID() || client.gotAction.Generation != 3 || client.gotAction.Args["k"] != "v" {
83 t.Fatalf("action params = %+v", client.gotAction)
84 }
85
86 if _, err := c.InvokeExtensionAction(context.Background(), "not-a-slash-name", nil); err == nil {
87 t.Fatal("InvokeExtensionAction accepted a malformed name")
88 }
89 if _, err := c.InvokeExtensionAction(context.Background(), "/alpha:undeclared", nil); err == nil {
90 t.Fatal("InvokeExtensionAction accepted an undeclared action")
91 }
92
93 if err := c.SubmitExtensionForm(context.Background(), "alpha", "f1", map[string]any{"name": "x"}); err != nil {
94 t.Fatalf("SubmitExtensionForm: %v", err)
95 }
96 if client.gotSubmit == nil || client.gotSubmit.SurfaceID != "f1" || client.gotSubmit.Values["name"] != "x" {
97 t.Fatalf("submit params = %+v", client.gotSubmit)
98 }
99 }
100
101 func TestSetExtensionUIFirstInstallWins(t *testing.T) {
102 client := &fakeExtensionClient{}
103 c, hub := newExtensionUIController(t, client)
104 replacement := uihub.New(uihub.Options{SessionID: "sess-2", Generation: 9})
105 c.SetExtensionUI(replacement)
106 if got := c.ExtensionActions(); len(got) != 1 {
107 t.Fatalf("a second SetExtensionUI swapped the hub: actions = %+v", got)
108 }
109 _ = hub
110 }
111
112 func TestEmitExtensionEventReachesSink(t *testing.T) {
113 var got []event.Event
114 c := New(Options{Sink: event.FuncSink(func(e event.Event) { got = append(got, e) })})
115 c.EmitExtensionEvent(event.Event{
116 Kind: event.ExtensionStatus,
117 Extension: &event.ExtensionSurfacePayload{
118 PluginID: "alpha", SurfaceID: "s1", Kind: event.ExtensionSurfaceStatus,
119 Status: &event.ExtensionStatusView{Label: "working"},
120 },
121 })
122 if len(got) != 1 || got[0].Kind != event.ExtensionStatus || got[0].Extension.Status.Label != "working" {
123 t.Fatalf("emitted events = %+v", got)
124 }
125 }
126
127 // TestInvokeExtensionActionRedactsResultMessage proves the sidecar-sourced
128 // message reaches the frontend credential-free.
129 func TestInvokeExtensionActionRedactsResultMessage(t *testing.T) {
130 client := &fakeExtensionClient{actionResult: protocol.UIActionResult{
131 Accepted: true, Message: "token api_key=sk-abcdef1234567890SECRETKEY stored",
132 }}
133 c, _ := newExtensionUIController(t, client)
134 message, err := c.InvokeExtensionAction(context.Background(), "/alpha:act1", nil)
135 if err != nil {
136 t.Fatalf("InvokeExtensionAction: %v", err)
137 }
138 if strings.Contains(message, "sk-abcdef") {
139 t.Fatalf("message not redacted: %q", message)
140 }
141 }
142
142 lines GO