@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
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
type Server,
|
|
6
6
|
type ServerResponse,
|
|
7
7
|
} from "node:http";
|
|
8
|
-
import {
|
|
8
|
+
import { createApp } from "../apps/store";
|
|
9
|
+
import { closeDb, createAgent, createTaskExtended, createUser, getDb, initDb } from "../be/db";
|
|
10
|
+
import { insertScript } from "../be/scripts/db";
|
|
9
11
|
import { handleAssets } from "../http/assets";
|
|
10
12
|
import { handlePages } from "../http/pages";
|
|
11
13
|
import { handleSchedules } from "../http/schedules";
|
|
@@ -146,9 +148,44 @@ describe("asset namespace REST contract", () => {
|
|
|
146
148
|
expect(page.status).toBe(201);
|
|
147
149
|
expect(page.body.key).toBe("shared/team/");
|
|
148
150
|
|
|
151
|
+
const app = createApp({
|
|
152
|
+
name: "Namespaced app",
|
|
153
|
+
definition: { models: {}, pages: {}, defaultPage: "main" } as never,
|
|
154
|
+
});
|
|
155
|
+
const script = insertScript({
|
|
156
|
+
name: `namespaced-script-${Date.now()}`,
|
|
157
|
+
scope: "agent",
|
|
158
|
+
scopeId: agentId,
|
|
159
|
+
source: "export default async function () { return { ok: true }; }",
|
|
160
|
+
description: "Namespaced script",
|
|
161
|
+
intent: "Exercise asset namespace moves",
|
|
162
|
+
signatureJson: "{}",
|
|
163
|
+
agentId,
|
|
164
|
+
embeddingMode: "skip",
|
|
165
|
+
});
|
|
166
|
+
expect(
|
|
167
|
+
await api("PATCH", `/api/assets/app/${app.id}/key`, { key: "shared/team/" }, { agentId }),
|
|
168
|
+
).toMatchObject({ status: 200, body: { entityType: "app", key: "shared/team/" } });
|
|
169
|
+
expect(
|
|
170
|
+
await api(
|
|
171
|
+
"PATCH",
|
|
172
|
+
`/api/assets/script/${script.id}/key`,
|
|
173
|
+
{ key: "shared/team/" },
|
|
174
|
+
{ agentId },
|
|
175
|
+
),
|
|
176
|
+
).toMatchObject({ status: 200, body: { entityType: "script", key: "shared/team/" } });
|
|
177
|
+
expect(
|
|
178
|
+
await api(
|
|
179
|
+
"PATCH",
|
|
180
|
+
`/api/assets/script/${script.id}/key`,
|
|
181
|
+
{ key: "shared/other/" },
|
|
182
|
+
{ agentId: secondAgentId },
|
|
183
|
+
),
|
|
184
|
+
).toMatchObject({ status: 403 });
|
|
185
|
+
|
|
149
186
|
const aggregate = await api(
|
|
150
187
|
"GET",
|
|
151
|
-
"/api/assets?keyPrefix=shared/team&types=task,workflow,schedule,page",
|
|
188
|
+
"/api/assets?keyPrefix=shared/team&types=task,workflow,schedule,page,app,script",
|
|
152
189
|
);
|
|
153
190
|
expect(aggregate.status).toBe(200);
|
|
154
191
|
expect(aggregate.body.assets).toEqual(
|
|
@@ -165,8 +202,17 @@ describe("asset namespace REST contract", () => {
|
|
|
165
202
|
key: "shared/team/",
|
|
166
203
|
}),
|
|
167
204
|
expect.objectContaining({ entityType: "page", id: page.body.id, key: "shared/team/" }),
|
|
205
|
+
expect.objectContaining({ entityType: "app", id: app.id, key: "shared/team/" }),
|
|
206
|
+
expect.objectContaining({ entityType: "script", id: script.id, key: "shared/team/" }),
|
|
168
207
|
]),
|
|
169
208
|
);
|
|
209
|
+
expect(
|
|
210
|
+
getDb()
|
|
211
|
+
.prepare<{ entity_type: string }, [string, string]>(
|
|
212
|
+
"SELECT entity_type FROM asset_key_history WHERE entity_id IN (?, ?) ORDER BY entity_type",
|
|
213
|
+
)
|
|
214
|
+
.all(app.id, script.id),
|
|
215
|
+
).toEqual([{ entity_type: "app" }, { entity_type: "script" }]);
|
|
170
216
|
expect(JSON.stringify(aggregate.body)).not.toContain("namespaced task");
|
|
171
217
|
expect(JSON.stringify(aggregate.body)).not.toContain("private content omitted");
|
|
172
218
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createApp } from "../apps/store";
|
|
2
3
|
import { auditAssetKeys } from "../be/asset-key-audit";
|
|
3
4
|
import {
|
|
4
5
|
closeDb,
|
|
@@ -17,17 +18,35 @@ import {
|
|
|
17
18
|
moveAssetKey,
|
|
18
19
|
upsertAssetKeyMapping,
|
|
19
20
|
} from "../be/db";
|
|
21
|
+
import { insertScript } from "../be/scripts/db";
|
|
20
22
|
import { createStandaloneScheduleTask } from "../scheduler/scheduler";
|
|
21
23
|
|
|
22
24
|
const TEST_DB_PATH = "./test-asset-key-invariants.sqlite";
|
|
23
25
|
|
|
24
26
|
let agentId: string;
|
|
25
27
|
let userId: string;
|
|
28
|
+
let appId: string;
|
|
29
|
+
let scriptId: string;
|
|
26
30
|
|
|
27
31
|
beforeAll(() => {
|
|
28
32
|
initDb(TEST_DB_PATH);
|
|
29
33
|
agentId = createAgent({ name: "namespace-worker", isLead: false, status: "idle" }).id;
|
|
30
34
|
userId = createUser({ name: "Namespace User", email: "namespace@example.com" }).id;
|
|
35
|
+
appId = createApp({
|
|
36
|
+
name: "Default app",
|
|
37
|
+
definition: { models: {}, pages: {}, defaultPage: "main" } as never,
|
|
38
|
+
}).id;
|
|
39
|
+
scriptId = insertScript({
|
|
40
|
+
name: "default-script",
|
|
41
|
+
scope: "agent",
|
|
42
|
+
scopeId: agentId,
|
|
43
|
+
source: "export default async function () { return { ok: true }; }",
|
|
44
|
+
description: "Asset namespace test script",
|
|
45
|
+
intent: "Verify script asset keys",
|
|
46
|
+
signatureJson: "{}",
|
|
47
|
+
agentId,
|
|
48
|
+
embeddingMode: "skip",
|
|
49
|
+
}).id;
|
|
31
50
|
});
|
|
32
51
|
|
|
33
52
|
afterAll(() => {
|
|
@@ -59,9 +78,30 @@ describe("cross-entity asset namespace invariants", () => {
|
|
|
59
78
|
`shared/schedule:${schedule.id}/`,
|
|
60
79
|
`shared/page:${page.id}/`,
|
|
61
80
|
]);
|
|
81
|
+
expect(
|
|
82
|
+
getDb()
|
|
83
|
+
.prepare<{ key: string }, [string]>('SELECT "key" AS key FROM apps WHERE id = ?')
|
|
84
|
+
.get(appId)?.key,
|
|
85
|
+
).toBe(`shared/app:${appId}/`);
|
|
86
|
+
expect(
|
|
87
|
+
getDb()
|
|
88
|
+
.prepare<{ key: string }, [string]>('SELECT "key" AS key FROM scripts WHERE id = ?')
|
|
89
|
+
.get(scriptId)?.key,
|
|
90
|
+
).toBe(`shared/script:${scriptId}/`);
|
|
62
91
|
expect(auditAssetKeys(getDb()).fatalCount).toBe(0);
|
|
63
92
|
});
|
|
64
93
|
|
|
94
|
+
test("app and script triggers reject malformed keys", () => {
|
|
95
|
+
for (const [table, id] of [
|
|
96
|
+
["apps", appId],
|
|
97
|
+
["scripts", scriptId],
|
|
98
|
+
] as const) {
|
|
99
|
+
expect(() =>
|
|
100
|
+
getDb().run(`UPDATE ${table} SET "key" = ? WHERE id = ?`, ["Shared/invalid", id]),
|
|
101
|
+
).toThrow("invalid asset namespace key");
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
65
105
|
test("children inherit a parent namespace unless explicitly overridden", () => {
|
|
66
106
|
const parent = createTaskExtended("parent", { agentId, key: "shared/projects/" });
|
|
67
107
|
const child = createTaskExtended("child", { parentTaskId: parent.id });
|
|
@@ -167,11 +207,63 @@ describe("cross-entity asset namespace invariants", () => {
|
|
|
167
207
|
expect(summaries.some((asset) => asset.entityType === "workflow")).toBe(true);
|
|
168
208
|
expect(summaries.some((asset) => asset.entityType === "schedule")).toBe(true);
|
|
169
209
|
expect(summaries.some((asset) => asset.entityType === "page")).toBe(true);
|
|
210
|
+
expect(summaries.some((asset) => asset.entityType === "app" && asset.id === appId)).toBe(true);
|
|
211
|
+
expect(summaries.some((asset) => asset.entityType === "script" && asset.id === scriptId)).toBe(
|
|
212
|
+
true,
|
|
213
|
+
);
|
|
170
214
|
expect(summaries.some((asset) => asset.entityType === "file")).toBe(true);
|
|
215
|
+
const expectedChecked = getDb()
|
|
216
|
+
.prepare<{ count: number }, []>(
|
|
217
|
+
`SELECT
|
|
218
|
+
(SELECT COUNT(*) FROM agent_tasks) +
|
|
219
|
+
(SELECT COUNT(*) FROM workflows) +
|
|
220
|
+
(SELECT COUNT(*) FROM scheduled_tasks) +
|
|
221
|
+
(SELECT COUNT(*) FROM pages) +
|
|
222
|
+
(SELECT COUNT(*) FROM apps) +
|
|
223
|
+
(SELECT COUNT(*) FROM scripts) +
|
|
224
|
+
(SELECT COUNT(*) FROM asset_key_mappings) AS count`,
|
|
225
|
+
)
|
|
226
|
+
.get()!.count;
|
|
227
|
+
expect(auditAssetKeys(getDb()).checked).toBe(expectedChecked);
|
|
171
228
|
expect(JSON.stringify(summaries)).not.toContain("scheduled work");
|
|
172
229
|
expect(JSON.stringify(summaries)).not.toContain("<p>ok</p>");
|
|
173
230
|
});
|
|
174
231
|
|
|
232
|
+
test("app and script moves update keys and write typed history rows", () => {
|
|
233
|
+
expect(
|
|
234
|
+
moveAssetKey({ entityType: "app", id: appId, key: "shared/products/", changedBy: userId }),
|
|
235
|
+
).toBe(true);
|
|
236
|
+
expect(
|
|
237
|
+
moveAssetKey({
|
|
238
|
+
entityType: "script",
|
|
239
|
+
id: scriptId,
|
|
240
|
+
key: "shared/automation/",
|
|
241
|
+
changedBy: userId,
|
|
242
|
+
}),
|
|
243
|
+
).toBe(true);
|
|
244
|
+
|
|
245
|
+
expect(
|
|
246
|
+
getDb()
|
|
247
|
+
.prepare<{ key: string }, [string]>('SELECT "key" AS key FROM apps WHERE id = ?')
|
|
248
|
+
.get(appId),
|
|
249
|
+
).toEqual({ key: "shared/products/" });
|
|
250
|
+
expect(
|
|
251
|
+
getDb()
|
|
252
|
+
.prepare<{ key: string }, [string]>('SELECT "key" AS key FROM scripts WHERE id = ?')
|
|
253
|
+
.get(scriptId),
|
|
254
|
+
).toEqual({ key: "shared/automation/" });
|
|
255
|
+
expect(
|
|
256
|
+
getDb()
|
|
257
|
+
.prepare<{ entity_type: string; new_key: string }, [string, string]>(
|
|
258
|
+
"SELECT entity_type, new_key FROM asset_key_history WHERE entity_id IN (?, ?) ORDER BY entity_type",
|
|
259
|
+
)
|
|
260
|
+
.all(appId, scriptId),
|
|
261
|
+
).toEqual([
|
|
262
|
+
{ entity_type: "app", new_key: "shared/products/" },
|
|
263
|
+
{ entity_type: "script", new_key: "shared/automation/" },
|
|
264
|
+
]);
|
|
265
|
+
});
|
|
266
|
+
|
|
175
267
|
test("prefix filters treat SQL wildcard characters as literal key content", () => {
|
|
176
268
|
const literal = createTaskExtended("literal wildcard", { agentId, key: "shared/percent%/" });
|
|
177
269
|
const neighbor = createTaskExtended("wildcard neighbor", { agentId, key: "shared/percentx/" });
|
|
@@ -3,6 +3,7 @@ import { closeDb, createTaskExtended, createUser, getDb, initDb } from "../be/db
|
|
|
3
3
|
|
|
4
4
|
const FRESH_DB = "./test-asset-key-migration-fresh.sqlite";
|
|
5
5
|
const HISTORICAL_DB = "./test-asset-key-migration-historical.sqlite";
|
|
6
|
+
const HISTORY_REBUILD_DB = "./test-asset-key-migration-history-rebuild.sqlite";
|
|
6
7
|
const LEGACY_STATUS_DB = "./test-asset-key-migration-legacy-status.sqlite";
|
|
7
8
|
|
|
8
9
|
const globals = globalThis as typeof globalThis & {
|
|
@@ -33,15 +34,59 @@ function dropMigration115ForHistoricalFixture(): void {
|
|
|
33
34
|
"idx_workflows_asset_key",
|
|
34
35
|
"idx_scheduled_tasks_asset_key",
|
|
35
36
|
"idx_pages_asset_key",
|
|
37
|
+
"idx_apps_asset_key",
|
|
38
|
+
"idx_scripts_asset_key",
|
|
36
39
|
]) {
|
|
37
40
|
db.run(`DROP INDEX IF EXISTS "${index}"`);
|
|
38
41
|
}
|
|
39
42
|
db.run("DROP TABLE asset_key_history");
|
|
40
43
|
db.run("DROP TABLE asset_key_mappings");
|
|
41
|
-
for (const table of ["agent_tasks", "workflows", "scheduled_tasks", "pages"]) {
|
|
44
|
+
for (const table of ["agent_tasks", "workflows", "scheduled_tasks", "pages", "apps", "scripts"]) {
|
|
42
45
|
db.run(`ALTER TABLE "${table}" DROP COLUMN "key"`);
|
|
43
46
|
}
|
|
44
|
-
db.run("DELETE FROM _migrations WHERE version
|
|
47
|
+
db.run("DELETE FROM _migrations WHERE version IN (115, 129)");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function dropMigration129ForHistoricalFixture(): void {
|
|
51
|
+
const db = getDb();
|
|
52
|
+
for (const trigger of [
|
|
53
|
+
"validate_apps_asset_key_insert",
|
|
54
|
+
"validate_apps_asset_key_update",
|
|
55
|
+
"validate_scripts_asset_key_insert",
|
|
56
|
+
"validate_scripts_asset_key_update",
|
|
57
|
+
]) {
|
|
58
|
+
db.run(`DROP TRIGGER "${trigger}"`);
|
|
59
|
+
}
|
|
60
|
+
db.run("DROP INDEX idx_apps_asset_key");
|
|
61
|
+
db.run("DROP INDEX idx_scripts_asset_key");
|
|
62
|
+
db.run('ALTER TABLE apps DROP COLUMN "key"');
|
|
63
|
+
db.run('ALTER TABLE scripts DROP COLUMN "key"');
|
|
64
|
+
|
|
65
|
+
db.run(`
|
|
66
|
+
CREATE TABLE asset_key_history_before_129 (
|
|
67
|
+
id TEXT PRIMARY KEY,
|
|
68
|
+
entity_type TEXT NOT NULL CHECK (entity_type IN ('task', 'workflow', 'schedule', 'page', 'file')),
|
|
69
|
+
entity_id TEXT NOT NULL,
|
|
70
|
+
previous_key TEXT,
|
|
71
|
+
new_key TEXT NOT NULL,
|
|
72
|
+
changed_by TEXT REFERENCES users(id),
|
|
73
|
+
changed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
74
|
+
)
|
|
75
|
+
`);
|
|
76
|
+
db.run(`
|
|
77
|
+
INSERT INTO asset_key_history_before_129 (
|
|
78
|
+
id, entity_type, entity_id, previous_key, new_key, changed_by, changed_at
|
|
79
|
+
)
|
|
80
|
+
SELECT id, entity_type, entity_id, previous_key, new_key, changed_by, changed_at
|
|
81
|
+
FROM asset_key_history
|
|
82
|
+
`);
|
|
83
|
+
db.run("DROP TABLE asset_key_history");
|
|
84
|
+
db.run("ALTER TABLE asset_key_history_before_129 RENAME TO asset_key_history");
|
|
85
|
+
db.run(`
|
|
86
|
+
CREATE INDEX idx_asset_key_history_entity
|
|
87
|
+
ON asset_key_history(entity_type, entity_id, changed_at DESC)
|
|
88
|
+
`);
|
|
89
|
+
db.run("DELETE FROM _migrations WHERE version = 129");
|
|
45
90
|
}
|
|
46
91
|
|
|
47
92
|
beforeAll(async () => {
|
|
@@ -50,6 +95,7 @@ beforeAll(async () => {
|
|
|
50
95
|
closeDb();
|
|
51
96
|
await removeDb(FRESH_DB);
|
|
52
97
|
await removeDb(HISTORICAL_DB);
|
|
98
|
+
await removeDb(HISTORY_REBUILD_DB);
|
|
53
99
|
await removeDb(LEGACY_STATUS_DB);
|
|
54
100
|
initDb(FRESH_DB);
|
|
55
101
|
});
|
|
@@ -60,12 +106,20 @@ afterAll(async () => {
|
|
|
60
106
|
globals.__savedAssetKeyTemplate = undefined;
|
|
61
107
|
await removeDb(FRESH_DB);
|
|
62
108
|
await removeDb(HISTORICAL_DB);
|
|
109
|
+
await removeDb(HISTORY_REBUILD_DB);
|
|
63
110
|
await removeDb(LEGACY_STATUS_DB);
|
|
64
111
|
});
|
|
65
112
|
|
|
66
|
-
describe("
|
|
113
|
+
describe("asset namespace key migrations", () => {
|
|
67
114
|
test("fresh schema has mandatory defaults, non-unique indexes, triggers, and mapping tables", () => {
|
|
68
|
-
for (const table of [
|
|
115
|
+
for (const table of [
|
|
116
|
+
"agent_tasks",
|
|
117
|
+
"workflows",
|
|
118
|
+
"scheduled_tasks",
|
|
119
|
+
"pages",
|
|
120
|
+
"apps",
|
|
121
|
+
"scripts",
|
|
122
|
+
]) {
|
|
69
123
|
const column = getDb()
|
|
70
124
|
.prepare<{ name: string; notnull: number; dflt_value: string | null }, []>(
|
|
71
125
|
`PRAGMA table_info("${table}")`,
|
|
@@ -89,6 +143,8 @@ describe("migration 115 asset namespace keys", () => {
|
|
|
89
143
|
"idx_workflows_asset_key",
|
|
90
144
|
"idx_scheduled_tasks_asset_key",
|
|
91
145
|
"idx_pages_asset_key",
|
|
146
|
+
"idx_apps_asset_key",
|
|
147
|
+
"idx_scripts_asset_key",
|
|
92
148
|
]) {
|
|
93
149
|
expect(indexes.has(index)).toBe(true);
|
|
94
150
|
}
|
|
@@ -98,7 +154,7 @@ describe("migration 115 asset namespace keys", () => {
|
|
|
98
154
|
"SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'trigger' AND name LIKE 'validate_%asset_key%'",
|
|
99
155
|
)
|
|
100
156
|
.get()?.count,
|
|
101
|
-
).toBeGreaterThanOrEqual(
|
|
157
|
+
).toBeGreaterThanOrEqual(12);
|
|
102
158
|
for (const table of ["asset_key_mappings", "asset_key_history"]) {
|
|
103
159
|
expect(
|
|
104
160
|
getDb()
|
|
@@ -108,6 +164,13 @@ describe("migration 115 asset namespace keys", () => {
|
|
|
108
164
|
.get(table)?.present,
|
|
109
165
|
).toBe(1);
|
|
110
166
|
}
|
|
167
|
+
const historySchema = getDb()
|
|
168
|
+
.prepare<{ sql: string }, []>(
|
|
169
|
+
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'asset_key_history'",
|
|
170
|
+
)
|
|
171
|
+
.get()!.sql;
|
|
172
|
+
expect(historySchema).toContain("'app'");
|
|
173
|
+
expect(historySchema).toContain("'script'");
|
|
111
174
|
});
|
|
112
175
|
|
|
113
176
|
test("old insert statements may omit key and repeated shared namespaces are valid", () => {
|
|
@@ -132,8 +195,27 @@ describe("migration 115 asset namespace keys", () => {
|
|
|
132
195
|
`INSERT INTO pages (agentId, slug, title, contentType, authMode, body)
|
|
133
196
|
VALUES ('legacy-agent', 'legacy-page', 'Legacy page', 'text/html', 'authed', '<p>ok</p>')`,
|
|
134
197
|
);
|
|
198
|
+
getDb().run(
|
|
199
|
+
`INSERT INTO apps (id, name, definition, created_at, updated_at)
|
|
200
|
+
VALUES (?, 'legacy app', '{}', ?, ?)`,
|
|
201
|
+
[crypto.randomUUID(), now, now],
|
|
202
|
+
);
|
|
203
|
+
getDb().run(
|
|
204
|
+
`INSERT INTO scripts (
|
|
205
|
+
id, name, scope, source, description, intent, signatureJson,
|
|
206
|
+
contentHash, createdAt, updatedAt
|
|
207
|
+
) VALUES (?, 'legacy-script', 'global', 'export default () => null', '', '', '{}', '', ?, ?)`,
|
|
208
|
+
[crypto.randomUUID(), now, now],
|
|
209
|
+
);
|
|
135
210
|
|
|
136
|
-
for (const table of [
|
|
211
|
+
for (const table of [
|
|
212
|
+
"agent_tasks",
|
|
213
|
+
"workflows",
|
|
214
|
+
"scheduled_tasks",
|
|
215
|
+
"pages",
|
|
216
|
+
"apps",
|
|
217
|
+
"scripts",
|
|
218
|
+
]) {
|
|
137
219
|
const count = getDb()
|
|
138
220
|
.prepare<{ count: number }, []>(
|
|
139
221
|
`SELECT COUNT(*) AS count FROM "${table}" WHERE "key" = 'shared/'`,
|
|
@@ -143,6 +225,63 @@ describe("migration 115 asset namespace keys", () => {
|
|
|
143
225
|
}
|
|
144
226
|
});
|
|
145
227
|
|
|
228
|
+
test("migration 129 preserves existing history rows while widening entity types", () => {
|
|
229
|
+
closeDb();
|
|
230
|
+
initDb(HISTORY_REBUILD_DB);
|
|
231
|
+
|
|
232
|
+
const historyId = crypto.randomUUID();
|
|
233
|
+
const entityId = crypto.randomUUID();
|
|
234
|
+
const user = createUser({ name: "History User", email: "history@example.com" });
|
|
235
|
+
const changedAt = "2026-08-08T12:34:56.789Z";
|
|
236
|
+
getDb().run(
|
|
237
|
+
`INSERT INTO asset_key_history (
|
|
238
|
+
id, entity_type, entity_id, previous_key, new_key, changed_by, changed_at
|
|
239
|
+
) VALUES (?, 'task', ?, 'shared/inbox/', 'shared/archive/', ?, ?)`,
|
|
240
|
+
[historyId, entityId, user.id, changedAt],
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
dropMigration129ForHistoricalFixture();
|
|
244
|
+
closeDb();
|
|
245
|
+
initDb(HISTORY_REBUILD_DB);
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
expect(
|
|
249
|
+
getDb()
|
|
250
|
+
.prepare<
|
|
251
|
+
{
|
|
252
|
+
id: string;
|
|
253
|
+
entity_type: string;
|
|
254
|
+
entity_id: string;
|
|
255
|
+
previous_key: string;
|
|
256
|
+
new_key: string;
|
|
257
|
+
changed_by: string;
|
|
258
|
+
changed_at: string;
|
|
259
|
+
},
|
|
260
|
+
[string]
|
|
261
|
+
>("SELECT * FROM asset_key_history WHERE id = ?")
|
|
262
|
+
.get(historyId),
|
|
263
|
+
).toEqual({
|
|
264
|
+
id: historyId,
|
|
265
|
+
entity_type: "task",
|
|
266
|
+
entity_id: entityId,
|
|
267
|
+
previous_key: "shared/inbox/",
|
|
268
|
+
new_key: "shared/archive/",
|
|
269
|
+
changed_by: user.id,
|
|
270
|
+
changed_at: changedAt,
|
|
271
|
+
});
|
|
272
|
+
expect(
|
|
273
|
+
getDb()
|
|
274
|
+
.prepare<{ count: number }, []>(
|
|
275
|
+
"SELECT COUNT(*) AS count FROM _migrations WHERE version = 129",
|
|
276
|
+
)
|
|
277
|
+
.get()?.count,
|
|
278
|
+
).toBe(1);
|
|
279
|
+
} finally {
|
|
280
|
+
closeDb();
|
|
281
|
+
initDb(FRESH_DB);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
146
285
|
test("triggers reject malformed and unknown-personal keys but allow an existing personal user", () => {
|
|
147
286
|
const taskId = getDb()
|
|
148
287
|
.prepare<{ id: string }, []>("SELECT id FROM agent_tasks LIMIT 1")
|
|
@@ -166,6 +305,13 @@ describe("migration 115 asset namespace keys", () => {
|
|
|
166
305
|
taskId,
|
|
167
306
|
]),
|
|
168
307
|
).not.toThrow();
|
|
308
|
+
|
|
309
|
+
for (const table of ["apps", "scripts"]) {
|
|
310
|
+
const id = getDb().prepare<{ id: string }, []>(`SELECT id FROM ${table} LIMIT 1`).get()!.id;
|
|
311
|
+
expect(() =>
|
|
312
|
+
getDb().run(`UPDATE ${table} SET "key" = ? WHERE id = ?`, ["INVALID", id]),
|
|
313
|
+
).toThrow("invalid asset namespace key");
|
|
314
|
+
}
|
|
169
315
|
});
|
|
170
316
|
|
|
171
317
|
test("upgrades a historical database, backfills attachment mappings, and is repeatable", async () => {
|
|
@@ -176,6 +322,8 @@ describe("migration 115 asset namespace keys", () => {
|
|
|
176
322
|
const now = new Date().toISOString();
|
|
177
323
|
const taskId = crypto.randomUUID();
|
|
178
324
|
const attachmentId = crypto.randomUUID();
|
|
325
|
+
const appId = crypto.randomUUID();
|
|
326
|
+
const scriptId = crypto.randomUUID();
|
|
179
327
|
getDb().run(
|
|
180
328
|
`INSERT INTO agent_tasks (id, task, status, source, createdAt, lastUpdatedAt)
|
|
181
329
|
VALUES (?, 'historical task', 'unassigned', 'api', ?, ?)`,
|
|
@@ -188,6 +336,18 @@ describe("migration 115 asset namespace keys", () => {
|
|
|
188
336
|
'reports/artifact.md', 'org', 'drive')`,
|
|
189
337
|
[attachmentId, taskId],
|
|
190
338
|
);
|
|
339
|
+
getDb().run(
|
|
340
|
+
`INSERT INTO apps (id, name, definition, created_at, updated_at)
|
|
341
|
+
VALUES (?, 'historical app', '{}', ?, ?)`,
|
|
342
|
+
[appId, now, now],
|
|
343
|
+
);
|
|
344
|
+
getDb().run(
|
|
345
|
+
`INSERT INTO scripts (
|
|
346
|
+
id, name, scope, source, description, intent, signatureJson,
|
|
347
|
+
contentHash, createdAt, updatedAt
|
|
348
|
+
) VALUES (?, 'historical-script', 'global', 'export default () => null', '', '', '{}', '', ?, ?)`,
|
|
349
|
+
[scriptId, now, now],
|
|
350
|
+
);
|
|
191
351
|
closeDb();
|
|
192
352
|
|
|
193
353
|
initDb(HISTORICAL_DB);
|
|
@@ -201,16 +361,22 @@ describe("migration 115 asset namespace keys", () => {
|
|
|
201
361
|
)
|
|
202
362
|
.get("reports/artifact.md"),
|
|
203
363
|
).toEqual({ key: "shared/", source_entity_id: attachmentId });
|
|
364
|
+
expect(getDb().query('SELECT "key" FROM apps WHERE id = ?').get(appId)).toEqual({
|
|
365
|
+
key: "shared/",
|
|
366
|
+
});
|
|
367
|
+
expect(getDb().query('SELECT "key" FROM scripts WHERE id = ?').get(scriptId)).toEqual({
|
|
368
|
+
key: "shared/",
|
|
369
|
+
});
|
|
204
370
|
|
|
205
371
|
closeDb();
|
|
206
372
|
expect(() => initDb(HISTORICAL_DB)).not.toThrow();
|
|
207
373
|
expect(
|
|
208
374
|
getDb()
|
|
209
375
|
.prepare<{ count: number }, []>(
|
|
210
|
-
"SELECT COUNT(*) AS count FROM _migrations WHERE version
|
|
376
|
+
"SELECT COUNT(*) AS count FROM _migrations WHERE version IN (115, 129)",
|
|
211
377
|
)
|
|
212
378
|
.get()?.count,
|
|
213
|
-
).toBe(
|
|
379
|
+
).toBe(2);
|
|
214
380
|
});
|
|
215
381
|
|
|
216
382
|
test("preserves asset keys when upgrading a legacy restrictive task status schema", () => {
|
|
@@ -34,6 +34,8 @@ describe("asset namespace key normalization", () => {
|
|
|
34
34
|
test("builds resource-specific defaults inside the shared namespace", () => {
|
|
35
35
|
expect(defaultAssetKey("task", "ABC-123")).toBe("shared/task:abc-123/");
|
|
36
36
|
expect(defaultAssetKey("workflow", "workflow-id")).toBe("shared/workflow:workflow-id/");
|
|
37
|
+
expect(defaultAssetKey("app", "APP-ID")).toBe("shared/app:app-id/");
|
|
38
|
+
expect(defaultAssetKey("script", "SCRIPT-ID")).toBe("shared/script:script-id/");
|
|
37
39
|
expect(defaultAssetKey("fs:agent-fs", "mapping-id")).toBe("shared/fs:agent-fs:mapping-id/");
|
|
38
40
|
});
|
|
39
41
|
|
|
@@ -328,7 +328,7 @@ describe("credential broker", () => {
|
|
|
328
328
|
expect(authorization).toBe("Bearer ghp_secret");
|
|
329
329
|
});
|
|
330
330
|
|
|
331
|
-
test("
|
|
331
|
+
test("drops a placeholder header for non-allowlisted hosts instead of forwarding the literal", async () => {
|
|
332
332
|
let authorization: string | null = null;
|
|
333
333
|
globalThis.fetch = (async (_input: string | URL | Request, init?: RequestInit) => {
|
|
334
334
|
authorization = new Headers(init?.headers).get("authorization");
|
|
@@ -352,7 +352,32 @@ describe("credential broker", () => {
|
|
|
352
352
|
headers: { Authorization: "Bearer [REDACTED:GITHUB_TOKEN]" },
|
|
353
353
|
});
|
|
354
354
|
|
|
355
|
-
|
|
355
|
+
// The literal placeholder is useless to any provider and must never egress.
|
|
356
|
+
expect(authorization).toBeNull();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("drops a placeholder header when no binding resolves (fresh install)", async () => {
|
|
360
|
+
let authorization: string | null = null;
|
|
361
|
+
let accept: string | null = null;
|
|
362
|
+
globalThis.fetch = (async (_input: string | URL | Request, init?: RequestInit) => {
|
|
363
|
+
const headers = new Headers(init?.headers);
|
|
364
|
+
authorization = headers.get("authorization");
|
|
365
|
+
accept = headers.get("accept");
|
|
366
|
+
return Response.json([]);
|
|
367
|
+
}) as typeof fetch;
|
|
368
|
+
|
|
369
|
+
patchFetchWithCredentialBroker([]);
|
|
370
|
+
|
|
371
|
+
await fetch("https://api.github.com/repos/owner/name/issues", {
|
|
372
|
+
headers: {
|
|
373
|
+
Accept: "application/vnd.github+json",
|
|
374
|
+
Authorization: "Bearer [REDACTED:GITHUB_TOKEN]",
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// No GITHUB_TOKEN configured: auth is omitted so public repos still pull.
|
|
379
|
+
expect(authorization).toBeNull();
|
|
380
|
+
expect(accept).toBe("application/vnd.github+json");
|
|
356
381
|
});
|
|
357
382
|
|
|
358
383
|
test("substitutes query placeholders for allowlisted hosts", async () => {
|