@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.
- 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/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/sharing/ShareAgentDialog.d.ts +10 -8
- package/sharing/ShareAgentDialog.d.ts.map +1 -1
- package/sharing/ShareAgentDialog.js +139 -54
- package/sharing/ShareAgentDialog.js.map +1 -1
- package/sharing/SharedAgentChat.d.ts +1 -1
- package/sharing/SharedAgentChat.d.ts.map +1 -1
- package/sharing/index.d.ts +4 -2
- package/sharing/index.d.ts.map +1 -1
- package/sharing/index.js +2 -1
- package/sharing/index.js.map +1 -1
- 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 +22 -19
- package/sharing/useRotateShareLink.d.ts.map +1 -1
- package/sharing/useRotateShareLink.js +22 -19
- package/sharing/useRotateShareLink.js.map +1 -1
- 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 +5 -4
- package/sharing/useShareAgent.d.ts.map +1 -1
- package/sharing/useShareAgent.js +5 -4
- package/sharing/useShareAgent.js.map +1 -1
- package/sharing/useShareToolReadiness.d.ts +29 -25
- package/sharing/useShareToolReadiness.d.ts.map +1 -1
- package/sharing/useShareToolReadiness.js +47 -51
- package/sharing/useShareToolReadiness.js.map +1 -1
- package/sharing/useSharedAgentProfile.d.ts +5 -4
- package/sharing/useSharedAgentProfile.d.ts.map +1 -1
- package/sharing/useSharedAgentProfile.js +6 -5
- package/sharing/useSharedAgentProfile.js.map +1 -1
- package/src/environment/EnvironmentPicker.tsx +23 -2
- package/src/index.ts +8 -4
- package/src/sharing/ShareAgentDialog.tsx +353 -119
- package/src/sharing/SharedAgentChat.tsx +1 -1
- package/src/sharing/__tests__/ShareAgentDialog.test.tsx +550 -369
- package/src/sharing/__tests__/useAgentShare.test.tsx +149 -0
- package/src/sharing/__tests__/useRotateShareLink.test.tsx +8 -8
- package/src/sharing/__tests__/useSaveAgentShare.test.tsx +241 -0
- package/src/sharing/__tests__/useShareAgent.test.tsx +6 -2
- package/src/sharing/__tests__/useShareToolReadiness.test.tsx +84 -47
- package/src/sharing/__tests__/useSharedAgentProfile.test.tsx +1 -1
- package/src/sharing/index.ts +9 -7
- package/src/sharing/useAgentShare.ts +91 -0
- package/src/sharing/useRotateShareLink.ts +30 -25
- package/src/sharing/useSaveAgentShare.ts +186 -0
- package/src/sharing/useShareAgent.tsx +5 -4
- package/src/sharing/useShareToolReadiness.ts +62 -60
- package/src/sharing/useSharedAgentProfile.ts +7 -6
- package/src/switch/Switch.tsx +6 -1
- package/src/switch/__tests__/Switch.test.tsx +19 -0
- package/switch/Switch.d.ts.map +1 -1
- package/switch/Switch.js +6 -1
- package/switch/Switch.js.map +1 -1
- package/sharing/useUpdateAgentSharing.d.ts +0 -86
- package/sharing/useUpdateAgentSharing.d.ts.map +0 -1
- package/sharing/useUpdateAgentSharing.js +0 -81
- package/sharing/useUpdateAgentSharing.js.map +0 -1
- package/src/sharing/__tests__/useUpdateAgentSharing.test.tsx +0 -200
- 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/
|
|
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
|
-
|
|
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
|
|
34
|
+
it("calls rotateShareLink with the share id and returns the updated share", async () => {
|
|
35
35
|
const rotated = {
|
|
36
|
-
metadata: { id: "
|
|
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("
|
|
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("
|
|
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
|
|
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("
|
|
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
|
-
|
|
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: {
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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
|
|
162
|
-
const client = mockStigmer({
|
|
163
|
-
instanceEnvRefs: [{ org: "acme", slug: "private-creds" }],
|
|
164
|
-
});
|
|
197
|
+
it("is n/a for org-audience shares — bindings are public-audience only", () => {
|
|
198
|
+
const client = mockStigmer({});
|
|
165
199
|
const { result } = renderHook(
|
|
166
|
-
() => useShareToolReadiness(toolAgent(),
|
|
167
|
-
{ wrapper: wrapper(client
|
|
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
|
|
174
|
-
const client = mockStigmer({
|
|
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(),
|
|
177
|
-
{ wrapper: wrapper(client) },
|
|
213
|
+
() => useShareToolReadiness(toolAgent(), makeDraft()),
|
|
214
|
+
{ wrapper: wrapper(client, "local") },
|
|
178
215
|
);
|
|
179
216
|
|
|
180
|
-
|
|
217
|
+
expect(result.current).toEqual({ status: "na" });
|
|
181
218
|
});
|
|
182
219
|
});
|