@desplega.ai/agent-swarm 1.149.1 → 1.151.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 +18 -3
- package/dist/{acp-adapter-vhkzfpjg.js → acp-adapter-ex8hzjfc.js} +5 -5
- package/dist/{actions-9d2694yk.js → actions-2bgpyhww.js} +39 -11
- package/dist/{app-fq2ggynt.js → app-1706zhr1.js} +7 -7
- package/dist/{artifact-gj9arwrr.js → artifact-td4q4dch.js} +1 -1
- package/dist/{assistant-edb6n9nn.js → assistant-bavke5p7.js} +44 -20
- package/dist/{azure-openai-responses-cqar64rc.js → azure-openai-responses-f38x2avq.js} +13 -13
- package/dist/{boot-reembed-msweabbp.js → boot-reembed-3cmrrmpa.js} +9 -9
- package/dist/{boot-reembed-q5wd3tha.js → boot-reembed-nafcmmmc.js} +14 -16
- package/dist/{boot-scrub-logs-3g1az933.js → boot-scrub-logs-cd6a2sq0.js} +5 -5
- package/dist/{claude-adapter-v3rnzsdc.js → claude-adapter-wfp2gzvz.js} +15 -16
- package/dist/{claude-managed-adapter-qvsmh6mv.js → claude-managed-adapter-dsmkcrjy.js} +3 -3
- package/dist/{claude-sdk-session-qxrpsna1.js → claude-sdk-session-bht9jjak.js} +15 -16
- package/dist/{cli-5m0qmvdp.js → cli-0qb1z6s2.js} +1 -1
- package/dist/{cli-9d8ntetj.js → cli-2658f6at.js} +1 -1
- package/dist/{cli-7evafqvy.js → cli-2tbvf1z1.js} +85 -69
- package/dist/{cli-k351hv5k.js → cli-3v9xhjcn.js} +24 -12
- package/dist/{cli-1ebpar1q.js → cli-4wppfkcm.js} +350 -20
- package/dist/{cli-4260r1tg.js → cli-5jd2z7av.js} +97 -84
- package/dist/{cli-cv7s5zdq.js → cli-5tpgq60f.js} +9865 -715
- package/dist/{cli-6skz3rk8.js → cli-7tgcstn2.js} +9 -4
- package/dist/{cli-01g3x1ca.js → cli-9004vfdf.js} +108 -56
- package/dist/{cli-1c94wvcw.js → cli-a1z3h107.js} +124 -99
- package/dist/{cli-9br8ypfv.js → cli-czjtgfys.js} +2 -2
- package/dist/{cli-5my3bjsd.js → cli-d3p82erb.js} +7 -7
- package/dist/{cli-e29gmdmw.js → cli-dv6cdjs2.js} +2 -1
- package/dist/{cli-wby1xnnb.js → cli-f1wky6zp.js} +1 -1
- package/dist/{cli-zvz62chv.js → cli-fm4ytsdk.js} +4 -4
- package/dist/{cli-tg8qj8ee.js → cli-frcta2w4.js} +6 -6
- package/dist/{cli-7xx10h0a.js → cli-g69fdc9g.js} +2 -2
- package/dist/{cli-cmp8g3sw.js → cli-gbfwcd3k.js} +44 -9
- package/dist/{handlers-nsg0bcye.js → cli-gjyvhmmh.js} +97 -45
- package/dist/{cli-08b07b4r.js → cli-ht5rs63n.js} +1 -1
- package/dist/{cli-jecc01cb.js → cli-j4nv2wqy.js} +2 -4
- package/dist/{cli-cd60fv6j.js → cli-j9xczhaq.js} +1 -1
- package/dist/cli-pf9q3696.js +172 -0
- package/dist/{cli-xm87hzbw.js → cli-pgj4a5fb.js} +1 -1
- package/dist/{cli-jezx9b9c.js → cli-ppm4975n.js} +9 -3
- package/dist/{cli-3z634pqe.js → cli-q88st9aj.js} +2 -2
- package/dist/{cli-3sysyjve.js → cli-r2q6pevn.js} +142 -49
- package/dist/{cli-07jd68a1.js → cli-re8z5p8p.js} +1 -1
- package/dist/{cli-qgd4sqz5.js → cli-s14agm5s.js} +121 -14
- package/dist/{cli-svbrpf5q.js → cli-sjaae6ff.js} +9872 -424
- package/dist/{cli-73gq0zxn.js → cli-vwm416m6.js} +13 -13
- package/dist/{cli-4wtq5jjv.js → cli-wbyk465z.js} +1 -1
- package/dist/{cli-3apbzgyk.js → cli-z61ykvam.js} +19 -14
- package/dist/{cli-e06a8a7y.js → cli-zkxymas5.js} +4 -3
- package/dist/cli.js +26 -25
- package/dist/{codex-adapter-jyw7a91z.js → codex-adapter-jjjk8w4m.js} +8 -9
- package/dist/{codex-hook-hfrt9shr.js → codex-hook-8jexc34x.js} +3 -3
- package/dist/{codex-session-runner-bmngj0s5.js → codex-session-runner-nd6ayxkx.js} +8 -9
- package/dist/{commands-n755g391.js → commands-9v4psyq1.js} +5 -5
- package/dist/{db-s1cgh5gv.js → db-wnt58tpe.js} +5 -5
- package/dist/dispatcher-05jh1aqk.js +74 -0
- package/dist/{e2b-p04vmtx0.js → e2b-413b8t4v.js} +1 -1
- package/dist/{google-generative-ai-tsskx7te.js → google-generative-ai-ndjmj5fa.js} +7 -7
- package/dist/{google-vertex-ppw8ydpa.js → google-vertex-fr665tqs.js} +10 -10
- package/dist/handlers-tw90pkh5.js +65 -0
- package/dist/{hook-jf0xe8zh.js → hook-123e2scb.js} +11 -12
- package/dist/{hook-h9cqrm8q.js → hook-sxe3r349.js} +11 -12
- package/dist/{http-nc6pydw5.js → http-ecsar8ez.js} +1442 -581
- package/dist/{index-ezxkds2n.js → index-16js521z.js} +335 -107
- package/dist/{index-769nc012.js → index-hw26vj40.js} +25 -19
- package/dist/{index-0wdy6gwc.js → index-nnrbpcjp.js} +25 -17
- package/dist/index-ymn7em0p.js +25 -0
- package/dist/{index-x1zcj09z.js → index-zj518gf9.js} +26 -20
- package/dist/{keepalive-nesm3xb6.js → keepalive-asqtcm4y.js} +7 -7
- package/dist/{lead-bwy6t2za.js → lead-xbfr7fvt.js} +40 -48
- package/dist/{maintenance-8w393zf0.js → maintenance-qma55qvw.js} +9 -9
- package/dist/{oauth-refresh-sweep-sh8xjy08.js → oauth-refresh-sweep-3s04c6qw.js} +6 -6
- package/dist/{onboard-25ps29h1.js → onboard-drskd386.js} +13 -4
- package/dist/{openai-codex-responses-g7w48y30.js → openai-codex-responses-wy6a43wd.js} +13 -13
- package/dist/{openai-responses-nmeyptke.js → openai-responses-6xwf7bzg.js} +19 -19
- package/dist/{opencode-adapter-dce4wskd.js → opencode-adapter-28axqa09.js} +4 -4
- package/dist/{otel-impl-b93gxnz5.js → otel-impl-943c96er.js} +1 -1
- package/dist/{pi-mono-adapter-zvshk5cj.js → pi-mono-adapter-jcqt71cg.js} +17 -19
- package/dist/{pricing-refresh-5v56x7gb.js → pricing-refresh-mx51az05.js} +7 -7
- package/dist/{rbac-roles-x55cnezn.js → rbac-roles-78baecqd.js} +13 -9
- package/dist/{rbac-roles-0p0s6pbj.js → rbac-roles-wcmrg6sv.js} +7 -7
- package/dist/{render-v2-hyd45w8b.js → render-v2-8zjntgf5.js} +7 -7
- package/dist/{seed-pricing-49gbgn96.js → seed-pricing-jqjs6cws.js} +6 -6
- package/dist/{setup-qvq64vzq.js → setup-4pks0vfk.js} +2 -2
- package/dist/work-objects-wangt792.js +165 -0
- package/dist/{worker-scj60cxg.js → worker-p2mr422x.js} +40 -48
- package/openapi.json +1365 -268
- package/package.json +4 -3
- package/src/agentmail/handlers.ts +95 -66
- package/src/be/budget-refusal-notify.ts +1 -0
- package/src/be/db/tasks/read.ts +3 -0
- package/src/be/db/tasks/write.ts +13 -1
- package/src/be/db-client.ts +5 -0
- package/src/be/db.ts +15 -5
- package/src/be/extensions/db.ts +461 -0
- package/src/be/extensions/validate.ts +107 -0
- package/src/be/memory/boot-reembed.ts +17 -12
- package/src/be/memory/index.ts +12 -0
- package/src/be/memory/providers/openai-embedding.ts +4 -0
- package/src/be/memory/types.ts +2 -0
- package/src/be/migrations/153_deferred_task_wait_members.sql +25 -0
- package/src/be/migrations/154_routing_source.sql +3 -0
- package/src/be/migrations/155_extensions.sql +66 -0
- package/src/be/migrations/156_inbox_item_state_read_at.sql +7 -0
- package/src/be/rbac-roles.ts +3 -2
- package/src/be/scripts/typecheck.ts +59 -41
- package/src/be/seed/schedules-seeder.ts +25 -2
- package/src/be/seed/workflows-seeder.ts +17 -0
- package/src/be/seed-scripts/catalog/delegate.ts +3 -1
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/steering.ts +1 -0
- package/src/be/swarm-config-guard.ts +3 -1
- package/src/commands/credential-refresh.ts +70 -0
- package/src/commands/onboard/compose-generator.ts +14 -2
- package/src/commands/onboard/env-generator.ts +7 -0
- package/src/commands/provider-credentials.ts +2 -25
- package/src/commands/runner.ts +24 -57
- package/src/extensions/apply-task-create.ts +135 -0
- package/src/extensions/bundle-path.ts +10 -0
- package/src/extensions/contract-runtime.ts +7 -0
- package/src/extensions/contract-types.generated.ts +4 -0
- package/src/extensions/contract.ts +270 -0
- package/src/extensions/ctx.ts +186 -0
- package/src/extensions/dispatcher.ts +479 -0
- package/src/extensions/identity.ts +44 -0
- package/src/extensions/lifecycle.ts +281 -0
- package/src/extensions/loader.ts +207 -0
- package/src/extensions/post-bridge.ts +161 -0
- package/src/github/handlers.ts +185 -140
- package/src/gitlab/handlers.ts +97 -77
- package/src/heartbeat/heartbeat.ts +155 -83
- package/src/http/agents.ts +1 -1
- package/src/http/all-routes.ts +1 -0
- package/src/http/approval-requests.ts +1 -0
- package/src/http/apps.ts +14 -9
- package/src/http/config.ts +4 -1
- package/src/http/core.ts +38 -2
- package/src/http/extensions.ts +547 -0
- package/src/http/inbox-state.ts +2 -0
- package/src/http/index.ts +41 -3
- package/src/http/mcp-bridge.ts +96 -62
- package/src/http/mcp.ts +1 -1
- package/src/http/script-runs.ts +5 -0
- package/src/http/status.ts +6 -4
- package/src/http/tasks.ts +51 -30
- package/src/integrations/kapso/inbound.ts +15 -10
- package/src/jira/sync.ts +26 -16
- package/src/linear/sync.ts +26 -16
- package/src/prompts/session-templates.ts +18 -13
- package/src/providers/claude-adapter.ts +21 -8
- package/src/rbac/admission.ts +1 -1
- package/src/rbac/elevated-agents.ts +24 -0
- package/src/rbac/legacy-policy.ts +35 -6
- package/src/rbac/permissions.ts +8 -0
- package/src/scheduler/deferred-task-waits.ts +138 -27
- package/src/scheduler/schedule-task.ts +54 -24
- package/src/scheduler/scheduler.ts +108 -125
- package/src/scripts-runtime/executors/native.ts +5 -1
- package/src/scripts-runtime/executors/types.ts +5 -0
- package/src/scripts-runtime/import-allowlist.ts +79 -19
- package/src/scripts-runtime/sdk-allowlist.ts +4 -0
- package/src/scripts-runtime/swarm-config.ts +8 -0
- package/src/scripts-runtime/swarm-sdk.ts +3 -0
- package/src/scripts-runtime/types/stdlib.d.ts +16 -3
- package/src/scripts-runtime/types/swarm-extension.d.ts +562 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +16 -3
- package/src/server.ts +6 -0
- package/src/slack/HEURISTICS.md +3 -1
- package/src/slack/actions.ts +21 -16
- package/src/slack/app.ts +2 -0
- package/src/slack/assistant.ts +4 -12
- package/src/slack/blocks.ts +1 -1
- package/src/slack/handlers.ts +107 -19
- package/src/slack/inbound-files.ts +9 -5
- package/src/slack/render-v2.ts +188 -38
- package/src/slack/router.ts +13 -3
- package/src/slack/steering.ts +3 -3
- package/src/slack/thread-buffer.ts +66 -23
- package/src/slack/work-objects.test.ts +252 -0
- package/src/slack/work-objects.ts +118 -0
- package/src/tasks/errors.ts +9 -0
- package/src/tasks/sibling-awareness.ts +35 -1
- package/src/tasks/worker-follow-up.ts +67 -11
- package/src/tests/acp-adapter.test.ts +10 -10
- package/src/tests/agent-fs-live-url.test.ts +40 -0
- package/src/tests/agent-fs-provision-seeder.test.ts +12 -12
- package/src/tests/api-key-tracking.test.ts +18 -18
- package/src/tests/api-key.test.ts +4 -2
- package/src/tests/apps-rbac.test.ts +1 -1
- package/src/tests/apps-spike5.test.ts +1 -1
- package/src/tests/apps-sync-engine.test.ts +8 -8
- package/src/tests/apps-user-config.test.ts +1 -1
- package/src/tests/artifact-sdk.test.ts +8 -8
- package/src/tests/asset-key-mcp.test.ts +2 -0
- package/src/tests/automation-preflight-alert.test.ts +1 -1
- package/src/tests/base-prompt.test.ts +7 -6
- package/src/tests/budget-user-scope.test.ts +1 -1
- package/src/tests/budgets-routes.test.ts +1 -1
- package/src/tests/claude-adapter-binary.test.ts +22 -40
- package/src/tests/claude-adapter-otel.test.ts +13 -7
- package/src/tests/claude-adapter.test.ts +8 -6
- package/src/tests/claude-hook-credentials.test.ts +3 -3
- package/src/tests/claude-managed-adapter.test.ts +4 -4
- package/src/tests/claude-managed-setup.test.ts +3 -3
- package/src/tests/claude-sdk-lifecycle.test.ts +1 -1
- package/src/tests/claude-sdk-transport.test.ts +6 -6
- package/src/tests/claude-stop-hook.test.ts +8 -8
- package/src/tests/claude-worker-parity.test.ts +3 -3
- package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +5 -5
- package/src/tests/codex-adapter.test.ts +7 -5
- package/src/tests/codex-app-server.test.ts +2 -2
- package/src/tests/codex-login.test.ts +15 -15
- package/src/tests/codex-oauth-credential-info.test.ts +4 -4
- package/src/tests/codex-oauth-keep-warm.test.ts +4 -4
- package/src/tests/codex-oauth-refresh-lock.test.ts +1 -1
- package/src/tests/codex-oauth-storage.test.ts +7 -7
- package/src/tests/codex-pool.test.ts +1 -1
- package/src/tests/codex-steering-hook.test.ts +1 -1
- package/src/tests/codex-swarm-events.test.ts +1 -1
- package/src/tests/config-api-only-keys.test.ts +1 -1
- package/src/tests/config-session-auth.test.ts +2 -2
- package/src/tests/connection-embedded-auth.test.ts +20 -18
- package/src/tests/core-auth.test.ts +1 -1
- package/src/tests/create-page-tool.test.ts +3 -3
- package/src/tests/credential-refresh.test.ts +115 -0
- package/src/tests/credential-wait.test.ts +2 -2
- package/src/tests/credentials.test.ts +6 -4
- package/src/tests/db-queries-oauth.test.ts +40 -40
- package/src/tests/defer-task.test.ts +73 -8
- package/src/tests/deferred-task-wait-migration.test.ts +64 -0
- package/src/tests/deferred-task-wake.test.ts +349 -8
- package/src/tests/devin-adapter.test.ts +1 -1
- package/src/tests/devin-api.test.ts +1 -1
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/e2b-dispatch.test.ts +21 -19
- package/src/tests/e2e-claude-transport.test.ts +3 -3
- package/src/tests/e2e-redact.test.ts +6 -6
- package/src/tests/ensure-token.test.ts +68 -68
- package/src/tests/entrypoint-api-readiness.test.ts +1 -1
- package/src/tests/entrypoint-codex-oauth-seed.test.ts +13 -13
- package/src/tests/env-flag.test.ts +1 -0
- package/src/tests/events-http.test.ts +1 -1
- package/src/tests/extensions-db.test.ts +216 -0
- package/src/tests/extensions-dispatcher.test.ts +275 -0
- package/src/tests/extensions-example-heartbeat-remediate.test.ts +69 -0
- package/src/tests/extensions-example-mcp-install.test.ts +63 -0
- package/src/tests/extensions-example-no-exclamation-marks.test.ts +72 -0
- package/src/tests/extensions-example-rewrite-and-suppress.test.ts +70 -0
- package/src/tests/extensions-example-route-slack-channel.test.ts +217 -0
- package/src/tests/extensions-heartbeat.test.ts +291 -0
- package/src/tests/extensions-http.test.ts +431 -0
- package/src/tests/extensions-lead-equivalence.test.ts +49 -0
- package/src/tests/extensions-lifecycle.test.ts +209 -0
- package/src/tests/extensions-loader.test.ts +95 -0
- package/src/tests/extensions-mcp-tools.test.ts +245 -0
- package/src/tests/extensions-pre-task.test.ts +487 -0
- package/src/tests/extensions-run-context.test.ts +41 -0
- package/src/tests/extensions-tool-call.test.ts +288 -0
- package/src/tests/extensions-validate.test.ts +196 -0
- package/src/tests/fixtures/extensions/bad-import/hooks.ts +8 -0
- package/src/tests/fixtures/extensions/bad-import/manifest.json +9 -0
- package/src/tests/fixtures/extensions/bad-return-shape/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/bad-return-shape/manifest.json +9 -0
- package/src/tests/fixtures/extensions/block-tasks-from-source/hooks.ts +22 -0
- package/src/tests/fixtures/extensions/block-tasks-from-source/manifest.json +9 -0
- package/src/tests/fixtures/extensions/ignore-channel/hooks.ts +22 -0
- package/src/tests/fixtures/extensions/ignore-channel/manifest.json +9 -0
- package/src/tests/fixtures/extensions/load.ts +10 -0
- package/src/tests/fixtures/extensions/minimal/hooks.ts +7 -0
- package/src/tests/fixtures/extensions/minimal/manifest.json +9 -0
- package/src/tests/fixtures/extensions/never-fail-long-tasks/hooks.ts +11 -0
- package/src/tests/fixtures/extensions/never-fail-long-tasks/manifest.json +9 -0
- package/src/tests/fixtures/extensions/no-exclamation-marks/hooks.ts +23 -0
- package/src/tests/fixtures/extensions/no-exclamation-marks/manifest.json +9 -0
- package/src/tests/fixtures/extensions/post-logger/hooks.ts +9 -0
- package/src/tests/fixtures/extensions/post-logger/manifest.json +9 -0
- package/src/tests/fixtures/extensions/priority-a/hooks.ts +7 -0
- package/src/tests/fixtures/extensions/priority-a/manifest.json +9 -0
- package/src/tests/fixtures/extensions/priority-b/hooks.ts +7 -0
- package/src/tests/fixtures/extensions/priority-b/manifest.json +9 -0
- package/src/tests/fixtures/extensions/record-only-on-tag/hooks.ts +16 -0
- package/src/tests/fixtures/extensions/record-only-on-tag/manifest.json +9 -0
- package/src/tests/fixtures/extensions/reserved-assets/hooks.ts +5 -0
- package/src/tests/fixtures/extensions/reserved-assets/manifest.json +10 -0
- package/src/tests/fixtures/extensions/rewrite-progress-prefix/hooks.ts +22 -0
- package/src/tests/fixtures/extensions/rewrite-progress-prefix/manifest.json +9 -0
- package/src/tests/fixtures/extensions/rewrite-task-priority/hooks.ts +7 -0
- package/src/tests/fixtures/extensions/rewrite-task-priority/manifest.json +9 -0
- package/src/tests/fixtures/extensions/route-channel-to-agent/hooks.ts +26 -0
- package/src/tests/fixtures/extensions/route-channel-to-agent/manifest.json +9 -0
- package/src/tests/fixtures/extensions/slow/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/slow/manifest.json +9 -0
- package/src/tests/fixtures/extensions/suppress-lead-follow-up/hooks.ts +12 -0
- package/src/tests/fixtures/extensions/suppress-lead-follow-up/manifest.json +9 -0
- package/src/tests/fixtures/extensions/throws/hooks.ts +12 -0
- package/src/tests/fixtures/extensions/throws/manifest.json +9 -0
- package/src/tests/fixtures/extensions/worker-runtime/hooks.ts +5 -0
- package/src/tests/fixtures/extensions/worker-runtime/manifest.json +9 -0
- package/src/tests/fs-routes.test.ts +1 -1
- package/src/tests/gitlab-auth.test.ts +10 -10
- package/src/tests/harness-provider-resolution.test.ts +8 -5
- package/src/tests/heartbeat-reroute-decision.test.ts +1 -0
- package/src/tests/helper-credentials.test.ts +137 -0
- package/src/tests/http/context-routes.test.ts +2 -2
- package/src/tests/http-api-integration.test.ts +5 -2
- package/src/tests/http-users.test.ts +1 -1
- package/src/tests/identity-refresh.test.ts +2 -2
- package/src/tests/internal-ai/complete-structured.test.ts +2 -2
- package/src/tests/internal-ai/credentials.test.ts +19 -17
- package/src/tests/jira-metadata.test.ts +1 -1
- package/src/tests/jira-oauth.test.ts +5 -5
- package/src/tests/jira-sync.test.ts +1 -1
- package/src/tests/jira-webhook-lifecycle.test.ts +3 -3
- package/src/tests/jira-webhook.test.ts +2 -2
- package/src/tests/kapso-inbound.test.ts +2 -2
- package/src/tests/kv-http.test.ts +1 -1
- package/src/tests/kv-namespace-resolution.test.ts +1 -1
- package/src/tests/kv-page-proxy.test.ts +2 -2
- package/src/tests/launch-password-rejection.test.ts +2 -2
- package/src/tests/linear-webhook.test.ts +1 -1
- package/src/tests/mcp-oauth-dcr-reuse.test.ts +6 -6
- package/src/tests/mcp-oauth-ensure-token.test.ts +28 -22
- package/src/tests/mcp-oauth-manual-client.test.ts +16 -12
- package/src/tests/mcp-oauth-queries.test.ts +35 -32
- package/src/tests/mcp-oauth-resolve-secrets.test.ts +8 -8
- package/src/tests/mcp-oauth-wrapper.test.ts +20 -20
- package/src/tests/mcp-user-route.test.ts +1 -1
- package/src/tests/memory-edges.test.ts +1 -1
- package/src/tests/memory-edit-authorization.test.ts +32 -0
- package/src/tests/memory-rate-endpoint.test.ts +1 -1
- package/src/tests/memory-rate-tool.test.ts +2 -2
- package/src/tests/memory-rater-e2e.test.ts +1 -1
- package/src/tests/memory-rater-llm-summarizer.test.ts +3 -3
- package/src/tests/memory-rater-llm.test.ts +1 -1
- package/src/tests/model-control.test.ts +3 -0
- package/src/tests/multi-runtime-enabled-stats.test.ts +2 -2
- package/src/tests/multi-runtime-registration.test.ts +111 -17
- package/src/tests/oauth-access-token-tool.test.ts +18 -18
- package/src/tests/oauth-callback-flow.test.ts +8 -8
- package/src/tests/oauth-credential-bindings.test.ts +29 -27
- package/src/tests/oauth-keepalive.test.ts +10 -10
- package/src/tests/oauth-presets.test.ts +13 -5
- package/src/tests/oauth-refresh-failure.test.ts +9 -9
- package/src/tests/oauth-refresh-sweep.test.ts +20 -13
- package/src/tests/oauth-wrapper.test.ts +1 -1
- package/src/tests/onboard-compose.test.ts +47 -3
- package/src/tests/onboard-env.test.ts +26 -26
- package/src/tests/onboard-non-interactive.test.ts +4 -4
- package/src/tests/opencode-adapter.test.ts +1 -1
- package/src/tests/opencode-plugin.test.ts +2 -2
- package/src/tests/otel-session-cost-metrics.test.ts +1 -1
- package/src/tests/page-proxy-authed.test.ts +2 -2
- package/src/tests/page-proxy.test.ts +2 -2
- package/src/tests/page-session.test.ts +7 -7
- package/src/tests/page-viewer-auth.test.ts +2 -2
- package/src/tests/pages-authed-mode.test.ts +1 -1
- package/src/tests/pages-http.test.ts +1 -1
- package/src/tests/pages-password-mode.test.ts +1 -1
- package/src/tests/pages-public-authed-401.test.ts +1 -1
- package/src/tests/pages-public-html.test.ts +2 -2
- package/src/tests/pages-view-count.test.ts +1 -1
- package/src/tests/pi-mono-adapter.test.ts +10 -10
- package/src/tests/pi-mono-extension.test.ts +2 -2
- package/src/tests/preload.ts +5 -0
- package/src/tests/pricing-routes.test.ts +1 -1
- package/src/tests/profile-sync-warning.test.ts +8 -8
- package/src/tests/profile-sync.test.ts +8 -8
- package/src/tests/prompt-template-session.test.ts +36 -5
- package/src/tests/provider-process-group.test.ts +95 -4
- package/src/tests/providers/pi-cost.test.ts +1 -1
- package/src/tests/queue-stall-alarm.test.ts +1 -1
- package/src/tests/rbac-admission.test.ts +19 -4
- package/src/tests/rbac-charact-http.test.ts +1 -1
- package/src/tests/rbac-e2e-helpers.ts +1 -1
- package/src/tests/rbac-engine.test.ts +39 -0
- package/src/tests/realtime-room-auth.test.ts +1 -1
- package/src/tests/realtime-room-surfaces.test.ts +1 -1
- package/src/tests/reload-config.test.ts +45 -1
- package/src/tests/routing-affinity-inventory.test.ts +1 -1
- package/src/tests/routing-reason-contract.test.ts +76 -3
- package/src/tests/routing-reason-inventory.test.ts +11 -6
- package/src/tests/routing-source.test.ts +53 -0
- package/src/tests/runner-agent-fs-provision-retry.test.ts +1 -1
- package/src/tests/runner-context-preamble.test.ts +1 -1
- package/src/tests/runner-deployment-resume.test.ts +1 -1
- package/src/tests/runner-fallback-output.test.ts +1 -1
- package/src/tests/runner-identity-refresh.test.ts +1 -1
- package/src/tests/runner-skills-refresh.test.ts +1 -1
- package/src/tests/schedule-target-type.test.ts +3 -3
- package/src/tests/scheduled-tasks.test.ts +14 -5
- package/src/tests/scheduler-occurrence.test.ts +347 -0
- package/src/tests/script-apis-mcp.test.ts +1 -1
- package/src/tests/script-connections-http.test.ts +61 -58
- package/src/tests/script-connections-mcp.test.ts +1 -1
- package/src/tests/script-connections.test.ts +3 -1
- package/src/tests/script-runs-http.test.ts +15 -7
- package/src/tests/script-workflows-runtime-e2e.test.ts +1 -1
- package/src/tests/scripts-boot-reembed.test.ts +4 -0
- package/src/tests/scripts-embeddings.test.ts +4 -0
- package/src/tests/scripts-external-api.test.ts +1 -1
- package/src/tests/scripts-http.test.ts +1 -1
- package/src/tests/scripts-import-allowlist.test.ts +44 -1
- package/src/tests/scripts-mcp-e2e.test.ts +81 -1
- package/src/tests/scripts-only-gating.test.ts +4 -2
- package/src/tests/scripts-retention.test.ts +1 -1
- package/src/tests/scripts-runtime-identity.test.ts +2 -2
- package/src/tests/scripts-runtime-secret-egress.test.ts +1 -1
- package/src/tests/scripts-runtime.test.ts +6 -4
- package/src/tests/scripts-typecheck.test.ts +21 -0
- package/src/tests/sdk-allowlist.test.ts +1 -1
- package/src/tests/secret-scrubber.test.ts +38 -31
- package/src/tests/seed-registry.test.ts +46 -8
- package/src/tests/seed-schedules.test.ts +44 -9
- package/src/tests/seed-workflows.test.ts +21 -9
- package/src/tests/send-task-output-schema.test.ts +9 -1
- package/src/tests/send-task-requested-by.test.ts +1 -0
- package/src/tests/send-task-slack-routing-guard.test.ts +2 -0
- package/src/tests/session-costs-codex-recompute.test.ts +1 -1
- package/src/tests/session-costs-golden.test.ts +2 -2
- package/src/tests/session-costs-model-key-normalize.test.ts +2 -2
- package/src/tests/session-costs-recompute-all-providers.test.ts +1 -1
- package/src/tests/session-tokens.test.ts +1 -1
- package/src/tests/shutdown-error-scrubbing.test.ts +1 -1
- package/src/tests/slack-blocks.test.ts +12 -1
- package/src/tests/slack-file-tools-attachments.test.ts +1 -1
- package/src/tests/slack-inbound-files.test.ts +222 -86
- package/src/tests/slack-reaction-config.test.ts +5 -4
- package/src/tests/slack-read-boundaries.test.ts +1 -1
- package/src/tests/slack-render-v2.test.ts +196 -4
- package/src/tests/slack-router.test.ts +17 -2
- package/src/tests/slack-steering.test.ts +19 -0
- package/src/tests/status.test.ts +19 -7
- package/src/tests/steering-core.test.ts +1 -1
- package/src/tests/steering-enabled.test.ts +6 -8
- package/src/tests/steering-transport.test.ts +4 -4
- package/src/tests/store-progress-output-guidance.test.ts +34 -0
- package/src/tests/swarm-tool-result-gate.test.ts +11 -11
- package/src/tests/swarm-x-tool.test.ts +7 -5
- package/src/tests/sync-ai-toolbox-skills.test.ts +19 -0
- package/src/tests/task-supersede-resume.test.ts +1 -1
- package/src/tests/task-tool-preload-mcp.test.ts +4 -1
- package/src/tests/task-tools-ctx.test.ts +2 -0
- package/src/tests/tasks-requested-by-filter.test.ts +2 -2
- package/src/tests/telemetry-init.test.ts +11 -3
- package/src/tests/tool-annotations.test.ts +1 -1
- package/src/tests/tool-output-agent-id.test.ts +1 -0
- package/src/tests/tool-registrar-no-input.test.ts +42 -1
- package/src/tests/tracker-fold.test.ts +7 -7
- package/src/tests/tracker-tools.test.ts +3 -3
- package/src/tests/ui-followup-lead-delegation.test.ts +1 -0
- package/src/tests/unified-oauth-migration.test.ts +5 -5
- package/src/tests/user-token-rest-auth.test.ts +1 -1
- package/src/tests/user-token-routes.test.ts +1 -1
- package/src/tests/whoami-route.test.ts +1 -1
- package/src/tests/workflow-agent-task.test.ts +15 -13
- package/src/tests/workflow-ctx.test.ts +1 -1
- package/src/tests/workflow-e2e.test.ts +1 -1
- package/src/tests/workflow-engine-v2.test.ts +3 -1
- package/src/tests/workflow-executors.test.ts +2 -2
- package/src/tests/workflow-http-v2.test.ts +8 -6
- package/src/tests/workflow-input-redaction.test.ts +7 -5
- package/src/tests/workflow-integration-io.test.ts +2 -2
- package/src/tests/workflow-llm.test.ts +5 -5
- package/src/tests/workflow-registry.test.ts +1 -1
- package/src/tests/workflow-swarm-script.test.ts +1 -1
- package/src/tests/workflow-triggers-v2.test.ts +23 -21
- package/src/tests/x-composio.test.ts +9 -7
- package/src/tests/x402-client.test.ts +1 -1
- package/src/tests/x402-config.test.ts +28 -41
- package/src/tools/defer-task.ts +156 -24
- package/src/tools/extension-common.ts +84 -0
- package/src/tools/extension-install.ts +65 -0
- package/src/tools/extension-list.ts +66 -0
- package/src/tools/script-common.ts +43 -18
- package/src/tools/send-task.ts +87 -105
- package/src/tools/skills/skill-publish.ts +1 -0
- package/src/tools/store-progress.ts +1 -1
- package/src/tools/task-action.ts +43 -22
- package/src/tools/task-tool-ctx.ts +2 -0
- package/src/tools/templates.ts +2 -1
- package/src/tools/tool-config.ts +3 -1
- package/src/tools/utils.ts +140 -16
- package/src/types.ts +144 -0
- package/src/utils/constants.ts +7 -1
- package/src/utils/credentials.test.ts +16 -16
- package/src/utils/multi-runtime.ts +2 -2
- package/src/utils/steering-enabled.ts +4 -4
- package/src/workflows/executors/agent-task.ts +18 -3
- package/templates/ai-toolbox.manifest.json +7 -1
- package/templates/extensions/README.md +17 -0
- package/templates/extensions/notify-on-complete/README.md +11 -0
- package/templates/extensions/notify-on-complete/hooks.ts +61 -0
- package/templates/extensions/notify-on-complete/manifest.json +7 -0
- package/templates/extensions/require-ticket-ref/README.md +11 -0
- package/templates/extensions/require-ticket-ref/hooks.ts +30 -0
- package/templates/extensions/require-ticket-ref/manifest.json +7 -0
- package/templates/extensions/require-verification-note/README.md +11 -0
- package/templates/extensions/require-verification-note/hooks.ts +32 -0
- package/templates/extensions/require-verification-note/manifest.json +7 -0
- package/templates/schedules/daily-hn-briefing/config.json +1 -1
- package/templates/schedules/gtm-weekly-review/config.json +1 -1
- package/templates/schedules/weekly-code-health-reports/config.json +1 -1
- package/templates/schedules/weekly-dependabot-triage/config.json +1 -1
- package/templates/schedules/weekly-dora-metrics/config.json +1 -1
- package/templates/schedules/weekly-harness-upgrade-check/config.json +1 -1
- package/templates/skills/delegate-work/files/scripts/codex-exec.sh +1 -1
- package/templates/skills/scheduling/content.md +2 -0
- package/templates/skills/serply-search/SKILL.md +95 -0
- package/templates/skills/serply-search/config.json +13 -0
- package/templates/skills/serply-search/content.md +90 -0
- package/templates/skills/swarm-extensions/config.json +14 -0
- package/templates/skills/swarm-extensions/content.md +120 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -2
- package/templates/skills/swarm-scripts/content.md +2 -2
- package/templates/workflows/claude-code-changelog-watch/config.json +1 -1
- package/templates/workflows/gsc-topic-miner/config.json +1 -1
- package/templates/workflows/linear-drain-loop/config.json +1 -1
- package/templates/workflows/llm-safe-release-context/config.json +1 -1
- package/templates/workflows/ralph-loop/config.json +1 -1
- package/dist/cli-7fehefdf.js +0 -562
- package/dist/cli-ayvy5kde.js +0 -164
- package/dist/cli-ddfw1mc4.js +0 -4322
- package/dist/cli-hzqmgcm2.js +0 -3066
- package/dist/cli-j4d4f6hv.js +0 -338
- package/dist/cli-qgm2pfpq.js +0 -6377
- package/dist/cli-sdzq01t9.js +0 -823
- package/dist/cli-xvkmedgn.js +0 -332
- package/dist/cli-y3pq4v9g.js +0 -664
- package/dist/{anthropic-messages-vs2rabzp.js → anthropic-messages-2y8nktar.js} +19 -19
- package/dist/{claude-managed-setup-f5pmsyrp.js → claude-managed-setup-48dwg9bv.js} +3 -3
- package/dist/{cli-81r683bv.js → cli-ch38q5z4.js} +0 -0
- package/dist/{cli-6664w7y4.js → cli-kj76p1ez.js} +8 -8
- package/dist/{cli-c71mg51n.js → cli-r2fpe7cx.js} +5 -5
- package/dist/{cli-4xz3pmrq.js → cli-y6x0gqtg.js} +16 -16
- package/dist/{index-aa3sa6s8.js → index-2wsdsnmm.js} +1 -1
- package/dist/{mistral-conversations-4ryk7h83.js → mistral-conversations-qhr84ph3.js} +9 -9
- package/dist/{openai-completions-0bqmsajw.js → openai-completions-32rdwmze.js} +19 -19
- package/dist/{openrouter-images-qx18fyp8.js → openrouter-images-9f9ek4rw.js} +3 -3
- package/dist/{pi-messages-79jvzz1d.js → pi-messages-b1mbr6gp.js} +4 -4
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { closeDb, getDbClient, initDb } from "../be/db";
|
|
3
|
+
import {
|
|
4
|
+
deleteExtension,
|
|
5
|
+
getExtensionById,
|
|
6
|
+
getExtensionFiles,
|
|
7
|
+
getExtensionVersion,
|
|
8
|
+
insertExtensionRun,
|
|
9
|
+
installExtension,
|
|
10
|
+
listExtensionRuns,
|
|
11
|
+
listExtensionVersions,
|
|
12
|
+
pruneExtensionRuns,
|
|
13
|
+
setExtensionState,
|
|
14
|
+
updateExtensionMeta,
|
|
15
|
+
} from "../be/extensions/db";
|
|
16
|
+
import { loadBundleFixture } from "./fixtures/extensions/load";
|
|
17
|
+
|
|
18
|
+
const TEST_DB_PATH = "./test-extensions-db.sqlite";
|
|
19
|
+
|
|
20
|
+
async function removeDbFiles(): Promise<void> {
|
|
21
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
22
|
+
await Bun.file(TEST_DB_PATH + suffix)
|
|
23
|
+
.delete()
|
|
24
|
+
.catch(() => {});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("extension DB helpers", () => {
|
|
29
|
+
beforeAll(async () => {
|
|
30
|
+
await removeDbFiles();
|
|
31
|
+
initDb(TEST_DB_PATH);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterAll(async () => {
|
|
35
|
+
closeDb();
|
|
36
|
+
await removeDbFiles();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
beforeEach(async () => {
|
|
40
|
+
await getDbClient().run("DELETE FROM extensions");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("install creates version 1 and one active file", async () => {
|
|
44
|
+
const bundle = await loadBundleFixture("minimal");
|
|
45
|
+
const result = await installExtension({
|
|
46
|
+
...bundle,
|
|
47
|
+
agentId: "lead-1",
|
|
48
|
+
createdBy: "user-1",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.isNew).toBe(true);
|
|
52
|
+
expect(result.contentDeduped).toBe(false);
|
|
53
|
+
expect(result.extension.version).toBe(1);
|
|
54
|
+
expect(result.extension.activeVersion).toBe(1);
|
|
55
|
+
expect(result.extension.enabled).toBe(false);
|
|
56
|
+
expect(await getExtensionFiles(result.extension.id)).toEqual(bundle.files);
|
|
57
|
+
expect((await listExtensionVersions(result.extension.id)).map((row) => row.version)).toEqual([
|
|
58
|
+
1,
|
|
59
|
+
]);
|
|
60
|
+
expect(
|
|
61
|
+
(
|
|
62
|
+
await getDbClient().get<{ count: number }>(
|
|
63
|
+
"SELECT COUNT(*) AS count FROM extension_files WHERE extensionId = ?",
|
|
64
|
+
[result.extension.id],
|
|
65
|
+
)
|
|
66
|
+
)?.count,
|
|
67
|
+
).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("matching bundles dedupe and changed hooks create a complete snapshot", async () => {
|
|
71
|
+
const bundle = await loadBundleFixture("minimal");
|
|
72
|
+
const first = await installExtension(bundle);
|
|
73
|
+
const same = await installExtension(bundle);
|
|
74
|
+
expect(same.contentDeduped).toBe(true);
|
|
75
|
+
expect(same.extension.version).toBe(1);
|
|
76
|
+
|
|
77
|
+
const changedFiles = {
|
|
78
|
+
...bundle.files,
|
|
79
|
+
"hooks.ts": `${bundle.files["hooks.ts"]}\n// version 2\n`,
|
|
80
|
+
};
|
|
81
|
+
const changed = await installExtension({
|
|
82
|
+
manifest: bundle.manifest,
|
|
83
|
+
files: changedFiles,
|
|
84
|
+
changeReason: "Change hooks",
|
|
85
|
+
});
|
|
86
|
+
expect(changed.contentDeduped).toBe(false);
|
|
87
|
+
expect(changed.extension.id).toBe(first.extension.id);
|
|
88
|
+
expect(changed.extension.version).toBe(2);
|
|
89
|
+
expect(await getExtensionFiles(changed.extension.id)).toEqual(changedFiles);
|
|
90
|
+
|
|
91
|
+
const versions = await listExtensionVersions(changed.extension.id);
|
|
92
|
+
expect(versions.map((row) => row.version)).toEqual([2, 1]);
|
|
93
|
+
expect(JSON.parse(versions[0]!.filesJson)).toEqual(changedFiles);
|
|
94
|
+
expect(JSON.parse(versions[1]!.filesJson)).toEqual(bundle.files);
|
|
95
|
+
expect((await getExtensionVersion(changed.extension.id, 2))?.changeReason).toBe("Change hooks");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("manifest-only changes create a version with a complete snapshot", async () => {
|
|
99
|
+
const bundle = await loadBundleFixture("minimal");
|
|
100
|
+
const first = await installExtension(bundle);
|
|
101
|
+
const manifest = { ...bundle.manifest, description: "Changed manifest description" };
|
|
102
|
+
const changed = await installExtension({ manifest, files: bundle.files });
|
|
103
|
+
|
|
104
|
+
expect(changed.contentDeduped).toBe(false);
|
|
105
|
+
expect(changed.extension.id).toBe(first.extension.id);
|
|
106
|
+
expect(changed.extension.version).toBe(2);
|
|
107
|
+
const versions = await listExtensionVersions(first.extension.id);
|
|
108
|
+
expect(versions.map((row) => row.version)).toEqual([2, 1]);
|
|
109
|
+
expect(JSON.parse(versions[0]!.manifestJson)).toEqual(manifest);
|
|
110
|
+
expect(JSON.parse(versions[1]!.manifestJson)).toEqual(bundle.manifest);
|
|
111
|
+
expect(JSON.parse(versions[0]!.filesJson)).toEqual(bundle.files);
|
|
112
|
+
expect(JSON.parse(versions[1]!.filesJson)).toEqual(bundle.files);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("identical content can activate the current version without creating another version", async () => {
|
|
116
|
+
const bundle = await loadBundleFixture("minimal");
|
|
117
|
+
const first = await installExtension(bundle);
|
|
118
|
+
const files = { ...bundle.files, "hooks.ts": `${bundle.files["hooks.ts"]}\n// version 2\n` };
|
|
119
|
+
const changed = await installExtension({ manifest: bundle.manifest, files });
|
|
120
|
+
expect(changed.extension).toMatchObject({ version: 2, activeVersion: 1 });
|
|
121
|
+
|
|
122
|
+
const activated = await installExtension({ manifest: bundle.manifest, files, activate: true });
|
|
123
|
+
expect(activated.contentDeduped).toBe(true);
|
|
124
|
+
expect(activated.extension).toMatchObject({ version: 2, activeVersion: 2 });
|
|
125
|
+
expect((await getExtensionById(first.extension.id))?.activeVersion).toBe(2);
|
|
126
|
+
expect(await listExtensionVersions(first.extension.id)).toHaveLength(2);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("concurrent changed installs allocate distinct versions atomically", async () => {
|
|
130
|
+
const bundle = await loadBundleFixture("minimal");
|
|
131
|
+
const first = await installExtension(bundle);
|
|
132
|
+
const changedA = { ...bundle.files, "hooks.ts": `${bundle.files["hooks.ts"]}\n// A\n` };
|
|
133
|
+
const changedB = { ...bundle.files, "hooks.ts": `${bundle.files["hooks.ts"]}\n// B\n` };
|
|
134
|
+
|
|
135
|
+
const results = await Promise.all([
|
|
136
|
+
installExtension({ manifest: bundle.manifest, files: changedA }),
|
|
137
|
+
installExtension({ manifest: bundle.manifest, files: changedB }),
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
expect(results.map((result) => result.extension.version).sort()).toEqual([2, 3]);
|
|
141
|
+
const versions = await listExtensionVersions(first.extension.id);
|
|
142
|
+
expect(versions.map((row) => row.version)).toEqual([3, 2, 1]);
|
|
143
|
+
expect(await getExtensionFiles(first.extension.id)).toEqual(JSON.parse(versions[0]!.filesJson));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("metadata and state updates preserve the stored bundle", async () => {
|
|
147
|
+
const installed = await installExtension(await loadBundleFixture("minimal"));
|
|
148
|
+
const updated = await updateExtensionMeta(installed.extension.id, {
|
|
149
|
+
priority: 10,
|
|
150
|
+
config: { channelId: "C1" },
|
|
151
|
+
description: "Updated description",
|
|
152
|
+
});
|
|
153
|
+
expect(updated?.priority).toBe(10);
|
|
154
|
+
expect(updated?.configJson).toBe('{"channelId":"C1"}');
|
|
155
|
+
expect(updated?.description).toBe("Updated description");
|
|
156
|
+
|
|
157
|
+
const enabled = await setExtensionState(installed.extension.id, {
|
|
158
|
+
enabled: true,
|
|
159
|
+
status: "enabled",
|
|
160
|
+
consecutiveFailures: 2,
|
|
161
|
+
lastError: "test error",
|
|
162
|
+
});
|
|
163
|
+
expect(enabled?.enabled).toBe(true);
|
|
164
|
+
expect(enabled?.status).toBe("enabled");
|
|
165
|
+
expect(enabled?.consecutiveFailures).toBe(2);
|
|
166
|
+
expect(enabled?.lastError).toBe("test error");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("run logs retain only the newest 500 rows", async () => {
|
|
170
|
+
const installed = await installExtension(await loadBundleFixture("minimal"));
|
|
171
|
+
for (let index = 0; index < 505; index++) {
|
|
172
|
+
await insertExtensionRun({
|
|
173
|
+
extensionId: installed.extension.id,
|
|
174
|
+
version: 1,
|
|
175
|
+
event: "pre.task.create",
|
|
176
|
+
action: "continue",
|
|
177
|
+
message: String(index),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
expect(
|
|
181
|
+
(
|
|
182
|
+
await getDbClient().get<{ count: number }>(
|
|
183
|
+
"SELECT count(*) AS count FROM extension_runs WHERE extensionId = ?",
|
|
184
|
+
[installed.extension.id],
|
|
185
|
+
)
|
|
186
|
+
)?.count,
|
|
187
|
+
).toBe(505);
|
|
188
|
+
await pruneExtensionRuns(installed.extension.id);
|
|
189
|
+
expect(
|
|
190
|
+
(
|
|
191
|
+
await getDbClient().get<{ count: number }>(
|
|
192
|
+
"SELECT count(*) AS count FROM extension_runs WHERE extensionId = ?",
|
|
193
|
+
[installed.extension.id],
|
|
194
|
+
)
|
|
195
|
+
)?.count,
|
|
196
|
+
).toBe(500);
|
|
197
|
+
const rows = await listExtensionRuns(installed.extension.id, 500);
|
|
198
|
+
expect(rows).toHaveLength(500);
|
|
199
|
+
expect(rows[0]?.message).toBe("504");
|
|
200
|
+
expect(rows.at(-1)?.message).toBe("5");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("delete cascades extension children", async () => {
|
|
204
|
+
const installed = await installExtension(await loadBundleFixture("minimal"));
|
|
205
|
+
await insertExtensionRun({
|
|
206
|
+
extensionId: installed.extension.id,
|
|
207
|
+
version: 1,
|
|
208
|
+
event: "post.task.created",
|
|
209
|
+
action: "continue",
|
|
210
|
+
});
|
|
211
|
+
expect(await deleteExtension(installed.extension.id)).toBe(true);
|
|
212
|
+
expect(await getExtensionById(installed.extension.id)).toBeNull();
|
|
213
|
+
expect(await listExtensionVersions(installed.extension.id)).toEqual([]);
|
|
214
|
+
expect(await listExtensionRuns(installed.extension.id)).toEqual([]);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { closeDb, getDbClient, getKv, initDb } from "../be/db";
|
|
3
|
+
import {
|
|
4
|
+
getExtensionById,
|
|
5
|
+
installExtension,
|
|
6
|
+
listExtensionRuns,
|
|
7
|
+
setExtensionState,
|
|
8
|
+
} from "../be/extensions/db";
|
|
9
|
+
import { buildCtx } from "../extensions/ctx";
|
|
10
|
+
import {
|
|
11
|
+
dispatchPost,
|
|
12
|
+
dispatchPre,
|
|
13
|
+
ExtensionAbortedError,
|
|
14
|
+
extensionIdForAgent,
|
|
15
|
+
isExtensionAgentId,
|
|
16
|
+
listRegistered,
|
|
17
|
+
registerLoaded,
|
|
18
|
+
unregister,
|
|
19
|
+
} from "../extensions/dispatcher";
|
|
20
|
+
import { ensureExtensionAgent } from "../extensions/identity";
|
|
21
|
+
import {
|
|
22
|
+
ExtensionLifecycleError,
|
|
23
|
+
setExtensionLoopbackBaseUrl,
|
|
24
|
+
stopExtensionRuntime,
|
|
25
|
+
} from "../extensions/lifecycle";
|
|
26
|
+
import { cleanExtensionTmpRoot, loadExtension } from "../extensions/loader";
|
|
27
|
+
import type { ExtensionInstallBody } from "../types";
|
|
28
|
+
import { loadBundleFixture } from "./fixtures/extensions/load";
|
|
29
|
+
|
|
30
|
+
const TEST_DB_PATH = "./test-extensions-dispatcher.sqlite";
|
|
31
|
+
let savedTimeout: string | undefined;
|
|
32
|
+
let savedFailureLimit: string | undefined;
|
|
33
|
+
|
|
34
|
+
async function removeDbFiles(): Promise<void> {
|
|
35
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
36
|
+
await Bun.file(TEST_DB_PATH + suffix)
|
|
37
|
+
.delete()
|
|
38
|
+
.catch(() => {});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function loadAndRegister(bundle: ExtensionInstallBody, priority = 100) {
|
|
43
|
+
const installed = await installExtension({ ...bundle, priority });
|
|
44
|
+
const record = await setExtensionState(installed.extension.id, {
|
|
45
|
+
agentId: await ensureExtensionAgent(installed.extension.name),
|
|
46
|
+
});
|
|
47
|
+
const loaded = await loadExtension({ record: record!, ...bundle });
|
|
48
|
+
registerLoaded(loaded);
|
|
49
|
+
return loaded;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function fixture(name: string, priority = 100) {
|
|
53
|
+
return await loadAndRegister(await loadBundleFixture(name), priority);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function taskPayload(description = "start") {
|
|
57
|
+
return { options: {}, description, origin: "rest" as const };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function blockBundle(name: string, reason: string): Promise<ExtensionInstallBody> {
|
|
61
|
+
const bundle = await loadBundleFixture("minimal");
|
|
62
|
+
bundle.manifest = { ...bundle.manifest, name, description: reason };
|
|
63
|
+
bundle.files["hooks.ts"] = `
|
|
64
|
+
import { block, type SwarmExtension } from "swarm-extension";
|
|
65
|
+
const extension: SwarmExtension = (api) => {
|
|
66
|
+
api.on("pre.task.create", () => block(${JSON.stringify(reason)}));
|
|
67
|
+
};
|
|
68
|
+
export default extension;
|
|
69
|
+
`;
|
|
70
|
+
return bundle;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("extension dispatcher", () => {
|
|
74
|
+
beforeAll(async () => {
|
|
75
|
+
savedTimeout = process.env.EXTENSION_HANDLER_TIMEOUT_MS;
|
|
76
|
+
savedFailureLimit = process.env.EXTENSION_MAX_CONSECUTIVE_FAILURES;
|
|
77
|
+
await removeDbFiles();
|
|
78
|
+
initDb(TEST_DB_PATH);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterAll(async () => {
|
|
82
|
+
for (const loaded of listRegistered()) {
|
|
83
|
+
unregister(loaded.record.id);
|
|
84
|
+
await loaded.dispose();
|
|
85
|
+
}
|
|
86
|
+
await cleanExtensionTmpRoot();
|
|
87
|
+
closeDb();
|
|
88
|
+
await removeDbFiles();
|
|
89
|
+
if (savedTimeout === undefined) delete process.env.EXTENSION_HANDLER_TIMEOUT_MS;
|
|
90
|
+
else process.env.EXTENSION_HANDLER_TIMEOUT_MS = savedTimeout;
|
|
91
|
+
if (savedFailureLimit === undefined) delete process.env.EXTENSION_MAX_CONSECUTIVE_FAILURES;
|
|
92
|
+
else process.env.EXTENSION_MAX_CONSECUTIVE_FAILURES = savedFailureLimit;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
beforeEach(async () => {
|
|
96
|
+
for (const loaded of listRegistered()) {
|
|
97
|
+
unregister(loaded.record.id);
|
|
98
|
+
await loaded.dispose();
|
|
99
|
+
}
|
|
100
|
+
await cleanExtensionTmpRoot();
|
|
101
|
+
await getDbClient().run("DELETE FROM extensions");
|
|
102
|
+
process.env.EXTENSION_HANDLER_TIMEOUT_MS = "5000";
|
|
103
|
+
process.env.EXTENSION_MAX_CONSECUTIVE_FAILURES = "5";
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("runs by priority and feeds each modify into the next handler", async () => {
|
|
107
|
+
await fixture("priority-a", 20);
|
|
108
|
+
await fixture("priority-b", 10);
|
|
109
|
+
expect(await dispatchPre("pre.task.create", taskPayload())).toEqual({
|
|
110
|
+
action: "modify",
|
|
111
|
+
data: { description: "startba" },
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("returns the first block", async () => {
|
|
116
|
+
await loadAndRegister(await blockBundle("block-first", "first"), 10);
|
|
117
|
+
await loadAndRegister(await blockBundle("block-second", "second"), 20);
|
|
118
|
+
const result = await dispatchPre("pre.task.create", taskPayload());
|
|
119
|
+
expect(result).toMatchObject({ action: "block", reason: "first" });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("fails open and records thrown handlers", async () => {
|
|
123
|
+
const loaded = await fixture("throws");
|
|
124
|
+
expect(await dispatchPre("pre.task.create", taskPayload())).toEqual({ action: "continue" });
|
|
125
|
+
expect((await getExtensionById(loaded.record.id))?.consecutiveFailures).toBe(1);
|
|
126
|
+
expect(await listExtensionRuns(loaded.record.id)).toMatchObject([
|
|
127
|
+
{ action: "error", event: "pre.task.create", message: "fixture failure" },
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("auto-disables after five consecutive failures", async () => {
|
|
132
|
+
const loaded = await fixture("throws");
|
|
133
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
134
|
+
expect(await dispatchPre("pre.task.create", taskPayload())).toEqual({ action: "continue" });
|
|
135
|
+
}
|
|
136
|
+
expect(await getExtensionById(loaded.record.id)).toMatchObject({
|
|
137
|
+
enabled: false,
|
|
138
|
+
status: "auto-disabled",
|
|
139
|
+
consecutiveFailures: 5,
|
|
140
|
+
});
|
|
141
|
+
expect(listRegistered()).toHaveLength(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("times out and fails open", async () => {
|
|
145
|
+
process.env.EXTENSION_HANDLER_TIMEOUT_MS = "5";
|
|
146
|
+
const loaded = await fixture("slow");
|
|
147
|
+
const handler = loaded.handlers[0]!;
|
|
148
|
+
const original = handler.handler;
|
|
149
|
+
const finished = Promise.withResolvers<void>();
|
|
150
|
+
let lateError: unknown;
|
|
151
|
+
handler.handler = async (event, ctx) => {
|
|
152
|
+
try {
|
|
153
|
+
return await original(event, ctx);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
lateError = error;
|
|
156
|
+
throw error;
|
|
157
|
+
} finally {
|
|
158
|
+
finished.resolve();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
expect(await dispatchPre("pre.task.create", taskPayload())).toEqual({ action: "continue" });
|
|
162
|
+
expect(
|
|
163
|
+
(await listExtensionRuns(loaded.record.id)).some((run) => run.action === "timeout"),
|
|
164
|
+
).toBe(true);
|
|
165
|
+
await finished.promise;
|
|
166
|
+
expect(lateError).toBeInstanceOf(ExtensionAbortedError);
|
|
167
|
+
expect(await getKv(`task:agent:${loaded.record.agentId}`, "ext:slow:after-timeout")).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("uses the script SDK through the bound loopback URL and rejects before listen", async () => {
|
|
171
|
+
await stopExtensionRuntime();
|
|
172
|
+
const loaded = await fixture("minimal");
|
|
173
|
+
const ctx = buildCtx(loaded, "pre.task.create", new AbortController().signal);
|
|
174
|
+
await expect(ctx.swarm.task_get({ taskId: "x" })).rejects.toBeInstanceOf(
|
|
175
|
+
ExtensionLifecycleError,
|
|
176
|
+
);
|
|
177
|
+
await expect(ctx.swarm.task_get({ taskId: "x" })).rejects.toThrow(
|
|
178
|
+
"extension loopback not ready",
|
|
179
|
+
);
|
|
180
|
+
const savedFetch = globalThis.fetch;
|
|
181
|
+
const savedKey = process.env.AGENT_SWARM_API_KEY;
|
|
182
|
+
const requests: Request[] = [];
|
|
183
|
+
try {
|
|
184
|
+
process.env.AGENT_SWARM_API_KEY = "123123";
|
|
185
|
+
setExtensionLoopbackBaseUrl("http://127.0.0.1:1");
|
|
186
|
+
globalThis.fetch = (async (input, init) => {
|
|
187
|
+
requests.push(new Request(input, init));
|
|
188
|
+
return Response.json({ task: { id: "x" } });
|
|
189
|
+
}) as typeof fetch;
|
|
190
|
+
let result: unknown;
|
|
191
|
+
loaded.handlers[0]!.handler = async (_event, handlerCtx) => {
|
|
192
|
+
result = await handlerCtx.swarm.task_get({ taskId: "x" });
|
|
193
|
+
};
|
|
194
|
+
await dispatchPre("pre.task.create", taskPayload());
|
|
195
|
+
expect(requests[0]?.url).toBe("http://127.0.0.1:1/api/tasks/x");
|
|
196
|
+
expect(requests[0]?.headers.get("X-Agent-ID")).toBe(loaded.record.agentId);
|
|
197
|
+
expect(requests[0]?.headers.get("Authorization")).toBe("Bearer 123123");
|
|
198
|
+
expect(result).toEqual({ success: true, status: 200, data: { task: { id: "x" } } });
|
|
199
|
+
expect(isExtensionAgentId(loaded.record.agentId)).toBe(true);
|
|
200
|
+
expect(extensionIdForAgent(loaded.record.agentId)).toBe(loaded.record.id);
|
|
201
|
+
} finally {
|
|
202
|
+
globalThis.fetch = savedFetch;
|
|
203
|
+
if (savedKey === undefined) delete process.env.AGENT_SWARM_API_KEY;
|
|
204
|
+
else process.env.AGENT_SWARM_API_KEY = savedKey;
|
|
205
|
+
await stopExtensionRuntime();
|
|
206
|
+
}
|
|
207
|
+
expect(isExtensionAgentId(loaded.record.agentId)).toBe(false);
|
|
208
|
+
expect(extensionIdForAgent(loaded.record.agentId)).toBeUndefined();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("post dispatch skips its creator and runs the other extension", async () => {
|
|
212
|
+
const first = await fixture("priority-a");
|
|
213
|
+
const second = await fixture("priority-b");
|
|
214
|
+
const calls: string[] = [];
|
|
215
|
+
for (const loaded of [first, second]) {
|
|
216
|
+
loaded.handlers = [
|
|
217
|
+
{
|
|
218
|
+
event: "post.slack.message",
|
|
219
|
+
priority: 100,
|
|
220
|
+
handler: () => {
|
|
221
|
+
calls.push(loaded.record.name);
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
await dispatchPost(
|
|
227
|
+
"post.slack.message",
|
|
228
|
+
{ channelId: "C1", userId: "U1", text: "test" },
|
|
229
|
+
{ skipExtensionId: first.record.id },
|
|
230
|
+
);
|
|
231
|
+
expect(calls).toEqual(["priority-b"]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("auto-disable skips remaining handlers in the same chain", async () => {
|
|
235
|
+
process.env.EXTENSION_MAX_CONSECUTIVE_FAILURES = "1";
|
|
236
|
+
const loaded = await fixture("throws");
|
|
237
|
+
let called = false;
|
|
238
|
+
loaded.handlers.push({
|
|
239
|
+
event: "pre.task.create",
|
|
240
|
+
priority: 200,
|
|
241
|
+
handler: () => {
|
|
242
|
+
called = true;
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
await dispatchPre("pre.task.create", taskPayload());
|
|
246
|
+
expect(called).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("refuses to dispatch inside a transaction", async () => {
|
|
250
|
+
const loaded = await fixture("priority-a");
|
|
251
|
+
const error = spyOn(console, "error").mockImplementation(() => {});
|
|
252
|
+
try {
|
|
253
|
+
const result = await getDbClient().transaction(async () =>
|
|
254
|
+
dispatchPre("pre.task.create", taskPayload()),
|
|
255
|
+
);
|
|
256
|
+
expect(result).toEqual({ action: "continue" });
|
|
257
|
+
expect(error).toHaveBeenCalledWith(
|
|
258
|
+
"[extensions] pre dispatch inside transaction:",
|
|
259
|
+
"pre.task.create",
|
|
260
|
+
);
|
|
261
|
+
expect(await listExtensionRuns(loaded.record.id)).toEqual([]);
|
|
262
|
+
} finally {
|
|
263
|
+
error.mockRestore();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("skips the requested extension id", async () => {
|
|
268
|
+
const loaded = await fixture("priority-a");
|
|
269
|
+
expect(
|
|
270
|
+
await dispatchPre("pre.task.create", taskPayload(), {
|
|
271
|
+
skipExtensionId: loaded.record.id,
|
|
272
|
+
}),
|
|
273
|
+
).toEqual({ action: "continue" });
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { afterAll, beforeAll, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
closeDb,
|
|
4
|
+
createAgent,
|
|
5
|
+
createTaskExtended,
|
|
6
|
+
getDbClient,
|
|
7
|
+
getTaskById,
|
|
8
|
+
initDb,
|
|
9
|
+
startTask,
|
|
10
|
+
} from "../be/db";
|
|
11
|
+
import { installExtension } from "../be/extensions/db";
|
|
12
|
+
import { validateBundle } from "../be/extensions/validate";
|
|
13
|
+
import { enableExtension, stopExtensionRuntime } from "../extensions/lifecycle";
|
|
14
|
+
import { codeLevelTriage } from "../heartbeat/heartbeat";
|
|
15
|
+
import { loadBundleFixture } from "./fixtures/extensions/load";
|
|
16
|
+
|
|
17
|
+
const TEST_DB_PATH = "./test-extensions-example-heartbeat-remediate.sqlite";
|
|
18
|
+
|
|
19
|
+
async function removeDbFiles(): Promise<void> {
|
|
20
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
21
|
+
await Bun.file(TEST_DB_PATH + suffix)
|
|
22
|
+
.delete()
|
|
23
|
+
.catch(() => {});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
beforeAll(async () => {
|
|
28
|
+
await removeDbFiles();
|
|
29
|
+
closeDb();
|
|
30
|
+
initDb(TEST_DB_PATH);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterAll(async () => {
|
|
34
|
+
await stopExtensionRuntime();
|
|
35
|
+
closeDb();
|
|
36
|
+
await removeDbFiles();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("never-fail-long-tasks records a stalled workflow task", async () => {
|
|
40
|
+
const bundle = await loadBundleFixture("never-fail-long-tasks");
|
|
41
|
+
const validation = await validateBundle(bundle);
|
|
42
|
+
if (!validation.ok) throw new Error(validation.diagnostics.join("\n"));
|
|
43
|
+
const installed = await installExtension(bundle);
|
|
44
|
+
await enableExtension(installed.extension.id);
|
|
45
|
+
|
|
46
|
+
const agent = await createAgent({ name: "example-worker", isLead: false, status: "busy" });
|
|
47
|
+
const task = await createTaskExtended("Example long workflow task", { agentId: agent.id });
|
|
48
|
+
await startTask(task.id);
|
|
49
|
+
await getDbClient().run("PRAGMA foreign_keys = OFF");
|
|
50
|
+
try {
|
|
51
|
+
await getDbClient().run("UPDATE agent_tasks SET workflowRunStepId = ? WHERE id = ?", [
|
|
52
|
+
crypto.randomUUID(),
|
|
53
|
+
task.id,
|
|
54
|
+
]);
|
|
55
|
+
} finally {
|
|
56
|
+
await getDbClient().run("PRAGMA foreign_keys = ON");
|
|
57
|
+
}
|
|
58
|
+
const staleAt = new Date(Date.now() - 10 * 60 * 1000).toISOString();
|
|
59
|
+
await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
|
|
60
|
+
staleAt,
|
|
61
|
+
task.id,
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const findings = await codeLevelTriage();
|
|
65
|
+
|
|
66
|
+
expect(findings.stalledTasks.map((candidate) => candidate.id)).toContain(task.id);
|
|
67
|
+
expect(findings.autoFailedTasks).toEqual([]);
|
|
68
|
+
expect((await getTaskById(task.id))?.status).toBe("in_progress");
|
|
69
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { registerExtensionInstallTool } from "../tools/extension-install";
|
|
4
|
+
import { loadBundleFixture } from "./fixtures/extensions/load";
|
|
5
|
+
|
|
6
|
+
type RegisteredTool = {
|
|
7
|
+
handler: (args: unknown, extra: unknown) => Promise<unknown>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
test("a lead can draft the minimal bundle while an operator retains activation", async () => {
|
|
11
|
+
const previousBaseUrl = process.env.MCP_BASE_URL;
|
|
12
|
+
const previousApiKey = process.env.AGENT_SWARM_API_KEY;
|
|
13
|
+
const savedFetch = globalThis.fetch;
|
|
14
|
+
const leadId = crypto.randomUUID();
|
|
15
|
+
|
|
16
|
+
process.env.MCP_BASE_URL = "http://extensions-example.test";
|
|
17
|
+
process.env.AGENT_SWARM_API_KEY = "extensions-example-key";
|
|
18
|
+
globalThis.fetch = (async (_input, init) => {
|
|
19
|
+
const body = JSON.parse(String(init?.body)) as Record<string, unknown>;
|
|
20
|
+
expect(body.enabled).toBeUndefined();
|
|
21
|
+
expect(body.activeVersion).toBeUndefined();
|
|
22
|
+
return new Response(
|
|
23
|
+
JSON.stringify({
|
|
24
|
+
extension: {
|
|
25
|
+
id: "extension-1",
|
|
26
|
+
name: "minimal",
|
|
27
|
+
version: 1,
|
|
28
|
+
activeVersion: 1,
|
|
29
|
+
enabled: false,
|
|
30
|
+
status: "disabled",
|
|
31
|
+
priority: 100,
|
|
32
|
+
consecutiveFailures: 0,
|
|
33
|
+
},
|
|
34
|
+
contentDeduped: false,
|
|
35
|
+
}),
|
|
36
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
37
|
+
);
|
|
38
|
+
}) as typeof globalThis.fetch;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const server = new McpServer({ name: "extensions-example", version: "1" });
|
|
42
|
+
registerExtensionInstallTool(server);
|
|
43
|
+
const tool = (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
|
|
44
|
+
._registeredTools["extension-install"]!;
|
|
45
|
+
const result = (await tool.handler(await loadBundleFixture("minimal"), {
|
|
46
|
+
sessionId: "extensions-example",
|
|
47
|
+
requestInfo: { headers: { "x-agent-id": leadId } },
|
|
48
|
+
})) as {
|
|
49
|
+
isError?: boolean;
|
|
50
|
+
structuredContent: Record<string, unknown>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
expect(result.isError).toBe(false);
|
|
54
|
+
expect(result.structuredContent).toMatchObject({ enabled: false, status: "disabled" });
|
|
55
|
+
expect(String(result.structuredContent.message)).toContain("operator must enable it");
|
|
56
|
+
} finally {
|
|
57
|
+
globalThis.fetch = savedFetch;
|
|
58
|
+
if (previousBaseUrl === undefined) delete process.env.MCP_BASE_URL;
|
|
59
|
+
else process.env.MCP_BASE_URL = previousBaseUrl;
|
|
60
|
+
if (previousApiKey === undefined) delete process.env.AGENT_SWARM_API_KEY;
|
|
61
|
+
else process.env.AGENT_SWARM_API_KEY = previousApiKey;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { closeDb, createAgent, createTaskExtended, getTaskById, initDb, startTask } from "../be/db";
|
|
5
|
+
import { installExtension } from "../be/extensions/db";
|
|
6
|
+
import { enableExtension, stopExtensionRuntime } from "../extensions/lifecycle";
|
|
7
|
+
import { registerStoreProgressTool } from "../tools/store-progress";
|
|
8
|
+
import { loadBundleFixture } from "./fixtures/extensions/load";
|
|
9
|
+
|
|
10
|
+
const TEST_DB_PATH = "./test-extensions-example-no-exclamation-marks.sqlite";
|
|
11
|
+
|
|
12
|
+
type RegisteredTool = {
|
|
13
|
+
handler: (args: unknown, extra: unknown) => Promise<CallToolResult>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
async function removeDbFiles(): Promise<void> {
|
|
17
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
18
|
+
await Bun.file(TEST_DB_PATH + suffix)
|
|
19
|
+
.delete()
|
|
20
|
+
.catch(() => {});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("no-exclamation-marks extension example", () => {
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
await removeDbFiles();
|
|
27
|
+
initDb(TEST_DB_PATH);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterAll(async () => {
|
|
31
|
+
await stopExtensionRuntime();
|
|
32
|
+
closeDb();
|
|
33
|
+
await removeDbFiles();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("rejects emphatic progress and accepts compliant progress", async () => {
|
|
37
|
+
const installed = await installExtension(await loadBundleFixture("no-exclamation-marks"));
|
|
38
|
+
await enableExtension(installed.extension.id);
|
|
39
|
+
const agent = await createAgent({
|
|
40
|
+
name: "no-exclamation-example-worker",
|
|
41
|
+
isLead: false,
|
|
42
|
+
status: "busy",
|
|
43
|
+
capabilities: [],
|
|
44
|
+
});
|
|
45
|
+
const task = await createTaskExtended("no exclamation example", {
|
|
46
|
+
agentId: agent.id,
|
|
47
|
+
source: "mcp",
|
|
48
|
+
});
|
|
49
|
+
await startTask(task.id);
|
|
50
|
+
|
|
51
|
+
const server = new McpServer({ name: "no-exclamation-example", version: "1.0.0" });
|
|
52
|
+
registerStoreProgressTool(server);
|
|
53
|
+
const tools = (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
|
|
54
|
+
._registeredTools;
|
|
55
|
+
const tool = tools["store-progress"];
|
|
56
|
+
if (!tool) throw new Error("store-progress was not registered");
|
|
57
|
+
const meta = {
|
|
58
|
+
sessionId: crypto.randomUUID(),
|
|
59
|
+
requestInfo: { headers: { "x-agent-id": agent.id } },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const blocked = await tool.handler({ taskId: task.id, progress: "done!" }, meta);
|
|
63
|
+
expect(blocked.isError).toBe(true);
|
|
64
|
+
expect(JSON.stringify(blocked)).toContain("Progress text must not contain exclamation marks.");
|
|
65
|
+
expect(JSON.stringify(blocked)).toContain("no-exclamation-marks");
|
|
66
|
+
expect((await getTaskById(task.id))?.progress).toBeUndefined();
|
|
67
|
+
|
|
68
|
+
const accepted = await tool.handler({ taskId: task.id, progress: "done." }, meta);
|
|
69
|
+
expect(accepted.isError).toBe(false);
|
|
70
|
+
expect((await getTaskById(task.id))?.progress).toBe("done.");
|
|
71
|
+
});
|
|
72
|
+
});
|