@stigmer/react 3.1.6 → 3.1.8

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.
Files changed (224) hide show
  1. package/agent/AgentDetailView.d.ts +9 -1
  2. package/agent/AgentDetailView.d.ts.map +1 -1
  3. package/agent/AgentDetailView.js +15 -4
  4. package/agent/AgentDetailView.js.map +1 -1
  5. package/environment/EnvironmentListPanel.d.ts.map +1 -1
  6. package/environment/EnvironmentListPanel.js +11 -3
  7. package/environment/EnvironmentListPanel.js.map +1 -1
  8. package/environment/EnvironmentVariableEditor.d.ts.map +1 -1
  9. package/environment/EnvironmentVariableEditor.js +9 -3
  10. package/environment/EnvironmentVariableEditor.js.map +1 -1
  11. package/environment/index.d.ts +1 -0
  12. package/environment/index.d.ts.map +1 -1
  13. package/environment/index.js +1 -0
  14. package/environment/index.js.map +1 -1
  15. package/environment/shareRestriction.d.ts +13 -0
  16. package/environment/shareRestriction.d.ts.map +1 -0
  17. package/environment/shareRestriction.js +15 -0
  18. package/environment/shareRestriction.js.map +1 -0
  19. package/execution/ToolCallItem.d.ts.map +1 -1
  20. package/execution/ToolCallItem.js +12 -0
  21. package/execution/ToolCallItem.js.map +1 -1
  22. package/execution/ToolRunGroup.js +4 -0
  23. package/execution/ToolRunGroup.js.map +1 -1
  24. package/execution/WriteBackCard.d.ts +14 -5
  25. package/execution/WriteBackCard.d.ts.map +1 -1
  26. package/execution/WriteBackCard.js +103 -32
  27. package/execution/WriteBackCard.js.map +1 -1
  28. package/execution/WriteBacksWidget.d.ts +1 -1
  29. package/execution/WriteBacksWidget.js +2 -2
  30. package/execution/WriteBacksWidget.js.map +1 -1
  31. package/execution/index.d.ts +2 -0
  32. package/execution/index.d.ts.map +1 -1
  33. package/execution/index.js +1 -0
  34. package/execution/index.js.map +1 -1
  35. package/execution/write-back-utils.d.ts +57 -0
  36. package/execution/write-back-utils.d.ts.map +1 -0
  37. package/execution/write-back-utils.js +81 -0
  38. package/execution/write-back-utils.js.map +1 -0
  39. package/index.d.ts +6 -2
  40. package/index.d.ts.map +1 -1
  41. package/index.js +6 -1
  42. package/index.js.map +1 -1
  43. package/internal/dev/dom-counter.d.ts.map +1 -1
  44. package/internal/dev/dom-counter.js +2 -2
  45. package/internal/dev/dom-counter.js.map +1 -1
  46. package/internal/dev/enabled.d.ts +7 -0
  47. package/internal/dev/enabled.d.ts.map +1 -0
  48. package/internal/dev/enabled.js +38 -0
  49. package/internal/dev/enabled.js.map +1 -0
  50. package/internal/dev/profiler-wrapper.d.ts +3 -3
  51. package/internal/dev/profiler-wrapper.d.ts.map +1 -1
  52. package/internal/dev/profiler-wrapper.js +5 -5
  53. package/internal/dev/profiler-wrapper.js.map +1 -1
  54. package/internal/dev/use-key-stability.d.ts.map +1 -1
  55. package/internal/dev/use-key-stability.js +2 -2
  56. package/internal/dev/use-key-stability.js.map +1 -1
  57. package/internal/dev/use-render-tracer.d.ts +2 -2
  58. package/internal/dev/use-render-tracer.d.ts.map +1 -1
  59. package/internal/dev/use-render-tracer.js +4 -4
  60. package/internal/dev/use-render-tracer.js.map +1 -1
  61. package/internal/dev/use-stream-rate.d.ts +2 -1
  62. package/internal/dev/use-stream-rate.d.ts.map +1 -1
  63. package/internal/dev/use-stream-rate.js +4 -3
  64. package/internal/dev/use-stream-rate.js.map +1 -1
  65. package/library/ResourceVisibilityControl.d.ts +3 -0
  66. package/library/ResourceVisibilityControl.d.ts.map +1 -1
  67. package/library/ResourceVisibilityControl.js +22 -8
  68. package/library/ResourceVisibilityControl.js.map +1 -1
  69. package/library/useUpdateVisibility.d.ts +5 -3
  70. package/library/useUpdateVisibility.d.ts.map +1 -1
  71. package/library/useUpdateVisibility.js +7 -2
  72. package/library/useUpdateVisibility.js.map +1 -1
  73. package/library/visibilityLevels.d.ts +17 -0
  74. package/library/visibilityLevels.d.ts.map +1 -1
  75. package/library/visibilityLevels.js +32 -0
  76. package/library/visibilityLevels.js.map +1 -1
  77. package/mcp-server/McpServerConnectDialog.d.ts.map +1 -1
  78. package/mcp-server/McpServerConnectDialog.js +7 -3
  79. package/mcp-server/McpServerConnectDialog.js.map +1 -1
  80. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  81. package/mcp-server/McpServerDetailView.js +6 -3
  82. package/mcp-server/McpServerDetailView.js.map +1 -1
  83. package/mcp-server/McpServerPicker.d.ts.map +1 -1
  84. package/mcp-server/McpServerPicker.js +3 -1
  85. package/mcp-server/McpServerPicker.js.map +1 -1
  86. package/mcp-server/OAuthRequiredNotice.d.ts +29 -0
  87. package/mcp-server/OAuthRequiredNotice.d.ts.map +1 -0
  88. package/mcp-server/OAuthRequiredNotice.js +29 -0
  89. package/mcp-server/OAuthRequiredNotice.js.map +1 -0
  90. package/mcp-server/StdioSandboxNotice.d.ts +43 -0
  91. package/mcp-server/StdioSandboxNotice.d.ts.map +1 -0
  92. package/mcp-server/StdioSandboxNotice.js +45 -0
  93. package/mcp-server/StdioSandboxNotice.js.map +1 -0
  94. package/mcp-server/useMcpServerCredentials.d.ts +12 -0
  95. package/mcp-server/useMcpServerCredentials.d.ts.map +1 -1
  96. package/mcp-server/useMcpServerCredentials.js +12 -4
  97. package/mcp-server/useMcpServerCredentials.js.map +1 -1
  98. package/package.json +4 -4
  99. package/session/NewSessionViewer.d.ts +11 -1
  100. package/session/NewSessionViewer.d.ts.map +1 -1
  101. package/session/NewSessionViewer.js +28 -8
  102. package/session/NewSessionViewer.js.map +1 -1
  103. package/session/SessionViewer.d.ts +9 -2
  104. package/session/SessionViewer.d.ts.map +1 -1
  105. package/session/SessionViewer.js +26 -12
  106. package/session/SessionViewer.js.map +1 -1
  107. package/session/audience.d.ts +12 -4
  108. package/session/audience.d.ts.map +1 -1
  109. package/session/facets/ChangesTab.d.ts.map +1 -1
  110. package/session/facets/ChangesTab.js +3 -1
  111. package/session/facets/ChangesTab.js.map +1 -1
  112. package/session/useNewSessionFlow.d.ts +12 -0
  113. package/session/useNewSessionFlow.d.ts.map +1 -1
  114. package/session/useNewSessionFlow.js +13 -1
  115. package/session/useNewSessionFlow.js.map +1 -1
  116. package/session/useSessionPageFlow.d.ts +13 -0
  117. package/session/useSessionPageFlow.d.ts.map +1 -1
  118. package/session/useSessionPageFlow.js +7 -2
  119. package/session/useSessionPageFlow.js.map +1 -1
  120. package/sharing/ShareAgentDialog.d.ts +55 -0
  121. package/sharing/ShareAgentDialog.d.ts.map +1 -0
  122. package/sharing/ShareAgentDialog.js +393 -0
  123. package/sharing/ShareAgentDialog.js.map +1 -0
  124. package/sharing/SharedAgentChat.d.ts +84 -0
  125. package/sharing/SharedAgentChat.d.ts.map +1 -0
  126. package/sharing/SharedAgentChat.js +80 -0
  127. package/sharing/SharedAgentChat.js.map +1 -0
  128. package/sharing/index.d.ts +16 -0
  129. package/sharing/index.d.ts.map +1 -0
  130. package/sharing/index.js +11 -0
  131. package/sharing/index.js.map +1 -0
  132. package/sharing/useRotateShareLink.d.ts +42 -0
  133. package/sharing/useRotateShareLink.d.ts.map +1 -0
  134. package/sharing/useRotateShareLink.js +55 -0
  135. package/sharing/useRotateShareLink.js.map +1 -0
  136. package/sharing/useShareAgent.d.ts +68 -0
  137. package/sharing/useShareAgent.d.ts.map +1 -0
  138. package/sharing/useShareAgent.js +41 -0
  139. package/sharing/useShareAgent.js.map +1 -0
  140. package/sharing/useShareToolReadiness.d.ts +48 -0
  141. package/sharing/useShareToolReadiness.d.ts.map +1 -0
  142. package/sharing/useShareToolReadiness.js +93 -0
  143. package/sharing/useShareToolReadiness.js.map +1 -0
  144. package/sharing/useSharedAgentProfile.d.ts +84 -0
  145. package/sharing/useSharedAgentProfile.d.ts.map +1 -0
  146. package/sharing/useSharedAgentProfile.js +72 -0
  147. package/sharing/useSharedAgentProfile.js.map +1 -0
  148. package/sharing/useUpdateAgentSharing.d.ts +86 -0
  149. package/sharing/useUpdateAgentSharing.d.ts.map +1 -0
  150. package/sharing/useUpdateAgentSharing.js +81 -0
  151. package/sharing/useUpdateAgentSharing.js.map +1 -0
  152. package/src/agent/AgentDetailView.tsx +28 -3
  153. package/src/environment/EnvironmentListPanel.tsx +47 -18
  154. package/src/environment/EnvironmentVariableEditor.tsx +27 -17
  155. package/src/environment/__tests__/shareRestriction.test.ts +35 -0
  156. package/src/environment/index.ts +5 -0
  157. package/src/environment/shareRestriction.ts +22 -0
  158. package/src/execution/ToolCallItem.tsx +19 -1
  159. package/src/execution/ToolRunGroup.tsx +4 -0
  160. package/src/execution/WriteBackCard.tsx +285 -88
  161. package/src/execution/WriteBacksWidget.tsx +5 -5
  162. package/src/execution/__tests__/ToolCallItem.test.tsx +51 -0
  163. package/src/execution/__tests__/ToolRunGroup.test.tsx +22 -0
  164. package/src/execution/__tests__/WriteBackCard.test.tsx +64 -10
  165. package/src/execution/__tests__/write-back-utils.test.ts +135 -0
  166. package/src/execution/index.ts +7 -0
  167. package/src/execution/write-back-utils.ts +107 -0
  168. package/src/index.ts +34 -0
  169. package/src/internal/dev/__tests__/use-key-stability.test.ts +4 -0
  170. package/src/internal/dev/__tests__/use-render-tracer.test.ts +4 -0
  171. package/src/internal/dev/dom-counter.ts +2 -3
  172. package/src/internal/dev/enabled.ts +39 -0
  173. package/src/internal/dev/profiler-wrapper.tsx +5 -6
  174. package/src/internal/dev/use-key-stability.ts +2 -3
  175. package/src/internal/dev/use-render-tracer.ts +4 -5
  176. package/src/internal/dev/use-stream-rate.ts +4 -4
  177. package/src/library/ResourceVisibilityControl.tsx +23 -7
  178. package/src/library/__tests__/visibilityLevels.test.ts +33 -0
  179. package/src/library/useUpdateVisibility.ts +9 -3
  180. package/src/library/visibilityLevels.ts +36 -0
  181. package/src/mcp-server/McpServerConnectDialog.tsx +23 -11
  182. package/src/mcp-server/McpServerDetailView.tsx +22 -9
  183. package/src/mcp-server/McpServerPicker.tsx +12 -9
  184. package/src/mcp-server/OAuthRequiredNotice.tsx +77 -0
  185. package/src/mcp-server/StdioSandboxNotice.tsx +98 -0
  186. package/src/mcp-server/__tests__/OAuthRequiredNotice.test.tsx +25 -0
  187. package/src/mcp-server/__tests__/StdioSandboxNotice.test.tsx +71 -0
  188. package/src/mcp-server/useMcpServerCredentials.ts +25 -4
  189. package/src/session/NewSessionViewer.tsx +67 -31
  190. package/src/session/SessionViewer.tsx +67 -36
  191. package/src/session/__tests__/audienceWiring.test.tsx +99 -0
  192. package/src/session/__tests__/useNewSessionFlow.test.tsx +75 -1
  193. package/src/session/__tests__/useSessionPageFlow.guest.test.tsx +112 -0
  194. package/src/session/audience.ts +12 -4
  195. package/src/session/facets/ChangesTab.tsx +6 -4
  196. package/src/session/useNewSessionFlow.ts +26 -1
  197. package/src/session/useSessionPageFlow.ts +24 -2
  198. package/src/sharing/ShareAgentDialog.tsx +1255 -0
  199. package/src/sharing/SharedAgentChat.tsx +283 -0
  200. package/src/sharing/__tests__/ShareAgentDialog.test.tsx +745 -0
  201. package/src/sharing/__tests__/SharedAgentChat.test.tsx +222 -0
  202. package/src/sharing/__tests__/useRotateShareLink.test.tsx +96 -0
  203. package/src/sharing/__tests__/useShareAgent.test.tsx +127 -0
  204. package/src/sharing/__tests__/useShareToolReadiness.test.tsx +182 -0
  205. package/src/sharing/__tests__/useSharedAgentProfile.test.tsx +201 -0
  206. package/src/sharing/__tests__/useUpdateAgentSharing.test.tsx +200 -0
  207. package/src/sharing/index.ts +30 -0
  208. package/src/sharing/useRotateShareLink.ts +76 -0
  209. package/src/sharing/useShareAgent.tsx +106 -0
  210. package/src/sharing/useShareToolReadiness.ts +129 -0
  211. package/src/sharing/useSharedAgentProfile.ts +131 -0
  212. package/src/sharing/useUpdateAgentSharing.ts +151 -0
  213. package/src/switch/Switch.tsx +92 -0
  214. package/src/switch/__tests__/Switch.test.tsx +88 -0
  215. package/src/switch/index.ts +2 -0
  216. package/styles.css +1 -1
  217. package/switch/Switch.d.ts +47 -0
  218. package/switch/Switch.d.ts.map +1 -0
  219. package/switch/Switch.js +36 -0
  220. package/switch/Switch.js.map +1 -0
  221. package/switch/index.d.ts +3 -0
  222. package/switch/index.d.ts.map +1 -0
  223. package/switch/index.js +2 -0
  224. package/switch/index.js.map +1 -0
