@stigmer/react 3.1.7 → 3.1.9
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/agent/AgentDetailView.d.ts +9 -1
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +15 -4
- package/agent/AgentDetailView.js.map +1 -1
- package/environment/EnvironmentListPanel.d.ts.map +1 -1
- package/environment/EnvironmentListPanel.js +11 -3
- package/environment/EnvironmentListPanel.js.map +1 -1
- package/environment/EnvironmentPicker.d.ts +17 -1
- package/environment/EnvironmentPicker.d.ts.map +1 -1
- package/environment/EnvironmentPicker.js +3 -2
- package/environment/EnvironmentPicker.js.map +1 -1
- package/environment/EnvironmentVariableEditor.d.ts.map +1 -1
- package/environment/EnvironmentVariableEditor.js +9 -3
- package/environment/EnvironmentVariableEditor.js.map +1 -1
- package/environment/index.d.ts +1 -0
- package/environment/index.d.ts.map +1 -1
- package/environment/index.js +1 -0
- package/environment/index.js.map +1 -1
- package/environment/shareRestriction.d.ts +13 -0
- package/environment/shareRestriction.d.ts.map +1 -0
- package/environment/shareRestriction.js +15 -0
- package/environment/shareRestriction.js.map +1 -0
- package/execution/ToolCallItem.d.ts.map +1 -1
- package/execution/ToolCallItem.js +12 -0
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/ToolRunGroup.js +4 -0
- package/execution/ToolRunGroup.js.map +1 -1
- package/execution/WriteBackCard.d.ts +14 -5
- package/execution/WriteBackCard.d.ts.map +1 -1
- package/execution/WriteBackCard.js +103 -32
- package/execution/WriteBackCard.js.map +1 -1
- package/execution/WriteBacksWidget.d.ts +1 -1
- package/execution/WriteBacksWidget.js +2 -2
- package/execution/WriteBacksWidget.js.map +1 -1
- package/execution/index.d.ts +2 -0
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +1 -0
- package/execution/index.js.map +1 -1
- package/execution/write-back-utils.d.ts +57 -0
- package/execution/write-back-utils.d.ts.map +1 -0
- package/execution/write-back-utils.js +81 -0
- package/execution/write-back-utils.js.map +1 -0
- package/index.d.ts +6 -2
- package/index.d.ts.map +1 -1
- package/index.js +6 -1
- package/index.js.map +1 -1
- package/internal/dev/dom-counter.d.ts.map +1 -1
- package/internal/dev/dom-counter.js +2 -2
- package/internal/dev/dom-counter.js.map +1 -1
- package/internal/dev/enabled.d.ts +7 -0
- package/internal/dev/enabled.d.ts.map +1 -0
- package/internal/dev/enabled.js +38 -0
- package/internal/dev/enabled.js.map +1 -0
- package/internal/dev/profiler-wrapper.d.ts +3 -3
- package/internal/dev/profiler-wrapper.d.ts.map +1 -1
- package/internal/dev/profiler-wrapper.js +5 -5
- package/internal/dev/profiler-wrapper.js.map +1 -1
- package/internal/dev/use-key-stability.d.ts.map +1 -1
- package/internal/dev/use-key-stability.js +2 -2
- package/internal/dev/use-key-stability.js.map +1 -1
- package/internal/dev/use-render-tracer.d.ts +2 -2
- package/internal/dev/use-render-tracer.d.ts.map +1 -1
- package/internal/dev/use-render-tracer.js +4 -4
- package/internal/dev/use-render-tracer.js.map +1 -1
- package/internal/dev/use-stream-rate.d.ts +2 -1
- package/internal/dev/use-stream-rate.d.ts.map +1 -1
- package/internal/dev/use-stream-rate.js +4 -3
- package/internal/dev/use-stream-rate.js.map +1 -1
- package/library/ResourceVisibilityControl.d.ts +3 -0
- package/library/ResourceVisibilityControl.d.ts.map +1 -1
- package/library/ResourceVisibilityControl.js +22 -8
- package/library/ResourceVisibilityControl.js.map +1 -1
- package/library/useUpdateVisibility.d.ts +5 -3
- package/library/useUpdateVisibility.d.ts.map +1 -1
- package/library/useUpdateVisibility.js +7 -2
- package/library/useUpdateVisibility.js.map +1 -1
- package/library/visibilityLevels.d.ts +17 -0
- package/library/visibilityLevels.d.ts.map +1 -1
- package/library/visibilityLevels.js +32 -0
- package/library/visibilityLevels.js.map +1 -1
- package/package.json +4 -4
- package/session/NewSessionViewer.d.ts +11 -1
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +28 -8
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +9 -2
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +26 -12
- package/session/SessionViewer.js.map +1 -1
- package/session/audience.d.ts +12 -4
- package/session/audience.d.ts.map +1 -1
- package/session/facets/ChangesTab.d.ts.map +1 -1
- package/session/facets/ChangesTab.js +3 -1
- package/session/facets/ChangesTab.js.map +1 -1
- package/session/useNewSessionFlow.d.ts +12 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +13 -1
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +13 -0
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +7 -2
- package/session/useSessionPageFlow.js.map +1 -1
- package/sharing/ShareAgentDialog.d.ts +57 -0
- package/sharing/ShareAgentDialog.d.ts.map +1 -0
- package/sharing/ShareAgentDialog.js +478 -0
- package/sharing/ShareAgentDialog.js.map +1 -0
- package/sharing/SharedAgentChat.d.ts +84 -0
- package/sharing/SharedAgentChat.d.ts.map +1 -0
- package/sharing/SharedAgentChat.js +80 -0
- package/sharing/SharedAgentChat.js.map +1 -0
- package/sharing/index.d.ts +18 -0
- package/sharing/index.d.ts.map +1 -0
- package/sharing/index.js +12 -0
- package/sharing/index.js.map +1 -0
- package/sharing/useAgentShare.d.ts +43 -0
- package/sharing/useAgentShare.d.ts.map +1 -0
- package/sharing/useAgentShare.js +54 -0
- package/sharing/useAgentShare.js.map +1 -0
- package/sharing/useRotateShareLink.d.ts +45 -0
- package/sharing/useRotateShareLink.d.ts.map +1 -0
- package/sharing/useRotateShareLink.js +58 -0
- package/sharing/useRotateShareLink.js.map +1 -0
- package/sharing/useSaveAgentShare.d.ts +105 -0
- package/sharing/useSaveAgentShare.d.ts.map +1 -0
- package/sharing/useSaveAgentShare.js +97 -0
- package/sharing/useSaveAgentShare.js.map +1 -0
- package/sharing/useShareAgent.d.ts +69 -0
- package/sharing/useShareAgent.d.ts.map +1 -0
- package/sharing/useShareAgent.js +42 -0
- package/sharing/useShareAgent.js.map +1 -0
- package/sharing/useShareToolReadiness.d.ts +52 -0
- package/sharing/useShareToolReadiness.d.ts.map +1 -0
- package/sharing/useShareToolReadiness.js +89 -0
- package/sharing/useShareToolReadiness.js.map +1 -0
- package/sharing/useSharedAgentProfile.d.ts +85 -0
- package/sharing/useSharedAgentProfile.d.ts.map +1 -0
- package/sharing/useSharedAgentProfile.js +73 -0
- package/sharing/useSharedAgentProfile.js.map +1 -0
- package/src/agent/AgentDetailView.tsx +28 -3
- package/src/environment/EnvironmentListPanel.tsx +47 -18
- package/src/environment/EnvironmentPicker.tsx +23 -2
- package/src/environment/EnvironmentVariableEditor.tsx +27 -17
- package/src/environment/__tests__/shareRestriction.test.ts +35 -0
- package/src/environment/index.ts +5 -0
- package/src/environment/shareRestriction.ts +22 -0
- package/src/execution/ToolCallItem.tsx +19 -1
- package/src/execution/ToolRunGroup.tsx +4 -0
- package/src/execution/WriteBackCard.tsx +285 -88
- package/src/execution/WriteBacksWidget.tsx +5 -5
- package/src/execution/__tests__/ToolCallItem.test.tsx +51 -0
- package/src/execution/__tests__/ToolRunGroup.test.tsx +22 -0
- package/src/execution/__tests__/WriteBackCard.test.tsx +64 -10
- package/src/execution/__tests__/write-back-utils.test.ts +135 -0
- package/src/execution/index.ts +7 -0
- package/src/execution/write-back-utils.ts +107 -0
- package/src/index.ts +38 -0
- package/src/internal/dev/__tests__/use-key-stability.test.ts +4 -0
- package/src/internal/dev/__tests__/use-render-tracer.test.ts +4 -0
- package/src/internal/dev/dom-counter.ts +2 -3
- package/src/internal/dev/enabled.ts +39 -0
- package/src/internal/dev/profiler-wrapper.tsx +5 -6
- package/src/internal/dev/use-key-stability.ts +2 -3
- package/src/internal/dev/use-render-tracer.ts +4 -5
- package/src/internal/dev/use-stream-rate.ts +4 -4
- package/src/library/ResourceVisibilityControl.tsx +23 -7
- package/src/library/__tests__/visibilityLevels.test.ts +33 -0
- package/src/library/useUpdateVisibility.ts +9 -3
- package/src/library/visibilityLevels.ts +36 -0
- package/src/session/NewSessionViewer.tsx +67 -31
- package/src/session/SessionViewer.tsx +67 -36
- package/src/session/__tests__/audienceWiring.test.tsx +99 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +75 -1
- package/src/session/__tests__/useSessionPageFlow.guest.test.tsx +112 -0
- package/src/session/audience.ts +12 -4
- package/src/session/facets/ChangesTab.tsx +6 -4
- package/src/session/useNewSessionFlow.ts +26 -1
- package/src/session/useSessionPageFlow.ts +24 -2
- package/src/sharing/ShareAgentDialog.tsx +1489 -0
- package/src/sharing/SharedAgentChat.tsx +283 -0
- package/src/sharing/__tests__/ShareAgentDialog.test.tsx +926 -0
- package/src/sharing/__tests__/SharedAgentChat.test.tsx +222 -0
- package/src/sharing/__tests__/useAgentShare.test.tsx +149 -0
- package/src/sharing/__tests__/useRotateShareLink.test.tsx +96 -0
- package/src/sharing/__tests__/useSaveAgentShare.test.tsx +241 -0
- package/src/sharing/__tests__/useShareAgent.test.tsx +131 -0
- package/src/sharing/__tests__/useShareToolReadiness.test.tsx +219 -0
- package/src/sharing/__tests__/useSharedAgentProfile.test.tsx +201 -0
- package/src/sharing/index.ts +32 -0
- package/src/sharing/useAgentShare.ts +91 -0
- package/src/sharing/useRotateShareLink.ts +81 -0
- package/src/sharing/useSaveAgentShare.ts +186 -0
- package/src/sharing/useShareAgent.tsx +107 -0
- package/src/sharing/useShareToolReadiness.ts +131 -0
- package/src/sharing/useSharedAgentProfile.ts +132 -0
- package/src/switch/Switch.tsx +97 -0
- package/src/switch/__tests__/Switch.test.tsx +107 -0
- package/src/switch/index.ts +2 -0
- package/styles.css +1 -1
- package/switch/Switch.d.ts +47 -0
- package/switch/Switch.d.ts.map +1 -0
- package/switch/Switch.js +41 -0
- package/switch/Switch.js.map +1 -0
- package/switch/index.d.ts +3 -0
- package/switch/index.d.ts.map +1 -0
- package/switch/index.js +2 -0
- package/switch/index.js.map +1 -0
|
@@ -0,0 +1,926 @@
|
|
|
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 { AgentShareAudience } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/spec_pb";
|
|
5
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
6
|
+
import type { AgentShareInput } from "@stigmer/sdk";
|
|
7
|
+
import { StigmerContext } from "../../context";
|
|
8
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
9
|
+
import { DeploymentModeContext } from "../../deployment-mode";
|
|
10
|
+
import { ShareAgentDialog } from "../ShareAgentDialog";
|
|
11
|
+
|
|
12
|
+
// Toasts are visual feedback owned by the feedback module; keep them inert.
|
|
13
|
+
vi.mock("../../feedback/toast", () => ({
|
|
14
|
+
toast: { success: vi.fn(), error: vi.fn() },
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// happy-dom does not implement the native dialog show/close methods.
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
HTMLDialogElement.prototype.showModal = function showModal() {
|
|
20
|
+
this.open = true;
|
|
21
|
+
};
|
|
22
|
+
HTMLDialogElement.prototype.close = function close() {
|
|
23
|
+
this.open = false;
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(cleanup);
|
|
28
|
+
|
|
29
|
+
interface MockOverrides {
|
|
30
|
+
getByAgent?: (input: unknown) => Promise<unknown>;
|
|
31
|
+
apply?: (input: AgentShareInput) => Promise<unknown>;
|
|
32
|
+
rotateShareLink?: (input: unknown) => Promise<unknown>;
|
|
33
|
+
getOrCreateBillingAccount?: (orgId: string) => Promise<unknown>;
|
|
34
|
+
environments?: unknown[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createMockStigmer(overrides: MockOverrides = {}) {
|
|
38
|
+
return {
|
|
39
|
+
agentShare: {
|
|
40
|
+
getByAgent:
|
|
41
|
+
overrides.getByAgent ??
|
|
42
|
+
vi.fn().mockResolvedValue({ totalCount: 0, items: [] }),
|
|
43
|
+
apply: overrides.apply ?? vi.fn().mockResolvedValue({}),
|
|
44
|
+
rotateShareLink:
|
|
45
|
+
overrides.rotateShareLink ?? vi.fn().mockResolvedValue({}),
|
|
46
|
+
},
|
|
47
|
+
environment: {
|
|
48
|
+
list: vi.fn().mockResolvedValue({
|
|
49
|
+
items: overrides.environments ?? [],
|
|
50
|
+
totalCount: overrides.environments?.length ?? 0,
|
|
51
|
+
}),
|
|
52
|
+
// The readiness hook checks bound refs' visibility; the picker's
|
|
53
|
+
// list above is the source of the fixtures, so resolve from it.
|
|
54
|
+
getByReference: vi
|
|
55
|
+
.fn()
|
|
56
|
+
.mockImplementation(({ slug }: { org: string; slug: string }) => {
|
|
57
|
+
const env = (overrides.environments ?? []).find(
|
|
58
|
+
(e) => (e as { metadata?: { slug?: string } }).metadata?.slug === slug,
|
|
59
|
+
);
|
|
60
|
+
return env
|
|
61
|
+
? Promise.resolve(env)
|
|
62
|
+
: Promise.reject(new Error("not found"));
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
billing: {
|
|
66
|
+
getOrCreateBillingAccount:
|
|
67
|
+
overrides.getOrCreateBillingAccount ??
|
|
68
|
+
vi.fn().mockResolvedValue(null),
|
|
69
|
+
},
|
|
70
|
+
} as never;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function Providers({
|
|
74
|
+
client,
|
|
75
|
+
mode = "cloud",
|
|
76
|
+
children,
|
|
77
|
+
}: {
|
|
78
|
+
client: unknown;
|
|
79
|
+
mode?: "cloud" | "local";
|
|
80
|
+
children: ReactNode;
|
|
81
|
+
}) {
|
|
82
|
+
return (
|
|
83
|
+
<FetchCacheContext.Provider value={null}>
|
|
84
|
+
<DeploymentModeContext.Provider value={mode}>
|
|
85
|
+
<StigmerContext.Provider value={client as never}>
|
|
86
|
+
{children}
|
|
87
|
+
</StigmerContext.Provider>
|
|
88
|
+
</DeploymentModeContext.Provider>
|
|
89
|
+
</FetchCacheContext.Provider>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function makeAgent(overrides?: { mcpUsages?: boolean }) {
|
|
94
|
+
return {
|
|
95
|
+
metadata: {
|
|
96
|
+
id: "agt_1",
|
|
97
|
+
org: "acme",
|
|
98
|
+
slug: "support-agent",
|
|
99
|
+
name: "Support Agent",
|
|
100
|
+
},
|
|
101
|
+
spec: {
|
|
102
|
+
mcpServerUsages: overrides?.mcpUsages
|
|
103
|
+
? [{ mcpServerRef: { org: "acme", slug: "github" } }]
|
|
104
|
+
: [],
|
|
105
|
+
},
|
|
106
|
+
} as never;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function makeShare(
|
|
110
|
+
spec?: {
|
|
111
|
+
enabled?: boolean;
|
|
112
|
+
audience?: AgentShareAudience;
|
|
113
|
+
allowedOrigins?: string[];
|
|
114
|
+
messages?: {
|
|
115
|
+
rateLimited?: string;
|
|
116
|
+
unavailable?: string;
|
|
117
|
+
conversationEnded?: string;
|
|
118
|
+
};
|
|
119
|
+
environmentRefs?: { org: string; slug: string }[];
|
|
120
|
+
},
|
|
121
|
+
shareLinkToken?: string,
|
|
122
|
+
) {
|
|
123
|
+
return {
|
|
124
|
+
metadata: {
|
|
125
|
+
id: "ash_1",
|
|
126
|
+
org: "acme",
|
|
127
|
+
slug: "support-agent",
|
|
128
|
+
name: "Support Agent",
|
|
129
|
+
},
|
|
130
|
+
spec: { agentRef: { org: "acme", slug: "support-agent" }, ...spec },
|
|
131
|
+
...(shareLinkToken !== undefined ? { status: { shareLinkToken } } : {}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function withShare(share: unknown) {
|
|
136
|
+
return vi.fn().mockResolvedValue({ totalCount: 1, items: [share] });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function orgSharedEnv(slug: string, name?: string) {
|
|
140
|
+
return {
|
|
141
|
+
metadata: {
|
|
142
|
+
slug,
|
|
143
|
+
name: name ?? slug,
|
|
144
|
+
visibility: ApiResourceVisibility.visibility_org,
|
|
145
|
+
},
|
|
146
|
+
spec: { description: "" },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const buildShareUrl = (org: string, slug: string) =>
|
|
151
|
+
`https://app.example.com/chat/${org}/${slug}`;
|
|
152
|
+
|
|
153
|
+
/** Render the open dialog and wait for the share load to settle. */
|
|
154
|
+
async function renderOpenDialog(
|
|
155
|
+
client: unknown,
|
|
156
|
+
props?: Partial<Parameters<typeof ShareAgentDialog>[0]> & { mode?: "cloud" | "local" },
|
|
157
|
+
) {
|
|
158
|
+
const { mode, ...dialogProps } = props ?? {};
|
|
159
|
+
render(
|
|
160
|
+
<Providers client={client} mode={mode}>
|
|
161
|
+
<ShareAgentDialog
|
|
162
|
+
open
|
|
163
|
+
onOpenChange={() => {}}
|
|
164
|
+
agent={makeAgent()}
|
|
165
|
+
buildShareUrl={buildShareUrl}
|
|
166
|
+
{...dialogProps}
|
|
167
|
+
/>
|
|
168
|
+
</Providers>,
|
|
169
|
+
);
|
|
170
|
+
await waitFor(() =>
|
|
171
|
+
expect(screen.queryByLabelText("Loading sharing settings")).toBeNull(),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
describe("ShareAgentDialog", () => {
|
|
176
|
+
it("mounts no body while closed (share and billing fetches stay lazy)", () => {
|
|
177
|
+
const getByAgent = vi.fn();
|
|
178
|
+
const getOrCreateBillingAccount = vi.fn();
|
|
179
|
+
render(
|
|
180
|
+
<Providers
|
|
181
|
+
client={createMockStigmer({ getByAgent, getOrCreateBillingAccount })}
|
|
182
|
+
>
|
|
183
|
+
<ShareAgentDialog
|
|
184
|
+
open={false}
|
|
185
|
+
onOpenChange={() => {}}
|
|
186
|
+
agent={makeAgent()}
|
|
187
|
+
buildShareUrl={buildShareUrl}
|
|
188
|
+
/>
|
|
189
|
+
</Providers>,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect(screen.queryByText("Share")).toBeNull();
|
|
193
|
+
expect(getByAgent).not.toHaveBeenCalled();
|
|
194
|
+
expect(getOrCreateBillingAccount).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("shows a loading state while the share resolves, then the form", async () => {
|
|
198
|
+
let resolveLoad: (v: unknown) => void = () => {};
|
|
199
|
+
const getByAgent = vi.fn().mockReturnValue(
|
|
200
|
+
new Promise((resolve) => {
|
|
201
|
+
resolveLoad = resolve;
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
render(
|
|
205
|
+
<Providers client={createMockStigmer({ getByAgent })}>
|
|
206
|
+
<ShareAgentDialog
|
|
207
|
+
open
|
|
208
|
+
onOpenChange={() => {}}
|
|
209
|
+
agent={makeAgent()}
|
|
210
|
+
buildShareUrl={buildShareUrl}
|
|
211
|
+
/>
|
|
212
|
+
</Providers>,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
expect(screen.getByLabelText("Loading sharing settings")).toBeTruthy();
|
|
216
|
+
resolveLoad({ totalCount: 1, items: [makeShare({ enabled: true })] });
|
|
217
|
+
await waitFor(() =>
|
|
218
|
+
expect(
|
|
219
|
+
screen.getByText("https://app.example.com/chat/acme/support-agent"),
|
|
220
|
+
).toBeTruthy(),
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("offers retry when the share load fails", async () => {
|
|
225
|
+
const share = makeShare({ enabled: true });
|
|
226
|
+
const getByAgent = vi
|
|
227
|
+
.fn()
|
|
228
|
+
.mockRejectedValueOnce(new Error("backend unavailable"))
|
|
229
|
+
.mockResolvedValueOnce({ totalCount: 1, items: [share] });
|
|
230
|
+
render(
|
|
231
|
+
<Providers client={createMockStigmer({ getByAgent })}>
|
|
232
|
+
<ShareAgentDialog
|
|
233
|
+
open
|
|
234
|
+
onOpenChange={() => {}}
|
|
235
|
+
agent={makeAgent()}
|
|
236
|
+
buildShareUrl={buildShareUrl}
|
|
237
|
+
/>
|
|
238
|
+
</Providers>,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// A failed load must never render a form that would create a share
|
|
242
|
+
// over an existing one the dialog couldn't see.
|
|
243
|
+
expect(
|
|
244
|
+
await screen.findByText(/couldn't load this agent's sharing settings/i),
|
|
245
|
+
).toBeTruthy();
|
|
246
|
+
|
|
247
|
+
fireEvent.click(screen.getByRole("button", { name: "Try again", hidden: true }));
|
|
248
|
+
await waitFor(() =>
|
|
249
|
+
expect(
|
|
250
|
+
screen.getByText("https://app.example.com/chat/acme/support-agent"),
|
|
251
|
+
).toBeTruthy(),
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("renders header, toggle, and the share link from buildShareUrl", async () => {
|
|
256
|
+
const client = createMockStigmer({
|
|
257
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
258
|
+
});
|
|
259
|
+
await renderOpenDialog(client);
|
|
260
|
+
|
|
261
|
+
expect(screen.getByText("Share")).toBeTruthy();
|
|
262
|
+
expect(screen.getByText("Support Agent")).toBeTruthy();
|
|
263
|
+
expect(screen.getByRole("switch", { hidden: true }).getAttribute("aria-checked")).toBe("true");
|
|
264
|
+
expect(
|
|
265
|
+
screen.getByText("https://app.example.com/chat/acme/support-agent"),
|
|
266
|
+
).toBeTruthy();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("builds the link from the SHARE's slug when it differs from the agent's", async () => {
|
|
270
|
+
// A manifest-renamed share: the hosted URL lives at the share's slug.
|
|
271
|
+
const renamed = {
|
|
272
|
+
...makeShare({ enabled: true }),
|
|
273
|
+
metadata: { id: "ash_1", org: "acme", slug: "help-desk", name: "Help Desk" },
|
|
274
|
+
};
|
|
275
|
+
const client = createMockStigmer({ getByAgent: withShare(renamed) });
|
|
276
|
+
await renderOpenDialog(client);
|
|
277
|
+
|
|
278
|
+
expect(
|
|
279
|
+
screen.getByText("https://app.example.com/chat/acme/help-desk"),
|
|
280
|
+
).toBeTruthy();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("falls back to the relative /chat path when buildShareUrl is omitted", async () => {
|
|
284
|
+
const client = createMockStigmer({
|
|
285
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
286
|
+
});
|
|
287
|
+
await renderOpenDialog(client, { buildShareUrl: undefined });
|
|
288
|
+
|
|
289
|
+
expect(screen.getByText("/chat/acme/support-agent")).toBeTruthy();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("never-shared agent (no share yet)", () => {
|
|
293
|
+
it("renders the off state with disabled copy affordances", async () => {
|
|
294
|
+
const client = createMockStigmer();
|
|
295
|
+
await renderOpenDialog(client);
|
|
296
|
+
|
|
297
|
+
expect(
|
|
298
|
+
screen.getByRole("switch", { hidden: true }).getAttribute("aria-checked"),
|
|
299
|
+
).toBe("false");
|
|
300
|
+
const copy = screen.getByRole("button", {
|
|
301
|
+
name: "Copy",
|
|
302
|
+
hidden: true,
|
|
303
|
+
}) as HTMLButtonElement;
|
|
304
|
+
expect(copy.disabled).toBe(true);
|
|
305
|
+
expect(
|
|
306
|
+
screen.getByText(/sharing is off, so this link doesn't work yet/i),
|
|
307
|
+
).toBeTruthy();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("first enable creates the canonical share via apply", async () => {
|
|
311
|
+
const apply = vi.fn().mockResolvedValue(makeShare({ enabled: true }));
|
|
312
|
+
const client = createMockStigmer({ apply });
|
|
313
|
+
await renderOpenDialog(client);
|
|
314
|
+
|
|
315
|
+
fireEvent.click(screen.getByRole("switch", { hidden: true }));
|
|
316
|
+
|
|
317
|
+
await waitFor(() => expect(apply).toHaveBeenCalledTimes(1));
|
|
318
|
+
const input = apply.mock.calls[0][0] as AgentShareInput;
|
|
319
|
+
// Create identity: the agent's own org/slug/name (the server's D2
|
|
320
|
+
// default made explicit) plus the agent reference.
|
|
321
|
+
expect(input.org).toBe("acme");
|
|
322
|
+
expect(input.slug).toBe("support-agent");
|
|
323
|
+
expect(input.agentRef).toEqual({ org: "acme", slug: "support-agent" });
|
|
324
|
+
expect(input.enabled).toBe(true);
|
|
325
|
+
|
|
326
|
+
await waitFor(() =>
|
|
327
|
+
expect(
|
|
328
|
+
screen.getByRole("switch", { hidden: true }).getAttribute("aria-checked"),
|
|
329
|
+
).toBe("true"),
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("Sharing-off state", () => {
|
|
335
|
+
it("shows the reason on the Embed tab too", async () => {
|
|
336
|
+
const client = createMockStigmer({
|
|
337
|
+
getByAgent: withShare(makeShare({ enabled: false })),
|
|
338
|
+
});
|
|
339
|
+
await renderOpenDialog(client);
|
|
340
|
+
|
|
341
|
+
fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
|
|
342
|
+
expect(
|
|
343
|
+
screen.getByText(/sharing is off, so this embed doesn't work yet/i),
|
|
344
|
+
).toBeTruthy();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("drops the hint and enables copy once sharing is on", async () => {
|
|
348
|
+
const client = createMockStigmer({
|
|
349
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
350
|
+
});
|
|
351
|
+
await renderOpenDialog(client);
|
|
352
|
+
|
|
353
|
+
const copy = screen.getByRole("button", {
|
|
354
|
+
name: "Copy",
|
|
355
|
+
hidden: true,
|
|
356
|
+
}) as HTMLButtonElement;
|
|
357
|
+
expect(copy.disabled).toBe(false);
|
|
358
|
+
expect(screen.queryByText(/sharing is off/i)).toBeNull();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("shows the indexability warning", async () => {
|
|
363
|
+
const client = createMockStigmer({
|
|
364
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
365
|
+
});
|
|
366
|
+
await renderOpenDialog(client);
|
|
367
|
+
|
|
368
|
+
expect(
|
|
369
|
+
screen.getByText(/forwarded and indexed by search engines/),
|
|
370
|
+
).toBeTruthy();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe("Audience", () => {
|
|
374
|
+
it("defaults to Public link and switching to Org members applies the full spec", async () => {
|
|
375
|
+
const apply = vi.fn().mockResolvedValue({});
|
|
376
|
+
const client = createMockStigmer({
|
|
377
|
+
apply,
|
|
378
|
+
getByAgent: withShare(
|
|
379
|
+
makeShare({
|
|
380
|
+
enabled: true,
|
|
381
|
+
allowedOrigins: ["https://example.com"],
|
|
382
|
+
messages: { rateLimited: "Easy there." },
|
|
383
|
+
}),
|
|
384
|
+
),
|
|
385
|
+
});
|
|
386
|
+
await renderOpenDialog(client);
|
|
387
|
+
|
|
388
|
+
const publicOption = screen.getByRole("radio", {
|
|
389
|
+
name: "Public link",
|
|
390
|
+
hidden: true,
|
|
391
|
+
});
|
|
392
|
+
expect(publicOption.getAttribute("aria-checked")).toBe("true");
|
|
393
|
+
|
|
394
|
+
fireEvent.click(
|
|
395
|
+
screen.getByRole("radio", { name: "Org members", hidden: true }),
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
await waitFor(() => expect(apply).toHaveBeenCalledTimes(1));
|
|
399
|
+
const input = apply.mock.calls[0][0] as AgentShareInput;
|
|
400
|
+
expect(input.audience).toBe(AgentShareAudience.org);
|
|
401
|
+
// Whole-spec replace: the audience switch must not clobber the rest.
|
|
402
|
+
expect(input.enabled).toBe(true);
|
|
403
|
+
expect(input.allowedOrigins).toEqual(["https://example.com"]);
|
|
404
|
+
expect(input.messages?.rateLimited).toBe("Easy there.");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("switching to Org members drops credential bindings (public-audience only)", async () => {
|
|
408
|
+
const apply = vi.fn().mockResolvedValue({});
|
|
409
|
+
const client = createMockStigmer({
|
|
410
|
+
apply,
|
|
411
|
+
getByAgent: withShare(
|
|
412
|
+
makeShare({
|
|
413
|
+
enabled: true,
|
|
414
|
+
environmentRefs: [{ org: "acme", slug: "github-creds" }],
|
|
415
|
+
}),
|
|
416
|
+
),
|
|
417
|
+
environments: [orgSharedEnv("github-creds")],
|
|
418
|
+
});
|
|
419
|
+
await renderOpenDialog(client);
|
|
420
|
+
|
|
421
|
+
fireEvent.click(
|
|
422
|
+
screen.getByRole("radio", { name: "Org members", hidden: true }),
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// The proto CEL rule rejects environment_refs on org-audience
|
|
426
|
+
// shares — carrying them would fail the whole apply.
|
|
427
|
+
await waitFor(() => expect(apply).toHaveBeenCalledTimes(1));
|
|
428
|
+
const input = apply.mock.calls[0][0] as AgentShareInput;
|
|
429
|
+
expect(input.audience).toBe(AgentShareAudience.org);
|
|
430
|
+
expect(input.environmentRefs).toEqual([]);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("renders org-audience copy: member link, revocation note, no indexability warning", async () => {
|
|
434
|
+
const client = createMockStigmer({
|
|
435
|
+
getByAgent: withShare(
|
|
436
|
+
makeShare({ enabled: true, audience: AgentShareAudience.org }),
|
|
437
|
+
),
|
|
438
|
+
});
|
|
439
|
+
await renderOpenDialog(client);
|
|
440
|
+
|
|
441
|
+
expect(screen.getByText("Organization members can chat")).toBeTruthy();
|
|
442
|
+
expect(screen.getByText("Member chat link")).toBeTruthy();
|
|
443
|
+
expect(
|
|
444
|
+
screen.getByText(/access is checked on every message/i),
|
|
445
|
+
).toBeTruthy();
|
|
446
|
+
expect(
|
|
447
|
+
screen.queryByText(/forwarded and indexed by search engines/),
|
|
448
|
+
).toBeNull();
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("replaces the Embed tab with a public-only explanation for the org audience", async () => {
|
|
452
|
+
const client = createMockStigmer({
|
|
453
|
+
getByAgent: withShare(
|
|
454
|
+
makeShare({ enabled: true, audience: AgentShareAudience.org }),
|
|
455
|
+
),
|
|
456
|
+
});
|
|
457
|
+
await renderOpenDialog(client);
|
|
458
|
+
|
|
459
|
+
fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
|
|
460
|
+
expect(
|
|
461
|
+
screen.getByText(/Embedding isn't available for org-members-only sharing/),
|
|
462
|
+
).toBeTruthy();
|
|
463
|
+
expect(screen.queryByText(/<stigmer-agent/)).toBeNull();
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("enabling applies the complete spec and notifies the host", async () => {
|
|
468
|
+
const apply = vi.fn().mockResolvedValue({});
|
|
469
|
+
const onSharingChanged = vi.fn();
|
|
470
|
+
const client = createMockStigmer({
|
|
471
|
+
apply,
|
|
472
|
+
getByAgent: withShare(
|
|
473
|
+
makeShare({
|
|
474
|
+
enabled: false,
|
|
475
|
+
allowedOrigins: ["https://example.com"],
|
|
476
|
+
messages: { rateLimited: "Easy there." },
|
|
477
|
+
}),
|
|
478
|
+
),
|
|
479
|
+
});
|
|
480
|
+
await renderOpenDialog(client, { onSharingChanged });
|
|
481
|
+
|
|
482
|
+
fireEvent.click(screen.getByRole("switch", { hidden: true }));
|
|
483
|
+
|
|
484
|
+
await waitFor(() => expect(apply).toHaveBeenCalledTimes(1));
|
|
485
|
+
const input = apply.mock.calls[0][0] as AgentShareInput;
|
|
486
|
+
expect(input.enabled).toBe(true);
|
|
487
|
+
// Apply replaces the spec wholesale — the toggle must carry the
|
|
488
|
+
// existing origins and messages or it would silently erase them.
|
|
489
|
+
expect(input.allowedOrigins).toEqual(["https://example.com"]);
|
|
490
|
+
expect(input.messages?.rateLimited).toBe("Easy there.");
|
|
491
|
+
await waitFor(() => expect(onSharingChanged).toHaveBeenCalled());
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("adopts the server's returned share state after a commit", async () => {
|
|
495
|
+
// Server echoes the apply but with a normalized origin list.
|
|
496
|
+
const apply = vi.fn().mockResolvedValue(
|
|
497
|
+
makeShare({
|
|
498
|
+
enabled: true,
|
|
499
|
+
allowedOrigins: ["https://normalized.example.com"],
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
502
|
+
const client = createMockStigmer({
|
|
503
|
+
apply,
|
|
504
|
+
getByAgent: withShare(makeShare({ enabled: false })),
|
|
505
|
+
});
|
|
506
|
+
await renderOpenDialog(client);
|
|
507
|
+
|
|
508
|
+
fireEvent.click(screen.getByRole("switch", { hidden: true }));
|
|
509
|
+
await waitFor(() =>
|
|
510
|
+
expect(screen.getByRole("switch", { hidden: true }).getAttribute("aria-checked")).toBe(
|
|
511
|
+
"true",
|
|
512
|
+
),
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
|
|
516
|
+
expect(screen.getByText("https://normalized.example.com")).toBeTruthy();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe("Tool credentials", () => {
|
|
520
|
+
it("warns needs-credentials for a tool-using share without bindings", async () => {
|
|
521
|
+
const client = createMockStigmer({
|
|
522
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
523
|
+
});
|
|
524
|
+
await renderOpenDialog(client, { agent: makeAgent({ mcpUsages: true }) });
|
|
525
|
+
|
|
526
|
+
expect(
|
|
527
|
+
screen.getByText(/no credentials are bound to this share/i),
|
|
528
|
+
).toBeTruthy();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("binds an org-shared environment by applying the appended refs", async () => {
|
|
532
|
+
const apply = vi.fn().mockResolvedValue({});
|
|
533
|
+
const client = createMockStigmer({
|
|
534
|
+
apply,
|
|
535
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
536
|
+
environments: [orgSharedEnv("github-creds", "GitHub Creds")],
|
|
537
|
+
});
|
|
538
|
+
await renderOpenDialog(client, { agent: makeAgent({ mcpUsages: true }) });
|
|
539
|
+
|
|
540
|
+
// The section is expanded by default for tool-using agents.
|
|
541
|
+
const picker = await screen.findByLabelText("Add environment", {
|
|
542
|
+
// The select is visually present inside the collapsible.
|
|
543
|
+
selector: "select",
|
|
544
|
+
});
|
|
545
|
+
fireEvent.change(picker, { target: { value: "github-creds" } });
|
|
546
|
+
|
|
547
|
+
await waitFor(() => expect(apply).toHaveBeenCalledTimes(1));
|
|
548
|
+
const input = apply.mock.calls[0][0] as AgentShareInput;
|
|
549
|
+
expect(input.environmentRefs).toEqual([
|
|
550
|
+
{ org: "acme", slug: "github-creds" },
|
|
551
|
+
]);
|
|
552
|
+
// The binding rides the full spec — nothing else changes.
|
|
553
|
+
expect(input.enabled).toBe(true);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("offers only org-shared environments in the picker", async () => {
|
|
557
|
+
const privateEnv = {
|
|
558
|
+
metadata: {
|
|
559
|
+
slug: "personal-creds",
|
|
560
|
+
name: "Personal Creds",
|
|
561
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
562
|
+
},
|
|
563
|
+
spec: { description: "" },
|
|
564
|
+
};
|
|
565
|
+
const client = createMockStigmer({
|
|
566
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
567
|
+
environments: [orgSharedEnv("github-creds", "GitHub Creds"), privateEnv],
|
|
568
|
+
});
|
|
569
|
+
await renderOpenDialog(client, { agent: makeAgent({ mcpUsages: true }) });
|
|
570
|
+
|
|
571
|
+
await screen.findByLabelText("Add environment", { selector: "select" });
|
|
572
|
+
// Private environments are guest-unusable (the runtime merge skips
|
|
573
|
+
// them) — offering them would bind credentials that never apply.
|
|
574
|
+
expect(screen.getByText("GitHub Creds")).toBeTruthy();
|
|
575
|
+
expect(screen.queryByText("Personal Creds")).toBeNull();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it("hides the section entirely for org-audience shares", async () => {
|
|
579
|
+
const client = createMockStigmer({
|
|
580
|
+
getByAgent: withShare(
|
|
581
|
+
makeShare({ enabled: true, audience: AgentShareAudience.org }),
|
|
582
|
+
),
|
|
583
|
+
});
|
|
584
|
+
await renderOpenDialog(client, { agent: makeAgent({ mcpUsages: true }) });
|
|
585
|
+
|
|
586
|
+
expect(screen.queryByText("Tool credentials")).toBeNull();
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
describe("Embed tab", () => {
|
|
591
|
+
it("shows the one-line script snippet: loader from the app origin + <stigmer-agent>", async () => {
|
|
592
|
+
const client = createMockStigmer({
|
|
593
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
594
|
+
});
|
|
595
|
+
await renderOpenDialog(client);
|
|
596
|
+
|
|
597
|
+
fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
|
|
598
|
+
// The loader must be served from the SAME origin as the share URL —
|
|
599
|
+
// embed.js derives the chat-page origin from its own script URL.
|
|
600
|
+
expect(
|
|
601
|
+
screen.getByText(
|
|
602
|
+
/<script src="https:\/\/app\.example\.com\/embed\.js" async><\/script>/,
|
|
603
|
+
),
|
|
604
|
+
).toBeTruthy();
|
|
605
|
+
expect(
|
|
606
|
+
screen.getByText(
|
|
607
|
+
/<stigmer-agent org="acme" agent="support-agent"><\/stigmer-agent>/,
|
|
608
|
+
),
|
|
609
|
+
).toBeTruthy();
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it("keeps the iframe snippet available as the collapsed no-JavaScript alternative", async () => {
|
|
613
|
+
const client = createMockStigmer({
|
|
614
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
615
|
+
});
|
|
616
|
+
await renderOpenDialog(client);
|
|
617
|
+
|
|
618
|
+
fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
|
|
619
|
+
// Collapsed by default — the script snippet is the primary path.
|
|
620
|
+
expect(screen.queryByText(/<iframe/)).toBeNull();
|
|
621
|
+
|
|
622
|
+
fireEvent.click(
|
|
623
|
+
screen.getByRole("button", {
|
|
624
|
+
name: /No-JavaScript alternative/,
|
|
625
|
+
hidden: true,
|
|
626
|
+
}),
|
|
627
|
+
);
|
|
628
|
+
expect(
|
|
629
|
+
screen.getByText(
|
|
630
|
+
/src="https:\/\/app\.example\.com\/chat\/acme\/support-agent"/,
|
|
631
|
+
),
|
|
632
|
+
).toBeTruthy();
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("rejects an invalid origin without calling the RPC", async () => {
|
|
636
|
+
const apply = vi.fn();
|
|
637
|
+
const client = createMockStigmer({
|
|
638
|
+
apply,
|
|
639
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
640
|
+
});
|
|
641
|
+
await renderOpenDialog(client);
|
|
642
|
+
|
|
643
|
+
fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
|
|
644
|
+
const input = screen.getByLabelText("Add allowed origin");
|
|
645
|
+
fireEvent.change(input, {
|
|
646
|
+
target: { value: "https://example.com/path" },
|
|
647
|
+
});
|
|
648
|
+
fireEvent.click(screen.getByText("Add"));
|
|
649
|
+
|
|
650
|
+
expect(await screen.findByRole("alert", { hidden: true })).toBeTruthy();
|
|
651
|
+
expect(apply).not.toHaveBeenCalled();
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it("adds a valid origin by applying the appended list", async () => {
|
|
655
|
+
const apply = vi.fn().mockResolvedValue({});
|
|
656
|
+
const client = createMockStigmer({
|
|
657
|
+
apply,
|
|
658
|
+
getByAgent: withShare(
|
|
659
|
+
makeShare({
|
|
660
|
+
enabled: true,
|
|
661
|
+
allowedOrigins: ["https://existing.example.com"],
|
|
662
|
+
}),
|
|
663
|
+
),
|
|
664
|
+
});
|
|
665
|
+
await renderOpenDialog(client);
|
|
666
|
+
|
|
667
|
+
fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
|
|
668
|
+
fireEvent.change(screen.getByLabelText("Add allowed origin"), {
|
|
669
|
+
target: { value: "https://new.example.com" },
|
|
670
|
+
});
|
|
671
|
+
fireEvent.click(screen.getByText("Add"));
|
|
672
|
+
|
|
673
|
+
await waitFor(() => expect(apply).toHaveBeenCalledTimes(1));
|
|
674
|
+
const input = apply.mock.calls[0][0] as AgentShareInput;
|
|
675
|
+
expect(input.allowedOrigins).toEqual([
|
|
676
|
+
"https://existing.example.com",
|
|
677
|
+
"https://new.example.com",
|
|
678
|
+
]);
|
|
679
|
+
// Enabled state rides along untouched.
|
|
680
|
+
expect(input.enabled).toBe(true);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it("rejects a duplicate origin", async () => {
|
|
684
|
+
const apply = vi.fn();
|
|
685
|
+
const client = createMockStigmer({
|
|
686
|
+
apply,
|
|
687
|
+
getByAgent: withShare(
|
|
688
|
+
makeShare({
|
|
689
|
+
enabled: true,
|
|
690
|
+
allowedOrigins: ["https://example.com"],
|
|
691
|
+
}),
|
|
692
|
+
),
|
|
693
|
+
});
|
|
694
|
+
await renderOpenDialog(client);
|
|
695
|
+
|
|
696
|
+
fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
|
|
697
|
+
fireEvent.change(screen.getByLabelText("Add allowed origin"), {
|
|
698
|
+
target: { value: "https://example.com" },
|
|
699
|
+
});
|
|
700
|
+
fireEvent.click(screen.getByText("Add"));
|
|
701
|
+
|
|
702
|
+
expect(await screen.findByRole("alert", { hidden: true })).toBeTruthy();
|
|
703
|
+
expect(apply).not.toHaveBeenCalled();
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it("removes an origin by applying the filtered list", async () => {
|
|
707
|
+
const apply = vi.fn().mockResolvedValue({});
|
|
708
|
+
const client = createMockStigmer({
|
|
709
|
+
apply,
|
|
710
|
+
getByAgent: withShare(
|
|
711
|
+
makeShare({
|
|
712
|
+
enabled: true,
|
|
713
|
+
allowedOrigins: ["https://a.example.com", "https://b.example.com"],
|
|
714
|
+
}),
|
|
715
|
+
),
|
|
716
|
+
});
|
|
717
|
+
await renderOpenDialog(client);
|
|
718
|
+
|
|
719
|
+
fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
|
|
720
|
+
fireEvent.click(screen.getByLabelText("Remove https://a.example.com"));
|
|
721
|
+
|
|
722
|
+
await waitFor(() => expect(apply).toHaveBeenCalledTimes(1));
|
|
723
|
+
const input = apply.mock.calls[0][0] as AgentShareInput;
|
|
724
|
+
expect(input.allowedOrigins).toEqual(["https://b.example.com"]);
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
describe("visitor messages", () => {
|
|
729
|
+
it("saves edited messages as part of the complete spec", async () => {
|
|
730
|
+
const apply = vi.fn().mockResolvedValue({});
|
|
731
|
+
const client = createMockStigmer({
|
|
732
|
+
apply,
|
|
733
|
+
getByAgent: withShare(
|
|
734
|
+
makeShare({
|
|
735
|
+
enabled: true,
|
|
736
|
+
allowedOrigins: ["https://example.com"],
|
|
737
|
+
}),
|
|
738
|
+
),
|
|
739
|
+
});
|
|
740
|
+
await renderOpenDialog(client);
|
|
741
|
+
|
|
742
|
+
fireEvent.click(screen.getByText("Customize visitor messages"));
|
|
743
|
+
fireEvent.change(screen.getByLabelText(/Rate limited/), {
|
|
744
|
+
target: { value: "Please slow down." },
|
|
745
|
+
});
|
|
746
|
+
fireEvent.click(screen.getByText("Save messages"));
|
|
747
|
+
|
|
748
|
+
await waitFor(() => expect(apply).toHaveBeenCalledTimes(1));
|
|
749
|
+
const input = apply.mock.calls[0][0] as AgentShareInput;
|
|
750
|
+
expect(input.messages?.rateLimited).toBe("Please slow down.");
|
|
751
|
+
expect(input.enabled).toBe(true);
|
|
752
|
+
expect(input.allowedOrigins).toEqual(["https://example.com"]);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it("caps each message at 300 characters", async () => {
|
|
756
|
+
const client = createMockStigmer({
|
|
757
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
758
|
+
});
|
|
759
|
+
await renderOpenDialog(client);
|
|
760
|
+
|
|
761
|
+
fireEvent.click(screen.getByText("Customize visitor messages"));
|
|
762
|
+
const field = screen.getByLabelText(/Rate limited/) as HTMLTextAreaElement;
|
|
763
|
+
fireEvent.change(field, { target: { value: "x".repeat(400) } });
|
|
764
|
+
expect(field.value.length).toBe(300);
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
describe("who pays", () => {
|
|
769
|
+
it("shows the who-pays line with the balance on cloud", async () => {
|
|
770
|
+
const getOrCreateBillingAccount = vi.fn().mockResolvedValue({
|
|
771
|
+
balance: { availableMicros: BigInt(12_500_000) },
|
|
772
|
+
});
|
|
773
|
+
const client = createMockStigmer({
|
|
774
|
+
getOrCreateBillingAccount,
|
|
775
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
776
|
+
});
|
|
777
|
+
await renderOpenDialog(client, { mode: "cloud" });
|
|
778
|
+
|
|
779
|
+
expect(screen.getByText(/Visitors chat on/)).toBeTruthy();
|
|
780
|
+
await waitFor(() =>
|
|
781
|
+
expect(screen.getByText(/\$12\.50 available/)).toBeTruthy(),
|
|
782
|
+
);
|
|
783
|
+
expect(getOrCreateBillingAccount).toHaveBeenCalledWith("acme");
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it("degrades to the who-pays line alone in local mode (no billing fetch)", async () => {
|
|
787
|
+
const getOrCreateBillingAccount = vi.fn();
|
|
788
|
+
const client = createMockStigmer({
|
|
789
|
+
getOrCreateBillingAccount,
|
|
790
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
791
|
+
});
|
|
792
|
+
await renderOpenDialog(client, { mode: "local" });
|
|
793
|
+
|
|
794
|
+
expect(screen.getByText(/Visitors chat on/)).toBeTruthy();
|
|
795
|
+
expect(screen.queryByText(/available/)).toBeNull();
|
|
796
|
+
expect(getOrCreateBillingAccount).not.toHaveBeenCalled();
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it("degrades silently when the billing fetch fails", async () => {
|
|
800
|
+
const getOrCreateBillingAccount = vi
|
|
801
|
+
.fn()
|
|
802
|
+
.mockRejectedValue(new Error("billing unavailable"));
|
|
803
|
+
const client = createMockStigmer({
|
|
804
|
+
getOrCreateBillingAccount,
|
|
805
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
806
|
+
});
|
|
807
|
+
await renderOpenDialog(client, { mode: "cloud" });
|
|
808
|
+
|
|
809
|
+
await waitFor(() => expect(getOrCreateBillingAccount).toHaveBeenCalled());
|
|
810
|
+
expect(screen.getByText(/Visitors chat on/)).toBeTruthy();
|
|
811
|
+
expect(screen.queryByText(/available/)).toBeNull();
|
|
812
|
+
expect(screen.queryByText(/billing unavailable/)).toBeNull();
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
describe("Developer tab", () => {
|
|
817
|
+
it("shows the platform client snippet and docs link", async () => {
|
|
818
|
+
const client = createMockStigmer({
|
|
819
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
820
|
+
});
|
|
821
|
+
await renderOpenDialog(client);
|
|
822
|
+
|
|
823
|
+
fireEvent.click(screen.getByRole("tab", { name: /Developer/, hidden: true }));
|
|
824
|
+
expect(screen.getByText(/createPlatformClientAuth/)).toBeTruthy();
|
|
825
|
+
const link = screen.getByText(/platform client guide/);
|
|
826
|
+
expect(link.getAttribute("href")).toContain("platform-client");
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it("renders in-flow without showModal when modal is false", async () => {
|
|
831
|
+
const showModal = vi.spyOn(HTMLDialogElement.prototype, "showModal");
|
|
832
|
+
const client = createMockStigmer({
|
|
833
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
834
|
+
});
|
|
835
|
+
await renderOpenDialog(client, { modal: false });
|
|
836
|
+
|
|
837
|
+
expect(screen.getByText("Share")).toBeTruthy();
|
|
838
|
+
expect(showModal).not.toHaveBeenCalled();
|
|
839
|
+
showModal.mockRestore();
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it("requests close via Done and the close affordance", async () => {
|
|
843
|
+
const onOpenChange = vi.fn();
|
|
844
|
+
const client = createMockStigmer({
|
|
845
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
846
|
+
});
|
|
847
|
+
await renderOpenDialog(client, { onOpenChange });
|
|
848
|
+
|
|
849
|
+
screen.getByText("Done").click();
|
|
850
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
851
|
+
|
|
852
|
+
onOpenChange.mockClear();
|
|
853
|
+
screen.getByLabelText("Close").click();
|
|
854
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
describe("Reset link (rotatable share token)", () => {
|
|
858
|
+
it("appends the status token to the shown link and embed snippet", async () => {
|
|
859
|
+
const client = createMockStigmer({
|
|
860
|
+
getByAgent: withShare(makeShare({ enabled: true }, "tok123")),
|
|
861
|
+
});
|
|
862
|
+
await renderOpenDialog(client);
|
|
863
|
+
|
|
864
|
+
expect(
|
|
865
|
+
screen.getByText("https://app.example.com/chat/acme/support-agent?k=tok123"),
|
|
866
|
+
).toBeTruthy();
|
|
867
|
+
|
|
868
|
+
fireEvent.click(screen.getByRole("tab", { name: /Embed/, hidden: true }));
|
|
869
|
+
expect(screen.getByText(/token="tok123"/)).toBeTruthy();
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it("rotates on Reset link, adopts the fresh token, and notifies the host", async () => {
|
|
873
|
+
const rotateShareLink = vi
|
|
874
|
+
.fn()
|
|
875
|
+
.mockResolvedValue(makeShare({ enabled: true }, "fresh-token"));
|
|
876
|
+
const onSharingChanged = vi.fn();
|
|
877
|
+
const client = createMockStigmer({
|
|
878
|
+
rotateShareLink,
|
|
879
|
+
getByAgent: withShare(makeShare({ enabled: true })),
|
|
880
|
+
});
|
|
881
|
+
await renderOpenDialog(client, { onSharingChanged });
|
|
882
|
+
|
|
883
|
+
// A plain link shows no token before the reset.
|
|
884
|
+
expect(
|
|
885
|
+
screen.getByText("https://app.example.com/chat/acme/support-agent"),
|
|
886
|
+
).toBeTruthy();
|
|
887
|
+
|
|
888
|
+
fireEvent.click(screen.getByRole("button", { name: "Reset link", hidden: true }));
|
|
889
|
+
|
|
890
|
+
await waitFor(() =>
|
|
891
|
+
expect(
|
|
892
|
+
screen.getByText(
|
|
893
|
+
"https://app.example.com/chat/acme/support-agent?k=fresh-token",
|
|
894
|
+
),
|
|
895
|
+
).toBeTruthy(),
|
|
896
|
+
);
|
|
897
|
+
expect(rotateShareLink).toHaveBeenCalledTimes(1);
|
|
898
|
+
// The rotation targets the loaded share by its own id.
|
|
899
|
+
expect(
|
|
900
|
+
(rotateShareLink.mock.calls[0][0] as { resourceId: string }).resourceId,
|
|
901
|
+
).toBe("ash_1");
|
|
902
|
+
expect(onSharingChanged).toHaveBeenCalled();
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it("hides the Reset control and the token for org-members-only shares", async () => {
|
|
906
|
+
const client = createMockStigmer({
|
|
907
|
+
getByAgent: withShare(
|
|
908
|
+
makeShare(
|
|
909
|
+
{ enabled: true, audience: AgentShareAudience.org },
|
|
910
|
+
"tok123",
|
|
911
|
+
),
|
|
912
|
+
),
|
|
913
|
+
});
|
|
914
|
+
await renderOpenDialog(client);
|
|
915
|
+
|
|
916
|
+
// Org access is gated by membership, not the link token: the member
|
|
917
|
+
// link stays clean and the Reset lever is not offered.
|
|
918
|
+
expect(
|
|
919
|
+
screen.getByText("https://app.example.com/chat/acme/support-agent"),
|
|
920
|
+
).toBeTruthy();
|
|
921
|
+
expect(
|
|
922
|
+
screen.queryByRole("button", { name: "Reset link", hidden: true }),
|
|
923
|
+
).toBeNull();
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
});
|