@desplega.ai/agent-swarm 1.156.0 → 1.158.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/dist/{acp-adapter-c1msptrw.js → acp-adapter-xaq4n6pc.js} +1 -1
- package/dist/{actions-wd41qtdc.js → actions-p9hz5yat.js} +15 -15
- package/dist/{app-tz2qrnah.js → app-g26wdcz6.js} +4 -4
- package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
- package/dist/{assistant-c3q1zj2t.js → assistant-wydfqkb3.js} +16 -16
- package/dist/{boot-reembed-0be1krkm.js → boot-reembed-g5ckg9yb.js} +6 -6
- package/dist/{boot-reembed-r00j03f3.js → boot-reembed-se8zmdcp.js} +5 -5
- package/dist/{boot-scrub-logs-9j4hd0vw.js → boot-scrub-logs-wfxqan61.js} +3 -3
- package/dist/{seed-pricing-ncgxz1r2.js → boot-seeds-s3q01xj3.js} +10 -10
- package/dist/{claude-adapter-112w28qc.js → claude-adapter-tv2x4f36.js} +7 -7
- package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
- package/dist/{claude-sdk-session-k7pb5kfz.js → claude-sdk-session-t442qz6f.js} +8 -8
- package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
- package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
- package/dist/{cli-mx3bzbv8.js → cli-20cj37vt.js} +1968 -44
- package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
- package/dist/{cli-24w39x6n.js → cli-47m6dkd3.js} +362 -126
- package/dist/{cli-90x9ewbz.js → cli-53sz8gkz.js} +1 -1
- package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
- package/dist/{cli-08n8jxcw.js → cli-65hjmev5.js} +57 -17
- package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
- package/dist/{cli-7j2098n5.js → cli-7gtvvpbh.js} +4 -4
- package/dist/{cli-b05d7szr.js → cli-7s3syk22.js} +18 -11
- package/dist/{cli-9w4ex8vh.js → cli-8qmme6vd.js} +2 -2
- package/dist/{cli-28wkjpdp.js → cli-987hg0aa.js} +2 -2
- package/dist/{cli-7pgcpcrg.js → cli-9httdpyq.js} +1 -1
- package/dist/{cli-jk26c1sz.js → cli-aky8tctm.js} +26 -93
- package/dist/{cli-865swy5p.js → cli-c07r9a2m.js} +3 -2
- package/dist/{cli-nyqza443.js → cli-fgqxwrpv.js} +291 -12
- package/dist/{cli-9pyah9c4.js → cli-ftfvggzx.js} +633 -166
- package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
- package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
- package/dist/{cli-y8bfxjtw.js → cli-j55n64b6.js} +23 -22
- package/dist/{cli-7vj52450.js → cli-j57qkrpq.js} +86 -28
- package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
- package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
- package/dist/{cli-tp0d7vzb.js → cli-rjehvrts.js} +4 -4
- package/dist/{cli-s8hpxeg4.js → cli-rqz8qbw7.js} +245 -75
- package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
- package/dist/cli-t1ze64h3.js +811 -0
- package/dist/{cli-hc2fkhdv.js → cli-ws6rqycm.js} +3 -2
- package/dist/{cli-t1cd3mhp.js → cli-wscdvcqz.js} +13 -5
- package/dist/{cli-xa4j64zh.js → cli-x24pshtq.js} +5 -5
- package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
- package/dist/{cli-szcyv1mm.js → cli-xhaeg2nc.js} +1 -1
- package/dist/cli.js +30 -16
- package/dist/{codex-adapter-g4tj1f2g.js → codex-adapter-cnm8wk11.js} +5 -5
- package/dist/{codex-session-runner-ad8ha7zt.js → codex-session-runner-gfg547zj.js} +5 -5
- package/dist/{commands-z3gd8ppc.js → commands-e3bn0p75.js} +3 -3
- package/dist/{db-bezm97r9.js → db-03v9kzr2.js} +17 -3
- package/dist/{dispatcher-a97hc33y.js → dispatcher-nrmg8yht.js} +14 -14
- package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
- package/dist/{handlers-wtdy1x02.js → handlers-aqcedbyb.js} +17 -17
- package/dist/{hook-7edgmzng.js → hook-7vkskkre.js} +19 -11
- package/dist/{hook-fvs58a5b.js → hook-ybvmrmhq.js} +4 -4
- package/dist/{http-phnf93hp.js → http-2eqa8t24.js} +432 -195
- package/dist/{index-7x131mmf.js → index-1yks3qcr.js} +14 -14
- package/dist/{index-1fktvwxc.js → index-a3vn96n8.js} +31 -28
- package/dist/{index-yp8rasxk.js → index-c6zsv1bd.js} +5 -5
- package/dist/{index-50jxgw0d.js → index-chgdkq36.js} +14 -14
- package/dist/{index-7jwk7nsm.js → index-x71bvq0p.js} +15 -15
- package/dist/{keepalive-nnz07ebd.js → keepalive-vtkf5n99.js} +5 -5
- package/dist/{lead-mg7qcgww.js → lead-h3sp357x.js} +26 -26
- package/dist/{maintenance-ywmnqrpx.js → maintenance-jzeqtk3q.js} +6 -6
- package/dist/{oauth-refresh-sweep-c63sqfs1.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
- package/dist/{onboard-2zj8sqkq.js → onboard-tmsp0qbz.js} +278 -81
- package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
- package/dist/{otel-impl-jgdfm26f.js → otel-impl-9mz73q65.js} +416 -301
- package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
- package/dist/{pricing-refresh-3whntb69.js → pricing-refresh-v1cbr9te.js} +5 -5
- package/dist/{rbac-roles-3zptcc3p.js → rbac-roles-c9wqz83x.js} +4 -4
- package/dist/{render-v2-dbvpyysc.js → render-v2-gryrh9h8.js} +4 -4
- package/dist/{setup-qwx0j8z9.js → setup-q9720bt6.js} +2 -2
- package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
- package/dist/{work-objects-sjbcmvep.js → work-objects-b76x7deb.js} +17 -17
- package/dist/{worker-xezqdkb7.js → worker-6vrxbv41.js} +26 -26
- package/openapi.json +1626 -629
- package/package.json +3 -2
- package/src/apps/catalog.generated.json +229 -0
- package/src/apps/page-validator.ts +71 -16
- package/src/apps/row-store.ts +66 -11
- package/src/apps/sync.ts +59 -17
- package/src/artifact-sdk/browser-sdk.ts +436 -2
- package/src/be/approval-sweeps.ts +109 -0
- package/src/be/boot-seeds.ts +40 -0
- package/src/be/chunking.ts +1 -1
- package/src/be/db/agents.ts +20 -0
- package/src/be/db/approvals.ts +78 -0
- package/src/be/db/tasks/read.ts +197 -39
- package/src/be/db/tasks/write.ts +41 -9
- package/src/be/db-client.ts +107 -48
- package/src/be/db.ts +219 -76
- package/src/be/events.ts +33 -1
- package/src/be/memory/graph-expansion.ts +10 -3
- package/src/be/memory/providers/sqlite-store.ts +55 -13
- package/src/be/memory/raters/retrieval.ts +5 -2
- package/src/be/memory/reranker.ts +26 -2
- package/src/be/memory/types.ts +2 -0
- package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
- package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
- package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
- package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
- package/src/be/models-catalog.ts +2 -0
- package/src/be/modelsdev-cache.json +1857 -0
- package/src/be/modelsdev-cache.ts +14 -1
- package/src/be/pricing-normalize.ts +2 -0
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-pricing.ts +3 -1
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/steering.ts +53 -3
- package/src/be/swarm-config-guard.ts +3 -0
- package/src/be/task-citations.ts +29 -0
- package/src/cli.tsx +14 -1
- package/src/commands/onboard/compose-generator.ts +8 -3
- package/src/commands/onboard/container-engine.ts +220 -0
- package/src/commands/onboard/manifest.ts +1 -0
- package/src/commands/onboard/steps/health-check.tsx +4 -1
- package/src/commands/onboard/steps/prereq-check.tsx +50 -54
- package/src/commands/onboard/steps/start.tsx +31 -16
- package/src/commands/onboard/types.ts +7 -2
- package/src/commands/onboard.tsx +39 -4
- package/src/commands/profile-sync.ts +79 -26
- package/src/commands/runner.ts +8 -1
- package/src/extensions/catalog.generated.json +17 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/heartbeat/heartbeat.ts +263 -103
- package/src/hooks/hook.ts +21 -12
- package/src/http/agents.ts +35 -0
- package/src/http/approval-requests.ts +136 -64
- package/src/http/apps.ts +14 -3
- package/src/http/config.ts +15 -1
- package/src/http/events.ts +62 -2
- package/src/http/index.ts +6 -4
- package/src/http/memory.ts +21 -7
- package/src/http/schedules.ts +1 -1
- package/src/http/session-data.ts +9 -4
- package/src/http/sessions.ts +25 -18
- package/src/http/stats.ts +19 -7
- package/src/http/status.ts +11 -1
- package/src/http/tasks.ts +40 -8
- package/src/http/webhooks.ts +67 -46
- package/src/http/workflows.ts +15 -20
- package/src/linear/outbound.ts +37 -15
- package/src/otel-impl.ts +151 -1
- package/src/otel.ts +14 -3
- package/src/prompts/memories.ts +19 -2
- package/src/providers/claude-managed-models.ts +8 -0
- package/src/providers/codex-agents-md.ts +140 -17
- package/src/providers/modelsdev-reasoning.json +551 -0
- package/src/rbac/legacy-policy.ts +16 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/scheduler/deferred-task-waits.ts +28 -0
- package/src/scheduler/schedule-task.ts +3 -0
- package/src/scheduler/scheduler.ts +14 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/swarm-sdk.ts +9 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/server.ts +12 -7
- package/src/slack/app.ts +35 -4
- package/src/slack/connection-state.ts +17 -0
- package/src/tasks/context-key.ts +1 -0
- package/src/tasks/worker-follow-up.ts +6 -2
- package/src/tests/agent-task-activity.test.ts +74 -0
- package/src/tests/agents-list-model-display.test.ts +6 -1
- package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
- package/src/tests/approval-requests.test.ts +395 -0
- package/src/tests/approval-sweeps-config.test.ts +47 -0
- package/src/tests/approval-sweeps.test.ts +508 -0
- package/src/tests/apps-kibo-blocks.test.ts +77 -0
- package/src/tests/apps-spike.test.ts +56 -1
- package/src/tests/apps-sync-engine.test.ts +20 -9
- package/src/tests/boot-seeds.test.ts +170 -0
- package/src/tests/cancel-approval-request-tool.test.ts +115 -0
- package/src/tests/claude-managed-adapter.test.ts +6 -0
- package/src/tests/codex-adapter.test.ts +126 -1
- package/src/tests/config-key-filter.test.ts +80 -0
- package/src/tests/context-window.test.ts +1 -0
- package/src/tests/db-client-busy-retry.test.ts +96 -0
- package/src/tests/db-client.test.ts +60 -1
- package/src/tests/db-task-read-contract.test.ts +10 -0
- package/src/tests/db-task-write-contract.test.ts +9 -0
- package/src/tests/deferred-schedule-reaper.test.ts +193 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/events-http.test.ts +103 -0
- package/src/tests/extensions-catalog.test.ts +22 -17
- package/src/tests/extensions-pre-task.test.ts +107 -0
- package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
- package/src/tests/follow-up-config-inheritance.test.ts +260 -0
- package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
- package/src/tests/heartbeat-reboot-live.test.ts +95 -0
- package/src/tests/heartbeat-stall-cas.test.ts +80 -0
- package/src/tests/heartbeat.test.ts +99 -3
- package/src/tests/hook-mcp-config.test.ts +29 -0
- package/src/tests/http-api-integration.test.ts +16 -2
- package/src/tests/kv-storage.test.ts +62 -0
- package/src/tests/linear-outbound-sync.test.ts +120 -5
- package/src/tests/list-endpoint-slimming.test.ts +91 -0
- package/src/tests/memory-graph-organic-route.test.ts +155 -0
- package/src/tests/memory-http-recall-gating.test.ts +14 -4
- package/src/tests/memory-hybrid.test.ts +161 -1
- package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
- package/src/tests/memory.test.ts +22 -0
- package/src/tests/onboard-container-engine.test.ts +240 -0
- package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
- package/src/tests/pricing-routes.test.ts +14 -0
- package/src/tests/profile-sync-warning.test.ts +8 -4
- package/src/tests/promote-draft-task-route.test.ts +19 -0
- package/src/tests/provider-registration.test.ts +296 -42
- package/src/tests/rbac-engine.test.ts +30 -0
- package/src/tests/reasoning-effort.test.ts +8 -0
- package/src/tests/script-connections.test.ts +7 -0
- package/src/tests/sdk-allowlist.test.ts +39 -0
- package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
- package/src/tests/sessions-detail-batch.test.ts +250 -0
- package/src/tests/sessions.test.ts +118 -0
- package/src/tests/slack-config.test.ts +22 -0
- package/src/tests/sonnet-5-5-registry.test.ts +44 -0
- package/src/tests/status.test.ts +36 -1
- package/src/tests/swarm-diff.test.ts +316 -10
- package/src/tests/task-completion-idempotency.test.ts +2 -1
- package/src/tests/tasks-requested-by-filter.test.ts +3 -1
- package/src/tests/tool-annotations.test.ts +1 -1
- package/src/tests/ui-list-slim-projections.test.ts +158 -0
- package/src/tests/webhooks-task-blocked.test.ts +127 -0
- package/src/tests/workflow-approval-recovery.test.ts +203 -0
- package/src/tests/workflow-hitl-routing.test.ts +22 -1
- package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
- package/src/tests/workflow-http-v2.test.ts +99 -8
- package/src/tests/workflow-retry-validation.test.ts +56 -0
- package/src/tests/workflow-tla-races.test.ts +389 -0
- package/src/tests/workflow-validation-port-routing.test.ts +113 -1
- package/src/tools/cancel-approval-request.ts +57 -0
- package/src/tools/memory-search.ts +1 -0
- package/src/tools/request-human-input.ts +4 -2
- package/src/tools/schedules/list-schedules.ts +4 -2
- package/src/tools/send-task.ts +4 -0
- package/src/tools/templates.ts +19 -0
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/workflows/list-workflow-runs.ts +1 -0
- package/src/types.ts +38 -0
- package/src/utils/context-window.ts +1 -0
- package/src/workflows/approval-cancel.ts +152 -0
- package/src/workflows/approval-notifications.ts +84 -0
- package/src/workflows/checkpoint.ts +10 -5
- package/src/workflows/definition.ts +21 -0
- package/src/workflows/engine.ts +152 -21
- package/src/workflows/executors/human-in-the-loop.ts +1 -1
- package/src/workflows/recovery.ts +100 -45
- package/src/workflows/resume.ts +49 -18
- package/src/workflows/retry-poller.ts +71 -21
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/github-sender-allowlist/README.md +18 -0
- package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
- package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
- package/templates/skills/apps/content.md +20 -1
- package/templates/skills/code-quality/content.md +26 -1
- package/templates/skills/pages/content.md +16 -4
- package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
- package/templates/skills/scheduling/content.md +1 -1
- package/dist/cli-2daksqps.js +0 -378
- package/dist/rbac-roles-3zw68x72.js +0 -37
|
@@ -26,7 +26,12 @@ import {
|
|
|
26
26
|
withMutationLock,
|
|
27
27
|
} from "../apps/row-store";
|
|
28
28
|
import { createApp, getApp, updateApp } from "../apps/store";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
getAppSyncStatus,
|
|
31
|
+
RECONCILE_CHUNK_SIZE,
|
|
32
|
+
runAppSync,
|
|
33
|
+
type SyncPassResult,
|
|
34
|
+
} from "../apps/sync";
|
|
30
35
|
import {
|
|
31
36
|
closeDb,
|
|
32
37
|
createAgent,
|
|
@@ -1277,17 +1282,20 @@ spawnDescribe("concurrency", () => {
|
|
|
1277
1282
|
});
|
|
1278
1283
|
|
|
1279
1284
|
test("a mid-pass write failure reports the committed churn instead of zero counts", async () => {
|
|
1280
|
-
//
|
|
1281
|
-
// SECOND
|
|
1282
|
-
//
|
|
1283
|
-
|
|
1285
|
+
// Rows are written in chunked transactions: fail the second create of the
|
|
1286
|
+
// SECOND chunk after the first chunk committed. The pass counts must say
|
|
1287
|
+
// so instead of reporting the zero-count base, and the failing chunk must
|
|
1288
|
+
// roll back without leaving partial rows.
|
|
1289
|
+
const total = RECONCILE_CHUNK_SIZE + 2;
|
|
1290
|
+
const records = Array.from({ length: total }, (_, index) => ghRecord(index + 1));
|
|
1291
|
+
const script = await fixtureScript("partial", records);
|
|
1284
1292
|
const appId = await createSyncApp(issueDefinition(script.id));
|
|
1285
1293
|
|
|
1286
1294
|
const realCreate = rowStore.createAppRowUnlocked;
|
|
1287
1295
|
let creates = 0;
|
|
1288
1296
|
const spy = spyOn(rowStore, "createAppRowUnlocked").mockImplementation((...callArgs) => {
|
|
1289
1297
|
creates += 1;
|
|
1290
|
-
if (creates === 2) throw new Error("kv write failed (injected)");
|
|
1298
|
+
if (creates === RECONCILE_CHUNK_SIZE + 2) throw new Error("kv write failed (injected)");
|
|
1291
1299
|
return realCreate(...callArgs);
|
|
1292
1300
|
});
|
|
1293
1301
|
try {
|
|
@@ -1296,9 +1304,12 @@ spawnDescribe("concurrency", () => {
|
|
|
1296
1304
|
|
|
1297
1305
|
expect(result.ok).toBe(false);
|
|
1298
1306
|
expect(pass.error).toContain("kv write failed (injected)");
|
|
1299
|
-
expect(pass).toMatchObject({ pulled:
|
|
1300
|
-
expect(
|
|
1301
|
-
expect(await getAppSyncStatus(appId, "issue", "gh")).toMatchObject({
|
|
1307
|
+
expect(pass).toMatchObject({ pulled: total, created: RECONCILE_CHUNK_SIZE });
|
|
1308
|
+
expect(await rowsOf(appId)).toHaveLength(RECONCILE_CHUNK_SIZE);
|
|
1309
|
+
expect(await getAppSyncStatus(appId, "issue", "gh")).toMatchObject({
|
|
1310
|
+
ok: false,
|
|
1311
|
+
created: RECONCILE_CHUNK_SIZE,
|
|
1312
|
+
});
|
|
1302
1313
|
} finally {
|
|
1303
1314
|
spy.mockRestore();
|
|
1304
1315
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { ensurePricingSeeded, ensureRbacSeeded } from "../be/boot-seeds";
|
|
6
|
+
import { closeDb, getDbClient, initDb } from "../be/db";
|
|
7
|
+
import { loadModelsDevCache } from "../be/modelsdev-cache";
|
|
8
|
+
import { BUILTIN_ROLES } from "../be/rbac-roles";
|
|
9
|
+
import { createServer } from "../server";
|
|
10
|
+
|
|
11
|
+
const TEST_DB_PATH = "./test-boot-seeds.sqlite";
|
|
12
|
+
const BUILTIN_ROLE_ID = "rbac-role-requester";
|
|
13
|
+
|
|
14
|
+
async function removeDbFiles(): Promise<void> {
|
|
15
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
16
|
+
try {
|
|
17
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
18
|
+
} catch {
|
|
19
|
+
// File does not exist.
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function countPricingSeedRows(): Promise<number> {
|
|
25
|
+
const row = await getDbClient().get<{ n: number }>(
|
|
26
|
+
"SELECT COUNT(*) AS n FROM pricing WHERE effective_from = 0",
|
|
27
|
+
);
|
|
28
|
+
return row?.n ?? 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function countBuiltinPermissions(): Promise<number> {
|
|
32
|
+
const row = await getDbClient().get<{ n: number }>(
|
|
33
|
+
"SELECT COUNT(*) AS n FROM role_permissions WHERE roleId = ?",
|
|
34
|
+
[BUILTIN_ROLE_ID],
|
|
35
|
+
);
|
|
36
|
+
return row?.n ?? 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Fresh DB handle per test: the guards are keyed on the live handle.
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
closeDb();
|
|
42
|
+
await removeDbFiles();
|
|
43
|
+
initDb(TEST_DB_PATH);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
closeDb();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterAll(async () => {
|
|
51
|
+
await removeDbFiles();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("ensurePricingSeeded", () => {
|
|
55
|
+
test("seeds once per database handle, and again for a new handle", async () => {
|
|
56
|
+
ensurePricingSeeded();
|
|
57
|
+
const seeded = await countPricingSeedRows();
|
|
58
|
+
expect(seeded).toBeGreaterThan(0);
|
|
59
|
+
|
|
60
|
+
// A second call must not touch the table: wipe it and confirm it stays empty.
|
|
61
|
+
await getDbClient().run("DELETE FROM pricing WHERE effective_from = 0");
|
|
62
|
+
ensurePricingSeeded();
|
|
63
|
+
expect(await countPricingSeedRows()).toBe(0);
|
|
64
|
+
|
|
65
|
+
// Reopening the same file gives a new handle, which is seeded again.
|
|
66
|
+
closeDb();
|
|
67
|
+
initDb(TEST_DB_PATH);
|
|
68
|
+
ensurePricingSeeded();
|
|
69
|
+
expect(await countPricingSeedRows()).toBe(seeded);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("ensureRbacSeeded", () => {
|
|
74
|
+
test("syncs once per database handle, and again for a new handle", async () => {
|
|
75
|
+
ensureRbacSeeded();
|
|
76
|
+
const seeded = await countBuiltinPermissions();
|
|
77
|
+
expect(seeded).toBeGreaterThan(0);
|
|
78
|
+
|
|
79
|
+
await getDbClient().run("DELETE FROM role_permissions WHERE roleId = ?", [BUILTIN_ROLE_ID]);
|
|
80
|
+
ensureRbacSeeded();
|
|
81
|
+
expect(await countBuiltinPermissions()).toBe(0);
|
|
82
|
+
|
|
83
|
+
closeDb();
|
|
84
|
+
initDb(TEST_DB_PATH);
|
|
85
|
+
ensureRbacSeeded();
|
|
86
|
+
expect(await countBuiltinPermissions()).toBe(seeded);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a failed sync does not mark the handle done", async () => {
|
|
90
|
+
// Make the built-in role unrecoverable: its name is taken by another role id.
|
|
91
|
+
const builtin = BUILTIN_ROLES.find((role) => role.id === BUILTIN_ROLE_ID);
|
|
92
|
+
if (!builtin) throw new Error("test fixture: built-in role missing");
|
|
93
|
+
const client = getDbClient();
|
|
94
|
+
await client.run("PRAGMA foreign_keys = OFF");
|
|
95
|
+
await client.run("DELETE FROM roles WHERE id = ?", [BUILTIN_ROLE_ID]);
|
|
96
|
+
await client.run("PRAGMA foreign_keys = ON");
|
|
97
|
+
await client.run(
|
|
98
|
+
"INSERT INTO roles (id, name, description, isBuiltin, grantsAll) VALUES ('rbac-role-collision', ?, 'x', 0, 0)",
|
|
99
|
+
[builtin.name],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(() => ensureRbacSeeded()).toThrow(/already uses that name/);
|
|
103
|
+
// Not cached as done: the next caller retries and sees the same error.
|
|
104
|
+
expect(() => ensureRbacSeeded()).toThrow(/already uses that name/);
|
|
105
|
+
|
|
106
|
+
await client.run("DELETE FROM roles WHERE id = 'rbac-role-collision'");
|
|
107
|
+
ensureRbacSeeded();
|
|
108
|
+
expect(await countBuiltinPermissions()).toBeGreaterThan(0);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("createServer (one call per MCP session)", () => {
|
|
113
|
+
let savedDatabasePath: string | undefined;
|
|
114
|
+
|
|
115
|
+
beforeEach(() => {
|
|
116
|
+
savedDatabasePath = process.env.DATABASE_PATH;
|
|
117
|
+
process.env.DATABASE_PATH = TEST_DB_PATH;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
if (savedDatabasePath === undefined) delete process.env.DATABASE_PATH;
|
|
122
|
+
else process.env.DATABASE_PATH = savedDatabasePath;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("seeds pricing and RBAC on the first session only", async () => {
|
|
126
|
+
await createServer();
|
|
127
|
+
expect(await countPricingSeedRows()).toBeGreaterThan(0);
|
|
128
|
+
expect(await countBuiltinPermissions()).toBeGreaterThan(0);
|
|
129
|
+
|
|
130
|
+
await getDbClient().run("DELETE FROM pricing WHERE effective_from = 0");
|
|
131
|
+
await getDbClient().run("DELETE FROM role_permissions WHERE roleId = ?", [BUILTIN_ROLE_ID]);
|
|
132
|
+
|
|
133
|
+
// A later session re-registers tools but must not re-run the seeds.
|
|
134
|
+
const second = await createServer();
|
|
135
|
+
expect(second).toBeDefined();
|
|
136
|
+
expect(await countPricingSeedRows()).toBe(0);
|
|
137
|
+
expect(await countBuiltinPermissions()).toBe(0);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("loadModelsDevCache", () => {
|
|
142
|
+
const saved = process.env.MODELSDEV_CACHE_PATH;
|
|
143
|
+
let dir: string | undefined;
|
|
144
|
+
|
|
145
|
+
afterEach(async () => {
|
|
146
|
+
if (saved === undefined) delete process.env.MODELSDEV_CACHE_PATH;
|
|
147
|
+
else process.env.MODELSDEV_CACHE_PATH = saved;
|
|
148
|
+
if (dir) await rm(dir, { recursive: true, force: true });
|
|
149
|
+
dir = undefined;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("returns the same parsed object while it is alive", () => {
|
|
153
|
+
const first = loadModelsDevCache();
|
|
154
|
+
expect(first).not.toBeNull();
|
|
155
|
+
expect(loadModelsDevCache()).toBe(first);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("keys the memo on the resolved path", async () => {
|
|
159
|
+
dir = await mkdtemp(path.join(tmpdir(), "modelsdev-cache-"));
|
|
160
|
+
const alt = path.join(dir, "cache.json");
|
|
161
|
+
await writeFile(alt, JSON.stringify({ anthropic: { id: "anthropic-alt", models: {} } }));
|
|
162
|
+
|
|
163
|
+
const bundled = loadModelsDevCache();
|
|
164
|
+
process.env.MODELSDEV_CACHE_PATH = alt;
|
|
165
|
+
const overridden = loadModelsDevCache();
|
|
166
|
+
|
|
167
|
+
expect(overridden?.anthropic?.id).toBe("anthropic-alt");
|
|
168
|
+
expect(overridden).not.toBe(bundled);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import {
|
|
5
|
+
closeDb,
|
|
6
|
+
createAgent,
|
|
7
|
+
createApprovalRequest,
|
|
8
|
+
createTaskExtended,
|
|
9
|
+
getApprovalRequestById,
|
|
10
|
+
getDbClient,
|
|
11
|
+
initDb,
|
|
12
|
+
} from "../be/db";
|
|
13
|
+
import { registerCancelApprovalRequestTool } from "../tools/cancel-approval-request";
|
|
14
|
+
|
|
15
|
+
const TEST_DB_PATH = "./test-cancel-approval-request-tool.sqlite";
|
|
16
|
+
|
|
17
|
+
type ToolResult = {
|
|
18
|
+
isError?: boolean;
|
|
19
|
+
structuredContent: { success: boolean; alreadyCancelled?: boolean; runCancelled?: boolean };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let callTool: (args: unknown, agentId: string) => Promise<ToolResult>;
|
|
23
|
+
let leadId = "";
|
|
24
|
+
let ownerId = "";
|
|
25
|
+
let workerId = "";
|
|
26
|
+
|
|
27
|
+
async function makeRequest() {
|
|
28
|
+
const sourceTask = await createTaskExtended("source", { agentId: ownerId, source: "mcp" });
|
|
29
|
+
const id = crypto.randomUUID();
|
|
30
|
+
await createApprovalRequest({
|
|
31
|
+
id,
|
|
32
|
+
title: "Tool cancel",
|
|
33
|
+
questions: [{ id: "q1", type: "approval", label: "Approve?", required: true }],
|
|
34
|
+
approvers: { policy: "any" },
|
|
35
|
+
sourceTaskId: sourceTask.id,
|
|
36
|
+
});
|
|
37
|
+
return id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("cancel-approval-request tool", () => {
|
|
41
|
+
beforeAll(async () => {
|
|
42
|
+
initDb(TEST_DB_PATH);
|
|
43
|
+
leadId = (await createAgent({ name: "tool-lead", isLead: true, status: "idle" })).id;
|
|
44
|
+
ownerId = (await createAgent({ name: "tool-owner", isLead: false, status: "idle" })).id;
|
|
45
|
+
workerId = (await createAgent({ name: "tool-worker", isLead: false, status: "idle" })).id;
|
|
46
|
+
const server = new McpServer({ name: "cancel-approval-request-test", version: "1.0.0" });
|
|
47
|
+
registerCancelApprovalRequestTool(server);
|
|
48
|
+
const tool = (
|
|
49
|
+
server as unknown as {
|
|
50
|
+
_registeredTools: Record<
|
|
51
|
+
string,
|
|
52
|
+
{ handler: (args: unknown, extra: unknown) => Promise<unknown> }
|
|
53
|
+
>;
|
|
54
|
+
}
|
|
55
|
+
)._registeredTools["cancel-approval-request"];
|
|
56
|
+
if (!tool) throw new Error("cancel-approval-request tool was not registered");
|
|
57
|
+
callTool = async (args, agentId) =>
|
|
58
|
+
(await tool.handler(args, {
|
|
59
|
+
sessionId: "cancel-approval-request-test",
|
|
60
|
+
requestInfo: { headers: { "x-agent-id": agentId } },
|
|
61
|
+
})) as ToolResult;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterAll(async () => {
|
|
65
|
+
closeDb();
|
|
66
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
67
|
+
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("a lead cancels a pending request, then a second call is a no-op", async () => {
|
|
72
|
+
const requestId = await makeRequest();
|
|
73
|
+
|
|
74
|
+
const first = await callTool({ requestId, reason: "not needed" }, leadId);
|
|
75
|
+
expect(first.isError).toBeFalsy();
|
|
76
|
+
expect(first.structuredContent.alreadyCancelled).toBe(false);
|
|
77
|
+
const row = await getApprovalRequestById(requestId);
|
|
78
|
+
expect(row!.status).toBe("cancelled");
|
|
79
|
+
expect(row!.resolvedBy).toBe(leadId);
|
|
80
|
+
expect(row!.resolutionReason).toBe("not needed");
|
|
81
|
+
|
|
82
|
+
const second = await callTool({ requestId }, leadId);
|
|
83
|
+
expect(second.isError).toBeFalsy();
|
|
84
|
+
expect(second.structuredContent.alreadyCancelled).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a worker that does not own the source task gets a tool error", async () => {
|
|
88
|
+
const requestId = await makeRequest();
|
|
89
|
+
|
|
90
|
+
const result = await callTool({ requestId }, workerId);
|
|
91
|
+
|
|
92
|
+
expect(result.isError).toBe(true);
|
|
93
|
+
expect((await getApprovalRequestById(requestId))!.status).toBe("pending");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("the owner agent may cancel", async () => {
|
|
97
|
+
const requestId = await makeRequest();
|
|
98
|
+
const result = await callTool({ requestId }, ownerId);
|
|
99
|
+
expect(result.isError).toBeFalsy();
|
|
100
|
+
expect((await getApprovalRequestById(requestId))!.status).toBe("cancelled");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("an expired pending request gets a tool error and becomes timeout", async () => {
|
|
104
|
+
const requestId = await makeRequest();
|
|
105
|
+
await getDbClient().run("UPDATE approval_requests SET expiresAt = ? WHERE id = ?", [
|
|
106
|
+
new Date(Date.now() - 60_000).toISOString(),
|
|
107
|
+
requestId,
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
const result = await callTool({ requestId }, leadId);
|
|
111
|
+
|
|
112
|
+
expect(result.isError).toBe(true);
|
|
113
|
+
expect((await getApprovalRequestById(requestId))!.status).toBe("timeout");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -922,6 +922,12 @@ describe("ClaudeManagedAdapter (Phase 4) — repo provisioning + cost data", ()
|
|
|
922
922
|
cacheReadPerMillion: 1.0,
|
|
923
923
|
cacheWritePerMillion: 12.5,
|
|
924
924
|
});
|
|
925
|
+
expect(CLAUDE_MANAGED_MODEL_PRICING["claude-sonnet-5-5"]).toEqual({
|
|
926
|
+
inputPerMillion: 2.0,
|
|
927
|
+
outputPerMillion: 10.0,
|
|
928
|
+
cacheReadPerMillion: 0.2,
|
|
929
|
+
cacheWritePerMillion: 2.5,
|
|
930
|
+
});
|
|
925
931
|
expect(CLAUDE_MANAGED_MODEL_PRICING["claude-sonnet-5"]).toEqual({
|
|
926
932
|
inputPerMillion: 2.0,
|
|
927
933
|
outputPerMillion: 10.0,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
-
import { mkdirSync, rmSync } from "node:fs";
|
|
10
|
+
import { lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import type {
|
|
13
13
|
AgentMessageItem,
|
|
@@ -1130,6 +1130,8 @@ describe("CodexAdapter.canResume", () => {
|
|
|
1130
1130
|
|
|
1131
1131
|
describe("writeCodexAgentsMd round-trip", () => {
|
|
1132
1132
|
const tmpDir = `/tmp/codex-agents-md-test-${Date.now()}`;
|
|
1133
|
+
// Crash-recovery records live outside every per-test repo dir.
|
|
1134
|
+
const stateDir = join(tmpDir, ".state");
|
|
1133
1135
|
|
|
1134
1136
|
beforeAll(() => {
|
|
1135
1137
|
mkdirSync(tmpDir, { recursive: true });
|
|
@@ -1239,6 +1241,129 @@ stale
|
|
|
1239
1241
|
expect(after).not.toContain("<swarm_system_prompt>");
|
|
1240
1242
|
expect(after).toContain("# Project instructions");
|
|
1241
1243
|
});
|
|
1244
|
+
|
|
1245
|
+
test("cleanup restores a plain AGENTS.md byte-identically", async () => {
|
|
1246
|
+
const dir = join(tmpDir, "byte-identical");
|
|
1247
|
+
mkdirSync(dir, { recursive: true });
|
|
1248
|
+
const original = "# Project instructions\n\nDo things.\n";
|
|
1249
|
+
await Bun.write(join(dir, "AGENTS.md"), original);
|
|
1250
|
+
|
|
1251
|
+
const handle = await writeCodexAgentsMd(dir, "swarm prompt");
|
|
1252
|
+
await handle.cleanup();
|
|
1253
|
+
|
|
1254
|
+
expect(readFileSync(join(dir, "AGENTS.md"), "utf8")).toBe(original);
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
test("never writes through an AGENTS.md -> CLAUDE.md symlink", async () => {
|
|
1258
|
+
const dir = join(tmpDir, "symlink");
|
|
1259
|
+
mkdirSync(dir, { recursive: true });
|
|
1260
|
+
const original = "# Repo rules\n\nDo things.\n";
|
|
1261
|
+
const claudeMd = join(dir, "CLAUDE.md");
|
|
1262
|
+
const agentsMd = join(dir, "AGENTS.md");
|
|
1263
|
+
await Bun.write(claudeMd, original);
|
|
1264
|
+
symlinkSync("CLAUDE.md", agentsMd);
|
|
1265
|
+
|
|
1266
|
+
const handle = await writeCodexAgentsMd(dir, "swarm prompt", { stateDir });
|
|
1267
|
+
|
|
1268
|
+
// During the session: AGENTS.md is a real file, CLAUDE.md is untouched.
|
|
1269
|
+
expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
|
|
1270
|
+
const during = readFileSync(agentsMd, "utf8");
|
|
1271
|
+
expect(during).toContain("swarm prompt");
|
|
1272
|
+
expect(during).toContain("# Repo rules");
|
|
1273
|
+
expect(readFileSync(claudeMd, "utf8")).toBe(original);
|
|
1274
|
+
|
|
1275
|
+
await handle.cleanup();
|
|
1276
|
+
|
|
1277
|
+
expect(lstatSync(agentsMd).isSymbolicLink()).toBe(true);
|
|
1278
|
+
expect(readlinkSync(agentsMd)).toBe("CLAUDE.md");
|
|
1279
|
+
expect(readFileSync(claudeMd, "utf8")).toBe(original);
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
test("carries agent edits to a symlinked AGENTS.md over to the link target", async () => {
|
|
1283
|
+
const dir = join(tmpDir, "symlink-edit");
|
|
1284
|
+
mkdirSync(dir, { recursive: true });
|
|
1285
|
+
const claudeMd = join(dir, "CLAUDE.md");
|
|
1286
|
+
const agentsMd = join(dir, "AGENTS.md");
|
|
1287
|
+
await Bun.write(claudeMd, "# Repo rules\n");
|
|
1288
|
+
symlinkSync("CLAUDE.md", agentsMd);
|
|
1289
|
+
|
|
1290
|
+
const handle = await writeCodexAgentsMd(dir, "swarm prompt", { stateDir });
|
|
1291
|
+
await Bun.write(agentsMd, `${readFileSync(agentsMd, "utf8")}- new rule\n`);
|
|
1292
|
+
await handle.cleanup();
|
|
1293
|
+
|
|
1294
|
+
expect(lstatSync(agentsMd).isSymbolicLink()).toBe(true);
|
|
1295
|
+
expect(readFileSync(claudeMd, "utf8")).toBe("# Repo rules\n- new rule\n");
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
test("restores a symlink left swapped out by a crashed session", async () => {
|
|
1299
|
+
const dir = join(tmpDir, "symlink-crash");
|
|
1300
|
+
mkdirSync(dir, { recursive: true });
|
|
1301
|
+
const original = "# Repo rules\n";
|
|
1302
|
+
const claudeMd = join(dir, "CLAUDE.md");
|
|
1303
|
+
const agentsMd = join(dir, "AGENTS.md");
|
|
1304
|
+
await Bun.write(claudeMd, original);
|
|
1305
|
+
symlinkSync("CLAUDE.md", agentsMd);
|
|
1306
|
+
|
|
1307
|
+
// First session never runs cleanup (crash).
|
|
1308
|
+
await writeCodexAgentsMd(dir, "first prompt", { stateDir });
|
|
1309
|
+
expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
|
|
1310
|
+
|
|
1311
|
+
const handle = await writeCodexAgentsMd(dir, "second prompt", { stateDir });
|
|
1312
|
+
const during = readFileSync(agentsMd, "utf8");
|
|
1313
|
+
expect(during).toContain("second prompt");
|
|
1314
|
+
expect(during).not.toContain("first prompt");
|
|
1315
|
+
expect(readFileSync(claudeMd, "utf8")).toBe(original);
|
|
1316
|
+
|
|
1317
|
+
await handle.cleanup();
|
|
1318
|
+
expect(lstatSync(agentsMd).isSymbolicLink()).toBe(true);
|
|
1319
|
+
expect(readFileSync(claudeMd, "utf8")).toBe(original);
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
test("ignores a forged symlink marker pointing out of the repo", async () => {
|
|
1323
|
+
const dir = join(tmpDir, "forged-marker");
|
|
1324
|
+
mkdirSync(dir, { recursive: true });
|
|
1325
|
+
const secretPath = join(tmpDir, "outside-secret.txt");
|
|
1326
|
+
const secret = "TOP-SECRET-CONTENTS\n";
|
|
1327
|
+
await Bun.write(secretPath, secret);
|
|
1328
|
+
const agentsMd = join(dir, "AGENTS.md");
|
|
1329
|
+
|
|
1330
|
+
for (const forged of ["../outside-secret.txt", secretPath]) {
|
|
1331
|
+
const original = `<swarm_system_prompt symlink="${encodeURIComponent(forged)}">\nold\n</swarm_system_prompt>\n# Rules\n`;
|
|
1332
|
+
await Bun.write(agentsMd, original);
|
|
1333
|
+
|
|
1334
|
+
const handle = await writeCodexAgentsMd(dir, "swarm prompt", { stateDir });
|
|
1335
|
+
expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
|
|
1336
|
+
expect(readFileSync(agentsMd, "utf8")).not.toContain("TOP-SECRET");
|
|
1337
|
+
expect(readFileSync(secretPath, "utf8")).toBe(secret);
|
|
1338
|
+
|
|
1339
|
+
await handle.cleanup();
|
|
1340
|
+
expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
|
|
1341
|
+
expect(readFileSync(agentsMd, "utf8")).toBe(original);
|
|
1342
|
+
expect(readFileSync(secretPath, "utf8")).toBe(secret);
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
test("never reads or writes an out-of-tree symlink target", async () => {
|
|
1347
|
+
const dir = join(tmpDir, "symlink-out-of-tree");
|
|
1348
|
+
mkdirSync(dir, { recursive: true });
|
|
1349
|
+
const secretPath = join(tmpDir, "outside-target.txt");
|
|
1350
|
+
const secret = "OUT-OF-TREE\n";
|
|
1351
|
+
await Bun.write(secretPath, secret);
|
|
1352
|
+
const agentsMd = join(dir, "AGENTS.md");
|
|
1353
|
+
symlinkSync("../outside-target.txt", agentsMd);
|
|
1354
|
+
|
|
1355
|
+
const handle = await writeCodexAgentsMd(dir, "swarm prompt", { stateDir });
|
|
1356
|
+
expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
|
|
1357
|
+
const during = readFileSync(agentsMd, "utf8");
|
|
1358
|
+
expect(during).toContain("swarm prompt");
|
|
1359
|
+
expect(during).not.toContain("OUT-OF-TREE");
|
|
1360
|
+
|
|
1361
|
+
await Bun.write(agentsMd, `${during}- agent edit\n`);
|
|
1362
|
+
await handle.cleanup();
|
|
1363
|
+
|
|
1364
|
+
expect(readlinkSync(agentsMd)).toBe("../outside-target.txt");
|
|
1365
|
+
expect(readFileSync(secretPath, "utf8")).toBe(secret);
|
|
1366
|
+
});
|
|
1242
1367
|
});
|
|
1243
1368
|
|
|
1244
1369
|
// ─── Phase 3: model catalogue ────────────────────────────────────────────────
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createServer, type Server } from "node:http";
|
|
3
|
+
import { closeDb, initDb, upsertSwarmConfig } from "../be/db";
|
|
4
|
+
import { handleConfig } from "../http/config";
|
|
5
|
+
import { handleCore } from "../http/core";
|
|
6
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
7
|
+
import { listenOnFreePort } from "./test-net";
|
|
8
|
+
|
|
9
|
+
// ctx.swarm.config_get / config_list send `key` to these routes. Both used to
|
|
10
|
+
// ignore it and return every row, so a script asking for one secret got all.
|
|
11
|
+
|
|
12
|
+
const API_KEY = "example-config-key-filter-test-key";
|
|
13
|
+
|
|
14
|
+
let server: Server;
|
|
15
|
+
let baseUrl: string;
|
|
16
|
+
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
initDb(":memory:");
|
|
19
|
+
await upsertSwarmConfig({ scope: "global", key: "WANTED_KEY", value: "wanted" });
|
|
20
|
+
await upsertSwarmConfig({
|
|
21
|
+
scope: "global",
|
|
22
|
+
key: "OTHER_SECRET",
|
|
23
|
+
value: "other-secret-value",
|
|
24
|
+
isSecret: true,
|
|
25
|
+
});
|
|
26
|
+
await upsertSwarmConfig({ scope: "global", key: "PLAIN_KEY", value: "plain" });
|
|
27
|
+
|
|
28
|
+
server = createServer(async (req, res) => {
|
|
29
|
+
if (await handleCore(req, res, req.headers["x-agent-id"] as string | undefined, API_KEY)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const segments = getPathSegments(req.url || "");
|
|
33
|
+
const query = parseQueryParams(req.url || "");
|
|
34
|
+
if (await handleConfig(req, res, segments, query)) return;
|
|
35
|
+
res.writeHead(404).end();
|
|
36
|
+
});
|
|
37
|
+
baseUrl = `http://127.0.0.1:${await listenOnFreePort(server)}`;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterAll(async () => {
|
|
41
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
42
|
+
closeDb();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
async function getKeys(path: string): Promise<string[]> {
|
|
46
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
47
|
+
headers: { Authorization: `Bearer ${API_KEY}` },
|
|
48
|
+
});
|
|
49
|
+
expect(response.status).toBe(200);
|
|
50
|
+
const body = (await response.json()) as { configs: Array<{ key: string }> };
|
|
51
|
+
return body.configs.map((config) => config.key);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ALL_KEYS = ["OTHER_SECRET", "PLAIN_KEY", "WANTED_KEY"];
|
|
55
|
+
|
|
56
|
+
describe.each([
|
|
57
|
+
["/api/config/resolved", "?"],
|
|
58
|
+
["/api/config", "?scope=global&"],
|
|
59
|
+
])("GET %s key filter", (path, prefix) => {
|
|
60
|
+
test("?key=<existing> returns exactly that row", async () => {
|
|
61
|
+
expect(await getKeys(`${path}${prefix}key=WANTED_KEY&includeSecrets=true`)).toEqual([
|
|
62
|
+
"WANTED_KEY",
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("?key=<unknown> returns an empty list", async () => {
|
|
67
|
+
expect(await getKeys(`${path}${prefix}key=NO_SUCH_KEY&includeSecrets=true`)).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("no key returns every row", async () => {
|
|
71
|
+
expect((await getKeys(`${path}${prefix}includeSecrets=true`)).sort()).toEqual(ALL_KEYS);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("an empty key is rejected instead of returning every row", async () => {
|
|
75
|
+
const response = await fetch(`${baseUrl}${path}${prefix}key=`, {
|
|
76
|
+
headers: { Authorization: `Bearer ${API_KEY}` },
|
|
77
|
+
});
|
|
78
|
+
expect(response.status).toBe(400);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -27,6 +27,7 @@ describe("getContextWindowSize", () => {
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
test("returns 1M for sonnet models", () => {
|
|
30
|
+
expect(getContextWindowSize("claude-sonnet-5-5")).toBe(1_000_000);
|
|
30
31
|
expect(getContextWindowSize("claude-sonnet-5")).toBe(1_000_000);
|
|
31
32
|
expect(getContextWindowSize("claude-sonnet-4-6")).toBe(1_000_000);
|
|
32
33
|
expect(getContextWindowSize("sonnet")).toBe(1_000_000);
|