@stigmer/cli 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/package.json +5 -5
- package/registry/metadata.d.ts.map +1 -1
- package/registry/metadata.js +2 -0
- package/registry/metadata.js.map +1 -1
- package/registry/verb-support.d.ts.map +1 -1
- package/registry/verb-support.js +3 -0
- package/registry/verb-support.js.map +1 -1
- package/resources/apply/apply.d.ts.map +1 -1
- package/resources/apply/apply.js +2 -0
- package/resources/apply/apply.js.map +1 -1
- package/resources/apply/handlers.d.ts.map +1 -1
- package/resources/apply/handlers.js +11 -0
- package/resources/apply/handlers.js.map +1 -1
- package/resources/share.d.ts +5 -4
- package/resources/share.d.ts.map +1 -1
- package/resources/share.js +97 -53
- package/resources/share.js.map +1 -1
- package/src/registry/metadata.ts +2 -0
- package/src/registry/verb-support.ts +3 -0
- package/src/resources/apply/apply.ts +2 -0
- package/src/resources/apply/handlers.ts +11 -0
- package/src/resources/share.test.ts +208 -99
- package/src/resources/share.ts +124 -64
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// Unit tests for the share-agent resource layer. The critical contract is
|
|
2
|
-
// merge-preservation: `
|
|
3
|
-
// CLI toggle must never wipe console-configured origins
|
|
2
|
+
// merge-preservation: `agentShare.apply` replaces the share's spec wholesale,
|
|
3
|
+
// so a CLI toggle must never wipe console-configured origins, visitor
|
|
4
|
+
// messages, credential bindings, or the audience.
|
|
4
5
|
|
|
5
|
-
import {
|
|
6
|
+
import { create } from "@bufbuild/protobuf";
|
|
6
7
|
import { AgentSchema, type Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
7
|
-
import {
|
|
8
|
-
import type
|
|
9
|
-
import { AgentSharingAudience } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
10
|
-
import { buildEmbedSnippet, type Stigmer } from "@stigmer/sdk";
|
|
8
|
+
import { AgentShareAudience } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/spec_pb";
|
|
9
|
+
import { buildEmbedSnippet, type AgentShareInput, type Stigmer } from "@stigmer/sdk";
|
|
11
10
|
import { describe, expect, it } from "vitest";
|
|
12
11
|
import { classify, ExitCode, UsageError } from "../errors/index.js";
|
|
13
12
|
import { shareAgent } from "./share.js";
|
|
@@ -15,32 +14,55 @@ import { shareAgent } from "./share.js";
|
|
|
15
14
|
const CLOUD = { appOrigin: "https://app.stigmer.ai", isLocal: false };
|
|
16
15
|
const LOCAL = { appOrigin: "http://localhost:8234", isLocal: true };
|
|
17
16
|
|
|
18
|
-
function makeAgent(
|
|
19
|
-
sharing?: {
|
|
20
|
-
enabled?: boolean;
|
|
21
|
-
audience?: AgentSharingAudience;
|
|
22
|
-
allowedOrigins?: string[];
|
|
23
|
-
messages?: { rateLimited?: string; unavailable?: string; conversationEnded?: string };
|
|
24
|
-
},
|
|
25
|
-
shareLinkToken?: string,
|
|
26
|
-
): Agent {
|
|
17
|
+
function makeAgent(): Agent {
|
|
27
18
|
return create(AgentSchema, {
|
|
28
19
|
metadata: { id: "agt_01ARZ3NDEKTSV4RRFFQ69G5FAV", org: "acme", slug: "support-agent", name: "Support Agent" },
|
|
29
|
-
spec:
|
|
30
|
-
...(shareLinkToken !== undefined ? { status: { shareLinkToken } } : {}),
|
|
20
|
+
spec: {},
|
|
31
21
|
});
|
|
32
22
|
}
|
|
33
23
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
interface ShareFixture {
|
|
25
|
+
slug?: string;
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
audience?: AgentShareAudience;
|
|
28
|
+
allowedOrigins?: string[];
|
|
29
|
+
messages?: { rateLimited?: string; unavailable?: string; conversationEnded?: string };
|
|
30
|
+
environmentRefs?: { org: string; slug: string }[];
|
|
31
|
+
shareLinkToken?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Plain-object AgentShare fixture: the CLI reads it structurally (metadata /
|
|
35
|
+
// spec / status), so proto construction adds nothing here.
|
|
36
|
+
function makeShare(fixture: ShareFixture = {}) {
|
|
37
|
+
const { shareLinkToken, slug, ...spec } = fixture;
|
|
38
|
+
return {
|
|
39
|
+
metadata: {
|
|
40
|
+
id: "ash_01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
|
41
|
+
org: "acme",
|
|
42
|
+
slug: slug ?? "support-agent",
|
|
43
|
+
name: "Support Agent",
|
|
44
|
+
},
|
|
45
|
+
spec: {
|
|
46
|
+
agentRef: { org: "acme", slug: "support-agent" },
|
|
47
|
+
...spec,
|
|
48
|
+
},
|
|
49
|
+
...(shareLinkToken !== undefined ? { status: { shareLinkToken } } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// A minimal fake of the SDK surface shareAgent touches: agent resolution +
|
|
54
|
+
// call-recording agentShare getByAgent/apply/rotateShareLink (skill.test.ts
|
|
55
|
+
// convention). Apply answers with the applied input merged over the existing
|
|
56
|
+
// share (id assigned), mirroring the server's upsert. Rotation answers with
|
|
57
|
+
// the share re-stamped with `rotatedToken`, mirroring fresh entropy.
|
|
38
58
|
function fakeClient(
|
|
39
59
|
agent: Agent,
|
|
60
|
+
share: ReturnType<typeof makeShare> | null,
|
|
40
61
|
opts: { failWith?: Error; rotatedToken?: string } = {},
|
|
41
62
|
) {
|
|
42
|
-
const
|
|
63
|
+
const applies: AgentShareInput[] = [];
|
|
43
64
|
let rotations = 0;
|
|
65
|
+
let currentShare = share;
|
|
44
66
|
const client = {
|
|
45
67
|
agent: {
|
|
46
68
|
async get() {
|
|
@@ -49,99 +71,147 @@ function fakeClient(
|
|
|
49
71
|
async getByReference() {
|
|
50
72
|
return agent;
|
|
51
73
|
},
|
|
52
|
-
|
|
74
|
+
},
|
|
75
|
+
agentShare: {
|
|
76
|
+
async getByAgent() {
|
|
77
|
+
return {
|
|
78
|
+
totalCount: currentShare ? 1 : 0,
|
|
79
|
+
items: currentShare ? [currentShare] : [],
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
async apply(input: AgentShareInput) {
|
|
53
83
|
if (opts.failWith) throw opts.failWith;
|
|
54
|
-
|
|
55
|
-
|
|
84
|
+
applies.push(input);
|
|
85
|
+
currentShare = {
|
|
86
|
+
metadata: {
|
|
87
|
+
id: currentShare?.metadata.id ?? "ash_01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
|
88
|
+
org: input.org,
|
|
89
|
+
slug: input.slug ?? "",
|
|
90
|
+
name: input.name,
|
|
91
|
+
},
|
|
92
|
+
spec: {
|
|
93
|
+
agentRef: { org: input.agentRef.org, slug: input.agentRef.slug },
|
|
94
|
+
enabled: input.enabled ?? false,
|
|
95
|
+
audience: input.audience ?? AgentShareAudience.unspecified,
|
|
96
|
+
allowedOrigins: input.allowedOrigins ?? [],
|
|
97
|
+
messages: input.messages as never,
|
|
98
|
+
environmentRefs: input.environmentRefs ?? [],
|
|
99
|
+
},
|
|
100
|
+
...(currentShare?.status ? { status: currentShare.status } : {}),
|
|
101
|
+
} as ReturnType<typeof makeShare>;
|
|
102
|
+
return currentShare;
|
|
56
103
|
},
|
|
57
104
|
async rotateShareLink() {
|
|
58
105
|
rotations += 1;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
shareLinkToken: opts.rotatedToken ?? "fresh-token",
|
|
62
|
-
}
|
|
63
|
-
return
|
|
106
|
+
currentShare = {
|
|
107
|
+
...(currentShare as ReturnType<typeof makeShare>),
|
|
108
|
+
status: { shareLinkToken: opts.rotatedToken ?? "fresh-token" },
|
|
109
|
+
};
|
|
110
|
+
return currentShare;
|
|
64
111
|
},
|
|
65
112
|
},
|
|
66
113
|
} as unknown as Stigmer;
|
|
67
|
-
return { client,
|
|
114
|
+
return { client, applies, rotationCount: () => rotations };
|
|
68
115
|
}
|
|
69
116
|
|
|
70
117
|
describe("shareAgent merge-preservation (fails closed)", () => {
|
|
71
|
-
it("enabling preserves console-configured origins and
|
|
72
|
-
const
|
|
118
|
+
it("enabling preserves console-configured origins, messages, and bindings verbatim", async () => {
|
|
119
|
+
const share = makeShare({
|
|
73
120
|
enabled: false,
|
|
74
121
|
allowedOrigins: ["https://example.com", "https://docs.example.com"],
|
|
75
122
|
messages: { rateLimited: "Slow down!", unavailable: "Back soon.", conversationEnded: "Bye!" },
|
|
123
|
+
environmentRefs: [{ org: "acme", slug: "github-org-shared" }],
|
|
76
124
|
});
|
|
77
|
-
const { client,
|
|
125
|
+
const { client, applies } = fakeClient(makeAgent(), share);
|
|
78
126
|
|
|
79
127
|
await shareAgent(client, "acme/support-agent", "acme", { enabled: true, ...CLOUD });
|
|
80
128
|
|
|
81
|
-
expect(
|
|
82
|
-
expect(
|
|
83
|
-
expect(
|
|
84
|
-
expect(
|
|
85
|
-
expect(
|
|
86
|
-
expect(
|
|
87
|
-
expect(
|
|
129
|
+
expect(applies).toHaveLength(1);
|
|
130
|
+
expect(applies[0].org).toBe("acme");
|
|
131
|
+
expect(applies[0].slug).toBe("support-agent");
|
|
132
|
+
expect(applies[0].agentRef).toEqual({ org: "acme", slug: "support-agent" });
|
|
133
|
+
expect(applies[0].enabled).toBe(true);
|
|
134
|
+
expect(applies[0].allowedOrigins).toEqual(["https://example.com", "https://docs.example.com"]);
|
|
135
|
+
expect(applies[0].messages?.rateLimited).toBe("Slow down!");
|
|
136
|
+
expect(applies[0].messages?.unavailable).toBe("Back soon.");
|
|
137
|
+
expect(applies[0].messages?.conversationEnded).toBe("Bye!");
|
|
138
|
+
expect(applies[0].environmentRefs).toEqual([{ org: "acme", slug: "github-org-shared" }]);
|
|
88
139
|
});
|
|
89
140
|
|
|
90
141
|
it("disabling preserves origins and messages, flipping only enabled", async () => {
|
|
91
|
-
const
|
|
142
|
+
const share = makeShare({
|
|
92
143
|
enabled: true,
|
|
93
144
|
allowedOrigins: ["https://example.com"],
|
|
94
145
|
messages: { rateLimited: "Slow down!" },
|
|
95
146
|
});
|
|
96
|
-
const { client,
|
|
147
|
+
const { client, applies } = fakeClient(makeAgent(), share);
|
|
97
148
|
|
|
98
149
|
const result = await shareAgent(client, "acme/support-agent", "acme", { enabled: false, ...CLOUD });
|
|
99
150
|
|
|
100
|
-
expect(
|
|
101
|
-
expect(
|
|
102
|
-
expect(
|
|
103
|
-
expect(
|
|
151
|
+
expect(applies).toHaveLength(1);
|
|
152
|
+
expect(applies[0].enabled).toBe(false);
|
|
153
|
+
expect(applies[0].allowedOrigins).toEqual(["https://example.com"]);
|
|
154
|
+
expect(applies[0].messages?.rateLimited).toBe("Slow down!");
|
|
104
155
|
expect(result.message).toContain("Sharing disabled");
|
|
105
156
|
// No link/snippet on disable — the URL no longer works.
|
|
106
157
|
expect(result.sections).toHaveLength(0);
|
|
107
158
|
});
|
|
108
159
|
|
|
109
|
-
it("
|
|
110
|
-
const { client,
|
|
160
|
+
it("enabling a never-shared agent creates the canonical share", async () => {
|
|
161
|
+
const { client, applies } = fakeClient(makeAgent(), null);
|
|
111
162
|
|
|
112
163
|
await shareAgent(client, "acme/support-agent", "acme", { enabled: true, ...CLOUD });
|
|
113
164
|
|
|
114
|
-
expect(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
expect(
|
|
165
|
+
expect(applies).toHaveLength(1);
|
|
166
|
+
// Create identity: the agent's own org/slug (the server's default,
|
|
167
|
+
// made explicit) with the agent reference and empty config.
|
|
168
|
+
expect(applies[0].slug).toBe("support-agent");
|
|
169
|
+
expect(applies[0].agentRef).toEqual({ org: "acme", slug: "support-agent" });
|
|
170
|
+
expect(applies[0].enabled).toBe(true);
|
|
171
|
+
expect(applies[0].allowedOrigins).toEqual([]);
|
|
172
|
+
expect(applies[0].messages?.rateLimited).toBe("");
|
|
173
|
+
expect(applies[0].environmentRefs).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("edits a renamed share under its own slug (never forks a second share)", async () => {
|
|
177
|
+
const share = makeShare({ slug: "help-desk", enabled: false });
|
|
178
|
+
const { client, applies } = fakeClient(makeAgent(), share);
|
|
179
|
+
|
|
180
|
+
const result = await shareAgent(client, "acme/support-agent", "acme", { enabled: true, ...CLOUD });
|
|
181
|
+
|
|
182
|
+
expect(applies).toHaveLength(1);
|
|
183
|
+
// Applying with the agent's slug would CREATE a second share; the
|
|
184
|
+
// toggle must target the existing row's identity.
|
|
185
|
+
expect(applies[0].slug).toBe("help-desk");
|
|
186
|
+
const link = result.sections.find((s) => s.title === "Public chat link");
|
|
187
|
+
expect(link?.items).toEqual(["https://app.stigmer.ai/chat/acme/help-desk"]);
|
|
118
188
|
});
|
|
119
189
|
|
|
120
190
|
it("a plain toggle preserves an org-members-only audience (never reverts to public)", async () => {
|
|
121
|
-
const
|
|
191
|
+
const share = makeShare({
|
|
122
192
|
enabled: false,
|
|
123
|
-
audience:
|
|
193
|
+
audience: AgentShareAudience.org,
|
|
124
194
|
allowedOrigins: [],
|
|
125
195
|
});
|
|
126
|
-
const { client,
|
|
196
|
+
const { client, applies } = fakeClient(makeAgent(), share);
|
|
127
197
|
|
|
128
198
|
await shareAgent(client, "acme/support-agent", "acme", { enabled: true, ...CLOUD });
|
|
129
199
|
|
|
130
|
-
expect(
|
|
200
|
+
expect(applies).toHaveLength(1);
|
|
131
201
|
// A re-enable without --audience must keep the org restriction: writing
|
|
132
202
|
// public here would silently expose an internal agent to the internet.
|
|
133
|
-
expect(
|
|
203
|
+
expect(applies[0].audience).toBe(AgentShareAudience.org);
|
|
134
204
|
});
|
|
135
205
|
});
|
|
136
206
|
|
|
137
207
|
describe("shareAgent audience", () => {
|
|
138
|
-
it("--audience org sets the audience and
|
|
139
|
-
const
|
|
208
|
+
it("--audience org sets the audience and applies the full spec", async () => {
|
|
209
|
+
const share = makeShare({
|
|
140
210
|
enabled: true,
|
|
141
211
|
allowedOrigins: ["https://example.com"],
|
|
142
212
|
messages: { rateLimited: "Slow down!" },
|
|
143
213
|
});
|
|
144
|
-
const { client,
|
|
214
|
+
const { client, applies } = fakeClient(makeAgent(), share);
|
|
145
215
|
|
|
146
216
|
const result = await shareAgent(client, "acme/support-agent", "acme", {
|
|
147
217
|
enabled: true,
|
|
@@ -149,11 +219,11 @@ describe("shareAgent audience", () => {
|
|
|
149
219
|
...CLOUD,
|
|
150
220
|
});
|
|
151
221
|
|
|
152
|
-
expect(
|
|
153
|
-
expect(
|
|
154
|
-
expect(
|
|
155
|
-
expect(
|
|
156
|
-
expect(
|
|
222
|
+
expect(applies).toHaveLength(1);
|
|
223
|
+
expect(applies[0].audience).toBe(AgentShareAudience.org);
|
|
224
|
+
expect(applies[0].enabled).toBe(true);
|
|
225
|
+
expect(applies[0].allowedOrigins).toEqual(["https://example.com"]);
|
|
226
|
+
expect(applies[0].messages?.rateLimited).toBe("Slow down!");
|
|
157
227
|
|
|
158
228
|
// Org shares print the member link and NO embed snippet (embeds serve
|
|
159
229
|
// anonymous guests, which org shares refuse).
|
|
@@ -165,8 +235,8 @@ describe("shareAgent audience", () => {
|
|
|
165
235
|
});
|
|
166
236
|
|
|
167
237
|
it("--audience public converts an org share back to an explicit public one", async () => {
|
|
168
|
-
const
|
|
169
|
-
const { client,
|
|
238
|
+
const share = makeShare({ enabled: true, audience: AgentShareAudience.org });
|
|
239
|
+
const { client, applies } = fakeClient(makeAgent(), share);
|
|
170
240
|
|
|
171
241
|
await shareAgent(client, "acme/support-agent", "acme", {
|
|
172
242
|
enabled: true,
|
|
@@ -174,14 +244,14 @@ describe("shareAgent audience", () => {
|
|
|
174
244
|
...CLOUD,
|
|
175
245
|
});
|
|
176
246
|
|
|
177
|
-
expect(
|
|
247
|
+
expect(applies).toHaveLength(1);
|
|
178
248
|
// Written as the explicit enum value, never left unspecified.
|
|
179
|
-
expect(
|
|
249
|
+
expect(applies[0].audience).toBe(AgentShareAudience.public);
|
|
180
250
|
});
|
|
181
251
|
|
|
182
252
|
it("an audience change alone triggers a write even when enabled matches", async () => {
|
|
183
|
-
const
|
|
184
|
-
const { client,
|
|
253
|
+
const share = makeShare({ enabled: true });
|
|
254
|
+
const { client, applies } = fakeClient(makeAgent(), share);
|
|
185
255
|
|
|
186
256
|
const result = await shareAgent(client, "acme/support-agent", "acme", {
|
|
187
257
|
enabled: true,
|
|
@@ -189,13 +259,13 @@ describe("shareAgent audience", () => {
|
|
|
189
259
|
...CLOUD,
|
|
190
260
|
});
|
|
191
261
|
|
|
192
|
-
expect(
|
|
262
|
+
expect(applies).toHaveLength(1);
|
|
193
263
|
expect(result.message).toContain("Sharing enabled");
|
|
194
264
|
});
|
|
195
265
|
|
|
196
266
|
it("matching enabled AND audience stays idempotent (no write)", async () => {
|
|
197
|
-
const
|
|
198
|
-
const { client,
|
|
267
|
+
const share = makeShare({ enabled: true, audience: AgentShareAudience.org });
|
|
268
|
+
const { client, applies } = fakeClient(makeAgent(), share);
|
|
199
269
|
|
|
200
270
|
const result = await shareAgent(client, "acme/support-agent", "acme", {
|
|
201
271
|
enabled: true,
|
|
@@ -203,36 +273,47 @@ describe("shareAgent audience", () => {
|
|
|
203
273
|
...CLOUD,
|
|
204
274
|
});
|
|
205
275
|
|
|
206
|
-
expect(
|
|
276
|
+
expect(applies).toHaveLength(0);
|
|
207
277
|
expect(result.message).toContain("already on");
|
|
208
278
|
});
|
|
209
279
|
});
|
|
210
280
|
|
|
211
281
|
describe("shareAgent idempotency", () => {
|
|
212
282
|
it("skips the write when sharing is already enabled, still reporting the link", async () => {
|
|
213
|
-
const { client,
|
|
283
|
+
const { client, applies } = fakeClient(makeAgent(), makeShare({ enabled: true }));
|
|
214
284
|
|
|
215
285
|
const result = await shareAgent(client, "acme/support-agent", "acme", { enabled: true, ...CLOUD });
|
|
216
286
|
|
|
217
|
-
expect(
|
|
287
|
+
expect(applies).toHaveLength(0);
|
|
218
288
|
expect(result.message).toContain("already on");
|
|
219
289
|
const link = result.sections.find((s) => s.title === "Public chat link");
|
|
220
290
|
expect(link?.items).toEqual(["https://app.stigmer.ai/chat/acme/support-agent"]);
|
|
221
291
|
});
|
|
222
292
|
|
|
223
293
|
it("skips the write when sharing is already disabled", async () => {
|
|
224
|
-
const { client,
|
|
294
|
+
const { client, applies } = fakeClient(makeAgent(), makeShare({ enabled: false }));
|
|
295
|
+
|
|
296
|
+
const result = await shareAgent(client, "acme/support-agent", "acme", { enabled: false, ...CLOUD });
|
|
297
|
+
|
|
298
|
+
expect(applies).toHaveLength(0);
|
|
299
|
+
expect(result.message).toContain("already off");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("disabling a never-shared agent creates nothing", async () => {
|
|
303
|
+
const { client, applies } = fakeClient(makeAgent(), null);
|
|
225
304
|
|
|
226
305
|
const result = await shareAgent(client, "acme/support-agent", "acme", { enabled: false, ...CLOUD });
|
|
227
306
|
|
|
228
|
-
|
|
307
|
+
// Materializing a share row just to mark it disabled would create a
|
|
308
|
+
// resource the owner never asked for.
|
|
309
|
+
expect(applies).toHaveLength(0);
|
|
229
310
|
expect(result.message).toContain("already off");
|
|
230
311
|
});
|
|
231
312
|
});
|
|
232
313
|
|
|
233
314
|
describe("shareAgent output", () => {
|
|
234
|
-
it("builds the link and snippet from the RESOLVED
|
|
235
|
-
const { client } = fakeClient(makeAgent({ enabled: false }));
|
|
315
|
+
it("builds the link and snippet from the RESOLVED share's org/slug (ID refs included)", async () => {
|
|
316
|
+
const { client } = fakeClient(makeAgent(), makeShare({ enabled: false }));
|
|
236
317
|
|
|
237
318
|
const result = await shareAgent(client, "agt_01ARZ3NDEKTSV4RRFFQ69G5FAV", "", { enabled: true, ...CLOUD });
|
|
238
319
|
|
|
@@ -241,7 +322,7 @@ describe("shareAgent output", () => {
|
|
|
241
322
|
});
|
|
242
323
|
|
|
243
324
|
it("emits the exact embed snippet the web share dialog emits", async () => {
|
|
244
|
-
const { client } = fakeClient(makeAgent({ enabled: false }));
|
|
325
|
+
const { client } = fakeClient(makeAgent(), makeShare({ enabled: false }));
|
|
245
326
|
|
|
246
327
|
const result = await shareAgent(client, "acme/support-agent", "acme", { enabled: true, ...CLOUD });
|
|
247
328
|
|
|
@@ -250,7 +331,7 @@ describe("shareAgent output", () => {
|
|
|
250
331
|
});
|
|
251
332
|
|
|
252
333
|
it("includes the who-pays and indexability hints", async () => {
|
|
253
|
-
const { client } = fakeClient(makeAgent({ enabled: false }));
|
|
334
|
+
const { client } = fakeClient(makeAgent(), makeShare({ enabled: false }));
|
|
254
335
|
|
|
255
336
|
const result = await shareAgent(client, "acme/support-agent", "acme", { enabled: true, ...CLOUD });
|
|
256
337
|
|
|
@@ -259,7 +340,7 @@ describe("shareAgent output", () => {
|
|
|
259
340
|
});
|
|
260
341
|
|
|
261
342
|
it("warns that a local link won't serve visitors (guest chat is Cloud-only)", async () => {
|
|
262
|
-
const { client } = fakeClient(makeAgent({ enabled: false }));
|
|
343
|
+
const { client } = fakeClient(makeAgent(), makeShare({ enabled: false }));
|
|
263
344
|
|
|
264
345
|
const result = await shareAgent(client, "acme/support-agent", "acme", { enabled: true, ...LOCAL });
|
|
265
346
|
|
|
@@ -269,7 +350,7 @@ describe("shareAgent output", () => {
|
|
|
269
350
|
});
|
|
270
351
|
|
|
271
352
|
it("omits the local warning on a cloud backend", async () => {
|
|
272
|
-
const { client } = fakeClient(makeAgent({ enabled: false }));
|
|
353
|
+
const { client } = fakeClient(makeAgent(), makeShare({ enabled: false }));
|
|
273
354
|
|
|
274
355
|
const result = await shareAgent(client, "acme/support-agent", "acme", { enabled: true, ...CLOUD });
|
|
275
356
|
|
|
@@ -279,8 +360,9 @@ describe("shareAgent output", () => {
|
|
|
279
360
|
|
|
280
361
|
describe("shareAgent --reset-link (rotatable share token)", () => {
|
|
281
362
|
it("rotates and prints the fresh tokened link and snippet", async () => {
|
|
282
|
-
const { client,
|
|
283
|
-
makeAgent(
|
|
363
|
+
const { client, applies, rotationCount } = fakeClient(
|
|
364
|
+
makeAgent(),
|
|
365
|
+
makeShare({ enabled: true }),
|
|
284
366
|
{ rotatedToken: "fresh-token" },
|
|
285
367
|
);
|
|
286
368
|
|
|
@@ -290,8 +372,8 @@ describe("shareAgent --reset-link (rotatable share token)", () => {
|
|
|
290
372
|
...CLOUD,
|
|
291
373
|
});
|
|
292
374
|
|
|
293
|
-
// Already enabled: no
|
|
294
|
-
expect(
|
|
375
|
+
// Already enabled: no spec write, exactly one rotation.
|
|
376
|
+
expect(applies).toHaveLength(0);
|
|
295
377
|
expect(rotationCount()).toBe(1);
|
|
296
378
|
expect(result.message).toContain("Share link reset");
|
|
297
379
|
|
|
@@ -306,9 +388,30 @@ describe("shareAgent --reset-link (rotatable share token)", () => {
|
|
|
306
388
|
expect(result.hints.some((h) => h.includes("Re-share the new link"))).toBe(true);
|
|
307
389
|
});
|
|
308
390
|
|
|
391
|
+
it("enable + reset on a never-shared agent creates the share, then rotates it", async () => {
|
|
392
|
+
const { client, applies, rotationCount } = fakeClient(makeAgent(), null, {
|
|
393
|
+
rotatedToken: "fresh-token",
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const result = await shareAgent(client, "acme/support-agent", "acme", {
|
|
397
|
+
enabled: true,
|
|
398
|
+
resetLink: true,
|
|
399
|
+
...CLOUD,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// The rotation needs a share id, which only exists after the create.
|
|
403
|
+
expect(applies).toHaveLength(1);
|
|
404
|
+
expect(rotationCount()).toBe(1);
|
|
405
|
+
const link = result.sections.find((s) => s.title === "Public chat link");
|
|
406
|
+
expect(link?.items).toEqual([
|
|
407
|
+
"https://app.stigmer.ai/chat/acme/support-agent?k=fresh-token",
|
|
408
|
+
]);
|
|
409
|
+
});
|
|
410
|
+
|
|
309
411
|
it("a plain toggle on a locked link keeps printing the existing tokened URL", async () => {
|
|
310
412
|
const { client, rotationCount } = fakeClient(
|
|
311
|
-
makeAgent(
|
|
413
|
+
makeAgent(),
|
|
414
|
+
makeShare({ enabled: true, shareLinkToken: "existing-token" }),
|
|
312
415
|
);
|
|
313
416
|
|
|
314
417
|
const result = await shareAgent(client, "acme/support-agent", "acme", {
|
|
@@ -327,7 +430,8 @@ describe("shareAgent --reset-link (rotatable share token)", () => {
|
|
|
327
430
|
|
|
328
431
|
it("refuses --reset-link for an org-members-only share with actionable guidance", async () => {
|
|
329
432
|
const { client, rotationCount } = fakeClient(
|
|
330
|
-
makeAgent(
|
|
433
|
+
makeAgent(),
|
|
434
|
+
makeShare({ enabled: true, audience: AgentShareAudience.org }),
|
|
331
435
|
);
|
|
332
436
|
|
|
333
437
|
const err = await shareAgent(client, "acme/support-agent", "acme", {
|
|
@@ -345,7 +449,12 @@ describe("shareAgent --reset-link (rotatable share token)", () => {
|
|
|
345
449
|
|
|
346
450
|
it("the member link never carries a token, even when one exists in status", async () => {
|
|
347
451
|
const { client } = fakeClient(
|
|
348
|
-
makeAgent(
|
|
452
|
+
makeAgent(),
|
|
453
|
+
makeShare({
|
|
454
|
+
enabled: true,
|
|
455
|
+
audience: AgentShareAudience.org,
|
|
456
|
+
shareLinkToken: "existing-token",
|
|
457
|
+
}),
|
|
349
458
|
);
|
|
350
459
|
|
|
351
460
|
const result = await shareAgent(client, "acme/support-agent", "acme", {
|
|
@@ -360,27 +469,27 @@ describe("shareAgent --reset-link (rotatable share token)", () => {
|
|
|
360
469
|
|
|
361
470
|
describe("shareAgent errors", () => {
|
|
362
471
|
it("fails fast with org guidance for a bare slug and no org", async () => {
|
|
363
|
-
const { client,
|
|
472
|
+
const { client, applies } = fakeClient(makeAgent(), null);
|
|
364
473
|
|
|
365
474
|
const err = await shareAgent(client, "support-agent", "", { enabled: true, ...CLOUD }).catch((e: unknown) => e);
|
|
366
475
|
|
|
367
476
|
expect(err).toBeInstanceOf(UsageError);
|
|
368
477
|
expect((err as Error).message).toContain("organization not set");
|
|
369
478
|
expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
|
|
370
|
-
expect(
|
|
479
|
+
expect(applies).toHaveLength(0);
|
|
371
480
|
});
|
|
372
481
|
|
|
373
482
|
it("does not apply the org guard to an ID reference", async () => {
|
|
374
|
-
const { client,
|
|
483
|
+
const { client, applies } = fakeClient(makeAgent(), makeShare({ enabled: false }));
|
|
375
484
|
|
|
376
485
|
await shareAgent(client, "agt_01ARZ3NDEKTSV4RRFFQ69G5FAV", "", { enabled: true, ...CLOUD });
|
|
377
486
|
|
|
378
|
-
expect(
|
|
487
|
+
expect(applies).toHaveLength(1);
|
|
379
488
|
});
|
|
380
489
|
|
|
381
490
|
it("propagates server rejections (permission denied) unchanged", async () => {
|
|
382
491
|
const denied = new Error("permission denied: can_edit required");
|
|
383
|
-
const { client } = fakeClient(makeAgent({ enabled: false }), { failWith: denied });
|
|
492
|
+
const { client } = fakeClient(makeAgent(), makeShare({ enabled: false }), { failWith: denied });
|
|
384
493
|
|
|
385
494
|
await expect(shareAgent(client, "acme/support-agent", "acme", { enabled: true, ...CLOUD })).rejects.toThrow(
|
|
386
495
|
/permission denied/,
|