@desplega.ai/agent-swarm 1.129.0 → 1.131.0
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 +2 -2
- package/dist/{actions-kfv2dawe.js → actions-w53mbns9.js} +11 -11
- package/dist/{anthropic-messages-3xgme8my.js → anthropic-messages-rjmd2e7n.js} +5 -5
- package/dist/{app-q8vjk6e8.js → app-m9gwqsqv.js} +8 -8
- package/dist/{artifact-hg4t1nx7.js → artifact-jysj3wrg.js} +2 -2
- package/dist/{assistant-g037r345.js → assistant-bp65m6f3.js} +13 -13
- package/dist/{azure-openai-responses-xa4sh9f6.js → azure-openai-responses-c0yjmbfd.js} +8 -5
- package/dist/{boot-reembed-18pzsrxa.js → boot-reembed-62p0at2k.js} +6 -6
- package/dist/{boot-reembed-wpks2cj8.js → boot-reembed-xmfp6wdh.js} +7 -7
- package/dist/{boot-scrub-logs-n7szd57r.js → boot-scrub-logs-rr8q2dms.js} +5 -5
- package/dist/{claude-adapter-qz4r0ajt.js → claude-adapter-2y47mrkd.js} +9 -9
- package/dist/{cli-mhjnbgb6.js → cli-0rcyq6d9.js} +450 -186
- package/dist/{cli-ttkmehy0.js → cli-2q72qz2s.js} +274 -25
- package/dist/{cli-gztgc3hz.js → cli-2wadx7p2.js} +2 -2
- package/dist/cli-3fj60etf.js +153 -0
- package/dist/{cli-7ecgm5v0.js → cli-3kf51j15.js} +1 -1
- package/dist/{cli-vgexpfsr.js → cli-57khth04.js} +6 -3
- package/dist/{cli-2mbe7vk0.js → cli-5dsbcjjw.js} +1 -1
- package/dist/{cli-etr62zy6.js → cli-5kx36evn.js} +2 -2
- package/dist/{cli-v2tr6amf.js → cli-5n6e5yfn.js} +30 -8
- package/dist/{cli-hx02s9zq.js → cli-5v4rw6yd.js} +2 -2
- package/dist/{cli-v37yr6mf.js → cli-5ze9rv97.js} +4 -4
- package/dist/{cli-ch7t5v7n.js → cli-6map995v.js} +9 -4
- package/dist/{cli-h0457a67.js → cli-70xqj0z3.js} +3 -3
- package/dist/{cli-kk4f9r17.js → cli-72q8m8gk.js} +8 -6
- package/dist/{cli-7padk69a.js → cli-78xwvkhj.js} +2 -2
- package/dist/{cli-dxtxacy7.js → cli-9dr5qzc0.js} +45 -8
- package/dist/{cli-17zxdgsq.js → cli-a40rnf71.js} +6 -6
- package/dist/{cli-t53vnnxz.js → cli-a4jzdpa3.js} +2514 -1055
- package/dist/{cli-w10fkrf1.js → cli-akdvwc9v.js} +354 -115
- package/dist/{cli-1qkk5er0.js → cli-bmze986d.js} +1 -1
- package/dist/{cli-ga12dc1d.js → cli-bqw0mt1n.js} +3 -3
- package/dist/{cli-0sf92efp.js → cli-cdeg08wx.js} +1 -1
- package/dist/{cli-a8ynhkyj.js → cli-cvgjwnh1.js} +17 -15
- package/dist/{cli-9fxg3v5m.js → cli-d2tfkcga.js} +4 -4
- package/dist/{cli-nhcd7x43.js → cli-d3x3x1c9.js} +2 -2
- package/dist/{cli-6nh2qxvh.js → cli-darcwax3.js} +1 -1
- package/dist/{cli-yjbgmysx.js → cli-e7ckcg4v.js} +12 -12
- package/dist/{cli-4n2gcjpd.js → cli-efxfnq6q.js} +4 -3
- package/dist/{cli-z9xqqhfk.js → cli-er3a4bb0.js} +1 -1
- package/dist/{cli-xg2eezyv.js → cli-f1am9szv.js} +1 -1
- package/dist/{cli-xy4cwsm9.js → cli-fywms69x.js} +1 -1
- package/dist/{cli-33jj76y8.js → cli-g6b8x1h8.js} +1 -1
- package/dist/{cli-epsvdckx.js → cli-gm9cbhbq.js} +20 -10
- package/dist/{cli-rkndnn90.js → cli-k9qkf1hw.js} +6 -4
- package/dist/{cli-zejvt395.js → cli-kh3ba2zf.js} +1 -1
- package/dist/{cli-gb25p1h2.js → cli-nm5gtp1x.js} +2 -2
- package/dist/{cli-akxnpx17.js → cli-pm6xbh84.js} +1829 -1629
- package/dist/{cli-2k0ztbfp.js → cli-psh3c4va.js} +1 -1
- package/dist/{cli-39vgrsep.js → cli-qv61hp37.js} +2 -2
- package/dist/{cli-hv2fndhm.js → cli-r6kqqbj0.js} +6 -4
- package/dist/{cli-1y1cm570.js → cli-rzbpmxdr.js} +1 -1
- package/dist/{cli-mmrk00vp.js → cli-t1xze5qh.js} +1 -1
- package/dist/{cli-g252a9ky.js → cli-t55yw3a1.js} +2 -2
- package/dist/{cli-7bz8dzqa.js → cli-t76vw5db.js} +1184 -1522
- package/dist/{cli-4n4mdv21.js → cli-xpxde7rm.js} +7 -3
- package/dist/{cli-fpaa5k2q.js → cli-yw8vghx2.js} +54 -18
- package/dist/cli-zsspvvm2.js +490 -0
- package/dist/cli.js +27 -25
- package/dist/{codex-adapter-7v6t7ff6.js → codex-adapter-d24q75wk.js} +7 -7
- package/dist/{codex-hook-amndawyb.js → codex-hook-hq54t9ft.js} +2 -2
- package/dist/{codex-session-runner-e7j4khe2.js → codex-session-runner-g50vcsex.js} +7 -7
- package/dist/{commands-ex0k5tbf.js → commands-c3nrc5hg.js} +5 -5
- package/dist/{db-df7jatfc.js → db-nt7fg3tw.js} +5 -5
- package/dist/{google-generative-ai-y42hnw9p.js → google-generative-ai-hysrd8qx.js} +7 -5
- package/dist/{google-vertex-4sg66gb6.js → google-vertex-p5arrga0.js} +7 -5
- package/dist/{handlers-1j2cfqt2.js → handlers-fkr6swat.js} +13 -13
- package/dist/{hook-nx9a0mj3.js → hook-77wpvnjc.js} +7 -7
- package/dist/{hook-w6yaf6sy.js → hook-g019yehy.js} +8 -8
- package/dist/{http-4tz5h8wg.js → http-jewj0syk.js} +2993 -716
- package/dist/{index-0bpkzb09.js → index-070v4eyb.js} +3 -3
- package/dist/{index-0s67qv7h.js → index-6gm5he8h.js} +11 -11
- package/dist/{index-35gtv6gj.js → index-7n2xpbd4.js} +310 -40
- package/dist/{index-ker3gccb.js → index-9znhar3x.js} +4 -4
- package/dist/{index-5n2c8yay.js → index-hase6zbe.js} +2 -2
- package/dist/{index-kcf3tm9h.js → index-hfvggd0z.js} +12 -12
- package/dist/{index-7c0jqna0.js → index-hp1kdbjj.js} +13 -13
- package/dist/{keepalive-3j4a9168.js → keepalive-j0fjvqfd.js} +7 -7
- package/dist/{lead-6kpevymb.js → lead-1n8x30xe.js} +33 -33
- package/dist/{maintenance-m1k82ean.js → maintenance-gvev11sa.js} +7 -7
- package/dist/{mistral-conversations-bjkdfb6z.js → mistral-conversations-81fwav0j.js} +6 -6
- package/dist/{oauth-refresh-sweep-881k8e4q.js → oauth-refresh-sweep-s9ecsvh0.js} +6 -6
- package/dist/{onboard-c0ykffjn.js → onboard-pvm5b38w.js} +2 -2
- package/dist/{openai-codex-responses-xetseqvt.js → openai-codex-responses-5g27pd6f.js} +43 -23
- package/dist/{openai-completions-5r2g5axa.js → openai-completions-3vkmfz2a.js} +55 -11
- package/dist/{openai-responses-ce67hd23.js → openai-responses-dg4t7j7a.js} +8 -5
- package/dist/{openrouter-images-pzfp22jb.js → openrouter-images-g3gm8931.js} +1 -1
- package/dist/{otel-impl-sqagjerv.js → otel-impl-p6jvgd9c.js} +6 -4
- package/dist/{otel-3mp81ppd.js → otel-nqa12wf3.js} +2 -2
- package/dist/{pi-mono-adapter-k584k16g.js → pi-mono-adapter-t091s27g.js} +17117 -7517
- package/dist/{pricing-refresh-y82whbf9.js → pricing-refresh-k49rcyed.js} +7 -7
- package/dist/{rbac-roles-nvkqmzyd.js → rbac-roles-30pspvy7.js} +7 -7
- package/dist/{rbac-roles-4ht8gegc.js → rbac-roles-cr80g96t.js} +6 -6
- package/dist/{render-v2-wvqdgvvp.js → render-v2-xm457ng1.js} +8 -8
- package/dist/{seed-pricing-fy3rxhde.js → seed-pricing-yhpv35fr.js} +6 -6
- package/dist/{setup-ak5cnc6f.js → setup-nytnmwwr.js} +2 -2
- package/dist/{worker-d8769q8a.js → worker-msytr2cw.js} +33 -33
- package/openapi.json +34039 -11036
- package/package.json +8 -6
- package/src/apps/definition.ts +368 -26
- package/src/apps/row-store.ts +66 -10
- package/src/apps/schema-migrate.ts +95 -1
- package/src/apps/store.ts +13 -4
- package/src/apps/sync-run-as.ts +18 -0
- package/src/apps/sync.ts +934 -0
- package/src/apps/version.ts +16 -2
- package/src/assets/key.ts +8 -1
- package/src/be/asset-key-audit.ts +3 -1
- package/src/be/db-queries/mcp-oauth.ts +297 -24
- package/src/be/db.ts +42 -1
- package/src/be/migrations/129_asset_keys_apps_scripts.sql +161 -0
- package/src/be/scripts/db.ts +5 -2
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-scripts/catalog/app-sync-run.ts +32 -0
- package/src/be/seed-scripts/catalog/github-issues-pull.ts +102 -0
- package/src/be/seed-scripts/index.ts +18 -0
- package/src/be/seed-skills/index.ts +7 -0
- package/src/http/active-sessions.ts +26 -18
- package/src/http/agents.ts +94 -27
- package/src/http/api-keys.ts +91 -17
- package/src/http/approval-requests.ts +87 -10
- package/src/http/apps.ts +361 -53
- package/src/http/assets.ts +165 -31
- package/src/http/budgets.ts +7 -5
- package/src/http/codex-oauth-keep-warm.ts +21 -3
- package/src/http/config.ts +46 -16
- package/src/http/context.ts +33 -6
- package/src/http/ecosystem.ts +20 -6
- package/src/http/events.ts +24 -9
- package/src/http/favorites.ts +20 -6
- package/src/http/fs.ts +60 -17
- package/src/http/heartbeat.ts +18 -4
- package/src/http/inbox-state.ts +12 -6
- package/src/http/integrations.ts +25 -5
- package/src/http/kv.ts +36 -15
- package/src/http/mcp-bridge.ts +5 -1
- package/src/http/mcp-oauth.ts +332 -17
- package/src/http/mcp-servers.ts +56 -18
- package/src/http/memory.ts +282 -29
- package/src/http/metrics.ts +80 -17
- package/src/http/oauth-callback.ts +10 -4
- package/src/http/oauth-generic.ts +5 -1
- package/src/http/oauth-locks.ts +6 -3
- package/src/http/openapi.ts +57 -9
- package/src/http/page-proxy.ts +25 -5
- package/src/http/pages-public.ts +37 -20
- package/src/http/pages.ts +93 -28
- package/src/http/poll.ts +89 -5
- package/src/http/pricing.ts +9 -6
- package/src/http/prompt-templates.ts +85 -21
- package/src/http/route-def.ts +67 -4
- package/src/http/schedules.ts +63 -14
- package/src/http/script-connection-proxy.ts +30 -4
- package/src/http/script-connections.ts +393 -43
- package/src/http/script-runs.ts +73 -19
- package/src/http/scripts.ts +257 -39
- package/src/http/session-data.ts +87 -14
- package/src/http/sessions.ts +80 -5
- package/src/http/skills.ts +115 -35
- package/src/http/stats.ts +94 -13
- package/src/http/task-templates.ts +6 -4
- package/src/http/tasks.ts +230 -61
- package/src/http/trackers/jira.ts +90 -30
- package/src/http/trackers/linear.ts +46 -12
- package/src/http/users.ts +162 -25
- package/src/http/utils.ts +12 -2
- package/src/http/webhooks.ts +30 -16
- package/src/http/workflow-events.ts +18 -5
- package/src/http/workflows.ts +128 -39
- package/src/http/x.ts +37 -6
- package/src/oauth/ensure-mcp-token.ts +16 -1
- package/src/oauth/mcp-wrapper.ts +281 -26
- package/src/providers/pi-mono-adapter.ts +1 -1
- package/src/scripts-runtime/credential-broker/README.md +1 -1
- package/src/scripts-runtime/credential-broker/fetch-patch.ts +12 -2
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/stdlib/fetch.ts +6 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/server.ts +2 -0
- package/src/tests/apps-rbac.test.ts +110 -0
- package/src/tests/apps-spike2.test.ts +4 -1
- package/src/tests/apps-sync-engine.test.ts +2540 -0
- package/src/tests/apps-sync.test.ts +1627 -0
- package/src/tests/asset-key-api.test.ts +48 -2
- package/src/tests/asset-key-invariants.test.ts +92 -0
- package/src/tests/asset-key-migration.test.ts +174 -8
- package/src/tests/asset-key-normalization.test.ts +2 -0
- package/src/tests/credential-broker.test.ts +27 -2
- package/src/tests/mcp-oauth-dcr-reuse.test.ts +743 -0
- package/src/tests/mcp-oauth-ensure-token.test.ts +45 -0
- package/src/tests/mcp-oauth-manual-client.test.ts +135 -2
- package/src/tests/mcp-oauth-queries.test.ts +153 -0
- package/src/tests/mcp-oauth-wrapper.test.ts +432 -0
- package/src/tests/memory-http-recall-gating.test.ts +2 -0
- package/src/tests/openapi-response-contract.test.ts +138 -0
- package/src/tests/rbac-admission-e2e.test.ts +68 -0
- package/src/tests/script-connections.test.ts +3 -0
- package/src/tests/scripts-runtime-fetch.test.ts +121 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +4 -2
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/swarm-tool-result-gate.test.ts +32 -0
- package/src/tests/task-working-dir.test.ts +8 -4
- package/src/tests/whoami-route.test.ts +127 -0
- package/src/tests/workflow-engine-v2.test.ts +80 -7
- package/src/tests/workflow-swarm-script.test.ts +104 -0
- package/src/tools/app-get.ts +10 -2
- package/src/tools/app-rollback.ts +1 -0
- package/src/tools/app-sync.ts +112 -0
- package/src/tools/script-common.ts +20 -0
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/utils.ts +11 -0
- package/src/tools/workflows/create-workflow.ts +4 -1
- package/src/types.ts +1394 -1206
- package/src/utils/template.ts +8 -1
- package/src/utils/zod-openapi.ts +11 -0
- package/src/workflows/engine.ts +27 -4
- package/src/workflows/executors/swarm-script.ts +28 -0
- package/src/workflows/retry-poller.ts +19 -2
- package/templates/skills/apps/content.md +94 -5
- package/templates/skills/asset-namespaces/content.md +2 -2
- package/templates/skills/scheduled-task-resilience/SKILL.md +102 -0
- package/templates/skills/scheduled-task-resilience/config.json +14 -0
- package/templates/skills/scheduled-task-resilience/content.md +97 -0
- package/dist/cli-7wmybrt5.js +0 -115
- package/dist/cli-d91pvmq1.js +0 -14
|
@@ -26,6 +26,7 @@ setDefaultTimeout(120_000);
|
|
|
26
26
|
|
|
27
27
|
const REQUESTER_ROLE_ID = "rbac-role-requester";
|
|
28
28
|
const CONFIG_SECRET_READER_ROLE_ID = "rbac-test-config-secret-reader";
|
|
29
|
+
const APP_MANAGER_ROLE_ID = "rbac-test-app-manager";
|
|
29
30
|
const CONFIG_SECRET_KEY = "RBAC_ADMISSION_SECRET";
|
|
30
31
|
const CONFIG_SECRET_VALUE = "rbac-admission-secret-value";
|
|
31
32
|
|
|
@@ -81,6 +82,29 @@ function grantUserConfigSecretRead(dbPath: string, userId: string): void {
|
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
function grantUserAppManage(dbPath: string, userId: string): void {
|
|
86
|
+
const db = new Database(dbPath);
|
|
87
|
+
try {
|
|
88
|
+
db.run("PRAGMA busy_timeout = 5000");
|
|
89
|
+
db.transaction(() => {
|
|
90
|
+
db.prepare(
|
|
91
|
+
`INSERT OR IGNORE INTO roles (id, name, description, isBuiltin, grantsAll)
|
|
92
|
+
VALUES (?, ?, ?, 0, 0)`,
|
|
93
|
+
).run(APP_MANAGER_ROLE_ID, "app-manager", "Test app manager");
|
|
94
|
+
db.prepare("INSERT OR IGNORE INTO role_permissions (roleId, verb) VALUES (?, ?)").run(
|
|
95
|
+
APP_MANAGER_ROLE_ID,
|
|
96
|
+
"app.manage",
|
|
97
|
+
);
|
|
98
|
+
db.prepare(
|
|
99
|
+
`INSERT OR IGNORE INTO principal_roles (principalType, principalId, roleId)
|
|
100
|
+
VALUES ('user', ?, ?)`,
|
|
101
|
+
).run(userId, APP_MANAGER_ROLE_ID);
|
|
102
|
+
})();
|
|
103
|
+
} finally {
|
|
104
|
+
db.close();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
84
108
|
describe("RBAC admission over real HTTP", () => {
|
|
85
109
|
afterAll(async () => {
|
|
86
110
|
if (server) {
|
|
@@ -205,6 +229,21 @@ describe("RBAC admission over real HTTP", () => {
|
|
|
205
229
|
expect(defaultCreate.status).toBe(201);
|
|
206
230
|
const taskId = defaultCreate.body.id as string;
|
|
207
231
|
|
|
232
|
+
const appCreate = await api(server.base, "POST", "/api/apps", {
|
|
233
|
+
body: {
|
|
234
|
+
name: "RBAC admission app",
|
|
235
|
+
definition: {
|
|
236
|
+
models: { note: { columns: { title: { kind: "string" } } } },
|
|
237
|
+
pages: {
|
|
238
|
+
main: { root: "root", elements: { root: { type: "Container", props: {} } } },
|
|
239
|
+
},
|
|
240
|
+
defaultPage: "main",
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
expect(appCreate.status).toBe(201);
|
|
245
|
+
const appId = appCreate.body.app.id as string;
|
|
246
|
+
|
|
208
247
|
rewriteUserToRequester(dbPath, userId);
|
|
209
248
|
|
|
210
249
|
const deniedTaskCreate = await api(server.base, "POST", "/api/tasks", {
|
|
@@ -214,6 +253,26 @@ describe("RBAC admission over real HTTP", () => {
|
|
|
214
253
|
expect(deniedTaskCreate.status).toBe(403);
|
|
215
254
|
expect(deniedTaskCreate.body.error).toContain("admission: route has no permission verb");
|
|
216
255
|
|
|
256
|
+
const deniedAppMove = await api(server.base, "PATCH", `/api/assets/app/${appId}/key`, {
|
|
257
|
+
bearer: userToken,
|
|
258
|
+
body: { key: "shared/rbac-denied/" },
|
|
259
|
+
});
|
|
260
|
+
expect(deniedAppMove.status).toBe(403);
|
|
261
|
+
expect(deniedAppMove.body.error).toContain("missing permission 'app.manage'");
|
|
262
|
+
|
|
263
|
+
grantUserAppManage(dbPath, userId);
|
|
264
|
+
|
|
265
|
+
const allowedAppMove = await api(server.base, "PATCH", `/api/assets/app/${appId}/key`, {
|
|
266
|
+
bearer: userToken,
|
|
267
|
+
body: { key: "shared/rbac-allowed/" },
|
|
268
|
+
});
|
|
269
|
+
expect(allowedAppMove.status).toBe(200);
|
|
270
|
+
expect(allowedAppMove.body).toEqual({
|
|
271
|
+
entityType: "app",
|
|
272
|
+
id: appId,
|
|
273
|
+
key: "shared/rbac-allowed/",
|
|
274
|
+
});
|
|
275
|
+
|
|
217
276
|
const allowedFavorite = await api(server.base, "PUT", "/api/favorites", {
|
|
218
277
|
bearer: userToken,
|
|
219
278
|
body: { itemType: "workflow", itemId: "wf-rbac-admission", favorite: true },
|
|
@@ -311,6 +370,7 @@ describe("RBAC admission over real HTTP", () => {
|
|
|
311
370
|
expect(denyRows.map((row) => row.resourceId).sort()).toEqual([
|
|
312
371
|
"GET /api/agents/{id}/mcp-servers",
|
|
313
372
|
"GET /api/scripts/{id}/apis/{endpointId}/secret",
|
|
373
|
+
"PATCH /api/assets/app/{id}/key",
|
|
314
374
|
"POST /api/scripts/{id}/apis",
|
|
315
375
|
"POST /api/tasks",
|
|
316
376
|
]);
|
|
@@ -336,6 +396,14 @@ describe("RBAC admission over real HTTP", () => {
|
|
|
336
396
|
row.decision === "allow",
|
|
337
397
|
),
|
|
338
398
|
).toBe(true);
|
|
399
|
+
expect(
|
|
400
|
+
userHttpRows.some(
|
|
401
|
+
(row) =>
|
|
402
|
+
row.resourceId === "PATCH /api/assets/app/{id}/key" &&
|
|
403
|
+
row.verb === "app.manage" &&
|
|
404
|
+
row.decision === "allow",
|
|
405
|
+
),
|
|
406
|
+
).toBe(true);
|
|
339
407
|
|
|
340
408
|
rewriteUserToRequester(dbPath, userId);
|
|
341
409
|
|
|
@@ -535,6 +535,9 @@ describe("script connections", () => {
|
|
|
535
535
|
markMigrationApplied(database, "127_backfill_user_attribution.sql");
|
|
536
536
|
// 128 alters session_costs, which this partial fixture does not create.
|
|
537
537
|
markMigrationApplied(database, "128_session_costs_accuracy.sql");
|
|
538
|
+
// 129 alters scripts and rebuilds asset_key_history, neither of which this
|
|
539
|
+
// migration-112-only fixture creates.
|
|
540
|
+
markMigrationApplied(database, "129_asset_keys_apps_scripts.sql");
|
|
538
541
|
|
|
539
542
|
const id = crypto.randomUUID();
|
|
540
543
|
const now = new Date().toISOString();
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// DES-770: runtimeFetch must honor already-aborted / mid-flight caller aborts
|
|
2
|
+
// instead of running (and retrying) attempts for a cancelled request. All tests
|
|
3
|
+
// count real attempts against a Bun.serve fixture — no mock.module (leaks
|
|
4
|
+
// process-wide across the test run).
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
6
|
+
import { runtimeFetch } from "../scripts-runtime/stdlib/fetch";
|
|
7
|
+
|
|
8
|
+
describe("runtimeFetch abort handling", () => {
|
|
9
|
+
let fixture: ReturnType<typeof Bun.serve>;
|
|
10
|
+
let origin = "";
|
|
11
|
+
const hits: Record<string, number> = {};
|
|
12
|
+
// Stalled handlers park their resolvers here; afterAll releases them so the
|
|
13
|
+
// fixture can shut down cleanly.
|
|
14
|
+
const stallReleases: Array<() => void> = [];
|
|
15
|
+
let onStallHit: (() => void) | undefined;
|
|
16
|
+
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
fixture = Bun.serve({
|
|
19
|
+
port: 0,
|
|
20
|
+
hostname: "127.0.0.1",
|
|
21
|
+
fetch: async (req: Request) => {
|
|
22
|
+
const path = new URL(req.url).pathname;
|
|
23
|
+
hits[path] = (hits[path] ?? 0) + 1;
|
|
24
|
+
if (path.startsWith("/stall")) {
|
|
25
|
+
onStallHit?.();
|
|
26
|
+
await new Promise<void>((resolve) => stallReleases.push(resolve));
|
|
27
|
+
}
|
|
28
|
+
if (path === "/flaky" && hits[path] === 1) {
|
|
29
|
+
return new Response("boom", { status: 500 });
|
|
30
|
+
}
|
|
31
|
+
return new Response("ok");
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
origin = `http://127.0.0.1:${fixture.port}`;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterAll(() => {
|
|
38
|
+
for (const release of stallReleases) release();
|
|
39
|
+
fixture.stop(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
async function rejectionOf(promise: Promise<unknown>): Promise<unknown> {
|
|
43
|
+
return promise.then(
|
|
44
|
+
() => {
|
|
45
|
+
throw new Error("expected runtimeFetch to reject");
|
|
46
|
+
},
|
|
47
|
+
(error: unknown) => error,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test("already-aborted signal rejects immediately with zero fetch attempts", async () => {
|
|
52
|
+
const error = await rejectionOf(
|
|
53
|
+
runtimeFetch(`${origin}/pre-aborted`, {
|
|
54
|
+
signal: AbortSignal.abort(),
|
|
55
|
+
retries: 3,
|
|
56
|
+
timeoutMs: 30_000,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
expect((error as DOMException).name).toBe("AbortError");
|
|
60
|
+
expect(hits["/pre-aborted"]).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("already-aborted signal propagates the caller's abort reason", async () => {
|
|
64
|
+
const reason = new Error("caller cancelled");
|
|
65
|
+
const error = await rejectionOf(
|
|
66
|
+
runtimeFetch(`${origin}/pre-aborted-reason`, {
|
|
67
|
+
signal: AbortSignal.abort(reason),
|
|
68
|
+
retries: 3,
|
|
69
|
+
timeoutMs: 30_000,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
expect(error).toBe(reason);
|
|
73
|
+
expect(hits["/pre-aborted-reason"]).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("mid-flight abort rejects after exactly one attempt, no retry", async () => {
|
|
77
|
+
const arrived = new Promise<void>((resolve) => {
|
|
78
|
+
onStallHit = resolve;
|
|
79
|
+
});
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
const pending = runtimeFetch(`${origin}/stall-abort`, {
|
|
82
|
+
signal: controller.signal,
|
|
83
|
+
retries: 3,
|
|
84
|
+
timeoutMs: 5_000,
|
|
85
|
+
});
|
|
86
|
+
await arrived;
|
|
87
|
+
onStallHit = undefined;
|
|
88
|
+
controller.abort();
|
|
89
|
+
const error = await rejectionOf(pending);
|
|
90
|
+
expect((error as DOMException).name).toBe("AbortError");
|
|
91
|
+
// Give a would-be retry time to land before asserting the attempt count.
|
|
92
|
+
await Bun.sleep(50);
|
|
93
|
+
expect(hits["/stall-abort"]).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("timeout abort still retries up to the retry budget", async () => {
|
|
97
|
+
// Live, never-aborted caller signal: proves signal presence doesn't turn
|
|
98
|
+
// the controller's own timeout abort into a terminal error.
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const error = await rejectionOf(
|
|
101
|
+
runtimeFetch(`${origin}/stall-timeout`, {
|
|
102
|
+
signal: controller.signal,
|
|
103
|
+
retries: 2,
|
|
104
|
+
timeoutMs: 100,
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
expect(error).toBeInstanceOf(Error);
|
|
108
|
+
expect(hits["/stall-timeout"]).toBe(2);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("5xx responses still retry and succeed on a later attempt", async () => {
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
const res = await runtimeFetch(`${origin}/flaky`, {
|
|
114
|
+
signal: controller.signal,
|
|
115
|
+
retries: 3,
|
|
116
|
+
timeoutMs: 5_000,
|
|
117
|
+
});
|
|
118
|
+
expect(res.status).toBe(200);
|
|
119
|
+
expect(hits["/flaky"]).toBe(2);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -111,7 +111,7 @@ describe("egress-substitution", () => {
|
|
|
111
111
|
expect(capturedHeaders?.get("authorization")).toBe("Bearer ghp_real_secret_value_123");
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
test("
|
|
114
|
+
test("drops the placeholder header for a non-allowlisted host", async () => {
|
|
115
115
|
let capturedHeaders: Headers | undefined;
|
|
116
116
|
globalThis.fetch = async (_input: any, init?: any) => {
|
|
117
117
|
capturedHeaders = new Headers(init?.headers);
|
|
@@ -134,7 +134,9 @@ describe("egress-substitution", () => {
|
|
|
134
134
|
headers: { Authorization: "Bearer [REDACTED:GITHUB_TOKEN]" },
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
// The placeholder header is DROPPED for non-allowlisted hosts: the
|
|
138
|
+
// literal marker is useless to the destination and must never egress.
|
|
139
|
+
expect(capturedHeaders?.get("authorization")).toBeNull();
|
|
138
140
|
});
|
|
139
141
|
|
|
140
142
|
test("passes through requests with no redacted placeholders", async () => {
|
|
@@ -55,8 +55,8 @@ afterAll(async () => {
|
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
describe("seed-scripts catalog", () => {
|
|
58
|
-
test("manifest holds
|
|
59
|
-
expect(SEED_SCRIPTS.length).toBe(
|
|
58
|
+
test("manifest holds 26 unique, well-described scripts", () => {
|
|
59
|
+
expect(SEED_SCRIPTS.length).toBe(26);
|
|
60
60
|
const names = SEED_SCRIPTS.map((s) => s.name);
|
|
61
61
|
expect(new Set(names).size).toBe(names.length);
|
|
62
62
|
for (const s of SEED_SCRIPTS) {
|
|
@@ -267,6 +267,38 @@ describe("finalizeSwarmToolResult", () => {
|
|
|
267
267
|
expect((nonEmpty.structuredContent as { nudge?: string }).nudge).toBeUndefined();
|
|
268
268
|
});
|
|
269
269
|
|
|
270
|
+
test("NUDGES map: app-get steers to the callable surface only when one exists", () => {
|
|
271
|
+
const withSurface = finalizeSwarmToolResult("app-get", {
|
|
272
|
+
ok: true,
|
|
273
|
+
message: 'App "PM Inbox" (a1).',
|
|
274
|
+
data: {
|
|
275
|
+
app: {
|
|
276
|
+
definition: {
|
|
277
|
+
models: {},
|
|
278
|
+
queries: { open: { model: "issue" } },
|
|
279
|
+
actions: { refresh: { kind: "sync" } },
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
expect((withSurface.structuredContent as { nudge?: string }).nudge).toContain(
|
|
285
|
+
"callable surface",
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const bare = finalizeSwarmToolResult("app-get", {
|
|
289
|
+
ok: true,
|
|
290
|
+
message: 'App "Bare" (a2).',
|
|
291
|
+
data: { app: { definition: { models: {} } } },
|
|
292
|
+
});
|
|
293
|
+
expect((bare.structuredContent as { nudge?: string }).nudge).toBeUndefined();
|
|
294
|
+
|
|
295
|
+
const failed = finalizeSwarmToolResult("app-get", {
|
|
296
|
+
ok: false,
|
|
297
|
+
message: "App a3 not found.",
|
|
298
|
+
});
|
|
299
|
+
expect((failed.structuredContent as { nudge?: string }).nudge).toBeUndefined();
|
|
300
|
+
});
|
|
301
|
+
|
|
270
302
|
test("NUDGES map: memory-search rating steer fires only when a result carries a rateHint", () => {
|
|
271
303
|
const withHint = finalizeSwarmToolResult("memory-search", {
|
|
272
304
|
ok: true,
|
|
@@ -55,10 +55,14 @@ describe("Task Working Directory", () => {
|
|
|
55
55
|
expect(() => dirSchema.parse("")).toThrow();
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
test("
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
test("accepts relative paths (row contract mirrors what the create body stores verbatim)", () => {
|
|
59
|
+
// The create-task body validates dir as z.string() and the DB stores it
|
|
60
|
+
// as-is, so rows CAN hold relative paths — the entity schema must not
|
|
61
|
+
// pin .startsWith("/") or response validation would reject honest rows
|
|
62
|
+
// after the write already landed (PR #1141 review). Absolute paths stay
|
|
63
|
+
// the documented expectation; enforcement would belong on the INPUT side.
|
|
64
|
+
expect(dirSchema.parse("relative/path")).toBe("relative/path");
|
|
65
|
+
expect(dirSchema.parse("./local")).toBe("./local");
|
|
62
66
|
});
|
|
63
67
|
});
|
|
64
68
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/whoami — bearer → principal resolution (DES-771).
|
|
3
|
+
*
|
|
4
|
+
* The embedded dashboard authenticates with a per-user `aswt_` token and asks
|
|
5
|
+
* this endpoint which user it is acting as (the server forces requester/audit
|
|
6
|
+
* attribution from the token, see user-token-rest-auth.test.ts). The operator
|
|
7
|
+
* key resolves to `kind: "operator"` with no bound user; inactive tokens are
|
|
8
|
+
* rejected by the transport-level auth (401), never mapped to a user.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
12
|
+
import { unlinkSync } from "node:fs";
|
|
13
|
+
import {
|
|
14
|
+
createServer as createHttpServer,
|
|
15
|
+
type IncomingMessage,
|
|
16
|
+
type Server,
|
|
17
|
+
type ServerResponse,
|
|
18
|
+
} from "node:http";
|
|
19
|
+
import { closeDb, createUser, initDb, updateUser } from "../be/db";
|
|
20
|
+
import { type IdentityActor, mintToken, revokeToken } from "../be/users";
|
|
21
|
+
import { handleCore } from "../http/core";
|
|
22
|
+
import { handleUsers } from "../http/users";
|
|
23
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
24
|
+
|
|
25
|
+
const TEST_DB_PATH = "./test-whoami-route.sqlite";
|
|
26
|
+
const API_KEY = "test-api-key";
|
|
27
|
+
const ACTOR: IdentityActor = { kind: "operator", id: "op:test" };
|
|
28
|
+
|
|
29
|
+
interface WhoamiBody {
|
|
30
|
+
kind: "operator" | "user";
|
|
31
|
+
user: { id: string; name: string; status: string } | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createTestServer(): Server {
|
|
35
|
+
return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
36
|
+
const pathSegments = getPathSegments(req.url || "");
|
|
37
|
+
const queryParams = parseQueryParams(req.url || "");
|
|
38
|
+
const myAgentId = req.headers["x-agent-id"] as string | undefined;
|
|
39
|
+
|
|
40
|
+
if (await handleCore(req, res, myAgentId, API_KEY)) return;
|
|
41
|
+
if (await handleUsers(req, res, pathSegments, queryParams)) return;
|
|
42
|
+
|
|
43
|
+
res.writeHead(404);
|
|
44
|
+
res.end("Not Found");
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function listen(server: Server): Promise<number> {
|
|
49
|
+
await new Promise<void>((resolve) => server.listen(0, resolve));
|
|
50
|
+
const addr = server.address();
|
|
51
|
+
if (!addr || typeof addr === "string") throw new Error("no port");
|
|
52
|
+
return addr.port;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cleanupDb() {
|
|
56
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
57
|
+
try {
|
|
58
|
+
unlinkSync(`${TEST_DB_PATH}${suffix}`);
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe("GET /api/whoami", () => {
|
|
64
|
+
let server: Server;
|
|
65
|
+
let port: number;
|
|
66
|
+
|
|
67
|
+
beforeAll(async () => {
|
|
68
|
+
cleanupDb();
|
|
69
|
+
initDb(TEST_DB_PATH);
|
|
70
|
+
server = createTestServer();
|
|
71
|
+
port = await listen(server);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterAll(() => {
|
|
75
|
+
server.close();
|
|
76
|
+
closeDb();
|
|
77
|
+
cleanupDb();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const whoami = (bearer?: string) =>
|
|
81
|
+
fetch(`http://localhost:${port}/api/whoami`, {
|
|
82
|
+
headers: bearer ? { Authorization: `Bearer ${bearer}` } : {},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("operator key resolves to kind=operator with no user", async () => {
|
|
86
|
+
const res = await whoami(API_KEY);
|
|
87
|
+
expect(res.status).toBe(200);
|
|
88
|
+
const body = (await res.json()) as WhoamiBody;
|
|
89
|
+
expect(body.kind).toBe("operator");
|
|
90
|
+
expect(body.user).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("active user token resolves to its bound user", async () => {
|
|
94
|
+
const user = createUser({ name: "Whoami User", email: "whoami@example.com" });
|
|
95
|
+
const { plaintext } = mintToken(user.id, "rest", ACTOR);
|
|
96
|
+
|
|
97
|
+
const res = await whoami(plaintext);
|
|
98
|
+
expect(res.status).toBe(200);
|
|
99
|
+
const body = (await res.json()) as WhoamiBody;
|
|
100
|
+
expect(body.kind).toBe("user");
|
|
101
|
+
expect(body.user?.id).toBe(user.id);
|
|
102
|
+
expect(body.user?.name).toBe("Whoami User");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("revoked token is rejected at auth (401)", async () => {
|
|
106
|
+
const user = createUser({ name: "Revoked User" });
|
|
107
|
+
const { plaintext, tokenId } = mintToken(user.id, "rest", ACTOR);
|
|
108
|
+
revokeToken(tokenId, ACTOR);
|
|
109
|
+
|
|
110
|
+
const res = await whoami(plaintext);
|
|
111
|
+
expect(res.status).toBe(401);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("suspended user's token is rejected at auth (401)", async () => {
|
|
115
|
+
const user = createUser({ name: "Suspended User" });
|
|
116
|
+
const { plaintext } = mintToken(user.id, "rest", ACTOR);
|
|
117
|
+
updateUser(user.id, { status: "suspended" });
|
|
118
|
+
|
|
119
|
+
const res = await whoami(plaintext);
|
|
120
|
+
expect(res.status).toBe(401);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("missing bearer is rejected (401)", async () => {
|
|
124
|
+
const res = await whoami();
|
|
125
|
+
expect(res.status).toBe(401);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -786,6 +786,81 @@ describe("Workflow Engine v2 (Phase 3)", () => {
|
|
|
786
786
|
const nodeIds = steps.map((s) => s.nodeId);
|
|
787
787
|
expect(nodeIds).not.toContain("after");
|
|
788
788
|
});
|
|
789
|
+
|
|
790
|
+
test("unresolved inline script-body tokens fail with the node and token named", async () => {
|
|
791
|
+
const registry = createTestRegistry();
|
|
792
|
+
registry.register(new ScriptExecutor(mockDeps));
|
|
793
|
+
const workflow = makeWorkflow({
|
|
794
|
+
nodes: [
|
|
795
|
+
{
|
|
796
|
+
id: "unsafe-script",
|
|
797
|
+
type: "script",
|
|
798
|
+
config: { runtime: "bash", script: "echo {{trigger.payload}}" },
|
|
799
|
+
},
|
|
800
|
+
],
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
const runId = await startWorkflowExecution(workflow, { payload: "attacker-data" }, registry);
|
|
804
|
+
const run = getWorkflowRun(runId);
|
|
805
|
+
const [step] = getWorkflowRunStepsByRunId(runId);
|
|
806
|
+
|
|
807
|
+
expect(run?.status).toBe("failed");
|
|
808
|
+
expect(run?.error).toContain('node "unsafe-script"');
|
|
809
|
+
expect(run?.error).toContain("{{trigger.payload}}");
|
|
810
|
+
expect(run?.error).toContain("config.args");
|
|
811
|
+
expect(step?.status).toBe("failed");
|
|
812
|
+
expect(step?.error).toContain("{{trigger.payload}}");
|
|
813
|
+
expect(step?.retryCount).toBe(0);
|
|
814
|
+
expect(step?.output).toBeUndefined();
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test("bundled ralph-loop scripts execute with trigger and upstream values passed as args", async () => {
|
|
818
|
+
const receipt = (await Bun.file(
|
|
819
|
+
new URL("../../docs-site/public/receipts/workflows/ralph-loop.json", import.meta.url),
|
|
820
|
+
).json()) as { definition: WorkflowDefinition };
|
|
821
|
+
const receiptNode = (id: string) => {
|
|
822
|
+
const node = receipt.definition.nodes.find((candidate) => candidate.id === id);
|
|
823
|
+
if (!node) throw new Error(`Missing ${id} in bundled ralph-loop receipt`);
|
|
824
|
+
return node;
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
const registry = createTestRegistry();
|
|
828
|
+
registry.register(new ScriptExecutor(mockDeps));
|
|
829
|
+
const workflow = makeWorkflow({
|
|
830
|
+
nodes: [
|
|
831
|
+
{ ...receiptNode("validate-trigger"), next: "ralph-iteration" },
|
|
832
|
+
{
|
|
833
|
+
id: "ralph-iteration",
|
|
834
|
+
type: "script",
|
|
835
|
+
config: {
|
|
836
|
+
runtime: "bash",
|
|
837
|
+
script: `jq -n '{taskOutput:{iteration:1}}'`,
|
|
838
|
+
},
|
|
839
|
+
next: "max-check",
|
|
840
|
+
},
|
|
841
|
+
{ ...receiptNode("max-check"), next: undefined },
|
|
842
|
+
],
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
const runId = await startWorkflowExecution(
|
|
846
|
+
workflow,
|
|
847
|
+
{ goal: "ship safely", maxIterations: 3 },
|
|
848
|
+
registry,
|
|
849
|
+
);
|
|
850
|
+
const run = getWorkflowRun(runId);
|
|
851
|
+
const steps = getWorkflowRunStepsByRunId(runId);
|
|
852
|
+
|
|
853
|
+
expect(run?.status).toBe("completed");
|
|
854
|
+
expect(steps.map((step) => [step.nodeId, step.status])).toEqual([
|
|
855
|
+
["validate-trigger", "completed"],
|
|
856
|
+
["ralph-iteration", "completed"],
|
|
857
|
+
["max-check", "completed"],
|
|
858
|
+
]);
|
|
859
|
+
expect(run?.context).toMatchObject({
|
|
860
|
+
"validate-trigger": { ok: true, goal: "ship safely", maxIterations: 3 },
|
|
861
|
+
"max-check": { atMax: false, iteration: 1, max: 3 },
|
|
862
|
+
});
|
|
863
|
+
});
|
|
789
864
|
});
|
|
790
865
|
|
|
791
866
|
// ─── Context Interpolation ────────────────────────────────
|
|
@@ -836,9 +911,7 @@ describe("interpolateNodeConfig — script node", () => {
|
|
|
836
911
|
|
|
837
912
|
const script = (value as { script: string }).script;
|
|
838
913
|
expect(script).not.toContain("curl attacker.example");
|
|
839
|
-
|
|
840
|
-
// token), not substituted with the attacker-controlled value.
|
|
841
|
-
expect(script).toBe("echo ");
|
|
914
|
+
expect(script).toBe("echo {{trigger.payload}}");
|
|
842
915
|
expect(unresolved).toContain("trigger.payload");
|
|
843
916
|
});
|
|
844
917
|
|
|
@@ -900,7 +973,7 @@ describe("interpolateNodeConfig — script node", () => {
|
|
|
900
973
|
|
|
901
974
|
const script = (value as { script: string }).script;
|
|
902
975
|
expect(script).not.toContain("curl attacker.example");
|
|
903
|
-
expect(script).toBe("echo ");
|
|
976
|
+
expect(script).toBe("echo {{payload}}");
|
|
904
977
|
expect(unresolved).toContain("payload");
|
|
905
978
|
});
|
|
906
979
|
|
|
@@ -914,7 +987,7 @@ describe("interpolateNodeConfig — script node", () => {
|
|
|
914
987
|
|
|
915
988
|
const { value } = interpolateNodeConfig(node, ctx);
|
|
916
989
|
|
|
917
|
-
expect((value as { script: string }).script).toBe("echo ");
|
|
990
|
+
expect((value as { script: string }).script).toBe("echo {{input}}");
|
|
918
991
|
});
|
|
919
992
|
|
|
920
993
|
test("an inputs alias pointing at workflow input is still interpolated into the script", () => {
|
|
@@ -940,7 +1013,7 @@ describe("interpolateNodeConfig — script node", () => {
|
|
|
940
1013
|
|
|
941
1014
|
const { value, unresolved } = interpolateNodeConfig(node, ctx);
|
|
942
1015
|
|
|
943
|
-
expect((value as { script: string }).script).toBe("echo ");
|
|
1016
|
+
expect((value as { script: string }).script).toBe("echo {{fetchStep.stdout}}");
|
|
944
1017
|
expect(unresolved).toContain("fetchStep.stdout");
|
|
945
1018
|
});
|
|
946
1019
|
|
|
@@ -954,6 +1027,6 @@ describe("interpolateNodeConfig — script node", () => {
|
|
|
954
1027
|
|
|
955
1028
|
const { value } = interpolateNodeConfig(node, ctx);
|
|
956
1029
|
|
|
957
|
-
expect((value as { script: string }).script).toBe("echo ");
|
|
1030
|
+
expect((value as { script: string }).script).toBe("echo {{prev}}");
|
|
958
1031
|
});
|
|
959
1032
|
});
|