@desplega.ai/agent-swarm 2.0.0 → 2.1.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-sj2g0m84.js → acp-adapter-kr4abrsk.js} +10 -10
- package/dist/{actions-fegk44gr.js → actions-eh4g6w2r.js} +27 -27
- package/dist/{amp-adapter-ad8ft89x.js → amp-adapter-7h7wdt8q.js} +5 -5
- package/dist/{app-kfvgbvzy.js → app-7k8s9w5p.js} +9 -9
- package/dist/{assistant-z7zdy9ja.js → assistant-wswt2y7p.js} +28 -28
- package/dist/{boot-reembed-j8xa181e.js → boot-reembed-9apm9qw6.js} +7 -7
- package/dist/{boot-reembed-5c2z0mbe.js → boot-reembed-whzbdthj.js} +7 -7
- package/dist/{boot-scrub-sweep-hnkn3rgt.js → boot-scrub-sweep-ndzmvmgt.js} +8 -8
- package/dist/{boot-seeds-kwjq4d2z.js → boot-seeds-e9sta0yt.js} +13 -11
- package/dist/{claude-adapter-9vmvpvgd.js → claude-adapter-4mqd3r6r.js} +11 -11
- package/dist/{claude-managed-adapter-k9qae05j.js → claude-managed-adapter-9wswb52m.js} +3 -3
- package/dist/{claude-managed-setup-q54zx5q2.js → claude-managed-setup-tpe1effx.js} +4 -4
- package/dist/{claude-sdk-session-9jye6cxr.js → claude-sdk-session-q49ghmh9.js} +10340 -10197
- package/dist/{cli-ke896fcf.js → cli-01xvj1rq.js} +10 -10
- package/dist/{cli-tr4sxp68.js → cli-035vekcw.js} +2 -2
- package/dist/{cli-z5jjpaxw.js → cli-0pd8hzhc.js} +3 -3
- package/dist/{cli-n5vwg842.js → cli-0veb92a3.js} +6 -1
- package/dist/{cli-wayw93nt.js → cli-10jesr25.js} +3 -3
- package/dist/{cli-se8w6wny.js → cli-1y7yywmj.js} +1 -1
- package/dist/{cli-v1yb0rqb.js → cli-20krdhz5.js} +47 -19
- package/dist/{cli-d5f9ed5c.js → cli-29rja109.js} +39 -20
- package/dist/{cli-apx87jne.js → cli-2pwa0q4d.js} +15 -4
- package/dist/{cli-ee9gtz70.js → cli-4mengw46.js} +348 -25
- package/dist/{cli-03gedhng.js → cli-5twdke5x.js} +6 -6
- package/dist/{cli-6razj6t4.js → cli-711vwbxn.js} +2 -2
- package/dist/{cli-s2sp957d.js → cli-78vv7446.js} +1 -1
- package/dist/{cli-3fbmmawm.js → cli-7n6s5man.js} +4 -4
- package/dist/{cli-hnhtk6cj.js → cli-8wwxw8a3.js} +1 -1
- package/dist/{cli-hnmhtzvh.js → cli-9jzdj90z.js} +2 -0
- package/dist/{cli-skxvvpap.js → cli-cvv9k0hh.js} +28 -25
- package/dist/{cli-74ahz61x.js → cli-d9286m9h.js} +2 -2
- package/dist/{cli-4jxm2qtz.js → cli-gxzkg1de.js} +28 -16
- package/dist/{cli-zkpks3r3.js → cli-h1arqn31.js} +1 -1
- package/dist/{cli-r62a35ea.js → cli-hqr88ew6.js} +1 -1
- package/dist/{cli-ra0xzggj.js → cli-j67mh2vg.js} +2 -2
- package/dist/{cli-29jc5q7w.js → cli-ksphgh48.js} +2 -2
- package/dist/{cli-eejsgvm4.js → cli-mgz4nbgg.js} +4 -4
- package/dist/{cli-vatyhshy.js → cli-mz5as5ha.js} +4 -4
- package/dist/{cli-f7kr7svz.js → cli-n8ndw8j7.js} +2 -2
- package/dist/{cli-1e4b6bzh.js → cli-naqzr17j.js} +7 -7
- package/dist/{cli-q0ke1yww.js → cli-nm1p8jfr.js} +6 -6
- package/dist/{cli-m50t4309.js → cli-p3zbc1rw.js} +5 -5
- package/dist/{cli-cgtksawx.js → cli-pe05f8yk.js} +23 -3
- package/dist/{cli-vdach5a9.js → cli-qmzsbkvy.js} +4 -4
- package/dist/{cli-n3hzg89b.js → cli-qnm4e9e4.js} +57 -579
- package/dist/{cli-s4setq91.js → cli-qqy4q48b.js} +3 -3
- package/dist/{cli-k382tshb.js → cli-r35rw5de.js} +8 -7
- package/dist/{cli-k3dp5tpf.js → cli-r610p9bs.js} +2 -2
- package/dist/{cli-e6mpjhed.js → cli-rc6pf5kv.js} +1 -1
- package/dist/{cli-3dj8k4vy.js → cli-rdxm77k3.js} +1 -1
- package/dist/{cli-tqaazemc.js → cli-s69nfyeh.js} +1 -1
- package/dist/{cli-6jyj59nb.js → cli-sawv63e8.js} +2 -2
- package/dist/{cli-q50942rs.js → cli-sg19gbf0.js} +1 -1
- package/dist/{cli-p1x85d2t.js → cli-shk7r5cs.js} +12 -9
- package/dist/{cli-przpmdgr.js → cli-sksetb2q.js} +11 -1
- package/dist/{cli-d2qr7aj4.js → cli-t02z59f2.js} +1 -1
- package/dist/{cli-j0pfptdp.js → cli-v7g551te.js} +39 -43
- package/dist/{cli-bzszjf2p.js → cli-vgqzp4em.js} +519 -126
- package/dist/cli-wfnemr1t.js +86 -0
- package/dist/{cli-ne236afy.js → cli-wms6x9db.js} +8 -8
- package/dist/{cli-5hkm7whv.js → cli-x1r545x8.js} +1 -1
- package/dist/{cli-kt6zqg0e.js → cli-xczsgshn.js} +2 -2
- package/dist/{cli-ef1hg1wv.js → cli-y3167wt1.js} +430 -238
- package/dist/{cli-tsde1hdr.js → cli-yjmqpxt3.js} +12 -11
- package/dist/{cli-9nfx44eh.js → cli-z780ec7c.js} +1 -1
- package/dist/{cli-r2pnvj8g.js → cli-zm9t2vde.js} +2 -2
- package/dist/cli.js +24 -23
- package/dist/{codex-adapter-y81sssk0.js → codex-adapter-4z16v9n0.js} +8 -8
- package/dist/{codex-login-f2rcfpp9.js → codex-login-pkepy2j1.js} +2 -2
- package/dist/{codex-session-runner-8vx4j6a9.js → codex-session-runner-rvms9y2x.js} +8 -8
- package/dist/{commands-kcpvpcrr.js → commands-2a9rtyat.js} +8 -8
- package/dist/{controller-ywrmt6yd.js → controller-3k3madbn.js} +4 -4
- package/dist/{controllers-connect-kwpvj3rf.js → controllers-connect-v9yn8zb9.js} +30 -30
- package/dist/{cursor-adapter-cehv1qns.js → cursor-adapter-98gbhyzn.js} +4 -4
- package/dist/{db-ww5d7jq3.js → db-eywwsnjw.js} +9 -7
- package/dist/{db-k8ankj20.js → db-ncypk9np.js} +9 -9
- package/dist/{devin-adapter-x1022qb1.js → devin-adapter-zeq8b9gz.js} +2 -2
- package/dist/{dispatcher-e8gq5m68.js → dispatcher-w94kc575.js} +26 -26
- package/dist/{docker-ff6y1190.js → docker-b1e4cqab.js} +2 -2
- package/dist/{dsh-adapter-3epjm3af.js → dsh-adapter-47ztej36.js} +4 -4
- package/dist/{e2b-8ygwhg0f.js → e2b-bbt8ey45.js} +3 -3
- package/dist/{e2b-swhnpw3n.js → e2b-dp804sen.js} +3 -3
- package/dist/{event-bus-6axnngdj.js → event-bus-j2jdg3qb.js} +2 -2
- package/dist/{external-channel-h3359cwn.js → external-channel-zrkg8ar9.js} +8 -8
- package/dist/{grok-adapter-0d4x7gkx.js → grok-adapter-s9ncp9vy.js} +11 -11
- package/dist/{handlers-an1gzv21.js → handlers-kqftvjgw.js} +29 -29
- package/dist/{heartbeat-6dakeb61.js → heartbeat-9q96p8v9.js} +31 -31
- package/dist/{hook-6v6cv6d5.js → hook-56qe10dp.js} +7 -7
- package/dist/{hook-sa5xye4y.js → hook-yy6rw42k.js} +8 -8
- package/dist/{http-r7f3eky0.js → http-r2dnb8f8.js} +963 -75
- package/dist/{index-7ep4gzdk.js → index-3gtmkhc6.js} +26 -26
- package/dist/{index-v1v6n7hg.js → index-3k5hkz7r.js} +31 -31
- package/dist/{index-rmpw9a8q.js → index-5e101240.js} +26 -26
- package/dist/{index-s2wxgzr0.js → index-5wyfjc2c.js} +392 -15
- package/dist/{index-zk96aq1k.js → index-vxvhxxbn.js} +7 -7
- package/dist/{keepalive-be9vwy5h.js → keepalive-psj6j54c.js} +9 -9
- package/dist/{lead-0861cwrf.js → lead-hajz4688.js} +43 -43
- package/dist/{maintenance-mzzgc8bn.js → maintenance-ps02sb5m.js} +7 -7
- package/dist/{node-mn1zs7an.js → node-mwb5r697.js} +1 -1
- package/dist/{oauth-refresh-sweep-cpdnfwdj.js → oauth-refresh-sweep-0284h93b.js} +8 -8
- package/dist/{onboard-fh8qs4r3.js → onboard-rbf6bg01.js} +2 -2
- package/dist/{opencode-adapter-nm42wwj8.js → opencode-adapter-13dcatgq.js} +8 -8
- package/dist/{otel-impl-x6c1c9g4.js → otel-impl-zvqcbhp7.js} +2 -2
- package/dist/{pi-mono-adapter-9n2ya240.js → pi-mono-adapter-xxy47q4v.js} +7 -7
- package/dist/{pricing-refresh-qb0rv0ap.js → pricing-refresh-kefx9v3t.js} +9 -9
- package/dist/{rbac-roles-1gzdwbh1.js → rbac-roles-3m36rxm2.js} +8 -8
- package/dist/{rbac-roles-3mraey52.js → rbac-roles-z76xv0hq.js} +9 -9
- package/dist/{render-v2-agpa45zz.js → render-v2-wz73bs25.js} +9 -9
- package/dist/{resume-x0ydy9s9.js → resume-k4wzge8w.js} +14 -14
- package/dist/{setup-as9ga0d6.js → setup-2fp2pypp.js} +2 -2
- package/dist/{work-objects-y5g1y7ev.js → work-objects-6k0ywz3m.js} +29 -29
- package/dist/{worker-860c3e38.js → worker-5v3rxsdh.js} +43 -43
- package/dist/{x-m477fr0h.js → x-4mtdeym1.js} +2 -2
- package/openapi.json +777 -6
- package/package.json +8 -7
- package/src/agentmail/handlers.ts +21 -11
- package/src/apps/definition.ts +16 -0
- package/src/azure-devops/api.ts +52 -2
- package/src/azure-devops/boards-sync.ts +50 -20
- package/src/azure-devops/index.ts +1 -0
- package/src/azure-devops/outbound.ts +95 -0
- package/src/be/boot-seeds.ts +11 -0
- package/src/be/db/model-profiles.ts +315 -0
- package/src/be/db/tasks/read.ts +7 -0
- package/src/be/db.ts +53 -47
- package/src/be/migrations/219_relabel_pi_context_compactions.sql +22 -0
- package/src/be/migrations/220_model_profiles.sql +137 -0
- package/src/be/model-tier-resolution.ts +145 -11
- package/src/be/model-validation.ts +8 -1
- package/src/be/scripts/typecheck.ts +12 -0
- package/src/be/secret-registry.ts +6 -2
- package/src/be/seed-model-profiles.ts +41 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +368 -0
- package/src/be/seed-scripts/index.ts +9 -0
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/runner.ts +3 -17
- package/src/extensions/catalog.generated.json +17 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/loader.ts +10 -5
- package/src/heartbeat/heartbeat.ts +2 -1
- package/src/http/agents.ts +49 -2
- package/src/http/all-routes.ts +1 -0
- package/src/http/apps.ts +30 -3
- package/src/http/config.ts +26 -2
- package/src/http/core.ts +4 -0
- package/src/http/index.ts +11 -1
- package/src/http/model-profiles.ts +303 -0
- package/src/http/pages-public.ts +11 -5
- package/src/http/poll.ts +16 -5
- package/src/http/script-runs.ts +1 -1
- package/src/http/scripts.ts +25 -2
- package/src/http/tasks.ts +18 -0
- package/src/http/webhooks.ts +1 -2
- package/src/pages/feedback-overlay.ts +65 -6
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-extension.ts +17 -3
- package/src/providers/reasoning-effort.ts +10 -0
- package/src/rbac/legacy-policy.ts +8 -0
- package/src/rbac/permissions.ts +5 -0
- package/src/scheduler/deferred-task-waits.ts +18 -5
- package/src/scheduler/scheduler.ts +7 -1
- package/src/scripts-runtime/executors/quickjs-prelude.ts +75 -6
- package/src/scripts-runtime/executors/quickjs-runner.ts +85 -8
- package/src/scripts-runtime/executors/quickjs-web-shims.ts +1041 -0
- package/src/scripts-runtime/extract-args-schema.ts +9 -8
- package/src/scripts-runtime/quickjs-compat.ts +464 -0
- package/src/scripts-runtime/sdk-allowlist.ts +4 -0
- package/src/scripts-runtime/types/stdlib.d.ts +12 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +12 -0
- package/src/server.ts +10 -1
- package/src/slack/blocks.ts +4 -2
- package/src/slack/reaction-shortcode.ts +14 -0
- package/src/slack/render-v2.ts +12 -7
- package/src/slack/responses.ts +5 -4
- package/src/slack/thread-buffer.ts +41 -22
- package/src/tasks/worker-follow-up.ts +11 -2
- package/src/tests/agentmail-filters.test.ts +47 -0
- package/src/tests/apps-spike2.test.ts +35 -0
- package/src/tests/azure-devops-outbound.test.ts +420 -0
- package/src/tests/db-task-read-contract.test.ts +2 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/deferred-task-wake.test.ts +78 -1
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/env-flag.test.ts +1 -0
- package/src/tests/extensions-catalog.test.ts +1 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +413 -0
- package/src/tests/extensions-loader.test.ts +15 -0
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +168 -0
- package/src/tests/heartbeat-jev-gate.test.ts +385 -0
- package/src/tests/model-profile-backcompat.test.ts +327 -0
- package/src/tests/model-profile-providers.test.ts +302 -0
- package/src/tests/model-profile-runtime-secrets.test.ts +223 -0
- package/src/tests/model-profiles-api.test.ts +614 -0
- package/src/tests/pages-feedback-keys.test.ts +121 -0
- package/src/tests/pages-feedback.test.ts +3 -0
- package/src/tests/pi-context-compaction.test.ts +193 -0
- package/src/tests/profile-claim.test.ts +269 -0
- package/src/tests/quickjs-compat.test.ts +213 -0
- package/src/tests/rbac-engine.test.ts +21 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/script-executor-conformance.test.ts +252 -1
- package/src/tests/script-executor-quickjs.test.ts +96 -0
- package/src/tests/script-runs-http.test.ts +89 -0
- package/src/tests/scripts-http.test.ts +16 -0
- package/src/tests/secret-scrubber.test.ts +10 -0
- package/src/tests/seed-model-profiles.test.ts +174 -0
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/skill-invocations.test.ts +4 -2
- package/src/tests/slack-blocks.test.ts +36 -0
- package/src/tests/slack-inline-output.test.ts +14 -0
- package/src/tests/slack-reaction-config.test.ts +20 -0
- package/src/tests/slack-render-v2.test.ts +37 -0
- package/src/tests/slack-thread-buffer.test.ts +98 -1
- package/src/tests/workflow-llm.test.ts +61 -0
- package/src/tools/model-profiles/list-model-profiles.ts +54 -0
- package/src/tools/request-human-input.ts +11 -3
- package/src/tools/script-apis.ts +5 -1
- package/src/tools/script-upsert.ts +4 -1
- package/src/tools/send-task.ts +24 -1
- package/src/tools/system-one-decide.ts +104 -0
- package/src/tools/task-action.ts +22 -1
- package/src/tools/tool-config.ts +6 -0
- package/src/types.ts +36 -1
- package/src/utils/secret-scrubber.ts +19 -12
- package/src/utils/vcs-repo-match.ts +29 -0
- package/src/workflows/executors/raw-llm.ts +8 -3
- package/src/workflows/executors/system-one-decision.ts +21 -11
- package/src/workflows/executors/workflow-llm.ts +22 -0
- package/src/workflows/index.ts +21 -12
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/comms-adherence-gate/README.md +88 -0
- package/templates/extensions/comms-adherence-gate/hooks.ts +489 -0
- package/templates/extensions/comms-adherence-gate/manifest.json +8 -0
- package/dist/cli-5t4f29ez.js +0 -33
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
import { resetAzureDevOpsBotIdCache, updateWorkItemFields } from "../azure-devops/api";
|
|
5
|
+
import { initAzureDevOps, resetAzureDevOps } from "../azure-devops/auth";
|
|
6
|
+
import { initAzureDevOpsOutbound, teardownAzureDevOpsOutbound } from "../azure-devops/outbound";
|
|
7
|
+
import {
|
|
8
|
+
closeDb,
|
|
9
|
+
createAgent,
|
|
10
|
+
createTaskExtended,
|
|
11
|
+
createWorkflow,
|
|
12
|
+
getDbClient,
|
|
13
|
+
initDb,
|
|
14
|
+
listWorkflowRuns,
|
|
15
|
+
} from "../be/db";
|
|
16
|
+
import {
|
|
17
|
+
createTrackerSyncIfAbsent,
|
|
18
|
+
deleteTrackerSync,
|
|
19
|
+
getAllTrackerSyncs,
|
|
20
|
+
} from "../be/db-queries/tracker";
|
|
21
|
+
import { emitTaskStarted } from "../be/task-lifecycle-events";
|
|
22
|
+
import { handleWebhooks } from "../http/webhooks";
|
|
23
|
+
import { initWorkflows, stopRetryPoller, stopWaitPoller } from "../workflows";
|
|
24
|
+
import { workflowEventBus } from "../workflows/event-bus";
|
|
25
|
+
import commentedFixture from "./fixtures/azure-boards/workitem.commented.json";
|
|
26
|
+
import updatedFixture from "./fixtures/azure-boards/workitem.updated.json";
|
|
27
|
+
|
|
28
|
+
const TEST_DB_PATH = "./test-azure-devops-outbound.sqlite";
|
|
29
|
+
const SECRET = "outbound-secret";
|
|
30
|
+
const BOT_ID = "7d40ac7f-6bdb-6baf-a2e2-dc9594e967d4";
|
|
31
|
+
const ORG_URL = "https://dev.azure.com/acme-org";
|
|
32
|
+
const ENV_KEYS = [
|
|
33
|
+
"AZURE_DEVOPS_WEBHOOK_SECRET",
|
|
34
|
+
"AZURE_DEVOPS_TOKEN",
|
|
35
|
+
"AZURE_DEVOPS_ORG_URL",
|
|
36
|
+
"AZURE_DEVOPS_BOT_ID",
|
|
37
|
+
"AZURE_DEVOPS_ALLOWED_ACTORS",
|
|
38
|
+
"AZURE_DEVOPS_WORKITEM_STATE_STARTED",
|
|
39
|
+
"AZURE_DEVOPS_WORKITEM_STATE_DONE",
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
type Json = Record<string, any>;
|
|
43
|
+
const originalFetch = globalThis.fetch;
|
|
44
|
+
let nextId = 0;
|
|
45
|
+
let patches: { url: string; contentType: string | null; body: Json[] }[] = [];
|
|
46
|
+
let patchStatus = 200;
|
|
47
|
+
|
|
48
|
+
function clearEnv() {
|
|
49
|
+
for (const key of ENV_KEYS) delete process.env[key];
|
|
50
|
+
resetAzureDevOps();
|
|
51
|
+
resetAzureDevOpsBotIdCache();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function linkedTask(workItemId: number | string): Promise<string> {
|
|
55
|
+
const task = await createTaskExtended("linked work", { source: "azure-devops" });
|
|
56
|
+
await createTrackerSyncIfAbsent({
|
|
57
|
+
provider: "azure-devops",
|
|
58
|
+
entityType: "task",
|
|
59
|
+
providerEntityType: "WorkItem",
|
|
60
|
+
swarmId: task.id,
|
|
61
|
+
externalId: `dev.azure.com/acme-org#${workItemId}`,
|
|
62
|
+
lastSyncOrigin: "external",
|
|
63
|
+
syncDirection: "inbound",
|
|
64
|
+
});
|
|
65
|
+
return task.id;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Let the fire-and-forget handlers run. */
|
|
69
|
+
const settle = () => new Promise((r) => setTimeout(r, 25));
|
|
70
|
+
|
|
71
|
+
async function post(body: Json) {
|
|
72
|
+
const req = {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { authorization: `Basic ${Buffer.from(`hook:${SECRET}`).toString("base64")}` },
|
|
75
|
+
async *[Symbol.asyncIterator]() {
|
|
76
|
+
yield Buffer.from(JSON.stringify(body));
|
|
77
|
+
},
|
|
78
|
+
} as unknown as IncomingMessage;
|
|
79
|
+
const res = { writeHead: () => res, end: () => res } as unknown as ServerResponse;
|
|
80
|
+
await handleWebhooks(req, res, ["api", "azure-devops", "webhook"]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
beforeAll(async () => {
|
|
84
|
+
clearEnv();
|
|
85
|
+
try {
|
|
86
|
+
await unlink(TEST_DB_PATH);
|
|
87
|
+
} catch {}
|
|
88
|
+
initDb(TEST_DB_PATH);
|
|
89
|
+
await createAgent({
|
|
90
|
+
id: "lead-outbound-001",
|
|
91
|
+
name: "OutboundLead",
|
|
92
|
+
status: "idle",
|
|
93
|
+
isLead: true,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
afterAll(async () => {
|
|
98
|
+
teardownAzureDevOpsOutbound();
|
|
99
|
+
clearEnv();
|
|
100
|
+
closeDb();
|
|
101
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
102
|
+
try {
|
|
103
|
+
await unlink(`${TEST_DB_PATH}${suffix}`);
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
beforeEach(async () => {
|
|
109
|
+
process.env.AZURE_DEVOPS_WEBHOOK_SECRET = SECRET;
|
|
110
|
+
process.env.AZURE_DEVOPS_TOKEN = "pat";
|
|
111
|
+
process.env.AZURE_DEVOPS_ORG_URL = ORG_URL;
|
|
112
|
+
initAzureDevOps();
|
|
113
|
+
patches = [];
|
|
114
|
+
patchStatus = 200;
|
|
115
|
+
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
116
|
+
if (String(input).endsWith("/_apis/connectionData")) {
|
|
117
|
+
return Response.json({
|
|
118
|
+
authenticatedUser: {
|
|
119
|
+
id: BOT_ID,
|
|
120
|
+
properties: { Account: { $value: "swarm-bot@example.com" } },
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (init?.method === "PATCH") {
|
|
125
|
+
patches.push({
|
|
126
|
+
url: String(input),
|
|
127
|
+
contentType: new Headers(init.headers).get("content-type"),
|
|
128
|
+
body: JSON.parse(String(init.body)),
|
|
129
|
+
});
|
|
130
|
+
return new Response("{}", { status: patchStatus });
|
|
131
|
+
}
|
|
132
|
+
throw new Error(`unexpected fetch ${String(input)}`);
|
|
133
|
+
}) as typeof fetch;
|
|
134
|
+
for (const sync of await getAllTrackerSyncs("azure-devops")) await deleteTrackerSync(sync.id);
|
|
135
|
+
initAzureDevOpsOutbound();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
teardownAzureDevOpsOutbound();
|
|
140
|
+
globalThis.fetch = originalFetch;
|
|
141
|
+
clearEnv();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("work item state transitions", () => {
|
|
145
|
+
test("sets the started state on the linked work item", async () => {
|
|
146
|
+
process.env.AZURE_DEVOPS_WORKITEM_STATE_STARTED = "Doing";
|
|
147
|
+
const taskId = await linkedTask(9);
|
|
148
|
+
emitTaskStarted({ id: taskId } as never);
|
|
149
|
+
await settle();
|
|
150
|
+
expect(patches).toHaveLength(1);
|
|
151
|
+
expect(patches[0]?.url).toBe(`${ORG_URL}/_apis/wit/workitems/9?api-version=7.1`);
|
|
152
|
+
expect(patches[0]?.contentType).toBe("application/json-patch+json");
|
|
153
|
+
expect(patches[0]?.body).toEqual([{ op: "add", path: "/fields/System.State", value: "Doing" }]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("sets the done state when the task completes", async () => {
|
|
157
|
+
process.env.AZURE_DEVOPS_WORKITEM_STATE_DONE = " Done ";
|
|
158
|
+
const taskId = await linkedTask(9);
|
|
159
|
+
workflowEventBus.emit("task.completed", { taskId, output: "ok" });
|
|
160
|
+
await settle();
|
|
161
|
+
expect(patches[0]?.body[0]?.value).toBe("Done");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("does nothing when the state vars are unset", async () => {
|
|
165
|
+
const taskId = await linkedTask(9);
|
|
166
|
+
emitTaskStarted({ id: taskId } as never);
|
|
167
|
+
workflowEventBus.emit("task.completed", { taskId });
|
|
168
|
+
await settle();
|
|
169
|
+
expect(patches).toHaveLength(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("ignores tasks without a work item link", async () => {
|
|
173
|
+
process.env.AZURE_DEVOPS_WORKITEM_STATE_DONE = "Done";
|
|
174
|
+
const task = await createTaskExtended("plain", {});
|
|
175
|
+
workflowEventBus.emit("task.completed", { taskId: task.id });
|
|
176
|
+
await settle();
|
|
177
|
+
expect(patches).toHaveLength(0);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("a rejected update does not throw", async () => {
|
|
181
|
+
process.env.AZURE_DEVOPS_WORKITEM_STATE_DONE = "Nope";
|
|
182
|
+
patchStatus = 400;
|
|
183
|
+
const taskId = await linkedTask(9);
|
|
184
|
+
workflowEventBus.emit("task.completed", { taskId });
|
|
185
|
+
await settle();
|
|
186
|
+
expect(patches).toHaveLength(1);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("stops after teardown", async () => {
|
|
190
|
+
process.env.AZURE_DEVOPS_WORKITEM_STATE_DONE = "Done";
|
|
191
|
+
const taskId = await linkedTask(9);
|
|
192
|
+
teardownAzureDevOpsOutbound();
|
|
193
|
+
workflowEventBus.emit("task.completed", { taskId });
|
|
194
|
+
await settle();
|
|
195
|
+
expect(patches).toHaveLength(0);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("work item workflow events", () => {
|
|
200
|
+
const unsubscribers: (() => void)[] = [];
|
|
201
|
+
afterEach(() => {
|
|
202
|
+
for (const off of unsubscribers.splice(0)) off();
|
|
203
|
+
});
|
|
204
|
+
function capture(name: string): Json[] {
|
|
205
|
+
const seen: Json[] = [];
|
|
206
|
+
const handler = (d: Json) => seen.push(d);
|
|
207
|
+
workflowEventBus.on(name, handler);
|
|
208
|
+
unsubscribers.push(() => workflowEventBus.off(name, handler));
|
|
209
|
+
return seen;
|
|
210
|
+
}
|
|
211
|
+
function fresh(sample: unknown): Json {
|
|
212
|
+
const event = structuredClone(sample) as Json;
|
|
213
|
+
event.id = `00000000-0000-4000-8000-${String(900 + ++nextId).padStart(12, "0")}`;
|
|
214
|
+
if (event.resource?.commentVersionRef) {
|
|
215
|
+
event.resource.commentVersionRef.commentId = 2_000_000 + nextId;
|
|
216
|
+
}
|
|
217
|
+
return event;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
test("emits azure-devops.work_item.assigned on an assignee change", async () => {
|
|
221
|
+
const seen = capture("azure-devops.work_item.assigned");
|
|
222
|
+
const event = fresh(updatedFixture);
|
|
223
|
+
event.resource.fields = {
|
|
224
|
+
...event.resource.fields,
|
|
225
|
+
"System.AssignedTo": { oldValue: "A <a@example.com>", newValue: "B <b@example.com>" },
|
|
226
|
+
};
|
|
227
|
+
await post(event);
|
|
228
|
+
expect(seen).toHaveLength(1);
|
|
229
|
+
expect(seen[0]).toMatchObject({
|
|
230
|
+
work_item_id: event.resource.workItemId,
|
|
231
|
+
action: "assigned",
|
|
232
|
+
assigned_to: "B <b@example.com>",
|
|
233
|
+
previous_assignee: "A <a@example.com>",
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("does not emit assigned when the assignee did not change", async () => {
|
|
238
|
+
const seen = capture("azure-devops.work_item.assigned");
|
|
239
|
+
const event = fresh(updatedFixture);
|
|
240
|
+
delete event.resource.fields["System.AssignedTo"];
|
|
241
|
+
await post(event);
|
|
242
|
+
expect(seen).toHaveLength(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("emits azure-devops.work_item.commented for a new comment only", async () => {
|
|
246
|
+
const seen = capture("azure-devops.work_item.commented");
|
|
247
|
+
const first = fresh(commentedFixture);
|
|
248
|
+
first.resource.commentVersionRef.version = 1;
|
|
249
|
+
await post(first);
|
|
250
|
+
const edit = fresh(commentedFixture);
|
|
251
|
+
edit.resource.commentVersionRef.version = 2;
|
|
252
|
+
await post(edit);
|
|
253
|
+
expect(seen).toHaveLength(1);
|
|
254
|
+
expect(seen[0]).toMatchObject({
|
|
255
|
+
work_item_id: first.resource.id,
|
|
256
|
+
action: "commented",
|
|
257
|
+
comment: first.resource.fields["System.History"],
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("work item state ordering and commit boundary", () => {
|
|
263
|
+
test("a held Doing PATCH cannot land after Done", async () => {
|
|
264
|
+
process.env.AZURE_DEVOPS_WORKITEM_STATE_STARTED = "Doing";
|
|
265
|
+
process.env.AZURE_DEVOPS_WORKITEM_STATE_DONE = "Done";
|
|
266
|
+
const taskId = await linkedTask(9);
|
|
267
|
+
const applied: string[] = [];
|
|
268
|
+
let releaseDoing!: () => void;
|
|
269
|
+
const held = new Promise<void>((resolve) => {
|
|
270
|
+
releaseDoing = resolve;
|
|
271
|
+
});
|
|
272
|
+
const inner = globalThis.fetch;
|
|
273
|
+
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
274
|
+
if (init?.method !== "PATCH") return inner(input, init);
|
|
275
|
+
const value = JSON.parse(String(init.body))[0].value as string;
|
|
276
|
+
if (value === "Doing") await held;
|
|
277
|
+
applied.push(value);
|
|
278
|
+
return new Response("{}", { status: 200 });
|
|
279
|
+
}) as typeof fetch;
|
|
280
|
+
|
|
281
|
+
emitTaskStarted({ id: taskId } as never);
|
|
282
|
+
await settle();
|
|
283
|
+
workflowEventBus.emit("task.completed", { taskId });
|
|
284
|
+
await settle();
|
|
285
|
+
expect(applied).toEqual([]);
|
|
286
|
+
releaseDoing();
|
|
287
|
+
await settle();
|
|
288
|
+
expect(applied).toEqual(["Doing", "Done"]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("a rolled-back start never reaches Azure", async () => {
|
|
292
|
+
process.env.AZURE_DEVOPS_WORKITEM_STATE_STARTED = "Doing";
|
|
293
|
+
const taskId = await linkedTask(9);
|
|
294
|
+
await getDbClient()
|
|
295
|
+
.transaction(async () => {
|
|
296
|
+
emitTaskStarted({ id: taskId } as never);
|
|
297
|
+
throw new Error("rollback");
|
|
298
|
+
})
|
|
299
|
+
.catch(() => {});
|
|
300
|
+
await settle();
|
|
301
|
+
expect(patches).toHaveLength(0);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("a committed start is delivered after the commit", async () => {
|
|
305
|
+
process.env.AZURE_DEVOPS_WORKITEM_STATE_STARTED = "Doing";
|
|
306
|
+
const taskId = await linkedTask(9);
|
|
307
|
+
await getDbClient().transaction(async () => {
|
|
308
|
+
emitTaskStarted({ id: taskId } as never);
|
|
309
|
+
});
|
|
310
|
+
await settle();
|
|
311
|
+
expect(patches).toHaveLength(1);
|
|
312
|
+
expect(patches[0]?.body[0]?.value).toBe("Doing");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("scrubs secrets from a rejected update response", async () => {
|
|
316
|
+
const token = `ghp_${"a1B2c3D4e5".repeat(4)}`;
|
|
317
|
+
const inner = globalThis.fetch;
|
|
318
|
+
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) =>
|
|
319
|
+
init?.method === "PATCH"
|
|
320
|
+
? new Response(`bad token ${token}`, { status: 401 })
|
|
321
|
+
: inner(input, init)) as typeof fetch;
|
|
322
|
+
const logged: string[] = [];
|
|
323
|
+
const original = console.error;
|
|
324
|
+
console.error = (...args: unknown[]) => void logged.push(args.join(" "));
|
|
325
|
+
try {
|
|
326
|
+
await updateWorkItemFields(ORG_URL, 9, [
|
|
327
|
+
{ op: "add", path: "/fields/System.State", value: "X" },
|
|
328
|
+
]);
|
|
329
|
+
} finally {
|
|
330
|
+
console.error = original;
|
|
331
|
+
}
|
|
332
|
+
expect(logged.join("\n")).toContain("401");
|
|
333
|
+
expect(logged.join("\n")).not.toContain(token);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("work item workflow events: trust and delivery boundaries", () => {
|
|
338
|
+
const unsubscribers: (() => void)[] = [];
|
|
339
|
+
afterEach(() => {
|
|
340
|
+
for (const off of unsubscribers.splice(0)) off();
|
|
341
|
+
});
|
|
342
|
+
function capture(name: string): Json[] {
|
|
343
|
+
const seen: Json[] = [];
|
|
344
|
+
const handler = (d: Json) => seen.push(d);
|
|
345
|
+
workflowEventBus.on(name, handler);
|
|
346
|
+
unsubscribers.push(() => workflowEventBus.off(name, handler));
|
|
347
|
+
return seen;
|
|
348
|
+
}
|
|
349
|
+
function fresh(sample: unknown): Json {
|
|
350
|
+
const event = structuredClone(sample) as Json;
|
|
351
|
+
event.id = `00000000-0000-4000-8000-${String(5000 + ++nextId).padStart(12, "0")}`;
|
|
352
|
+
if (event.resource?.commentVersionRef) {
|
|
353
|
+
event.resource.commentVersionRef.commentId = 3_000_000 + nextId;
|
|
354
|
+
event.resource.commentVersionRef.version = 1;
|
|
355
|
+
}
|
|
356
|
+
return event;
|
|
357
|
+
}
|
|
358
|
+
function assigned(): Json {
|
|
359
|
+
const event = fresh(updatedFixture);
|
|
360
|
+
event.resource.fields = {
|
|
361
|
+
...event.resource.fields,
|
|
362
|
+
"System.AssignedTo": { oldValue: "A <a@example.com>", newValue: "B <b@example.com>" },
|
|
363
|
+
};
|
|
364
|
+
return event;
|
|
365
|
+
}
|
|
366
|
+
function excludeOrg(event: Json): Json {
|
|
367
|
+
const swap = (value: unknown) => String(value).replace("acme-org", "excluded-org");
|
|
368
|
+
event.resourceContainers.account.baseUrl = swap(event.resourceContainers.account.baseUrl);
|
|
369
|
+
event.resourceContainers.collection.baseUrl = swap(event.resourceContainers.collection.baseUrl);
|
|
370
|
+
event.resource.url = swap(event.resource.url);
|
|
371
|
+
return event;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
test("events from an untrusted organization are not published", async () => {
|
|
375
|
+
const assignedSeen = capture("azure-devops.work_item.assigned");
|
|
376
|
+
const commentedSeen = capture("azure-devops.work_item.commented");
|
|
377
|
+
await post(excludeOrg(assigned()));
|
|
378
|
+
await post(excludeOrg(fresh(commentedFixture)));
|
|
379
|
+
expect(assignedSeen).toHaveLength(0);
|
|
380
|
+
expect(commentedSeen).toHaveLength(0);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("concurrent and sequential retries publish once", async () => {
|
|
384
|
+
const assignedSeen = capture("azure-devops.work_item.assigned");
|
|
385
|
+
const commentedSeen = capture("azure-devops.work_item.commented");
|
|
386
|
+
const assignment = assigned();
|
|
387
|
+
const comment = fresh(commentedFixture);
|
|
388
|
+
await Promise.all([post(assignment), post(assignment)]);
|
|
389
|
+
await post(assignment);
|
|
390
|
+
await Promise.all([post(comment), post(comment)]);
|
|
391
|
+
await post(comment);
|
|
392
|
+
// Same comment re-delivered under a new delivery id: the comment receipt covers it.
|
|
393
|
+
const redelivered = structuredClone(comment);
|
|
394
|
+
redelivered.id = `00000000-0000-4000-8000-${String(5000 + ++nextId).padStart(12, "0")}`;
|
|
395
|
+
await post(redelivered);
|
|
396
|
+
expect(assignedSeen).toHaveLength(1);
|
|
397
|
+
expect(commentedSeen).toHaveLength(1);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("published events start enabled event-trigger workflows", async () => {
|
|
401
|
+
await initWorkflows();
|
|
402
|
+
stopRetryPoller();
|
|
403
|
+
stopWaitPoller();
|
|
404
|
+
const make = (eventName: string) =>
|
|
405
|
+
createWorkflow({
|
|
406
|
+
name: `azure-trigger-${crypto.randomUUID().slice(0, 8)}`,
|
|
407
|
+
definition: {
|
|
408
|
+
nodes: [{ id: "n1", type: "notify", config: { channel: "swarm", template: "t" } }],
|
|
409
|
+
},
|
|
410
|
+
triggers: [{ type: "event", eventName: eventName as never }],
|
|
411
|
+
});
|
|
412
|
+
const onAssigned = await make("azure-devops.work_item.assigned");
|
|
413
|
+
const onCommented = await make("azure-devops.work_item.commented");
|
|
414
|
+
await post(assigned());
|
|
415
|
+
await post(fresh(commentedFixture));
|
|
416
|
+
await settle();
|
|
417
|
+
expect(await listWorkflowRuns(onAssigned.id)).toHaveLength(1);
|
|
418
|
+
expect(await listWorkflowRuns(onCommented.id)).toHaveLength(1);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
@@ -110,6 +110,7 @@ value:NOT_EXTENSION_AGENT_SQL
|
|
|
110
110
|
value:ReservedAgentRoleError
|
|
111
111
|
value:SESSION_ACTIVITY_REFRESH_MIN_INTERVAL_MS
|
|
112
112
|
value:SKILL_FILE_LIMITS
|
|
113
|
+
value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
|
|
113
114
|
value:UNATTRIBUTED_USER_ID
|
|
114
115
|
value:__resetSqliteVecExtensionPathCacheForTests
|
|
115
116
|
value:abandonSlackOutcomeDelivery
|
|
@@ -610,6 +611,7 @@ const selected = `type:ExistingTrackerContextWork
|
|
|
610
611
|
type:ExistingTrackerContextWorkReason
|
|
611
612
|
type:PendingChatRelay
|
|
612
613
|
type:TaskFilters
|
|
614
|
+
value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
|
|
613
615
|
value:findCompletedTaskInThread
|
|
614
616
|
value:findExistingLinearTrackerContextWork
|
|
615
617
|
value:findRecentCancelledTaskInThread
|
|
@@ -133,6 +133,7 @@ value:NOT_EXTENSION_AGENT_SQL
|
|
|
133
133
|
value:ReservedAgentRoleError
|
|
134
134
|
value:SESSION_ACTIVITY_REFRESH_MIN_INTERVAL_MS
|
|
135
135
|
value:SKILL_FILE_LIMITS
|
|
136
|
+
value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
|
|
136
137
|
value:UNATTRIBUTED_USER_ID
|
|
137
138
|
value:__resetSqliteVecExtensionPathCacheForTests
|
|
138
139
|
value:abandonSlackOutcomeDelivery
|
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
supersedeTask,
|
|
22
22
|
updateScheduledTask,
|
|
23
23
|
} from "../be/db";
|
|
24
|
+
import { installExtension, setExtensionState } from "../be/extensions/db";
|
|
25
|
+
import { listRegistered, registerLoaded, unregister } from "../extensions/dispatcher";
|
|
26
|
+
import { ensureExtensionAgent } from "../extensions/identity";
|
|
27
|
+
import { loadExtension } from "../extensions/loader";
|
|
24
28
|
import {
|
|
25
29
|
dispatchDeferredTaskWait,
|
|
26
30
|
initDeferredTaskWaits,
|
|
@@ -32,6 +36,7 @@ import { dispatchScheduleTarget, executeSchedule } from "../scheduler/scheduler"
|
|
|
32
36
|
import { createResumeFollowUp } from "../tasks/worker-follow-up";
|
|
33
37
|
import { registerDeferTaskTool } from "../tools/defer-task";
|
|
34
38
|
import type { AgentTask } from "../types";
|
|
39
|
+
import { loadBundleFixture } from "./fixtures/extensions/load";
|
|
35
40
|
|
|
36
41
|
type Result = { structuredContent: { success: boolean; message: string; scheduleId?: string } };
|
|
37
42
|
type Tool = {
|
|
@@ -69,7 +74,13 @@ beforeAll(async () => {
|
|
|
69
74
|
"defer-task"
|
|
70
75
|
]!;
|
|
71
76
|
});
|
|
72
|
-
afterEach(() =>
|
|
77
|
+
afterEach(async () => {
|
|
78
|
+
stopDeferredTaskWaits();
|
|
79
|
+
for (const loaded of listRegistered()) {
|
|
80
|
+
unregister(loaded.record.id);
|
|
81
|
+
await loaded.dispose();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
73
84
|
afterAll(async () => {
|
|
74
85
|
closeDb();
|
|
75
86
|
templateGlobals.__testMigrationTemplate = savedTemplate;
|
|
@@ -172,6 +183,72 @@ async function multiFixture(mode?: "all" | "any", event: TaskWakeEvent = "settle
|
|
|
172
183
|
}
|
|
173
184
|
|
|
174
185
|
describe("defer-task wakeOn", () => {
|
|
186
|
+
async function ceilingHook(action: string) {
|
|
187
|
+
const bundle = await loadBundleFixture("minimal");
|
|
188
|
+
bundle.manifest = { ...bundle.manifest, name: `ceiling-${crypto.randomUUID()}` };
|
|
189
|
+
bundle.files["hooks.ts"] = `
|
|
190
|
+
import { block, modify } from "swarm-extension";
|
|
191
|
+
export default (api) => {
|
|
192
|
+
api.on("pre.task.create", () => ${action});
|
|
193
|
+
};
|
|
194
|
+
`;
|
|
195
|
+
const installed = await installExtension(bundle);
|
|
196
|
+
const record = await setExtensionState(installed.extension.id, {
|
|
197
|
+
agentId: await ensureExtensionAgent(installed.extension.name),
|
|
198
|
+
});
|
|
199
|
+
const loaded = await loadExtension({ record: record!, ...bundle });
|
|
200
|
+
registerLoaded(loaded);
|
|
201
|
+
return loaded;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
test("ceiling honours pre.task.create blocks and leaves the wait pending", async () => {
|
|
205
|
+
const { schedule } = await fixture();
|
|
206
|
+
await ceilingHook('block("ceiling denied")');
|
|
207
|
+
await executeSchedule(schedule);
|
|
208
|
+
expect(await children(schedule.id)).toHaveLength(0);
|
|
209
|
+
expect(await waitState(schedule.id)).toMatchObject({ status: "pending", childTaskId: null });
|
|
210
|
+
expect(await getScheduledTaskById(schedule.id)).toMatchObject({
|
|
211
|
+
consecutiveErrors: 1,
|
|
212
|
+
lastErrorMessage: expect.stringContaining("ceiling denied"),
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("ceiling applies pre.task.create payload modifications", async () => {
|
|
217
|
+
const { schedule } = await fixture();
|
|
218
|
+
await ceilingHook('modify({ description: "modified ceiling wake" })');
|
|
219
|
+
await executeSchedule(schedule);
|
|
220
|
+
const found = await children(schedule.id);
|
|
221
|
+
expect(found).toHaveLength(1);
|
|
222
|
+
expect(found[0]!.task).toBe("modified ceiling wake");
|
|
223
|
+
expect(await waitState(schedule.id)).toMatchObject({ status: "fired", firedBy: "ceiling" });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("a ceiling moved during pre-hook preparation cannot fire the stale occurrence", async () => {
|
|
227
|
+
const { schedule } = await fixture();
|
|
228
|
+
const nextRunAt = new Date(Date.now() + 120_000).toISOString();
|
|
229
|
+
const loaded = await ceilingHook('modify({ description: "stale preparation" })');
|
|
230
|
+
const handler = loaded.handlers[0]!;
|
|
231
|
+
const original = handler.handler;
|
|
232
|
+
handler.handler = async (event, ctx) => {
|
|
233
|
+
await updateScheduledTask(schedule.id, { nextRunAt });
|
|
234
|
+
return original(event, ctx);
|
|
235
|
+
};
|
|
236
|
+
await executeSchedule(schedule);
|
|
237
|
+
expect(await children(schedule.id)).toHaveLength(0);
|
|
238
|
+
expect(await waitState(schedule.id)).toMatchObject({ status: "pending" });
|
|
239
|
+
expect(await getScheduledTaskById(schedule.id)).toMatchObject({ enabled: true, nextRunAt });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("a stale ceiling snapshot cannot fire after re-defer moves nextRunAt", async () => {
|
|
243
|
+
const { schedule } = await fixture();
|
|
244
|
+
const nextRunAt = new Date(Date.now() + 120_000).toISOString();
|
|
245
|
+
await updateScheduledTask(schedule.id, { nextRunAt });
|
|
246
|
+
await executeSchedule(schedule);
|
|
247
|
+
expect(await children(schedule.id)).toHaveLength(0);
|
|
248
|
+
expect(await waitState(schedule.id)).toMatchObject({ status: "pending" });
|
|
249
|
+
expect(await getScheduledTaskById(schedule.id)).toMatchObject({ enabled: true, nextRunAt });
|
|
250
|
+
});
|
|
251
|
+
|
|
175
252
|
test.each([
|
|
176
253
|
"completed",
|
|
177
254
|
"failed",
|
|
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
|
|
|
103
103
|
expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
|
|
104
104
|
|
|
105
105
|
const generatedKeys = await generatedOperationKeys();
|
|
106
|
-
expect(operationKeys).toHaveLength(
|
|
107
|
-
expect(generatedKeys).toHaveLength(
|
|
106
|
+
expect(operationKeys).toHaveLength(430);
|
|
107
|
+
expect(generatedKeys).toHaveLength(430);
|
|
108
108
|
expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -376,7 +376,7 @@ describe("dsh harness", () => {
|
|
|
376
376
|
await rename(join(config.cwd, "dsh"), join(config.cwd, "npx"));
|
|
377
377
|
config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
|
|
378
378
|
await expect(new DshAdapter().createSession(config)).rejects.toThrow(
|
|
379
|
-
"dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-alpha.
|
|
379
|
+
"dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-alpha.5",
|
|
380
380
|
);
|
|
381
381
|
expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
|
|
382
382
|
});
|
|
@@ -76,6 +76,7 @@ describe("swarm-config-guard: Configuration-page value validation", () => {
|
|
|
76
76
|
"SLACK_REACTION_STEERED",
|
|
77
77
|
"SLACK_REACTION_COMPLETED",
|
|
78
78
|
"SLACK_REACTION_FAILED",
|
|
79
|
+
"SLACK_STATUS_ICON_COMPLETED",
|
|
79
80
|
];
|
|
80
81
|
for (const key of keys) {
|
|
81
82
|
expect(validateConfigValue(key, "thumbsup")).toBeNull();
|