@desplega.ai/agent-swarm 1.78.1 → 1.79.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/README.md +1 -0
- package/openapi.json +1335 -236
- package/package.json +4 -4
- package/plugin/skills/artifacts/SKILL.md +151 -0
- package/plugin/skills/artifacts/examples/static-report.sh +1 -1
- package/plugin/skills/kv-storage/SKILL.md +168 -0
- package/plugin/skills/pages/SKILL.md +423 -0
- package/src/artifact-sdk/browser-sdk.ts +396 -19
- package/src/be/db.ts +548 -0
- package/src/be/migrations/059_pages.sql +34 -0
- package/src/be/migrations/060_page_versions.sql +19 -0
- package/src/be/migrations/061_kv_store.sql +34 -0
- package/src/be/migrations/062_pages_view_count.sql +9 -0
- package/src/commands/artifact.ts +17 -11
- package/src/commands/provider-credentials.ts +1 -1
- package/src/http/index.ts +9 -1
- package/src/http/kv.ts +658 -0
- package/src/http/page-proxy.ts +213 -0
- package/src/http/pages-public.ts +507 -0
- package/src/http/pages.ts +608 -0
- package/src/http/status.ts +1 -1
- package/src/http/utils.ts +68 -5
- package/src/pages/version.ts +44 -0
- package/src/prompts/session-templates.ts +51 -0
- package/src/providers/pi-mono-adapter.ts +3 -3
- package/src/providers/pi-mono-extension.ts +1 -1
- package/src/server.ts +29 -1
- package/src/tasks/context-key.ts +28 -0
- package/src/telemetry.ts +65 -1
- package/src/tests/artifact-commands.test.ts +92 -0
- package/src/tests/artifact-sdk.test.ts +80 -74
- package/src/tests/context-key.test.ts +17 -0
- package/src/tests/create-page-tool.test.ts +197 -0
- package/src/tests/fixtures/sample-json-page.json +52 -0
- package/src/tests/kv-http.test.ts +331 -0
- package/src/tests/kv-namespace-resolution.test.ts +172 -0
- package/src/tests/kv-page-proxy.test.ts +212 -0
- package/src/tests/kv-storage.test.ts +227 -0
- package/src/tests/kv-tool.test.ts +217 -0
- package/src/tests/launch-password-rejection.test.ts +139 -0
- package/src/tests/page-proxy-authed.test.ts +146 -0
- package/src/tests/page-proxy.test.ts +270 -0
- package/src/tests/page-session.test.ts +169 -0
- package/src/tests/pages-actions-endpoint.test.ts +102 -0
- package/src/tests/pages-authed-mode.test.ts +211 -0
- package/src/tests/pages-http.test.ts +193 -0
- package/src/tests/pages-list-endpoint.test.ts +149 -0
- package/src/tests/pages-password-hash.test.ts +57 -0
- package/src/tests/pages-password-mode.test.ts +265 -0
- package/src/tests/pages-public-authed-401.test.ts +102 -0
- package/src/tests/pages-public-html.test.ts +151 -0
- package/src/tests/pages-public-json-redirect.test.ts +86 -0
- package/src/tests/pages-storage.test.ts +196 -0
- package/src/tests/pages-versioning.test.ts +231 -0
- package/src/tests/pages-view-count.test.ts +220 -0
- package/src/tests/prompt-template-session.test.ts +3 -2
- package/src/tests/skill-update-scope.test.ts +165 -0
- package/src/tests/swarm-diff.test.ts +303 -0
- package/src/tests/telemetry-init.test.ts +149 -0
- package/src/tests/workflow-wait-event.test.ts +4 -7
- package/src/tools/create-page.ts +263 -0
- package/src/tools/kv/index.ts +5 -0
- package/src/tools/kv/kv-delete.ts +89 -0
- package/src/tools/kv/kv-get.ts +64 -0
- package/src/tools/kv/kv-incr.ts +116 -0
- package/src/tools/kv/kv-list.ts +81 -0
- package/src/tools/kv/kv-set.ts +194 -0
- package/src/tools/kv/resolve-namespace.ts +58 -0
- package/src/tools/skills/skill-update.ts +26 -0
- package/src/tools/tool-config.ts +10 -0
- package/src/types.ts +107 -0
- package/src/utils/internal-ai/complete-structured.ts +2 -2
- package/src/utils/internal-ai/credentials.ts +3 -3
- package/src/utils/page-session.ts +254 -0
- package/plugin/skills/artifacts/skill.md +0 -70
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies the per-page view counter:
|
|
3
|
+
* - `view_count` bumps on every 200 from `GET /p/:id` and `GET /p/:id.json`
|
|
4
|
+
* - 401/403/404 responses do NOT bump
|
|
5
|
+
* - Bumps survive across requests (writes are committed)
|
|
6
|
+
* - Counter surfaces on `GET /api/pages` listing and `GET /api/pages/:id`
|
|
7
|
+
*
|
|
8
|
+
* No dedup by viewer — that's the explicit design (per Taras: "super simple
|
|
9
|
+
* counter field, that's it"). If someone wants unique views later, that's a
|
|
10
|
+
* follow-up.
|
|
11
|
+
*/
|
|
12
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
import { unlink } from "node:fs/promises";
|
|
15
|
+
import {
|
|
16
|
+
createServer as createHttpServer,
|
|
17
|
+
type IncomingMessage,
|
|
18
|
+
type Server,
|
|
19
|
+
type ServerResponse,
|
|
20
|
+
} from "node:http";
|
|
21
|
+
import { closeDb, initDb } from "../be/db";
|
|
22
|
+
import { handlePages } from "../http/pages";
|
|
23
|
+
import { handlePagesPublic } from "../http/pages-public";
|
|
24
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
25
|
+
|
|
26
|
+
const TEST_DB_PATH = "./test-pages-view-count.sqlite";
|
|
27
|
+
const TEST_PORT = 13095;
|
|
28
|
+
const BASE = `http://localhost:${TEST_PORT}`;
|
|
29
|
+
|
|
30
|
+
function createTestServer(): Server {
|
|
31
|
+
return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
32
|
+
const pathSegments = getPathSegments(req.url || "");
|
|
33
|
+
const queryParams = parseQueryParams(req.url || "");
|
|
34
|
+
const myAgentId = req.headers["x-agent-id"] as string | undefined;
|
|
35
|
+
if (await handlePagesPublic(req, res, pathSegments, queryParams)) return;
|
|
36
|
+
if (await handlePages(req, res, pathSegments, queryParams, myAgentId)) return;
|
|
37
|
+
res.writeHead(404);
|
|
38
|
+
res.end("not found");
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function getViewCount(id: string, agentId: string): Promise<number> {
|
|
43
|
+
const res = await fetch(`${BASE}/api/pages/${id}`, {
|
|
44
|
+
headers: { "X-Agent-ID": agentId },
|
|
45
|
+
});
|
|
46
|
+
expect(res.status).toBe(200);
|
|
47
|
+
const json = (await res.json()) as { viewCount?: number };
|
|
48
|
+
return typeof json.viewCount === "number" ? json.viewCount : 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("Pages — view_count counter", () => {
|
|
52
|
+
let server: Server;
|
|
53
|
+
const agentId = crypto.randomUUID();
|
|
54
|
+
const headers = { "Content-Type": "application/json", "X-Agent-ID": agentId };
|
|
55
|
+
|
|
56
|
+
beforeAll(async () => {
|
|
57
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
58
|
+
try {
|
|
59
|
+
await unlink(`${TEST_DB_PATH}${suffix}`);
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
initDb(TEST_DB_PATH);
|
|
63
|
+
server = createTestServer();
|
|
64
|
+
await new Promise<void>((resolve) => server.listen(TEST_PORT, () => resolve()));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterAll(async () => {
|
|
68
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
69
|
+
closeDb();
|
|
70
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
71
|
+
try {
|
|
72
|
+
await unlink(`${TEST_DB_PATH}${suffix}`);
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("public HTML page: 3 fetches → view_count = 3", async () => {
|
|
78
|
+
const post = await fetch(`${BASE}/api/pages`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers,
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
slug: "view-count-html",
|
|
83
|
+
title: "View Count HTML",
|
|
84
|
+
contentType: "text/html",
|
|
85
|
+
authMode: "public",
|
|
86
|
+
body: "<h1>hi</h1>",
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
expect(post.status).toBe(201);
|
|
90
|
+
const { id } = (await post.json()) as { id: string };
|
|
91
|
+
|
|
92
|
+
expect(await getViewCount(id, agentId)).toBe(0);
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < 3; i++) {
|
|
95
|
+
const r = await fetch(`${BASE}/p/${id}`);
|
|
96
|
+
expect(r.status).toBe(200);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
expect(await getViewCount(id, agentId)).toBe(3);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("/p/:id.json fetches also bump the counter", async () => {
|
|
103
|
+
const post = await fetch(`${BASE}/api/pages`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers,
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
slug: "view-count-json-path",
|
|
108
|
+
title: "View Count JSON Path",
|
|
109
|
+
contentType: "text/html",
|
|
110
|
+
authMode: "public",
|
|
111
|
+
body: "<h1>hi</h1>",
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
expect(post.status).toBe(201);
|
|
115
|
+
const { id } = (await post.json()) as { id: string };
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < 2; i++) {
|
|
118
|
+
const r = await fetch(`${BASE}/p/${id}.json`);
|
|
119
|
+
expect(r.status).toBe(200);
|
|
120
|
+
}
|
|
121
|
+
// One additional HTML fetch — both paths bump the same counter.
|
|
122
|
+
expect((await fetch(`${BASE}/p/${id}`)).status).toBe(200);
|
|
123
|
+
expect(await getViewCount(id, agentId)).toBe(3);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("404 on unknown page id does NOT crash and does not touch any counter", async () => {
|
|
127
|
+
const bogus = "0".repeat(32);
|
|
128
|
+
const r = await fetch(`${BASE}/p/${bogus}`);
|
|
129
|
+
expect(r.status).toBe(404);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("password-protected page: 401 without unlock does NOT bump", async () => {
|
|
133
|
+
const post = await fetch(`${BASE}/api/pages`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers,
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
slug: "view-count-pw",
|
|
138
|
+
title: "View Count Password",
|
|
139
|
+
contentType: "text/html",
|
|
140
|
+
authMode: "password",
|
|
141
|
+
password: "letmein",
|
|
142
|
+
body: "<h1>secret</h1>",
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
expect(post.status).toBe(201);
|
|
146
|
+
const { id } = (await post.json()) as { id: string };
|
|
147
|
+
|
|
148
|
+
// No `?key=`, no Basic header → 401, no counter bump.
|
|
149
|
+
const r1 = await fetch(`${BASE}/p/${id}`);
|
|
150
|
+
expect(r1.status).toBe(401);
|
|
151
|
+
const r2 = await fetch(`${BASE}/p/${id}.json`);
|
|
152
|
+
expect(r2.status).toBe(401);
|
|
153
|
+
|
|
154
|
+
expect(await getViewCount(id, agentId)).toBe(0);
|
|
155
|
+
|
|
156
|
+
// After unlocking via ?key= → counter bumps.
|
|
157
|
+
const ok = await fetch(`${BASE}/p/${id}?key=letmein`);
|
|
158
|
+
expect(ok.status).toBe(200);
|
|
159
|
+
expect(await getViewCount(id, agentId)).toBe(1);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("authed page: 401 without cookie does NOT bump", async () => {
|
|
163
|
+
const post = await fetch(`${BASE}/api/pages`, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers,
|
|
166
|
+
body: JSON.stringify({
|
|
167
|
+
slug: "view-count-authed",
|
|
168
|
+
title: "View Count Authed",
|
|
169
|
+
contentType: "text/html",
|
|
170
|
+
authMode: "authed",
|
|
171
|
+
body: "<h1>members only</h1>",
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
expect(post.status).toBe(201);
|
|
175
|
+
const { id } = (await post.json()) as { id: string };
|
|
176
|
+
|
|
177
|
+
// No cookie → 401.
|
|
178
|
+
const r = await fetch(`${BASE}/p/${id}`);
|
|
179
|
+
expect(r.status).toBe(401);
|
|
180
|
+
expect(await getViewCount(id, agentId)).toBe(0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("JSON content-type page: 302→SPA does NOT double-count (only .json bumps)", async () => {
|
|
184
|
+
const post = await fetch(`${BASE}/api/pages`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers,
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
slug: "view-count-jsonct",
|
|
189
|
+
title: "JSON Content Page",
|
|
190
|
+
contentType: "application/json",
|
|
191
|
+
authMode: "public",
|
|
192
|
+
body: JSON.stringify({ kind: "spec" }),
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
expect(post.status).toBe(201);
|
|
196
|
+
const { id } = (await post.json()) as { id: string };
|
|
197
|
+
|
|
198
|
+
// /p/:id 302s — should NOT bump.
|
|
199
|
+
const redir = await fetch(`${BASE}/p/${id}`, { redirect: "manual" });
|
|
200
|
+
expect(redir.status).toBe(302);
|
|
201
|
+
expect(await getViewCount(id, agentId)).toBe(0);
|
|
202
|
+
|
|
203
|
+
// /p/:id.json bumps.
|
|
204
|
+
const j = await fetch(`${BASE}/p/${id}.json`);
|
|
205
|
+
expect(j.status).toBe(200);
|
|
206
|
+
expect(await getViewCount(id, agentId)).toBe(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("listing endpoint exposes viewCount", async () => {
|
|
210
|
+
const res = await fetch(`${BASE}/api/pages`, {
|
|
211
|
+
headers: { "X-Agent-ID": agentId },
|
|
212
|
+
});
|
|
213
|
+
expect(res.status).toBe(200);
|
|
214
|
+
const body = (await res.json()) as { pages: Array<{ id: string; viewCount?: number }> };
|
|
215
|
+
expect(body.pages.length).toBeGreaterThan(0);
|
|
216
|
+
for (const p of body.pages) {
|
|
217
|
+
expect(typeof p.viewCount).toBe("number");
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -69,6 +69,7 @@ describe("Session templates — registration", () => {
|
|
|
69
69
|
"system.agent.system",
|
|
70
70
|
"system.agent.services",
|
|
71
71
|
"system.agent.artifacts",
|
|
72
|
+
"system.agent.share_urls",
|
|
72
73
|
];
|
|
73
74
|
|
|
74
75
|
for (const eventType of systemTemplates) {
|
|
@@ -88,10 +89,10 @@ describe("Session templates — registration", () => {
|
|
|
88
89
|
}
|
|
89
90
|
});
|
|
90
91
|
|
|
91
|
-
test("total of
|
|
92
|
+
test("total of 18 session/system templates registered", () => {
|
|
92
93
|
const all = getAllTemplateDefinitions();
|
|
93
94
|
const sessionSystem = all.filter((d) => d.category === "system" || d.category === "session");
|
|
94
|
-
expect(sessionSystem.length).toBe(
|
|
95
|
+
expect(sessionSystem.length).toBe(18);
|
|
95
96
|
});
|
|
96
97
|
});
|
|
97
98
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { closeDb, createAgent, createSkill, getSkillById, initDb } from "../be/db";
|
|
5
|
+
import { registerSkillUpdateTool } from "../tools/skills/skill-update";
|
|
6
|
+
|
|
7
|
+
const TEST_DB_PATH = "./test-skill-update-scope.sqlite";
|
|
8
|
+
|
|
9
|
+
const LEAD_ID = "aaaa0000-0000-4000-8000-000000000010";
|
|
10
|
+
const WORKER_ID = "bbbb0000-0000-4000-8000-000000000020";
|
|
11
|
+
|
|
12
|
+
type StructuredContent = {
|
|
13
|
+
yourAgentId?: string;
|
|
14
|
+
success: boolean;
|
|
15
|
+
message: string;
|
|
16
|
+
skill?: { id: string; scope: string; ownerAgentId: string | null };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
async function callSkillUpdate(
|
|
20
|
+
server: McpServer,
|
|
21
|
+
callerAgentId: string | undefined,
|
|
22
|
+
args: Record<string, unknown>,
|
|
23
|
+
): Promise<{ structuredContent: StructuredContent }> {
|
|
24
|
+
// biome-ignore lint/complexity/noBannedTypes: accessing internal MCP SDK type for test
|
|
25
|
+
const tools = (server as unknown as { _registeredTools: Record<string, { handler: Function }> })
|
|
26
|
+
._registeredTools;
|
|
27
|
+
const handler = tools["skill-update"].handler;
|
|
28
|
+
|
|
29
|
+
const extra = {
|
|
30
|
+
sessionId: "test-session",
|
|
31
|
+
requestInfo: {
|
|
32
|
+
headers: {
|
|
33
|
+
"x-agent-id": callerAgentId ?? "",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const result = await handler(args, extra);
|
|
39
|
+
return result as { structuredContent: StructuredContent };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("skill-update scope promotion", () => {
|
|
43
|
+
let server: McpServer;
|
|
44
|
+
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
47
|
+
try {
|
|
48
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
49
|
+
} catch {
|
|
50
|
+
// File doesn't exist
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
closeDb();
|
|
55
|
+
initDb(TEST_DB_PATH);
|
|
56
|
+
|
|
57
|
+
createAgent({ id: LEAD_ID, name: "Test Lead", isLead: true, status: "idle" });
|
|
58
|
+
createAgent({ id: WORKER_ID, name: "Test Worker", isLead: false, status: "idle" });
|
|
59
|
+
|
|
60
|
+
server = new McpServer({ name: "test-skill-update-scope", version: "1.0.0" });
|
|
61
|
+
registerSkillUpdateTool(server);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterAll(async () => {
|
|
65
|
+
closeDb();
|
|
66
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
67
|
+
try {
|
|
68
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
69
|
+
} catch {
|
|
70
|
+
// ignore
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("worker cannot promote their own skill to swarm scope", async () => {
|
|
76
|
+
const skill = createSkill({
|
|
77
|
+
name: "worker-skill-self-promote",
|
|
78
|
+
description: "Worker tries to promote",
|
|
79
|
+
content:
|
|
80
|
+
"---\nname: worker-skill-self-promote\ndescription: Worker tries to promote\n---\n\nBody.",
|
|
81
|
+
type: "personal",
|
|
82
|
+
scope: "agent",
|
|
83
|
+
ownerAgentId: WORKER_ID,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const result = await callSkillUpdate(server, WORKER_ID, {
|
|
87
|
+
skillId: skill.id,
|
|
88
|
+
scope: "swarm",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.structuredContent.success).toBe(false);
|
|
92
|
+
expect(result.structuredContent.message).toContain("lead");
|
|
93
|
+
|
|
94
|
+
const stored = getSkillById(skill.id);
|
|
95
|
+
expect(stored?.scope).toBe("agent");
|
|
96
|
+
expect(stored?.ownerAgentId).toBe(WORKER_ID);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("lead can promote a worker's agent-scope skill to swarm without changing ownerAgentId", async () => {
|
|
100
|
+
const skill = createSkill({
|
|
101
|
+
name: "worker-skill-lead-promote",
|
|
102
|
+
description: "Lead promotes",
|
|
103
|
+
content: "---\nname: worker-skill-lead-promote\ndescription: Lead promotes\n---\n\nBody.",
|
|
104
|
+
type: "personal",
|
|
105
|
+
scope: "agent",
|
|
106
|
+
ownerAgentId: WORKER_ID,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const result = await callSkillUpdate(server, LEAD_ID, {
|
|
110
|
+
skillId: skill.id,
|
|
111
|
+
scope: "swarm",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(result.structuredContent.success).toBe(true);
|
|
115
|
+
expect(result.structuredContent.skill?.scope).toBe("swarm");
|
|
116
|
+
expect(result.structuredContent.skill?.ownerAgentId).toBe(WORKER_ID);
|
|
117
|
+
|
|
118
|
+
const stored = getSkillById(skill.id);
|
|
119
|
+
expect(stored?.scope).toBe("swarm");
|
|
120
|
+
expect(stored?.ownerAgentId).toBe(WORKER_ID);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("lead demoting a swarm skill back to agent scope is allowed", async () => {
|
|
124
|
+
const skill = createSkill({
|
|
125
|
+
name: "swarm-skill-demote",
|
|
126
|
+
description: "Demote test",
|
|
127
|
+
content: "---\nname: swarm-skill-demote\ndescription: Demote test\n---\n\nBody.",
|
|
128
|
+
type: "personal",
|
|
129
|
+
scope: "swarm",
|
|
130
|
+
ownerAgentId: WORKER_ID,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = await callSkillUpdate(server, LEAD_ID, {
|
|
134
|
+
skillId: skill.id,
|
|
135
|
+
scope: "agent",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(result.structuredContent.success).toBe(true);
|
|
139
|
+
expect(result.structuredContent.skill?.scope).toBe("agent");
|
|
140
|
+
|
|
141
|
+
const stored = getSkillById(skill.id);
|
|
142
|
+
expect(stored?.scope).toBe("agent");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("omitting scope leaves it unchanged", async () => {
|
|
146
|
+
const skill = createSkill({
|
|
147
|
+
name: "scope-untouched",
|
|
148
|
+
description: "No scope change",
|
|
149
|
+
content: "---\nname: scope-untouched\ndescription: No scope change\n---\n\nBody.",
|
|
150
|
+
type: "personal",
|
|
151
|
+
scope: "agent",
|
|
152
|
+
ownerAgentId: WORKER_ID,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const result = await callSkillUpdate(server, WORKER_ID, {
|
|
156
|
+
skillId: skill.id,
|
|
157
|
+
isEnabled: false,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(result.structuredContent.success).toBe(true);
|
|
161
|
+
const stored = getSkillById(skill.id);
|
|
162
|
+
expect(stored?.scope).toBe("agent");
|
|
163
|
+
expect(stored?.isEnabled).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|