@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,222 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup, fireEvent, act } from "@testing-library/react";
|
|
3
|
+
import type { UseSharedAgentProfileReturn } from "../useSharedAgentProfile";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// SharedAgentChat state machine and wiring.
|
|
7
|
+
//
|
|
8
|
+
// The session organisms are replaced with prop-capturing probes — their
|
|
9
|
+
// behavior (including the guest-audience mapping) is covered by their own
|
|
10
|
+
// tests. These tests assert the organism's states (loading / error /
|
|
11
|
+
// unavailable / chat), the guest wiring it hands the viewers, and the
|
|
12
|
+
// launcher→viewer handoff on session creation.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
type CapturedProps = Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
const newSessionViewerProps: CapturedProps[] = [];
|
|
18
|
+
vi.mock("../../session/NewSessionViewer", () => ({
|
|
19
|
+
NewSessionViewer: (props: CapturedProps) => {
|
|
20
|
+
newSessionViewerProps.push(props);
|
|
21
|
+
return <div data-testid="new-session-probe" />;
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const sessionViewerProps: CapturedProps[] = [];
|
|
26
|
+
vi.mock("../../session/SessionViewer", () => ({
|
|
27
|
+
SessionViewer: (props: CapturedProps) => {
|
|
28
|
+
sessionViewerProps.push(props);
|
|
29
|
+
return <div data-testid="session-probe" />;
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const profileState: { current: UseSharedAgentProfileReturn } = {
|
|
34
|
+
current: {
|
|
35
|
+
profile: null,
|
|
36
|
+
isLoading: true,
|
|
37
|
+
isRefetching: false,
|
|
38
|
+
error: null,
|
|
39
|
+
refetch: vi.fn(),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const profileHookCalls: unknown[][] = [];
|
|
43
|
+
vi.mock("../useSharedAgentProfile", () => ({
|
|
44
|
+
useSharedAgentProfile: (...args: unknown[]) => {
|
|
45
|
+
profileHookCalls.push(args);
|
|
46
|
+
return profileState.current;
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
import { SharedAgentChat } from "../SharedAgentChat";
|
|
51
|
+
|
|
52
|
+
const PROFILE = {
|
|
53
|
+
org: "acme",
|
|
54
|
+
slug: "support-agent",
|
|
55
|
+
name: "Support Agent",
|
|
56
|
+
description: "Answers support questions",
|
|
57
|
+
iconUrl: "",
|
|
58
|
+
defaultInstanceId: "inst_1",
|
|
59
|
+
} as never;
|
|
60
|
+
|
|
61
|
+
function setProfileState(state: Partial<UseSharedAgentProfileReturn>) {
|
|
62
|
+
profileState.current = {
|
|
63
|
+
profile: null,
|
|
64
|
+
isLoading: false,
|
|
65
|
+
isRefetching: false,
|
|
66
|
+
error: null,
|
|
67
|
+
refetch: vi.fn(),
|
|
68
|
+
...state,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
newSessionViewerProps.length = 0;
|
|
74
|
+
sessionViewerProps.length = 0;
|
|
75
|
+
profileHookCalls.length = 0;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
cleanup();
|
|
80
|
+
vi.clearAllMocks();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("SharedAgentChat", () => {
|
|
84
|
+
it("renders a loading skeleton while the profile resolves", () => {
|
|
85
|
+
setProfileState({ isLoading: true });
|
|
86
|
+
render(<SharedAgentChat org="acme" slug="support-agent" />);
|
|
87
|
+
|
|
88
|
+
expect(screen.getByLabelText("Loading agent")).toBeTruthy();
|
|
89
|
+
expect(newSessionViewerProps).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("resolves via the public path by default and the member path for sharingAudience=org", () => {
|
|
93
|
+
setProfileState({ profile: PROFILE });
|
|
94
|
+
render(<SharedAgentChat org="acme" slug="support-agent" />);
|
|
95
|
+
expect(profileHookCalls[0]).toEqual([
|
|
96
|
+
"acme",
|
|
97
|
+
"support-agent",
|
|
98
|
+
{ audience: "public" },
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
cleanup();
|
|
102
|
+
profileHookCalls.length = 0;
|
|
103
|
+
|
|
104
|
+
render(
|
|
105
|
+
<SharedAgentChat org="acme" slug="support-agent" sharingAudience="org" />,
|
|
106
|
+
);
|
|
107
|
+
expect(profileHookCalls[0]).toEqual([
|
|
108
|
+
"acme",
|
|
109
|
+
"support-agent",
|
|
110
|
+
{ audience: "org" },
|
|
111
|
+
]);
|
|
112
|
+
// Presentation is identical either way: the session organisms still
|
|
113
|
+
// render with the pure-chat guest audience.
|
|
114
|
+
expect(newSessionViewerProps[0]?.audience).toBe("guest");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("renders an error state with retry on transient failures", () => {
|
|
118
|
+
const refetch = vi.fn();
|
|
119
|
+
setProfileState({ error: new Error("network down"), refetch });
|
|
120
|
+
render(<SharedAgentChat org="acme" slug="support-agent" />);
|
|
121
|
+
|
|
122
|
+
expect(screen.getByText("Something went wrong")).toBeTruthy();
|
|
123
|
+
fireEvent.click(screen.getByRole("button", { name: "Try again" }));
|
|
124
|
+
expect(refetch).toHaveBeenCalledTimes(1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("renders the unavailable state when the agent is not shared", () => {
|
|
128
|
+
setProfileState({ profile: null });
|
|
129
|
+
render(<SharedAgentChat org="acme" slug="support-agent" />);
|
|
130
|
+
|
|
131
|
+
expect(screen.getByText("This agent isn't available")).toBeTruthy();
|
|
132
|
+
expect(newSessionViewerProps).toHaveLength(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("renders the unavailable state when the profile has no default instance", () => {
|
|
136
|
+
setProfileState({
|
|
137
|
+
profile: { ...(PROFILE as object), defaultInstanceId: "" } as never,
|
|
138
|
+
});
|
|
139
|
+
render(<SharedAgentChat org="acme" slug="support-agent" />);
|
|
140
|
+
|
|
141
|
+
expect(screen.getByText("This agent isn't available")).toBeTruthy();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("renders the agent header and a guest launcher pinned to the shared instance", () => {
|
|
145
|
+
setProfileState({ profile: PROFILE });
|
|
146
|
+
render(<SharedAgentChat org="acme" slug="support-agent" />);
|
|
147
|
+
|
|
148
|
+
expect(screen.getByRole("heading", { name: "Support Agent" })).toBeTruthy();
|
|
149
|
+
expect(screen.getByText("Answers support questions")).toBeTruthy();
|
|
150
|
+
|
|
151
|
+
expect(newSessionViewerProps).toHaveLength(1);
|
|
152
|
+
expect(newSessionViewerProps[0]).toMatchObject({
|
|
153
|
+
org: "acme",
|
|
154
|
+
audience: "guest",
|
|
155
|
+
initialAgentRef: { org: "acme", slug: "support-agent" },
|
|
156
|
+
initialInstanceId: "inst_1",
|
|
157
|
+
enableGitHub: false,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("hands off to a guest SessionViewer once the session is created", () => {
|
|
162
|
+
setProfileState({ profile: PROFILE });
|
|
163
|
+
const onSessionCreated = vi.fn();
|
|
164
|
+
render(
|
|
165
|
+
<SharedAgentChat
|
|
166
|
+
org="acme"
|
|
167
|
+
slug="support-agent"
|
|
168
|
+
onSessionCreated={onSessionCreated}
|
|
169
|
+
/>,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const created = newSessionViewerProps[0]
|
|
173
|
+
.onSessionCreated as (id: string) => void;
|
|
174
|
+
act(() => created("ses_1"));
|
|
175
|
+
|
|
176
|
+
expect(onSessionCreated).toHaveBeenCalledWith("ses_1");
|
|
177
|
+
expect(screen.getByTestId("session-probe")).toBeTruthy();
|
|
178
|
+
expect(sessionViewerProps[0]).toMatchObject({
|
|
179
|
+
sessionId: "ses_1",
|
|
180
|
+
org: "acme",
|
|
181
|
+
audience: "guest",
|
|
182
|
+
enableGitHub: false,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("surfaces launcher errors through the composer footer", () => {
|
|
187
|
+
setProfileState({ profile: PROFILE });
|
|
188
|
+
const { rerender } = render(
|
|
189
|
+
<SharedAgentChat org="acme" slug="support-agent" />,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const onError = newSessionViewerProps[0].onError as (msg: string) => void;
|
|
193
|
+
act(() => onError("Rate limit reached"));
|
|
194
|
+
rerender(<SharedAgentChat org="acme" slug="support-agent" />);
|
|
195
|
+
|
|
196
|
+
const footer = newSessionViewerProps.at(-1)!.footerContent;
|
|
197
|
+
expect(footer).toBeTruthy();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("shows the Powered by Stigmer footer by default and hides it on request", () => {
|
|
201
|
+
setProfileState({ profile: PROFILE });
|
|
202
|
+
const { unmount } = render(<SharedAgentChat org="acme" slug="support-agent" />);
|
|
203
|
+
expect(screen.getByText("Powered by Stigmer")).toBeTruthy();
|
|
204
|
+
unmount();
|
|
205
|
+
|
|
206
|
+
render(
|
|
207
|
+
<SharedAgentChat org="acme" slug="support-agent" showPoweredBy={false} />,
|
|
208
|
+
);
|
|
209
|
+
expect(screen.queryByText("Powered by Stigmer")).toBeNull();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("falls back to the slug when the profile has no display name", () => {
|
|
213
|
+
setProfileState({
|
|
214
|
+
profile: { ...(PROFILE as object), name: "" } as never,
|
|
215
|
+
});
|
|
216
|
+
render(<SharedAgentChat org="acme" slug="support-agent" />);
|
|
217
|
+
|
|
218
|
+
expect(
|
|
219
|
+
screen.getByRole("heading", { name: "support-agent" }),
|
|
220
|
+
).toBeTruthy();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { GetAgentSharesByAgentRequest } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/io_pb";
|
|
5
|
+
import { StigmerContext } from "../../context";
|
|
6
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
7
|
+
import { useAgentShare } from "../useAgentShare";
|
|
8
|
+
|
|
9
|
+
function createMockStigmer(overrides: {
|
|
10
|
+
getByAgent?: (input: GetAgentSharesByAgentRequest) => Promise<unknown>;
|
|
11
|
+
} = {}) {
|
|
12
|
+
return {
|
|
13
|
+
agentShare: {
|
|
14
|
+
getByAgent:
|
|
15
|
+
overrides.getByAgent ??
|
|
16
|
+
vi.fn().mockResolvedValue({ totalCount: 0, items: [] }),
|
|
17
|
+
},
|
|
18
|
+
} as never;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function wrapper(client: unknown) {
|
|
22
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
23
|
+
return (
|
|
24
|
+
<FetchCacheContext.Provider value={null}>
|
|
25
|
+
<StigmerContext.Provider value={client as never}>
|
|
26
|
+
{children}
|
|
27
|
+
</StigmerContext.Provider>
|
|
28
|
+
</FetchCacheContext.Provider>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const AGENT = {
|
|
34
|
+
metadata: {
|
|
35
|
+
id: "agt_1",
|
|
36
|
+
org: "acme",
|
|
37
|
+
slug: "support-agent",
|
|
38
|
+
name: "Support Agent",
|
|
39
|
+
},
|
|
40
|
+
} as never;
|
|
41
|
+
|
|
42
|
+
function makeShare(slug: string, id = `ash_${slug}`) {
|
|
43
|
+
return {
|
|
44
|
+
metadata: { id, org: "acme", slug },
|
|
45
|
+
spec: { enabled: true },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("useAgentShare", () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
vi.restoreAllMocks();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("loads the agent's shares by agent id", async () => {
|
|
55
|
+
const share = makeShare("support-agent");
|
|
56
|
+
const getByAgent = vi
|
|
57
|
+
.fn()
|
|
58
|
+
.mockResolvedValue({ totalCount: 1, items: [share] });
|
|
59
|
+
const client = createMockStigmer({ getByAgent });
|
|
60
|
+
|
|
61
|
+
const { result } = renderHook(() => useAgentShare(AGENT), {
|
|
62
|
+
wrapper: wrapper(client),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(result.current.isLoading).toBe(true);
|
|
66
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
67
|
+
|
|
68
|
+
expect(result.current.share).toBe(share);
|
|
69
|
+
expect(result.current.error).toBeNull();
|
|
70
|
+
expect(getByAgent).toHaveBeenCalledWith(
|
|
71
|
+
expect.objectContaining({ agentId: "agt_1" }),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("resolves null (no share yet) when the agent has never been shared", async () => {
|
|
76
|
+
const client = createMockStigmer();
|
|
77
|
+
|
|
78
|
+
const { result } = renderHook(() => useAgentShare(AGENT), {
|
|
79
|
+
wrapper: wrapper(client),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
83
|
+
|
|
84
|
+
// The no-share state is not an error: the first save creates the share.
|
|
85
|
+
expect(result.current.share).toBeNull();
|
|
86
|
+
expect(result.current.error).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("picks the slug-matching share as canonical when several exist", async () => {
|
|
90
|
+
const canonical = makeShare("support-agent");
|
|
91
|
+
const renamed = makeShare("support-help-desk");
|
|
92
|
+
const getByAgent = vi
|
|
93
|
+
.fn()
|
|
94
|
+
.mockResolvedValue({ totalCount: 2, items: [renamed, canonical] });
|
|
95
|
+
const client = createMockStigmer({ getByAgent });
|
|
96
|
+
|
|
97
|
+
const { result } = renderHook(() => useAgentShare(AGENT), {
|
|
98
|
+
wrapper: wrapper(client),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
102
|
+
expect(result.current.share).toBe(canonical);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("falls back to the first share when none matches the agent slug", async () => {
|
|
106
|
+
// A manifest-created share with a custom slug is still manageable —
|
|
107
|
+
// preferring it over "no share" keeps the dialog editing the real row.
|
|
108
|
+
const renamed = makeShare("support-help-desk");
|
|
109
|
+
const getByAgent = vi
|
|
110
|
+
.fn()
|
|
111
|
+
.mockResolvedValue({ totalCount: 1, items: [renamed] });
|
|
112
|
+
const client = createMockStigmer({ getByAgent });
|
|
113
|
+
|
|
114
|
+
const { result } = renderHook(() => useAgentShare(AGENT), {
|
|
115
|
+
wrapper: wrapper(client),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
119
|
+
expect(result.current.share).toBe(renamed);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("skips fetching while the agent is null (stable no-op)", () => {
|
|
123
|
+
const getByAgent = vi.fn();
|
|
124
|
+
const client = createMockStigmer({ getByAgent });
|
|
125
|
+
|
|
126
|
+
const { result } = renderHook(() => useAgentShare(null), {
|
|
127
|
+
wrapper: wrapper(client),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(result.current.isLoading).toBe(false);
|
|
131
|
+
expect(result.current.share).toBeNull();
|
|
132
|
+
expect(getByAgent).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("exposes fetch failures as errors", async () => {
|
|
136
|
+
const getByAgent = vi
|
|
137
|
+
.fn()
|
|
138
|
+
.mockRejectedValue(new Error("backend unavailable"));
|
|
139
|
+
const client = createMockStigmer({ getByAgent });
|
|
140
|
+
|
|
141
|
+
const { result } = renderHook(() => useAgentShare(AGENT), {
|
|
142
|
+
wrapper: wrapper(client),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
146
|
+
expect(result.current.error).toBeTruthy();
|
|
147
|
+
expect(result.current.share).toBeNull();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { renderHook, act } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { RotateShareLinkInput } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/io_pb";
|
|
5
|
+
import { StigmerContext } from "../../context";
|
|
6
|
+
import { useRotateShareLink } from "../useRotateShareLink";
|
|
7
|
+
|
|
8
|
+
function createMockStigmer(overrides: {
|
|
9
|
+
rotateShareLink?: (input: RotateShareLinkInput) => Promise<unknown>;
|
|
10
|
+
} = {}) {
|
|
11
|
+
return {
|
|
12
|
+
agentShare: {
|
|
13
|
+
rotateShareLink:
|
|
14
|
+
overrides.rotateShareLink ?? vi.fn().mockResolvedValue({}),
|
|
15
|
+
},
|
|
16
|
+
} as never;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function wrapper(client: unknown) {
|
|
20
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
21
|
+
return (
|
|
22
|
+
<StigmerContext.Provider value={client as never}>
|
|
23
|
+
{children}
|
|
24
|
+
</StigmerContext.Provider>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("useRotateShareLink", () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("calls rotateShareLink with the share id and returns the updated share", async () => {
|
|
35
|
+
const rotated = {
|
|
36
|
+
metadata: { id: "ash_1" },
|
|
37
|
+
status: { shareLinkToken: "fresh-token" },
|
|
38
|
+
};
|
|
39
|
+
const rotateShareLink = vi.fn().mockResolvedValue(rotated);
|
|
40
|
+
const client = createMockStigmer({ rotateShareLink });
|
|
41
|
+
|
|
42
|
+
const { result } = renderHook(() => useRotateShareLink("ash_1"), {
|
|
43
|
+
wrapper: wrapper(client),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let returned: unknown;
|
|
47
|
+
await act(async () => {
|
|
48
|
+
returned = await result.current.rotateShareLink();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(rotateShareLink).toHaveBeenCalledTimes(1);
|
|
52
|
+
const input = rotateShareLink.mock.calls[0][0] as RotateShareLinkInput;
|
|
53
|
+
expect(input.resourceId).toBe("ash_1");
|
|
54
|
+
// The server generates the token — the request carries nothing but the id.
|
|
55
|
+
expect(Object.keys(input)).not.toContain("linkToken");
|
|
56
|
+
expect(returned).toBe(rotated);
|
|
57
|
+
expect(result.current.error).toBeNull();
|
|
58
|
+
expect(result.current.isPending).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("is a stable no-op while shareId is null (agent never shared)", async () => {
|
|
62
|
+
const rotateShareLink = vi.fn();
|
|
63
|
+
const client = createMockStigmer({ rotateShareLink });
|
|
64
|
+
|
|
65
|
+
const { result } = renderHook(() => useRotateShareLink(null), {
|
|
66
|
+
wrapper: wrapper(client),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
let returned: unknown = "sentinel";
|
|
70
|
+
await act(async () => {
|
|
71
|
+
returned = await result.current.rotateShareLink();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(returned).toBeUndefined();
|
|
75
|
+
expect(rotateShareLink).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("exposes RPC failures via error and rethrows for the caller", async () => {
|
|
79
|
+
const failure = new Error("unauthorized to rotate agent share link");
|
|
80
|
+
const rotateShareLink = vi.fn().mockRejectedValue(failure);
|
|
81
|
+
const client = createMockStigmer({ rotateShareLink });
|
|
82
|
+
|
|
83
|
+
const { result } = renderHook(() => useRotateShareLink("ash_1"), {
|
|
84
|
+
wrapper: wrapper(client),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await act(async () => {
|
|
88
|
+
await expect(result.current.rotateShareLink()).rejects.toThrow(
|
|
89
|
+
"unauthorized to rotate agent share link",
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(result.current.error).toBe(failure);
|
|
94
|
+
expect(result.current.isPending).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|