@stigmer/react 3.1.8 → 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.
Files changed (72) hide show
  1. package/environment/EnvironmentPicker.d.ts +17 -1
  2. package/environment/EnvironmentPicker.d.ts.map +1 -1
  3. package/environment/EnvironmentPicker.js +3 -2
  4. package/environment/EnvironmentPicker.js.map +1 -1
  5. package/index.d.ts +2 -2
  6. package/index.d.ts.map +1 -1
  7. package/index.js +2 -2
  8. package/index.js.map +1 -1
  9. package/package.json +4 -4
  10. package/sharing/ShareAgentDialog.d.ts +10 -8
  11. package/sharing/ShareAgentDialog.d.ts.map +1 -1
  12. package/sharing/ShareAgentDialog.js +139 -54
  13. package/sharing/ShareAgentDialog.js.map +1 -1
  14. package/sharing/SharedAgentChat.d.ts +1 -1
  15. package/sharing/SharedAgentChat.d.ts.map +1 -1
  16. package/sharing/index.d.ts +4 -2
  17. package/sharing/index.d.ts.map +1 -1
  18. package/sharing/index.js +2 -1
  19. package/sharing/index.js.map +1 -1
  20. package/sharing/useAgentShare.d.ts +43 -0
  21. package/sharing/useAgentShare.d.ts.map +1 -0
  22. package/sharing/useAgentShare.js +54 -0
  23. package/sharing/useAgentShare.js.map +1 -0
  24. package/sharing/useRotateShareLink.d.ts +22 -19
  25. package/sharing/useRotateShareLink.d.ts.map +1 -1
  26. package/sharing/useRotateShareLink.js +22 -19
  27. package/sharing/useRotateShareLink.js.map +1 -1
  28. package/sharing/useSaveAgentShare.d.ts +105 -0
  29. package/sharing/useSaveAgentShare.d.ts.map +1 -0
  30. package/sharing/useSaveAgentShare.js +97 -0
  31. package/sharing/useSaveAgentShare.js.map +1 -0
  32. package/sharing/useShareAgent.d.ts +5 -4
  33. package/sharing/useShareAgent.d.ts.map +1 -1
  34. package/sharing/useShareAgent.js +5 -4
  35. package/sharing/useShareAgent.js.map +1 -1
  36. package/sharing/useShareToolReadiness.d.ts +29 -25
  37. package/sharing/useShareToolReadiness.d.ts.map +1 -1
  38. package/sharing/useShareToolReadiness.js +47 -51
  39. package/sharing/useShareToolReadiness.js.map +1 -1
  40. package/sharing/useSharedAgentProfile.d.ts +5 -4
  41. package/sharing/useSharedAgentProfile.d.ts.map +1 -1
  42. package/sharing/useSharedAgentProfile.js +6 -5
  43. package/sharing/useSharedAgentProfile.js.map +1 -1
  44. package/src/environment/EnvironmentPicker.tsx +23 -2
  45. package/src/index.ts +8 -4
  46. package/src/sharing/ShareAgentDialog.tsx +353 -119
  47. package/src/sharing/SharedAgentChat.tsx +1 -1
  48. package/src/sharing/__tests__/ShareAgentDialog.test.tsx +550 -369
  49. package/src/sharing/__tests__/useAgentShare.test.tsx +149 -0
  50. package/src/sharing/__tests__/useRotateShareLink.test.tsx +8 -8
  51. package/src/sharing/__tests__/useSaveAgentShare.test.tsx +241 -0
  52. package/src/sharing/__tests__/useShareAgent.test.tsx +6 -2
  53. package/src/sharing/__tests__/useShareToolReadiness.test.tsx +84 -47
  54. package/src/sharing/__tests__/useSharedAgentProfile.test.tsx +1 -1
  55. package/src/sharing/index.ts +9 -7
  56. package/src/sharing/useAgentShare.ts +91 -0
  57. package/src/sharing/useRotateShareLink.ts +30 -25
  58. package/src/sharing/useSaveAgentShare.ts +186 -0
  59. package/src/sharing/useShareAgent.tsx +5 -4
  60. package/src/sharing/useShareToolReadiness.ts +62 -60
  61. package/src/sharing/useSharedAgentProfile.ts +7 -6
  62. package/src/switch/Switch.tsx +6 -1
  63. package/src/switch/__tests__/Switch.test.tsx +19 -0
  64. package/switch/Switch.d.ts.map +1 -1
  65. package/switch/Switch.js +6 -1
  66. package/switch/Switch.js.map +1 -1
  67. package/sharing/useUpdateAgentSharing.d.ts +0 -86
  68. package/sharing/useUpdateAgentSharing.d.ts.map +0 -1
  69. package/sharing/useUpdateAgentSharing.js +0 -81
  70. package/sharing/useUpdateAgentSharing.js.map +0 -1
  71. package/src/sharing/__tests__/useUpdateAgentSharing.test.tsx +0 -200
  72. package/src/sharing/useUpdateAgentSharing.ts +0 -151
