@desplega.ai/agent-swarm 1.131.0 → 1.133.1
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-w53mbns9.js → actions-cskpayd7.js} +11 -9
- package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
- package/dist/{app-m9gwqsqv.js → app-ang77nbb.js} +8 -6
- package/dist/{assistant-bp65m6f3.js → assistant-b4exebdw.js} +16 -13
- package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-m3921t4y.js} +8 -8
- package/dist/{boot-reembed-62p0at2k.js → boot-reembed-ps2aftpb.js} +9 -7
- package/dist/{boot-reembed-xmfp6wdh.js → boot-reembed-znyk3nxb.js} +10 -8
- package/dist/{boot-scrub-logs-rr8q2dms.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
- package/dist/{claude-adapter-2y47mrkd.js → claude-adapter-3ct8kgxw.js} +13 -9
- package/dist/{claude-managed-adapter-gv19tdew.js → claude-managed-adapter-akx4ntf2.js} +2 -2
- package/dist/{cli-t1xze5qh.js → cli-13w2hwkh.js} +1 -1
- package/dist/{cli-efxfnq6q.js → cli-1532szb1.js} +8 -6
- package/dist/{cli-d2tfkcga.js → cli-23d3rseb.js} +33 -17
- package/dist/{cli-nm5gtp1x.js → cli-2a75z9c5.js} +196 -7
- package/dist/{cli-fywms69x.js → cli-2t30rj0d.js} +1 -1
- package/dist/{cli-cvgjwnh1.js → cli-3c3tdgab.js} +10 -10
- package/dist/{cli-pm6xbh84.js → cli-3zzxa9jz.js} +46 -16
- package/dist/{cli-cdeg08wx.js → cli-4k6233gc.js} +1 -1
- package/dist/{cli-t55yw3a1.js → cli-5p2tz05r.js} +2 -2
- package/dist/{cli-0rcyq6d9.js → cli-5z2yhzvd.js} +125 -47
- package/dist/{cli-3kf51j15.js → cli-6zfq31nj.js} +9 -3
- package/dist/{cli-er3a4bb0.js → cli-8m9de10a.js} +6 -1
- package/dist/{cli-gm9cbhbq.js → cli-8t14vg9w.js} +27 -8
- package/dist/{cli-72q8m8gk.js → cli-9nfc2es8.js} +6 -6
- package/dist/{cli-5ze9rv97.js → cli-9ta3pwms.js} +11 -4
- package/dist/{cli-78xwvkhj.js → cli-avr0ns7z.js} +2 -2
- package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
- package/dist/{cli-2q72qz2s.js → cli-dgk20ft7.js} +15 -6
- package/dist/cli-et1bbwqf.js +13 -0
- package/dist/{cli-bqw0mt1n.js → cli-f5w27pj3.js} +3 -3
- package/dist/{cli-2wadx7p2.js → cli-f8dre4bz.js} +2 -2
- package/dist/{cli-e7ckcg4v.js → cli-ftwpv451.js} +170 -387
- package/dist/{cli-g6b8x1h8.js → cli-g854s2dw.js} +1 -1
- package/dist/{cli-5kx36evn.js → cli-ggmdxeqa.js} +13 -8
- package/dist/cli-hkwfe3qj.js +530 -0
- package/dist/{cli-5dsbcjjw.js → cli-jqwnqx9m.js} +1 -1
- package/dist/{cli-9dr5qzc0.js → cli-ny122vh4.js} +21 -10
- package/dist/{cli-5v4rw6yd.js → cli-pvzqhbs9.js} +2 -2
- package/dist/cli-pzq5rjym.js +15 -0
- package/dist/{cli-xpxde7rm.js → cli-qes8ef67.js} +48 -30
- package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
- package/dist/cli-qys3rhhz.js +27 -0
- package/dist/{cli-rzbpmxdr.js → cli-r6tpm37h.js} +2 -2
- package/dist/{cli-qv61hp37.js → cli-rch80c76.js} +2 -2
- package/dist/{cli-0258203w.js → cli-rpp0x4zy.js} +137 -17
- package/dist/{cli-5n6e5yfn.js → cli-s1k0wxhe.js} +12 -7
- package/dist/{cli-psh3c4va.js → cli-sfd9v7sj.js} +1 -1
- package/dist/{cli-70xqj0z3.js → cli-sh3ytsyj.js} +49 -39
- package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
- package/dist/{cli-d3x3x1c9.js → cli-xdb4mpnp.js} +2 -2
- package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
- package/dist/{cli-yw8vghx2.js → cli-xv8t8p3c.js} +65 -32
- package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
- package/dist/{cli-t76vw5db.js → cli-y08jvxzx.js} +601 -140
- package/dist/{cli-a4jzdpa3.js → cli-zfbf3srj.js} +650 -245
- package/dist/{cli-a40rnf71.js → cli-zmq89k34.js} +6 -6
- package/dist/{cli-qmdnhpej.js → cli-zr9q011v.js} +23 -13
- package/dist/cli.js +22 -22
- package/dist/{codex-adapter-d24q75wk.js → codex-adapter-bef93pq4.js} +9 -7
- package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
- package/dist/{codex-session-runner-g50vcsex.js → codex-session-runner-5pkxys7x.js} +9 -7
- package/dist/{commands-c3nrc5hg.js → commands-e23s1bff.js} +9 -7
- package/dist/{db-nt7fg3tw.js → db-m3nabatn.js} +17 -5
- package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
- package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
- package/dist/{handlers-fkr6swat.js → handlers-akazedvr.js} +16 -13
- package/dist/{hook-77wpvnjc.js → hook-ze4cpf0v.js} +55 -41
- package/dist/{hook-g019yehy.js → hook-zfqyyenw.js} +10 -7
- package/dist/{http-jewj0syk.js → http-sja0n6kw.js} +730 -150
- package/dist/index-0gr8vd6k.js +39 -0
- package/dist/{index-7n2xpbd4.js → index-354vkhdv.js} +154 -74
- package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
- package/dist/{index-hfvggd0z.js → index-95t3cqq5.js} +16 -14
- package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
- package/dist/{index-6gm5he8h.js → index-vpn36mma.js} +15 -13
- package/dist/{keepalive-j0fjvqfd.js → keepalive-3sp3meqj.js} +16 -10
- package/dist/{lead-1n8x30xe.js → lead-mefeewkf.js} +39 -36
- package/dist/{maintenance-gvev11sa.js → maintenance-8tbrbjfg.js} +10 -8
- package/dist/mistral-conversations-00zrp7g4.js +717 -0
- package/dist/{oauth-refresh-sweep-s9ecsvh0.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
- package/dist/{onboard-pvm5b38w.js → onboard-mt2tgyjg.js} +7 -7
- package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
- package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
- package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
- package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
- package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
- package/dist/{otel-impl-p6jvgd9c.js → otel-impl-z6pxt3j9.js} +224 -9
- package/dist/{pi-mono-adapter-t091s27g.js → pi-mono-adapter-btvqmj58.js} +1138 -408
- package/dist/{pricing-refresh-k49rcyed.js → pricing-refresh-kyq8cezh.js} +9 -7
- package/dist/{rbac-roles-cr80g96t.js → rbac-roles-3ryfrfna.js} +8 -6
- package/dist/{rbac-roles-30pspvy7.js → rbac-roles-6qfvbee1.js} +9 -7
- package/dist/{render-v2-xm457ng1.js → render-v2-p61ppjey.js} +8 -6
- package/dist/{seed-pricing-yhpv35fr.js → seed-pricing-j56bbj4y.js} +8 -6
- package/dist/{setup-nytnmwwr.js → setup-tpr6c1kn.js} +2 -2
- package/dist/{worker-msytr2cw.js → worker-9w1v0mbb.js} +39 -36
- package/openapi.json +542 -16
- package/package.json +6 -6
- package/src/be/budget-refusal-notify.ts +14 -7
- package/src/be/db-queries/task-artifacts.ts +182 -0
- package/src/be/db.ts +783 -169
- package/src/be/events.ts +5 -0
- package/src/be/migrations/130_scratch_script_retention_grace.sql +6 -0
- package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
- package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
- package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
- package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
- package/src/be/migrations/136_task_requester_provenance.sql +37 -0
- package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
- package/src/be/multi-runtime.ts +430 -0
- package/src/be/scripts/db.ts +43 -1
- package/src/be/scripts/extract-schema.ts +3 -1
- package/src/be/scripts/retention.ts +164 -0
- package/src/be/scripts/run-saved.ts +16 -1
- package/src/be/scripts/typecheck.ts +3 -2
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/steering.ts +1 -1
- package/src/be/swarm-config-guard.ts +18 -0
- package/src/commands/credential-wait.ts +54 -0
- package/src/commands/onboard/steps/core-credentials.tsx +1 -1
- package/src/commands/onboard/steps/custom-templates.tsx +1 -1
- package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
- package/src/commands/onboard/steps/health-check.tsx +1 -1
- package/src/commands/onboard/steps/start.tsx +1 -1
- package/src/commands/profile-sync-audit.ts +56 -0
- package/src/commands/profile-sync.ts +287 -15
- package/src/commands/provider-credentials.ts +46 -19
- package/src/commands/runner.ts +196 -33
- package/src/commands/templates.ts +22 -0
- package/src/github/handlers.ts +55 -9
- package/src/heartbeat/heartbeat.ts +71 -12
- package/src/hooks/hook.ts +49 -33
- package/src/http/active-sessions.ts +11 -1
- package/src/http/agents.ts +261 -30
- package/src/http/all-routes.ts +1 -0
- package/src/http/config.ts +13 -2
- package/src/http/core.ts +102 -4
- package/src/http/db-query-bounded.ts +313 -0
- package/src/http/db-query-shared.ts +249 -0
- package/src/http/db-query.ts +54 -42
- package/src/http/events.ts +2 -0
- package/src/http/index.ts +27 -2
- package/src/http/mcp-bridge.ts +37 -3
- package/src/http/mcp.ts +12 -0
- package/src/http/memory.ts +13 -2
- package/src/http/openapi.ts +1 -0
- package/src/http/poll.ts +70 -27
- package/src/http/route-def.ts +43 -9
- package/src/http/schedules.ts +2 -2
- package/src/http/scripts.ts +31 -4
- package/src/http/session-data.ts +44 -0
- package/src/http/tasks.ts +2 -2
- package/src/http/users.ts +74 -52
- package/src/http/webhooks.ts +5 -2
- package/src/http/workflows.ts +1 -4
- package/src/http/x.ts +14 -0
- package/src/integrations/kapso/inbound.ts +1 -1
- package/src/jira/outbound.ts +39 -8
- package/src/jira/webhook-lifecycle.ts +13 -2
- package/src/linear/outbound.ts +43 -10
- package/src/oauth/keepalive.ts +13 -2
- package/src/prompts/base-prompt.ts +1 -1
- package/src/prompts/session-templates.ts +45 -7
- package/src/providers/claude-adapter.ts +6 -0
- package/src/providers/claude-managed-models.ts +34 -10
- package/src/providers/codex-adapter.ts +6 -0
- package/src/providers/opencode-adapter.ts +6 -0
- package/src/providers/pi-mono-adapter.ts +8 -0
- package/src/providers/pricing-sources.md +24 -0
- package/src/queue-stall-alarm.ts +216 -0
- package/src/scheduler/scheduler.ts +7 -0
- package/src/script-workflows/executor.ts +2 -1
- package/src/scripts-runtime/executors/native.ts +2 -1
- package/src/scripts-runtime/executors/types.ts +5 -0
- package/src/scripts-runtime/loader.ts +5 -0
- package/src/scripts-runtime/swarm-config.ts +8 -0
- package/src/scripts-runtime/swarm-sdk.ts +7 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/tests/budget-claim-gate.test.ts +20 -0
- package/src/tests/claude-adapter.test.ts +32 -0
- package/src/tests/claude-managed-adapter.test.ts +38 -7
- package/src/tests/codex-adapter.test.ts +30 -0
- package/src/tests/create-task-validation.test.ts +128 -0
- package/src/tests/credential-wait.test.ts +67 -1
- package/src/tests/db-query.test.ts +711 -2
- package/src/tests/http-api-integration.test.ts +81 -0
- package/src/tests/http-users.test.ts +166 -1
- package/src/tests/identity-field-budget.test.ts +176 -0
- package/src/tests/mcp-tools-user.test.ts +34 -0
- package/src/tests/mcp-user-route.test.ts +17 -0
- package/src/tests/metrics-http.test.ts +44 -0
- package/src/tests/multi-runtime-registration.test.ts +2703 -0
- package/src/tests/openapi-response-contract.test.ts +24 -0
- package/src/tests/openapi-route-headers.test.ts +83 -0
- package/src/tests/opencode-adapter.test.ts +40 -0
- package/src/tests/outbound-listener-rejection.test.ts +138 -0
- package/src/tests/pi-mono-adapter.test.ts +98 -0
- package/src/tests/profile-sync-warning.test.ts +277 -0
- package/src/tests/profile-sync.test.ts +232 -6
- package/src/tests/prompt-template-session.test.ts +32 -3
- package/src/tests/queue-stall-alarm.test.ts +226 -0
- package/src/tests/requester-comms.test.ts +32 -0
- package/src/tests/runner-requester-profile.test.ts +38 -0
- package/src/tests/sandboxed-process.test.ts +45 -0
- package/src/tests/schedule-target-type.test.ts +38 -0
- package/src/tests/script-connections.test.ts +16 -3
- package/src/tests/scripts-external-api.test.ts +8 -3
- package/src/tests/scripts-http.test.ts +12 -0
- package/src/tests/scripts-retention.test.ts +384 -0
- package/src/tests/scripts-runtime-identity.test.ts +375 -0
- package/src/tests/scripts-runtime.test.ts +4 -3
- package/src/tests/sdk-allowlist.test.ts +47 -0
- package/src/tests/send-task-requested-by.test.ts +9 -0
- package/src/tests/session-costs.test.ts +506 -1
- package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
- package/src/tests/steering-core.test.ts +28 -0
- package/src/tests/task-artifacts.test.ts +386 -0
- package/src/tests/task-attachments-schema.test.ts +17 -1
- package/src/tests/task-completion-idempotency.test.ts +57 -0
- package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
- package/src/tests/update-profile-api.test.ts +67 -39
- package/src/tests/update-profile-auth.test.ts +48 -4
- package/src/tests/user-attribution-migration.test.ts +54 -0
- package/src/tests/user-identity.test.ts +72 -0
- package/src/tests/workflow-async-v2.test.ts +39 -1
- package/src/tests/workflow-http-v2.test.ts +3 -2
- package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
- package/src/tests/workflow-retry-v2.test.ts +45 -1
- package/src/tests/workflow-swarm-script.test.ts +14 -5
- package/src/tools/db-query.ts +18 -8
- package/src/tools/get-task-details.ts +24 -3
- package/src/tools/manage-user.ts +36 -3
- package/src/tools/poll-task.ts +93 -14
- package/src/tools/script-common.ts +5 -0
- package/src/tools/send-task.ts +1 -4
- package/src/tools/store-progress.ts +13 -2
- package/src/tools/swarm-config/delete-config.ts +9 -0
- package/src/tools/swarm-config/set-config.ts +3 -2
- package/src/tools/task-action.ts +30 -0
- package/src/tools/task-tool-ctx.ts +8 -1
- package/src/tools/update-profile.ts +16 -21
- package/src/tools/utils.ts +5 -0
- package/src/tools/workflows/trigger-workflow.ts +7 -1
- package/src/types.ts +165 -1
- package/src/utils/github-pull-request.ts +76 -0
- package/src/utils/identity-field-budget.ts +73 -0
- package/src/utils/multi-runtime.ts +20 -0
- package/src/utils/requester-comms.ts +24 -0
- package/src/utils/sandboxed-process.ts +40 -14
- package/src/workflows/engine.ts +22 -2
- package/src/workflows/executors/swarm-script.ts +24 -1
- package/src/workflows/resume.ts +19 -11
- package/src/workflows/retry-poller.ts +1 -0
- package/src/workflows/triggers.ts +4 -0
- package/templates/schedules/daily-compounding-reflection/content.md +12 -3
- package/templates/skills/db-query-guidance/config.json +14 -0
- package/templates/skills/db-query-guidance/content.md +57 -0
- package/dist/cli-3te4jdkm.js +0 -225
- package/dist/cli-bmze986d.js +0 -7
- package/dist/cli-kh3ba2zf.js +0 -607
- package/dist/index-hp1kdbjj.js +0 -36
- package/dist/mistral-conversations-81fwav0j.js +0 -30578
- package/dist/otel-nqa12wf3.js +0 -3149
- package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
|
@@ -11,6 +11,7 @@ import type { Subprocess } from "bun";
|
|
|
11
11
|
import { Webhook } from "svix";
|
|
12
12
|
import { slackContextKey } from "../tasks/context-key";
|
|
13
13
|
import { MAX_PROFILE_FILE_LENGTH } from "../utils/constants";
|
|
14
|
+
import { SOUL_MD_MAX_CHARS } from "../utils/identity-field-budget";
|
|
14
15
|
|
|
15
16
|
const TEST_PORT = Number(
|
|
16
17
|
process.env.HTTP_API_INTEGRATION_TEST_PORT ?? 30000 + Math.floor(Math.random() * 20000),
|
|
@@ -315,6 +316,86 @@ describe("Agents", () => {
|
|
|
315
316
|
expect(rejected.status).toBe(400);
|
|
316
317
|
});
|
|
317
318
|
|
|
319
|
+
test("PUT /api/agents/:id/profile — persists structured filesystem sync rejections", async () => {
|
|
320
|
+
const current = "s".repeat(SOUL_MD_MAX_CHARS);
|
|
321
|
+
const accepted = await put(`/api/agents/${ids.workerAgent}/profile`, {
|
|
322
|
+
body: { soulMd: current, changeSource: "session_sync" },
|
|
323
|
+
agentId: ids.workerAgent,
|
|
324
|
+
});
|
|
325
|
+
expect(accepted.status).toBe(200);
|
|
326
|
+
|
|
327
|
+
const rejected = await put(`/api/agents/${ids.workerAgent}/profile`, {
|
|
328
|
+
body: { soulMd: `${current}x`, changeSource: "session_sync" },
|
|
329
|
+
agentId: ids.workerAgent,
|
|
330
|
+
});
|
|
331
|
+
expect(rejected.status).toBe(400);
|
|
332
|
+
expect(rejected.body.profileSyncRejection).toMatchObject({
|
|
333
|
+
field: "soulMd",
|
|
334
|
+
diskSize: SOUL_MD_MAX_CHARS + 1,
|
|
335
|
+
dbSize: SOUL_MD_MAX_CHARS,
|
|
336
|
+
budget: SOUL_MD_MAX_CHARS,
|
|
337
|
+
delta: 1,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const query = new URLSearchParams({
|
|
341
|
+
event: "system.profile_sync_rejected",
|
|
342
|
+
agentId: ids.workerAgent,
|
|
343
|
+
dataField: "soulMd",
|
|
344
|
+
limit: "1",
|
|
345
|
+
});
|
|
346
|
+
const events = await get(`/api/events?${query}`, { agentId: ids.workerAgent });
|
|
347
|
+
expect(events.status).toBe(200);
|
|
348
|
+
expect(events.body.events).toHaveLength(1);
|
|
349
|
+
expect(events.body.events[0]).toMatchObject({
|
|
350
|
+
category: "system",
|
|
351
|
+
event: "system.profile_sync_rejected",
|
|
352
|
+
status: "error",
|
|
353
|
+
source: "api",
|
|
354
|
+
agentId: ids.workerAgent,
|
|
355
|
+
data: {
|
|
356
|
+
field: "soulMd",
|
|
357
|
+
diskSize: SOUL_MD_MAX_CHARS + 1,
|
|
358
|
+
dbSize: SOUL_MD_MAX_CHARS,
|
|
359
|
+
budget: SOUL_MD_MAX_CHARS,
|
|
360
|
+
delta: 1,
|
|
361
|
+
changeSource: "session_sync",
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const reconciled = await put(`/api/agents/${ids.workerAgent}/profile`, {
|
|
366
|
+
body: { soulMd: current, changeSource: "session_sync" },
|
|
367
|
+
agentId: ids.workerAgent,
|
|
368
|
+
});
|
|
369
|
+
expect(reconciled.status).toBe(200);
|
|
370
|
+
const reconciliationEvents = await get(
|
|
371
|
+
`/api/events?${new URLSearchParams({
|
|
372
|
+
event: "system.profile_sync_reconciled",
|
|
373
|
+
agentId: ids.workerAgent,
|
|
374
|
+
dataField: "soulMd",
|
|
375
|
+
limit: "1",
|
|
376
|
+
})}`,
|
|
377
|
+
{ agentId: ids.workerAgent },
|
|
378
|
+
);
|
|
379
|
+
expect(reconciliationEvents.status).toBe(200);
|
|
380
|
+
expect(reconciliationEvents.body.events[0]).toMatchObject({
|
|
381
|
+
event: "system.profile_sync_reconciled",
|
|
382
|
+
status: "ok",
|
|
383
|
+
data: { field: "soulMd", changeSource: "session_sync" },
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const unrelated = await get(
|
|
387
|
+
`/api/events?${new URLSearchParams({
|
|
388
|
+
event: "system.profile_sync_rejected",
|
|
389
|
+
agentId: ids.workerAgent,
|
|
390
|
+
dataField: "toolsMd",
|
|
391
|
+
limit: "1",
|
|
392
|
+
})}`,
|
|
393
|
+
{ agentId: ids.workerAgent },
|
|
394
|
+
);
|
|
395
|
+
expect(unrelated.status).toBe(200);
|
|
396
|
+
expect(unrelated.body.events).toHaveLength(0);
|
|
397
|
+
});
|
|
398
|
+
|
|
318
399
|
test("PUT /api/agents/:id/profile — non-existent returns 404", async () => {
|
|
319
400
|
const { status } = await put(`/api/agents/${randomUUID()}/profile`, {
|
|
320
401
|
body: { role: "ghost" },
|
|
@@ -20,7 +20,20 @@ import {
|
|
|
20
20
|
type Server,
|
|
21
21
|
type ServerResponse,
|
|
22
22
|
} from "node:http";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
closeDb,
|
|
25
|
+
createScheduledTask,
|
|
26
|
+
createUser,
|
|
27
|
+
createWorkflow,
|
|
28
|
+
createWorkflowRun,
|
|
29
|
+
getBudget,
|
|
30
|
+
getDb,
|
|
31
|
+
getScheduledTaskById,
|
|
32
|
+
getWorkflowRun,
|
|
33
|
+
initDb,
|
|
34
|
+
updateWorkflowRun,
|
|
35
|
+
upsertKv,
|
|
36
|
+
} from "../be/db";
|
|
24
37
|
import { fingerprintApiKey, linkIdentity } from "../be/users";
|
|
25
38
|
import { handleCore } from "../http/core";
|
|
26
39
|
import { handleUsers } from "../http/users";
|
|
@@ -91,6 +104,10 @@ beforeEach(() => {
|
|
|
91
104
|
db.run("DELETE FROM user_identity_events");
|
|
92
105
|
db.run("DELETE FROM user_external_ids");
|
|
93
106
|
db.run("DELETE FROM user_tokens");
|
|
107
|
+
db.run("DELETE FROM workflow_run_steps");
|
|
108
|
+
db.run("DELETE FROM workflow_runs");
|
|
109
|
+
db.run("DELETE FROM workflows");
|
|
110
|
+
db.run("DELETE FROM scheduled_tasks");
|
|
94
111
|
db.run("DELETE FROM users");
|
|
95
112
|
db.run("DELETE FROM budgets");
|
|
96
113
|
db.run("DELETE FROM kv_entries");
|
|
@@ -325,6 +342,56 @@ describe("PATCH /api/users/:id", () => {
|
|
|
325
342
|
expect(fields).toContain("name");
|
|
326
343
|
expect(fields).not.toContain("role");
|
|
327
344
|
});
|
|
345
|
+
|
|
346
|
+
test("comms merges into metadata.comms and preserves sibling metadata keys", async () => {
|
|
347
|
+
const u = createUser({ name: "CommsMerge", metadata: { customerId: "abc123" } });
|
|
348
|
+
|
|
349
|
+
const r = await authedFetch(`/api/users/${u.id}`, {
|
|
350
|
+
method: "PATCH",
|
|
351
|
+
body: JSON.stringify({
|
|
352
|
+
comms: { tone: "casual", verbosity: "short" },
|
|
353
|
+
}),
|
|
354
|
+
});
|
|
355
|
+
expect(r.status).toBe(200);
|
|
356
|
+
const { user } = (await r.json()) as { user: { metadata: Record<string, unknown> | null } };
|
|
357
|
+
expect(user.metadata).toEqual({
|
|
358
|
+
customerId: "abc123",
|
|
359
|
+
comms: { tone: "casual", verbosity: "short" },
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("comms: null removes only the comms key", async () => {
|
|
364
|
+
const u = createUser({
|
|
365
|
+
name: "CommsRemove",
|
|
366
|
+
metadata: { customerId: "abc123", comms: { tone: "formal" } },
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const r = await authedFetch(`/api/users/${u.id}`, {
|
|
370
|
+
method: "PATCH",
|
|
371
|
+
body: JSON.stringify({ comms: null }),
|
|
372
|
+
});
|
|
373
|
+
expect(r.status).toBe(200);
|
|
374
|
+
const { user } = (await r.json()) as { user: { metadata: Record<string, unknown> | null } };
|
|
375
|
+
expect(user.metadata).toEqual({ customerId: "abc123" });
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("comms combined with metadata applies metadata first then comms", async () => {
|
|
379
|
+
const u = createUser({ name: "CommsWithMetadata", metadata: { customerId: "abc123" } });
|
|
380
|
+
|
|
381
|
+
const r = await authedFetch(`/api/users/${u.id}`, {
|
|
382
|
+
method: "PATCH",
|
|
383
|
+
body: JSON.stringify({
|
|
384
|
+
metadata: { region: "eu" },
|
|
385
|
+
comms: { tone: "formal" },
|
|
386
|
+
}),
|
|
387
|
+
});
|
|
388
|
+
expect(r.status).toBe(200);
|
|
389
|
+
const { user } = (await r.json()) as { user: { metadata: Record<string, unknown> | null } };
|
|
390
|
+
expect(user.metadata).toEqual({
|
|
391
|
+
region: "eu",
|
|
392
|
+
comms: { tone: "formal" },
|
|
393
|
+
});
|
|
394
|
+
});
|
|
328
395
|
});
|
|
329
396
|
|
|
330
397
|
describe("identity link/unlink", () => {
|
|
@@ -620,6 +687,24 @@ describe("POST /api/users/:id/merge", () => {
|
|
|
620
687
|
const actor = { kind: "operator" as const, id: OPERATOR_FP };
|
|
621
688
|
linkIdentity(source.id, "slack", "U_SRC", actor);
|
|
622
689
|
linkIdentity(source.id, "github", "src-gh", actor);
|
|
690
|
+
const workflow = createWorkflow({
|
|
691
|
+
name: `merge-user-workflow-${crypto.randomUUID()}`,
|
|
692
|
+
definition: { nodes: [] },
|
|
693
|
+
});
|
|
694
|
+
const run = createWorkflowRun({
|
|
695
|
+
id: crypto.randomUUID(),
|
|
696
|
+
workflowId: workflow.id,
|
|
697
|
+
createdBy: source.id,
|
|
698
|
+
});
|
|
699
|
+
updateWorkflowRun(run.id, {
|
|
700
|
+
context: { swarm: { requestedByUserId: source.id }, retained: "value" },
|
|
701
|
+
});
|
|
702
|
+
const schedule = createScheduledTask({
|
|
703
|
+
name: `merge-user-schedule-${crypto.randomUUID()}`,
|
|
704
|
+
intervalMs: 60_000,
|
|
705
|
+
taskTemplate: "Run merged schedule",
|
|
706
|
+
createdBy: source.id,
|
|
707
|
+
});
|
|
623
708
|
|
|
624
709
|
const r = await authedFetch(`/api/users/${target.id}/merge`, {
|
|
625
710
|
method: "POST",
|
|
@@ -647,6 +732,13 @@ describe("POST /api/users/:id/merge", () => {
|
|
|
647
732
|
// Source user is gone.
|
|
648
733
|
const sourceR = await authedFetch(`/api/users/${source.id}`);
|
|
649
734
|
expect(sourceR.status).toBe(404);
|
|
735
|
+
expect(getWorkflowRun(run.id)?.createdBy).toBe(target.id);
|
|
736
|
+
expect(getWorkflowRun(run.id)?.context).toEqual({
|
|
737
|
+
swarm: { requestedByUserId: target.id },
|
|
738
|
+
retained: "value",
|
|
739
|
+
});
|
|
740
|
+
expect(getScheduledTaskById(schedule.id)?.createdBy).toBe(target.id);
|
|
741
|
+
expect(getScheduledTaskById(schedule.id)?.updatedBy).toBe(target.id);
|
|
650
742
|
});
|
|
651
743
|
|
|
652
744
|
test("manual_merge event payload carries the source user's id/name", async () => {
|
|
@@ -675,6 +767,79 @@ describe("POST /api/users/:id/merge", () => {
|
|
|
675
767
|
expect(mergeEvent!.after?.source?.email).toBe("ms@x.com");
|
|
676
768
|
});
|
|
677
769
|
|
|
770
|
+
test("rolls back identity moves when deletion fails", async () => {
|
|
771
|
+
const target = createUser({ name: "RollbackTarget", email: "rollback-target@x.com" });
|
|
772
|
+
const source = createUser({ name: "RollbackSource", email: "rollback-source@x.com" });
|
|
773
|
+
const workflow = createWorkflow({
|
|
774
|
+
name: `rollback-attribution-${crypto.randomUUID()}`,
|
|
775
|
+
definition: { nodes: [] },
|
|
776
|
+
});
|
|
777
|
+
const run = createWorkflowRun({
|
|
778
|
+
id: crypto.randomUUID(),
|
|
779
|
+
workflowId: workflow.id,
|
|
780
|
+
createdBy: source.id,
|
|
781
|
+
});
|
|
782
|
+
updateWorkflowRun(run.id, {
|
|
783
|
+
context: { swarm: { requestedByUserId: source.id }, retained: "rollback" },
|
|
784
|
+
});
|
|
785
|
+
const schedule = createScheduledTask({
|
|
786
|
+
name: `rollback-user-schedule-${crypto.randomUUID()}`,
|
|
787
|
+
intervalMs: 60_000,
|
|
788
|
+
taskTemplate: "Retain rollback schedule",
|
|
789
|
+
createdBy: source.id,
|
|
790
|
+
});
|
|
791
|
+
linkIdentity(source.id, "slack", "U_MERGE_ROLLBACK", {
|
|
792
|
+
kind: "operator",
|
|
793
|
+
id: OPERATOR_FP,
|
|
794
|
+
});
|
|
795
|
+
getDb().run(`CREATE TEMP TRIGGER fail_merge_source_delete
|
|
796
|
+
BEFORE DELETE ON users WHEN OLD.id = '${source.id}'
|
|
797
|
+
BEGIN SELECT RAISE(ABORT, 'forced merge failure'); END`);
|
|
798
|
+
|
|
799
|
+
try {
|
|
800
|
+
const response = await authedFetch(`/api/users/${target.id}/merge`, {
|
|
801
|
+
method: "POST",
|
|
802
|
+
body: JSON.stringify({ sourceUserId: source.id }),
|
|
803
|
+
});
|
|
804
|
+
expect(response.status).toBe(500);
|
|
805
|
+
|
|
806
|
+
const identity = getDb()
|
|
807
|
+
.prepare<{ userId: string }, [string, string]>(
|
|
808
|
+
"SELECT userId FROM user_external_ids WHERE kind = ? AND externalId = ?",
|
|
809
|
+
)
|
|
810
|
+
.get("slack", "U_MERGE_ROLLBACK");
|
|
811
|
+
expect(identity?.userId).toBe(source.id);
|
|
812
|
+
expect(
|
|
813
|
+
getDb()
|
|
814
|
+
.prepare<{ count: number }, [string]>("SELECT COUNT(*) AS count FROM users WHERE id = ?")
|
|
815
|
+
.get(source.id)?.count,
|
|
816
|
+
).toBe(1);
|
|
817
|
+
expect(
|
|
818
|
+
getDb()
|
|
819
|
+
.prepare<{ emailAliases: string }, [string]>(
|
|
820
|
+
"SELECT emailAliases FROM users WHERE id = ?",
|
|
821
|
+
)
|
|
822
|
+
.get(target.id)?.emailAliases,
|
|
823
|
+
).toBe("[]");
|
|
824
|
+
expect(
|
|
825
|
+
getDb()
|
|
826
|
+
.prepare<{ count: number }, [string]>(
|
|
827
|
+
"SELECT COUNT(*) AS count FROM user_identity_events WHERE userId = ?",
|
|
828
|
+
)
|
|
829
|
+
.get(target.id)?.count,
|
|
830
|
+
).toBe(0);
|
|
831
|
+
expect(getWorkflowRun(run.id)?.createdBy).toBe(source.id);
|
|
832
|
+
expect(getWorkflowRun(run.id)?.context).toEqual({
|
|
833
|
+
swarm: { requestedByUserId: source.id },
|
|
834
|
+
retained: "rollback",
|
|
835
|
+
});
|
|
836
|
+
expect(getScheduledTaskById(schedule.id)?.createdBy).toBe(source.id);
|
|
837
|
+
expect(getScheduledTaskById(schedule.id)?.updatedBy).toBe(source.id);
|
|
838
|
+
} finally {
|
|
839
|
+
getDb().run("DROP TRIGGER fail_merge_source_delete");
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
|
|
678
843
|
test("400 when target == source", async () => {
|
|
679
844
|
const u = createUser({ name: "Self" });
|
|
680
845
|
const r = await authedFetch(`/api/users/${u.id}/merge`, {
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { closeDb, createAgent, getAgentById, getDb, initDb, updateAgentProfile } from "../be/db";
|
|
3
|
+
import { postHookProfileUpdate } from "../hooks/hook";
|
|
4
|
+
import { BOOTSTRAP_MAX_CHARS } from "../prompts/base-prompt";
|
|
5
|
+
import {
|
|
6
|
+
checkIdentityFieldBudget,
|
|
7
|
+
IDENTITY_FIELD_BUDGETS,
|
|
8
|
+
IDENTITY_MD_MAX_CHARS,
|
|
9
|
+
IdentityFieldBudgetError,
|
|
10
|
+
SOUL_MD_MAX_CHARS,
|
|
11
|
+
} from "../utils/identity-field-budget";
|
|
12
|
+
|
|
13
|
+
const TEST_DB_PATH = "./test-identity-field-budget.sqlite";
|
|
14
|
+
|
|
15
|
+
describe("checkIdentityFieldBudget", () => {
|
|
16
|
+
test("exports the session-injection budgets from one policy module", () => {
|
|
17
|
+
expect(SOUL_MD_MAX_CHARS).toBe(10_000);
|
|
18
|
+
expect(IDENTITY_MD_MAX_CHARS).toBe(10_000);
|
|
19
|
+
expect(IDENTITY_FIELD_BUDGETS.claudeMd).toBe(BOOTSTRAP_MAX_CHARS);
|
|
20
|
+
expect(IDENTITY_FIELD_BUDGETS.toolsMd).toBe(BOOTSTRAP_MAX_CHARS);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("accepts writes at or below budget, including growth", () => {
|
|
24
|
+
expect(
|
|
25
|
+
checkIdentityFieldBudget({
|
|
26
|
+
field: "soulMd",
|
|
27
|
+
currentValue: "x",
|
|
28
|
+
nextValue: "x".repeat(SOUL_MD_MAX_CHARS),
|
|
29
|
+
}),
|
|
30
|
+
).toEqual({ ok: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("accepts a shrinking write while the result remains over budget", () => {
|
|
34
|
+
expect(
|
|
35
|
+
checkIdentityFieldBudget({
|
|
36
|
+
field: "identityMd",
|
|
37
|
+
currentValue: "x".repeat(12_000),
|
|
38
|
+
nextValue: "x".repeat(11_999),
|
|
39
|
+
}),
|
|
40
|
+
).toEqual({ ok: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("rejects growth and accepts equal-length rewrites while over budget", () => {
|
|
44
|
+
const growing = checkIdentityFieldBudget({
|
|
45
|
+
field: "claudeMd",
|
|
46
|
+
currentValue: "x".repeat(BOOTSTRAP_MAX_CHARS + 1),
|
|
47
|
+
nextValue: "x".repeat(BOOTSTRAP_MAX_CHARS + 2),
|
|
48
|
+
});
|
|
49
|
+
const equal = checkIdentityFieldBudget({
|
|
50
|
+
field: "toolsMd",
|
|
51
|
+
currentValue: "x".repeat(BOOTSTRAP_MAX_CHARS + 1),
|
|
52
|
+
nextValue: "y".repeat(BOOTSTRAP_MAX_CHARS + 1),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(growing.ok).toBeFalse();
|
|
56
|
+
expect(equal).toEqual({ ok: true });
|
|
57
|
+
if (growing.ok) throw new Error("Expected budget rejection");
|
|
58
|
+
expect(growing.reason).toContain("claudeMd");
|
|
59
|
+
expect(growing.reason).toContain("current size 20001");
|
|
60
|
+
expect(growing.reason).toContain("budget 20000");
|
|
61
|
+
expect(growing.reason).toContain("delta +1");
|
|
62
|
+
expect(growing.reason).toBe(
|
|
63
|
+
"Update rejected for claudeMd: current size 20001 characters, budget 20000 characters, delta +1 characters." +
|
|
64
|
+
" The tail past the 20000-character cap is dropped from the base prompt and only reaches harnesses with a native CLAUDE.md loader." +
|
|
65
|
+
" Move durable content into memories and keep pointers to it in this field.",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const toolsGrowth = checkIdentityFieldBudget({
|
|
69
|
+
field: "toolsMd",
|
|
70
|
+
currentValue: "x".repeat(BOOTSTRAP_MAX_CHARS + 1),
|
|
71
|
+
nextValue: "x".repeat(BOOTSTRAP_MAX_CHARS + 2),
|
|
72
|
+
});
|
|
73
|
+
if (toolsGrowth.ok) throw new Error("Expected toolsMd budget rejection");
|
|
74
|
+
expect(toolsGrowth.reason).toBe(
|
|
75
|
+
"Update rejected for toolsMd: current size 20001 characters, budget 20000 characters, delta +1 characters." +
|
|
76
|
+
" Content past the 20000-character cap is already silently dropped at read time, so shrinking that tail loses nothing sessions currently receive." +
|
|
77
|
+
" Move durable content into memories and keep pointers to it in this field.",
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("updateAgentProfile identity budget enforcement", () => {
|
|
83
|
+
const agentId = "identity-budget-agent";
|
|
84
|
+
|
|
85
|
+
beforeAll(async () => {
|
|
86
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
87
|
+
await Bun.file(TEST_DB_PATH + suffix)
|
|
88
|
+
.delete()
|
|
89
|
+
.catch(() => {});
|
|
90
|
+
}
|
|
91
|
+
initDb(TEST_DB_PATH);
|
|
92
|
+
createAgent({ id: agentId, name: "Identity Budget Agent", isLead: false, status: "idle" });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterAll(async () => {
|
|
96
|
+
closeDb();
|
|
97
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
98
|
+
await Bun.file(TEST_DB_PATH + suffix)
|
|
99
|
+
.delete()
|
|
100
|
+
.catch(() => {});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("preserves in-budget behavior and leaves heartbeatMd ungated", () => {
|
|
105
|
+
const heartbeat = "h".repeat(25_000);
|
|
106
|
+
const result = updateAgentProfile(agentId, {
|
|
107
|
+
soulMd: "s".repeat(SOUL_MD_MAX_CHARS),
|
|
108
|
+
heartbeatMd: heartbeat,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(result?.soulMd).toHaveLength(SOUL_MD_MAX_CHARS);
|
|
112
|
+
expect(result?.heartbeatMd).toBe(heartbeat);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("allows an existing oversized value to shrink or change at equal length but rejects growth", () => {
|
|
116
|
+
const current = "c".repeat(BOOTSTRAP_MAX_CHARS + 10);
|
|
117
|
+
getDb().prepare("UPDATE agents SET claudeMd = ? WHERE id = ?").run(current, agentId);
|
|
118
|
+
|
|
119
|
+
const shrunk = "s".repeat(BOOTSTRAP_MAX_CHARS + 9);
|
|
120
|
+
expect(updateAgentProfile(agentId, { claudeMd: shrunk })?.claudeMd).toBe(shrunk);
|
|
121
|
+
|
|
122
|
+
const equalLength = "e".repeat(shrunk.length);
|
|
123
|
+
expect(updateAgentProfile(agentId, { claudeMd: equalLength })?.claudeMd).toBe(equalLength);
|
|
124
|
+
expect(() => updateAgentProfile(agentId, { claudeMd: `${equalLength}g` })).toThrow(
|
|
125
|
+
IdentityFieldBudgetError,
|
|
126
|
+
);
|
|
127
|
+
expect(getAgentById(agentId)?.claudeMd).toBe(equalLength);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("Stop-hook profile sync visibility", () => {
|
|
132
|
+
test("logs a rejected response with the server's budget reason", async () => {
|
|
133
|
+
const reason =
|
|
134
|
+
"Update rejected for toolsMd: current size 20001 characters, budget 20000 characters, delta +1 characters.";
|
|
135
|
+
const errorSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
136
|
+
try {
|
|
137
|
+
await postHookProfileUpdate({
|
|
138
|
+
url: "https://api.example.test/api/agents/agent-1/profile",
|
|
139
|
+
headers: { Authorization: "Bearer secret", "X-Agent-ID": "agent-1" },
|
|
140
|
+
body: { toolsMd: "next", changeSource: "session_sync" },
|
|
141
|
+
label: "identity",
|
|
142
|
+
fetchImpl: (async () =>
|
|
143
|
+
new Response(JSON.stringify({ error: reason }), { status: 400 })) as typeof fetch,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
147
|
+
const logged = String(errorSpy.mock.calls[0]?.[0]);
|
|
148
|
+
expect(logged).toContain("identity profile sync failed: HTTP 400");
|
|
149
|
+
expect(logged).toContain(reason);
|
|
150
|
+
} finally {
|
|
151
|
+
errorSpy.mockRestore();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("logs thrown sync errors without failing shutdown", async () => {
|
|
156
|
+
const errorSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
157
|
+
try {
|
|
158
|
+
await expect(
|
|
159
|
+
postHookProfileUpdate({
|
|
160
|
+
url: "https://api.example.test/api/agents/agent-1/profile",
|
|
161
|
+
headers: {},
|
|
162
|
+
body: { claudeMd: "next" },
|
|
163
|
+
label: "claudeMd",
|
|
164
|
+
fetchImpl: (async () => {
|
|
165
|
+
throw new Error("network unavailable");
|
|
166
|
+
}) as typeof fetch,
|
|
167
|
+
}),
|
|
168
|
+
).resolves.toBeUndefined();
|
|
169
|
+
expect(String(errorSpy.mock.calls[0]?.[0])).toContain(
|
|
170
|
+
"claudeMd profile sync errored: network unavailable",
|
|
171
|
+
);
|
|
172
|
+
} finally {
|
|
173
|
+
errorSpy.mockRestore();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -436,4 +436,38 @@ describe("manage-user MCP tool (identities array)", () => {
|
|
|
436
436
|
const idents = getUserIdentities(userId);
|
|
437
437
|
expect(idents).toHaveLength(0);
|
|
438
438
|
});
|
|
439
|
+
|
|
440
|
+
test("update with comms merges into metadata.comms and preserves sibling metadata keys", async () => {
|
|
441
|
+
const created = createUser({ name: "Comms Merge", metadata: { customerId: "abc123" } });
|
|
442
|
+
|
|
443
|
+
const result = await callTool(server, "manage-user", {
|
|
444
|
+
action: "update",
|
|
445
|
+
userId: created.id,
|
|
446
|
+
comms: { tone: "casual", verbosity: "short" },
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
expect(result.isError).toBe(false);
|
|
450
|
+
const user = structuredOf(result).user as { metadata: Record<string, unknown> | null };
|
|
451
|
+
expect(user.metadata).toEqual({
|
|
452
|
+
customerId: "abc123",
|
|
453
|
+
comms: { tone: "casual", verbosity: "short" },
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("update with comms: null removes only the comms key", async () => {
|
|
458
|
+
const created = createUser({
|
|
459
|
+
name: "Comms Remove",
|
|
460
|
+
metadata: { customerId: "abc123", comms: { tone: "formal" } },
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const result = await callTool(server, "manage-user", {
|
|
464
|
+
action: "update",
|
|
465
|
+
userId: created.id,
|
|
466
|
+
comms: null,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
expect(result.isError).toBe(false);
|
|
470
|
+
const user = structuredOf(result).user as { metadata: Record<string, unknown> | null };
|
|
471
|
+
expect(user.metadata).toEqual({ customerId: "abc123" });
|
|
472
|
+
});
|
|
439
473
|
});
|
|
@@ -362,6 +362,23 @@ describe("/mcp-user auth and tool surface", () => {
|
|
|
362
362
|
expect(unknown.response.status).toBe(401);
|
|
363
363
|
});
|
|
364
364
|
|
|
365
|
+
test("owner /mcp returns method not found for sessionless server/discover", async () => {
|
|
366
|
+
const { response, payload } = await mcpPost(
|
|
367
|
+
API_KEY,
|
|
368
|
+
{ jsonrpc: "2.0", id: 7, method: "server/discover", params: {} },
|
|
369
|
+
undefined,
|
|
370
|
+
"/mcp",
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
expect(response.status).toBe(200);
|
|
374
|
+
expect(response.headers.get("mcp-session-id")).toBeNull();
|
|
375
|
+
expect(payload).toEqual({
|
|
376
|
+
jsonrpc: "2.0",
|
|
377
|
+
error: { code: -32601, message: "Method not found" },
|
|
378
|
+
id: 7,
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
365
382
|
test("owner /mcp path initializes with a known agent and rejects a different X-Agent-ID on the session", async () => {
|
|
366
383
|
const owner = createAgent({ name: "Owner MCP Agent", isLead: false, status: "idle" });
|
|
367
384
|
const other = createAgent({ name: "Other MCP Agent", isLead: false, status: "idle" });
|
|
@@ -22,6 +22,7 @@ type MetricRunResponse = {
|
|
|
22
22
|
result: {
|
|
23
23
|
columns: string[];
|
|
24
24
|
rows: Record<string, unknown>[];
|
|
25
|
+
truncated?: boolean;
|
|
25
26
|
};
|
|
26
27
|
}>;
|
|
27
28
|
result: {
|
|
@@ -378,4 +379,47 @@ describe("Metrics HTTP API", () => {
|
|
|
378
379
|
expect(res.status).toBe(400);
|
|
379
380
|
}
|
|
380
381
|
});
|
|
382
|
+
|
|
383
|
+
// Test D (Fix 1, db-query bounded-execution proposal §8): `truncated` still
|
|
384
|
+
// reaches the metrics response. Metric queries stay on the synchronous
|
|
385
|
+
// executeReadOnlyQuery path in this change (proposal §5.1's closing note),
|
|
386
|
+
// so this passes today via runMetricWidget's `total > rows.length` check —
|
|
387
|
+
// it must keep passing once db-query's other two call sites move to the
|
|
388
|
+
// bounded path.
|
|
389
|
+
test("D: a widget whose query returns more rows than maxRows reports truncated: true", async () => {
|
|
390
|
+
const created = await fetch(`${BASE}/api/metrics/definitions`, {
|
|
391
|
+
method: "POST",
|
|
392
|
+
headers,
|
|
393
|
+
body: JSON.stringify({
|
|
394
|
+
slug: "truncated-widget",
|
|
395
|
+
title: "Truncated Widget",
|
|
396
|
+
definition: {
|
|
397
|
+
version: 1,
|
|
398
|
+
widgets: [
|
|
399
|
+
{
|
|
400
|
+
id: "many-rows",
|
|
401
|
+
title: "Many rows",
|
|
402
|
+
query: {
|
|
403
|
+
sql: `WITH RECURSIVE cnt(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM cnt WHERE x < 50) SELECT x FROM cnt`,
|
|
404
|
+
maxRows: 10,
|
|
405
|
+
},
|
|
406
|
+
viz: { type: "table" },
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
},
|
|
410
|
+
}),
|
|
411
|
+
});
|
|
412
|
+
expect(created.status).toBe(201);
|
|
413
|
+
const { id } = (await created.json()) as { id: string };
|
|
414
|
+
|
|
415
|
+
const run = await fetch(`${BASE}/api/metrics/definitions/${id}/run`, {
|
|
416
|
+
method: "POST",
|
|
417
|
+
headers,
|
|
418
|
+
body: JSON.stringify({ variables: {} }),
|
|
419
|
+
});
|
|
420
|
+
expect(run.status).toBe(200);
|
|
421
|
+
const runBody = (await run.json()) as MetricRunResponse;
|
|
422
|
+
expect(runBody.widgets[0]?.result.rows.length).toBe(10);
|
|
423
|
+
expect(runBody.widgets[0]?.result.truncated).toBe(true);
|
|
424
|
+
});
|
|
381
425
|
});
|