| 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 |