@desplega.ai/agent-swarm 1.158.0 → 1.159.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-adapter-xaq4n6pc.js → acp-adapter-qhch0n2v.js} +1 -1
- package/dist/{actions-p9hz5yat.js → actions-dtsh1mfb.js} +33 -29
- package/dist/{anthropic-messages-51pz26qe.js → anthropic-messages-f2h06k64.js} +8 -3
- package/dist/{app-g26wdcz6.js → app-y1b9srva.js} +10 -9
- package/dist/{assistant-wydfqkb3.js → assistant-31hwy5xj.js} +34 -30
- package/dist/{azure-openai-responses-8gksqtmz.js → azure-openai-responses-0k1mfax5.js} +12 -13
- package/dist/{boot-reembed-se8zmdcp.js → boot-reembed-48d2qpkt.js} +11 -11
- package/dist/{boot-reembed-g5ckg9yb.js → boot-reembed-w0bryhkr.js} +12 -12
- package/dist/{boot-scrub-logs-wfxqan61.js → boot-scrub-logs-t65wbrn7.js} +9 -8
- package/dist/{boot-seeds-s3q01xj3.js → boot-seeds-h7c32hvm.js} +12 -11
- package/dist/{claude-adapter-tv2x4f36.js → claude-adapter-1t6611ne.js} +26 -21
- package/dist/{claude-managed-adapter-41b2s7fd.js → claude-managed-adapter-werp659j.js} +4 -2
- package/dist/{claude-managed-setup-60g8aazx.js → claude-managed-setup-y6rdgbc1.js} +11 -2
- package/dist/claude-sdk-session-zhkyk8yw.js +29319 -0
- package/dist/{cli-vj6h9kc2.js → cli-0m7wct0d.js} +59 -1
- package/dist/{cli-wscdvcqz.js → cli-0n2e4x49.js} +1 -1
- package/dist/cli-0xqde53b.js +176 -0
- package/dist/{cli-xberxa1r.js → cli-10ym38qc.js} +2 -2
- package/dist/{cli-5b6b62tz.js → cli-13hw4z91.js} +1 -1
- package/dist/{cli-rqz8qbw7.js → cli-20035trh.js} +50 -18
- package/dist/{cli-k0g1sjzm.js → cli-31nkw3g3.js} +3 -3
- package/dist/cli-3gvrr8vj.js +1422 -0
- package/dist/{cli-c07r9a2m.js → cli-48527fpw.js} +9 -8
- package/dist/{cli-24k806sc.js → cli-4aj25szq.js} +2 -2
- package/dist/{cli-7gtvvpbh.js → cli-4tvgz99m.js} +7 -7
- package/dist/cli-503ya656.js +455 -0
- package/dist/{cli-cw9zck8j.js → cli-5f2hq128.js} +8 -4
- package/dist/{cli-s8eet254.js → cli-68bmme5p.js} +11078 -27833
- package/dist/{cli-ftfvggzx.js → cli-79aq9cgb.js} +794 -563
- package/dist/cli-7cxw8b60.js +14910 -0
- package/dist/cli-7kyk9jr3.js +22 -0
- package/dist/cli-7zttzetj.js +324 -0
- package/dist/{cli-y95wp3tg.js → cli-96qd5psa.js} +1 -1
- package/dist/{cli-g8djp6qe.js → cli-9yg65c1j.js} +22 -0
- package/dist/{cli-3gmcwfyx.js → cli-ardpzhjm.js} +5 -5
- package/dist/{cli-7s3syk22.js → cli-b6thtzac.js} +5 -5
- package/dist/{cli-00w2c66p.js → cli-bgv70kym.js} +13 -2
- package/dist/{cli-j55n64b6.js → cli-bv0a38v9.js} +163 -44
- package/dist/cli-ca2w7zv7.js +115 -0
- package/dist/cli-cpvsaqsw.js +333 -0
- package/dist/cli-cygyqj3b.js +754 -0
- package/dist/{cli-x24pshtq.js → cli-dvjpkkjd.js} +6 -6
- package/dist/{cli-5d10vd2m.js → cli-e0ndxhtk.js} +4 -4
- package/dist/{cli-4198qc9v.js → cli-e4xnc0qp.js} +15 -3
- package/dist/{cli-rjehvrts.js → cli-eg7cdg68.js} +8 -8
- package/dist/cli-ejpygnqz.js +3815 -0
- package/dist/{cli-fgqxwrpv.js → cli-fjtp3463.js} +1631 -127
- package/dist/{cli-zt1227k1.js → cli-fkqhbg8g.js} +5 -3
- package/dist/{cli-47m6dkd3.js → cli-g9df2m37.js} +409 -168
- package/dist/cli-gzyh0j0g.js +8455 -0
- package/dist/cli-hgqfttfc.js +183 -0
- package/dist/cli-hj65wsts.js +17755 -0
- package/dist/cli-j9z8mfx3.js +476 -0
- package/dist/{cli-aehm1rbq.js → cli-kc3dp84d.js} +1135 -61
- package/dist/{cli-h38x0qhg.js → cli-kdnehe0t.js} +4 -3
- package/dist/{cli-20cj37vt.js → cli-m4excttz.js} +136 -2216
- package/dist/{cli-8qmme6vd.js → cli-mg24hkt4.js} +2 -2
- package/dist/{cli-xhaeg2nc.js → cli-mzkqpxee.js} +1 -1
- package/dist/{cli-q67y4wzm.js → cli-phsaj4xq.js} +24 -2
- package/dist/{cli-pw9rj5t9.js → cli-pqetb83f.js} +1 -176
- package/dist/{cli-rj19hc34.js → cli-qjxy52w7.js} +1 -1
- package/dist/{cli-53sz8gkz.js → cli-qv4jv8bx.js} +1 -1
- package/dist/cli-qvnnx4ct.js +1051 -0
- package/dist/cli-rb9m6rpe.js +4980 -0
- package/dist/{cli-tt9k8r35.js → cli-rfezr056.js} +476 -909
- package/dist/{cli-ggm92wcn.js → cli-rgjprct3.js} +1 -0
- package/dist/{cli-9httdpyq.js → cli-rrqmqr1s.js} +1 -1
- package/dist/cli-rykfmgwg.js +102 -0
- package/dist/{cli-eetrvv91.js → cli-t3p77qk1.js} +1 -1
- package/dist/{cli-68w09sjw.js → cli-vn0qv9cj.js} +25 -88
- package/dist/{cli-987hg0aa.js → cli-vwbbaecp.js} +2 -2
- package/dist/cli-wd9hz40d.js +69 -0
- package/dist/{cli-j57qkrpq.js → cli-wgjx0q33.js} +13 -13
- package/dist/cli-xgpf00sk.js +2149 -0
- package/dist/{cli-mcnhgnn1.js → cli-ykk5jaax.js} +184 -26
- package/dist/{cli-aky8tctm.js → cli-z9x0sh1t.js} +3 -3
- package/dist/{cli-1hkrch3k.js → cli-zw65mab6.js} +5 -5
- package/dist/cli.js +38 -29
- package/dist/cloudflare-workers-ai-system-one-1sg586qv.js +46 -0
- package/dist/{codex-adapter-cnm8wk11.js → codex-adapter-jqfqqa21.js} +21 -14
- package/dist/{codex-hook-9hptmdhj.js → codex-hook-5eys3dya.js} +2 -2
- package/dist/{codex-session-runner-gfg547zj.js → codex-session-runner-zd341dsm.js} +21 -14
- package/dist/{commands-e3bn0p75.js → commands-vfsysz4s.js} +9 -8
- package/dist/{db-03v9kzr2.js → db-s3znhvbn.js} +15 -10
- package/dist/{dispatcher-nrmg8yht.js → dispatcher-5xn0c7rf.js} +32 -28
- package/dist/{dsh-adapter-k03nfqa6.js → dsh-adapter-894mgfvs.js} +3 -3
- package/dist/{e2b-93x1k0j5.js → e2b-ez07znam.js} +4 -3
- package/dist/execute-71ysgv35.js +965 -0
- package/dist/{google-generative-ai-bxmv50nc.js → google-generative-ai-1nfcdphp.js} +5 -4
- package/dist/{google-vertex-809af514.js → google-vertex-1tqcd7b1.js} +5 -4
- package/dist/{handlers-aqcedbyb.js → handlers-5w4jwycr.js} +35 -31
- package/dist/{hook-7vkskkre.js → hook-853bxrr6.js} +16 -13
- package/dist/{hook-ybvmrmhq.js → hook-z0hbxj85.js} +16 -13
- package/dist/{http-2eqa8t24.js → http-akc3wy3m.js} +600 -123
- package/dist/{index-f6cryxwe.js → index-4a5njnm1.js} +2 -59
- package/dist/{index-a3vn96n8.js → index-5sz16ksh.js} +187 -47
- package/dist/{index-c6zsv1bd.js → index-bjmjh9h4.js} +11 -11
- package/dist/{index-chgdkq36.js → index-cd151qkq.js} +32 -28
- package/dist/{index-x71bvq0p.js → index-k661dzzr.js} +33 -29
- package/dist/{index-1yks3qcr.js → index-ttpbhnjg.js} +32 -28
- package/dist/{keepalive-vtkf5n99.js → keepalive-8p9rpvz5.js} +11 -10
- package/dist/{lead-h3sp357x.js → lead-jp69z5h3.js} +49 -45
- package/dist/llama-cpp-classify-t2tccams.js +335 -0
- package/dist/{maintenance-jzeqtk3q.js → maintenance-fwx4ev0k.js} +12 -12
- package/dist/{mistral-conversations-zx0kn554.js → mistral-conversations-bhvwqcnk.js} +14 -17
- package/dist/{oauth-refresh-sweep-qp3dzmdp.js → oauth-refresh-sweep-cwcq1ctx.js} +10 -9
- package/dist/{onboard-tmsp0qbz.js → onboard-2b4q0t84.js} +6 -5
- package/dist/{openai-codex-responses-4vjqzvjp.js → openai-codex-responses-nekthgks.js} +21 -8
- package/dist/{openai-completions-9hjrqbw8.js → openai-completions-hxtzwj0s.js} +8 -11
- package/dist/{openai-responses-7b1pn0xt.js → openai-responses-w4r118w7.js} +22 -17
- package/dist/{opencode-adapter-x6x2da1z.js → opencode-adapter-zz7xvjj6.js} +6 -4
- package/dist/{openrouter-images-9f9ek4rw.js → openrouter-images-b6sqqjgn.js} +4 -6
- package/dist/{otel-impl-9mz73q65.js → otel-impl-g8rs4vwr.js} +1 -1
- package/dist/{pi-messages-txpjj39q.js → pi-messages-vxdt8v7j.js} +2 -1
- package/dist/{pi-mono-adapter-m6dvv0dz.js → pi-mono-adapter-n0me28wt.js} +29 -17
- package/dist/pricing-refresh-0v77385v.js +39 -0
- package/dist/{rbac-roles-c9wqz83x.js → rbac-roles-krjyazbj.js} +10 -9
- package/dist/{render-v2-gryrh9h8.js → render-v2-3wjsp9aw.js} +10 -9
- package/dist/runtime-ktjysacx.js +24 -0
- package/dist/{setup-q9720bt6.js → setup-yeqryke6.js} +6 -5
- package/dist/typesafe-system-one-96960jxp.js +27 -0
- package/dist/{virtual-modules-3htpgn8x.js → virtual-modules-dzdeq1sf.js} +21 -13
- package/dist/{work-objects-b76x7deb.js → work-objects-3pakpkw0.js} +35 -31
- package/dist/{worker-6vrxbv41.js → worker-85testgj.js} +49 -45
- package/openapi.json +863 -14
- package/package.json +9 -8
- package/src/be/db/agents.ts +20 -0
- package/src/be/db/tasks/human-free.ts +442 -0
- package/src/be/db/tasks/read.ts +56 -1
- package/src/be/db/tasks/write.ts +107 -11
- package/src/be/db.ts +89 -165
- package/src/be/harness-model-support.ts +182 -0
- package/src/be/human-free-drain.ts +67 -0
- package/src/be/migrations/182_task_human_free_flag.sql +63 -0
- package/src/be/migrations/183_human_free_reclassify_queue.sql +25 -0
- package/src/be/migrations/184_model_catalog.sql +120 -0
- package/src/be/model-catalog-store.ts +644 -0
- package/src/be/model-tier-keys.ts +31 -0
- package/src/be/model-tier-resolution.ts +489 -0
- package/src/be/model-validation.ts +112 -0
- package/src/be/models-catalog.ts +61 -7
- package/src/be/modelsdev-cache.json +82 -0
- package/src/be/modelsdev-cache.ts +2 -0
- package/src/be/pricing-refresh.ts +154 -12
- package/src/be/scripts/typecheck.ts +15 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +61 -9
- package/src/be/swarm-config-guard.ts +6 -0
- package/src/cli.tsx +10 -0
- package/src/commands/claude-managed-setup.ts +3 -1
- package/src/commands/runner.ts +76 -2
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/heartbeat/heartbeat.ts +64 -15
- package/src/heartbeat/templates.ts +2 -2
- package/src/http/agents.ts +49 -1
- package/src/http/index.ts +8 -0
- package/src/http/models-catalog.ts +357 -8
- package/src/http/poll.ts +45 -2
- package/src/http/schedules.ts +36 -6
- package/src/http/session-data.ts +2 -5
- package/src/http/stats.ts +6 -8
- package/src/http/tasks.ts +26 -1
- package/src/http/usage-cache.ts +56 -15
- package/src/http/workflows.ts +35 -8
- package/src/prompts/session-templates.ts +1 -0
- package/src/providers/claude-managed-models.ts +47 -161
- package/src/providers/codex-adapter.ts +4 -4
- package/src/providers/codex-models.ts +65 -198
- package/src/providers/reasoning-effort.ts +44 -140
- package/src/rbac/legacy-policy.ts +25 -0
- package/src/rbac/permissions.ts +10 -0
- package/src/scripts-runtime/sdk-allowlist.ts +4 -0
- package/src/scripts-runtime/types/stdlib.d.ts +15 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
- package/src/server.ts +6 -0
- package/src/slack/message-text.ts +56 -2
- package/src/slack/thread-buffer.ts +10 -2
- package/src/tests/agents-list-model-display.test.ts +4 -6
- package/src/tests/base-prompt.test.ts +3 -0
- package/src/tests/claude-adapter.test.ts +10 -0
- package/src/tests/claude-managed-adapter.test.ts +12 -18
- package/src/tests/codex-adapter.test.ts +2 -1
- package/src/tests/codex-gpt6-release.test.ts +66 -5
- package/src/tests/context-window.test.ts +7 -5
- package/src/tests/db-extraction-contract.test.ts +1 -0
- package/src/tests/db-task-read-contract.test.ts +5 -1
- package/src/tests/db-task-write-contract.test.ts +4 -1
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/explicit-model-validation.test.ts +332 -0
- package/src/tests/harness-model-support.test.ts +374 -0
- package/src/tests/heartbeat-checklist-config.test.ts +71 -0
- package/src/tests/heartbeat-checklist.test.ts +4 -2
- package/src/tests/heartbeat-orphan-resume.test.ts +48 -0
- package/src/tests/human-free-flag.test.ts +279 -0
- package/src/tests/human-free-oracle.ts +57 -0
- package/src/tests/human-free-reclassify.test.ts +669 -0
- package/src/tests/migration-046-budgets.test.ts +3 -3
- package/src/tests/model-catalog-migration.test.ts +134 -0
- package/src/tests/model-catalog-table.test.ts +249 -0
- package/src/tests/model-catalog-write-access.test.ts +387 -0
- package/src/tests/model-tier-resolution.test.ts +519 -0
- package/src/tests/models-catalog.test.ts +1 -1
- package/src/tests/multi-runtime-enabled-stats.test.ts +65 -1
- package/src/tests/package-publish.test.ts +137 -36
- package/src/tests/pricing-routes.test.ts +2 -2
- package/src/tests/rbac-engine.test.ts +42 -2
- package/src/tests/reasoning-effort.test.ts +109 -0
- package/src/tests/refresh-modelsdev-carryforward.test.ts +5 -5
- package/src/tests/runtime-model-catalog.test.ts +86 -0
- package/src/tests/script-connections.test.ts +5 -0
- package/src/tests/seed-scripts.test.ts +89 -0
- package/src/tests/session-costs.test.ts +14 -11
- package/src/tests/slack-message-text.test.ts +101 -0
- package/src/tests/slack-thread-buffer-context.test.ts +93 -0
- package/src/tests/task-cascade-fail.test.ts +148 -2
- package/src/tests/tool-annotations.test.ts +1 -1
- package/src/tests/usage-cache.test.ts +166 -0
- package/src/tests/workflow-executors.test.ts +5 -2
- package/src/tests/workflow-system-one-decision-review.test.ts +1021 -0
- package/src/tests/workflow-system-one-decision-save-warnings.test.ts +365 -0
- package/src/tests/workflow-system-one-decision.test.ts +2063 -0
- package/src/tools/model-catalog/index.ts +137 -0
- package/src/tools/schedules/create-schedule.ts +16 -1
- package/src/tools/schedules/patch-schedule.ts +17 -0
- package/src/tools/schedules/update-schedule.ts +17 -0
- package/src/tools/send-task.ts +14 -0
- package/src/tools/task-action.ts +12 -0
- package/src/tools/tool-config.ts +3 -1
- package/src/tools/workflows/create-workflow.ts +9 -2
- package/src/tools/workflows/patch-workflow-node.ts +17 -7
- package/src/tools/workflows/patch-workflow.ts +9 -2
- package/src/tools/workflows/update-workflow.ts +9 -2
- package/src/types.ts +30 -1
- package/src/utils/context-window.ts +47 -53
- package/src/utils/harness-cli-version.ts +115 -0
- package/src/utils/harness-model-error.ts +33 -0
- package/src/utils/runtime-model-catalog.ts +137 -0
- package/src/workflows/approval-resolution.ts +39 -0
- package/src/workflows/definition.ts +7 -0
- package/src/workflows/engine.ts +69 -1
- package/src/workflows/executors/agent-task.ts +9 -0
- package/src/workflows/executors/base.ts +53 -0
- package/src/workflows/executors/human-in-the-loop.ts +8 -8
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/system-one-decision.ts +1232 -0
- package/src/workflows/executors/system-one-providers.ts +301 -0
- package/src/workflows/executors/workflow-llm.ts +13 -1
- package/src/workflows/readiness.ts +86 -0
- package/src/workflows/recovery.ts +12 -12
- package/src/workflows/resume.ts +23 -9
- package/templates/schedules/daily-blocker-digest/content.md +4 -5
- package/templates/skills/agent-browser/SKILL.md +6 -5
- package/templates/skills/agent-browser/content.md +6 -5
- package/templates/skills/heartbeat-runbook/config.json +1 -1
- package/templates/skills/heartbeat-runbook/content.md +2 -2
- package/templates/skills/swarm-scripts/SKILL.md +1 -1
- package/templates/skills/swarm-scripts/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +81 -1
- package/templates/workflows/claude-code-changelog-watch/content.md +2 -0
- package/templates/workflows/pr-review-status-sweep/content.md +2 -0
- package/dist/claude-sdk-session-t442qz6f.js +0 -39108
- package/dist/cli-0xjkj1j7.js +0 -61
- package/dist/cli-65hjmev5.js +0 -1141
- package/dist/cli-aw1jwp1r.js +0 -14075
- package/dist/cli-h967nb6r.js +0 -6603
- package/dist/cli-vapyk8sw.js +0 -20
- package/dist/cli-ws6rqycm.js +0 -247
- package/dist/pricing-refresh-v1cbr9te.js +0 -27
- package/src/providers/modelsdev-reasoning.json +0 -6453
- package/dist/{codex-login-x3jk4etn.js → codex-login-he9vrn90.js} +3 -3
- package/dist/{index-f24115ge.js → index-2wsdsnmm.js} +3 -3
|
@@ -1691,32 +1691,35 @@ describe("Session Costs API", () => {
|
|
|
1691
1691
|
try {
|
|
1692
1692
|
await getAttributionByPerson({ startDate: "2026-08-19", endDate: "2026-08-19" });
|
|
1693
1693
|
const call = querySpy.mock.calls.find(([sql]) =>
|
|
1694
|
-
String(sql).includes("task_tree(rootId, taskId
|
|
1694
|
+
String(sql).includes("task_tree(rootId, taskId)"),
|
|
1695
1695
|
);
|
|
1696
1696
|
const sql = String(call?.[0] ?? "");
|
|
1697
|
-
const seed = sql.slice(0, sql.indexOf("task_tree(rootId, taskId
|
|
1697
|
+
const seed = sql.slice(0, sql.indexOf("task_tree(rootId, taskId)"));
|
|
1698
1698
|
|
|
1699
1699
|
expect(seed).toContain("selected_roots");
|
|
1700
1700
|
expect(seed).toContain("t.requestedByUserId IS NOT NULL");
|
|
1701
1701
|
expect(seed).toContain("t.createdAt >= ?");
|
|
1702
1702
|
expect(seed).toContain("t.createdAt < ?");
|
|
1703
1703
|
expect(seed).toContain("t.parentTaskId IS NULL");
|
|
1704
|
+
expect(seed).toContain("t.isHumanFree = 0");
|
|
1704
1705
|
expect(seed).not.toContain("human_free_tasks");
|
|
1705
1706
|
expect(sql).toMatch(
|
|
1706
|
-
/task_tree\(rootId, taskId
|
|
1707
|
+
/task_tree\(rootId, taskId\) AS \(\s*SELECT id, id\s*FROM selected_roots/,
|
|
1707
1708
|
);
|
|
1709
|
+
// The recursion carries ids only; a task's output is read by id afterwards.
|
|
1710
|
+
expect(sql).not.toContain("task_tree(rootId, taskId, output)");
|
|
1708
1711
|
expect(sql.match(/\?/g)).toHaveLength(2);
|
|
1709
1712
|
|
|
1713
|
+
// Reach reads the stored human-free flag; it walks no ancestry.
|
|
1710
1714
|
const reachCall = querySpy.mock.calls.find(([preparedSql]) =>
|
|
1711
|
-
String(preparedSql).includes("
|
|
1715
|
+
String(preparedSql).includes("COUNT(DISTINCT t.agentId)"),
|
|
1712
1716
|
);
|
|
1713
1717
|
const reachSql = String(reachCall?.[0] ?? "");
|
|
1714
|
-
|
|
1715
|
-
expect(
|
|
1716
|
-
expect(
|
|
1717
|
-
expect(
|
|
1718
|
-
expect(
|
|
1719
|
-
expect(reachSql).toContain("JOIN task_ancestry child ON parent.id = child.parentTaskId");
|
|
1718
|
+
expect(reachSql).toContain("t.requestedByUserId IS NOT NULL");
|
|
1719
|
+
expect(reachSql).toContain("t.createdAt >= ?");
|
|
1720
|
+
expect(reachSql).toContain("t.createdAt < ?");
|
|
1721
|
+
expect(reachSql).toContain("t.isHumanFree = 0");
|
|
1722
|
+
expect(reachSql).not.toContain("RECURSIVE");
|
|
1720
1723
|
expect(reachSql.match(/\?/g)).toHaveLength(2);
|
|
1721
1724
|
} finally {
|
|
1722
1725
|
querySpy.mockRestore();
|
|
@@ -2107,7 +2110,7 @@ describe("Session Costs API", () => {
|
|
|
2107
2110
|
try {
|
|
2108
2111
|
await getAttributionByPerson({});
|
|
2109
2112
|
const call = querySpy.mock.calls.find(([sql]) =>
|
|
2110
|
-
String(sql).includes("task_tree(rootId, taskId
|
|
2113
|
+
String(sql).includes("task_tree(rootId, taskId)"),
|
|
2111
2114
|
);
|
|
2112
2115
|
const sql = String(call?.[0] ?? "");
|
|
2113
2116
|
|
|
@@ -240,6 +240,107 @@ describe("extractSlackMessageText", () => {
|
|
|
240
240
|
expect(result).toContain("Details here");
|
|
241
241
|
});
|
|
242
242
|
|
|
243
|
+
// Shape of a real multi-line human message: Slack sends mrkdwn `text` and a
|
|
244
|
+
// rich_text block with one element per styled run, mention and link.
|
|
245
|
+
const multiLineHumanMessage = {
|
|
246
|
+
text: "<@U0A1B2C3D> can you check the deploy?\n*It failed* twice & the logs say:\n• timeout on <https://ci.example.com/run/42|run 42>\n• retry :fire:",
|
|
247
|
+
blocks: [
|
|
248
|
+
{
|
|
249
|
+
type: "rich_text",
|
|
250
|
+
elements: [
|
|
251
|
+
{
|
|
252
|
+
type: "rich_text_section",
|
|
253
|
+
elements: [
|
|
254
|
+
{ type: "user", user_id: "U0A1B2C3D" },
|
|
255
|
+
{ type: "text", text: " can you check the deploy?\n" },
|
|
256
|
+
{ type: "text", text: "It failed", style: { bold: true } },
|
|
257
|
+
{ type: "text", text: " twice & the logs say:\n" },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
type: "rich_text_list",
|
|
262
|
+
style: "bullet",
|
|
263
|
+
elements: [
|
|
264
|
+
{
|
|
265
|
+
type: "rich_text_section",
|
|
266
|
+
elements: [
|
|
267
|
+
{ type: "text", text: "timeout on " },
|
|
268
|
+
{ type: "link", url: "https://ci.example.com/run/42", text: "run 42" },
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
type: "rich_text_section",
|
|
273
|
+
elements: [
|
|
274
|
+
{ type: "text", text: "retry " },
|
|
275
|
+
{ type: "emoji", name: "fire" },
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
test("multi-line rich_text human message renders once, as the top-level text", () => {
|
|
286
|
+
const result = extractSlackMessageText(multiLineHumanMessage);
|
|
287
|
+
expect(result).toBe(multiLineHumanMessage.text);
|
|
288
|
+
expect(result.split("can you check the deploy?").length - 1).toBe(1);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("single-line rich_text with a mention renders once and keeps the mention", () => {
|
|
292
|
+
const msg = {
|
|
293
|
+
text: "hey <@U0A1B2C3D> ship it",
|
|
294
|
+
blocks: [
|
|
295
|
+
{
|
|
296
|
+
type: "rich_text",
|
|
297
|
+
elements: [
|
|
298
|
+
{
|
|
299
|
+
type: "rich_text_section",
|
|
300
|
+
elements: [
|
|
301
|
+
{ type: "text", text: "hey " },
|
|
302
|
+
{ type: "user", user_id: "U0A1B2C3D" },
|
|
303
|
+
{ type: "text", text: " ship it" },
|
|
304
|
+
],
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
expect(extractSlackMessageText(msg)).toBe("hey <@U0A1B2C3D> ship it");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("negative control: rich_text that differs from top-level text keeps both", () => {
|
|
314
|
+
const msg = {
|
|
315
|
+
text: "Deploy summary",
|
|
316
|
+
blocks: [
|
|
317
|
+
{
|
|
318
|
+
type: "rich_text",
|
|
319
|
+
elements: [
|
|
320
|
+
{
|
|
321
|
+
type: "rich_text_section",
|
|
322
|
+
elements: [{ type: "text", text: "Deploy failed on step 3\nRolled back to v1.2" }],
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
};
|
|
328
|
+
expect(extractSlackMessageText(msg)).toBe(
|
|
329
|
+
"Deploy summary\nDeploy failed on step 3\nRolled back to v1.2",
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("negative control: rich_text dedupe leaves non-rich_text blocks in place", () => {
|
|
334
|
+
const msg = {
|
|
335
|
+
...multiLineHumanMessage,
|
|
336
|
+
blocks: [
|
|
337
|
+
...multiLineHumanMessage.blocks,
|
|
338
|
+
{ type: "context", elements: [{ type: "mrkdwn", text: "Sent from CI bot" }] },
|
|
339
|
+
],
|
|
340
|
+
};
|
|
341
|
+
expect(extractSlackMessageText(msg)).toBe(`${multiLineHumanMessage.text}\nSent from CI bot`);
|
|
342
|
+
});
|
|
343
|
+
|
|
243
344
|
test("context block elements are captured", () => {
|
|
244
345
|
const msg = {
|
|
245
346
|
text: "",
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { unlinkSync } from "node:fs";
|
|
3
|
+
import { closeDb, createAgent, getLatestActiveTaskInThread, initDb } from "../be/db";
|
|
4
|
+
|
|
5
|
+
process.env.SLACK_RENDER_V2 = "false";
|
|
6
|
+
process.env.STEERING_ENABLED = "false";
|
|
7
|
+
|
|
8
|
+
const TEST_DB_PATH = "./test-slack-thread-buffer-context.sqlite";
|
|
9
|
+
|
|
10
|
+
// Replies Slack returns for the thread: the root, one earlier human reply, and
|
|
11
|
+
// the buffered follow-up itself (conversations.replies includes it by the time
|
|
12
|
+
// the buffer flushes).
|
|
13
|
+
const threadMessages = [
|
|
14
|
+
{ ts: "7000.0001", user: "U0ROOT", text: "original request" },
|
|
15
|
+
{ ts: "7000.0005", user: "U0EARLIER", text: "earlier reply that is only context" },
|
|
16
|
+
{
|
|
17
|
+
ts: "7000.0010",
|
|
18
|
+
user: "U0FOLLOWUP",
|
|
19
|
+
text: "please also fix the retry\nand add a test for it",
|
|
20
|
+
blocks: [
|
|
21
|
+
{
|
|
22
|
+
type: "rich_text",
|
|
23
|
+
elements: [
|
|
24
|
+
{
|
|
25
|
+
type: "rich_text_section",
|
|
26
|
+
elements: [{ type: "text", text: "please also fix the retry\nand add a test for it" }],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const realSlackApp = await import("../slack/app");
|
|
35
|
+
mock.module("../slack/app", () => ({
|
|
36
|
+
...realSlackApp,
|
|
37
|
+
getSlackApp: () => ({
|
|
38
|
+
client: {
|
|
39
|
+
conversations: { replies: async () => ({ ok: true, messages: threadMessages }) },
|
|
40
|
+
users: { info: async () => ({ ok: true, user: { profile: {} } }) },
|
|
41
|
+
chat: { postMessage: async () => ({ ok: true, ts: "7000.0099" }) },
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const { instantFlush, bufferThreadMessage } = await import("../slack/thread-buffer");
|
|
47
|
+
|
|
48
|
+
beforeAll(async () => {
|
|
49
|
+
initDb(TEST_DB_PATH);
|
|
50
|
+
await createAgent({ name: "lead-agent", isLead: true, status: "idle", capabilities: [] });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterAll(() => {
|
|
54
|
+
closeDb();
|
|
55
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
56
|
+
try {
|
|
57
|
+
unlinkSync(`${TEST_DB_PATH}${suffix}`);
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("buffered follow-up task description", () => {
|
|
63
|
+
test("carries each buffered message once, outside <thread_context>", async () => {
|
|
64
|
+
const channelId = "C700";
|
|
65
|
+
const threadTs = "7000.0001";
|
|
66
|
+
bufferThreadMessage(
|
|
67
|
+
channelId,
|
|
68
|
+
threadTs,
|
|
69
|
+
"please also fix the retry\nand add a test for it",
|
|
70
|
+
"U0FOLLOWUP",
|
|
71
|
+
"7000.0010",
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
await instantFlush(`${channelId}:${threadTs}`);
|
|
75
|
+
|
|
76
|
+
const task = await getLatestActiveTaskInThread(channelId, threadTs);
|
|
77
|
+
expect(task).not.toBeNull();
|
|
78
|
+
const description = task!.task;
|
|
79
|
+
const context = description.slice(
|
|
80
|
+
description.indexOf("<thread_context>"),
|
|
81
|
+
description.indexOf("</thread_context>"),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// The follow-up appears exactly once, in the buffered section.
|
|
85
|
+
expect(description.split("please also fix the retry").length - 1).toBe(1);
|
|
86
|
+
expect(context).not.toContain("please also fix the retry");
|
|
87
|
+
expect(description).toContain("1 message(s) buffered");
|
|
88
|
+
|
|
89
|
+
// Negative control: messages that are not buffered stay in the context.
|
|
90
|
+
expect(context).toContain("original request");
|
|
91
|
+
expect(context).toContain("earlier reply that is only context");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlinkSync } from "node:fs";
|
|
3
3
|
import {
|
|
4
|
+
backfillSupersedeTaskResumeTaskId,
|
|
4
5
|
cancelTask,
|
|
5
6
|
cascadeFailDependents,
|
|
6
7
|
closeDb,
|
|
@@ -230,7 +231,7 @@ describe("cascadeFailDependents", () => {
|
|
|
230
231
|
expect(childAfter!.failureReason).toContain("was cancelled");
|
|
231
232
|
});
|
|
232
233
|
|
|
233
|
-
test("supersedeTask
|
|
234
|
+
test("supersedeTask with a resume id re-points never-started dependents", async () => {
|
|
234
235
|
const agent = await createAgent({
|
|
235
236
|
name: "cascade-worker-6",
|
|
236
237
|
isLead: false,
|
|
@@ -239,19 +240,164 @@ describe("cascadeFailDependents", () => {
|
|
|
239
240
|
});
|
|
240
241
|
|
|
241
242
|
const parent = await createTaskExtended("Parent supersede", { agentId: agent.id });
|
|
243
|
+
const other = await createTaskExtended("Other dependency", { agentId: agent.id });
|
|
242
244
|
const child = await createTaskExtended("Child of superseded", {
|
|
245
|
+
agentId: agent.id,
|
|
246
|
+
dependsOn: [parent.id, other.id],
|
|
247
|
+
});
|
|
248
|
+
const resume = await createTaskExtended("Resume of parent", { agentId: agent.id });
|
|
249
|
+
|
|
250
|
+
await startTask(parent.id);
|
|
251
|
+
await supersedeTask(parent.id, { reason: "context limit", resumeTaskId: resume.id });
|
|
252
|
+
|
|
253
|
+
const childAfter = await getTaskById(child.id);
|
|
254
|
+
expect(childAfter!.status).toBe("pending");
|
|
255
|
+
expect(childAfter!.dependsOn).toEqual([resume.id, other.id]);
|
|
256
|
+
|
|
257
|
+
const log = await getDbClient().get<{ oldValue: string; newValue: string; metadata: string }>(
|
|
258
|
+
"SELECT oldValue, newValue, metadata FROM agent_log WHERE taskId = ? AND eventType = 'task_dependency_repointed'",
|
|
259
|
+
[child.id],
|
|
260
|
+
);
|
|
261
|
+
expect(log?.oldValue).toBe(parent.id);
|
|
262
|
+
expect(log?.newValue).toBe(resume.id);
|
|
263
|
+
expect(JSON.parse(log!.metadata).reason).toBe("supersede_resume");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("supersedeTask without a resume id defers dependents until the backfill", async () => {
|
|
267
|
+
const agent = await createAgent({
|
|
268
|
+
name: "cascade-worker-6b",
|
|
269
|
+
isLead: false,
|
|
270
|
+
status: "idle",
|
|
271
|
+
capabilities: [],
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const parent = await createTaskExtended("Parent supersede late resume", { agentId: agent.id });
|
|
275
|
+
const child = await createTaskExtended("Child of late-resumed", {
|
|
276
|
+
agentId: agent.id,
|
|
277
|
+
dependsOn: [parent.id],
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await startTask(parent.id);
|
|
281
|
+
await supersedeTask(parent.id, { reason: "graceful_shutdown", resumeTaskId: null });
|
|
282
|
+
|
|
283
|
+
// The window between supersede and resume creation must not fail it.
|
|
284
|
+
expect((await getTaskById(child.id))!.status).toBe("pending");
|
|
285
|
+
|
|
286
|
+
const resume = await createTaskExtended("Late resume", { agentId: agent.id });
|
|
287
|
+
await backfillSupersedeTaskResumeTaskId(parent.id, resume.id);
|
|
288
|
+
|
|
289
|
+
const childAfter = await getTaskById(child.id);
|
|
290
|
+
expect(childAfter!.status).toBe("pending");
|
|
291
|
+
expect(childAfter!.dependsOn).toEqual([resume.id]);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("supersede settle does not rewrite a dependent that already started", async () => {
|
|
295
|
+
const agent = await createAgent({
|
|
296
|
+
name: "cascade-worker-6c",
|
|
297
|
+
isLead: false,
|
|
298
|
+
status: "idle",
|
|
299
|
+
capabilities: [],
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const parent = await createTaskExtended("Parent supersede started dep", {
|
|
303
|
+
agentId: agent.id,
|
|
304
|
+
});
|
|
305
|
+
const child = await createTaskExtended("Started child", {
|
|
243
306
|
agentId: agent.id,
|
|
244
307
|
dependsOn: [parent.id],
|
|
245
308
|
});
|
|
309
|
+
const resume = await createTaskExtended("Resume for started dep", { agentId: agent.id });
|
|
246
310
|
|
|
247
311
|
await startTask(parent.id);
|
|
248
|
-
await
|
|
312
|
+
await startTask(child.id);
|
|
313
|
+
await supersedeTask(parent.id, { reason: "context limit", resumeTaskId: resume.id });
|
|
249
314
|
|
|
250
315
|
const childAfter = await getTaskById(child.id);
|
|
316
|
+
expect(childAfter!.dependsOn).toEqual([parent.id]);
|
|
251
317
|
expect(childAfter!.status).toBe("failed");
|
|
252
318
|
expect(childAfter!.failureReason).toContain("was superseded");
|
|
253
319
|
});
|
|
254
320
|
|
|
321
|
+
test("concurrent settlements of a shared dependent keep both replacements", async () => {
|
|
322
|
+
const agent = await createAgent({
|
|
323
|
+
name: "cascade-worker-6d",
|
|
324
|
+
isLead: false,
|
|
325
|
+
status: "idle",
|
|
326
|
+
capabilities: [],
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const a = await createTaskExtended("Shared parent A", { agentId: agent.id });
|
|
330
|
+
const b = await createTaskExtended("Shared parent B", { agentId: agent.id });
|
|
331
|
+
const child = await createTaskExtended("Depends on A and B", {
|
|
332
|
+
agentId: agent.id,
|
|
333
|
+
dependsOn: [a.id, b.id],
|
|
334
|
+
});
|
|
335
|
+
const ra = await createTaskExtended("Resume of A", { agentId: agent.id });
|
|
336
|
+
const rb = await createTaskExtended("Resume of B", { agentId: agent.id });
|
|
337
|
+
|
|
338
|
+
for (const parent of [a, b]) {
|
|
339
|
+
await startTask(parent.id);
|
|
340
|
+
await supersedeTask(parent.id, { reason: "crash_recovery", resumeTaskId: null });
|
|
341
|
+
}
|
|
342
|
+
await Promise.all([
|
|
343
|
+
backfillSupersedeTaskResumeTaskId(a.id, ra.id),
|
|
344
|
+
backfillSupersedeTaskResumeTaskId(b.id, rb.id),
|
|
345
|
+
]);
|
|
346
|
+
|
|
347
|
+
const childAfter = await getTaskById(child.id);
|
|
348
|
+
expect(childAfter!.status).toBe("pending");
|
|
349
|
+
expect(childAfter!.dependsOn).toEqual([ra.id, rb.id]);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// A dependent's status changes right after any read the settlement makes.
|
|
353
|
+
// An accept out of `offered` is still never-started and must be re-pointed;
|
|
354
|
+
// the claim-into-in_progress guard is covered by the started-dependent test.
|
|
355
|
+
for (const moveTo of ["reviewing", "pending"] as const) {
|
|
356
|
+
test(`settlement racing offered -> ${moveTo} still re-points the dependent`, async () => {
|
|
357
|
+
const agent = await createAgent({
|
|
358
|
+
name: `cascade-worker-6e-${moveTo}`,
|
|
359
|
+
isLead: false,
|
|
360
|
+
status: "idle",
|
|
361
|
+
capabilities: [],
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const parent = await createTaskExtended("Parent of offered dep", { agentId: agent.id });
|
|
365
|
+
const child = await createTaskExtended("Offered child", {
|
|
366
|
+
agentId: agent.id,
|
|
367
|
+
dependsOn: [parent.id],
|
|
368
|
+
});
|
|
369
|
+
const resume = await createTaskExtended("Resume for offered dep", { agentId: agent.id });
|
|
370
|
+
const client = getDbClient();
|
|
371
|
+
await client.run("UPDATE agent_tasks SET status = 'offered' WHERE id = ?", [child.id]);
|
|
372
|
+
await startTask(parent.id);
|
|
373
|
+
await supersedeTask(parent.id, { reason: "crash_recovery", resumeTaskId: null });
|
|
374
|
+
|
|
375
|
+
const query = client.query.bind(client);
|
|
376
|
+
let raced = false;
|
|
377
|
+
client.query = (async (sql: string, params?: unknown[]) => {
|
|
378
|
+
const rows = (await query(sql, params as never)) as { id?: string; status?: string }[];
|
|
379
|
+
if (!raced && rows.some((r) => r.id === child.id && r.status === "offered")) {
|
|
380
|
+
raced = true;
|
|
381
|
+
await client.run("UPDATE agent_tasks SET status = ? WHERE id = ?", [moveTo, child.id]);
|
|
382
|
+
}
|
|
383
|
+
return rows;
|
|
384
|
+
}) as typeof client.query;
|
|
385
|
+
try {
|
|
386
|
+
await backfillSupersedeTaskResumeTaskId(parent.id, resume.id);
|
|
387
|
+
} finally {
|
|
388
|
+
client.query = query;
|
|
389
|
+
}
|
|
390
|
+
// No read to race (single-statement settlement): the accept lands after.
|
|
391
|
+
if (!raced) {
|
|
392
|
+
await client.run("UPDATE agent_tasks SET status = ? WHERE id = ?", [moveTo, child.id]);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const childAfter = await getTaskById(child.id);
|
|
396
|
+
expect(childAfter!.status).toBe(moveTo);
|
|
397
|
+
expect(childAfter!.dependsOn).toEqual([resume.id]);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
255
401
|
test("wide fan-out: multiple dependents all cascade-failed", async () => {
|
|
256
402
|
const agent = await createAgent({
|
|
257
403
|
name: "cascade-worker-7",
|
|
@@ -378,7 +378,7 @@ describe("Tool Annotations & Classification", () => {
|
|
|
378
378
|
// Includes 11 skill tools, 7 MCP server tools, reusable script tools, and the
|
|
379
379
|
// native Kapso/WhatsApp tools (register/unregister number + send/reply message).
|
|
380
380
|
expect(count).toBeGreaterThanOrEqual(45);
|
|
381
|
-
expect(count).toBeLessThanOrEqual(
|
|
381
|
+
expect(count).toBeLessThanOrEqual(151);
|
|
382
382
|
});
|
|
383
383
|
|
|
384
384
|
test("core tools are fewer than deferred tools", () => {
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, setSystemTime, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
cachedUsageReport,
|
|
4
|
+
clearUsageCache,
|
|
5
|
+
USAGE_CACHE_MAX_STALE_MS,
|
|
6
|
+
USAGE_CACHE_TTL_MS,
|
|
7
|
+
} from "../http/usage-cache";
|
|
8
|
+
|
|
9
|
+
const T0 = new Date("2026-09-29T12:00:00.000Z");
|
|
10
|
+
|
|
11
|
+
function loader(values: unknown[]) {
|
|
12
|
+
let calls = 0;
|
|
13
|
+
const load = async () => {
|
|
14
|
+
const value = values[Math.min(calls, values.length - 1)];
|
|
15
|
+
calls++;
|
|
16
|
+
return value;
|
|
17
|
+
};
|
|
18
|
+
return { load, calls: () => calls };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The background refresh is a promise chain; let it settle.
|
|
22
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
23
|
+
|
|
24
|
+
describe("cachedUsageReport", () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
clearUsageCache();
|
|
27
|
+
setSystemTime(T0);
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
clearUsageCache();
|
|
31
|
+
setSystemTime();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("serves a fresh entry without loading again", async () => {
|
|
35
|
+
const source = loader(["a", "b"]);
|
|
36
|
+
expect(await cachedUsageReport("k", source.load)).toBe("a");
|
|
37
|
+
setSystemTime(new Date(T0.getTime() + USAGE_CACHE_TTL_MS - 1));
|
|
38
|
+
expect(await cachedUsageReport("k", source.load)).toBe("a");
|
|
39
|
+
expect(source.calls()).toBe(1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("keeps keys apart", async () => {
|
|
43
|
+
const source = loader(["a", "b"]);
|
|
44
|
+
expect(await cachedUsageReport("one", source.load)).toBe("a");
|
|
45
|
+
expect(await cachedUsageReport("two", source.load)).toBe("b");
|
|
46
|
+
expect(source.calls()).toBe(2);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("past the bucket it answers with the stale value and refreshes once in the background", async () => {
|
|
50
|
+
const source = loader(["old", "new"]);
|
|
51
|
+
await cachedUsageReport("k", source.load);
|
|
52
|
+
|
|
53
|
+
setSystemTime(new Date(T0.getTime() + USAGE_CACHE_TTL_MS + 1));
|
|
54
|
+
expect(await cachedUsageReport("k", source.load)).toBe("old");
|
|
55
|
+
expect(await cachedUsageReport("k", source.load)).toBe("old");
|
|
56
|
+
await settle();
|
|
57
|
+
// One refresh served both stale reads.
|
|
58
|
+
expect(source.calls()).toBe(2);
|
|
59
|
+
expect(await cachedUsageReport("k", source.load)).toBe("new");
|
|
60
|
+
expect(source.calls()).toBe(2);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("a stale read does not wait for the refresh", async () => {
|
|
64
|
+
let release: (value: string) => void = () => {};
|
|
65
|
+
const slow = new Promise<string>((resolve) => {
|
|
66
|
+
release = resolve;
|
|
67
|
+
});
|
|
68
|
+
await cachedUsageReport("k", async () => "old");
|
|
69
|
+
|
|
70
|
+
setSystemTime(new Date(T0.getTime() + USAGE_CACHE_TTL_MS + 1));
|
|
71
|
+
expect(await cachedUsageReport("k", () => slow)).toBe("old");
|
|
72
|
+
release("new");
|
|
73
|
+
await settle();
|
|
74
|
+
expect(await cachedUsageReport("k", async () => "unused")).toBe("new");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("blocks on a load once an entry is older than the stale limit", async () => {
|
|
78
|
+
const source = loader(["old", "new"]);
|
|
79
|
+
await cachedUsageReport("k", source.load);
|
|
80
|
+
|
|
81
|
+
setSystemTime(new Date(T0.getTime() + USAGE_CACHE_MAX_STALE_MS + 1));
|
|
82
|
+
expect(await cachedUsageReport("k", source.load)).toBe("new");
|
|
83
|
+
expect(source.calls()).toBe(2);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("concurrent misses share one load", async () => {
|
|
87
|
+
let calls = 0;
|
|
88
|
+
const load = async () => {
|
|
89
|
+
calls++;
|
|
90
|
+
await settle();
|
|
91
|
+
return "shared";
|
|
92
|
+
};
|
|
93
|
+
const results = await Promise.all([
|
|
94
|
+
cachedUsageReport("k", load),
|
|
95
|
+
cachedUsageReport("k", load),
|
|
96
|
+
cachedUsageReport("k", load),
|
|
97
|
+
]);
|
|
98
|
+
expect(results).toEqual(["shared", "shared", "shared"]);
|
|
99
|
+
expect(calls).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("a failed background refresh keeps the stale value and the next read retries", async () => {
|
|
103
|
+
await cachedUsageReport("k", async () => "old");
|
|
104
|
+
setSystemTime(new Date(T0.getTime() + USAGE_CACHE_TTL_MS + 1));
|
|
105
|
+
|
|
106
|
+
const original = console.error;
|
|
107
|
+
console.error = () => {};
|
|
108
|
+
try {
|
|
109
|
+
expect(
|
|
110
|
+
await cachedUsageReport("k", async () => {
|
|
111
|
+
throw new Error("db busy");
|
|
112
|
+
}),
|
|
113
|
+
).toBe("old");
|
|
114
|
+
await settle();
|
|
115
|
+
} finally {
|
|
116
|
+
console.error = original;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
expect(await cachedUsageReport("k", async () => "retried")).toBe("old");
|
|
120
|
+
await settle();
|
|
121
|
+
expect(await cachedUsageReport("k", async () => "unused")).toBe("retried");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("a load with no stale value to fall back on rejects to the caller", async () => {
|
|
125
|
+
await expect(
|
|
126
|
+
cachedUsageReport("k", async () => {
|
|
127
|
+
throw new Error("db busy");
|
|
128
|
+
}),
|
|
129
|
+
).rejects.toThrow("db busy");
|
|
130
|
+
// The failure is not cached.
|
|
131
|
+
expect(await cachedUsageReport("k", async () => "ok")).toBe("ok");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("clearing drops entries, and a load already running cannot write back", async () => {
|
|
135
|
+
let release: (value: string) => void = () => {};
|
|
136
|
+
const inFlight = new Promise<string>((resolve) => {
|
|
137
|
+
release = resolve;
|
|
138
|
+
});
|
|
139
|
+
const pending = cachedUsageReport("k", () => inFlight);
|
|
140
|
+
|
|
141
|
+
clearUsageCache();
|
|
142
|
+
release("before-clear");
|
|
143
|
+
expect(await pending).toBe("before-clear");
|
|
144
|
+
|
|
145
|
+
// Nothing was stored, so the next read loads again.
|
|
146
|
+
expect(await cachedUsageReport("k", async () => "after-clear")).toBe("after-clear");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("evicts the oldest entry past the size cap", async () => {
|
|
150
|
+
for (let i = 0; i < 100; i++) await cachedUsageReport(`k${i}`, async () => `v${i}`);
|
|
151
|
+
await cachedUsageReport("extra", async () => "extra");
|
|
152
|
+
|
|
153
|
+
let reloaded = false;
|
|
154
|
+
await cachedUsageReport("k0", async () => {
|
|
155
|
+
reloaded = true;
|
|
156
|
+
return "v0-again";
|
|
157
|
+
});
|
|
158
|
+
expect(reloaded).toBe(true);
|
|
159
|
+
let survivorReloaded = false;
|
|
160
|
+
await cachedUsageReport("k99", async () => {
|
|
161
|
+
survivorReloaded = true;
|
|
162
|
+
return "unused";
|
|
163
|
+
});
|
|
164
|
+
expect(survivorReloaded).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -984,7 +984,7 @@ describe("ValidateExecutor", () => {
|
|
|
984
984
|
// ─── Registry Wiring ─────────────────────────────────────────
|
|
985
985
|
|
|
986
986
|
describe("createExecutorRegistry", () => {
|
|
987
|
-
test("registers all
|
|
987
|
+
test("registers all 13 executors (9 instant + 4 async)", () => {
|
|
988
988
|
const registry = createExecutorRegistry(mockDeps);
|
|
989
989
|
const types = registry.types();
|
|
990
990
|
|
|
@@ -992,6 +992,7 @@ describe("createExecutorRegistry", () => {
|
|
|
992
992
|
expect(types).toContain("code-match");
|
|
993
993
|
expect(types).toContain("notify");
|
|
994
994
|
expect(types).toContain("raw-llm");
|
|
995
|
+
expect(types).toContain("system-one-decision");
|
|
995
996
|
expect(types).toContain("script");
|
|
996
997
|
expect(types).toContain("swarm-script");
|
|
997
998
|
expect(types).toContain("vcs");
|
|
@@ -1000,7 +1001,7 @@ describe("createExecutorRegistry", () => {
|
|
|
1000
1001
|
expect(types).toContain("foreach");
|
|
1001
1002
|
expect(types).toContain("human-in-the-loop");
|
|
1002
1003
|
expect(types).toContain("wait");
|
|
1003
|
-
expect(types).toHaveLength(
|
|
1004
|
+
expect(types).toHaveLength(13);
|
|
1004
1005
|
});
|
|
1005
1006
|
|
|
1006
1007
|
test("instant executors have mode instant, async executors have mode async", () => {
|
|
@@ -1022,6 +1023,8 @@ describe("createExecutorRegistry", () => {
|
|
|
1022
1023
|
expect(registry.get("foreach").mode).toBe("async");
|
|
1023
1024
|
expect(registry.get("human-in-the-loop").mode).toBe("async");
|
|
1024
1025
|
expect(registry.get("wait").mode).toBe("async");
|
|
1026
|
+
// Answers at once, but waits when `humanReview` sends an answer to a person.
|
|
1027
|
+
expect(registry.get("system-one-decision").mode).toBe("async");
|
|
1025
1028
|
});
|
|
1026
1029
|
|
|
1027
1030
|
test("get() retrieves the correct executor by type", () => {
|