@@ -0,0 +1,745 @@
1
+ import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
2
+ import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import type { UpdateAgentSharingInput } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/io_pb";
5
+ import { AgentSharingAudience } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
6
+ import { StigmerContext } from "../../context";
7
+ import { FetchCacheContext } from "../../internal/FetchCacheProvider";
8
+ import { DeploymentModeContext } from "../../deployment-mode";
9
+ import { ShareAgentDialog } from "../ShareAgentDialog";
10
+
11
+ // Toasts are visual feedback owned by the feedback module; keep them inert.
12
+ vi.mock("../../feedback/toast", () => ({
13
+ toast: { success: vi.fn(), error: vi.fn() },
14
+ }));
15
+
16
+ // happy-dom does not implement the native dialog show/close methods.
17
+ beforeAll(() => {
18
+ HTMLDialogElement.prototype.showModal = function showModal() {
19
+ this.open = true;
20
+ };
21
+ HTMLDialogElement.prototype.close = function close() {
22
+ this.open = false;
23
+ };
24
+ });
25
+
26
+ afterEach(cleanup);
27
+
28
+ interface MockOverrides {
29
+ updateSharing?: (input: UpdateAgentSharingInput) => Promise<unknown>;
30
+ rotateShareLink?: (input: unknown) => Promise<unknown>;
31
+ getOrCreateBillingAccount?: (orgId: string) => Promise<unknown>;
32
+ }
33
+
34
+ function createMockStigmer(overrides: MockOverrides = {}) {
35
+ return {
36
+ agent: {
37
+ updateSharing: overrides.updateSharing ?? vi.fn().mockResolvedValue({}),
38
+ rotateShareLink:
39
+ overrides.rotateShareLink ?? vi.fn().mockResolvedValue({}),
40
+ },
41
+ billing: {
42
+ getOrCreateBillingAccount:
43
+ overrides.getOrCreateBillingAccount ??
44
+ vi.fn().mockResolvedValue(null),
45
+ },
46
+ } as never;
47
+ }
48
+
49
+ function Providers({
50
+ client,
51
+ mode = "cloud",
52
+ children,
53
+ }: {
54
+ client: unknown;
55
+ mode?: "cloud" | "local";
56
+ children: ReactNode;
57
+ }) {
58
+ return (
59
+ <FetchCacheContext.Provider value={null}>
60
+ <DeploymentModeContext.Provider value={mode}>
61
+ <StigmerContext.Provider value={client as never}>
62
+ {children}
63
+ </StigmerContext.Provider>
64
+ </DeploymentModeContext.Provider>
65
+ </FetchCacheContext.Provider>
66
+ );
67
+ }
68
+
69
+ function makeAgent(
70
+ sharing?: {
71
+ enabled?: boolean;
72
+ audience?: AgentSharingAudience;
73
+ allowedOrigins?: string[];
74
+ messages?: {
75
+ rateLimited?: string;
76
+ unavailable?: string;
77
+ conversationEnded?: string;
78
+ };
79
+ },
80
+ shareLinkToken?: string,
81
+ ) {
82
+ return {
83
+ metadata: {
84
+ id: "agt_1",
85
+ org: "acme",
86
+ slug: "support-agent",
87
+ name: "Support Agent",
88
+ },
89
+ spec: { sharing },
90
+ ...(shareLinkToken !== undefined
91
+ ? { status: { shareLinkToken } }
92
+ : {}),
93
+ } as never;
94
+ }
95
+
96
+ const buildShareUrl = (org: string, slug: string) =>
97
+ `https://app.example.com/chat/${org}/${slug}`;
98
+
99
+ describe("ShareAgentDialog", () => {
100
+ it("mounts no body while closed (billing fetch stays lazy)", () => {
101
+ const getOrCreateBillingAccount = vi.fn();
102
+ render(
103
+ <Providers client={createMockStigmer({ getOrCreateBillingAccount })}>
104
+ <ShareAgentDialog
105
+ open={false}
106
+ onOpenChange={() => {}}
107
+ agent={makeAgent()}
108
+ buildShareUrl={buildShareUrl}
109
+ />
110
+ </Providers>,
111
+ );
112
+
113
+ expect(screen.queryByText("Share")).toBeNull();
114
+ expect(getOrCreateBillingAccount).not.toHaveBeenCalled();
115
+ });
116
+
117
+ it("renders header, toggle, and the share link from buildShareUrl", () => {
118
+ render(
119
+ <Providers client={createMockStigmer()}>
120
+ <ShareAgentDialog
121
+ open
122
+ onOpenChange={() => {}}
123
+ agent={makeAgent({ enabled: true })}
124
+ buildShareUrl={buildShareUrl}
125
+ />
126
+ </Providers>,
127
+ );
128
+
129
+ expect(screen.getByText("Share")).toBeTruthy();
130
+ expect(screen.getByText("Support Agent")).toBeTruthy();
131
+ expect(screen.getByRole("switch", { hidden: true }).getAttribute("aria-checked")).toBe("true");
132
+ expect(
133
+ screen.getByText("https://app.example.com/chat/acme/support-agent"),
134
+ ).toBeTruthy();
135
+ });
136
+
137
+ it("falls back to the relative /chat path when buildShareUrl is omitted", () => {
138
+ render(
139
+ <Providers client={createMockStigmer()}>
140
+ <ShareAgentDialog
141
+ open
142
+ onOpenChange={() => {}}
143
+ agent={makeAgent({ enabled: true })}
144
+ />
145
+ </Providers>,
146
+ );
147
+
148
+ expect(screen.getByText("/chat/acme/support-agent")).toBeTruthy();
149
+ });
150
+
151
+ it("shows the indexability warning", () => {
152
+ render(
153
+ <Providers client={createMockStigmer()}>
154
+ <ShareAgentDialog
155
+ open
156
+ onOpenChange={() => {}}
157
+ agent={makeAgent({ enabled: true })}
158
+ buildShareUrl={buildShareUrl}
159
+ />
160
+ </Providers>,
161
+ );
162
+
163
+ expect(
164
+ screen.getByText(/forwarded and indexed by search engines/),
165
+ ).toBeTruthy();
166
+ });
167
+
168
+ describe("Audience", () => {
169
+ it("defaults to Public link and switching to Org members commits the full block", async () => {
170
+ const updateSharing = vi.fn().mockResolvedValue({});
171
+ render(
172
+ <Providers client={createMockStigmer({ updateSharing })}>
173
+ <ShareAgentDialog
174
+ open
175
+ onOpenChange={() => {}}
176
+ agent={makeAgent({
177
+ enabled: true,
178
+ allowedOrigins: ["https://example.com"],
179
+ messages: { rateLimited: "Easy there." },
180
+ })}
181
+ buildShareUrl={buildShareUrl}
182
+ />
183
+ </Providers>,
184
+ );
185
+
186
+ const publicOption = screen.getByRole("radio", {
187
+ name: "Public link",
188
+ hidden: true,
189
+ });
190
+ expect(publicOption.getAttribute("aria-checked")).toBe("true");
191
+
192
+ fireEvent.click(
193
+ screen.getByRole("radio", { name: "Org members", hidden: true }),
194
+ );
195
+
196
+ await waitFor(() => expect(updateSharing).toHaveBeenCalledTimes(1));
197
+ const input = updateSharing.mock.calls[0][0] as UpdateAgentSharingInput;
198
+ expect(input.sharing?.audience).toBe(AgentSharingAudience.org);
199
+ // Whole-block replace: the audience switch must not clobber the rest.
200
+ expect(input.sharing?.enabled).toBe(true);
201
+ expect(input.sharing?.allowedOrigins).toEqual(["https://example.com"]);
202
+ expect(input.sharing?.messages?.rateLimited).toBe("Easy there.");
203
+ });
204
+
205
+ it("renders org-audience copy: member link, revocation note, no indexability warning", () => {
206
+ render(
207
+ <Providers client={createMockStigmer()}>
208
+ <ShareAgentDialog
209
+ open
210
+ onOpenChange={() => {}}
211
+ agent={makeAgent({
212
+ enabled: true,
213
+ audience: AgentSharingAudience.org,
214
+ })}
215
+ buildShareUrl={buildShareUrl}
216
+ />
217
+ </Providers>,
218
+ );
219
+
220
+ expect(screen.getByText("Organization members can chat")).toBeTruthy();
221
+ expect(screen.getByText("Member chat link")).toBeTruthy();
222
+ expect(
223
+ screen.getByText(/access is checked on every message/i),
224
+ ).toBeTruthy();
225
+ expect(
226
+ screen.queryByText(/forwarded and indexed by search engines/),
227
+ ).toBeNull();
228
+ });
229
+
230
+ it("replaces the Embed tab with a public-only explanation for the org audience", () => {
231
+ render(
232
+ <Providers client={createMockStigmer()}>
233
+ <ShareAgentDialog
234
+ open
235
+ onOpenChange={() => {}}
236
+ agent={makeAgent({
237
+ enabled: true,
238
+ audience: AgentSharingAudience.org,
239
+ })}
240
+ buildShareUrl={buildShareUrl}
241
+ />
242
+ </Providers>,
243
+ );
244
+
245
+ fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
246
+ expect(
247
+ screen.getByText(/Embedding isn't available for org-members-only sharing/),
248
+ ).toBeTruthy();
249
+ expect(screen.queryByText(/<stigmer-agent/)).toBeNull();
250
+ });
251
+ });
252
+
253
+ it("enabling sends the complete sharing block and notifies the host", async () => {
254
+ const updateSharing = vi.fn().mockResolvedValue({});
255
+ const onSharingChanged = vi.fn();
256
+ render(
257
+ <Providers client={createMockStigmer({ updateSharing })}>
258
+ <ShareAgentDialog
259
+ open
260
+ onOpenChange={() => {}}
261
+ agent={makeAgent({
262
+ enabled: false,
263
+ allowedOrigins: ["https://example.com"],
264
+ messages: { rateLimited: "Easy there." },
265
+ })}
266
+ buildShareUrl={buildShareUrl}
267
+ onSharingChanged={onSharingChanged}
268
+ />
269
+ </Providers>,
270
+ );
271
+
272
+ fireEvent.click(screen.getByRole("switch", { hidden: true }));
273
+
274
+ await waitFor(() => expect(updateSharing).toHaveBeenCalledTimes(1));
275
+ const input = updateSharing.mock.calls[0][0] as UpdateAgentSharingInput;
276
+ expect(input.resourceId).toBe("agt_1");
277
+ expect(input.sharing?.enabled).toBe(true);
278
+ // The RPC replaces spec.sharing wholesale — the toggle must carry the
279
+ // existing origins and messages or it would silently erase them.
280
+ expect(input.sharing?.allowedOrigins).toEqual(["https://example.com"]);
281
+ expect(input.sharing?.messages?.rateLimited).toBe("Easy there.");
282
+ await waitFor(() => expect(onSharingChanged).toHaveBeenCalled());
283
+ });
284
+
285
+ it("adopts the server's returned sharing state after a commit", async () => {
286
+ // Server echoes the update but with a normalized origin list.
287
+ const updateSharing = vi.fn().mockResolvedValue(
288
+ makeAgent({
289
+ enabled: true,
290
+ allowedOrigins: ["https://normalized.example.com"],
291
+ }),
292
+ );
293
+ render(
294
+ <Providers client={createMockStigmer({ updateSharing })}>
295
+ <ShareAgentDialog
296
+ open
297
+ onOpenChange={() => {}}
298
+ agent={makeAgent({ enabled: false })}
299
+ buildShareUrl={buildShareUrl}
300
+ />
301
+ </Providers>,
302
+ );
303
+
304
+ fireEvent.click(screen.getByRole("switch", { hidden: true }));
305
+ await waitFor(() =>
306
+ expect(screen.getByRole("switch", { hidden: true }).getAttribute("aria-checked")).toBe(
307
+ "true",
308
+ ),
309
+ );
310
+
311
+ fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
312
+ expect(screen.getByText("https://normalized.example.com")).toBeTruthy();
313
+ });
314
+
315
+ describe("Embed tab", () => {
316
+ it("shows the one-line script snippet: loader from the app origin + <stigmer-agent>", () => {
317
+ render(
318
+ <Providers client={createMockStigmer()}>
319
+ <ShareAgentDialog
320
+ open
321
+ onOpenChange={() => {}}
322
+ agent={makeAgent({ enabled: true })}
323
+ buildShareUrl={buildShareUrl}
324
+ />
325
+ </Providers>,
326
+ );
327
+
328
+ fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
329
+ // The loader must be served from the SAME origin as the share URL —
330
+ // embed.js derives the chat-page origin from its own script URL.
331
+ expect(
332
+ screen.getByText(
333
+ /<script src="https:\/\/app\.example\.com\/embed\.js" async><\/script>/,
334
+ ),
335
+ ).toBeTruthy();
336
+ expect(
337
+ screen.getByText(
338
+ /<stigmer-agent org="acme" agent="support-agent"><\/stigmer-agent>/,
339
+ ),
340
+ ).toBeTruthy();
341
+ });
342
+
343
+ it("keeps the iframe snippet available as the collapsed no-JavaScript alternative", () => {
344
+ render(
345
+ <Providers client={createMockStigmer()}>
346
+ <ShareAgentDialog
347
+ open
348
+ onOpenChange={() => {}}
349
+ agent={makeAgent({ enabled: true })}
350
+ buildShareUrl={buildShareUrl}
351
+ />
352
+ </Providers>,
353
+ );
354
+
355
+ fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
356
+ // Collapsed by default — the script snippet is the primary path.
357
+ expect(screen.queryByText(/<iframe/)).toBeNull();
358
+
359
+ fireEvent.click(
360
+ screen.getByRole("button", {
361
+ name: /No-JavaScript alternative/,
362
+ hidden: true,
363
+ }),
364
+ );
365
+ expect(
366
+ screen.getByText(
367
+ /src="https:\/\/app\.example\.com\/chat\/acme\/support-agent"/,
368
+ ),
369
+ ).toBeTruthy();
370
+ });
371
+
372
+ it("rejects an invalid origin without calling the RPC", async () => {
373
+ const updateSharing = vi.fn();
374
+ render(
375
+ <Providers client={createMockStigmer({ updateSharing })}>
376
+ <ShareAgentDialog
377
+ open
378
+ onOpenChange={() => {}}
379
+ agent={makeAgent({ enabled: true })}
380
+ buildShareUrl={buildShareUrl}
381
+ />
382
+ </Providers>,
383
+ );
384
+
385
+ fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
386
+ const input = screen.getByLabelText("Add allowed origin");
387
+ fireEvent.change(input, {
388
+ target: { value: "https://example.com/path" },
389
+ });
390
+ fireEvent.click(screen.getByText("Add"));
391
+
392
+ expect(await screen.findByRole("alert", { hidden: true })).toBeTruthy();
393
+ expect(updateSharing).not.toHaveBeenCalled();
394
+ });
395
+
396
+ it("adds a valid origin by committing the appended list", async () => {
397
+ const updateSharing = vi.fn().mockResolvedValue({});
398
+ render(
399
+ <Providers client={createMockStigmer({ updateSharing })}>
400
+ <ShareAgentDialog
401
+ open
402
+ onOpenChange={() => {}}
403
+ agent={makeAgent({
404
+ enabled: true,
405
+ allowedOrigins: ["https://existing.example.com"],
406
+ })}
407
+ buildShareUrl={buildShareUrl}
408
+ />
409
+ </Providers>,
410
+ );
411
+
412
+ fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
413
+ fireEvent.change(screen.getByLabelText("Add allowed origin"), {
414
+ target: { value: "https://new.example.com" },
415
+ });
416
+ fireEvent.click(screen.getByText("Add"));
417
+
418
+ await waitFor(() => expect(updateSharing).toHaveBeenCalledTimes(1));
419
+ const input = updateSharing.mock.calls[0][0] as UpdateAgentSharingInput;
420
+ expect(input.sharing?.allowedOrigins).toEqual([
421
+ "https://existing.example.com",
422
+ "https://new.example.com",
423
+ ]);
424
+ // Enabled state rides along untouched.
425
+ expect(input.sharing?.enabled).toBe(true);
426
+ });
427
+
428
+ it("rejects a duplicate origin", async () => {
429
+ const updateSharing = vi.fn();
430
+ render(
431
+ <Providers client={createMockStigmer({ updateSharing })}>
432
+ <ShareAgentDialog
433
+ open
434
+ onOpenChange={() => {}}
435
+ agent={makeAgent({
436
+ enabled: true,
437
+ allowedOrigins: ["https://example.com"],
438
+ })}
439
+ buildShareUrl={buildShareUrl}
440
+ />
441
+ </Providers>,
442
+ );
443
+
444
+ fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
445
+ fireEvent.change(screen.getByLabelText("Add allowed origin"), {
446
+ target: { value: "https://example.com" },
447
+ });
448
+ fireEvent.click(screen.getByText("Add"));
449
+
450
+ expect(await screen.findByRole("alert", { hidden: true })).toBeTruthy();
451
+ expect(updateSharing).not.toHaveBeenCalled();
452
+ });
453
+
454
+ it("removes an origin by committing the filtered list", async () => {
455
+ const updateSharing = vi.fn().mockResolvedValue({});
456
+ render(
457
+ <Providers client={createMockStigmer({ updateSharing })}>
458
+ <ShareAgentDialog
459
+ open
460
+ onOpenChange={() => {}}
461
+ agent={makeAgent({
462
+ enabled: true,
463
+ allowedOrigins: ["https://a.example.com", "https://b.example.com"],
464
+ })}
465
+ buildShareUrl={buildShareUrl}
466
+ />
467
+ </Providers>,
468
+ );
469
+
470
+ fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
471
+ fireEvent.click(screen.getByLabelText("Remove https://a.example.com"));
472
+
473
+ await waitFor(() => expect(updateSharing).toHaveBeenCalledTimes(1));
474
+ const input = updateSharing.mock.calls[0][0] as UpdateAgentSharingInput;
475
+ expect(input.sharing?.allowedOrigins).toEqual(["https://b.example.com"]);
476
+ });
477
+ });
478
+
479
+ describe("visitor messages", () => {
480
+ it("saves edited messages as part of the complete block", async () => {
481
+ const updateSharing = vi.fn().mockResolvedValue({});
482
+ render(
483
+ <Providers client={createMockStigmer({ updateSharing })}>
484
+ <ShareAgentDialog
485
+ open
486
+ onOpenChange={() => {}}
487
+ agent={makeAgent({
488
+ enabled: true,
489
+ allowedOrigins: ["https://example.com"],
490
+ })}
491
+ buildShareUrl={buildShareUrl}
492
+ />
493
+ </Providers>,
494
+ );
495
+
496
+ fireEvent.click(screen.getByText("Customize visitor messages"));
497
+ fireEvent.change(screen.getByLabelText(/Rate limited/), {
498
+ target: { value: "Please slow down." },
499
+ });
500
+ fireEvent.click(screen.getByText("Save messages"));
501
+
502
+ await waitFor(() => expect(updateSharing).toHaveBeenCalledTimes(1));
503
+ const input = updateSharing.mock.calls[0][0] as UpdateAgentSharingInput;
504
+ expect(input.sharing?.messages?.rateLimited).toBe("Please slow down.");
505
+ expect(input.sharing?.enabled).toBe(true);
506
+ expect(input.sharing?.allowedOrigins).toEqual(["https://example.com"]);
507
+ });
508
+
509
+ it("caps each message at 300 characters", () => {
510
+ render(
511
+ <Providers client={createMockStigmer()}>
512
+ <ShareAgentDialog
513
+ open
514
+ onOpenChange={() => {}}
515
+ agent={makeAgent({ enabled: true })}
516
+ buildShareUrl={buildShareUrl}
517
+ />
518
+ </Providers>,
519
+ );
520
+
521
+ fireEvent.click(screen.getByText("Customize visitor messages"));
522
+ const field = screen.getByLabelText(/Rate limited/) as HTMLTextAreaElement;
523
+ fireEvent.change(field, { target: { value: "x".repeat(400) } });
524
+ expect(field.value.length).toBe(300);
525
+ });
526
+ });
527
+
528
+ describe("who pays", () => {
529
+ it("shows the who-pays line with the balance on cloud", async () => {
530
+ const getOrCreateBillingAccount = vi.fn().mockResolvedValue({
531
+ balance: { availableMicros: BigInt(12_500_000) },
532
+ });
533
+ render(
534
+ <Providers
535
+ client={createMockStigmer({ getOrCreateBillingAccount })}
536
+ mode="cloud"
537
+ >
538
+ <ShareAgentDialog
539
+ open
540
+ onOpenChange={() => {}}
541
+ agent={makeAgent({ enabled: true })}
542
+ buildShareUrl={buildShareUrl}
543
+ />
544
+ </Providers>,
545
+ );
546
+
547
+ expect(screen.getByText(/Visitors chat on/)).toBeTruthy();
548
+ await waitFor(() =>
549
+ expect(screen.getByText(/\$12\.50 available/)).toBeTruthy(),
550
+ );
551
+ expect(getOrCreateBillingAccount).toHaveBeenCalledWith("acme");
552
+ });
553
+
554
+ it("degrades to the who-pays line alone in local mode (no billing fetch)", () => {
555
+ const getOrCreateBillingAccount = vi.fn();
556
+ render(
557
+ <Providers
558
+ client={createMockStigmer({ getOrCreateBillingAccount })}
559
+ mode="local"
560
+ >
561
+ <ShareAgentDialog
562
+ open
563
+ onOpenChange={() => {}}
564
+ agent={makeAgent({ enabled: true })}
565
+ buildShareUrl={buildShareUrl}
566
+ />
567
+ </Providers>,
568
+ );
569
+
570
+ expect(screen.getByText(/Visitors chat on/)).toBeTruthy();
571
+ expect(screen.queryByText(/available/)).toBeNull();
572
+ expect(getOrCreateBillingAccount).not.toHaveBeenCalled();
573
+ });
574
+
575
+ it("degrades silently when the billing fetch fails", async () => {
576
+ const getOrCreateBillingAccount = vi
577
+ .fn()
578
+ .mockRejectedValue(new Error("billing unavailable"));
579
+ render(
580
+ <Providers
581
+ client={createMockStigmer({ getOrCreateBillingAccount })}
582
+ mode="cloud"
583
+ >
584
+ <ShareAgentDialog
585
+ open
586
+ onOpenChange={() => {}}
587
+ agent={makeAgent({ enabled: true })}
588
+ buildShareUrl={buildShareUrl}
589
+ />
590
+ </Providers>,
591
+ );
592
+
593
+ await waitFor(() => expect(getOrCreateBillingAccount).toHaveBeenCalled());
594
+ expect(screen.getByText(/Visitors chat on/)).toBeTruthy();
595
+ expect(screen.queryByText(/available/)).toBeNull();
596
+ expect(screen.queryByText(/billing unavailable/)).toBeNull();
597
+ });
598
+ });
599
+
600
+ describe("Developer tab", () => {
601
+ it("shows the platform client snippet and docs link", () => {
602
+ render(
603
+ <Providers client={createMockStigmer()}>
604
+ <ShareAgentDialog
605
+ open
606
+ onOpenChange={() => {}}
607
+ agent={makeAgent({ enabled: true })}
608
+ buildShareUrl={buildShareUrl}
609
+ />
610
+ </Providers>,
611
+ );
612
+
613
+ fireEvent.click(screen.getByRole("tab", { name: /Developer/, hidden: true }));
614
+ expect(screen.getByText(/createPlatformClientAuth/)).toBeTruthy();
615
+ const link = screen.getByText(/platform client guide/);
616
+ expect(link.getAttribute("href")).toContain("platform-client");
617
+ });
618
+ });
619
+
620
+ it("renders in-flow without showModal when modal is false", () => {
621
+ const showModal = vi.spyOn(HTMLDialogElement.prototype, "showModal");
622
+ render(
623
+ <Providers client={createMockStigmer()}>
624
+ <ShareAgentDialog
625
+ open
626
+ onOpenChange={() => {}}
627
+ agent={makeAgent({ enabled: true })}
628
+ buildShareUrl={buildShareUrl}
629
+ modal={false}
630
+ />
631
+ </Providers>,
632
+ );
633
+
634
+ expect(screen.getByText("Share")).toBeTruthy();
635
+ expect(showModal).not.toHaveBeenCalled();
636
+ showModal.mockRestore();
637
+ });
638
+
639
+ it("requests close via Done and the close affordance", () => {
640
+ const onOpenChange = vi.fn();
641
+ render(
642
+ <Providers client={createMockStigmer()}>
643
+ <ShareAgentDialog
644
+ open
645
+ onOpenChange={onOpenChange}
646
+ agent={makeAgent({ enabled: true })}
647
+ buildShareUrl={buildShareUrl}
648
+ />
649
+ </Providers>,
650
+ );
651
+
652
+ screen.getByText("Done").click();
653
+ expect(onOpenChange).toHaveBeenCalledWith(false);
654
+
655
+ onOpenChange.mockClear();
656
+ screen.getByLabelText("Close").click();
657
+ expect(onOpenChange).toHaveBeenCalledWith(false);
658
+ });
659
+
660
+ describe("Reset link (rotatable share token)", () => {
661
+ it("appends the status token to the shown link and embed snippet", () => {
662
+ render(
663
+ <Providers client={createMockStigmer()}>
664
+ <ShareAgentDialog
665
+ open
666
+ onOpenChange={() => {}}
667
+ agent={makeAgent({ enabled: true }, "tok123")}
668
+ buildShareUrl={buildShareUrl}
669
+ />
670
+ </Providers>,
671
+ );
672
+
673
+ expect(
674
+ screen.getByText("https://app.example.com/chat/acme/support-agent?k=tok123"),
675
+ ).toBeTruthy();
676
+
677
+ fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
678
+ expect(screen.getByText(/token="tok123"/)).toBeTruthy();
679
+ });
680
+
681
+ it("rotates on Reset link, adopts the fresh token, and notifies the host", async () => {
682
+ const rotateShareLink = vi.fn().mockResolvedValue({
683
+ metadata: { id: "agt_1" },
684
+ status: { shareLinkToken: "fresh-token" },
685
+ });
686
+ const onSharingChanged = vi.fn();
687
+ render(
688
+ <Providers client={createMockStigmer({ rotateShareLink })}>
689
+ <ShareAgentDialog
690
+ open
691
+ onOpenChange={() => {}}
692
+ agent={makeAgent({ enabled: true })}
693
+ buildShareUrl={buildShareUrl}
694
+ onSharingChanged={onSharingChanged}
695
+ />
696
+ </Providers>,
697
+ );
698
+
699
+ // A plain link shows no token before the reset.
700
+ expect(
701
+ screen.getByText("https://app.example.com/chat/acme/support-agent"),
702
+ ).toBeTruthy();
703
+
704
+ fireEvent.click(screen.getByRole("button", { name: "Reset link", hidden: true }));
705
+
706
+ await waitFor(() =>
707
+ expect(
708
+ screen.getByText(
709
+ "https://app.example.com/chat/acme/support-agent?k=fresh-token",
710
+ ),
711
+ ).toBeTruthy(),
712
+ );
713
+ expect(rotateShareLink).toHaveBeenCalledTimes(1);
714
+ expect(onSharingChanged).toHaveBeenCalled();
715
+ });
716
+
717
+ it("hides the Reset control and the token for org-members-only shares", () => {
718
+ render(
719
+ <Providers client={createMockStigmer()}>
720
+ <ShareAgentDialog
721
+ open
722
+ onOpenChange={() => {}}
723
+ agent={makeAgent(
724
+ {
725
+ enabled: true,
726
+ audience: AgentSharingAudience.org,
727
+ },
728
+ "tok123",
729
+ )}
730
+ buildShareUrl={buildShareUrl}
731
+ />
732
+ </Providers>,
733
+ );
734
+
735
+ // Org access is gated by membership, not the link token: the member
736
+ // link stays clean and the Reset lever is not offered.
737
+ expect(
738
+ screen.getByText("https://app.example.com/chat/acme/support-agent"),
739
+ ).toBeTruthy();
740
+ expect(
741
+ screen.queryByRole("button", { name: "Reset link", hidden: true }),
742
+ ).toBeNull();
743
+ });
744
+ });
745
+ });