@frockbot/plugin-bot-template 0.0.0 → 0.1.1
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/frockbot.json +43 -0
- package/package.json +47 -6
- package/src/agent.test.ts +194 -0
- package/src/agent.ts +221 -0
- package/src/backend.test.ts +326 -0
- package/src/backend.ts +262 -0
- package/src/client/BotTemplateImportSection.vue +325 -0
- package/src/client/BotTemplateSection.vue +338 -0
- package/src/client/index.ts +210 -0
- package/src/client/state.ts +43 -0
- package/src/env.d.ts +6 -0
- package/src/import-apply.test.ts +455 -0
- package/src/import.test.ts +266 -0
- package/src/import.ts +292 -0
- package/src/index.ts +9 -0
- package/src/manifest.ts +3 -0
- package/src/scrub.test.ts +528 -0
- package/src/scrub.ts +523 -0
- package/src/shared.ts +620 -0
- package/src/user.test.ts +361 -0
- package/src/user.ts +913 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +31 -0
- package/README.md +0 -3
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
// The scrub matrix, row by row.
|
|
2
|
+
//
|
|
3
|
+
// Each `it` below is one row of the table in the slice plan, and the assertion
|
|
4
|
+
// is the whole contract for that row: what reaches the template, and what the
|
|
5
|
+
// summary says was left behind. The suite also carries the two negative
|
|
6
|
+
// contracts from register line 326 — a Skill whose body failed to load is
|
|
7
|
+
// dropped rather than re-read, and the scrub never touches the source.
|
|
8
|
+
import { describe, expect, it } from "bun:test";
|
|
9
|
+
import {
|
|
10
|
+
buildBotTemplateV1,
|
|
11
|
+
describeTemplateSummaryV1,
|
|
12
|
+
shareableServerUrlV1,
|
|
13
|
+
templateSlugV1,
|
|
14
|
+
type TemplateSourceV1,
|
|
15
|
+
} from "./scrub.ts";
|
|
16
|
+
import type { TemplateOmissionReasonV1 } from "./shared.ts";
|
|
17
|
+
|
|
18
|
+
const sheep = {
|
|
19
|
+
schemaVersion: 1 as const,
|
|
20
|
+
background: "meadow",
|
|
21
|
+
upper: "wool",
|
|
22
|
+
middle: "scarf",
|
|
23
|
+
lower: "boots",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function source(overrides: Partial<TemplateSourceV1> = {}): TemplateSourceV1 {
|
|
27
|
+
return {
|
|
28
|
+
botId: "budget",
|
|
29
|
+
profile: { name: "Budget", title: "Money minder" },
|
|
30
|
+
sheep,
|
|
31
|
+
skills: [],
|
|
32
|
+
routines: [],
|
|
33
|
+
packages: [],
|
|
34
|
+
connections: [],
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function omitted(
|
|
40
|
+
summary: { omitted: { reason: TemplateOmissionReasonV1; count: number }[] },
|
|
41
|
+
reason: TemplateOmissionReasonV1,
|
|
42
|
+
): number {
|
|
43
|
+
return summary.omitted.find((entry) => entry.reason === reason)?.count ?? 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("skills", () => {
|
|
47
|
+
it("carries an own-root Skill body verbatim", () => {
|
|
48
|
+
const { template } = buildBotTemplateV1(
|
|
49
|
+
source({
|
|
50
|
+
skills: [
|
|
51
|
+
{
|
|
52
|
+
source: "bot",
|
|
53
|
+
slug: "reconcile",
|
|
54
|
+
name: "Reconcile",
|
|
55
|
+
description: "Match the ledger",
|
|
56
|
+
body: "# Reconcile\nOpen the ledger.",
|
|
57
|
+
writer: { kind: "bot" },
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
expect(template.skills).toEqual([
|
|
63
|
+
{
|
|
64
|
+
slug: "reconcile",
|
|
65
|
+
name: "Reconcile",
|
|
66
|
+
description: "Match the ledger",
|
|
67
|
+
body: "# Reconcile\nOpen the ledger.",
|
|
68
|
+
},
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("carries an own-root Skill its User wrote", () => {
|
|
73
|
+
const { template } = buildBotTemplateV1(
|
|
74
|
+
source({
|
|
75
|
+
skills: [
|
|
76
|
+
{
|
|
77
|
+
source: "bot",
|
|
78
|
+
slug: "notes",
|
|
79
|
+
name: "Notes",
|
|
80
|
+
body: "body",
|
|
81
|
+
writer: { kind: "user" },
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
expect(template.skills).toHaveLength(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("omits a managed Skill", () => {
|
|
90
|
+
const { template, summary } = buildBotTemplateV1(
|
|
91
|
+
source({
|
|
92
|
+
skills: [
|
|
93
|
+
{
|
|
94
|
+
source: "managed",
|
|
95
|
+
slug: "managed-one",
|
|
96
|
+
name: "Managed",
|
|
97
|
+
body: "body",
|
|
98
|
+
writer: { kind: "first-party" },
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
expect(template.skills).toEqual([]);
|
|
104
|
+
expect(omitted(summary, "managed-skill")).toBe(1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("omits a plugin-borne Skill", () => {
|
|
108
|
+
const { template, summary } = buildBotTemplateV1(
|
|
109
|
+
source({
|
|
110
|
+
skills: [
|
|
111
|
+
{
|
|
112
|
+
source: "plugin",
|
|
113
|
+
slug: "plugin-one",
|
|
114
|
+
name: "Plugin",
|
|
115
|
+
body: "body",
|
|
116
|
+
writer: { kind: "first-party" },
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
expect(template.skills).toEqual([]);
|
|
122
|
+
expect(omitted(summary, "plugin-skill")).toBe(1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("omits an own-root Skill with no recorded writer", () => {
|
|
126
|
+
const { template, summary } = buildBotTemplateV1(
|
|
127
|
+
source({
|
|
128
|
+
skills: [
|
|
129
|
+
{
|
|
130
|
+
source: "bot",
|
|
131
|
+
slug: "shell-written",
|
|
132
|
+
name: "Shell",
|
|
133
|
+
body: "body",
|
|
134
|
+
writer: { kind: "unattributed" },
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
expect(template.skills).toEqual([]);
|
|
140
|
+
expect(omitted(summary, "unattributed-skill")).toBe(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("drops a Skill whose body failed to load and never re-reads it", () => {
|
|
144
|
+
const input = source({
|
|
145
|
+
skills: [
|
|
146
|
+
{
|
|
147
|
+
source: "bot",
|
|
148
|
+
slug: "broken",
|
|
149
|
+
name: "Broken",
|
|
150
|
+
writer: { kind: "bot" },
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
source: "bot",
|
|
154
|
+
slug: "good",
|
|
155
|
+
name: "Good",
|
|
156
|
+
body: "body",
|
|
157
|
+
writer: { kind: "bot" },
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
const { template, summary } = buildBotTemplateV1(input);
|
|
162
|
+
expect(template.skills.map((skill) => skill.slug)).toEqual(["good"]);
|
|
163
|
+
expect(omitted(summary, "unreadable-skill")).toBe(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("scrubs while building the pack, never by editing the Bot", () => {
|
|
167
|
+
const input = source({
|
|
168
|
+
profile: { name: "Budget" },
|
|
169
|
+
skills: [
|
|
170
|
+
{
|
|
171
|
+
source: "managed",
|
|
172
|
+
slug: "managed-one",
|
|
173
|
+
name: "Managed",
|
|
174
|
+
body: "body",
|
|
175
|
+
writer: { kind: "first-party" },
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
packages: [
|
|
179
|
+
{
|
|
180
|
+
packageId: "mcp",
|
|
181
|
+
version: "0.0.1",
|
|
182
|
+
state: "installed",
|
|
183
|
+
values: { apiKey: "sk-live-do-not-share" },
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
});
|
|
187
|
+
const before = structuredClone(input);
|
|
188
|
+
buildBotTemplateV1(input);
|
|
189
|
+
expect(input).toEqual(before);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("gives two Skills with the same derived slug distinct slugs", () => {
|
|
193
|
+
const { template } = buildBotTemplateV1(
|
|
194
|
+
source({
|
|
195
|
+
skills: [
|
|
196
|
+
{
|
|
197
|
+
source: "bot",
|
|
198
|
+
name: "Daily note",
|
|
199
|
+
body: "a",
|
|
200
|
+
writer: { kind: "bot" },
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
source: "bot",
|
|
204
|
+
name: "Daily note",
|
|
205
|
+
body: "b",
|
|
206
|
+
writer: { kind: "bot" },
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
expect(template.skills.map((skill) => skill.slug)).toEqual([
|
|
212
|
+
"daily-note",
|
|
213
|
+
"daily-note-2",
|
|
214
|
+
]);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("routines", () => {
|
|
219
|
+
it("carries a cron Routine with its schedule and time zone", () => {
|
|
220
|
+
const { template } = buildBotTemplateV1(
|
|
221
|
+
source({
|
|
222
|
+
routines: [
|
|
223
|
+
{
|
|
224
|
+
routineId: "r-1",
|
|
225
|
+
name: "Morning ledger",
|
|
226
|
+
prompt: "Reconcile yesterday.",
|
|
227
|
+
schedule: "0 9 * * *",
|
|
228
|
+
timezone: "Australia/Sydney",
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
expect(template.routines).toEqual([
|
|
234
|
+
{
|
|
235
|
+
slug: "morning-ledger",
|
|
236
|
+
name: "Morning ledger",
|
|
237
|
+
prompt: "Reconcile yesterday.",
|
|
238
|
+
schedule: "0 9 * * *",
|
|
239
|
+
timezone: "Australia/Sydney",
|
|
240
|
+
triggerKind: "cron",
|
|
241
|
+
},
|
|
242
|
+
]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("carries a webhook Routine as a trigger kind and no key or digest", () => {
|
|
246
|
+
const { template } = buildBotTemplateV1(
|
|
247
|
+
source({
|
|
248
|
+
routines: [
|
|
249
|
+
{
|
|
250
|
+
routineId: "r-2",
|
|
251
|
+
name: "On delivery",
|
|
252
|
+
prompt: "Handle the payload.",
|
|
253
|
+
trigger: { kind: "webhook" },
|
|
254
|
+
timezone: "UTC",
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
expect(template.routines).toEqual([
|
|
260
|
+
{
|
|
261
|
+
slug: "on-delivery",
|
|
262
|
+
name: "On delivery",
|
|
263
|
+
prompt: "Handle the payload.",
|
|
264
|
+
timezone: "UTC",
|
|
265
|
+
triggerKind: "webhook",
|
|
266
|
+
},
|
|
267
|
+
]);
|
|
268
|
+
const document = JSON.stringify(template);
|
|
269
|
+
expect(document).not.toContain("key");
|
|
270
|
+
expect(document).not.toContain("digest");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("packages", () => {
|
|
275
|
+
it("carries an installed Catalog Package by its Catalog identity", () => {
|
|
276
|
+
const { template } = buildBotTemplateV1(
|
|
277
|
+
source({
|
|
278
|
+
packages: [
|
|
279
|
+
{
|
|
280
|
+
packageId: "mcp",
|
|
281
|
+
version: "0.0.2",
|
|
282
|
+
state: "installed",
|
|
283
|
+
catalogId: "example-connector",
|
|
284
|
+
catalogGeneration: "gen-7",
|
|
285
|
+
provenance: "catalog",
|
|
286
|
+
displayName: "Example connector",
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
expect(template.packages).toEqual([
|
|
292
|
+
{
|
|
293
|
+
packageId: "mcp",
|
|
294
|
+
catalogId: "example-connector",
|
|
295
|
+
version: "0.0.2",
|
|
296
|
+
displayName: "Example connector",
|
|
297
|
+
},
|
|
298
|
+
]);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("omits a first-party Package and counts it as built in", () => {
|
|
302
|
+
const { template, summary } = buildBotTemplateV1(
|
|
303
|
+
source({
|
|
304
|
+
packages: [
|
|
305
|
+
{
|
|
306
|
+
packageId: "clock",
|
|
307
|
+
version: "0.0.1",
|
|
308
|
+
state: "installed",
|
|
309
|
+
provenance: "first-party",
|
|
310
|
+
},
|
|
311
|
+
{ packageId: "echo", version: "0.0.1", state: "installed" },
|
|
312
|
+
],
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
315
|
+
expect(template.packages).toEqual([]);
|
|
316
|
+
expect(omitted(summary, "first-party-package")).toBe(2);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("omits setup values entirely and records that it did", () => {
|
|
320
|
+
const { template, summary } = buildBotTemplateV1(
|
|
321
|
+
source({
|
|
322
|
+
packages: [
|
|
323
|
+
{
|
|
324
|
+
packageId: "mcp",
|
|
325
|
+
version: "0.0.1",
|
|
326
|
+
state: "installed",
|
|
327
|
+
catalogId: "example-connector",
|
|
328
|
+
provenance: "catalog",
|
|
329
|
+
values: { apiKey: "sk-live-do-not-share" },
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
expect(JSON.stringify(template)).not.toContain("sk-live");
|
|
335
|
+
expect(Object.keys(template.packages[0]!)).toEqual([
|
|
336
|
+
"packageId",
|
|
337
|
+
"catalogId",
|
|
338
|
+
"version",
|
|
339
|
+
"displayName",
|
|
340
|
+
]);
|
|
341
|
+
expect(omitted(summary, "package-values")).toBe(1);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("does not carry a Package that is disabled or failed", () => {
|
|
345
|
+
const { template } = buildBotTemplateV1(
|
|
346
|
+
source({
|
|
347
|
+
packages: [
|
|
348
|
+
{
|
|
349
|
+
packageId: "mcp",
|
|
350
|
+
version: "0.0.1",
|
|
351
|
+
state: "disabled",
|
|
352
|
+
catalogId: "example-connector",
|
|
353
|
+
provenance: "catalog",
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
expect(template.packages).toEqual([]);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe("MCP servers and Connections", () => {
|
|
363
|
+
const publicServer = {
|
|
364
|
+
packageId: "mcp",
|
|
365
|
+
connectionTypeId: "mcp-remote",
|
|
366
|
+
displayName: "Example",
|
|
367
|
+
state: "ready",
|
|
368
|
+
keyed: false,
|
|
369
|
+
settings: { url: "https://mcp.example.test/mcp", transport: "sse" },
|
|
370
|
+
} as const;
|
|
371
|
+
|
|
372
|
+
const keyedServer = {
|
|
373
|
+
packageId: "mcp",
|
|
374
|
+
connectionTypeId: "mcp-remote-key",
|
|
375
|
+
displayName: "Beeper",
|
|
376
|
+
state: "ready",
|
|
377
|
+
keyed: true,
|
|
378
|
+
settings: { url: "https://beeper.example.test/mcp" },
|
|
379
|
+
} as const;
|
|
380
|
+
|
|
381
|
+
it("carries a public server as its url and transport", () => {
|
|
382
|
+
const { template } = buildBotTemplateV1(
|
|
383
|
+
source({ connections: [publicServer] }),
|
|
384
|
+
);
|
|
385
|
+
expect(template.mcpServers).toEqual([
|
|
386
|
+
{
|
|
387
|
+
kind: "public",
|
|
388
|
+
name: "Example",
|
|
389
|
+
url: "https://mcp.example.test/mcp",
|
|
390
|
+
transport: "sse",
|
|
391
|
+
},
|
|
392
|
+
]);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("carries a keyed server as a placeholder with no url and no key", () => {
|
|
396
|
+
const { template, summary } = buildBotTemplateV1(
|
|
397
|
+
source({ connections: [keyedServer] }),
|
|
398
|
+
);
|
|
399
|
+
expect(template.mcpServers).toEqual([
|
|
400
|
+
{
|
|
401
|
+
kind: "needs-connection",
|
|
402
|
+
name: "Beeper",
|
|
403
|
+
connectionTypeId: "mcp-remote-key",
|
|
404
|
+
hint: "This server needs your own Connection and credential.",
|
|
405
|
+
},
|
|
406
|
+
]);
|
|
407
|
+
expect(JSON.stringify(template)).not.toContain("beeper.example.test");
|
|
408
|
+
expect(summary.needsConnection).toBe(1);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("never carries a url for a private-network server", () => {
|
|
412
|
+
const { template, summary } = buildBotTemplateV1(
|
|
413
|
+
source({
|
|
414
|
+
connections: [
|
|
415
|
+
{
|
|
416
|
+
packageId: "mcp",
|
|
417
|
+
connectionTypeId: "mcp-remote",
|
|
418
|
+
displayName: "Local",
|
|
419
|
+
state: "ready",
|
|
420
|
+
keyed: false,
|
|
421
|
+
settings: { url: "https://10.1.2.3/mcp" },
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
}),
|
|
425
|
+
);
|
|
426
|
+
expect(template.mcpServers[0]).toMatchObject({ kind: "needs-connection" });
|
|
427
|
+
expect(JSON.stringify(template)).not.toContain("10.1.2.3");
|
|
428
|
+
expect(omitted(summary, "private-network-server")).toBe(1);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("never carries a connectionId, safeMetadata or an Assignment", () => {
|
|
432
|
+
const { template, summary } = buildBotTemplateV1(
|
|
433
|
+
source({
|
|
434
|
+
connections: [publicServer, keyedServer],
|
|
435
|
+
assignmentCount: 3,
|
|
436
|
+
hasModelAssignment: true,
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
const document = JSON.stringify(template);
|
|
440
|
+
expect(document).not.toContain("connectionId");
|
|
441
|
+
expect(document).not.toContain("safeMetadata");
|
|
442
|
+
expect(document).not.toContain("assignment");
|
|
443
|
+
expect(omitted(summary, "connection")).toBe(2);
|
|
444
|
+
expect(omitted(summary, "assignment")).toBe(3);
|
|
445
|
+
expect(omitted(summary, "model")).toBe(1);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("omits a Connection that names no server endpoint", () => {
|
|
449
|
+
const { template, summary } = buildBotTemplateV1(
|
|
450
|
+
source({
|
|
451
|
+
connections: [
|
|
452
|
+
{
|
|
453
|
+
packageId: "provider-ollama-cloud",
|
|
454
|
+
connectionTypeId: "ollama-cloud-account",
|
|
455
|
+
displayName: "Work",
|
|
456
|
+
state: "ready",
|
|
457
|
+
keyed: true,
|
|
458
|
+
},
|
|
459
|
+
],
|
|
460
|
+
}),
|
|
461
|
+
);
|
|
462
|
+
expect(template.mcpServers).toEqual([]);
|
|
463
|
+
expect(omitted(summary, "connection")).toBe(1);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("skips a Connection that is not ready", () => {
|
|
467
|
+
const { template } = buildBotTemplateV1(
|
|
468
|
+
source({
|
|
469
|
+
connections: [{ ...publicServer, state: "failed" }],
|
|
470
|
+
}),
|
|
471
|
+
);
|
|
472
|
+
expect(template.mcpServers).toEqual([]);
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
describe("profile avatar and Memory", () => {
|
|
477
|
+
it("exports the Bot's sheep avatar", () => {
|
|
478
|
+
const { template } = buildBotTemplateV1(source());
|
|
479
|
+
expect(template.profile.avatar).toEqual({ kind: "sheep", recipe: sheep });
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("records that Memory is never exported", () => {
|
|
483
|
+
const { template, summary } = buildBotTemplateV1(source());
|
|
484
|
+
expect(Object.keys(template)).not.toContain("memory");
|
|
485
|
+
expect(omitted(summary, "memory")).toBe(1);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
describe("helpers", () => {
|
|
490
|
+
it("refuses a non-https or private server url", () => {
|
|
491
|
+
expect(shareableServerUrlV1("https://mcp.example.test/mcp")).toBe(
|
|
492
|
+
"https://mcp.example.test/mcp",
|
|
493
|
+
);
|
|
494
|
+
expect(shareableServerUrlV1("http://mcp.example.test/mcp")).toBeUndefined();
|
|
495
|
+
expect(shareableServerUrlV1("https://localhost/mcp")).toBeUndefined();
|
|
496
|
+
expect(shareableServerUrlV1("https://192.168.0.4/mcp")).toBeUndefined();
|
|
497
|
+
expect(shareableServerUrlV1("https://[::1]/mcp")).toBeUndefined();
|
|
498
|
+
expect(
|
|
499
|
+
shareableServerUrlV1("https://a:b@mcp.example.test/"),
|
|
500
|
+
).toBeUndefined();
|
|
501
|
+
expect(shareableServerUrlV1(42)).toBeUndefined();
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("falls back when a name slugifies to nothing", () => {
|
|
505
|
+
expect(templateSlugV1(" ", "routine")).toBe("routine");
|
|
506
|
+
expect(templateSlugV1("Morning Ledger!", "routine")).toBe("morning-ledger");
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("describes what was packed and what was scrubbed", () => {
|
|
510
|
+
const { summary } = buildBotTemplateV1(
|
|
511
|
+
source({
|
|
512
|
+
skills: [
|
|
513
|
+
{
|
|
514
|
+
source: "bot",
|
|
515
|
+
slug: "a",
|
|
516
|
+
name: "A",
|
|
517
|
+
body: "b",
|
|
518
|
+
writer: { kind: "bot" },
|
|
519
|
+
},
|
|
520
|
+
],
|
|
521
|
+
}),
|
|
522
|
+
);
|
|
523
|
+
const description = describeTemplateSummaryV1(summary);
|
|
524
|
+
expect(description).toContain("1 Skill");
|
|
525
|
+
expect(description).toContain("Memory");
|
|
526
|
+
expect(description).toContain("Nothing is shared until you choose");
|
|
527
|
+
});
|
|
528
|
+
});
|