@desplega.ai/agent-swarm 1.172.0 → 2.0.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 +1 -1
- package/dist/{acp-adapter-fpecx1g1.js → acp-adapter-sj2g0m84.js} +18 -17
- package/dist/{actions-pw87a1a8.js → actions-fegk44gr.js} +65 -35
- package/dist/{amp-adapter-8jem2bc0.js → amp-adapter-ad8ft89x.js} +8 -7
- package/dist/{anthropic-messages-8ee2f22q.js → anthropic-messages-r6czkjv1.js} +7 -7
- package/dist/{app-wmf0m843.js → app-kfvgbvzy.js} +19 -13
- package/dist/{assistant-emp1e108.js → assistant-z7zdy9ja.js} +65 -38
- package/dist/{boot-reembed-1de4f4aj.js → boot-reembed-5c2z0mbe.js} +11 -9
- package/dist/{boot-reembed-4ymn9atn.js → boot-reembed-j8xa181e.js} +11 -9
- package/dist/{boot-scrub-sweep-adhk1s8c.js → boot-scrub-sweep-hnkn3rgt.js} +12 -10
- package/dist/{boot-seeds-8q637aew.js → boot-seeds-kwjq4d2z.js} +15 -13
- package/dist/{claude-adapter-t26cv8a4.js → claude-adapter-9vmvpvgd.js} +20 -19
- package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-k9qae05j.js} +7 -5
- package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-q54zx5q2.js} +6 -4
- package/dist/{claude-sdk-session-gsfsh662.js → claude-sdk-session-9jye6cxr.js} +12698 -11995
- package/dist/{cli-k46r9hmk.js → cli-03gedhng.js} +16 -14
- package/dist/{cli-kb4ehzx5.js → cli-1e4b6bzh.js} +7 -7
- package/dist/{cli-d603qf86.js → cli-29jc5q7w.js} +10 -8
- package/dist/cli-2m1ktfzz.js +17795 -0
- package/dist/{cli-8vs9trd4.js → cli-3dj8k4vy.js} +1 -1
- package/dist/{cli-zxm9p3x4.js → cli-3fbmmawm.js} +5 -5
- package/dist/{cli-gmdajfw5.js → cli-4jxm2qtz.js} +4987 -1708
- package/dist/{cli-tadfgd0t.js → cli-5hkm7whv.js} +33 -4
- package/dist/{cli-6jb2rf77.js → cli-5t4f29ez.js} +3 -3
- package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
- package/dist/cli-6cvfxs8r.js +47 -0
- package/dist/{cli-pj6x9qce.js → cli-6jyj59nb.js} +2 -2
- package/dist/{cli-388egyf6.js → cli-6razj6t4.js} +2 -2
- package/dist/{cli-qsztpfry.js → cli-74ahz61x.js} +2 -2
- package/dist/{cli-4nz8p1p5.js → cli-7x0g4sn1.js} +15474 -16082
- package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
- package/dist/{cli-81r1wjyv.js → cli-9nfx44eh.js} +1 -1
- package/dist/cli-9traj3dn.js +638 -0
- package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
- package/dist/{cli-bf7485pm.js → cli-apx87jne.js} +2 -2
- package/dist/{cli-ztrv9y21.js → cli-bzszjf2p.js} +3689 -1081
- package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
- package/dist/{cli-hsem16hb.js → cli-cgtksawx.js} +7 -4
- package/dist/{cli-db3m6zdt.js → cli-d2qr7aj4.js} +1 -1
- package/dist/{cli-1hgat6pa.js → cli-d5f9ed5c.js} +5 -5
- package/dist/cli-e3g5mbkz.js +776 -0
- package/dist/{cli-7jykv7ef.js → cli-e6mpjhed.js} +1 -1
- package/dist/{cli-j6kpj9az.js → cli-ee9gtz70.js} +1553 -68
- package/dist/{cli-e0a9sk00.js → cli-eejsgvm4.js} +4 -4
- package/dist/{cli-tgpfnm6r.js → cli-ef1hg1wv.js} +518 -59
- package/dist/cli-f527ce74.js +781 -0
- package/dist/{cli-ppy4nnnh.js → cli-f7kr7svz.js} +2 -2
- package/dist/cli-hjwmhcg1.js +10622 -0
- package/dist/{cli-2bw3w47j.js → cli-hnhtk6cj.js} +1 -1
- package/dist/{cli-ywm1azg9.js → cli-hnmhtzvh.js} +6 -4
- package/dist/{cli-4fnb6884.js → cli-hvqbtna4.js} +7 -1259
- package/dist/{cli-pstq9888.js → cli-j0pfptdp.js} +220 -103
- package/dist/cli-jta2eyzc.js +619 -0
- package/dist/{cli-esnpehf5.js → cli-k382tshb.js} +12 -13
- package/dist/cli-k3dp5tpf.js +263 -0
- package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
- package/dist/cli-kceah1eh.js +92 -0
- package/dist/{cli-prrt9fa0.js → cli-ke896fcf.js} +10 -10
- package/dist/{cli-nv4zd0b9.js → cli-kt6zqg0e.js} +6 -6
- package/dist/{cli-g6t9texm.js → cli-m50t4309.js} +5 -18
- package/dist/cli-n2wq4j7m.js +6985 -0
- package/dist/{cli-c2pq2rjb.js → cli-n3hzg89b.js} +46 -15
- package/dist/{cli-tmhsxhb8.js → cli-n5vwg842.js} +23 -6
- package/dist/{cli-5rv1j9ga.js → cli-ne236afy.js} +53 -7
- package/dist/cli-p0sg24zp.js +16 -0
- package/dist/{cli-d161g69q.js → cli-p1x85d2t.js} +50 -2
- package/dist/{cli-7pz3b7x5.js → cli-przpmdgr.js} +25 -0
- package/dist/{cli-g4f5pj26.js → cli-q0ke1yww.js} +111 -25
- package/dist/{cli-e3pmyt39.js → cli-q50942rs.js} +1 -1
- package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
- package/dist/{cli-gj81e47t.js → cli-r2pnvj8g.js} +2 -2
- package/dist/{cli-r8smj8b8.js → cli-r62a35ea.js} +19 -3
- package/dist/{cli-cd0hv9nh.js → cli-ra0xzggj.js} +2 -2
- package/dist/cli-rhs9pz33.js +294 -0
- package/dist/{cli-nyhb1r6x.js → cli-s2sp957d.js} +52 -29
- package/dist/{cli-1x7da0de.js → cli-s4setq91.js} +8 -8
- package/dist/{cli-xczpdp9e.js → cli-se8w6wny.js} +6 -6
- package/dist/cli-skxvvpap.js +10140 -0
- package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
- package/dist/{cli-0p3enk35.js → cli-tqaazemc.js} +9 -1
- package/dist/{cli-cf69s3ew.js → cli-tr4sxp68.js} +3 -3
- package/dist/{cli-8hzfb6xz.js → cli-tsde1hdr.js} +15 -15
- package/dist/{cli-cceskcvt.js → cli-txqb9s38.js} +8 -8
- package/dist/{cli-aspcaef3.js → cli-v1yb0rqb.js} +6 -6
- package/dist/{cli-j1pmc07z.js → cli-vatyhshy.js} +268 -40
- package/dist/{cli-04emfe2q.js → cli-vdach5a9.js} +5 -5
- package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
- package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
- package/dist/{cli-hwwvy36w.js → cli-wayw93nt.js} +3 -3
- package/dist/cli-wmgrkscj.js +23254 -0
- package/dist/{cli-gp1rtyvk.js → cli-z5jjpaxw.js} +29 -6
- package/dist/{cli-451ks6aw.js → cli-zkpks3r3.js} +1 -1
- package/dist/cli.js +32 -48
- package/dist/{codex-adapter-m4tgdysy.js → codex-adapter-y81sssk0.js} +16 -15
- package/dist/{codex-login-841kv5fk.js → codex-login-f2rcfpp9.js} +3 -2
- package/dist/{codex-session-runner-ymwzm6ea.js → codex-session-runner-8vx4j6a9.js} +16 -15
- package/dist/{commands-n0hshcvs.js → commands-kcpvpcrr.js} +21 -10
- package/dist/{controller-hq5ggpcp.js → controller-ywrmt6yd.js} +5 -4
- package/dist/{controllers-connect-y07cqcvy.js → controllers-connect-kwpvj3rf.js} +45 -38
- package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-cehv1qns.js} +7 -6
- package/dist/{db-hkqvrdcb.js → db-k8ankj20.js} +13 -11
- package/dist/{db-4w7tmt1g.js → db-ww5d7jq3.js} +29 -9
- package/dist/{devin-adapter-xgtj680j.js → devin-adapter-x1022qb1.js} +3 -2
- package/dist/{dispatcher-5sd69y14.js → dispatcher-e8gq5m68.js} +41 -34
- package/dist/{docker-f503k9p0.js → docker-ff6y1190.js} +3 -2
- package/dist/{dsh-adapter-2h8m4cab.js → dsh-adapter-3epjm3af.js} +7 -6
- package/dist/{e2b-66xra4az.js → e2b-8ygwhg0f.js} +4 -3
- package/dist/{e2b-xwhyj8cw.js → e2b-swhnpw3n.js} +4 -3
- package/dist/{event-bus-vjtv82gm.js → event-bus-6axnngdj.js} +3 -2
- package/dist/external-channel-h3359cwn.js +52 -0
- package/dist/{google-generative-ai-7pavrbv7.js → google-generative-ai-g8db7x0f.js} +5 -3
- package/dist/{google-vertex-3f1jk41f.js → google-vertex-r2692q0r.js} +5 -3
- package/dist/{grok-adapter-8m9yqzbq.js → grok-adapter-0d4x7gkx.js} +19 -18
- package/dist/{handlers-s20xr10r.js → handlers-an1gzv21.js} +44 -37
- package/dist/{heartbeat-3k31q4g2.js → heartbeat-6dakeb61.js} +46 -39
- package/dist/{hook-gdn6a2fn.js → hook-6v6cv6d5.js} +12 -24
- package/dist/{hook-ygwerrdh.js → hook-sa5xye4y.js} +13 -12
- package/dist/{http-jdteeg81.js → http-r7f3eky0.js} +1598 -279
- package/dist/index-52j1aja4.js +3465 -0
- package/dist/{index-e0snym4t.js → index-7ep4gzdk.js} +41 -34
- package/dist/index-9gw58n4k.js +193 -0
- package/dist/index-9j87mcm8.js +56305 -0
- package/dist/index-bsmcresk.js +79424 -0
- package/dist/index-mh85qhec.js +475 -0
- package/dist/{index-vm3bm9t7.js → index-rmpw9a8q.js} +41 -34
- package/dist/{index-6ksewd5y.js → index-s2wxgzr0.js} +1015 -732
- package/dist/{index-tsszt7g9.js → index-v1v6n7hg.js} +46 -39
- package/dist/index-we27kd31.js +12810 -0
- package/dist/{index-ffqzqgwb.js → index-zk96aq1k.js} +11 -9
- package/dist/{keepalive-jekc896t.js → keepalive-be9vwy5h.js} +13 -11
- package/dist/{lead-4k9s9szx.js → lead-0861cwrf.js} +64 -57
- package/dist/{maintenance-shazch4n.js → maintenance-mzzgc8bn.js} +11 -9
- package/dist/node-c9we95xf.js +1 -1
- package/dist/{node-fkajaq96.js → node-mn1zs7an.js} +2 -1
- package/dist/{oauth-refresh-sweep-5gb36kn8.js → oauth-refresh-sweep-cpdnfwdj.js} +12 -10
- package/dist/{onboard-ydrbnvww.js → onboard-fh8qs4r3.js} +11 -3
- package/dist/{opencode-adapter-5zh0he20.js → opencode-adapter-nm42wwj8.js} +19 -14
- package/dist/{otel-impl-6pf5cv86.js → otel-impl-x6c1c9g4.js} +248 -869
- package/dist/{pi-mono-adapter-ef9wwx9m.js → pi-mono-adapter-9n2ya240.js} +245 -107
- package/dist/{pricing-refresh-s52nsa5q.js → pricing-refresh-qb0rv0ap.js} +13 -11
- package/dist/{rbac-roles-aq3msh4e.js → rbac-roles-1gzdwbh1.js} +12 -10
- package/dist/{rbac-roles-q5gb5e8v.js → rbac-roles-3mraey52.js} +13 -11
- package/dist/{render-v2-pd1rg54q.js → render-v2-agpa45zz.js} +19 -13
- package/dist/{resume-gw4a2c6v.js → resume-x0ydy9s9.js} +27 -21
- package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
- package/dist/{setup-jda1ksdh.js → setup-as9ga0d6.js} +2 -2
- package/dist/{virtual-modules-b3ga2m65.js → virtual-modules-9gkxqrap.js} +7 -6
- package/dist/{work-objects-tb7ycye8.js → work-objects-y5g1y7ev.js} +49 -37
- package/dist/{worker-njjccz5d.js → worker-860c3e38.js} +64 -57
- package/dist/{x-qfmygqqv.js → x-m477fr0h.js} +3 -2
- package/openapi.json +951 -356
- package/package.json +12 -13
- package/src/apps/catalog.generated.json +1309 -53
- package/src/apps/page-validator.ts +93 -31
- package/src/azure-devops/api.ts +94 -18
- package/src/azure-devops/auth.ts +32 -0
- package/src/azure-devops/boards-sync.ts +708 -0
- package/src/azure-devops/handlers.ts +52 -8
- package/src/azure-devops/html.ts +26 -0
- package/src/azure-devops/index.ts +15 -0
- package/src/azure-devops/templates.ts +86 -0
- package/src/azure-devops/types.ts +65 -1
- package/src/be/config-refs.ts +60 -0
- package/src/be/db/page-versions.ts +15 -0
- package/src/be/db/runtime.ts +35 -8
- package/src/be/db/skill-name-resolution.ts +36 -0
- package/src/be/db/tasks/read.ts +230 -0
- package/src/be/db/tasks/write.ts +7 -0
- package/src/be/db.ts +74 -13
- package/src/be/migrations/213_script_connections_sql.sql +79 -0
- package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
- package/src/be/migrations/216_task_resolved_effort.sql +5 -0
- package/src/be/migrations/217_chat_sdk_state.sql +104 -0
- package/src/be/model-catalog-store.ts +5 -1
- package/src/be/model-tier-resolution.ts +12 -1
- package/src/be/model-validation.ts +5 -2
- package/src/be/models-catalog.ts +26 -1
- package/src/be/modelsdev-cache.json +198 -63
- package/src/be/modelsdev-cache.ts +9 -1
- package/src/be/script-connections.ts +47 -8
- package/src/be/script-credential-broker.ts +39 -2
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +10 -8
- package/src/be/seed/runner.ts +13 -1
- package/src/be/seed/state-db.ts +5 -0
- package/src/be/seed/types.ts +6 -0
- package/src/be/seed-pricing.ts +19 -2
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/seed-skills/bundled-files.generated.json +10 -18
- package/src/be/seed-skills/index.ts +107 -3
- package/src/be/sql-connections.ts +118 -0
- package/src/be/swarm-config-guard.ts +53 -0
- package/src/cli.tsx +0 -16
- package/src/commands/onboard/non-interactive.ts +4 -2
- package/src/commands/profile-sync.ts +5 -3
- package/src/commands/provider-credentials.ts +52 -6
- package/src/commands/runner.ts +196 -56
- package/src/e2b/env.ts +5 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +45 -0
- package/src/extensions/dispatcher.ts +7 -0
- package/src/extensions/post-bridge.ts +40 -0
- package/src/github/handlers.ts +11 -4
- package/src/heartbeat/templates.ts +8 -5
- package/src/hooks/hook.ts +0 -26
- package/src/http/all-routes.ts +0 -1
- package/src/http/config.ts +36 -17
- package/src/http/core.ts +4 -0
- package/src/http/index.ts +14 -2
- package/src/http/integrations.ts +88 -1
- package/src/http/mcp-oauth.ts +6 -13
- package/src/http/mcp.ts +15 -5
- package/src/http/page-proxy.ts +2 -4
- package/src/http/pages-public.ts +6 -5
- package/src/http/pages.ts +5 -14
- package/src/http/poll.ts +28 -68
- package/src/http/requested-by.ts +54 -0
- package/src/http/route-def.ts +7 -6
- package/src/http/script-connection-proxy.ts +51 -4
- package/src/http/script-connections.ts +5 -2
- package/src/http/scripts.ts +1 -0
- package/src/http/session-cost-recompute.ts +12 -2
- package/src/http/session-data.ts +4 -0
- package/src/http/stats.ts +1 -30
- package/src/http/tasks.ts +61 -5
- package/src/http/utils.ts +14 -0
- package/src/http/webhooks.ts +183 -2
- package/src/http/x.ts +1 -0
- package/src/integrations/chat-sdk/adapters.ts +129 -0
- package/src/integrations/chat-sdk/attachments.ts +232 -0
- package/src/integrations/chat-sdk/background.ts +36 -0
- package/src/integrations/chat-sdk/commands.ts +194 -0
- package/src/integrations/chat-sdk/config.ts +198 -0
- package/src/integrations/chat-sdk/discord-api.ts +63 -0
- package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
- package/src/integrations/chat-sdk/handlers.ts +28 -0
- package/src/integrations/chat-sdk/inbound.ts +426 -0
- package/src/integrations/chat-sdk/limits.ts +48 -0
- package/src/integrations/chat-sdk/logger.ts +81 -0
- package/src/integrations/chat-sdk/outbound.ts +123 -0
- package/src/integrations/chat-sdk/relay.ts +172 -0
- package/src/integrations/chat-sdk/router.ts +169 -0
- package/src/integrations/chat-sdk/runtime.ts +351 -0
- package/src/integrations/chat-sdk/schema.ts +245 -0
- package/src/integrations/chat-sdk/state.ts +243 -0
- package/src/integrations/chat-sdk/status.ts +244 -0
- package/src/integrations/chat-sdk/steering.ts +58 -0
- package/src/integrations/chat-sdk/teams-origin.ts +77 -0
- package/src/integrations/chat-sdk/telegram-api.ts +56 -0
- package/src/integrations/chat-sdk/templates.ts +16 -0
- package/src/integrations/chat-sdk/test-hooks.ts +37 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
- package/src/integrations/chat-sdk/typing.ts +65 -0
- package/src/jira/adf.ts +3 -1
- package/src/kv-reserved-namespaces.ts +2 -0
- package/src/oauth/mcp-wrapper.ts +10 -6
- package/src/otel-impl.ts +2 -1
- package/src/pages/version.ts +4 -5
- package/src/prompts/base-prompt.ts +111 -45
- package/src/providers/acp-target-catalog.ts +6 -0
- package/src/providers/acp-targets.ts +56 -3
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +27 -2
- package/src/providers/claude-flag-settings.ts +133 -0
- package/src/providers/claude-managed-adapter.ts +1 -1
- package/src/providers/claude-sdk-session.ts +7 -0
- package/src/providers/claude-session-events.ts +6 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/opencode-adapter.ts +268 -37
- package/src/providers/opencode-billing.ts +86 -0
- package/src/providers/opencode-swarm-plugin.ts +51 -0
- package/src/providers/pi-mono-adapter.ts +398 -153
- package/src/providers/pricing-sources.md +37 -3
- package/src/providers/reasoning-effort.ts +27 -0
- package/src/providers/types.ts +11 -2
- package/src/rbac/legacy-policy.ts +8 -2
- package/src/rbac/permissions.ts +21 -0
- package/src/scheduler/scheduler.ts +4 -2
- package/src/scripts-runtime/api-client.ts +15 -1
- package/src/scripts-runtime/api-types.ts +9 -1
- package/src/scripts-runtime/ctx.ts +1 -1
- package/src/scripts-runtime/loader.ts +19 -7
- package/src/scripts-runtime/mcp-client.ts +30 -19
- package/src/scripts-runtime/sdk-allowlist.ts +8 -6
- package/src/scripts-runtime/swarm-sdk.ts +0 -11
- package/src/scripts-runtime/types/stdlib.d.ts +27 -15
- package/src/scripts-runtime/types/swarm-extension.d.ts +63 -1
- package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -15
- package/src/server.ts +52 -18
- package/src/slack/actions.ts +29 -1
- package/src/slack/app.ts +14 -2
- package/src/slack/assistant.ts +36 -2
- package/src/slack/channel-join.ts +3 -16
- package/src/slack/commands.ts +9 -1
- package/src/slack/event-dedup.ts +1 -1
- package/src/slack/external-channel-egress.ts +113 -0
- package/src/slack/external-channel.ts +309 -0
- package/src/slack/handlers.ts +61 -0
- package/src/slack/render-v2.ts +40 -47
- package/src/slack/work-objects.ts +5 -0
- package/src/tasks/additive-buffer.ts +4 -2
- package/src/tasks/context-key.ts +94 -2
- package/src/tasks/model-pin.ts +67 -0
- package/src/tasks/worker-follow-up.ts +4 -31
- package/src/telemetry.ts +2 -1
- package/src/tests/acp-adapter.test.ts +42 -0
- package/src/tests/api-drain-worker-e2e.test.ts +1 -6
- package/src/tests/apps-catalog-feedback.test.ts +310 -0
- package/src/tests/apps-catalog-inputs.test.ts +70 -0
- package/src/tests/apps-catalog-table.test.ts +113 -0
- package/src/tests/azure-devops-boards-sync.test.ts +584 -0
- package/src/tests/azure-devops-html.test.ts +13 -0
- package/src/tests/azure-devops-webhook-malformed.test.ts +4 -0
- package/src/tests/base-prompt.test.ts +58 -54
- package/src/tests/chat-sdk-commands.test.ts +191 -0
- package/src/tests/chat-sdk-config.test.ts +334 -0
- package/src/tests/chat-sdk-discord-api.test.ts +68 -0
- package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
- package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
- package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
- package/src/tests/chat-sdk-harness.ts +146 -0
- package/src/tests/chat-sdk-inbound.test.ts +363 -0
- package/src/tests/chat-sdk-relay.test.ts +470 -0
- package/src/tests/chat-sdk-reload.test.ts +128 -0
- package/src/tests/chat-sdk-shutdown.test.ts +220 -0
- package/src/tests/chat-sdk-slack.test.ts +268 -0
- package/src/tests/chat-sdk-state.test.ts +163 -0
- package/src/tests/chat-sdk-status-route.test.ts +382 -0
- package/src/tests/chat-sdk-teams.test.ts +277 -0
- package/src/tests/chat-sdk-tools.test.ts +520 -0
- package/src/tests/chat-sdk-typing.test.ts +120 -0
- package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
- package/src/tests/claude-adapter-binary.test.ts +144 -0
- package/src/tests/claude-flag-settings.test.ts +114 -0
- package/src/tests/claude-managed-adapter.test.ts +1 -1
- package/src/tests/claude-sdk-lifecycle.test.ts +148 -1
- package/src/tests/claude-sdk-process.fixture.ts +7 -0
- package/src/tests/claude-thinking-tokens.test.ts +57 -0
- package/src/tests/claude-worker-parity.test.ts +2 -7
- package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
- package/src/tests/config-refs.test.ts +66 -0
- package/src/tests/context-key.test.ts +51 -0
- package/src/tests/cors-allowlist.test.ts +25 -0
- package/src/tests/create-page-tool.test.ts +18 -0
- package/src/tests/credential-check.test.ts +139 -0
- package/src/tests/credentials.test.ts +21 -0
- package/src/tests/db-runtime.test.ts +61 -0
- package/src/tests/db-task-read-contract.test.ts +20 -0
- package/src/tests/db-task-write-contract.test.ts +10 -0
- package/src/tests/deferred-task-wake.test.ts +52 -4
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/enabled-capabilities.test.ts +20 -0
- package/src/tests/entrypoint-api-readiness.test.ts +14 -8
- package/src/tests/entrypoint-config-env-export.test.ts +5 -2
- package/src/tests/entrypoint-export-to-env.test.ts +78 -0
- package/src/tests/explicit-model-validation.test.ts +105 -1
- package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
- package/src/tests/extensions-post-bridge.test.ts +51 -0
- package/src/tests/fetch-resolved-env.test.ts +128 -0
- package/src/tests/fixtures/azure-boards/workitem.commented.json +89 -0
- package/src/tests/fixtures/azure-boards/workitem.created.json +81 -0
- package/src/tests/fixtures/azure-boards/workitem.deleted.json +70 -0
- package/src/tests/fixtures/azure-boards/workitem.restored.json +82 -0
- package/src/tests/fixtures/azure-boards/workitem.updated.json +130 -0
- package/src/tests/fixtures/catalog-feedback-definition.json.txt +860 -0
- package/src/tests/fixtures/catalog-inputs-definition.json.txt +910 -0
- package/src/tests/fixtures/catalog-table-definition.json.txt +468 -0
- package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
- package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
- package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
- package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
- package/src/tests/fixtures/runner-identity-refresh.ts +3 -0
- package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
- package/src/tests/github-handlers.test.ts +35 -0
- package/src/tests/harness-provider-resolution.test.ts +12 -0
- package/src/tests/heartbeat-checklist.test.ts +3 -2
- package/src/tests/hook-runtime-close.test.ts +0 -2
- package/src/tests/http-api-integration.test.ts +0 -25
- package/src/tests/migration-046-budgets.test.ts +17 -16
- package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
- package/src/tests/model-tier-resolution.test.ts +93 -0
- package/src/tests/models-catalog.test.ts +42 -1
- package/src/tests/node-env-runtime.test.ts +98 -0
- package/src/tests/openai-compatible-providers.test.ts +134 -0
- package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
- package/src/tests/opencode-adapter.test.ts +348 -3
- package/src/tests/opencode-billing.test.ts +28 -0
- package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
- package/src/tests/opencode-shell-env.test.ts +145 -0
- package/src/tests/openrouter-base-url.test.ts +101 -151
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/pages-browser-sdk.test.ts +76 -0
- package/src/tests/pages-feedback.test.ts +1 -1
- package/src/tests/paused-tasks-requested-by.test.ts +102 -0
- package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
- package/src/tests/pi-mono-adapter.test.ts +385 -5
- package/src/tests/pi-shell-env.test.ts +170 -0
- package/src/tests/prompt-template-remaining.test.ts +35 -0
- package/src/tests/rbac-engine.test.ts +11 -3
- package/src/tests/runner-exit-after-tasks.test.ts +0 -4
- package/src/tests/runner-identity-refresh.test.ts +3 -0
- package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
- package/src/tests/script-connections-sql.test.ts +675 -0
- package/src/tests/script-connections.test.ts +7 -1
- package/src/tests/scripts-http.test.ts +201 -1
- package/src/tests/scripts-only-gating.test.ts +2 -3
- package/src/tests/scripts-runtime.test.ts +58 -40
- package/src/tests/secret-scrubber.test.ts +86 -0
- package/src/tests/seed-skills-bundled-files.test.ts +45 -53
- package/src/tests/seed-skills-retire.test.ts +182 -0
- package/src/tests/send-task-requester-gate.test.ts +58 -0
- package/src/tests/session-cost-recompute-unit.test.ts +94 -0
- package/src/tests/skill-get-name-resolution.test.ts +137 -0
- package/src/tests/slack-channel-join.test.ts +4 -1
- package/src/tests/slack-external-channel-config.test.ts +65 -0
- package/src/tests/slack-external-channel-egress.test.ts +237 -0
- package/src/tests/slack-external-channel-guard.test.ts +280 -0
- package/src/tests/slack-external-channel-tools.test.ts +173 -0
- package/src/tests/slack-external-channel.test.ts +370 -0
- package/src/tests/slack-render-v2.test.ts +86 -10
- package/src/tests/sonnet-5-5-registry.test.ts +9 -1
- package/src/tests/spec-base-url.test.ts +16 -1
- package/src/tests/swarm-config-export-to-env.test.ts +267 -0
- package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
- package/src/tests/swarm-diff.test.ts +1 -1
- package/src/tests/task-model-effort-rerun.test.ts +164 -0
- package/src/tests/task-session-prompts.test.ts +132 -0
- package/src/tests/task-tool-manifest.test.ts +14 -0
- package/src/tests/task-tools-ctx.test.ts +2 -0
- package/src/tests/tool-annotations.test.ts +6 -6
- package/src/tests/ui-logs-parser.test.ts +43 -0
- package/src/tests/used-by-route.test.ts +6 -0
- package/src/tests/vendored-openapi.test.ts +16 -1
- package/src/tests/vercel-ai-gateway.test.ts +149 -0
- package/src/tests/workflow-swarm-script.test.ts +38 -0
- package/src/tests/workflow-wait-filter.test.ts +5 -3
- package/src/tools/chat-access.ts +41 -0
- package/src/tools/chat-open-dm.ts +59 -0
- package/src/tools/chat-post.ts +74 -0
- package/src/tools/chat-read-thread.ts +100 -0
- package/src/tools/chat-reply.ts +75 -0
- package/src/tools/create-page.ts +3 -13
- package/src/tools/defer-task.ts +17 -0
- package/src/tools/model-catalog/index.ts +2 -1
- package/src/tools/register-chat-channel.ts +139 -0
- package/src/tools/send-task.ts +24 -0
- package/src/tools/skills/skill-get.ts +17 -9
- package/src/tools/slack-post.ts +6 -0
- package/src/tools/slack-read.ts +8 -0
- package/src/tools/slack-reply.ts +6 -0
- package/src/tools/slack-start-thread.ts +6 -0
- package/src/tools/slack-update.ts +6 -0
- package/src/tools/slack-upload-file.ts +6 -0
- package/src/tools/swarm-config/get-config.ts +2 -1
- package/src/tools/swarm-config/list-config.ts +2 -1
- package/src/tools/swarm-config/set-config.ts +16 -3
- package/src/tools/tool-config.ts +8 -6
- package/src/types.ts +12 -2
- package/src/utils/config-env-export.ts +74 -0
- package/src/utils/credentials.test.ts +92 -0
- package/src/utils/credentials.ts +52 -4
- package/src/utils/harness-provider.ts +15 -6
- package/src/utils/node-env.ts +24 -0
- package/src/utils/runtime-model-catalog.ts +11 -1
- package/src/utils/secret-scrubber.ts +71 -2
- package/src/utils/task-tool-manifest.ts +19 -2
- package/src/workflows/executors/swarm-script.ts +1 -0
- package/templates/skills/agent-browser/SKILL.md +1 -1
- package/templates/skills/agent-browser/content.md +1 -1
- package/templates/skills/apps/SKILL.md +749 -0
- package/templates/skills/apps/content.md +60 -22
- package/templates/skills/chat-sdk/config.json +14 -0
- package/templates/skills/chat-sdk/content.md +84 -0
- package/templates/skills/composio-gmail/content.md +7 -0
- package/templates/skills/composio-google-docs/content.md +1 -1
- package/templates/skills/email-domain-health/config.json +14 -0
- package/templates/skills/email-domain-health/content.md +131 -0
- package/templates/skills/kv-storage/content.md +2 -2
- package/templates/skills/pages/content.md +8 -10
- package/templates/skills/qa-loom/config.json +14 -0
- package/templates/skills/qa-loom/content.md +187 -0
- package/templates/skills/qa-loom/files/build-loom.mjs +221 -0
- package/templates/skills/qa-loom/files/run-qa.sh +57 -0
- package/templates/skills/slack-interaction/content.md +4 -0
- package/templates/skills/swarm-extensions/content.md +2 -0
- package/templates/skills/swarm-scripts/SKILL.md +4 -2
- package/templates/skills/swarm-scripts/config.json +1 -1
- package/templates/skills/swarm-scripts/content.md +4 -2
- package/templates/skills/workflow-iterate/content.md +38 -0
- package/dist/artifact-eczsknc3.js +0 -3843
- package/dist/cli-9xsmdpdt.js +0 -56377
- package/dist/cli-bd957dgb.js +0 -153
- package/dist/cli-q51yprfq.js +0 -1104
- package/dist/cli-t1ze64h3.js +0 -811
- package/dist/index-9ch1n761.js +0 -1102
- package/src/artifact-sdk/index.ts +0 -2
- package/src/artifact-sdk/localtunnel.d.ts +0 -20
- package/src/artifact-sdk/port.ts +0 -12
- package/src/artifact-sdk/server.ts +0 -183
- package/src/artifact-sdk/tunnel.ts +0 -19
- package/src/commands/artifact.ts +0 -249
- package/src/http/ecosystem.ts +0 -77
- package/src/tests/artifact-commands.test.ts +0 -92
- package/src/tests/artifact-sdk.test.ts +0 -793
- package/src/tests/register-service-security.test.ts +0 -194
- package/src/tools/list-services.ts +0 -107
- package/src/tools/register-service.ts +0 -177
- package/src/tools/unregister-service.ts +0 -73
- package/src/tools/update-service-status.ts +0 -96
- package/templates/skills/artifacts/config.json +0 -14
- package/templates/skills/artifacts/content.md +0 -344
- package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
- package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
- package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
- package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
- package/dist/{azure-openai-responses-ap1gwxgr.js → azure-openai-responses-ygfreq4t.js} +5 -5
- package/dist/{cli-5rqrj7q0.js → cli-pw7ebjdt.js} +0 -0
- package/dist/{openai-codex-responses-33qhkkdy.js → openai-codex-responses-3bk0grnr.js} +4 -4
- package/dist/{openai-completions-s88xm3ns.js → openai-completions-5d4jpakc.js} +9 -9
- package/dist/{openai-responses-7xd2f2e2.js → openai-responses-krb51f59.js} +5 -5
- package/dist/{pi-messages-xszk613n.js → pi-messages-x9gzynm1.js} +3 -3
- /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
- /package/src/{artifact-sdk → pages}/feedback-overlay.ts +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
startTelegramBotApiMock,
|
|
7
|
+
type TelegramBotApiMock,
|
|
8
|
+
} from "../../scripts/dev/telegram-bot-api-mock";
|
|
9
|
+
import {
|
|
10
|
+
closeDb,
|
|
11
|
+
completeTask,
|
|
12
|
+
createAgent,
|
|
13
|
+
createTaskExtended,
|
|
14
|
+
getDbClient,
|
|
15
|
+
getPendingChatRelayTasks,
|
|
16
|
+
initDb,
|
|
17
|
+
} from "../be/db";
|
|
18
|
+
import {
|
|
19
|
+
getChatInstance,
|
|
20
|
+
getChatSdkHealth,
|
|
21
|
+
isChatSdkStopping,
|
|
22
|
+
startChatSdk,
|
|
23
|
+
stopChatSdk,
|
|
24
|
+
} from "../integrations/chat-sdk/runtime";
|
|
25
|
+
import { chatSdkContextKey } from "../tasks/context-key";
|
|
26
|
+
import type { Agent } from "../types";
|
|
27
|
+
import {
|
|
28
|
+
postTelegramUpdate,
|
|
29
|
+
snapshotChatSdkEnv,
|
|
30
|
+
TELEGRAM_SECRET,
|
|
31
|
+
telegramUpdate,
|
|
32
|
+
} from "./chat-sdk-harness";
|
|
33
|
+
|
|
34
|
+
const TEST_DB_PATH = join(tmpdir(), `test-chat-sdk-shutdown-${crypto.randomUUID()}.sqlite`);
|
|
35
|
+
const SEND_METHODS = /\/(sendMessage|sendRichMessage)$/;
|
|
36
|
+
|
|
37
|
+
type ObligationRow = {
|
|
38
|
+
delivered_at: string | null;
|
|
39
|
+
attempt_count: number;
|
|
40
|
+
given_up_at: string | null;
|
|
41
|
+
last_error: string | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let lead: Agent;
|
|
45
|
+
let mock: TelegramBotApiMock;
|
|
46
|
+
let proxy: ReturnType<typeof Bun.serve>;
|
|
47
|
+
/** While true, the proxy holds every send until `releaseHeld` answers it. */
|
|
48
|
+
let holdSends = false;
|
|
49
|
+
const held: Array<() => void> = [];
|
|
50
|
+
let restoreEnv: () => void;
|
|
51
|
+
|
|
52
|
+
async function removeDbFiles(): Promise<void> {
|
|
53
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
54
|
+
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function releaseHeld(): void {
|
|
59
|
+
for (const release of held.splice(0)) release();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function waitFor(check: () => boolean | Promise<boolean>, what: string): Promise<void> {
|
|
63
|
+
const deadline = Date.now() + 10_000;
|
|
64
|
+
while (!(await check())) {
|
|
65
|
+
if (Date.now() > deadline) throw new Error(`timed out waiting for ${what}`);
|
|
66
|
+
await Bun.sleep(20);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function obligation(taskId: string): Promise<ObligationRow | null> {
|
|
71
|
+
return getDbClient().get<ObligationRow>(
|
|
72
|
+
"SELECT delivered_at, attempt_count, given_up_at, last_error FROM chat_relay_obligations WHERE task_id = ?",
|
|
73
|
+
[taskId],
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function startChannel(): Promise<void> {
|
|
78
|
+
process.env.CHAT_SDK_ENABLED = "true";
|
|
79
|
+
process.env.CHAT_SDK_ADAPTERS = JSON.stringify({
|
|
80
|
+
telegram: { botToken: "t", secretToken: TELEGRAM_SECRET, mode: "webhook" },
|
|
81
|
+
});
|
|
82
|
+
process.env.CHAT_SDK_API_URL_OVERRIDES = JSON.stringify({
|
|
83
|
+
telegram: `http://127.0.0.1:${proxy.port}`,
|
|
84
|
+
});
|
|
85
|
+
process.env.CHAT_SDK_BUFFER_MS = "60000";
|
|
86
|
+
await startChatSdk();
|
|
87
|
+
expect(getChatInstance("telegram")).toBeDefined();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
beforeAll(async () => {
|
|
91
|
+
await removeDbFiles();
|
|
92
|
+
initDb(TEST_DB_PATH);
|
|
93
|
+
lead = await createAgent({
|
|
94
|
+
id: crypto.randomUUID(),
|
|
95
|
+
name: "ChatShutdownLead",
|
|
96
|
+
isLead: true,
|
|
97
|
+
status: "idle",
|
|
98
|
+
maxTasks: 10,
|
|
99
|
+
});
|
|
100
|
+
restoreEnv = snapshotChatSdkEnv();
|
|
101
|
+
mock = startTelegramBotApiMock();
|
|
102
|
+
// Forwards to the mock; a held send never answers until released.
|
|
103
|
+
proxy = Bun.serve({
|
|
104
|
+
port: 0,
|
|
105
|
+
async fetch(req) {
|
|
106
|
+
const url = new URL(req.url);
|
|
107
|
+
if (holdSends && SEND_METHODS.test(url.pathname)) {
|
|
108
|
+
await new Promise<void>((resolve) => held.push(resolve));
|
|
109
|
+
return Response.json({ ok: false, error_code: 500, description: "held" }, { status: 500 });
|
|
110
|
+
}
|
|
111
|
+
return fetch(`${mock.url}${url.pathname}${url.search}`, {
|
|
112
|
+
method: req.method,
|
|
113
|
+
headers: req.headers,
|
|
114
|
+
body: req.method === "GET" ? undefined : await req.arrayBuffer(),
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
afterAll(async () => {
|
|
121
|
+
releaseHeld();
|
|
122
|
+
await stopChatSdk(1_000);
|
|
123
|
+
proxy.stop(true);
|
|
124
|
+
mock.stop();
|
|
125
|
+
restoreEnv();
|
|
126
|
+
closeDb();
|
|
127
|
+
await removeDbFiles();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("stopChatSdk", () => {
|
|
131
|
+
test("a relay send that never answers cannot hold shutdown past the budget", async () => {
|
|
132
|
+
await startChannel();
|
|
133
|
+
holdSends = true;
|
|
134
|
+
const chatId = 991_001;
|
|
135
|
+
const task = await createTaskExtended("chat task", {
|
|
136
|
+
agentId: lead.id,
|
|
137
|
+
source: "chat",
|
|
138
|
+
contextKey: chatSdkContextKey(`telegram:${chatId}`),
|
|
139
|
+
});
|
|
140
|
+
await completeTask(task.id, "an answer the platform never acknowledges");
|
|
141
|
+
// The watcher claims the row and its send hangs in the proxy.
|
|
142
|
+
await waitFor(() => held.length > 0, "the held relay send");
|
|
143
|
+
|
|
144
|
+
const warn = spyOn(console, "warn");
|
|
145
|
+
const budgetMs = 300;
|
|
146
|
+
const started = performance.now();
|
|
147
|
+
await stopChatSdk(budgetMs);
|
|
148
|
+
const elapsed = performance.now() - started;
|
|
149
|
+
const warnings = warn.mock.calls.map((call) => String(call[0]));
|
|
150
|
+
warn.mockRestore();
|
|
151
|
+
|
|
152
|
+
// Margin for scheduler jitter; without the bound this never resolves.
|
|
153
|
+
expect(elapsed).toBeLessThan(budgetMs + 500);
|
|
154
|
+
expect(warnings.some((line) => line.includes("abandoned: in-flight relay send"))).toBe(true);
|
|
155
|
+
expect(getChatInstance("telegram")).toBeUndefined();
|
|
156
|
+
|
|
157
|
+
// The claimed row is still pending, and it is eligible again after the
|
|
158
|
+
// first backoff (3 s * 2^1), well inside the 24 h give-up age.
|
|
159
|
+
const row = await obligation(task.id);
|
|
160
|
+
expect(row?.delivered_at).toBeNull();
|
|
161
|
+
expect(row?.given_up_at).toBeNull();
|
|
162
|
+
expect(row?.attempt_count).toBe(1);
|
|
163
|
+
const pending = await getPendingChatRelayTasks(Date.now() + 7_000, 200, ["telegram"]);
|
|
164
|
+
expect(pending.map((entry) => entry.task.id)).toContain(task.id);
|
|
165
|
+
|
|
166
|
+
// Let the abandoned send fail so its tick settles before the next test.
|
|
167
|
+
holdSends = false;
|
|
168
|
+
releaseHeld();
|
|
169
|
+
await waitFor(
|
|
170
|
+
async () => (await obligation(task.id))?.last_error != null,
|
|
171
|
+
"the abandoned send to settle",
|
|
172
|
+
);
|
|
173
|
+
expect((await obligation(task.id))?.delivered_at).toBeNull();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("with nothing in flight, shutdown flushes buffered threads and stops in order", async () => {
|
|
177
|
+
await startChannel();
|
|
178
|
+
const chatId = 991_002;
|
|
179
|
+
const contextKey = chatSdkContextKey(`telegram:${chatId}`);
|
|
180
|
+
await postTelegramUpdate(telegramUpdate({ id: chatId, type: "private" }, "please help"));
|
|
181
|
+
const tasksForThread = async (): Promise<number> =>
|
|
182
|
+
(
|
|
183
|
+
await getDbClient().get<{ n: number }>(
|
|
184
|
+
"SELECT COUNT(*) AS n FROM agent_tasks WHERE contextKey = ?",
|
|
185
|
+
[contextKey],
|
|
186
|
+
)
|
|
187
|
+
)?.n ?? 0;
|
|
188
|
+
// The 60 s buffer still holds the message.
|
|
189
|
+
expect(await tasksForThread()).toBe(0);
|
|
190
|
+
const sendsBefore = mock.calls.length;
|
|
191
|
+
|
|
192
|
+
const warn = spyOn(console, "warn");
|
|
193
|
+
await stopChatSdk(5_000);
|
|
194
|
+
const warnings = warn.mock.calls.map((call) => String(call[0]));
|
|
195
|
+
warn.mockRestore();
|
|
196
|
+
|
|
197
|
+
expect(warnings.filter((line) => line.includes("Shutdown budget spent"))).toEqual([]);
|
|
198
|
+
expect(isChatSdkStopping()).toBe(true);
|
|
199
|
+
// The flush ran while the DB and adapters were up: the message became a task.
|
|
200
|
+
expect(await tasksForThread()).toBe(1);
|
|
201
|
+
expect(getChatInstance("telegram")).toBeUndefined();
|
|
202
|
+
expect(getChatSdkHealth()).toEqual({});
|
|
203
|
+
|
|
204
|
+
// The relay watcher is stopped: a later terminal task posts nothing.
|
|
205
|
+
const late = await createTaskExtended("late", {
|
|
206
|
+
agentId: lead.id,
|
|
207
|
+
source: "chat",
|
|
208
|
+
contextKey,
|
|
209
|
+
});
|
|
210
|
+
await completeTask(late.id, "after shutdown");
|
|
211
|
+
await Bun.sleep(400);
|
|
212
|
+
expect(
|
|
213
|
+
mock.calls
|
|
214
|
+
.slice(sendsBefore)
|
|
215
|
+
// The typing action of the buffered DM may still be in flight when `sendsBefore` is read.
|
|
216
|
+
.filter((call) => /send/.test(call.method) && call.method !== "sendChatAction"),
|
|
217
|
+
).toEqual([]);
|
|
218
|
+
expect((await obligation(late.id))?.attempt_count).toBe(0);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { createHmac } from "node:crypto";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import { unlink } from "node:fs/promises";
|
|
5
|
+
import { SlackMock } from "@desplega.ai/slack-mock";
|
|
6
|
+
import { closeDb, completeTask, createAgent, getDbClient, initDb } from "../be/db";
|
|
7
|
+
import { watchSocketConnection } from "../integrations/chat-sdk/adapters";
|
|
8
|
+
import { drainBackground } from "../integrations/chat-sdk/background";
|
|
9
|
+
import { createScrubbingLogger } from "../integrations/chat-sdk/logger";
|
|
10
|
+
import { processPendingChatRelays, stopChatRelayWatcher } from "../integrations/chat-sdk/relay";
|
|
11
|
+
import { getChatSdkHealth, startChatSdk, stopChatSdk } from "../integrations/chat-sdk/runtime";
|
|
12
|
+
import { isChatThreadBuffered } from "../integrations/chat-sdk/thread-buffer";
|
|
13
|
+
import { getEnabledCapabilities } from "../server";
|
|
14
|
+
import { chatSdkContextKey } from "../tasks/context-key";
|
|
15
|
+
import { flushChatBuffers, postChatWebhook, snapshotChatSdkEnv } from "./chat-sdk-harness";
|
|
16
|
+
|
|
17
|
+
const TEST_DB_PATH = "./test-chat-sdk-slack.sqlite";
|
|
18
|
+
const SIGNING_SECRET = "mock-signing-secret";
|
|
19
|
+
const CHANNEL_ID = "C0GENERAL0";
|
|
20
|
+
|
|
21
|
+
let slack: SlackMock;
|
|
22
|
+
let leadId: string;
|
|
23
|
+
const restoreEnv = snapshotChatSdkEnv();
|
|
24
|
+
|
|
25
|
+
type TaskRow = {
|
|
26
|
+
id: string;
|
|
27
|
+
source: string;
|
|
28
|
+
contextKey: string;
|
|
29
|
+
agentId: string | null;
|
|
30
|
+
task: string;
|
|
31
|
+
dependsOn: string | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
async function removeDbFiles(): Promise<void> {
|
|
35
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
36
|
+
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function chatTasks(contextKey: string): Promise<TaskRow[]> {
|
|
41
|
+
return getDbClient().query<TaskRow>(
|
|
42
|
+
"SELECT id, source, contextKey, agentId, task, dependsOn FROM agent_tasks WHERE contextKey = ? ORDER BY createdAt, rowid",
|
|
43
|
+
[contextKey],
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Socket-mode events reach the handler after slack-mock has its ack, so poll
|
|
49
|
+
* the thread buffer (bounded) instead of sleeping a fixed time.
|
|
50
|
+
*/
|
|
51
|
+
async function waitForBuffered(threadId: string, timeoutMs = 5_000): Promise<void> {
|
|
52
|
+
const deadline = Date.now() + timeoutMs;
|
|
53
|
+
while (!isChatThreadBuffered(threadId)) {
|
|
54
|
+
if (Date.now() > deadline) throw new Error(`no buffered message for ${threadId}`);
|
|
55
|
+
await Bun.sleep(10);
|
|
56
|
+
}
|
|
57
|
+
expect(await drainBackground(5_000)).toBe(true);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function startSlackAdapter(mode: "socket" | "webhook"): Promise<void> {
|
|
61
|
+
process.env.CHAT_SDK_ENABLED = "true";
|
|
62
|
+
process.env.CHAT_SDK_ADAPTERS = JSON.stringify({
|
|
63
|
+
slack: {
|
|
64
|
+
mode,
|
|
65
|
+
botToken: slack.env.SLACK_BOT_TOKEN,
|
|
66
|
+
...(mode === "socket"
|
|
67
|
+
? { appToken: slack.env.SLACK_APP_TOKEN }
|
|
68
|
+
: { signingSecret: SIGNING_SECRET }),
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
process.env.CHAT_SDK_API_URL_OVERRIDES = JSON.stringify({ slack: slack.env.SLACK_API_URL });
|
|
72
|
+
await startChatSdk();
|
|
73
|
+
// Relay ticks are driven by the tests.
|
|
74
|
+
await stopChatRelayWatcher();
|
|
75
|
+
expect(getChatSdkHealth().slack?.status).toBe("ok");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function signedHeaders(body: string, secret = SIGNING_SECRET): Record<string, string> {
|
|
79
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
80
|
+
const signature = `v0=${createHmac("sha256", secret).update(`v0:${timestamp}:${body}`).digest("hex")}`;
|
|
81
|
+
return {
|
|
82
|
+
"content-type": "application/json",
|
|
83
|
+
"x-slack-request-timestamp": timestamp,
|
|
84
|
+
"x-slack-signature": signature,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
beforeAll(async () => {
|
|
89
|
+
await removeDbFiles();
|
|
90
|
+
initDb(TEST_DB_PATH);
|
|
91
|
+
const lead = await createAgent({
|
|
92
|
+
id: crypto.randomUUID(),
|
|
93
|
+
name: "SlackChatLead",
|
|
94
|
+
isLead: true,
|
|
95
|
+
status: "idle",
|
|
96
|
+
capabilities: [],
|
|
97
|
+
});
|
|
98
|
+
leadId = lead.id;
|
|
99
|
+
slack = await SlackMock.start({
|
|
100
|
+
port: 0,
|
|
101
|
+
manifest: `${import.meta.dir}/../../slack-manifest.json`,
|
|
102
|
+
});
|
|
103
|
+
process.env.SLACK_DISABLE = "true";
|
|
104
|
+
process.env.CHAT_SDK_BUFFER_MS = "60000";
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
afterAll(async () => {
|
|
108
|
+
await stopChatSdk();
|
|
109
|
+
await slack.stop();
|
|
110
|
+
restoreEnv();
|
|
111
|
+
closeDb();
|
|
112
|
+
await removeDbFiles();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("socket connection watcher", () => {
|
|
116
|
+
test("reconnecting stays info; unable_to_socket_mode_start reaches the warning sink", () => {
|
|
117
|
+
const socketClient = new EventEmitter();
|
|
118
|
+
const issues: string[] = [];
|
|
119
|
+
const logger = createScrubbingLogger("slack", (level, message) =>
|
|
120
|
+
issues.push(`${level}: ${message}`),
|
|
121
|
+
);
|
|
122
|
+
const quiet = spyOn(console, "info").mockImplementation(() => {});
|
|
123
|
+
const warn = spyOn(console, "warn").mockImplementation(() => {});
|
|
124
|
+
try {
|
|
125
|
+
watchSocketConnection({ socketClient } as never, logger);
|
|
126
|
+
socketClient.emit("reconnecting");
|
|
127
|
+
expect(issues).toEqual([]);
|
|
128
|
+
socketClient.emit("unable_to_socket_mode_start");
|
|
129
|
+
expect(issues).toHaveLength(1);
|
|
130
|
+
expect(issues[0]).toContain("unable to start");
|
|
131
|
+
} finally {
|
|
132
|
+
quiet.mockRestore();
|
|
133
|
+
warn.mockRestore();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("clash guard", () => {
|
|
139
|
+
test("SLACK_DISABLE=true drops the built-in slack capability and keeps chat-sdk", () => {
|
|
140
|
+
process.env.CHAT_SDK_ENABLED = "true";
|
|
141
|
+
const capabilities = getEnabledCapabilities();
|
|
142
|
+
expect(capabilities).not.toContain("slack");
|
|
143
|
+
expect(capabilities).toContain("chat-sdk");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("Chat SDK Slack adapter, socket mode", () => {
|
|
148
|
+
let askTs: string;
|
|
149
|
+
let firstTaskId: string;
|
|
150
|
+
let contextKey: string;
|
|
151
|
+
|
|
152
|
+
beforeAll(async () => {
|
|
153
|
+
await startSlackAdapter("socket");
|
|
154
|
+
await slack.waitForConnection();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
afterAll(async () => {
|
|
158
|
+
await stopChatSdk();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("the webhook route answers the adapter's 405 in socket mode", async () => {
|
|
162
|
+
const body = JSON.stringify({ type: "url_verification", challenge: "c" });
|
|
163
|
+
const response = await postChatWebhook("slack", body, signedHeaders(body));
|
|
164
|
+
expect(response.status).toBe(405);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("a mention creates one chat task with a slack contextKey", async () => {
|
|
168
|
+
const ask = await slack.postMessage({
|
|
169
|
+
channel: "general",
|
|
170
|
+
user: "alice",
|
|
171
|
+
text: `<@${slack.bot.userId}> summarize the open tasks`,
|
|
172
|
+
});
|
|
173
|
+
askTs = ask.ts;
|
|
174
|
+
const threadId = `slack:${CHANNEL_ID}:${askTs}`;
|
|
175
|
+
contextKey = chatSdkContextKey(threadId);
|
|
176
|
+
expect(contextKey.startsWith("task:chat-sdk:slack:")).toBe(true);
|
|
177
|
+
|
|
178
|
+
await waitForBuffered(threadId);
|
|
179
|
+
await flushChatBuffers();
|
|
180
|
+
|
|
181
|
+
const tasks = await chatTasks(contextKey);
|
|
182
|
+
expect(tasks).toHaveLength(1);
|
|
183
|
+
expect(tasks[0]!.source).toBe("chat");
|
|
184
|
+
expect(tasks[0]!.agentId).toBe(leadId);
|
|
185
|
+
expect(tasks[0]!.task).toContain("summarize the open tasks");
|
|
186
|
+
firstTaskId = tasks[0]!.id;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("a human reply while the task is pending becomes a follow-up with dependsOn", async () => {
|
|
190
|
+
await slack.postMessage({
|
|
191
|
+
channel: "general",
|
|
192
|
+
user: "bob",
|
|
193
|
+
thread_ts: askTs,
|
|
194
|
+
text: "also include the failed ones",
|
|
195
|
+
});
|
|
196
|
+
await waitForBuffered(`slack:${CHANNEL_ID}:${askTs}`);
|
|
197
|
+
await flushChatBuffers();
|
|
198
|
+
|
|
199
|
+
const tasks = await chatTasks(contextKey);
|
|
200
|
+
expect(tasks).toHaveLength(2);
|
|
201
|
+
const followUp = tasks[1]!;
|
|
202
|
+
expect(followUp.task).toContain("also include the failed ones");
|
|
203
|
+
expect(JSON.parse(followUp.dependsOn ?? "[]")).toEqual([firstTaskId]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("completing the task posts its output in the Slack thread", async () => {
|
|
207
|
+
await completeTask(firstTaskId, "Three tasks are open.");
|
|
208
|
+
await processPendingChatRelays();
|
|
209
|
+
const reply = await slack.waitForMessage(
|
|
210
|
+
{ channel: "general", thread_ts: askTs, from: "bot" },
|
|
211
|
+
{ timeoutMs: 5_000 },
|
|
212
|
+
);
|
|
213
|
+
expect(reply.text).toContain("Three tasks are open.");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("Chat SDK Slack adapter, webhook mode", () => {
|
|
218
|
+
beforeAll(async () => {
|
|
219
|
+
await startSlackAdapter("webhook");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
afterAll(async () => {
|
|
223
|
+
await stopChatSdk();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("a signed url_verification is answered with the challenge", async () => {
|
|
227
|
+
const body = JSON.stringify({ type: "url_verification", challenge: "challenge-123" });
|
|
228
|
+
const response = await postChatWebhook("slack", body, signedHeaders(body));
|
|
229
|
+
expect(response.status).toBe(200);
|
|
230
|
+
expect(JSON.parse(response.body)).toEqual({ challenge: "challenge-123" });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("an unsigned or wrongly signed event_callback is rejected with 401", async () => {
|
|
234
|
+
const body = JSON.stringify({ type: "event_callback", event: { type: "app_mention" } });
|
|
235
|
+
const unsigned = await postChatWebhook("slack", body, { "content-type": "application/json" });
|
|
236
|
+
expect(unsigned.status).toBe(401);
|
|
237
|
+
const wrong = await postChatWebhook("slack", body, signedHeaders(body, "not-the-secret"));
|
|
238
|
+
expect(wrong.status).toBe(401);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("a signed app_mention creates a chat task", async () => {
|
|
242
|
+
const ts = `${Math.floor(Date.now() / 1000)}.000777`;
|
|
243
|
+
const body = JSON.stringify({
|
|
244
|
+
type: "event_callback",
|
|
245
|
+
team_id: slack.team.id,
|
|
246
|
+
api_app_id: "A0MOCKAPP0",
|
|
247
|
+
event_id: `Ev${crypto.randomUUID().replaceAll("-", "").slice(0, 10)}`,
|
|
248
|
+
event_time: Math.floor(Date.now() / 1000),
|
|
249
|
+
event: {
|
|
250
|
+
type: "app_mention",
|
|
251
|
+
user: "U0ALICE000",
|
|
252
|
+
text: `<@${slack.bot.userId}> webhook hello`,
|
|
253
|
+
ts,
|
|
254
|
+
channel: CHANNEL_ID,
|
|
255
|
+
event_ts: ts,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
const response = await postChatWebhook("slack", body, signedHeaders(body));
|
|
259
|
+
expect(response.status).toBe(200);
|
|
260
|
+
expect(await drainBackground(5_000)).toBe(true);
|
|
261
|
+
await flushChatBuffers();
|
|
262
|
+
|
|
263
|
+
const tasks = await chatTasks(chatSdkContextKey(`slack:${CHANNEL_ID}:${ts}`));
|
|
264
|
+
expect(tasks).toHaveLength(1);
|
|
265
|
+
expect(tasks[0]!.source).toBe("chat");
|
|
266
|
+
expect(tasks[0]!.task).toContain("webhook hello");
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import type { QueueEntry } from "chat";
|
|
4
|
+
import { closeDb, getDbClient, initDb } from "../be/db";
|
|
5
|
+
import { SqliteChatState, sweepExpired } from "../integrations/chat-sdk/state";
|
|
6
|
+
|
|
7
|
+
const TEST_DB_PATH = "./test-chat-sdk-state.sqlite";
|
|
8
|
+
|
|
9
|
+
async function removeDbFiles(): Promise<void> {
|
|
10
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
11
|
+
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const state = new SqliteChatState();
|
|
16
|
+
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
await removeDbFiles();
|
|
19
|
+
initDb(TEST_DB_PATH);
|
|
20
|
+
await state.connect();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
closeDb();
|
|
25
|
+
await removeDbFiles();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function entry(id: string, expiresAt = Date.now() + 60_000): QueueEntry {
|
|
29
|
+
return { enqueuedAt: Date.now(), expiresAt, message: { id } as unknown as QueueEntry["message"] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("SqliteChatState locks", () => {
|
|
33
|
+
test("acquire is exclusive until release", async () => {
|
|
34
|
+
const lock = await state.acquireLock("telegram:1", 60_000);
|
|
35
|
+
expect(lock).not.toBeNull();
|
|
36
|
+
expect(await state.acquireLock("telegram:1", 60_000)).toBeNull();
|
|
37
|
+
await state.releaseLock(lock!);
|
|
38
|
+
expect(await state.acquireLock("telegram:1", 60_000)).not.toBeNull();
|
|
39
|
+
await state.forceReleaseLock("telegram:1");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("release with a stale token keeps the current holder", async () => {
|
|
43
|
+
const lock = await state.acquireLock("telegram:2", 60_000);
|
|
44
|
+
await state.releaseLock({ ...lock!, token: "other" });
|
|
45
|
+
expect(await state.acquireLock("telegram:2", 60_000)).toBeNull();
|
|
46
|
+
await state.forceReleaseLock("telegram:2");
|
|
47
|
+
expect(await state.acquireLock("telegram:2", 60_000)).not.toBeNull();
|
|
48
|
+
await state.forceReleaseLock("telegram:2");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("an expired lock can be taken over and no longer extends", async () => {
|
|
52
|
+
const first = await state.acquireLock("telegram:3", 1);
|
|
53
|
+
await Bun.sleep(5);
|
|
54
|
+
const second = await state.acquireLock("telegram:3", 60_000);
|
|
55
|
+
expect(second).not.toBeNull();
|
|
56
|
+
expect(second!.token).not.toBe(first!.token);
|
|
57
|
+
expect(await state.extendLock(first!, 60_000)).toBe(false);
|
|
58
|
+
expect(await state.extendLock(second!, 60_000)).toBe(true);
|
|
59
|
+
await state.releaseLock(second!);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("SqliteChatState key-value", () => {
|
|
64
|
+
test("get/set/delete round trip JSON values", async () => {
|
|
65
|
+
await state.set("k:obj", { a: 1, b: ["x"] });
|
|
66
|
+
expect(await state.get<{ a: number; b: string[] }>("k:obj")).toEqual({ a: 1, b: ["x"] });
|
|
67
|
+
await state.delete("k:obj");
|
|
68
|
+
expect(await state.get("k:obj")).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("ttl expires values and 0 means no expiry", async () => {
|
|
72
|
+
await state.set("k:short", "v", 1);
|
|
73
|
+
await state.set("k:forever", "v", 0);
|
|
74
|
+
await Bun.sleep(5);
|
|
75
|
+
expect(await state.get("k:short")).toBeNull();
|
|
76
|
+
expect(await state.get("k:forever")).toBe("v");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("setIfNotExists claims once and reclaims after expiry", async () => {
|
|
80
|
+
expect(await state.setIfNotExists("dedupe:telegram:1", true, 60_000)).toBe(true);
|
|
81
|
+
expect(await state.setIfNotExists("dedupe:telegram:1", true, 60_000)).toBe(false);
|
|
82
|
+
expect(await state.setIfNotExists("dedupe:telegram:2", true, 1)).toBe(true);
|
|
83
|
+
await Bun.sleep(5);
|
|
84
|
+
expect(await state.setIfNotExists("dedupe:telegram:2", true, 60_000)).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("SqliteChatState lists", () => {
|
|
89
|
+
test("append keeps the newest maxLength in order", async () => {
|
|
90
|
+
for (let i = 1; i <= 5; i++) await state.appendToList("list:a", i, { maxLength: 3 });
|
|
91
|
+
expect(await state.getList<number>("list:a")).toEqual([3, 4, 5]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("ttl is reset on every append and an expired list starts over", async () => {
|
|
95
|
+
await state.appendToList("list:b", "one", { ttlMs: 1 });
|
|
96
|
+
await Bun.sleep(5);
|
|
97
|
+
expect(await state.getList("list:b")).toEqual([]);
|
|
98
|
+
await state.appendToList("list:b", "two", { ttlMs: 60_000 });
|
|
99
|
+
expect(await state.getList("list:b")).toEqual(["two"]);
|
|
100
|
+
|
|
101
|
+
// The second append moves every row of the list to the new expiry. A short
|
|
102
|
+
// first TTL would race the second append under CI load, so read the
|
|
103
|
+
// stored expiries instead of sleeping past them.
|
|
104
|
+
await state.appendToList("list:c", "x", { ttlMs: 30_000 });
|
|
105
|
+
const before = Date.now();
|
|
106
|
+
await state.appendToList("list:c", "y", { ttlMs: 60_000 });
|
|
107
|
+
const row = await getDbClient().get<{ lo: number; hi: number }>(
|
|
108
|
+
"SELECT MIN(expires_at) AS lo, MAX(expires_at) AS hi FROM chat_lists WHERE key = ?",
|
|
109
|
+
["list:c"],
|
|
110
|
+
);
|
|
111
|
+
expect(row?.lo).toBe(row?.hi);
|
|
112
|
+
expect(row?.lo).toBeGreaterThanOrEqual(before + 60_000);
|
|
113
|
+
expect(await state.getList("list:c")).toEqual(["x", "y"]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("SqliteChatState queue", () => {
|
|
118
|
+
test("FIFO with maxSize dropping the oldest", async () => {
|
|
119
|
+
expect(await state.enqueue("telegram:q", entry("m1"), 2)).toBe(1);
|
|
120
|
+
expect(await state.enqueue("telegram:q", entry("m2"), 2)).toBe(2);
|
|
121
|
+
expect(await state.enqueue("telegram:q", entry("m3"), 2)).toBe(2);
|
|
122
|
+
expect(await state.queueDepth("telegram:q")).toBe(2);
|
|
123
|
+
expect((await state.dequeue("telegram:q"))?.message.id).toBe("m2");
|
|
124
|
+
expect((await state.dequeue("telegram:q"))?.message.id).toBe("m3");
|
|
125
|
+
expect(await state.dequeue("telegram:q")).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("dequeue returns expired entries (the core filters them)", async () => {
|
|
129
|
+
await state.enqueue("telegram:q2", entry("old", Date.now() - 1), 5);
|
|
130
|
+
expect((await state.dequeue("telegram:q2"))?.message.id).toBe("old");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("SqliteChatState subscriptions", () => {
|
|
135
|
+
test("subscriptions survive disconnect", async () => {
|
|
136
|
+
await state.subscribe("telegram:sub");
|
|
137
|
+
await state.subscribe("telegram:sub");
|
|
138
|
+
expect(await state.isSubscribed("telegram:sub")).toBe(true);
|
|
139
|
+
await state.disconnect();
|
|
140
|
+
await state.connect();
|
|
141
|
+
expect(await state.isSubscribed("telegram:sub")).toBe(true);
|
|
142
|
+
await state.unsubscribe("telegram:sub");
|
|
143
|
+
expect(await state.isSubscribed("telegram:sub")).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("sweepExpired", () => {
|
|
148
|
+
test("removes expired kv, list and lock rows only", async () => {
|
|
149
|
+
await state.set("sweep:old", 1, 1);
|
|
150
|
+
await state.set("sweep:new", 1, 60_000);
|
|
151
|
+
await state.appendToList("sweep:list", 1, { ttlMs: 1 });
|
|
152
|
+
await state.acquireLock("telegram:sweep", 1);
|
|
153
|
+
await Bun.sleep(5);
|
|
154
|
+
await sweepExpired();
|
|
155
|
+
const db = getDbClient();
|
|
156
|
+
expect(await db.get("SELECT key FROM chat_state_kv WHERE key = 'sweep:old'")).toBeNull();
|
|
157
|
+
expect(await db.get("SELECT key FROM chat_state_kv WHERE key = 'sweep:new'")).not.toBeNull();
|
|
158
|
+
expect(await db.get("SELECT key FROM chat_lists WHERE key = 'sweep:list'")).toBeNull();
|
|
159
|
+
expect(
|
|
160
|
+
await db.get("SELECT thread_id FROM chat_locks WHERE thread_id = 'telegram:sweep'"),
|
|
161
|
+
).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
});
|