@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,241 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { renderHook, act, waitFor } 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 type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
|
|
6
|
+
import type { AgentShareInput } from "@stigmer/sdk";
|
|
7
|
+
import { StigmerContext } from "../../context";
|
|
8
|
+
import {
|
|
9
|
+
sharingAudienceFromProto,
|
|
10
|
+
useSaveAgentShare,
|
|
11
|
+
type AgentShareDraft,
|
|
12
|
+
} from "../useSaveAgentShare";
|
|
13
|
+
|
|
14
|
+
function createMockStigmer(overrides: {
|
|
15
|
+
apply?: (input: AgentShareInput) => Promise<unknown>;
|
|
16
|
+
} = {}) {
|
|
17
|
+
return {
|
|
18
|
+
agentShare: {
|
|
19
|
+
apply: overrides.apply ?? vi.fn().mockResolvedValue({}),
|
|
20
|
+
},
|
|
21
|
+
} as never;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function wrapper(client: unknown) {
|
|
25
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
26
|
+
return (
|
|
27
|
+
<StigmerContext.Provider value={client as never}>
|
|
28
|
+
{children}
|
|
29
|
+
</StigmerContext.Provider>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const AGENT = {
|
|
35
|
+
metadata: {
|
|
36
|
+
id: "agt_1",
|
|
37
|
+
org: "acme",
|
|
38
|
+
slug: "support-agent",
|
|
39
|
+
name: "Support Agent",
|
|
40
|
+
},
|
|
41
|
+
} as never;
|
|
42
|
+
|
|
43
|
+
const FULL_DRAFT: AgentShareDraft = {
|
|
44
|
+
enabled: true,
|
|
45
|
+
audience: "public",
|
|
46
|
+
allowedOrigins: ["https://example.com", "https://docs.example.com"],
|
|
47
|
+
messages: {
|
|
48
|
+
rateLimited: "Slow down, please.",
|
|
49
|
+
unavailable: "We're out of credits.",
|
|
50
|
+
conversationEnded: "This conversation has ended.",
|
|
51
|
+
},
|
|
52
|
+
environmentRefs: [{ org: "acme", slug: "github-org-shared" }],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
describe("useSaveAgentShare", () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
vi.restoreAllMocks();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("applies the complete configuration — identity, agent_ref, origins, messages, and bindings", async () => {
|
|
61
|
+
const apply = vi.fn().mockResolvedValue({});
|
|
62
|
+
const client = createMockStigmer({ apply });
|
|
63
|
+
|
|
64
|
+
const { result } = renderHook(() => useSaveAgentShare(AGENT), {
|
|
65
|
+
wrapper: wrapper(client),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// No existing share: the apply creates the canonical one, keyed on
|
|
69
|
+
// the agent's own org/slug (the server's D2 default, made explicit).
|
|
70
|
+
await act(() => result.current.save(FULL_DRAFT, null));
|
|
71
|
+
|
|
72
|
+
expect(apply).toHaveBeenCalledTimes(1);
|
|
73
|
+
const input = apply.mock.calls[0][0] as AgentShareInput;
|
|
74
|
+
expect(input.org).toBe("acme");
|
|
75
|
+
expect(input.slug).toBe("support-agent");
|
|
76
|
+
expect(input.name).toBe("Support Agent");
|
|
77
|
+
expect(input.agentRef).toEqual({ org: "acme", slug: "support-agent" });
|
|
78
|
+
expect(input.enabled).toBe(true);
|
|
79
|
+
expect(input.allowedOrigins).toEqual([
|
|
80
|
+
"https://example.com",
|
|
81
|
+
"https://docs.example.com",
|
|
82
|
+
]);
|
|
83
|
+
expect(input.messages?.rateLimited).toBe("Slow down, please.");
|
|
84
|
+
expect(input.messages?.unavailable).toBe("We're out of credits.");
|
|
85
|
+
expect(input.messages?.conversationEnded).toBe(
|
|
86
|
+
"This conversation has ended.",
|
|
87
|
+
);
|
|
88
|
+
expect(input.environmentRefs).toEqual([
|
|
89
|
+
{ org: "acme", slug: "github-org-shared" },
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("keeps the existing share's identity when editing (a renamed share is never forked)", async () => {
|
|
94
|
+
const apply = vi.fn().mockResolvedValue({});
|
|
95
|
+
const client = createMockStigmer({ apply });
|
|
96
|
+
// A share created via manifest with a non-default slug: applying with
|
|
97
|
+
// the agent's slug would create a SECOND share instead of updating.
|
|
98
|
+
const renamedShare = {
|
|
99
|
+
metadata: { id: "ash_1", org: "acme", slug: "help-desk", name: "Help Desk" },
|
|
100
|
+
spec: { enabled: true },
|
|
101
|
+
} as AgentShare;
|
|
102
|
+
|
|
103
|
+
const { result } = renderHook(() => useSaveAgentShare(AGENT), {
|
|
104
|
+
wrapper: wrapper(client),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await act(() => result.current.save(FULL_DRAFT, renamedShare));
|
|
108
|
+
|
|
109
|
+
const input = apply.mock.calls[0][0] as AgentShareInput;
|
|
110
|
+
expect(input.slug).toBe("help-desk");
|
|
111
|
+
expect(input.name).toBe("Help Desk");
|
|
112
|
+
// The agent reference is the agent's identity regardless of share slug.
|
|
113
|
+
expect(input.agentRef).toEqual({ org: "acme", slug: "support-agent" });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("disabling still carries origins, messages, bindings, and audience (toggle never clobbers config)", async () => {
|
|
117
|
+
const apply = vi.fn().mockResolvedValue({});
|
|
118
|
+
const client = createMockStigmer({ apply });
|
|
119
|
+
|
|
120
|
+
const { result } = renderHook(() => useSaveAgentShare(AGENT), {
|
|
121
|
+
wrapper: wrapper(client),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await act(() =>
|
|
125
|
+
result.current.save(
|
|
126
|
+
{ ...FULL_DRAFT, enabled: false, audience: "org", environmentRefs: [] },
|
|
127
|
+
null,
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const input = apply.mock.calls[0][0] as AgentShareInput;
|
|
132
|
+
expect(input.enabled).toBe(false);
|
|
133
|
+
// Apply replaces the spec wholesale — a disable that dropped these
|
|
134
|
+
// fields would silently erase the owner's configuration (an audience
|
|
135
|
+
// drop would silently revert an org-only share to public).
|
|
136
|
+
expect(input.allowedOrigins).toEqual(FULL_DRAFT.allowedOrigins);
|
|
137
|
+
expect(input.messages?.rateLimited).toBe(FULL_DRAFT.messages.rateLimited);
|
|
138
|
+
expect(input.audience).toBe(AgentShareAudience.org);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("maps the audience union to the proto enum, writing public explicitly", async () => {
|
|
142
|
+
const apply = vi.fn().mockResolvedValue({});
|
|
143
|
+
const client = createMockStigmer({ apply });
|
|
144
|
+
|
|
145
|
+
const { result } = renderHook(() => useSaveAgentShare(AGENT), {
|
|
146
|
+
wrapper: wrapper(client),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await act(() => result.current.save(FULL_DRAFT, null));
|
|
150
|
+
await act(() =>
|
|
151
|
+
result.current.save(
|
|
152
|
+
{ ...FULL_DRAFT, audience: "org", environmentRefs: [] },
|
|
153
|
+
null,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const first = apply.mock.calls[0][0] as AgentShareInput;
|
|
158
|
+
const second = apply.mock.calls[1][0] as AgentShareInput;
|
|
159
|
+
// "public" persists as the explicit enum value, never unspecified —
|
|
160
|
+
// a console-managed share can't be downgraded by a later manifest
|
|
161
|
+
// that relies on the unspecified-means-public default.
|
|
162
|
+
expect(first.audience).toBe(AgentShareAudience.public);
|
|
163
|
+
expect(second.audience).toBe(AgentShareAudience.org);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("is a stable no-op when the agent is null", async () => {
|
|
167
|
+
const apply = vi.fn();
|
|
168
|
+
const client = createMockStigmer({ apply });
|
|
169
|
+
|
|
170
|
+
const { result } = renderHook(() => useSaveAgentShare(null), {
|
|
171
|
+
wrapper: wrapper(client),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const returned = await act(() => result.current.save(FULL_DRAFT, null));
|
|
175
|
+
expect(returned).toBeUndefined();
|
|
176
|
+
expect(apply).not.toHaveBeenCalled();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("exposes isPending during the request", async () => {
|
|
180
|
+
let resolveRpc: (v: unknown) => void = () => {};
|
|
181
|
+
const apply = vi.fn().mockReturnValue(
|
|
182
|
+
new Promise((resolve) => {
|
|
183
|
+
resolveRpc = resolve;
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
const client = createMockStigmer({ apply });
|
|
187
|
+
|
|
188
|
+
const { result } = renderHook(() => useSaveAgentShare(AGENT), {
|
|
189
|
+
wrapper: wrapper(client),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
let pending: Promise<unknown>;
|
|
193
|
+
act(() => {
|
|
194
|
+
pending = result.current.save(FULL_DRAFT, null);
|
|
195
|
+
});
|
|
196
|
+
expect(result.current.isPending).toBe(true);
|
|
197
|
+
|
|
198
|
+
await act(async () => {
|
|
199
|
+
resolveRpc({});
|
|
200
|
+
await pending;
|
|
201
|
+
});
|
|
202
|
+
expect(result.current.isPending).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("captures and rethrows RPC errors", async () => {
|
|
206
|
+
const failure = new Error("permission denied");
|
|
207
|
+
const apply = vi.fn().mockRejectedValue(failure);
|
|
208
|
+
const client = createMockStigmer({ apply });
|
|
209
|
+
|
|
210
|
+
const { result } = renderHook(() => useSaveAgentShare(AGENT), {
|
|
211
|
+
wrapper: wrapper(client),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
let caught: unknown;
|
|
215
|
+
await act(async () => {
|
|
216
|
+
try {
|
|
217
|
+
await result.current.save(FULL_DRAFT, null);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
caught = err;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(caught).toBe(failure);
|
|
224
|
+
await waitFor(() => expect(result.current.error).toBe(failure));
|
|
225
|
+
expect(result.current.isPending).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("sharingAudienceFromProto", () => {
|
|
230
|
+
it("maps unspecified to public (a share without an audience is anyone-with-link)", () => {
|
|
231
|
+
expect(sharingAudienceFromProto(undefined)).toBe("public");
|
|
232
|
+
expect(sharingAudienceFromProto(AgentShareAudience.unspecified)).toBe(
|
|
233
|
+
"public",
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("maps the explicit values", () => {
|
|
238
|
+
expect(sharingAudienceFromProto(AgentShareAudience.public)).toBe("public");
|
|
239
|
+
expect(sharingAudienceFromProto(AgentShareAudience.org)).toBe("org");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
agentShare: {
|
|
33
|
+
getByAgent: vi.fn().mockResolvedValue({ totalCount: 0, items: [] }),
|
|
34
|
+
apply: vi.fn().mockResolvedValue({}),
|
|
35
|
+
rotateShareLink: vi.fn().mockResolvedValue({}),
|
|
36
|
+
},
|
|
37
|
+
billing: { getOrCreateBillingAccount: vi.fn().mockResolvedValue(null) },
|
|
38
|
+
} as never;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function wrapper(client: unknown) {
|
|
42
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
43
|
+
return (
|
|
44
|
+
<FetchCacheContext.Provider value={null}>
|
|
45
|
+
<StigmerContext.Provider value={client as never}>
|
|
46
|
+
{children}
|
|
47
|
+
</StigmerContext.Provider>
|
|
48
|
+
</FetchCacheContext.Provider>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const AGENT = {
|
|
54
|
+
metadata: {
|
|
55
|
+
id: "agt_1",
|
|
56
|
+
org: "acme",
|
|
57
|
+
slug: "support-agent",
|
|
58
|
+
name: "Support Agent",
|
|
59
|
+
},
|
|
60
|
+
spec: {},
|
|
61
|
+
} as never;
|
|
62
|
+
|
|
63
|
+
describe("useShareAgent", () => {
|
|
64
|
+
it("returns a Share action in the sharing group when the user can edit", async () => {
|
|
65
|
+
const { result } = renderHook(() => useShareAgent({ agent: AGENT }), {
|
|
66
|
+
wrapper: wrapper(createMockStigmer({ isAuthorized: true })),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await waitFor(() => expect(result.current.action).not.toBeNull());
|
|
70
|
+
expect(result.current.action?.id).toBe("share");
|
|
71
|
+
expect(result.current.action?.label).toBe("Share");
|
|
72
|
+
expect(result.current.action?.group).toBe("sharing");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns a null action while the agent is loading", () => {
|
|
76
|
+
const checkMyPermission = vi.fn();
|
|
77
|
+
const { result } = renderHook(() => useShareAgent({ agent: null }), {
|
|
78
|
+
wrapper: wrapper(createMockStigmer({ checkMyPermission })),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(result.current.action).toBeNull();
|
|
82
|
+
expect(result.current.dialog).toBeNull();
|
|
83
|
+
expect(checkMyPermission).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns a null action when the user lacks can_edit", async () => {
|
|
87
|
+
const client = createMockStigmer({ isAuthorized: false });
|
|
88
|
+
const { result } = renderHook(() => useShareAgent({ agent: AGENT }), {
|
|
89
|
+
wrapper: wrapper(client),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await waitFor(() =>
|
|
93
|
+
expect(
|
|
94
|
+
(client as { iamPolicy: { checkMyPermission: ReturnType<typeof vi.fn> } })
|
|
95
|
+
.iamPolicy.checkMyPermission,
|
|
96
|
+
).toHaveBeenCalled(),
|
|
97
|
+
);
|
|
98
|
+
await waitFor(() => expect(result.current.action).toBeNull());
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("checks the can_edit relation on the agent", async () => {
|
|
102
|
+
const client = createMockStigmer({ isAuthorized: true });
|
|
103
|
+
renderHook(() => useShareAgent({ agent: AGENT }), {
|
|
104
|
+
wrapper: wrapper(client),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const check = (
|
|
108
|
+
client as { iamPolicy: { checkMyPermission: ReturnType<typeof vi.fn> } }
|
|
109
|
+
).iamPolicy.checkMyPermission;
|
|
110
|
+
await waitFor(() => expect(check).toHaveBeenCalled());
|
|
111
|
+
const input = check.mock.calls[0][0] as {
|
|
112
|
+
resource?: { kind: string; id: string };
|
|
113
|
+
relation: string;
|
|
114
|
+
};
|
|
115
|
+
expect(input.relation).toBe("can_edit");
|
|
116
|
+
expect(input.resource?.kind).toBe("agent");
|
|
117
|
+
expect(input.resource?.id).toBe("agt_1");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("opens the dialog via the action", async () => {
|
|
121
|
+
const { result } = renderHook(() => useShareAgent({ agent: AGENT }), {
|
|
122
|
+
wrapper: wrapper(createMockStigmer({ isAuthorized: true })),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await waitFor(() => expect(result.current.action).not.toBeNull());
|
|
126
|
+
expect(result.current.isOpen).toBe(false);
|
|
127
|
+
|
|
128
|
+
result.current.action?.onAction();
|
|
129
|
+
await waitFor(() => expect(result.current.isOpen).toBe(true));
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
import type { AgentShareDraft, SharingAudience } from "../useSaveAgentShare";
|
|
12
|
+
|
|
13
|
+
function toolAgent(overrides?: { mcpUsages?: boolean }): Agent {
|
|
14
|
+
return create(AgentSchema, {
|
|
15
|
+
metadata: { id: "agt_1", org: "acme", slug: "helper" },
|
|
16
|
+
spec: {
|
|
17
|
+
instructions: "help",
|
|
18
|
+
mcpServerUsages:
|
|
19
|
+
(overrides?.mcpUsages ?? true)
|
|
20
|
+
? [{ mcpServerRef: { org: "acme", slug: "github" } }]
|
|
21
|
+
: [],
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeDraft(overrides?: {
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
audience?: SharingAudience;
|
|
29
|
+
environmentRefs?: { org: string; slug: string }[];
|
|
30
|
+
}): AgentShareDraft {
|
|
31
|
+
return {
|
|
32
|
+
enabled: overrides?.enabled ?? true,
|
|
33
|
+
audience: overrides?.audience ?? "public",
|
|
34
|
+
allowedOrigins: [],
|
|
35
|
+
messages: { rateLimited: "", unavailable: "", conversationEnded: "" },
|
|
36
|
+
environmentRefs: overrides?.environmentRefs ?? [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mockStigmer(overrides: {
|
|
41
|
+
envVisibility?: Record<string, ApiResourceVisibility>;
|
|
42
|
+
envError?: boolean;
|
|
43
|
+
}) {
|
|
44
|
+
return {
|
|
45
|
+
environment: {
|
|
46
|
+
getByReference: vi.fn().mockImplementation(
|
|
47
|
+
({ slug }: { org: string; slug: string }) => {
|
|
48
|
+
if (overrides.envError) {
|
|
49
|
+
return Promise.reject(new Error("not found"));
|
|
50
|
+
}
|
|
51
|
+
return Promise.resolve({
|
|
52
|
+
metadata: {
|
|
53
|
+
visibility:
|
|
54
|
+
overrides.envVisibility?.[slug] ??
|
|
55
|
+
ApiResourceVisibility.visibility_private,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
} as never;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function wrapper(client: unknown, mode: "cloud" | "local" = "cloud") {
|
|
65
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
66
|
+
return (
|
|
67
|
+
<StigmerContext.Provider value={client as never}>
|
|
68
|
+
<DeploymentModeContext.Provider value={mode}>
|
|
69
|
+
{children}
|
|
70
|
+
</DeploymentModeContext.Provider>
|
|
71
|
+
</StigmerContext.Provider>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function envLookup(client: unknown) {
|
|
77
|
+
return (
|
|
78
|
+
client as { environment: { getByReference: ReturnType<typeof vi.fn> } }
|
|
79
|
+
).environment.getByReference;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("useShareToolReadiness", () => {
|
|
83
|
+
it("reports needs-credentials when a tool-using share has no bindings — no lookups fire", async () => {
|
|
84
|
+
const client = mockStigmer({});
|
|
85
|
+
const { result } = renderHook(
|
|
86
|
+
() => useShareToolReadiness(toolAgent(), makeDraft()),
|
|
87
|
+
{ wrapper: wrapper(client) },
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// A tool-using agent with zero bound environments is broken for
|
|
91
|
+
// visitors BY CONSTRUCTION (guests receive credentials only from the
|
|
92
|
+
// share's bindings) — the hook says so instead of staying silent,
|
|
93
|
+
// and needs no server round-trip to know it.
|
|
94
|
+
expect(result.current).toEqual({ status: "needs-credentials" });
|
|
95
|
+
await waitFor(() => expect(envLookup(client)).not.toHaveBeenCalled());
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("reports blocked with the private environment refs listed", async () => {
|
|
99
|
+
const client = mockStigmer({
|
|
100
|
+
envVisibility: {
|
|
101
|
+
"shared-creds": ApiResourceVisibility.visibility_org,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const { result } = renderHook(
|
|
106
|
+
() =>
|
|
107
|
+
useShareToolReadiness(
|
|
108
|
+
toolAgent(),
|
|
109
|
+
makeDraft({
|
|
110
|
+
environmentRefs: [
|
|
111
|
+
{ org: "acme", slug: "private-creds" },
|
|
112
|
+
{ org: "acme", slug: "shared-creds" },
|
|
113
|
+
],
|
|
114
|
+
}),
|
|
115
|
+
),
|
|
116
|
+
{ wrapper: wrapper(client) },
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
await waitFor(() =>
|
|
120
|
+
expect(result.current).toEqual({
|
|
121
|
+
status: "blocked",
|
|
122
|
+
privateEnvironments: ["acme/private-creds"],
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("reports ready when every bound environment is org-shared", async () => {
|
|
128
|
+
const client = mockStigmer({
|
|
129
|
+
envVisibility: {
|
|
130
|
+
"shared-creds": ApiResourceVisibility.visibility_org,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const { result } = renderHook(
|
|
135
|
+
() =>
|
|
136
|
+
useShareToolReadiness(
|
|
137
|
+
toolAgent(),
|
|
138
|
+
makeDraft({
|
|
139
|
+
environmentRefs: [{ org: "acme", slug: "shared-creds" }],
|
|
140
|
+
}),
|
|
141
|
+
),
|
|
142
|
+
{ wrapper: wrapper(client) },
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
await waitFor(() => expect(result.current).toEqual({ status: "ready" }));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("treats an unreadable environment ref as blocking", async () => {
|
|
149
|
+
const client = mockStigmer({ envError: true });
|
|
150
|
+
|
|
151
|
+
const { result } = renderHook(
|
|
152
|
+
() =>
|
|
153
|
+
useShareToolReadiness(
|
|
154
|
+
toolAgent(),
|
|
155
|
+
makeDraft({
|
|
156
|
+
environmentRefs: [{ org: "acme", slug: "deleted-env" }],
|
|
157
|
+
}),
|
|
158
|
+
),
|
|
159
|
+
{ wrapper: wrapper(client) },
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
await waitFor(() =>
|
|
163
|
+
expect(result.current).toEqual({
|
|
164
|
+
status: "blocked",
|
|
165
|
+
privateEnvironments: ["acme/deleted-env"],
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("is n/a for agents without MCP tools — no lookups fire", async () => {
|
|
171
|
+
const client = mockStigmer({});
|
|
172
|
+
const { result } = renderHook(
|
|
173
|
+
() =>
|
|
174
|
+
useShareToolReadiness(
|
|
175
|
+
toolAgent({ mcpUsages: false }),
|
|
176
|
+
makeDraft({
|
|
177
|
+
environmentRefs: [{ org: "acme", slug: "shared-creds" }],
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
{ wrapper: wrapper(client) },
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(result.current).toEqual({ status: "na" });
|
|
184
|
+
await waitFor(() => expect(envLookup(client)).not.toHaveBeenCalled());
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("is n/a when sharing is disabled", () => {
|
|
188
|
+
const client = mockStigmer({});
|
|
189
|
+
const { result } = renderHook(
|
|
190
|
+
() => useShareToolReadiness(toolAgent(), makeDraft({ enabled: false })),
|
|
191
|
+
{ wrapper: wrapper(client) },
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(result.current).toEqual({ status: "na" });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("is n/a for org-audience shares — bindings are public-audience only", () => {
|
|
198
|
+
const client = mockStigmer({});
|
|
199
|
+
const { result } = renderHook(
|
|
200
|
+
() => useShareToolReadiness(toolAgent(), makeDraft({ audience: "org" })),
|
|
201
|
+
{ wrapper: wrapper(client) },
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Org-audience shares reject environment_refs at the proto boundary
|
|
205
|
+
// (member sessions carry no share linkage in Phase A), so there is
|
|
206
|
+
// no credential state to advise on.
|
|
207
|
+
expect(result.current).toEqual({ status: "na" });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("is n/a in local mode — no guest runtime, no secret gating", () => {
|
|
211
|
+
const client = mockStigmer({});
|
|
212
|
+
const { result } = renderHook(
|
|
213
|
+
() => useShareToolReadiness(toolAgent(), makeDraft()),
|
|
214
|
+
{ wrapper: wrapper(client, "local") },
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
expect(result.current).toEqual({ status: "na" });
|
|
218
|
+
});
|
|
219
|
+
});
|