@stigmer/react 3.1.7 → 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.
- 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/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 +55 -0
- package/sharing/ShareAgentDialog.d.ts.map +1 -0
- package/sharing/ShareAgentDialog.js +393 -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 +16 -0
- package/sharing/index.d.ts.map +1 -0
- package/sharing/index.js +11 -0
- package/sharing/index.js.map +1 -0
- package/sharing/useRotateShareLink.d.ts +42 -0
- package/sharing/useRotateShareLink.d.ts.map +1 -0
- package/sharing/useRotateShareLink.js +55 -0
- package/sharing/useRotateShareLink.js.map +1 -0
- package/sharing/useShareAgent.d.ts +68 -0
- package/sharing/useShareAgent.d.ts.map +1 -0
- package/sharing/useShareAgent.js +41 -0
- package/sharing/useShareAgent.js.map +1 -0
- package/sharing/useShareToolReadiness.d.ts +48 -0
- package/sharing/useShareToolReadiness.d.ts.map +1 -0
- package/sharing/useShareToolReadiness.js +93 -0
- package/sharing/useShareToolReadiness.js.map +1 -0
- package/sharing/useSharedAgentProfile.d.ts +84 -0
- package/sharing/useSharedAgentProfile.d.ts.map +1 -0
- package/sharing/useSharedAgentProfile.js +72 -0
- package/sharing/useSharedAgentProfile.js.map +1 -0
- package/sharing/useUpdateAgentSharing.d.ts +86 -0
- package/sharing/useUpdateAgentSharing.d.ts.map +1 -0
- package/sharing/useUpdateAgentSharing.js +81 -0
- package/sharing/useUpdateAgentSharing.js.map +1 -0
- package/src/agent/AgentDetailView.tsx +28 -3
- package/src/environment/EnvironmentListPanel.tsx +47 -18
- 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 +34 -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 +1255 -0
- package/src/sharing/SharedAgentChat.tsx +283 -0
- package/src/sharing/__tests__/ShareAgentDialog.test.tsx +745 -0
- package/src/sharing/__tests__/SharedAgentChat.test.tsx +222 -0
- package/src/sharing/__tests__/useRotateShareLink.test.tsx +96 -0
- package/src/sharing/__tests__/useShareAgent.test.tsx +127 -0
- package/src/sharing/__tests__/useShareToolReadiness.test.tsx +182 -0
- package/src/sharing/__tests__/useSharedAgentProfile.test.tsx +201 -0
- package/src/sharing/__tests__/useUpdateAgentSharing.test.tsx +200 -0
- package/src/sharing/index.ts +30 -0
- package/src/sharing/useRotateShareLink.ts +76 -0
- package/src/sharing/useShareAgent.tsx +106 -0
- package/src/sharing/useShareToolReadiness.ts +129 -0
- package/src/sharing/useSharedAgentProfile.ts +131 -0
- package/src/sharing/useUpdateAgentSharing.ts +151 -0
- package/src/switch/Switch.tsx +92 -0
- package/src/switch/__tests__/Switch.test.tsx +88 -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 +36 -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,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/agent/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
|
+
agent: {
|
|
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 agent id and returns the updated agent", async () => {
|
|
35
|
+
const rotated = {
|
|
36
|
+
metadata: { id: "agt_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("agt_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("agt_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 agentId is null", 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("agt_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
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
|
2
|
+
import { renderHook, waitFor, cleanup } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { StigmerContext } from "../../context";
|
|
5
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
6
|
+
import { useShareAgent } from "../useShareAgent";
|
|
7
|
+
|
|
8
|
+
// happy-dom does not implement the native dialog show/close methods.
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
HTMLDialogElement.prototype.showModal = function showModal() {
|
|
11
|
+
this.open = true;
|
|
12
|
+
};
|
|
13
|
+
HTMLDialogElement.prototype.close = function close() {
|
|
14
|
+
this.open = false;
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(cleanup);
|
|
19
|
+
|
|
20
|
+
function createMockStigmer(overrides: {
|
|
21
|
+
isAuthorized?: boolean;
|
|
22
|
+
checkMyPermission?: (...args: unknown[]) => Promise<unknown>;
|
|
23
|
+
} = {}) {
|
|
24
|
+
return {
|
|
25
|
+
iamPolicy: {
|
|
26
|
+
checkMyPermission:
|
|
27
|
+
overrides.checkMyPermission ??
|
|
28
|
+
vi.fn().mockResolvedValue({
|
|
29
|
+
isAuthorized: overrides.isAuthorized ?? true,
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
agent: { updateSharing: vi.fn().mockResolvedValue({}) },
|
|
33
|
+
billing: { getOrCreateBillingAccount: vi.fn().mockResolvedValue(null) },
|
|
34
|
+
} as never;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function wrapper(client: unknown) {
|
|
38
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
39
|
+
return (
|
|
40
|
+
<FetchCacheContext.Provider value={null}>
|
|
41
|
+
<StigmerContext.Provider value={client as never}>
|
|
42
|
+
{children}
|
|
43
|
+
</StigmerContext.Provider>
|
|
44
|
+
</FetchCacheContext.Provider>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const AGENT = {
|
|
50
|
+
metadata: {
|
|
51
|
+
id: "agt_1",
|
|
52
|
+
org: "acme",
|
|
53
|
+
slug: "support-agent",
|
|
54
|
+
name: "Support Agent",
|
|
55
|
+
},
|
|
56
|
+
spec: { sharing: undefined },
|
|
57
|
+
} as never;
|
|
58
|
+
|
|
59
|
+
describe("useShareAgent", () => {
|
|
60
|
+
it("returns a Share action in the sharing group when the user can edit", async () => {
|
|
61
|
+
const { result } = renderHook(() => useShareAgent({ agent: AGENT }), {
|
|
62
|
+
wrapper: wrapper(createMockStigmer({ isAuthorized: true })),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await waitFor(() => expect(result.current.action).not.toBeNull());
|
|
66
|
+
expect(result.current.action?.id).toBe("share");
|
|
67
|
+
expect(result.current.action?.label).toBe("Share");
|
|
68
|
+
expect(result.current.action?.group).toBe("sharing");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns a null action while the agent is loading", () => {
|
|
72
|
+
const checkMyPermission = vi.fn();
|
|
73
|
+
const { result } = renderHook(() => useShareAgent({ agent: null }), {
|
|
74
|
+
wrapper: wrapper(createMockStigmer({ checkMyPermission })),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(result.current.action).toBeNull();
|
|
78
|
+
expect(result.current.dialog).toBeNull();
|
|
79
|
+
expect(checkMyPermission).not.toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns a null action when the user lacks can_edit", async () => {
|
|
83
|
+
const client = createMockStigmer({ isAuthorized: false });
|
|
84
|
+
const { result } = renderHook(() => useShareAgent({ agent: AGENT }), {
|
|
85
|
+
wrapper: wrapper(client),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await waitFor(() =>
|
|
89
|
+
expect(
|
|
90
|
+
(client as { iamPolicy: { checkMyPermission: ReturnType<typeof vi.fn> } })
|
|
91
|
+
.iamPolicy.checkMyPermission,
|
|
92
|
+
).toHaveBeenCalled(),
|
|
93
|
+
);
|
|
94
|
+
await waitFor(() => expect(result.current.action).toBeNull());
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("checks the can_edit relation on the agent", async () => {
|
|
98
|
+
const client = createMockStigmer({ isAuthorized: true });
|
|
99
|
+
renderHook(() => useShareAgent({ agent: AGENT }), {
|
|
100
|
+
wrapper: wrapper(client),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const check = (
|
|
104
|
+
client as { iamPolicy: { checkMyPermission: ReturnType<typeof vi.fn> } }
|
|
105
|
+
).iamPolicy.checkMyPermission;
|
|
106
|
+
await waitFor(() => expect(check).toHaveBeenCalled());
|
|
107
|
+
const input = check.mock.calls[0][0] as {
|
|
108
|
+
resource?: { kind: string; id: string };
|
|
109
|
+
relation: string;
|
|
110
|
+
};
|
|
111
|
+
expect(input.relation).toBe("can_edit");
|
|
112
|
+
expect(input.resource?.kind).toBe("agent");
|
|
113
|
+
expect(input.resource?.id).toBe("agt_1");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("opens the dialog via the action", async () => {
|
|
117
|
+
const { result } = renderHook(() => useShareAgent({ agent: AGENT }), {
|
|
118
|
+
wrapper: wrapper(createMockStigmer({ isAuthorized: true })),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await waitFor(() => expect(result.current.action).not.toBeNull());
|
|
122
|
+
expect(result.current.isOpen).toBe(false);
|
|
123
|
+
|
|
124
|
+
result.current.action?.onAction();
|
|
125
|
+
await waitFor(() => expect(result.current.isOpen).toBe(true));
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { create } from "@bufbuild/protobuf";
|
|
5
|
+
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
6
|
+
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
7
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
8
|
+
import { StigmerContext } from "../../context";
|
|
9
|
+
import { DeploymentModeContext } from "../../deployment-mode";
|
|
10
|
+
import { useShareToolReadiness } from "../useShareToolReadiness";
|
|
11
|
+
|
|
12
|
+
function toolAgent(overrides?: {
|
|
13
|
+
mcpUsages?: boolean;
|
|
14
|
+
defaultInstanceId?: string;
|
|
15
|
+
}): Agent {
|
|
16
|
+
return create(AgentSchema, {
|
|
17
|
+
metadata: { id: "agt_1", org: "acme", slug: "helper" },
|
|
18
|
+
spec: {
|
|
19
|
+
instructions: "help",
|
|
20
|
+
mcpServerUsages:
|
|
21
|
+
(overrides?.mcpUsages ?? true)
|
|
22
|
+
? [{ mcpServerRef: { org: "acme", slug: "github" } }]
|
|
23
|
+
: [],
|
|
24
|
+
},
|
|
25
|
+
status: {
|
|
26
|
+
defaultInstanceId: overrides?.defaultInstanceId ?? "agi_1",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mockStigmer(overrides: {
|
|
32
|
+
instanceEnvRefs?: { org: string; slug: string }[];
|
|
33
|
+
envVisibility?: Record<string, ApiResourceVisibility>;
|
|
34
|
+
envError?: boolean;
|
|
35
|
+
}) {
|
|
36
|
+
return {
|
|
37
|
+
agentInstance: {
|
|
38
|
+
get: vi.fn().mockResolvedValue({
|
|
39
|
+
spec: { environmentRefs: overrides.instanceEnvRefs ?? [] },
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
environment: {
|
|
43
|
+
getByReference: vi.fn().mockImplementation(
|
|
44
|
+
({ slug }: { org: string; slug: string }) => {
|
|
45
|
+
if (overrides.envError) {
|
|
46
|
+
return Promise.reject(new Error("not found"));
|
|
47
|
+
}
|
|
48
|
+
return Promise.resolve({
|
|
49
|
+
metadata: {
|
|
50
|
+
visibility:
|
|
51
|
+
overrides.envVisibility?.[slug] ??
|
|
52
|
+
ApiResourceVisibility.visibility_private,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
} as never;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function wrapper(client: unknown, mode: "cloud" | "local" = "cloud") {
|
|
62
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
63
|
+
return (
|
|
64
|
+
<StigmerContext.Provider value={client as never}>
|
|
65
|
+
<DeploymentModeContext.Provider value={mode}>
|
|
66
|
+
{children}
|
|
67
|
+
</DeploymentModeContext.Provider>
|
|
68
|
+
</StigmerContext.Provider>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("useShareToolReadiness", () => {
|
|
74
|
+
it("reports blocked with the private environment refs listed", async () => {
|
|
75
|
+
const client = mockStigmer({
|
|
76
|
+
instanceEnvRefs: [
|
|
77
|
+
{ org: "acme", slug: "private-creds" },
|
|
78
|
+
{ org: "acme", slug: "shared-creds" },
|
|
79
|
+
],
|
|
80
|
+
envVisibility: {
|
|
81
|
+
"shared-creds": ApiResourceVisibility.visibility_org,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const { result } = renderHook(
|
|
86
|
+
() => useShareToolReadiness(toolAgent(), true),
|
|
87
|
+
{ wrapper: wrapper(client) },
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
await waitFor(() =>
|
|
91
|
+
expect(result.current).toEqual({
|
|
92
|
+
status: "blocked",
|
|
93
|
+
privateEnvironments: ["acme/private-creds"],
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("reports ready when every bound environment is org-shared", async () => {
|
|
99
|
+
const client = mockStigmer({
|
|
100
|
+
instanceEnvRefs: [{ org: "acme", slug: "shared-creds" }],
|
|
101
|
+
envVisibility: {
|
|
102
|
+
"shared-creds": ApiResourceVisibility.visibility_org,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const { result } = renderHook(
|
|
107
|
+
() => useShareToolReadiness(toolAgent(), true),
|
|
108
|
+
{ wrapper: wrapper(client) },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
await waitFor(() => expect(result.current).toEqual({ status: "ready" }));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("treats an unreadable environment ref as blocking", async () => {
|
|
115
|
+
const client = mockStigmer({
|
|
116
|
+
instanceEnvRefs: [{ org: "acme", slug: "deleted-env" }],
|
|
117
|
+
envError: true,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const { result } = renderHook(
|
|
121
|
+
() => useShareToolReadiness(toolAgent(), true),
|
|
122
|
+
{ wrapper: wrapper(client) },
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await waitFor(() =>
|
|
126
|
+
expect(result.current).toEqual({
|
|
127
|
+
status: "blocked",
|
|
128
|
+
privateEnvironments: ["acme/deleted-env"],
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("is n/a for agents without MCP tools — no lookups fire", async () => {
|
|
134
|
+
const client = mockStigmer({});
|
|
135
|
+
const { result } = renderHook(
|
|
136
|
+
() => useShareToolReadiness(toolAgent({ mcpUsages: false }), true),
|
|
137
|
+
{ wrapper: wrapper(client) },
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(result.current).toEqual({ status: "na" });
|
|
141
|
+
await waitFor(() => {
|
|
142
|
+
expect(
|
|
143
|
+
(client as { agentInstance: { get: ReturnType<typeof vi.fn> } })
|
|
144
|
+
.agentInstance.get,
|
|
145
|
+
).not.toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("is n/a when sharing is disabled", () => {
|
|
150
|
+
const client = mockStigmer({
|
|
151
|
+
instanceEnvRefs: [{ org: "acme", slug: "private-creds" }],
|
|
152
|
+
});
|
|
153
|
+
const { result } = renderHook(
|
|
154
|
+
() => useShareToolReadiness(toolAgent(), false),
|
|
155
|
+
{ wrapper: wrapper(client) },
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(result.current).toEqual({ status: "na" });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("is n/a in local mode — no guest runtime, no secret gating", () => {
|
|
162
|
+
const client = mockStigmer({
|
|
163
|
+
instanceEnvRefs: [{ org: "acme", slug: "private-creds" }],
|
|
164
|
+
});
|
|
165
|
+
const { result } = renderHook(
|
|
166
|
+
() => useShareToolReadiness(toolAgent(), true),
|
|
167
|
+
{ wrapper: wrapper(client, "local") },
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect(result.current).toEqual({ status: "na" });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("is n/a when the instance has no bound environments (no warning on a guess)", async () => {
|
|
174
|
+
const client = mockStigmer({ instanceEnvRefs: [] });
|
|
175
|
+
const { result } = renderHook(
|
|
176
|
+
() => useShareToolReadiness(toolAgent(), true),
|
|
177
|
+
{ wrapper: wrapper(client) },
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
await waitFor(() => expect(result.current).toEqual({ status: "na" }));
|
|
181
|
+
});
|
|
182
|
+
});
|