@desplega.ai/agent-swarm 1.80.0 → 1.80.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/openapi.json +399 -14
  2. package/package.json +3 -1
  3. package/src/artifact-sdk/server.ts +2 -1
  4. package/src/be/db.ts +1 -1
  5. package/src/be/migrations/064_scripts.sql +39 -0
  6. package/src/be/migrations/065_script_embeddings.sql +7 -0
  7. package/src/be/migrations/066_scripts_args_json_schema.sql +1 -0
  8. package/src/be/scripts/db.ts +417 -0
  9. package/src/be/scripts/embeddings.ts +233 -0
  10. package/src/be/scripts/extract-schema.ts +55 -0
  11. package/src/be/scripts/maintenance.ts +9 -0
  12. package/src/be/scripts/typecheck.ts +199 -0
  13. package/src/cli.tsx +22 -5
  14. package/src/commands/artifact.ts +3 -2
  15. package/src/commands/claude-managed-setup.ts +2 -1
  16. package/src/commands/codex-login.ts +5 -3
  17. package/src/commands/onboard.tsx +2 -1
  18. package/src/commands/runner.ts +153 -20
  19. package/src/commands/setup.tsx +5 -3
  20. package/src/hooks/hook.ts +4 -3
  21. package/src/http/index.ts +40 -29
  22. package/src/http/memory.ts +28 -0
  23. package/src/http/openapi.ts +1 -0
  24. package/src/http/page-proxy.ts +2 -1
  25. package/src/http/route-def.ts +1 -0
  26. package/src/http/schedules.ts +37 -0
  27. package/src/http/scripts.ts +388 -0
  28. package/src/linear/outbound.ts +9 -2
  29. package/src/otel.ts +5 -0
  30. package/src/providers/claude-adapter.ts +23 -1
  31. package/src/providers/types.ts +8 -0
  32. package/src/scripts-runtime/ctx.ts +23 -0
  33. package/src/scripts-runtime/eval-harness.ts +63 -0
  34. package/src/scripts-runtime/executors/native.ts +232 -0
  35. package/src/scripts-runtime/executors/registry.ts +16 -0
  36. package/src/scripts-runtime/executors/types.ts +63 -0
  37. package/src/scripts-runtime/extract-args-schema.ts +69 -0
  38. package/src/scripts-runtime/extract-signature.ts +81 -0
  39. package/src/scripts-runtime/import-allowlist.ts +109 -0
  40. package/src/scripts-runtime/loader.ts +96 -0
  41. package/src/scripts-runtime/redacted.ts +48 -0
  42. package/src/scripts-runtime/sdk-allowlist.ts +29 -0
  43. package/src/scripts-runtime/stdlib/fetch.ts +46 -0
  44. package/src/scripts-runtime/stdlib/glob.ts +8 -0
  45. package/src/scripts-runtime/stdlib/grep.ts +34 -0
  46. package/src/scripts-runtime/stdlib/index.ts +16 -0
  47. package/src/scripts-runtime/stdlib/table.ts +17 -0
  48. package/src/scripts-runtime/swarm-config.ts +35 -0
  49. package/src/scripts-runtime/swarm-sdk.ts +197 -0
  50. package/src/scripts-runtime/types/stdlib.d.ts +104 -0
  51. package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
  52. package/src/server.ts +12 -0
  53. package/src/tests/api-key.test.ts +33 -0
  54. package/src/tests/codex-login.test.ts +1 -1
  55. package/src/tests/error-tracker.test.ts +44 -0
  56. package/src/tests/linear-outbound-sync.test.ts +109 -0
  57. package/src/tests/mcp-tools.test.ts +69 -0
  58. package/src/tests/rate-limit-event.test.ts +292 -0
  59. package/src/tests/redacted.test.ts +29 -0
  60. package/src/tests/runner-tool-spans.test.ts +268 -0
  61. package/src/tests/script-executor-conformance.test.ts +142 -0
  62. package/src/tests/script-executor-registry.test.ts +17 -0
  63. package/src/tests/scripts-db.test.ts +329 -0
  64. package/src/tests/scripts-embeddings.test.ts +291 -0
  65. package/src/tests/scripts-extract-signature.test.ts +47 -0
  66. package/src/tests/scripts-http.test.ts +403 -0
  67. package/src/tests/scripts-import-allowlist.test.ts +55 -0
  68. package/src/tests/scripts-mcp-e2e.test.ts +269 -0
  69. package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
  70. package/src/tests/scripts-runtime.test.ts +344 -0
  71. package/src/tests/sdk-allowlist.test.ts +59 -0
  72. package/src/tests/secret-scrubber.test.ts +35 -1
  73. package/src/tests/swarm-config.test.ts +38 -0
  74. package/src/tests/tool-annotations.test.ts +2 -2
  75. package/src/tests/tool-call-progress.test.ts +30 -0
  76. package/src/tests/workflow-e2e.test.ts +218 -0
  77. package/src/tests/workflow-executors.test.ts +32 -2
  78. package/src/tests/workflow-input-redaction.test.ts +232 -0
  79. package/src/tests/workflow-swarm-script.test.ts +273 -0
  80. package/src/tools/memory-rate.ts +2 -1
  81. package/src/tools/script-common.ts +88 -0
  82. package/src/tools/script-delete.ts +35 -0
  83. package/src/tools/script-query-types.ts +37 -0
  84. package/src/tools/script-run.ts +43 -0
  85. package/src/tools/script-search.ts +32 -0
  86. package/src/tools/script-upsert.ts +43 -0
  87. package/src/tools/tool-config.ts +7 -0
  88. package/src/types.ts +61 -1
  89. package/src/utils/api-key.ts +28 -0
  90. package/src/utils/error-tracker.ts +58 -0
  91. package/src/utils/page-session.ts +8 -6
  92. package/src/utils/secret-scrubber.ts +22 -1
  93. package/src/workflows/engine.ts +12 -4
  94. package/src/workflows/executors/index.ts +1 -0
  95. package/src/workflows/executors/registry.ts +2 -0
  96. package/src/workflows/executors/script.ts +12 -1
  97. package/src/workflows/executors/swarm-script.ts +170 -0
  98. package/src/workflows/input.ts +65 -0
  99. package/src/workflows/recovery.ts +31 -3
  100. package/src/workflows/resume.ts +43 -5
