@stigmer/react 3.12.0 → 3.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/execution/useCreateAgentExecution.d.ts +3 -2
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/internal/isUnimplemented.d.ts +17 -0
- package/internal/isUnimplemented.d.ts.map +1 -0
- package/internal/isUnimplemented.js +26 -0
- package/internal/isUnimplemented.js.map +1 -0
- package/mcp-server/McpServerConnectDialog.js +7 -3
- package/mcp-server/McpServerConnectDialog.js.map +1 -1
- package/mcp-server/useMcpServerCredentials.d.ts +38 -10
- package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
- package/mcp-server/useMcpServerCredentials.js +50 -7
- package/mcp-server/useMcpServerCredentials.js.map +1 -1
- package/mcp-server/useOrgOAuthApp.d.ts +22 -0
- package/mcp-server/useOrgOAuthApp.d.ts.map +1 -1
- package/mcp-server/useOrgOAuthApp.js +39 -7
- package/mcp-server/useOrgOAuthApp.js.map +1 -1
- package/package.json +4 -4
- package/session/useSessionConversation.d.ts +4 -2
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js.map +1 -1
- package/skill/useSkillVersions.d.ts.map +1 -1
- package/skill/useSkillVersions.js +1 -9
- package/skill/useSkillVersions.js.map +1 -1
- package/src/execution/useCreateAgentExecution.ts +3 -2
- package/src/internal/isUnimplemented.ts +26 -0
- package/src/mcp-server/McpServerConnectDialog.tsx +29 -11
- package/src/mcp-server/__tests__/VendorApprovalBlocked.surfaces.test.tsx +103 -5
- package/src/mcp-server/__tests__/byoaEditionDegradation.test.tsx +323 -0
- package/src/mcp-server/__tests__/useMcpServerCredentials.test.tsx +306 -0
- package/src/mcp-server/useMcpServerCredentials.ts +87 -19
- package/src/mcp-server/useOrgOAuthApp.ts +54 -8
- package/src/session/useSessionConversation.ts +4 -2
- package/src/skill/useSkillVersions.ts +1 -11
- package/src/workflow/WorkflowCanvasEditor.tsx +37 -7
- package/src/workflow/WorkflowExecutionGraph.tsx +26 -1
- package/src/workflow/WorkflowExecutionViewer.tsx +27 -13
- package/src/workflow/WorkflowNode.tsx +5 -2
- package/src/workflow/WorkflowRunDialog.tsx +28 -1
- package/src/workflow/__tests__/WorkflowExecutionViewer.reconciliation.test.tsx +48 -1
- package/src/workflow/__tests__/WorkflowNode.test.tsx +93 -0
- package/src/workflow/__tests__/WorkflowRunDialog.test.tsx +127 -0
- package/src/workflow/__tests__/graph-history.test.ts +103 -0
- package/src/workflow/__tests__/overview-graph.test.ts +17 -0
- package/src/workflow/__tests__/useCanvasKeyboardShortcuts.undo.test.ts +115 -0
- package/src/workflow/__tests__/useWorkflowCanvas.undo.test.tsx +133 -0
- package/src/workflow/instance/CreateWorkflowInstanceDialog.tsx +16 -7
- package/src/workflow/instance/useDeleteWorkflowInstance.ts +9 -4
- package/src/workflow/layout/registry-dimensions.ts +2 -5
- package/src/workflow/node-shell/NodeShell.tsx +4 -0
- package/src/workflow/task-type-visual-registry.ts +5 -3
- package/src/workflow/useCanvasKeyboardShortcuts.ts +33 -3
- package/src/workflow/useGraphHistory.ts +22 -41
- package/src/workflow/useWorkflowCanvas.ts +12 -10
- package/src/workflow/useWorkflowSave.ts +2 -15
- package/src/workflow/workflow-graph-conversions.ts +25 -3
- package/workflow/WorkflowCanvasEditor.d.ts.map +1 -1
- package/workflow/WorkflowCanvasEditor.js +31 -6
- package/workflow/WorkflowCanvasEditor.js.map +1 -1
- package/workflow/WorkflowExecutionGraph.d.ts.map +1 -1
- package/workflow/WorkflowExecutionGraph.js +27 -1
- package/workflow/WorkflowExecutionGraph.js.map +1 -1
- package/workflow/WorkflowExecutionViewer.d.ts.map +1 -1
- package/workflow/WorkflowExecutionViewer.js +26 -12
- package/workflow/WorkflowExecutionViewer.js.map +1 -1
- package/workflow/WorkflowNode.d.ts.map +1 -1
- package/workflow/WorkflowNode.js +2 -2
- package/workflow/WorkflowNode.js.map +1 -1
- package/workflow/WorkflowRunDialog.d.ts +11 -1
- package/workflow/WorkflowRunDialog.d.ts.map +1 -1
- package/workflow/WorkflowRunDialog.js +16 -2
- package/workflow/WorkflowRunDialog.js.map +1 -1
- package/workflow/instance/CreateWorkflowInstanceDialog.d.ts.map +1 -1
- package/workflow/instance/CreateWorkflowInstanceDialog.js +10 -2
- package/workflow/instance/CreateWorkflowInstanceDialog.js.map +1 -1
- package/workflow/instance/useDeleteWorkflowInstance.d.ts +9 -4
- package/workflow/instance/useDeleteWorkflowInstance.d.ts.map +1 -1
- package/workflow/instance/useDeleteWorkflowInstance.js +4 -2
- package/workflow/instance/useDeleteWorkflowInstance.js.map +1 -1
- package/workflow/layout/registry-dimensions.d.ts.map +1 -1
- package/workflow/layout/registry-dimensions.js +2 -5
- package/workflow/layout/registry-dimensions.js.map +1 -1
- package/workflow/node-shell/NodeShell.js +1 -1
- package/workflow/node-shell/NodeShell.js.map +1 -1
- package/workflow/task-type-visual-registry.d.ts.map +1 -1
- package/workflow/task-type-visual-registry.js +5 -3
- package/workflow/task-type-visual-registry.js.map +1 -1
- package/workflow/useCanvasKeyboardShortcuts.d.ts +15 -4
- package/workflow/useCanvasKeyboardShortcuts.d.ts.map +1 -1
- package/workflow/useCanvasKeyboardShortcuts.js +25 -4
- package/workflow/useCanvasKeyboardShortcuts.js.map +1 -1
- package/workflow/useGraphHistory.d.ts +16 -9
- package/workflow/useGraphHistory.d.ts.map +1 -1
- package/workflow/useGraphHistory.js +19 -33
- package/workflow/useGraphHistory.js.map +1 -1
- package/workflow/useWorkflowCanvas.d.ts +1 -3
- package/workflow/useWorkflowCanvas.d.ts.map +1 -1
- package/workflow/useWorkflowCanvas.js +15 -9
- package/workflow/useWorkflowCanvas.js.map +1 -1
- package/workflow/useWorkflowSave.d.ts.map +1 -1
- package/workflow/useWorkflowSave.js +2 -15
- package/workflow/useWorkflowSave.js.map +1 -1
- package/workflow/workflow-graph-conversions.d.ts +19 -1
- package/workflow/workflow-graph-conversions.d.ts.map +1 -1
- package/workflow/workflow-graph-conversions.js +17 -3
- package/workflow/workflow-graph-conversions.js.map +1 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup, waitFor } from "@testing-library/react";
|
|
3
|
+
import { renderHook } from "@testing-library/react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { create } from "@bufbuild/protobuf";
|
|
6
|
+
import { createRouterTransport, ConnectError, Code } from "@connectrpc/connect";
|
|
7
|
+
import { Stigmer } from "@stigmer/sdk";
|
|
8
|
+
import { McpServerQueryController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/query_pb";
|
|
9
|
+
import {
|
|
10
|
+
GetOAuthGrantStatusOutputSchema,
|
|
11
|
+
GetOrgOAuthAppOutputSchema,
|
|
12
|
+
} from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
|
|
13
|
+
import {
|
|
14
|
+
McpServerSpecSchema,
|
|
15
|
+
McpServerAuthSchema,
|
|
16
|
+
HttpServerConfigSchema,
|
|
17
|
+
} from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
|
|
18
|
+
import {
|
|
19
|
+
McpServerStatusSchema,
|
|
20
|
+
OAuthStatusSchema,
|
|
21
|
+
OAuthAppSource,
|
|
22
|
+
ValidationState,
|
|
23
|
+
} from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
|
|
24
|
+
import { VendorApprovalStatus } from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/spec_pb";
|
|
25
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
26
|
+
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
27
|
+
import { samples } from "../../test/samples";
|
|
28
|
+
import { StigmerContext } from "../../context";
|
|
29
|
+
import { McpServerDetailView } from "../McpServerDetailView";
|
|
30
|
+
import { McpServerConnectDialog } from "../McpServerConnectDialog";
|
|
31
|
+
import { useOrgOAuthApp } from "../useOrgOAuthApp";
|
|
32
|
+
import type { UseMcpServerReturn } from "../useMcpServer";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Pins the edition degradation of the BYOA (org-OAuth-app override) surface
|
|
36
|
+
* (stigmer/stigmer#558, DD-019).
|
|
37
|
+
*
|
|
38
|
+
* The org-override RPCs (getOrgOAuthApp / setOrgOAuthApp / deleteOrgOAuthApp)
|
|
39
|
+
* are hosted-only by design: OSS has a flat OAuthApp store with no override
|
|
40
|
+
* binding, so the OSS server answers UNIMPLEMENTED for the whole surface.
|
|
41
|
+
* The SDK probes the capability through `getOrgOAuthApp` and must hide every
|
|
42
|
+
* BYOA affordance when the probe answers UNIMPLEMENTED — offering "Use your
|
|
43
|
+
* own OAuth app" on OSS is a dead end whose submit can only fail.
|
|
44
|
+
*
|
|
45
|
+
* `createRouterTransport` answers UNIMPLEMENTED for unregistered methods,
|
|
46
|
+
* which makes an OSS-like backend the default here; registering
|
|
47
|
+
* `getOrgOAuthApp` simulates the hosted edition.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
// happy-dom does not implement the native dialog show/close methods.
|
|
51
|
+
beforeAll(() => {
|
|
52
|
+
HTMLDialogElement.prototype.showModal = function showModal() {
|
|
53
|
+
this.open = true;
|
|
54
|
+
};
|
|
55
|
+
HTMLDialogElement.prototype.close = function close() {
|
|
56
|
+
this.open = false;
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(cleanup);
|
|
61
|
+
|
|
62
|
+
const ORG = "acme";
|
|
63
|
+
const SLUG = "vendor-crm";
|
|
64
|
+
const SERVER_ID = "mcps_01byoa";
|
|
65
|
+
const DOCS_URL = "https://vendor.example.com/oauth-docs";
|
|
66
|
+
|
|
67
|
+
/** A vendor-OAuth server; `approval` drives the blocked/plain CTA arms. */
|
|
68
|
+
function buildVendorOAuthServer(options: {
|
|
69
|
+
approval: VendorApprovalStatus;
|
|
70
|
+
}): McpServer {
|
|
71
|
+
const server = samples.mcpServer({ name: "Vendor CRM", org: ORG, slug: SLUG });
|
|
72
|
+
server.metadata!.id = SERVER_ID;
|
|
73
|
+
server.spec = create(McpServerSpecSchema, {
|
|
74
|
+
description: "CRM tools over the vendor's hosted MCP endpoint.",
|
|
75
|
+
serverType: {
|
|
76
|
+
case: "http",
|
|
77
|
+
value: create(HttpServerConfigSchema, { url: "https://mcp.vendor.example.com" }),
|
|
78
|
+
},
|
|
79
|
+
auth: create(McpServerAuthSchema, {
|
|
80
|
+
targetEnvVar: "VENDOR_ACCESS_TOKEN",
|
|
81
|
+
oauthAppRef: {
|
|
82
|
+
org: "stigmer",
|
|
83
|
+
kind: ApiResourceKind.oauth_app,
|
|
84
|
+
slug: "vendor-oauth",
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
server.status = create(McpServerStatusSchema, {
|
|
89
|
+
validationState: ValidationState.valid,
|
|
90
|
+
oauthStatus: create(OAuthStatusSchema, {
|
|
91
|
+
vendorApprovalStatus: options.approval,
|
|
92
|
+
vendorApprovalDocsUrl: DOCS_URL,
|
|
93
|
+
effectiveOauthSource: OAuthAppSource.OAUTH_APP_SOURCE_PLATFORM,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
return server;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function noGrant() {
|
|
100
|
+
return create(GetOAuthGrantStatusOutputSchema, { connected: false });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Hosted-edition read: the org has no override yet. */
|
|
104
|
+
function noOverride() {
|
|
105
|
+
return create(GetOrgOAuthAppOutputSchema, { hasOverride: false });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function makeClient(register: Parameters<typeof createRouterTransport>[0]) {
|
|
109
|
+
return new Stigmer({
|
|
110
|
+
baseUrl: "/",
|
|
111
|
+
getAccessToken: () => "test-token",
|
|
112
|
+
customTransport: createRouterTransport(register),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function renderWithTransport(
|
|
117
|
+
ui: ReactNode,
|
|
118
|
+
register: Parameters<typeof createRouterTransport>[0] = () => {},
|
|
119
|
+
) {
|
|
120
|
+
return render(
|
|
121
|
+
<StigmerContext.Provider value={makeClient(register)}>
|
|
122
|
+
{ui}
|
|
123
|
+
</StigmerContext.Provider>,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Hook: useOrgOAuthApp capability probe
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
describe("useOrgOAuthApp — edition capability probe", () => {
|
|
132
|
+
function renderProbe(register: Parameters<typeof createRouterTransport>[0]) {
|
|
133
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
134
|
+
<StigmerContext.Provider value={makeClient(register)}>
|
|
135
|
+
{children}
|
|
136
|
+
</StigmerContext.Provider>
|
|
137
|
+
);
|
|
138
|
+
return renderHook(() => useOrgOAuthApp(SERVER_ID, ORG), { wrapper });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
it("reports isSupported=true once the hosted backend answers", async () => {
|
|
142
|
+
const { result } = renderProbe((router) => {
|
|
143
|
+
router.service(McpServerQueryController, { getOrgOAuthApp: noOverride });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
147
|
+
expect(result.current.isSupported).toBe(true);
|
|
148
|
+
expect(result.current.hasOverride).toBe(false);
|
|
149
|
+
expect(result.current.error).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("treats UNIMPLEMENTED as capability-absent, not as an error", async () => {
|
|
153
|
+
// No getOrgOAuthApp registration — the router answers UNIMPLEMENTED,
|
|
154
|
+
// exactly what the OSS server does for the whole org-override surface.
|
|
155
|
+
const { result } = renderProbe((router) => {
|
|
156
|
+
router.service(McpServerQueryController, { getOAuthGrantStatus: noGrant });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
160
|
+
expect(result.current.isSupported).toBe(false);
|
|
161
|
+
expect(result.current.hasOverride).toBe(false);
|
|
162
|
+
expect(result.current.error).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("keeps genuine failures as errors and does not claim support", async () => {
|
|
166
|
+
const { result } = renderProbe((router) => {
|
|
167
|
+
router.service(McpServerQueryController, {
|
|
168
|
+
getOrgOAuthApp: () => {
|
|
169
|
+
throw new ConnectError("boom", Code.Internal);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await waitFor(() => expect(result.current.error).not.toBeNull());
|
|
175
|
+
expect(result.current.isSupported).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("stays idle (unsupported, no fetch) without a resource or org", () => {
|
|
179
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
180
|
+
<StigmerContext.Provider value={makeClient(() => {})}>
|
|
181
|
+
{children}
|
|
182
|
+
</StigmerContext.Provider>
|
|
183
|
+
);
|
|
184
|
+
const { result } = renderHook(() => useOrgOAuthApp(null, null), { wrapper });
|
|
185
|
+
expect(result.current.isSupported).toBe(false);
|
|
186
|
+
expect(result.current.isLoading).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Surface: McpServerDetailView
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
describe("McpServerDetailView — BYOA affordances follow the capability probe", () => {
|
|
195
|
+
function loadedState(server: McpServer): UseMcpServerReturn {
|
|
196
|
+
return {
|
|
197
|
+
mcpServer: server,
|
|
198
|
+
isLoading: false,
|
|
199
|
+
isRefetching: false,
|
|
200
|
+
error: null,
|
|
201
|
+
refetch: vi.fn(),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function renderDetail(
|
|
206
|
+
server: McpServer,
|
|
207
|
+
register: Parameters<typeof createRouterTransport>[0],
|
|
208
|
+
) {
|
|
209
|
+
return renderWithTransport(
|
|
210
|
+
<McpServerDetailView org={ORG} slug={SLUG} mcpServerState={loadedState(server)} />,
|
|
211
|
+
register,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
it("offers the BYOA link when the hosted backend supports the surface", async () => {
|
|
216
|
+
renderDetail(
|
|
217
|
+
buildVendorOAuthServer({ approval: VendorApprovalStatus.APPROVED }),
|
|
218
|
+
(router) => {
|
|
219
|
+
router.service(McpServerQueryController, {
|
|
220
|
+
getOAuthGrantStatus: noGrant,
|
|
221
|
+
getOrgOAuthApp: noOverride,
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(
|
|
227
|
+
await screen.findByRole("button", { name: "Use your own OAuth app" }),
|
|
228
|
+
).toBeTruthy();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("hides the BYOA link on an OSS backend (getOrgOAuthApp UNIMPLEMENTED)", async () => {
|
|
232
|
+
renderDetail(
|
|
233
|
+
buildVendorOAuthServer({ approval: VendorApprovalStatus.APPROVED }),
|
|
234
|
+
(router) => {
|
|
235
|
+
router.service(McpServerQueryController, {
|
|
236
|
+
getOAuthGrantStatus: noGrant,
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Wait for the connect bar to settle (the manual-entry action proves
|
|
242
|
+
// the secondary-action row rendered) before asserting absence.
|
|
243
|
+
await screen.findByRole("button", { name: "Enter token manually" });
|
|
244
|
+
await waitFor(() =>
|
|
245
|
+
expect(
|
|
246
|
+
screen.queryByRole("button", { name: "Use your own OAuth app" }),
|
|
247
|
+
).toBeNull(),
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("degrades the vendor-blocked notice copy on OSS instead of offering a dead CTA", async () => {
|
|
252
|
+
renderDetail(
|
|
253
|
+
buildVendorOAuthServer({ approval: VendorApprovalStatus.PENDING }),
|
|
254
|
+
(router) => {
|
|
255
|
+
router.service(McpServerQueryController, {
|
|
256
|
+
getOAuthGrantStatus: noGrant,
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
expect(
|
|
262
|
+
await screen.findByText(
|
|
263
|
+
"The platform's OAuth app is awaiting vendor approval. You can still connect by entering your own token manually.",
|
|
264
|
+
),
|
|
265
|
+
).toBeTruthy();
|
|
266
|
+
expect(
|
|
267
|
+
screen.queryByRole("button", { name: "Use your own OAuth app" }),
|
|
268
|
+
).toBeNull();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// Surface: McpServerConnectDialog
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
describe("McpServerConnectDialog — BYOA escape hatch follows the capability probe", () => {
|
|
277
|
+
function renderDialog(register: Parameters<typeof createRouterTransport>[0]) {
|
|
278
|
+
return renderWithTransport(
|
|
279
|
+
<McpServerConnectDialog
|
|
280
|
+
org={ORG}
|
|
281
|
+
slug={SLUG}
|
|
282
|
+
open
|
|
283
|
+
onClose={() => {}}
|
|
284
|
+
onOpenDetails={() => {}}
|
|
285
|
+
/>,
|
|
286
|
+
register,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
it("keeps the BYOA button on a hosted backend", async () => {
|
|
291
|
+
renderDialog((router) => {
|
|
292
|
+
router.service(McpServerQueryController, {
|
|
293
|
+
getByReference: () =>
|
|
294
|
+
buildVendorOAuthServer({ approval: VendorApprovalStatus.PENDING }),
|
|
295
|
+
getOAuthGrantStatus: noGrant,
|
|
296
|
+
getOrgOAuthApp: noOverride,
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(
|
|
301
|
+
await screen.findByRole("button", { name: "Use your own OAuth app" }),
|
|
302
|
+
).toBeTruthy();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("hides the BYOA button on an OSS backend and keeps honest copy", async () => {
|
|
306
|
+
renderDialog((router) => {
|
|
307
|
+
router.service(McpServerQueryController, {
|
|
308
|
+
getByReference: () =>
|
|
309
|
+
buildVendorOAuthServer({ approval: VendorApprovalStatus.PENDING }),
|
|
310
|
+
getOAuthGrantStatus: noGrant,
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(
|
|
315
|
+
await screen.findByText(/is awaiting vendor approval/),
|
|
316
|
+
).toBeTruthy();
|
|
317
|
+
await waitFor(() =>
|
|
318
|
+
expect(
|
|
319
|
+
screen.queryByRole("button", { name: "Use your own OAuth app" }),
|
|
320
|
+
).toBeNull(),
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { renderHook, waitFor, cleanup } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { create } from "@bufbuild/protobuf";
|
|
5
|
+
import { createRouterTransport, ConnectError, Code } from "@connectrpc/connect";
|
|
6
|
+
import { Stigmer } from "@stigmer/sdk";
|
|
7
|
+
import { McpServerQueryController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/query_pb";
|
|
8
|
+
import {
|
|
9
|
+
GetOAuthGrantStatusOutputSchema,
|
|
10
|
+
GetOrgOAuthAppOutputSchema,
|
|
11
|
+
} from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
|
|
12
|
+
import { EnvironmentQueryController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/query_pb";
|
|
13
|
+
import { EnvironmentListSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/io_pb";
|
|
14
|
+
import {
|
|
15
|
+
McpServerSpecSchema,
|
|
16
|
+
McpServerAuthSchema,
|
|
17
|
+
HttpServerConfigSchema,
|
|
18
|
+
} from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
|
|
19
|
+
import { OAuthAppSource } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
|
|
20
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
21
|
+
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
22
|
+
import { samples } from "../../test/samples";
|
|
23
|
+
import { StigmerContext } from "../../context";
|
|
24
|
+
import { useMcpServerCredentials } from "../useMcpServerCredentials";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Pins the client-side derivation of the org-override signal
|
|
28
|
+
* (stigmer-cloud#401): `effectiveOAuthSource` / `isOrgOAuthApp` /
|
|
29
|
+
* `canBringOwnApp` are resolved from the `getOrgOAuthApp` RPC keyed on
|
|
30
|
+
* the hook's `org` parameter — NOT from `status.oauth_status` fields 3-4,
|
|
31
|
+
* which no backend populates (the caller's active org is client-side
|
|
32
|
+
* context the read RPCs never carry; see the OAuthStatus proto).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
afterEach(cleanup);
|
|
36
|
+
|
|
37
|
+
const ORG = "acme";
|
|
38
|
+
|
|
39
|
+
function buildServer(options: { withAuth: boolean; withAppRef: boolean }): McpServer {
|
|
40
|
+
const server = samples.mcpServer({ name: "Vendor CRM", org: ORG, slug: "vendor-crm" });
|
|
41
|
+
server.spec = create(McpServerSpecSchema, {
|
|
42
|
+
serverType: {
|
|
43
|
+
case: "http",
|
|
44
|
+
value: create(HttpServerConfigSchema, { url: "https://mcp.vendor.example.com" }),
|
|
45
|
+
},
|
|
46
|
+
...(options.withAuth
|
|
47
|
+
? {
|
|
48
|
+
auth: create(McpServerAuthSchema, {
|
|
49
|
+
targetEnvVar: "VENDOR_ACCESS_TOKEN",
|
|
50
|
+
...(options.withAppRef
|
|
51
|
+
? {
|
|
52
|
+
oauthAppRef: {
|
|
53
|
+
org: "stigmer",
|
|
54
|
+
kind: ApiResourceKind.oauth_app,
|
|
55
|
+
slug: "vendor-oauth",
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
: {}),
|
|
59
|
+
}),
|
|
60
|
+
}
|
|
61
|
+
: {}),
|
|
62
|
+
});
|
|
63
|
+
return server;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Baseline handlers so personal-env and grant-status fetches stay healthy. */
|
|
67
|
+
function baselineHandlers() {
|
|
68
|
+
return {
|
|
69
|
+
mcpServer: {
|
|
70
|
+
getOAuthGrantStatus: () =>
|
|
71
|
+
create(GetOAuthGrantStatusOutputSchema, { connected: false }),
|
|
72
|
+
},
|
|
73
|
+
environment: {
|
|
74
|
+
list: () => create(EnvironmentListSchema, { items: [] }),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderCredentials(
|
|
80
|
+
server: McpServer | null,
|
|
81
|
+
org: string | null,
|
|
82
|
+
register: Parameters<typeof createRouterTransport>[0],
|
|
83
|
+
) {
|
|
84
|
+
const client = new Stigmer({
|
|
85
|
+
baseUrl: "/",
|
|
86
|
+
getAccessToken: () => "test-token",
|
|
87
|
+
customTransport: createRouterTransport(register),
|
|
88
|
+
});
|
|
89
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
90
|
+
<StigmerContext.Provider value={client}>{children}</StigmerContext.Provider>
|
|
91
|
+
);
|
|
92
|
+
return renderHook(() => useMcpServerCredentials(org, server), { wrapper });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe("useMcpServerCredentials — org-override derivation", () => {
|
|
96
|
+
it("reports ORG_OVERRIDE when the org has a BYOA override", async () => {
|
|
97
|
+
const base = baselineHandlers();
|
|
98
|
+
const { result } = renderCredentials(
|
|
99
|
+
buildServer({ withAuth: true, withAppRef: true }),
|
|
100
|
+
ORG,
|
|
101
|
+
(router) => {
|
|
102
|
+
router.service(McpServerQueryController, {
|
|
103
|
+
...base.mcpServer,
|
|
104
|
+
getOrgOAuthApp: () =>
|
|
105
|
+
create(GetOrgOAuthAppOutputSchema, {
|
|
106
|
+
hasOverride: true,
|
|
107
|
+
oauthAppId: "oauthapp_01acme",
|
|
108
|
+
clientId: "acme-client-id",
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
router.service(EnvironmentQueryController, base.environment);
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
116
|
+
expect(result.current.isOrgOAuthApp).toBe(true);
|
|
117
|
+
expect(result.current.effectiveOAuthSource).toBe(
|
|
118
|
+
OAuthAppSource.OAUTH_APP_SOURCE_ORG_OVERRIDE,
|
|
119
|
+
);
|
|
120
|
+
expect(result.current.canBringOwnApp).toBe(false);
|
|
121
|
+
expect(result.current.error).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("reports PLATFORM when no override exists for the org", async () => {
|
|
125
|
+
const base = baselineHandlers();
|
|
126
|
+
const { result } = renderCredentials(
|
|
127
|
+
buildServer({ withAuth: true, withAppRef: true }),
|
|
128
|
+
ORG,
|
|
129
|
+
(router) => {
|
|
130
|
+
router.service(McpServerQueryController, {
|
|
131
|
+
...base.mcpServer,
|
|
132
|
+
getOrgOAuthApp: () =>
|
|
133
|
+
create(GetOrgOAuthAppOutputSchema, { hasOverride: false }),
|
|
134
|
+
});
|
|
135
|
+
router.service(EnvironmentQueryController, base.environment);
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
140
|
+
expect(result.current.isOrgOAuthApp).toBe(false);
|
|
141
|
+
expect(result.current.effectiveOAuthSource).toBe(
|
|
142
|
+
OAuthAppSource.OAUTH_APP_SOURCE_PLATFORM,
|
|
143
|
+
);
|
|
144
|
+
expect(result.current.canBringOwnApp).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("degrades to no-override with BYOA hidden when the backend does not implement the RPC (OSS)", async () => {
|
|
148
|
+
// getOrgOAuthApp deliberately NOT registered: the router transport
|
|
149
|
+
// answers UNIMPLEMENTED — exactly what the OSS server returns for the
|
|
150
|
+
// hosted-only org-override surface (stigmer/stigmer#558). No override
|
|
151
|
+
// can exist on that edition, so PLATFORM with no error is the truthful
|
|
152
|
+
// source — and canBringOwnApp stays false (the #558 capability gate:
|
|
153
|
+
// offering BYOA where the submit could only fail is a dead end).
|
|
154
|
+
const base = baselineHandlers();
|
|
155
|
+
const { result } = renderCredentials(
|
|
156
|
+
buildServer({ withAuth: true, withAppRef: true }),
|
|
157
|
+
ORG,
|
|
158
|
+
(router) => {
|
|
159
|
+
router.service(McpServerQueryController, base.mcpServer);
|
|
160
|
+
router.service(EnvironmentQueryController, base.environment);
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
165
|
+
expect(result.current.isOrgOAuthApp).toBe(false);
|
|
166
|
+
expect(result.current.effectiveOAuthSource).toBe(
|
|
167
|
+
OAuthAppSource.OAUTH_APP_SOURCE_PLATFORM,
|
|
168
|
+
);
|
|
169
|
+
expect(result.current.canBringOwnApp).toBe(false);
|
|
170
|
+
expect(result.current.error).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("reports NONE without firing the RPC when the server has no oauth_app_ref", async () => {
|
|
174
|
+
const base = baselineHandlers();
|
|
175
|
+
const getOrgOAuthApp = vi.fn(() =>
|
|
176
|
+
create(GetOrgOAuthAppOutputSchema, { hasOverride: true }),
|
|
177
|
+
);
|
|
178
|
+
const { result } = renderCredentials(
|
|
179
|
+
buildServer({ withAuth: true, withAppRef: false }),
|
|
180
|
+
ORG,
|
|
181
|
+
(router) => {
|
|
182
|
+
router.service(McpServerQueryController, {
|
|
183
|
+
...base.mcpServer,
|
|
184
|
+
getOrgOAuthApp,
|
|
185
|
+
});
|
|
186
|
+
router.service(EnvironmentQueryController, base.environment);
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
191
|
+
expect(result.current.effectiveOAuthSource).toBe(
|
|
192
|
+
OAuthAppSource.OAUTH_APP_SOURCE_NONE,
|
|
193
|
+
);
|
|
194
|
+
expect(result.current.isOrgOAuthApp).toBe(false);
|
|
195
|
+
expect(result.current.canBringOwnApp).toBe(false);
|
|
196
|
+
expect(getOrgOAuthApp).not.toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("stays UNSPECIFIED without firing the RPC on a manual server", async () => {
|
|
200
|
+
const base = baselineHandlers();
|
|
201
|
+
const getOrgOAuthApp = vi.fn(() =>
|
|
202
|
+
create(GetOrgOAuthAppOutputSchema, { hasOverride: true }),
|
|
203
|
+
);
|
|
204
|
+
const { result } = renderCredentials(
|
|
205
|
+
buildServer({ withAuth: false, withAppRef: false }),
|
|
206
|
+
ORG,
|
|
207
|
+
(router) => {
|
|
208
|
+
router.service(McpServerQueryController, {
|
|
209
|
+
...base.mcpServer,
|
|
210
|
+
getOrgOAuthApp,
|
|
211
|
+
});
|
|
212
|
+
router.service(EnvironmentQueryController, base.environment);
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
217
|
+
expect(result.current.authMode).toBe("manual");
|
|
218
|
+
expect(result.current.effectiveOAuthSource).toBe(
|
|
219
|
+
OAuthAppSource.OAUTH_APP_SOURCE_UNSPECIFIED,
|
|
220
|
+
);
|
|
221
|
+
expect(getOrgOAuthApp).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("reports UNSPECIFIED and surfaces the error when the lookup genuinely fails", async () => {
|
|
225
|
+
const base = baselineHandlers();
|
|
226
|
+
const { result } = renderCredentials(
|
|
227
|
+
buildServer({ withAuth: true, withAppRef: true }),
|
|
228
|
+
ORG,
|
|
229
|
+
(router) => {
|
|
230
|
+
router.service(McpServerQueryController, {
|
|
231
|
+
...base.mcpServer,
|
|
232
|
+
getOrgOAuthApp: () => {
|
|
233
|
+
throw new ConnectError("store unavailable", Code.Internal);
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
router.service(EnvironmentQueryController, base.environment);
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
await waitFor(() => expect(result.current.error).not.toBeNull());
|
|
241
|
+
// Unknown is not PLATFORM: guessing would hide a real override.
|
|
242
|
+
expect(result.current.effectiveOAuthSource).toBe(
|
|
243
|
+
OAuthAppSource.OAUTH_APP_SOURCE_UNSPECIFIED,
|
|
244
|
+
);
|
|
245
|
+
expect(result.current.isOrgOAuthApp).toBe(false);
|
|
246
|
+
// An errored probe never confirmed the surface exists — BYOA hides.
|
|
247
|
+
expect(result.current.canBringOwnApp).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("reports UNSPECIFIED without firing the RPC when org is null", async () => {
|
|
251
|
+
const base = baselineHandlers();
|
|
252
|
+
const getOrgOAuthApp = vi.fn(() =>
|
|
253
|
+
create(GetOrgOAuthAppOutputSchema, { hasOverride: true }),
|
|
254
|
+
);
|
|
255
|
+
const { result } = renderCredentials(
|
|
256
|
+
buildServer({ withAuth: true, withAppRef: true }),
|
|
257
|
+
null,
|
|
258
|
+
(router) => {
|
|
259
|
+
router.service(McpServerQueryController, {
|
|
260
|
+
...base.mcpServer,
|
|
261
|
+
getOrgOAuthApp,
|
|
262
|
+
});
|
|
263
|
+
router.service(EnvironmentQueryController, base.environment);
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
268
|
+
expect(result.current.effectiveOAuthSource).toBe(
|
|
269
|
+
OAuthAppSource.OAUTH_APP_SOURCE_UNSPECIFIED,
|
|
270
|
+
);
|
|
271
|
+
expect(getOrgOAuthApp).not.toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("refetch() re-resolves the override (BYOA set/remove flows update live)", async () => {
|
|
275
|
+
const base = baselineHandlers();
|
|
276
|
+
let hasOverride = false;
|
|
277
|
+
const { result } = renderCredentials(
|
|
278
|
+
buildServer({ withAuth: true, withAppRef: true }),
|
|
279
|
+
ORG,
|
|
280
|
+
(router) => {
|
|
281
|
+
router.service(McpServerQueryController, {
|
|
282
|
+
...base.mcpServer,
|
|
283
|
+
getOrgOAuthApp: () =>
|
|
284
|
+
create(GetOrgOAuthAppOutputSchema, { hasOverride }),
|
|
285
|
+
});
|
|
286
|
+
router.service(EnvironmentQueryController, base.environment);
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
await waitFor(() =>
|
|
291
|
+
expect(result.current.effectiveOAuthSource).toBe(
|
|
292
|
+
OAuthAppSource.OAUTH_APP_SOURCE_PLATFORM,
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
hasOverride = true;
|
|
297
|
+
result.current.refetch();
|
|
298
|
+
|
|
299
|
+
await waitFor(() =>
|
|
300
|
+
expect(result.current.effectiveOAuthSource).toBe(
|
|
301
|
+
OAuthAppSource.OAUTH_APP_SOURCE_ORG_OVERRIDE,
|
|
302
|
+
),
|
|
303
|
+
);
|
|
304
|
+
expect(result.current.isOrgOAuthApp).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
});
|