@@ -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
+ });
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import { renderHook, act } from "@testing-library/react";
3
3
  import type { ReactNode } from "react";
4
- import type { RotateShareLinkInput } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/io_pb";
4
+ import type { RotateShareLinkInput } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/io_pb";
5
5
  import { StigmerContext } from "../../context";
6
6
  import { useRotateShareLink } from "../useRotateShareLink";
7
7
 
@@ -9,7 +9,7 @@ function createMockStigmer(overrides: {
9
9
  rotateShareLink?: (input: RotateShareLinkInput) => Promise<unknown>;
10
10
  } = {}) {
11
11
  return {
12
- agent: {
12
+ agentShare: {
13
13
  rotateShareLink:
14
14
  overrides.rotateShareLink ?? vi.fn().mockResolvedValue({}),
15
15
  },
@@ -31,15 +31,15 @@ describe("useRotateShareLink", () => {
31
31
  vi.restoreAllMocks();
32
32
  });
33
33
 
34
- it("calls rotateShareLink with the agent id and returns the updated agent", async () => {
34
+ it("calls rotateShareLink with the share id and returns the updated share", async () => {
35
35
  const rotated = {
36
- metadata: { id: "agt_1" },
36
+ metadata: { id: "ash_1" },
37
37
  status: { shareLinkToken: "fresh-token" },
38
38
  };
39
39
  const rotateShareLink = vi.fn().mockResolvedValue(rotated);
40
40
  const client = createMockStigmer({ rotateShareLink });
41
41
 
42
- const { result } = renderHook(() => useRotateShareLink("agt_1"), {
42
+ const { result } = renderHook(() => useRotateShareLink("ash_1"), {
43
43
  wrapper: wrapper(client),
44
44
  });
45
45
 
@@ -50,7 +50,7 @@ describe("useRotateShareLink", () => {
50
50
 
51
51
  expect(rotateShareLink).toHaveBeenCalledTimes(1);
52
52
  const input = rotateShareLink.mock.calls[0][0] as RotateShareLinkInput;
53
- expect(input.resourceId).toBe("agt_1");
53
+ expect(input.resourceId).toBe("ash_1");
54
54
  // The server generates the token — the request carries nothing but the id.
55
55
  expect(Object.keys(input)).not.toContain("linkToken");
56
56
  expect(returned).toBe(rotated);
@@ -58,7 +58,7 @@ describe("useRotateShareLink", () => {
58
58
  expect(result.current.isPending).toBe(false);
59
59
  });
60
60
 
61
- it("is a stable no-op while agentId is null", async () => {
61
+ it("is a stable no-op while shareId is null (agent never shared)", async () => {
62
62
  const rotateShareLink = vi.fn();
63
63
  const client = createMockStigmer({ rotateShareLink });
64
64
 
@@ -80,7 +80,7 @@ describe("useRotateShareLink", () => {
80
80
  const rotateShareLink = vi.fn().mockRejectedValue(failure);
81
81
  const client = createMockStigmer({ rotateShareLink });
82
82
 
83
- const { result } = renderHook(() => useRotateShareLink("agt_1"), {
83
+ const { result } = renderHook(() => useRotateShareLink("ash_1"), {
84
84
  wrapper: wrapper(client),
85
85
  });
86
86
 
@@ -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
+ });
@@ -29,7 +29,11 @@ function createMockStigmer(overrides: {
29
29
  isAuthorized: overrides.isAuthorized ?? true,
30
30
  }),
31
31
  },
32
- agent: { updateSharing: vi.fn().mockResolvedValue({}) },
32
+ agentShare: {
33
+ getByAgent: vi.fn().mockResolvedValue({ totalCount: 0, items: [] }),
34
+ apply: vi.fn().mockResolvedValue({}),
35
+ rotateShareLink: vi.fn().mockResolvedValue({}),
36
+ },
33
37
  billing: { getOrCreateBillingAccount: vi.fn().mockResolvedValue(null) },
34
38
  } as never;
35
39
  }
@@ -53,7 +57,7 @@ const AGENT = {
53
57
  slug: "support-agent",
54
58
  name: "Support Agent",
55
59
  },
56
- spec: { sharing: undefined },
60
+ spec: {},
57
61
  } as never;
58
62
 
59
63
  describe("useShareAgent", () => {
@@ -8,11 +8,9 @@ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apires
8
8
  import { StigmerContext } from "../../context";
9
9
  import { DeploymentModeContext } from "../../deployment-mode";
10
10
  import { useShareToolReadiness } from "../useShareToolReadiness";
11
+ import type { AgentShareDraft, SharingAudience } from "../useSaveAgentShare";
11
12
 
12
- function toolAgent(overrides?: {
13
- mcpUsages?: boolean;
14
- defaultInstanceId?: string;
15
- }): Agent {
13
+ function toolAgent(overrides?: { mcpUsages?: boolean }): Agent {
16
14
  return create(AgentSchema, {
17
15
  metadata: { id: "agt_1", org: "acme", slug: "helper" },
18
16
  spec: {
@@ -22,23 +20,28 @@ function toolAgent(overrides?: {
22
20
  ? [{ mcpServerRef: { org: "acme", slug: "github" } }]
23
21
  : [],
24
22
  },
25
- status: {
26
- defaultInstanceId: overrides?.defaultInstanceId ?? "agi_1",
27
- },
28
23
  });
29
24
  }
30
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
+
31
40
  function mockStigmer(overrides: {
32
- instanceEnvRefs?: { org: string; slug: string }[];
33
41
  envVisibility?: Record<string, ApiResourceVisibility>;
34
42
  envError?: boolean;
35
43
  }) {
36
44
  return {
37
- agentInstance: {
38
- get: vi.fn().mockResolvedValue({
39
- spec: { environmentRefs: overrides.instanceEnvRefs ?? [] },
40
- }),
41
- },
42
45
  environment: {
43
46
  getByReference: vi.fn().mockImplementation(
44
47
  ({ slug }: { org: string; slug: string }) => {
@@ -70,20 +73,46 @@ function wrapper(client: unknown, mode: "cloud" | "local" = "cloud") {
70
73
  };
71
74
  }
72
75
 
76
+ function envLookup(client: unknown) {
77
+ return (
78
+ client as { environment: { getByReference: ReturnType<typeof vi.fn> } }
79
+ ).environment.getByReference;
80
+ }
81
+
73
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
+
74
98
  it("reports blocked with the private environment refs listed", async () => {
75
99
  const client = mockStigmer({
76
- instanceEnvRefs: [
77
- { org: "acme", slug: "private-creds" },
78
- { org: "acme", slug: "shared-creds" },
79
- ],
80
100
  envVisibility: {
81
101
  "shared-creds": ApiResourceVisibility.visibility_org,
82
102
  },
83
103
  });
84
104
 
85
105
  const { result } = renderHook(
86
- () => useShareToolReadiness(toolAgent(), true),
106
+ () =>
107
+ useShareToolReadiness(
108
+ toolAgent(),
109
+ makeDraft({
110
+ environmentRefs: [
111
+ { org: "acme", slug: "private-creds" },
112
+ { org: "acme", slug: "shared-creds" },
113
+ ],
114
+ }),
115
+ ),
87
116
  { wrapper: wrapper(client) },
88
117
  );
89
118
 
@@ -97,14 +126,19 @@ describe("useShareToolReadiness", () => {
97
126
 
98
127
  it("reports ready when every bound environment is org-shared", async () => {
99
128
  const client = mockStigmer({
100
- instanceEnvRefs: [{ org: "acme", slug: "shared-creds" }],
101
129
  envVisibility: {
102
130
  "shared-creds": ApiResourceVisibility.visibility_org,
103
131
  },
104
132
  });
105
133
 
106
134
  const { result } = renderHook(
107
- () => useShareToolReadiness(toolAgent(), true),
135
+ () =>
136
+ useShareToolReadiness(
137
+ toolAgent(),
138
+ makeDraft({
139
+ environmentRefs: [{ org: "acme", slug: "shared-creds" }],
140
+ }),
141
+ ),
108
142
  { wrapper: wrapper(client) },
109
143
  );
110
144
 
@@ -112,13 +146,16 @@ describe("useShareToolReadiness", () => {
112
146
  });
113
147
 
114
148
  it("treats an unreadable environment ref as blocking", async () => {
115
- const client = mockStigmer({
116
- instanceEnvRefs: [{ org: "acme", slug: "deleted-env" }],
117
- envError: true,
118
- });
149
+ const client = mockStigmer({ envError: true });
119
150
 
120
151
  const { result } = renderHook(
121
- () => useShareToolReadiness(toolAgent(), true),
152
+ () =>
153
+ useShareToolReadiness(
154
+ toolAgent(),
155
+ makeDraft({
156
+ environmentRefs: [{ org: "acme", slug: "deleted-env" }],
157
+ }),
158
+ ),
122
159
  { wrapper: wrapper(client) },
123
160
  );
124
161
 
@@ -133,50 +170,50 @@ describe("useShareToolReadiness", () => {
133
170
  it("is n/a for agents without MCP tools — no lookups fire", async () => {
134
171
  const client = mockStigmer({});
135
172
  const { result } = renderHook(
136
- () => useShareToolReadiness(toolAgent({ mcpUsages: false }), true),
173
+ () =>
174
+ useShareToolReadiness(
175
+ toolAgent({ mcpUsages: false }),
176
+ makeDraft({
177
+ environmentRefs: [{ org: "acme", slug: "shared-creds" }],
178
+ }),
179
+ ),
137
180
  { wrapper: wrapper(client) },
138
181
  );
139
182
 
140
183
  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
- });
184
+ await waitFor(() => expect(envLookup(client)).not.toHaveBeenCalled());
147
185
  });
148
186
 
149
187
  it("is n/a when sharing is disabled", () => {
150
- const client = mockStigmer({
151
- instanceEnvRefs: [{ org: "acme", slug: "private-creds" }],
152
- });
188
+ const client = mockStigmer({});
153
189
  const { result } = renderHook(
154
- () => useShareToolReadiness(toolAgent(), false),
190
+ () => useShareToolReadiness(toolAgent(), makeDraft({ enabled: false })),
155
191
  { wrapper: wrapper(client) },
156
192
  );
157
193
 
158
194
  expect(result.current).toEqual({ status: "na" });
159
195
  });
160
196
 
161
- it("is n/a in local modeno guest runtime, no secret gating", () => {
162
- const client = mockStigmer({
163
- instanceEnvRefs: [{ org: "acme", slug: "private-creds" }],
164
- });
197
+ it("is n/a for org-audience sharesbindings are public-audience only", () => {
198
+ const client = mockStigmer({});
165
199
  const { result } = renderHook(
166
- () => useShareToolReadiness(toolAgent(), true),
167
- { wrapper: wrapper(client, "local") },
200
+ () => useShareToolReadiness(toolAgent(), makeDraft({ audience: "org" })),
201
+ { wrapper: wrapper(client) },
168
202
  );
169
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.
170
207
  expect(result.current).toEqual({ status: "na" });
171
208
  });
172
209
 
173
- it("is n/a when the instance has no bound environments (no warning on a guess)", async () => {
174
- const client = mockStigmer({ instanceEnvRefs: [] });
210
+ it("is n/a in local mode no guest runtime, no secret gating", () => {
211
+ const client = mockStigmer({});
175
212
  const { result } = renderHook(
176
- () => useShareToolReadiness(toolAgent(), true),
177
- { wrapper: wrapper(client) },
213
+ () => useShareToolReadiness(toolAgent(), makeDraft()),
214
+ { wrapper: wrapper(client, "local") },
178
215
  );
179
216
 
180
- await waitFor(() => expect(result.current).toEqual({ status: "na" }));
217
+ expect(result.current).toEqual({ status: "na" });
181
218
  });
182
219
  });