@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.
- package/openapi.json +399 -14
- package/package.json +3 -1
- package/src/artifact-sdk/server.ts +2 -1
- package/src/be/db.ts +1 -1
- package/src/be/migrations/064_scripts.sql +39 -0
- package/src/be/migrations/065_script_embeddings.sql +7 -0
- package/src/be/migrations/066_scripts_args_json_schema.sql +1 -0
- package/src/be/scripts/db.ts +417 -0
- package/src/be/scripts/embeddings.ts +233 -0
- package/src/be/scripts/extract-schema.ts +55 -0
- package/src/be/scripts/maintenance.ts +9 -0
- package/src/be/scripts/typecheck.ts +199 -0
- package/src/cli.tsx +22 -5
- package/src/commands/artifact.ts +3 -2
- package/src/commands/claude-managed-setup.ts +2 -1
- package/src/commands/codex-login.ts +5 -3
- package/src/commands/onboard.tsx +2 -1
- package/src/commands/runner.ts +153 -20
- package/src/commands/setup.tsx +5 -3
- package/src/hooks/hook.ts +4 -3
- package/src/http/index.ts +40 -29
- package/src/http/memory.ts +28 -0
- package/src/http/openapi.ts +1 -0
- package/src/http/page-proxy.ts +2 -1
- package/src/http/route-def.ts +1 -0
- package/src/http/schedules.ts +37 -0
- package/src/http/scripts.ts +388 -0
- package/src/linear/outbound.ts +9 -2
- package/src/otel.ts +5 -0
- package/src/providers/claude-adapter.ts +23 -1
- package/src/providers/types.ts +8 -0
- package/src/scripts-runtime/ctx.ts +23 -0
- package/src/scripts-runtime/eval-harness.ts +63 -0
- package/src/scripts-runtime/executors/native.ts +232 -0
- package/src/scripts-runtime/executors/registry.ts +16 -0
- package/src/scripts-runtime/executors/types.ts +63 -0
- package/src/scripts-runtime/extract-args-schema.ts +69 -0
- package/src/scripts-runtime/extract-signature.ts +81 -0
- package/src/scripts-runtime/import-allowlist.ts +109 -0
- package/src/scripts-runtime/loader.ts +96 -0
- package/src/scripts-runtime/redacted.ts +48 -0
- package/src/scripts-runtime/sdk-allowlist.ts +29 -0
- package/src/scripts-runtime/stdlib/fetch.ts +46 -0
- package/src/scripts-runtime/stdlib/glob.ts +8 -0
- package/src/scripts-runtime/stdlib/grep.ts +34 -0
- package/src/scripts-runtime/stdlib/index.ts +16 -0
- package/src/scripts-runtime/stdlib/table.ts +17 -0
- package/src/scripts-runtime/swarm-config.ts +35 -0
- package/src/scripts-runtime/swarm-sdk.ts +197 -0
- package/src/scripts-runtime/types/stdlib.d.ts +104 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
- package/src/server.ts +12 -0
- package/src/tests/api-key.test.ts +33 -0
- package/src/tests/codex-login.test.ts +1 -1
- package/src/tests/error-tracker.test.ts +44 -0
- package/src/tests/linear-outbound-sync.test.ts +109 -0
- package/src/tests/mcp-tools.test.ts +69 -0
- package/src/tests/rate-limit-event.test.ts +292 -0
- package/src/tests/redacted.test.ts +29 -0
- package/src/tests/runner-tool-spans.test.ts +268 -0
- package/src/tests/script-executor-conformance.test.ts +142 -0
- package/src/tests/script-executor-registry.test.ts +17 -0
- package/src/tests/scripts-db.test.ts +329 -0
- package/src/tests/scripts-embeddings.test.ts +291 -0
- package/src/tests/scripts-extract-signature.test.ts +47 -0
- package/src/tests/scripts-http.test.ts +403 -0
- package/src/tests/scripts-import-allowlist.test.ts +55 -0
- package/src/tests/scripts-mcp-e2e.test.ts +269 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
- package/src/tests/scripts-runtime.test.ts +344 -0
- package/src/tests/sdk-allowlist.test.ts +59 -0
- package/src/tests/secret-scrubber.test.ts +35 -1
- package/src/tests/swarm-config.test.ts +38 -0
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/tool-call-progress.test.ts +30 -0
- package/src/tests/workflow-e2e.test.ts +218 -0
- package/src/tests/workflow-executors.test.ts +32 -2
- package/src/tests/workflow-input-redaction.test.ts +232 -0
- package/src/tests/workflow-swarm-script.test.ts +273 -0
- package/src/tools/memory-rate.ts +2 -1
- package/src/tools/script-common.ts +88 -0
- package/src/tools/script-delete.ts +35 -0
- package/src/tools/script-query-types.ts +37 -0
- package/src/tools/script-run.ts +43 -0
- package/src/tools/script-search.ts +32 -0
- package/src/tools/script-upsert.ts +43 -0
- package/src/tools/tool-config.ts +7 -0
- package/src/types.ts +61 -1
- package/src/utils/api-key.ts +28 -0
- package/src/utils/error-tracker.ts +58 -0
- package/src/utils/page-session.ts +8 -6
- package/src/utils/secret-scrubber.ts +22 -1
- package/src/workflows/engine.ts +12 -4
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/script.ts +12 -1
- package/src/workflows/executors/swarm-script.ts +170 -0
- package/src/workflows/input.ts +65 -0
- package/src/workflows/recovery.ts +31 -3
- package/src/workflows/resume.ts +43 -5
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { type ActiveToolSpanEntry, implicitCloseActiveToolSpans } from "../commands/runner";
|
|
3
|
+
import type { Attributes, AttributeValue, SwarmSpan } from "../otel";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal recording SwarmSpan stub for asserting attributes/status/end calls.
|
|
7
|
+
* Keeps the runner-tool-spans unit test isolated from the real OTel SDK.
|
|
8
|
+
*/
|
|
9
|
+
type RecordingSpan = SwarmSpan & {
|
|
10
|
+
attrs: Record<string, AttributeValue>;
|
|
11
|
+
status?: { code: number; message?: string };
|
|
12
|
+
ended: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function makeSpan(): RecordingSpan {
|
|
16
|
+
const span: RecordingSpan = {
|
|
17
|
+
attrs: {},
|
|
18
|
+
ended: false,
|
|
19
|
+
setAttribute(key: string, value: AttributeValue) {
|
|
20
|
+
this.attrs[key] = value;
|
|
21
|
+
return this;
|
|
22
|
+
},
|
|
23
|
+
setAttributes(attributes: Attributes) {
|
|
24
|
+
for (const [k, v] of Object.entries(attributes)) {
|
|
25
|
+
if (v !== undefined) this.attrs[k] = v;
|
|
26
|
+
}
|
|
27
|
+
return this;
|
|
28
|
+
},
|
|
29
|
+
addEvent() {
|
|
30
|
+
return this;
|
|
31
|
+
},
|
|
32
|
+
recordException() {},
|
|
33
|
+
setStatus(s) {
|
|
34
|
+
this.status = s;
|
|
35
|
+
return this;
|
|
36
|
+
},
|
|
37
|
+
end() {
|
|
38
|
+
this.ended = true;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
return span;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function entry(span: SwarmSpan, opts: { startedAt: number }): ActiveToolSpanEntry {
|
|
45
|
+
return { span, startedAt: opts.startedAt };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("implicitCloseActiveToolSpans", () => {
|
|
49
|
+
test("closes worker.tool spans with implicit_close=true and accurate duration_ms", () => {
|
|
50
|
+
const span = makeSpan();
|
|
51
|
+
const map = new Map<string, ActiveToolSpanEntry>();
|
|
52
|
+
map.set("call-1", entry(span, { startedAt: 1_000 }));
|
|
53
|
+
|
|
54
|
+
const closed = implicitCloseActiveToolSpans(map, 1_750);
|
|
55
|
+
|
|
56
|
+
expect(closed).toBe(1);
|
|
57
|
+
expect(span.ended).toBe(true);
|
|
58
|
+
expect(span.attrs["agentswarm.tool.implicit_close"]).toBe(true);
|
|
59
|
+
expect(span.attrs["agentswarm.tool.duration_ms"]).toBe(750);
|
|
60
|
+
expect(span.attrs["agentswarm.tool.call_id"]).toBe("call-1");
|
|
61
|
+
expect(span.status?.code).toBe(1);
|
|
62
|
+
expect(map.has("call-1")).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("closes MCP spans at the assistant-message boundary too", () => {
|
|
66
|
+
const mcpSpan = makeSpan();
|
|
67
|
+
const harnessSpan = makeSpan();
|
|
68
|
+
const map = new Map<string, ActiveToolSpanEntry>();
|
|
69
|
+
map.set("mcp-1", entry(mcpSpan, { startedAt: 1_000 }));
|
|
70
|
+
map.set("call-1", entry(harnessSpan, { startedAt: 1_000 }));
|
|
71
|
+
|
|
72
|
+
const closed = implicitCloseActiveToolSpans(map, 2_000);
|
|
73
|
+
|
|
74
|
+
expect(closed).toBe(2);
|
|
75
|
+
expect(harnessSpan.ended).toBe(true);
|
|
76
|
+
expect(harnessSpan.attrs["agentswarm.tool.implicit_close"]).toBe(true);
|
|
77
|
+
expect(mcpSpan.ended).toBe(true);
|
|
78
|
+
expect(mcpSpan.attrs["agentswarm.tool.implicit_close"]).toBe(true);
|
|
79
|
+
expect(mcpSpan.attrs["agentswarm.tool.duration_ms"]).toBe(1_000);
|
|
80
|
+
expect(mcpSpan.attrs["agentswarm.tool.call_id"]).toBe("mcp-1");
|
|
81
|
+
expect(mcpSpan.status?.code).toBe(1);
|
|
82
|
+
expect(map.size).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("no-op on an empty map (and returns 0)", () => {
|
|
86
|
+
const map = new Map<string, ActiveToolSpanEntry>();
|
|
87
|
+
const closed = implicitCloseActiveToolSpans(map, Date.now());
|
|
88
|
+
expect(closed).toBe(0);
|
|
89
|
+
expect(map.size).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("closes multiple parallel spans (mix of harness and MCP) from the same turn", () => {
|
|
93
|
+
const a = makeSpan();
|
|
94
|
+
const b = makeSpan();
|
|
95
|
+
const c = makeSpan();
|
|
96
|
+
const map = new Map<string, ActiveToolSpanEntry>();
|
|
97
|
+
map.set("a", entry(a, { startedAt: 100 }));
|
|
98
|
+
map.set("b", entry(b, { startedAt: 200 }));
|
|
99
|
+
map.set("c", entry(c, { startedAt: 300 }));
|
|
100
|
+
|
|
101
|
+
const closed = implicitCloseActiveToolSpans(map, 1_000);
|
|
102
|
+
|
|
103
|
+
expect(closed).toBe(3);
|
|
104
|
+
expect(a.attrs["agentswarm.tool.duration_ms"]).toBe(900);
|
|
105
|
+
expect(b.attrs["agentswarm.tool.duration_ms"]).toBe(800);
|
|
106
|
+
expect(c.attrs["agentswarm.tool.duration_ms"]).toBe(700);
|
|
107
|
+
for (const span of [a, b, c]) {
|
|
108
|
+
expect(span.ended).toBe(true);
|
|
109
|
+
expect(span.attrs["agentswarm.tool.implicit_close"]).toBe(true);
|
|
110
|
+
}
|
|
111
|
+
expect(map.size).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("called twice after a single turn → second call is a no-op", () => {
|
|
115
|
+
const span = makeSpan();
|
|
116
|
+
const map = new Map<string, ActiveToolSpanEntry>();
|
|
117
|
+
map.set("call-1", entry(span, { startedAt: 1_000 }));
|
|
118
|
+
|
|
119
|
+
expect(implicitCloseActiveToolSpans(map, 1_500)).toBe(1);
|
|
120
|
+
expect(implicitCloseActiveToolSpans(map, 2_000)).toBe(0);
|
|
121
|
+
// The span should not be ended twice or get a second duration overwrite.
|
|
122
|
+
expect(span.attrs["agentswarm.tool.duration_ms"]).toBe(500);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("end-to-end boundary semantics (helper integration)", () => {
|
|
127
|
+
// Simulates the runner's event-handler contract:
|
|
128
|
+
// - tool_start adds an entry to the active-tool-spans map
|
|
129
|
+
// - assistant-message boundary calls `implicitCloseActiveToolSpans`
|
|
130
|
+
// - explicit tool_end closes the entry directly (no implicit_close attr)
|
|
131
|
+
// - session shutdown calls a `closeActiveToolSpans` analog as a safety net
|
|
132
|
+
// We don't pull in the runner module directly (it imports the entire
|
|
133
|
+
// provider/HTTP surface); instead the test mirrors its small fragment of
|
|
134
|
+
// logic on the same exported helper.
|
|
135
|
+
|
|
136
|
+
function startToolSpan(
|
|
137
|
+
map: Map<string, ActiveToolSpanEntry>,
|
|
138
|
+
toolCallId: string,
|
|
139
|
+
opts: { startedAt: number },
|
|
140
|
+
): RecordingSpan {
|
|
141
|
+
const span = makeSpan();
|
|
142
|
+
map.set(toolCallId, { span, startedAt: opts.startedAt });
|
|
143
|
+
return span;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function endToolSpan(
|
|
147
|
+
map: Map<string, ActiveToolSpanEntry>,
|
|
148
|
+
toolCallId: string,
|
|
149
|
+
now: number,
|
|
150
|
+
): void {
|
|
151
|
+
// Mirrors the explicit `tool_end` branch in runner.ts: sets duration + OK
|
|
152
|
+
// status and ends the span. Crucially does NOT set `implicit_close`.
|
|
153
|
+
const active = map.get(toolCallId);
|
|
154
|
+
if (!active) return;
|
|
155
|
+
active.span.setAttributes({
|
|
156
|
+
"agentswarm.tool.duration_ms": now - active.startedAt,
|
|
157
|
+
});
|
|
158
|
+
active.span.setStatus({ code: 1 });
|
|
159
|
+
active.span.end();
|
|
160
|
+
map.delete(toolCallId);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function shutdownSafetyNet(
|
|
164
|
+
map: Map<string, ActiveToolSpanEntry>,
|
|
165
|
+
now: number,
|
|
166
|
+
): { closed: number } {
|
|
167
|
+
// Mirrors `closeActiveToolSpans` (the safety net). After the boundary fix,
|
|
168
|
+
// we expect this to be a no-op in the typical case.
|
|
169
|
+
let closed = 0;
|
|
170
|
+
for (const [toolCallId, active] of map) {
|
|
171
|
+
active.span.setAttributes({
|
|
172
|
+
"agentswarm.tool.duration_ms": now - active.startedAt,
|
|
173
|
+
"agentswarm.tool.unclosed": true,
|
|
174
|
+
"agentswarm.tool.call_id": toolCallId,
|
|
175
|
+
});
|
|
176
|
+
active.span.end();
|
|
177
|
+
map.delete(toolCallId);
|
|
178
|
+
closed++;
|
|
179
|
+
}
|
|
180
|
+
return { closed };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
test("tool_start → assistant boundary → span closes with implicit_close=true", () => {
|
|
184
|
+
const map = new Map<string, ActiveToolSpanEntry>();
|
|
185
|
+
const span = startToolSpan(map, "call-1", { startedAt: 1_000 });
|
|
186
|
+
|
|
187
|
+
implicitCloseActiveToolSpans(map, 1_500);
|
|
188
|
+
|
|
189
|
+
expect(span.ended).toBe(true);
|
|
190
|
+
expect(span.attrs["agentswarm.tool.implicit_close"]).toBe(true);
|
|
191
|
+
expect(span.attrs["agentswarm.tool.duration_ms"]).toBe(500);
|
|
192
|
+
expect(span.attrs["agentswarm.tool.unclosed"]).toBeUndefined();
|
|
193
|
+
expect(map.size).toBe(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("tool_start → tool_end → span closes WITHOUT implicit_close", () => {
|
|
197
|
+
const map = new Map<string, ActiveToolSpanEntry>();
|
|
198
|
+
const span = startToolSpan(map, "call-1", { startedAt: 1_000 });
|
|
199
|
+
|
|
200
|
+
endToolSpan(map, "call-1", 1_200);
|
|
201
|
+
|
|
202
|
+
expect(span.ended).toBe(true);
|
|
203
|
+
expect(span.attrs["agentswarm.tool.duration_ms"]).toBe(200);
|
|
204
|
+
expect(span.attrs["agentswarm.tool.implicit_close"]).toBeUndefined();
|
|
205
|
+
expect(map.size).toBe(0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("MCP tool spans are also closed by the assistant-message boundary", () => {
|
|
209
|
+
const map = new Map<string, ActiveToolSpanEntry>();
|
|
210
|
+
const mcp = startToolSpan(map, "mcp-1", { startedAt: 1_000 });
|
|
211
|
+
|
|
212
|
+
implicitCloseActiveToolSpans(map, 2_000);
|
|
213
|
+
|
|
214
|
+
expect(mcp.ended).toBe(true);
|
|
215
|
+
expect(mcp.attrs["agentswarm.tool.implicit_close"]).toBe(true);
|
|
216
|
+
expect(mcp.attrs["agentswarm.tool.duration_ms"]).toBe(1_000);
|
|
217
|
+
expect(map.size).toBe(0);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("mixed harness + MCP tool_starts → both kinds closed with implicit_close=true at boundary", () => {
|
|
221
|
+
// Simulates a turn where the model invokes both a harness tool (Bash) and
|
|
222
|
+
// an MCP tool (e.g. mcp__agent-swarm__store-progress), and the next
|
|
223
|
+
// assistant message arrives without any `tool_end` events from the
|
|
224
|
+
// adapter (Claude SDK behavior).
|
|
225
|
+
const map = new Map<string, ActiveToolSpanEntry>();
|
|
226
|
+
const harness = startToolSpan(map, "bash-1", { startedAt: 1_000 });
|
|
227
|
+
const mcp = startToolSpan(map, "mcp-1", { startedAt: 1_050 });
|
|
228
|
+
|
|
229
|
+
const closed = implicitCloseActiveToolSpans(map, 2_500);
|
|
230
|
+
|
|
231
|
+
expect(closed).toBe(2);
|
|
232
|
+
expect(harness.ended).toBe(true);
|
|
233
|
+
expect(mcp.ended).toBe(true);
|
|
234
|
+
expect(harness.attrs["agentswarm.tool.implicit_close"]).toBe(true);
|
|
235
|
+
expect(mcp.attrs["agentswarm.tool.implicit_close"]).toBe(true);
|
|
236
|
+
expect(harness.attrs["agentswarm.tool.duration_ms"]).toBe(1_500);
|
|
237
|
+
expect(mcp.attrs["agentswarm.tool.duration_ms"]).toBe(1_450);
|
|
238
|
+
expect(harness.attrs["agentswarm.tool.unclosed"]).toBeUndefined();
|
|
239
|
+
expect(mcp.attrs["agentswarm.tool.unclosed"]).toBeUndefined();
|
|
240
|
+
expect(map.size).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("after boundary closes all spans, shutdown safety net closes 0", () => {
|
|
244
|
+
const map = new Map<string, ActiveToolSpanEntry>();
|
|
245
|
+
startToolSpan(map, "call-1", { startedAt: 1_000 });
|
|
246
|
+
startToolSpan(map, "call-2", { startedAt: 1_100 });
|
|
247
|
+
|
|
248
|
+
implicitCloseActiveToolSpans(map, 1_800);
|
|
249
|
+
expect(map.size).toBe(0);
|
|
250
|
+
|
|
251
|
+
const { closed } = shutdownSafetyNet(map, 2_000);
|
|
252
|
+
expect(closed).toBe(0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("if session ends before any boundary fires, safety net flags `unclosed`", () => {
|
|
256
|
+
const map = new Map<string, ActiveToolSpanEntry>();
|
|
257
|
+
const span = startToolSpan(map, "call-1", { startedAt: 1_000 });
|
|
258
|
+
|
|
259
|
+
// No boundary, straight to shutdown.
|
|
260
|
+
const { closed } = shutdownSafetyNet(map, 5_000);
|
|
261
|
+
|
|
262
|
+
expect(closed).toBe(1);
|
|
263
|
+
expect(span.ended).toBe(true);
|
|
264
|
+
expect(span.attrs["agentswarm.tool.unclosed"]).toBe(true);
|
|
265
|
+
expect(span.attrs["agentswarm.tool.implicit_close"]).toBeUndefined();
|
|
266
|
+
expect(span.attrs["agentswarm.tool.duration_ms"]).toBe(4_000);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { NativeScriptExecutor } from "../scripts-runtime/executors/native";
|
|
3
|
+
import type {
|
|
4
|
+
ExecutorInput,
|
|
5
|
+
ExecutorOutput,
|
|
6
|
+
ScriptExecutor,
|
|
7
|
+
} from "../scripts-runtime/executors/types";
|
|
8
|
+
import { DEFAULT_SCRIPT_RESOURCES } from "../scripts-runtime/executors/types";
|
|
9
|
+
|
|
10
|
+
const payload = {
|
|
11
|
+
system: {
|
|
12
|
+
apiKey: { value: "conformance-secret", isSecret: true as const },
|
|
13
|
+
agentId: { value: "agent-1", isSecret: false as const },
|
|
14
|
+
mcpBaseUrl: { value: "http://localhost:3013", isSecret: false as const },
|
|
15
|
+
},
|
|
16
|
+
user: {},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function input(overrides: Partial<ExecutorInput> = {}): ExecutorInput {
|
|
20
|
+
return {
|
|
21
|
+
source: "export default async (args) => args.x + 1;",
|
|
22
|
+
args: { x: 1 },
|
|
23
|
+
configPayload: payload,
|
|
24
|
+
resources: {
|
|
25
|
+
...DEFAULT_SCRIPT_RESOURCES,
|
|
26
|
+
memoryMb: 2048,
|
|
27
|
+
wallClockMs: 1_000,
|
|
28
|
+
...overrides.resources,
|
|
29
|
+
},
|
|
30
|
+
fsMode: "none",
|
|
31
|
+
network: "open",
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class FakeScriptExecutor implements ScriptExecutor {
|
|
37
|
+
readonly name = "fake";
|
|
38
|
+
|
|
39
|
+
async run(runInput: ExecutorInput): Promise<ExecutorOutput> {
|
|
40
|
+
if (runInput.fsMode === "workspace-rw") {
|
|
41
|
+
return {
|
|
42
|
+
result: undefined,
|
|
43
|
+
stdout: "",
|
|
44
|
+
stderr: "workspace-rw not supported",
|
|
45
|
+
truncated: { stdout: false, stderr: false },
|
|
46
|
+
durationMs: 0,
|
|
47
|
+
exitCode: 1,
|
|
48
|
+
error: "executor_error",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (runInput.signal?.aborted) {
|
|
53
|
+
return {
|
|
54
|
+
result: undefined,
|
|
55
|
+
stdout: "",
|
|
56
|
+
stderr: "",
|
|
57
|
+
truncated: { stdout: false, stderr: false },
|
|
58
|
+
durationMs: 0,
|
|
59
|
+
exitCode: 1,
|
|
60
|
+
error: "killed",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const stdout = "x".repeat(runInput.resources.maxStdoutBytes + 10);
|
|
65
|
+
return {
|
|
66
|
+
result: runInput.configPayload.system.apiKey.value,
|
|
67
|
+
stdout: stdout.slice(0, runInput.resources.maxStdoutBytes),
|
|
68
|
+
stderr: "",
|
|
69
|
+
truncated: { stdout: true, stderr: false },
|
|
70
|
+
durationMs: 1,
|
|
71
|
+
exitCode: 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function conformance(name: string, makeExecutor: () => ScriptExecutor) {
|
|
77
|
+
describe(`${name} ScriptExecutor conformance`, () => {
|
|
78
|
+
test("happy path run", async () => {
|
|
79
|
+
const output = await makeExecutor().run(
|
|
80
|
+
input({
|
|
81
|
+
source: "export default async (args) => args.x + 1;",
|
|
82
|
+
args: { x: 2 },
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
expect(output.exitCode).toBe(0);
|
|
86
|
+
expect(output.error).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("stdout cap is honored", async () => {
|
|
90
|
+
const output = await makeExecutor().run(
|
|
91
|
+
input({
|
|
92
|
+
resources: {
|
|
93
|
+
...DEFAULT_SCRIPT_RESOURCES,
|
|
94
|
+
memoryMb: 2048,
|
|
95
|
+
maxStdoutBytes: 64,
|
|
96
|
+
wallClockMs: 1_000,
|
|
97
|
+
},
|
|
98
|
+
source: "export default async () => { console.log('x'.repeat(512)); return true; };",
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
expect(output.stdout.length).toBeLessThanOrEqual(64);
|
|
102
|
+
expect(output.truncated.stdout).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("workspace-rw returns executor_error", async () => {
|
|
106
|
+
const output = await makeExecutor().run(input({ fsMode: "workspace-rw" }));
|
|
107
|
+
expect(output.error).toBe("executor_error");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("config payload is delivered", async () => {
|
|
111
|
+
const output = await makeExecutor().run(
|
|
112
|
+
input({
|
|
113
|
+
source:
|
|
114
|
+
"export default async (_args, ctx) => ctx.stdlib.Redacted.value(ctx.swarm.config.apiKey);",
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
expect(output.result).toBe("conformance-secret");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
conformance("native", () => new NativeScriptExecutor());
|
|
123
|
+
conformance("fake", () => new FakeScriptExecutor());
|
|
124
|
+
|
|
125
|
+
describe("native-only executor behavior", () => {
|
|
126
|
+
test("timeout maps to timeout", async () => {
|
|
127
|
+
const output = await new NativeScriptExecutor().run(
|
|
128
|
+
input({
|
|
129
|
+
resources: { ...DEFAULT_SCRIPT_RESOURCES, memoryMb: 2048, wallClockMs: 100 },
|
|
130
|
+
source: "export default async () => new Promise(() => {});",
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
expect(output.error).toBe("timeout");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("AbortSignal maps to killed", async () => {
|
|
137
|
+
const controller = new AbortController();
|
|
138
|
+
controller.abort();
|
|
139
|
+
const output = await new NativeScriptExecutor().run(input({ signal: controller.signal }));
|
|
140
|
+
expect(output.error).toBe("killed");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { NativeScriptExecutor } from "../scripts-runtime/executors/native";
|
|
3
|
+
import { getScriptExecutor } from "../scripts-runtime/executors/registry";
|
|
4
|
+
|
|
5
|
+
describe("getScriptExecutor", () => {
|
|
6
|
+
test("defaults to native", () => {
|
|
7
|
+
expect(getScriptExecutor()).toBeInstanceOf(NativeScriptExecutor);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("returns native when requested", () => {
|
|
11
|
+
expect(getScriptExecutor("native")).toBeInstanceOf(NativeScriptExecutor);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("throws for unknown executors", () => {
|
|
15
|
+
expect(() => getScriptExecutor("e2b")).toThrow("Available: native");
|
|
16
|
+
});
|
|
17
|
+
});
|