@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
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
clearVolatileSecretsForTesting,
|
|
25
25
|
refreshSecretScrubberCache,
|
|
26
26
|
} from "../utils/secret-scrubber";
|
|
27
|
+
import { executeRawLlm } from "../workflows/executors/raw-llm";
|
|
27
28
|
import { randomToken } from "./synthetic-secret-helpers";
|
|
28
29
|
|
|
29
30
|
const TEST_DB_PATH = "./test-script-runs-http.sqlite";
|
|
@@ -661,3 +662,91 @@ describe("durable launch source with an embedded secret", () => {
|
|
|
661
662
|
}
|
|
662
663
|
});
|
|
663
664
|
});
|
|
665
|
+
|
|
666
|
+
describe("POST /api/internal/raw-llm failure", () => {
|
|
667
|
+
const KEY_VARS = ["NAN_API_KEY", "HELMCODE_API_NAN"] as const;
|
|
668
|
+
|
|
669
|
+
// The provider echoes the key it received; `selectCredential` sends it
|
|
670
|
+
// trimmed, so a padded config value must still be redacted.
|
|
671
|
+
async function withNanKey(
|
|
672
|
+
keyVar: (typeof KEY_VARS)[number],
|
|
673
|
+
pad: string,
|
|
674
|
+
fn: (dummy: string, calls: () => number) => Promise<void>,
|
|
675
|
+
dummy = `nan-${randomToken(32)}`,
|
|
676
|
+
) {
|
|
677
|
+
const previous = Object.fromEntries(KEY_VARS.map((name) => [name, process.env[name]]));
|
|
678
|
+
const previousFetch = globalThis.fetch;
|
|
679
|
+
for (const name of KEY_VARS) delete process.env[name];
|
|
680
|
+
process.env[keyVar] = `${pad}${dummy}${pad}`;
|
|
681
|
+
refreshSecretScrubberCache();
|
|
682
|
+
let calls = 0;
|
|
683
|
+
globalThis.fetch = (async () => {
|
|
684
|
+
calls++;
|
|
685
|
+
return new Response(
|
|
686
|
+
JSON.stringify({ error: { message: `Invalid API key: ${dummy}`, type: "auth" } }),
|
|
687
|
+
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
688
|
+
);
|
|
689
|
+
}) as unknown as typeof fetch;
|
|
690
|
+
try {
|
|
691
|
+
await fn(dummy, () => calls);
|
|
692
|
+
} finally {
|
|
693
|
+
globalThis.fetch = previousFetch;
|
|
694
|
+
for (const name of KEY_VARS) {
|
|
695
|
+
if (previous[name] === undefined) delete process.env[name];
|
|
696
|
+
else process.env[name] = previous[name];
|
|
697
|
+
}
|
|
698
|
+
refreshSecretScrubberCache();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
for (const keyVar of KEY_VARS) {
|
|
703
|
+
for (const pad of ["", " "]) {
|
|
704
|
+
const label = pad ? "padded" : "unpadded";
|
|
705
|
+
test(`does not echo a provider error that carries the ${label} ${keyVar} key`, async () => {
|
|
706
|
+
await withNanKey(keyVar, pad, async (dummy, calls) => {
|
|
707
|
+
const response = await dispatch("/api/internal/raw-llm", {
|
|
708
|
+
method: "POST",
|
|
709
|
+
agentId,
|
|
710
|
+
body: JSON.stringify({ prompt: "hi", model: "nan/qwen3.8-flash" }),
|
|
711
|
+
});
|
|
712
|
+
expect(calls()).toBe(1);
|
|
713
|
+
expect(response.status).toBe(500);
|
|
714
|
+
expect(response.text).not.toContain(dummy);
|
|
715
|
+
const body = (await response.json()) as { error: string };
|
|
716
|
+
expect(body.error).toContain("LLM call failed");
|
|
717
|
+
expect(body.error).toContain(`[REDACTED:${keyVar}]`);
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// A random key can contain "529"; that must not read as an overload.
|
|
723
|
+
test(`a ${keyVar} key containing 529 is not mistaken for a rate limit`, async () => {
|
|
724
|
+
await withNanKey(
|
|
725
|
+
keyVar,
|
|
726
|
+
"",
|
|
727
|
+
async (dummy, calls) => {
|
|
728
|
+
const result = await executeRawLlm({ prompt: "hi", model: "nan/qwen3.8-flash" });
|
|
729
|
+
expect(calls()).toBe(1);
|
|
730
|
+
expect(result.status).toBe("failed");
|
|
731
|
+
if (result.status === "failed") expect(result.error).not.toContain(dummy);
|
|
732
|
+
},
|
|
733
|
+
`nan-5L76Mxo2D529A9S0GrqOZWIpYdfO1aIM`,
|
|
734
|
+
);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test(`scrubs the fallback-port error for a padded ${keyVar} key`, async () => {
|
|
738
|
+
await withNanKey(keyVar, " ", async (dummy, calls) => {
|
|
739
|
+
const result = await executeRawLlm({
|
|
740
|
+
prompt: "hi",
|
|
741
|
+
model: "nan/qwen3.8-flash",
|
|
742
|
+
fallbackPort: "fallback",
|
|
743
|
+
});
|
|
744
|
+
expect(calls()).toBe(1);
|
|
745
|
+
expect(result.status).toBe("success");
|
|
746
|
+
expect(result.error).toContain("using fallback port");
|
|
747
|
+
expect(result.error).not.toContain(dummy);
|
|
748
|
+
expect(result.error).toContain(`[REDACTED:${keyVar}]`);
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
});
|
|
@@ -242,6 +242,22 @@ describe("/api/scripts HTTP", () => {
|
|
|
242
242
|
expect(await second.json()).toEqual({ name: "double", version: 2, contentDeduped: false });
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
+
test("upsert saves a quickjs-incompatible script and returns advisory warnings", async () => {
|
|
246
|
+
const res = await upsert({
|
|
247
|
+
name: "uses-process",
|
|
248
|
+
// The script typings omit process, but a cast still typechecks.
|
|
249
|
+
source:
|
|
250
|
+
"export default async (): Promise<string> => (globalThis as any).process.env.HOME ?? '';",
|
|
251
|
+
});
|
|
252
|
+
expect(res.status).toBe(200);
|
|
253
|
+
expect(await res.json()).toEqual({
|
|
254
|
+
name: "uses-process",
|
|
255
|
+
version: 1,
|
|
256
|
+
contentDeduped: false,
|
|
257
|
+
warnings: ["quickjs compat (process, line 1): process is not available under quickjs"],
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
245
261
|
test("upsert typecheck failures return diagnostics and do not write rows", async () => {
|
|
246
262
|
const res = await upsert({
|
|
247
263
|
name: "bad-types",
|
|
@@ -438,6 +438,16 @@ describe("registerVolatileSecret", () => {
|
|
|
438
438
|
expect(scrubSecrets("http://127.0.0.1:9/v1")).toBe("http://127.0.0.1:9/v1");
|
|
439
439
|
});
|
|
440
440
|
|
|
441
|
+
test("padded singleton values redact their trimmed form", () => {
|
|
442
|
+
const fromEnv = `opaque${randomToken(20)}`;
|
|
443
|
+
const fromMap = `opaque${randomToken(20)}`;
|
|
444
|
+
process.env.PADDED_API_KEY = ` ${fromEnv}\n`;
|
|
445
|
+
refreshSecretScrubberCache();
|
|
446
|
+
registerEnvSecrets({ HELMCODE_API_NAN: ` ${fromMap} ` });
|
|
447
|
+
expect(scrubSecrets(`key: ${fromEnv}`)).toBe("key: [REDACTED:PADDED_API_KEY]");
|
|
448
|
+
expect(scrubSecrets(`key: ${fromMap}`)).toBe("key: [REDACTED:HELMCODE_API_NAN]");
|
|
449
|
+
});
|
|
450
|
+
|
|
441
451
|
test("ignores values shorter than the minimum length", () => {
|
|
442
452
|
registerVolatileSecret("short", "TOO_SHORT");
|
|
443
453
|
const out = scrubSecrets("contains short somewhere");
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// The shipped `<harness>-<tier>` seeds, and migration 220's profiles made from
|
|
2
|
+
// agents' MODEL_OVERRIDE + REASONING_EFFORT_OVERRIDE.
|
|
3
|
+
|
|
4
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { unlink } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
closeDb,
|
|
10
|
+
createAgent,
|
|
11
|
+
getDb,
|
|
12
|
+
getDbClient,
|
|
13
|
+
getSwarmConfigs,
|
|
14
|
+
initDb,
|
|
15
|
+
upsertSwarmConfig,
|
|
16
|
+
} from "../be/db";
|
|
17
|
+
import { getModelProfileByName, listModelProfiles } from "../be/db/model-profiles";
|
|
18
|
+
import { recordClaimModelResolution } from "../be/model-tier-resolution";
|
|
19
|
+
import { seedModelProfiles } from "../be/seed-model-profiles";
|
|
20
|
+
import { DEFAULT_MODEL_TIER_MAP, type MODEL_TIERS, type ProviderName } from "../types";
|
|
21
|
+
|
|
22
|
+
const TEST_DB_PATH = "./test-seed-model-profiles.sqlite";
|
|
23
|
+
|
|
24
|
+
async function removeDbFiles(path: string): Promise<void> {
|
|
25
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
26
|
+
try {
|
|
27
|
+
await unlink(path + suffix);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The part of migration 220 that turns agent overrides into profiles. */
|
|
35
|
+
function agentOverrideMigrationSql(): string {
|
|
36
|
+
const sql = readFileSync(
|
|
37
|
+
join(import.meta.dir, "../be/migrations/220_model_profiles.sql"),
|
|
38
|
+
"utf8",
|
|
39
|
+
);
|
|
40
|
+
return sql.slice(sql.indexOf("CREATE TEMP TABLE _model_profile_overrides"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
beforeAll(async () => {
|
|
44
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
45
|
+
initDb(TEST_DB_PATH);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterAll(async () => {
|
|
49
|
+
closeDb();
|
|
50
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
beforeEach(async () => {
|
|
54
|
+
const client = getDbClient();
|
|
55
|
+
// References first: migration 220's triggers refuse deleting a named profile.
|
|
56
|
+
await client.run("DELETE FROM agent_tasks");
|
|
57
|
+
await client.run("DELETE FROM swarm_config");
|
|
58
|
+
await client.run("DELETE FROM swarm_repos");
|
|
59
|
+
await client.run("DELETE FROM agents");
|
|
60
|
+
await client.run("DELETE FROM model_profiles");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("tier seeds", () => {
|
|
64
|
+
test("one profile per harness and tier, copied from the tier map; none for acp", async () => {
|
|
65
|
+
expect(await seedModelProfiles()).toBe(40);
|
|
66
|
+
const profiles = await listModelProfiles();
|
|
67
|
+
expect(profiles).toHaveLength(40);
|
|
68
|
+
expect(profiles.some((p) => p.harness === "acp")).toBe(false);
|
|
69
|
+
for (const profile of profiles) {
|
|
70
|
+
const tier = profile.seededFrom?.replace("tier:", "") as (typeof MODEL_TIERS)[number];
|
|
71
|
+
expect(profile.name).toBe(`${profile.harness}-${tier}`);
|
|
72
|
+
expect(profile.model).toBe(DEFAULT_MODEL_TIER_MAP[profile.harness as ProviderName][tier]);
|
|
73
|
+
expect(profile.effort).toBeNull();
|
|
74
|
+
}
|
|
75
|
+
expect((await getModelProfileByName("claude-smart"))?.model).toBe("opus");
|
|
76
|
+
expect((await getModelProfileByName("codex-ultra"))?.model).toBe("gpt-5.6-sol");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("a global MODEL_TIER row overrides the seed's model; reseeding never overwrites", async () => {
|
|
80
|
+
await upsertSwarmConfig({
|
|
81
|
+
scope: "global",
|
|
82
|
+
key: "MODEL_TIER_CLAUDE_SMART",
|
|
83
|
+
value: "claude-opus-5-5",
|
|
84
|
+
});
|
|
85
|
+
await seedModelProfiles();
|
|
86
|
+
expect((await getModelProfileByName("claude-smart"))?.model).toBe("claude-opus-5-5");
|
|
87
|
+
|
|
88
|
+
await getDbClient().run(
|
|
89
|
+
"UPDATE model_profiles SET model = 'edited' WHERE name = 'claude-regular'",
|
|
90
|
+
);
|
|
91
|
+
expect(await seedModelProfiles()).toBe(0);
|
|
92
|
+
expect((await getModelProfileByName("claude-regular"))?.model).toBe("edited");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("migration 220: profiles from agent overrides", () => {
|
|
97
|
+
async function agentWith(
|
|
98
|
+
name: string,
|
|
99
|
+
harnessProvider: ProviderName,
|
|
100
|
+
model?: string,
|
|
101
|
+
effort?: string,
|
|
102
|
+
): Promise<string> {
|
|
103
|
+
const agent = await createAgent({ name, isLead: false, status: "idle", harnessProvider });
|
|
104
|
+
if (model) {
|
|
105
|
+
await upsertSwarmConfig({
|
|
106
|
+
scope: "agent",
|
|
107
|
+
scopeId: agent.id,
|
|
108
|
+
key: "MODEL_OVERRIDE",
|
|
109
|
+
value: model,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (effort) {
|
|
113
|
+
await upsertSwarmConfig({
|
|
114
|
+
scope: "agent",
|
|
115
|
+
scopeId: agent.id,
|
|
116
|
+
key: "REASONING_EFFORT_OVERRIDE",
|
|
117
|
+
value: effort,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return agent.id;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
test("dedupes by (harness, model, effort) and makes each the agent's default", async () => {
|
|
124
|
+
const a = await agentWith("m-a", "claude", "claude-opus-5-5", "high");
|
|
125
|
+
const b = await agentWith("m-b", "claude", "claude-opus-5-5", "high");
|
|
126
|
+
const c = await agentWith("m-c", "codex", "gpt-6.1-sol", "xhigh");
|
|
127
|
+
const d = await agentWith("m-d", "claude", "openrouter/vendor/model:free");
|
|
128
|
+
const skipped = [
|
|
129
|
+
await agentWith("m-acp", "acp", "some-acp-model"),
|
|
130
|
+
await agentWith("m-alias", "claude", "latest:anthropic/opus"),
|
|
131
|
+
await agentWith("m-none", "claude"),
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
getDb().exec(agentOverrideMigrationSql());
|
|
135
|
+
|
|
136
|
+
const profiles = await listModelProfiles();
|
|
137
|
+
expect(profiles.map((p) => [p.name, p.harness, p.model, p.effort])).toEqual([
|
|
138
|
+
["claude-openrouter-vendor-model-free", "claude", "openrouter/vendor/model:free", null],
|
|
139
|
+
["claude-opus-5-5-high", "claude", "claude-opus-5-5", "high"],
|
|
140
|
+
["codex-gpt-6.1-sol-xhigh", "codex", "gpt-6.1-sol", "xhigh"],
|
|
141
|
+
]);
|
|
142
|
+
expect(profiles.every((p) => p.seededFrom === "agent-config" && p.allowCustomModel)).toBe(true);
|
|
143
|
+
|
|
144
|
+
const defaults = await getSwarmConfigs({ scope: "agent", key: "MODEL_PROFILE" });
|
|
145
|
+
expect(Object.fromEntries(defaults.map((row) => [row.scopeId, row.value]))).toEqual({
|
|
146
|
+
[a]: "claude-opus-5-5-high",
|
|
147
|
+
[b]: "claude-opus-5-5-high",
|
|
148
|
+
[c]: "codex-gpt-6.1-sol-xhigh",
|
|
149
|
+
[d]: "claude-openrouter-vendor-model-free",
|
|
150
|
+
});
|
|
151
|
+
expect(defaults.every((row) => row.exportToEnv === false)).toBe(true);
|
|
152
|
+
expect(defaults.some((row) => skipped.includes(row.scopeId ?? ""))).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("an overridden agent's untiered task resolves to the same model and effort as before", async () => {
|
|
156
|
+
const id = await agentWith("m-same", "codex", "gpt-6.1-sol", "xhigh");
|
|
157
|
+
getDb().exec(agentOverrideMigrationSql());
|
|
158
|
+
const task = await getDbClient().get<{ id: string }>(
|
|
159
|
+
`INSERT INTO agent_tasks (id, agentId, task, status, source, createdAt, lastUpdatedAt)
|
|
160
|
+
VALUES ('t-same', ?, 'work', 'pending', 'mcp', '2026-01-01', '2026-01-01') RETURNING id`,
|
|
161
|
+
[id],
|
|
162
|
+
);
|
|
163
|
+
const fields = await recordClaimModelResolution(
|
|
164
|
+
{ id: task?.id ?? "" },
|
|
165
|
+
{ id, harnessProvider: "codex" },
|
|
166
|
+
);
|
|
167
|
+
expect(fields).toMatchObject({
|
|
168
|
+
resolvedModel: "gpt-6.1-sol",
|
|
169
|
+
modelSource: "profile:agent-default",
|
|
170
|
+
resolvedProfile: "codex-gpt-6.1-sol-xhigh",
|
|
171
|
+
effort: "xhigh",
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -56,8 +56,8 @@ afterAll(async () => {
|
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
describe("seed-scripts catalog", () => {
|
|
59
|
-
test("manifest holds
|
|
60
|
-
expect(SEED_SCRIPTS.length).toBe(
|
|
59
|
+
test("manifest holds 28 unique, well-described scripts", () => {
|
|
60
|
+
expect(SEED_SCRIPTS.length).toBe(28);
|
|
61
61
|
const names = SEED_SCRIPTS.map((s) => s.name);
|
|
62
62
|
expect(new Set(names).size).toBe(names.length);
|
|
63
63
|
for (const s of SEED_SCRIPTS) {
|
|
@@ -78,6 +78,7 @@ afterAll(async () => {
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
describe("migration 196 skill invocations", () => {
|
|
81
|
+
// The full migration chain on a fresh DB can be slow under CI shard load.
|
|
81
82
|
test("fresh DB has the counter columns and the invocations table", () => {
|
|
82
83
|
const db = new Database(MIGRATION_DB_PATH, { create: true });
|
|
83
84
|
try {
|
|
@@ -109,8 +110,9 @@ describe("migration 196 skill invocations", () => {
|
|
|
109
110
|
} finally {
|
|
110
111
|
db.close();
|
|
111
112
|
}
|
|
112
|
-
});
|
|
113
|
+
}, 30_000);
|
|
113
114
|
|
|
115
|
+
// The full migration chain on a fresh DB can be slow under CI shard load.
|
|
114
116
|
test("existing DB keeps its skills and starts them at zero", async () => {
|
|
115
117
|
await removeDb(MIGRATION_DB_PATH);
|
|
116
118
|
const db = new Database(MIGRATION_DB_PATH, { create: true });
|
|
@@ -143,7 +145,7 @@ describe("migration 196 skill invocations", () => {
|
|
|
143
145
|
} finally {
|
|
144
146
|
db.close();
|
|
145
147
|
}
|
|
146
|
-
});
|
|
148
|
+
}, 30_000);
|
|
147
149
|
});
|
|
148
150
|
|
|
149
151
|
describe("skill.invoke ingestion", () => {
|
|
@@ -192,6 +192,42 @@ describe("buildCompletedBlocks", () => {
|
|
|
192
192
|
});
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
+
describe("completed status icon override", () => {
|
|
196
|
+
test("buildCompletedBlocks and tree lines use SLACK_STATUS_ICON_COMPLETED", () => {
|
|
197
|
+
process.env.SLACK_STATUS_ICON_COMPLETED = "bee-lgtm";
|
|
198
|
+
try {
|
|
199
|
+
const card = buildCompletedBlocks({
|
|
200
|
+
agentName: "Alpha",
|
|
201
|
+
taskId: "abcdef12-3456-7890-abcd-ef1234567890",
|
|
202
|
+
body: "Task output here",
|
|
203
|
+
});
|
|
204
|
+
expect(card[0].text.text.startsWith(":bee-lgtm: *Alpha*")).toBe(true);
|
|
205
|
+
|
|
206
|
+
const tree = buildTreeBlocks([
|
|
207
|
+
{
|
|
208
|
+
taskId: makeTaskId("icon0001"),
|
|
209
|
+
agentName: "Lead",
|
|
210
|
+
status: "completed",
|
|
211
|
+
children: [
|
|
212
|
+
{
|
|
213
|
+
taskId: makeTaskId("icon0002"),
|
|
214
|
+
agentName: "Worker",
|
|
215
|
+
status: "failed",
|
|
216
|
+
children: [],
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
]);
|
|
221
|
+
const text = tree[0].text.text;
|
|
222
|
+
expect(text).toContain(":bee-lgtm: *Lead*");
|
|
223
|
+
expect(text).toContain("❌ *Worker*");
|
|
224
|
+
expect(text).not.toContain("✅");
|
|
225
|
+
} finally {
|
|
226
|
+
delete process.env.SLACK_STATUS_ICON_COMPLETED;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
195
231
|
describe("buildFailedBlocks", () => {
|
|
196
232
|
test("returns single-line header + error section", () => {
|
|
197
233
|
const blocks = buildFailedBlocks({
|
|
@@ -71,4 +71,18 @@ describe("Slack inline completion output", () => {
|
|
|
71
71
|
expect(text).toContain("|`abcdef12`>");
|
|
72
72
|
expect(text).toContain(finalMarker);
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
test("the inline output header uses SLACK_STATUS_ICON_COMPLETED", () => {
|
|
76
|
+
process.env.SLACK_STATUS_ICON_COMPLETED = ":bee-lgtm:";
|
|
77
|
+
try {
|
|
78
|
+
const [first] = formatInlineCompletionOutputChunks({
|
|
79
|
+
agentName: "Analyst",
|
|
80
|
+
taskId: TASK_ID,
|
|
81
|
+
output: "Done.",
|
|
82
|
+
});
|
|
83
|
+
expect(first?.startsWith(":bee-lgtm: *Analyst* completed with output")).toBe(true);
|
|
84
|
+
} finally {
|
|
85
|
+
delete process.env.SLACK_STATUS_ICON_COMPLETED;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
74
88
|
});
|
|
@@ -2,6 +2,7 @@ import { afterAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
|
2
2
|
import * as otelModule from "../otel";
|
|
3
3
|
import { ackSlackMessage, finalizeSlackMessageReaction } from "../slack/ack";
|
|
4
4
|
import {
|
|
5
|
+
completedStatusIcon,
|
|
5
6
|
normalizeSlackReactionShortcode,
|
|
6
7
|
reactionName,
|
|
7
8
|
SLACK_REACTION_CONFIG_KEYS,
|
|
@@ -114,6 +115,25 @@ describe("reaction-shortcode.ts", () => {
|
|
|
114
115
|
expect(normalizeSlackReactionShortcode("thumbsup::skin-tone-6::skin-tone-6")).toBeNull();
|
|
115
116
|
});
|
|
116
117
|
|
|
118
|
+
test("completedStatusIcon defaults to ✅ and renders a configured name as :name:", () => {
|
|
119
|
+
const previous = process.env.SLACK_STATUS_ICON_COMPLETED;
|
|
120
|
+
try {
|
|
121
|
+
delete process.env.SLACK_STATUS_ICON_COMPLETED;
|
|
122
|
+
expect(completedStatusIcon()).toBe("✅");
|
|
123
|
+
for (const value of ["bee-lgtm", ":bee-lgtm:", " :Bee-LGTM: "]) {
|
|
124
|
+
process.env.SLACK_STATUS_ICON_COMPLETED = value;
|
|
125
|
+
expect(completedStatusIcon()).toBe(":bee-lgtm:");
|
|
126
|
+
}
|
|
127
|
+
for (const bad of ["", "::", "bee lgtm", "✅", "bee:lgtm"]) {
|
|
128
|
+
process.env.SLACK_STATUS_ICON_COMPLETED = bad;
|
|
129
|
+
expect(completedStatusIcon()).toBe("✅");
|
|
130
|
+
}
|
|
131
|
+
} finally {
|
|
132
|
+
if (previous === undefined) delete process.env.SLACK_STATUS_ICON_COMPLETED;
|
|
133
|
+
else process.env.SLACK_STATUS_ICON_COMPLETED = previous;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
117
137
|
test("no Slack source file outside ack.ts names a reaction shortcode or calls reactions.add", async () => {
|
|
118
138
|
const glob = new Bun.Glob("*.ts");
|
|
119
139
|
const dir = new URL("../slack/", import.meta.url);
|
|
@@ -1116,6 +1116,43 @@ describe("Slack renderer v2", () => {
|
|
|
1116
1116
|
expect(doneText.startsWith("🧵 ✅ done")).toBe(true);
|
|
1117
1117
|
});
|
|
1118
1118
|
|
|
1119
|
+
test("the completed glyph follows SLACK_STATUS_ICON_COMPLETED in tree, header and cards", async () => {
|
|
1120
|
+
const lead = await createAgent({ name: "Icon Lead", isLead: true, status: "idle" });
|
|
1121
|
+
const ask = await createTaskExtended("icon ask", { agentId: lead.id, source: "slack" });
|
|
1122
|
+
const now = new Date();
|
|
1123
|
+
const done = {
|
|
1124
|
+
...ask,
|
|
1125
|
+
status: "completed" as const,
|
|
1126
|
+
output: "Shipped.",
|
|
1127
|
+
lastUpdatedAt: now.toISOString(),
|
|
1128
|
+
finishedAt: now.toISOString(),
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
const defaultTree = await renderThreadTree([done], new Map(), now);
|
|
1132
|
+
expect(defaultTree.startsWith("🧵 ✅ done")).toBe(true);
|
|
1133
|
+
expect(defaultTree).toContain("↳ ✅ ");
|
|
1134
|
+
expect(await childOutcomeContent(done, false)).toBe("↳ ✅ Icon Lead — result: Shipped.");
|
|
1135
|
+
|
|
1136
|
+
process.env.SLACK_STATUS_ICON_COMPLETED = ":bee-lgtm:";
|
|
1137
|
+
try {
|
|
1138
|
+
const tree = await renderThreadTree([done], new Map(), now);
|
|
1139
|
+
expect(tree.startsWith("🧵 :bee-lgtm: done")).toBe(true);
|
|
1140
|
+
expect(tree).toContain("↳ :bee-lgtm: ");
|
|
1141
|
+
expect(tree).not.toContain("✅");
|
|
1142
|
+
expect(await childOutcomeContent(done, false)).toBe(
|
|
1143
|
+
"↳ :bee-lgtm: Icon Lead — result: Shipped.",
|
|
1144
|
+
);
|
|
1145
|
+
expect(await childOutcomeContent(done, true)).toBe("↳ :bee-lgtm: Icon Lead completed");
|
|
1146
|
+
|
|
1147
|
+
// Only the completed glyph moves; failure keeps its own icon.
|
|
1148
|
+
expect(
|
|
1149
|
+
await childOutcomeContent({ ...done, status: "failed", failureReason: "boom" }, false),
|
|
1150
|
+
).toBe("↳ ❌ Icon Lead — failed: boom");
|
|
1151
|
+
} finally {
|
|
1152
|
+
delete process.env.SLACK_STATUS_ICON_COMPLETED;
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1119
1156
|
test("a parked deferral reads waiting, and done once its wake-up settles", async () => {
|
|
1120
1157
|
const lead = await createAgent({ name: "Parked Header Lead", isLead: true, status: "idle" });
|
|
1121
1158
|
const ask = await createTaskExtended("parked header ask", {
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
import { unlinkSync } from "node:fs";
|
|
3
3
|
import {
|
|
4
4
|
closeDb,
|
|
5
5
|
createAgent,
|
|
6
6
|
createTaskExtended,
|
|
7
7
|
createUser,
|
|
8
|
+
getDbClient,
|
|
8
9
|
getLatestActiveTaskInThread,
|
|
9
10
|
getMostRecentTaskInThread,
|
|
10
11
|
getSlackTasksInThread,
|
|
11
12
|
initDb,
|
|
12
13
|
} from "../be/db";
|
|
14
|
+
import { installExtension, setExtensionState } from "../be/extensions/db";
|
|
13
15
|
import { type IdentityActor, linkIdentity } from "../be/users";
|
|
16
|
+
import { registerLoaded, unregister } from "../extensions/dispatcher";
|
|
17
|
+
import { ensureExtensionAgent } from "../extensions/identity";
|
|
18
|
+
import { cleanExtensionTmpRoot, loadExtension } from "../extensions/loader";
|
|
14
19
|
import { registerMessageHandler } from "../slack/handlers";
|
|
15
20
|
import {
|
|
16
21
|
bufferThreadMessage,
|
|
@@ -18,6 +23,7 @@ import {
|
|
|
18
23
|
instantFlush,
|
|
19
24
|
isThreadBuffered,
|
|
20
25
|
} from "../slack/thread-buffer";
|
|
26
|
+
import { loadBundleFixture } from "./fixtures/extensions/load";
|
|
21
27
|
|
|
22
28
|
process.env.SLACK_RENDER_V2 = "false";
|
|
23
29
|
|
|
@@ -73,6 +79,27 @@ afterAll(() => {
|
|
|
73
79
|
}
|
|
74
80
|
});
|
|
75
81
|
|
|
82
|
+
async function loadBlockingExtension(reason: string) {
|
|
83
|
+
const bundle = await loadBundleFixture("minimal");
|
|
84
|
+
const name = "slack-buffer-block-test";
|
|
85
|
+
bundle.manifest = { ...bundle.manifest, name };
|
|
86
|
+
bundle.files[bundle.manifest.assets.hooks] = `
|
|
87
|
+
import { block, type SwarmExtension } from "swarm-extension";
|
|
88
|
+
const extension: SwarmExtension = (api) => {
|
|
89
|
+
api.on("pre.task.create", () => block(${JSON.stringify(reason)}));
|
|
90
|
+
};
|
|
91
|
+
export default extension;
|
|
92
|
+
`;
|
|
93
|
+
const installed = await installExtension(bundle);
|
|
94
|
+
const record = await setExtensionState(installed.extension.id, {
|
|
95
|
+
agentId: await ensureExtensionAgent(name),
|
|
96
|
+
});
|
|
97
|
+
if (!record) throw new Error("Failed to load the blocking Slack test extension");
|
|
98
|
+
const loaded = await loadExtension({ record, ...bundle });
|
|
99
|
+
registerLoaded(loaded);
|
|
100
|
+
return loaded;
|
|
101
|
+
}
|
|
102
|
+
|
|
76
103
|
describe("Slack thread buffer", () => {
|
|
77
104
|
describe("ADDITIVE_SLACK thread ingress", () => {
|
|
78
105
|
const originalAdditiveSlack = process.env.ADDITIVE_SLACK;
|
|
@@ -612,4 +639,74 @@ describe("Slack thread buffer", () => {
|
|
|
612
639
|
expect(task!.task).toContain("solo message");
|
|
613
640
|
});
|
|
614
641
|
});
|
|
642
|
+
|
|
643
|
+
test(
|
|
644
|
+
"logs a blocked pre-task.create once at info level with its reason scrubbed",
|
|
645
|
+
async () => {
|
|
646
|
+
const secret = `ghp_${"Z".repeat(36)}`;
|
|
647
|
+
const extensionName = "slack-buffer-block-test";
|
|
648
|
+
const loaded = await loadBlockingExtension(`Ticket required: ${secret}`);
|
|
649
|
+
const info = spyOn(console, "info").mockImplementation(() => {});
|
|
650
|
+
const error = spyOn(console, "error").mockImplementation(() => {});
|
|
651
|
+
const channelId = "C_BLOCKED_FLUSH";
|
|
652
|
+
const threadTs = "14000.0001";
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
bufferThreadMessage(channelId, threadTs, "follow-up", "U1", "14000.0002");
|
|
656
|
+
await instantFlush(`${channelId}:${threadTs}`);
|
|
657
|
+
|
|
658
|
+
expect(info).toHaveBeenCalledTimes(1);
|
|
659
|
+
expect(info.mock.calls[0]?.[0]).toBe("[Slack] pre.task.create blocked:");
|
|
660
|
+
expect(info.mock.calls[0]?.[1]).toBe(extensionName);
|
|
661
|
+
expect(String(info.mock.calls[0]?.[2])).toContain("[REDACTED:");
|
|
662
|
+
expect(String(info.mock.calls[0]?.[2])).not.toContain(secret);
|
|
663
|
+
expect(error).not.toHaveBeenCalled();
|
|
664
|
+
expect(await getMostRecentTaskInThread(channelId, threadTs)).toBeNull();
|
|
665
|
+
} finally {
|
|
666
|
+
unregister(loaded.record.id);
|
|
667
|
+
await loaded.dispose();
|
|
668
|
+
info.mockRestore();
|
|
669
|
+
error.mockRestore();
|
|
670
|
+
await cleanExtensionTmpRoot();
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
{ timeout: 15_000 },
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
test(
|
|
677
|
+
"does not treat unexpected task creation errors as blocked",
|
|
678
|
+
async () => {
|
|
679
|
+
const triggerName = "fail_slack_buffer_task_creation";
|
|
680
|
+
const failure = "simulated Slack task creation failure";
|
|
681
|
+
const channelId = "C_UNEXPECTED_FLUSH";
|
|
682
|
+
const threadTs = "14000.0003";
|
|
683
|
+
const info = spyOn(console, "info").mockImplementation(() => {});
|
|
684
|
+
const error = spyOn(console, "error").mockImplementation(() => {});
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
await getDbClient().run(`DROP TRIGGER IF EXISTS ${triggerName}`);
|
|
688
|
+
await getDbClient().run(`
|
|
689
|
+
CREATE TRIGGER ${triggerName}
|
|
690
|
+
BEFORE INSERT ON agent_tasks
|
|
691
|
+
WHEN NEW.slackChannelId = '${channelId}'
|
|
692
|
+
BEGIN
|
|
693
|
+
SELECT RAISE(ABORT, '${failure}');
|
|
694
|
+
END
|
|
695
|
+
`);
|
|
696
|
+
|
|
697
|
+
bufferThreadMessage(channelId, threadTs, "follow-up", "U1", "14000.0004");
|
|
698
|
+
await instantFlush(`${channelId}:${threadTs}`);
|
|
699
|
+
|
|
700
|
+
expect(info).not.toHaveBeenCalled();
|
|
701
|
+
expect(error).toHaveBeenCalledTimes(1);
|
|
702
|
+
expect(String(error.mock.calls[0]?.[1])).toContain(failure);
|
|
703
|
+
expect(await getMostRecentTaskInThread(channelId, threadTs)).toBeNull();
|
|
704
|
+
} finally {
|
|
705
|
+
await getDbClient().run(`DROP TRIGGER IF EXISTS ${triggerName}`);
|
|
706
|
+
info.mockRestore();
|
|
707
|
+
error.mockRestore();
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
{ timeout: 15_000 },
|
|
711
|
+
);
|
|
615
712
|
});
|