@@ -0,0 +1,403 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import type { IncomingMessage, ServerResponse } from "node:http";
4
+ import { Readable } from "node:stream";
5
+ import { closeDb, createAgent, getDb, initDb } from "../be/db";
6
+ import { getScript, listScripts } from "../be/scripts/db";
7
+ import { setScriptEmbeddingProviderForTests } from "../be/scripts/embeddings";
8
+ import { handleCore } from "../http/core";
9
+ import { handleScripts } from "../http/scripts";
10
+ import { getPathSegments, parseQueryParams } from "../http/utils";
11
+ import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
12
+
13
+ const TEST_DB_PATH = "./test-scripts-http.sqlite";
14
+ const API_KEY = "test-scripts-http-key-1234567890";
15
+
16
+ function fakeEmbedding(text: string): Float32Array {
17
+ const lower = text.toLowerCase();
18
+ return new Float32Array([
19
+ lower.includes("lookup") ? 1 : 0,
20
+ lower.includes("multiply") ? 1 : 0,
21
+ lower.includes("linear") ? 1 : 0,
22
+ lower.includes("github") ? 1 : 0,
23
+ ]);
24
+ }
25
+
26
+ const fakeEmbeddingProvider = {
27
+ name: "test/fake-script-embedding",
28
+ dimensions: 4,
29
+ async embed(text: string) {
30
+ return fakeEmbedding(text);
31
+ },
32
+ async embedBatch(texts: string[]) {
33
+ return Promise.all(texts.map(fakeEmbedding));
34
+ },
35
+ };
36
+
37
+ async function removeDbFiles(path: string): Promise<void> {
38
+ for (const suffix of ["", "-wal", "-shm"]) {
39
+ try {
40
+ await unlink(path + suffix);
41
+ } catch (error) {
42
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
43
+ }
44
+ }
45
+ }
46
+
47
+ function validSource(multiplier: number) {
48
+ return `export default async (args: { value: number }): Promise<{ result: number }> => ({ result: args.value * ${multiplier} });`;
49
+ }
50
+
51
+ let workerId: string;
52
+ let leadId: string;
53
+ let savedEnv: NodeJS.ProcessEnv;
54
+
55
+ beforeAll(async () => {
56
+ savedEnv = { ...process.env };
57
+ await removeDbFiles(TEST_DB_PATH);
58
+ initDb(TEST_DB_PATH);
59
+ process.env.AGENT_SWARM_API_KEY = API_KEY;
60
+ delete process.env.API_KEY;
61
+ refreshSecretScrubberCache();
62
+ setScriptEmbeddingProviderForTests(fakeEmbeddingProvider);
63
+
64
+ const worker = createAgent({ name: "scripts-worker", isLead: false, status: "idle" });
65
+ const lead = createAgent({ name: "scripts-lead", isLead: true, status: "idle" });
66
+ workerId = worker.id;
67
+ leadId = lead.id;
68
+ });
69
+
70
+ afterAll(async () => {
71
+ closeDb();
72
+ setScriptEmbeddingProviderForTests(null);
73
+ await removeDbFiles(TEST_DB_PATH);
74
+ for (const key of Object.keys(process.env)) {
75
+ if (!(key in savedEnv)) delete process.env[key];
76
+ }
77
+ for (const [key, value] of Object.entries(savedEnv)) {
78
+ if (value === undefined) delete process.env[key];
79
+ else process.env[key] = value;
80
+ }
81
+ refreshSecretScrubberCache();
82
+ });
83
+
84
+ beforeEach(() => {
85
+ getDb().run("DELETE FROM scripts");
86
+ getDb().run("DELETE FROM events WHERE event = 'script.global_upsert'");
87
+ });
88
+
89
+ type TestResponse = {
90
+ status: number;
91
+ text: string;
92
+ json: () => Promise<unknown>;
93
+ };
94
+
95
+ async function dispatch(
96
+ path: string,
97
+ init: RequestInit & { agentId?: string } = {},
98
+ ): Promise<TestResponse> {
99
+ const headers: Record<string, string> = {
100
+ Authorization: `Bearer ${API_KEY}`,
101
+ "Content-Type": "application/json",
102
+ ...((init.headers as Record<string, string>) ?? {}),
103
+ };
104
+ if (init.agentId !== undefined) headers["X-Agent-ID"] = init.agentId;
105
+ const req = Readable.from(init.body ? [Buffer.from(String(init.body))] : []) as IncomingMessage;
106
+ req.method = init.method ?? "GET";
107
+ req.url = path;
108
+ req.headers = Object.fromEntries(
109
+ Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]),
110
+ );
111
+
112
+ let status = 200;
113
+ let text = "";
114
+ const res = {
115
+ headersSent: false,
116
+ writableEnded: false,
117
+ setHeader() {},
118
+ writeHead(code: number) {
119
+ status = code;
120
+ this.headersSent = true;
121
+ return this;
122
+ },
123
+ end(chunk?: unknown) {
124
+ if (chunk !== undefined) text += String(chunk);
125
+ this.writableEnded = true;
126
+ return this;
127
+ },
128
+ } as unknown as ServerResponse;
129
+
130
+ const agentId = req.headers["x-agent-id"] as string | undefined;
131
+ if (!(await handleCore(req, res, agentId, API_KEY))) {
132
+ const pathSegments = getPathSegments(req.url || "");
133
+ const queryParams = parseQueryParams(req.url || "");
134
+ if (!(await handleScripts(req, res, pathSegments, queryParams, agentId))) {
135
+ res.writeHead(404);
136
+ res.end("Not Found");
137
+ }
138
+ }
139
+
140
+ return {
141
+ status,
142
+ text,
143
+ json: async () => JSON.parse(text),
144
+ };
145
+ }
146
+
147
+ async function upsert(body: Record<string, unknown>, agentId = workerId): Promise<TestResponse> {
148
+ return dispatch("/api/scripts/upsert", {
149
+ method: "POST",
150
+ agentId,
151
+ body: JSON.stringify(body),
152
+ });
153
+ }
154
+
155
+ describe("/api/scripts HTTP", () => {
156
+ test("requires X-Agent-ID", async () => {
157
+ const res = await dispatch("/api/scripts/upsert", {
158
+ method: "POST",
159
+ body: JSON.stringify({ name: "missing-agent", source: validSource(2) }),
160
+ });
161
+ expect(res.status).toBe(400);
162
+ expect((await res.json()).error).toContain("X-Agent-ID");
163
+ });
164
+
165
+ test("upsert round-trips body and bumps version on source change", async () => {
166
+ const first = await upsert({
167
+ name: "double",
168
+ source: validSource(2),
169
+ description: "Double values",
170
+ intent: "Arithmetic reuse",
171
+ });
172
+ expect(first.status).toBe(200);
173
+ expect(await first.json()).toEqual({ name: "double", version: 1, contentDeduped: false });
174
+
175
+ const second = await upsert({
176
+ name: "double",
177
+ source: validSource(3),
178
+ description: "Triple values",
179
+ intent: "Arithmetic reuse",
180
+ });
181
+ expect(second.status).toBe(200);
182
+ expect(await second.json()).toEqual({ name: "double", version: 2, contentDeduped: false });
183
+ });
184
+
185
+ test("upsert typecheck failures return diagnostics and do not write rows", async () => {
186
+ const res = await upsert({
187
+ name: "bad-types",
188
+ source: `const x: number = "nope"; export default async () => x;`,
189
+ });
190
+ expect(res.status).toBe(400);
191
+ const body = await res.json();
192
+ expect(body.error).toBe("typecheck_failed");
193
+ expect(body.diagnostics.length).toBeGreaterThan(0);
194
+ expect(getScript({ name: "bad-types", scope: "agent", scopeId: workerId })).toBeNull();
195
+ });
196
+
197
+ test("upsert rejects unknown ctx.swarm tools", async () => {
198
+ const res = await upsert({
199
+ name: "unknown-tool",
200
+ source: `
201
+ import type { ScriptContext } from "swarm-sdk";
202
+ export default async (_args: unknown, ctx: ScriptContext) => ctx.swarm.no_such_tool({});
203
+ `,
204
+ });
205
+ expect(res.status).toBe(400);
206
+ expect((await res.json()).error).toBe("typecheck_failed");
207
+ });
208
+
209
+ test("global upsert is lead-only and writes audit events", async () => {
210
+ const denied = await upsert(
211
+ { name: "global-denied", scope: "global", source: validSource(2) },
212
+ workerId,
213
+ );
214
+ expect(denied.status).toBe(403);
215
+
216
+ const allowed = await upsert(
217
+ { name: "global-ok", scope: "global", source: validSource(2) },
218
+ leadId,
219
+ );
220
+ expect(allowed.status).toBe(200);
221
+
222
+ const event = getDb()
223
+ .prepare<{ data: string }, []>(
224
+ "SELECT data FROM events WHERE event = 'script.global_upsert' LIMIT 1",
225
+ )
226
+ .get();
227
+ expect(event).toBeTruthy();
228
+ expect(JSON.parse(event!.data).isNew).toBe(true);
229
+ });
230
+
231
+ test("global upsert promotion marks isPromotion when caller has agent script with same name", async () => {
232
+ await upsert({ name: "promote-me", source: validSource(2) }, leadId);
233
+ const res = await upsert(
234
+ { name: "promote-me", scope: "global", source: validSource(3) },
235
+ leadId,
236
+ );
237
+ expect(res.status).toBe(200);
238
+
239
+ const event = getDb()
240
+ .prepare<{ data: string }, []>(
241
+ "SELECT data FROM events WHERE event = 'script.global_upsert' ORDER BY createdAt DESC LIMIT 1",
242
+ )
243
+ .get();
244
+ expect(JSON.parse(event!.data).isPromotion).toBe(true);
245
+ });
246
+
247
+ test("failed promotion typecheck does not clear scratch flag", async () => {
248
+ const inline = await dispatch("/api/scripts/run", {
249
+ method: "POST",
250
+ agentId: workerId,
251
+ body: JSON.stringify({
252
+ source: `const x: number = "runtime-ok"; export default async () => "ok";`,
253
+ intent: "scratch promote",
254
+ }),
255
+ });
256
+ expect(inline.status).toBe(200);
257
+ const slug = (await inline.json()).autoSaved.slug as string;
258
+
259
+ const failed = await upsert({
260
+ name: slug,
261
+ source: `const x: number = "no"; export default async () => x;`,
262
+ });
263
+ expect(failed.status).toBe(400);
264
+ expect(getScript({ name: slug, scope: "agent", scopeId: workerId })?.isScratch).toBe(true);
265
+ });
266
+
267
+ test("run named scripts and inline scripts, auto-saving only successful inline source", async () => {
268
+ await upsert({ name: "named-runner", source: validSource(4) });
269
+ const named = await dispatch("/api/scripts/run", {
270
+ method: "POST",
271
+ agentId: workerId,
272
+ body: JSON.stringify({ name: "named-runner", args: { value: 3 }, intent: "run named" }),
273
+ });
274
+ expect(named.status).toBe(200);
275
+ expect((await named.json()).result).toEqual({ result: 12 });
276
+
277
+ const inline = await dispatch("/api/scripts/run", {
278
+ method: "POST",
279
+ agentId: workerId,
280
+ body: JSON.stringify({
281
+ source: `const x: number = "not typechecked"; export default async () => ({ ok: x });`,
282
+ intent: "inline type error still runs",
283
+ }),
284
+ });
285
+ expect(inline.status).toBe(200);
286
+ const inlineBody = await inline.json();
287
+ expect(inlineBody.result).toEqual({ ok: "not typechecked" });
288
+ expect(inlineBody.autoSaved.slug).toContain("scratch-inline-type-error-still-runs");
289
+
290
+ const beforeFailed = listScripts({
291
+ scope: "agent",
292
+ scopeId: workerId,
293
+ includeScratch: true,
294
+ }).length;
295
+ const failed = await dispatch("/api/scripts/run", {
296
+ method: "POST",
297
+ agentId: workerId,
298
+ body: JSON.stringify({
299
+ source: `export default async () => { throw new Error("boom"); };`,
300
+ intent: "failing inline",
301
+ }),
302
+ });
303
+ expect(failed.status).toBe(200);
304
+ expect((await failed.json()).autoSaved).toBeUndefined();
305
+ expect(listScripts({ scope: "agent", scopeId: workerId, includeScratch: true }).length).toBe(
306
+ beforeFailed,
307
+ );
308
+ });
309
+
310
+ test("workspace-rw named scripts return 501", async () => {
311
+ await upsert({ name: "workspace", source: validSource(2), fsMode: "workspace-rw" });
312
+ const res = await dispatch("/api/scripts/run", {
313
+ method: "POST",
314
+ agentId: workerId,
315
+ body: JSON.stringify({ name: "workspace", args: { value: 1 }, intent: "workspace" }),
316
+ });
317
+ expect(res.status).toBe(501);
318
+ });
319
+
320
+ test("search, types, and delete routes work", async () => {
321
+ await upsert({
322
+ name: "lookup-helper",
323
+ source: validSource(2),
324
+ description: "Find lookup rows",
325
+ });
326
+
327
+ const search = await dispatch("/api/scripts/search", {
328
+ method: "POST",
329
+ agentId: workerId,
330
+ body: JSON.stringify({ query: "lookup", limit: 5 }),
331
+ });
332
+ expect(search.status).toBe(200);
333
+ expect((await search.json()).results[0].name).toBe("lookup-helper");
334
+
335
+ const types = await dispatch("/api/scripts/lookup-helper/types", { agentId: workerId });
336
+ expect(types.status).toBe(200);
337
+ const typesBody = await types.json();
338
+ expect(typesBody.sdkTypes).toContain("SwarmSdk");
339
+ expect(typesBody.stdlibTypes).toContain('module "stdlib"');
340
+ expect(typesBody.signature.argsType).toContain("value");
341
+
342
+ const del = await dispatch("/api/scripts/lookup-helper", {
343
+ method: "DELETE",
344
+ agentId: workerId,
345
+ });
346
+ expect(del.status).toBe(200);
347
+ expect(await del.json()).toEqual({ deleted: true });
348
+ expect(getScript({ name: "lookup-helper", scope: "agent", scopeId: workerId })).toBeNull();
349
+ });
350
+
351
+ test("script_query_types returns argsJsonSchema for a script with argsSchema export", async () => {
352
+ const source = `
353
+ import { z } from "zod";
354
+ export const argsSchema = z.object({
355
+ repo: z.string(),
356
+ limit: z.number().default(10),
357
+ });
358
+ export default async (args: z.infer<typeof argsSchema>) => ({ repo: args.repo });
359
+ `;
360
+ await upsert({ name: "schema-script", source });
361
+
362
+ const types = await dispatch("/api/scripts/schema-script/types", { agentId: workerId });
363
+ expect(types.status).toBe(200);
364
+ const body = (await types.json()) as { argsJsonSchema: unknown };
365
+ expect(body.argsJsonSchema).not.toBeNull();
366
+ expect(typeof body.argsJsonSchema).toBe("object");
367
+ // JSON Schema should describe the repo and limit properties
368
+ const schema = body.argsJsonSchema as { properties?: Record<string, unknown> };
369
+ expect(schema.properties).toHaveProperty("repo");
370
+ expect(schema.properties).toHaveProperty("limit");
371
+ });
372
+
373
+ test("script_query_types returns argsJsonSchema: null for a script without argsSchema", async () => {
374
+ await upsert({ name: "no-schema-script", source: validSource(3) });
375
+
376
+ const types = await dispatch("/api/scripts/no-schema-script/types", { agentId: workerId });
377
+ expect(types.status).toBe(200);
378
+ const body = (await types.json()) as { argsJsonSchema: unknown };
379
+ expect(body.argsJsonSchema).toBeNull();
380
+ });
381
+
382
+ test("script_search includes argsJsonSchema in results", async () => {
383
+ const source = `
384
+ import { z } from "zod";
385
+ export const argsSchema = z.object({ query: z.string() });
386
+ export default async (args: z.infer<typeof argsSchema>) => ({ result: args.query });
387
+ `;
388
+ await upsert({ name: "search-with-schema", source, description: "search result helper" });
389
+
390
+ const search = await dispatch("/api/scripts/search", {
391
+ method: "POST",
392
+ agentId: workerId,
393
+ body: JSON.stringify({ query: "search result helper", limit: 5 }),
394
+ });
395
+ expect(search.status).toBe(200);
396
+ const body = (await search.json()) as {
397
+ results: Array<{ name: string; argsJsonSchema: unknown }>;
398
+ };
399
+ const result = body.results.find((r) => r.name === "search-with-schema");
400
+ expect(result).toBeDefined();
401
+ expect(result?.argsJsonSchema).not.toBeNull();
402
+ });
403
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { validateScriptImports } from "../scripts-runtime/import-allowlist";
3
+
4
+ describe("script import allowlist", () => {
5
+ test("allows relative imports and runtime barrels", () => {
6
+ const result = validateScriptImports(`
7
+ import helper from './helper';
8
+ import '../other';
9
+ import { SwarmSdk } from 'swarm-sdk';
10
+ import { table } from 'stdlib';
11
+ export default () => helper;
12
+ `);
13
+ expect(result.ok).toBe(true);
14
+ });
15
+
16
+ test("rejects forbidden static imports", () => {
17
+ const result = validateScriptImports("import fs from 'node:fs'; export default () => fs");
18
+ expect(result.ok).toBe(false);
19
+ if (!result.ok) expect(result.diagnostic).toContain("node:fs");
20
+ });
21
+
22
+ test("rejects child_process imports", () => {
23
+ const result = validateScriptImports("import cp from 'child_process'; export default () => cp");
24
+ expect(result.ok).toBe(false);
25
+ if (!result.ok) expect(result.diagnostic).toContain("child_process");
26
+ });
27
+
28
+ test("rejects bun:sqlite imports", () => {
29
+ const result = validateScriptImports(
30
+ "import sqlite from 'bun:sqlite'; export default () => sqlite",
31
+ );
32
+ expect(result.ok).toBe(false);
33
+ if (!result.ok) expect(result.diagnostic).toContain("bun:sqlite");
34
+ });
35
+
36
+ test("rejects literal dynamic imports", () => {
37
+ const result = validateScriptImports("export default async () => import('fs')");
38
+ expect(result.ok).toBe(false);
39
+ if (!result.ok) expect(result.diagnostic).toContain("fs");
40
+ });
41
+
42
+ test("rejects Function constructor dynamic import bypasses", () => {
43
+ const result = validateScriptImports(
44
+ `export default async () => new Function("return import('node:fs')")()`,
45
+ );
46
+ expect(result.ok).toBe(false);
47
+ if (!result.ok) expect(result.diagnostic).toContain("Function constructor");
48
+ });
49
+
50
+ test("rejects eval dynamic import bypasses", () => {
51
+ const result = validateScriptImports(`export default async () => eval("import('node:fs')")`);
52
+ expect(result.ok).toBe(false);
53
+ if (!result.ok) expect(result.diagnostic).toContain("eval");
54
+ });
55
+ });