@desplega.ai/agent-swarm 2.7.6 → 2.7.7
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-bp91d37f.js → acp-adapter-7b4vk3h5.js} +5 -4
- package/dist/{actions-yj0eeaqh.js → actions-3tt153af.js} +18 -18
- package/dist/{amp-adapter-4pg9h34q.js → amp-adapter-njpts0yr.js} +1 -1
- package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
- package/dist/{app-909e97cr.js → app-nky4378f.js} +3 -3
- package/dist/{assistant-qdyb5fyy.js → assistant-jnb669pa.js} +19 -19
- package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
- package/dist/{boot-reembed-fk0s9z18.js → boot-reembed-9mgvaztt.js} +1 -1
- package/dist/{boot-reembed-gs1rzrnh.js → boot-reembed-qs2djdej.js} +1 -1
- package/dist/{boot-scrub-sweep-4qfd4kfx.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
- package/dist/{boot-seeds-yr00k13d.js → boot-seeds-5cm6ambd.js} +4 -4
- package/dist/{boxd-vdmfssd9.js → boxd-nv507va8.js} +1 -1
- package/dist/{claude-adapter-qta1f1xn.js → claude-adapter-fn287zmm.js} +5 -4
- package/dist/{claude-managed-setup-sc6j4dm0.js → claude-managed-setup-wtdz2mj2.js} +1 -1
- package/dist/{claude-sdk-session-zgrz6yvc.js → claude-sdk-session-x91kqyhm.js} +8288 -8224
- package/dist/{cli-wp9sp1qb.js → cli-08bmz9z9.js} +1 -1
- package/dist/{cli-yy3rhsdw.js → cli-1c6tzt8g.js} +7 -7
- package/dist/{cli-q0k1w5zd.js → cli-35rsp8ye.js} +1 -1
- package/dist/{cli-xnrwazav.js → cli-3fg0bdcv.js} +1 -1
- package/dist/{cli-89knwags.js → cli-5b456bpf.js} +2 -2
- package/dist/{cli-drr5v6av.js → cli-5jnhv72d.js} +1 -1
- package/dist/{cli-dxae2yg7.js → cli-5zx699j9.js} +2 -2
- package/dist/{cli-0at8w34w.js → cli-6g0hb4cp.js} +8 -8
- package/dist/{cli-e3r2neck.js → cli-6pfd54vx.js} +5 -5
- package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
- package/dist/{cli-ekwks2vs.js → cli-72v799xn.js} +1 -1
- package/dist/cli-82j36f2m.js +45 -0
- package/dist/{cli-yx4kfngt.js → cli-8305gbq0.js} +29 -16
- package/dist/{cli-4necwtsv.js → cli-8sckt74b.js} +8 -8
- package/dist/{cli-vvpx3gsx.js → cli-95jzxcp3.js} +1 -1
- package/dist/{cli-hr2e2fft.js → cli-adeegh7d.js} +7 -7
- package/dist/{cli-m48g6p95.js → cli-afkqkrtw.js} +3 -3
- package/dist/{cli-0qatpy8q.js → cli-ayayvfg4.js} +24 -4
- package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
- package/dist/{cli-zm72anhg.js → cli-bfv7vdg6.js} +9 -9
- package/dist/{cli-hgaazk69.js → cli-caj33vcq.js} +5 -5
- package/dist/{cli-3ypk9vk1.js → cli-cb2a8pna.js} +1 -1
- package/dist/{cli-waqbxg08.js → cli-cppkf404.js} +17 -17
- package/dist/{cli-yn4kxs06.js → cli-ddaanqkd.js} +4 -4
- package/dist/{cli-386tyxxg.js → cli-e0kmphd6.js} +2 -2
- package/dist/{cli-54t00gt8.js → cli-e46hg0xw.js} +1 -1
- package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
- package/dist/{cli-436532ck.js → cli-excnnjg2.js} +165 -153
- package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
- package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
- package/dist/{cli-pd7vagab.js → cli-fyt2g4wd.js} +3 -3
- package/dist/{cli-gdhpcbhf.js → cli-g08zdaec.js} +1 -1
- package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
- package/dist/{cli-svj5090z.js → cli-gz6q4eve.js} +3 -3
- package/dist/{cli-8snvgs5r.js → cli-je518txm.js} +10 -10
- package/dist/{cli-rmmmrjkd.js → cli-jtjtabxe.js} +1 -1
- package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
- package/dist/{cli-es6va8jk.js → cli-k1nje3v3.js} +38 -43
- package/dist/{cli-p5gp8pcx.js → cli-k8196sh2.js} +6 -6
- package/dist/{cli-nwj0ee2w.js → cli-k899ehvh.js} +2 -2
- package/dist/{cli-decfb6v8.js → cli-m0da8j35.js} +23 -9
- package/dist/{cli-he9wchme.js → cli-mkk9rnzz.js} +4 -4
- package/dist/{cli-nhrvpaj7.js → cli-n6bv12jn.js} +510 -4
- package/dist/{cli-8bc1w0tw.js → cli-nmzhp1b5.js} +14 -12
- package/dist/{cli-0yv5z2pg.js → cli-p82cnnjq.js} +1 -1
- package/dist/{cli-c60vetmm.js → cli-pc6n7fre.js} +1 -1
- package/dist/{cli-xds5f77d.js → cli-pjctyea3.js} +2 -2
- package/dist/{cli-xbbndvjy.js → cli-pq73h5mz.js} +3 -3
- package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
- package/dist/{cli-xnqgpshy.js → cli-q3tkegby.js} +1 -1
- package/dist/cli-qghe0h66.js +67 -0
- package/dist/{cli-zbbwez0r.js → cli-qh4d4tvk.js} +4 -4
- package/dist/{cli-w6dt9ged.js → cli-rtt8g4v9.js} +1 -1
- package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
- package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
- package/dist/{cli-faxe6thg.js → cli-vz22fyqv.js} +186 -54
- package/dist/{cli-k2gzwsj2.js → cli-wk5yrfwd.js} +160 -26
- package/dist/{cli-shpbx5ee.js → cli-ws19768n.js} +1641 -49
- package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
- package/dist/{cli-kqdg1bsb.js → cli-yndmfxb3.js} +2 -2
- package/dist/{cli-bjb3z51z.js → cli-yyj3j5m4.js} +2 -2
- package/dist/cli.js +21 -21
- package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
- package/dist/{codex-adapter-ayd1fb6h.js → codex-adapter-cf2yxe9m.js} +3 -2
- package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
- package/dist/{codex-session-runner-jesa32xc.js → codex-session-runner-6vfzdgbd.js} +3 -2
- package/dist/{commands-hanewgf2.js → commands-t3mfy7vn.js} +3 -3
- package/dist/{controller-dt1n3c1q.js → controller-q7xrbaz0.js} +3 -3
- package/dist/{controllers-connect-xx8x64n5.js → controllers-connect-c6nm70kg.js} +19 -19
- package/dist/{cursor-adapter-9s4dwwdw.js → cursor-adapter-ftd81ga8.js} +1 -1
- package/dist/{db-3vttcbv4.js → db-dk572x25.js} +3 -3
- package/dist/{db-9dv0g1ab.js → db-vemtpmcc.js} +4 -2
- package/dist/{dispatcher-tx9hd1sv.js → dispatcher-kh7ecxh9.js} +17 -17
- package/dist/{dsh-adapter-007y4ejs.js → dsh-adapter-v8628r89.js} +1 -1
- package/dist/{e2b-we6hs6q5.js → e2b-fr12yccx.js} +1 -1
- package/dist/{e2b-a19d28rz.js → e2b-x6ywzak3.js} +1 -1
- package/dist/{easter-egg-3d-rgbrqb8s.js → easter-egg-3d-nenw4x63.js} +1 -1
- package/dist/{embedding-space-ny1e2940.js → embedding-space-0jepb239.js} +6 -6
- package/dist/{embeddings-2t386t13.js → embeddings-xxt49yxg.js} +1 -1
- package/dist/{execute-pksh0ceh.js → execute-1mhw6e4x.js} +1 -1
- package/dist/{external-channel-phgmhmd7.js → external-channel-55v12e60.js} +2 -2
- package/dist/{fx-adapter-fy8rr921.js → fx-adapter-bs8wqrk5.js} +6 -5
- package/dist/{google-generative-ai-8y3m3k4b.js → google-generative-ai-v94hr7h0.js} +5 -5
- package/dist/{google-vertex-w3v98n7b.js → google-vertex-g7vr6cf8.js} +5 -5
- package/dist/{grok-adapter-q0abvpaj.js → grok-adapter-gpjqmkcq.js} +6 -5
- package/dist/handlers-zxmp1ta4.js +32 -0
- package/dist/{heartbeat-kadyek8p.js → heartbeat-k42svnr0.js} +20 -20
- package/dist/{hook-9k551xnj.js → hook-114m7hap.js} +4 -3
- package/dist/{hook-5zs3msf6.js → hook-6nphweh0.js} +24 -9
- package/dist/{http-mhz9vw1f.js → http-r8cc4wz7.js} +1025 -173
- package/dist/index-a8phxqnk.js +37 -0
- package/dist/{index-vdygbq10.js → index-e3s8wmxy.js} +1 -1
- package/dist/{index-q361cg2f.js → index-fpk4bcd9.js} +1 -1
- package/dist/index-k6h00t4c.js +30 -0
- package/dist/{index-rvr00sg7.js → index-nh3wyqdt.js} +1017 -574
- package/dist/index-xrz7te8h.js +33 -0
- package/dist/{keepalive-w56f32cg.js → keepalive-x060nt9p.js} +3 -3
- package/dist/lead-956sd2yc.js +71 -0
- package/dist/{maintenance-7mdgxcea.js → maintenance-2xxfexhf.js} +1 -1
- package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
- package/dist/{oauth-refresh-sweep-29txy9vk.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
- package/dist/{onboard-kpsrkarj.js → onboard-zc48j237.js} +3 -3
- package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
- package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
- package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
- package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
- package/dist/{opencode-adapter-j25vm3rr.js → opencode-adapter-r690h1cr.js} +4 -3
- package/dist/{otel-impl-6kkct7a8.js → otel-impl-r5xbh81q.js} +4 -4
- package/dist/{pi-mono-adapter-exhkzhn2.js → pi-mono-adapter-c7v8sdec.js} +7 -6
- package/dist/{pricing-refresh-n2x7jh6k.js → pricing-refresh-3pmre11v.js} +3 -3
- package/dist/{rbac-roles-81ypx6es.js → rbac-roles-3vbshwsb.js} +1 -1
- package/dist/{rbac-roles-b5htkjby.js → rbac-roles-910mmkgz.js} +2 -2
- package/dist/{render-v2-qn6jmpk4.js → render-v2-pxykkbab.js} +3 -3
- package/dist/{resume-wd30wx23.js → resume-adda0bwy.js} +6 -6
- package/dist/{runtime-4hsg7h60.js → runtime-fnbm7stk.js} +1 -1
- package/dist/{session-access-5qvfevhk.js → session-access-2fvamrdc.js} +4 -4
- package/dist/{session-source-backfill-n1czavbr.js → session-source-backfill-08xvv0sm.js} +2 -2
- package/dist/{setup-dnb56qy5.js → setup-1aqmvmh1.js} +1 -1
- package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
- package/dist/{virtual-modules-76xxv9fs.js → virtual-modules-d2pjk9j1.js} +5 -5
- package/dist/{work-objects-4n21tfkm.js → work-objects-9s0yrm5j.js} +20 -20
- package/dist/{worker-follow-up-77hvzxww.js → worker-follow-up-3q78fqmr.js} +17 -17
- package/dist/worker-y15hy08h.js +71 -0
- package/openapi.json +197 -4
- package/package.json +5 -5
- package/src/apps/definition.ts +6 -5
- package/src/be/db/page-theme-tokens.ts +12 -0
- package/src/be/db/tasks/read.ts +9 -1
- package/src/be/db/tasks/write.ts +19 -0
- package/src/be/db-client.ts +107 -7
- package/src/be/db.ts +34 -2
- package/src/be/migrations/269_pages_design_system.sql +23 -0
- package/src/be/seed/schedules-seeder.ts +0 -10
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
- package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
- package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
- package/src/be/seed-scripts/index.ts +9 -0
- package/src/be/seed-skills/bundled-files.generated.json +3 -3
- package/src/be/swarm-config-guard.ts +32 -0
- package/src/be/task-auto-title-listener.ts +127 -0
- package/src/commands/onboard/agent-fs-version.ts +1 -1
- package/src/commands/profile-sync.ts +151 -16
- package/src/commands/runner.ts +15 -7
- package/src/hooks/hook.ts +20 -9
- package/src/http/index.ts +3 -0
- package/src/http/pages-public.ts +93 -129
- package/src/http/pages.ts +94 -21
- package/src/pages/browser-sdk.ts +48 -21
- package/src/pages/color-literals.ts +115 -0
- package/src/pages/page-head.ts +766 -0
- package/src/pages/version.ts +3 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +1 -0
- package/src/providers/codex-app-server.ts +27 -1
- package/src/providers/pi-mono-extension.ts +30 -15
- package/src/tasks/follow-up-gate.ts +318 -0
- package/src/tasks/worker-follow-up.ts +21 -1
- package/src/tests/codex-app-server.test.ts +59 -0
- package/src/tests/codex-gateway-route.test.ts +50 -1
- package/src/tests/create-page-tool.test.ts +501 -0
- package/src/tests/db-client-busy-retry.test.ts +81 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +2 -0
- package/src/tests/db-tasks-read.test.ts +21 -0
- package/src/tests/fixtures/pages/head-v1.html +56 -0
- package/src/tests/follow-up-gate-shadow.test.ts +513 -0
- package/src/tests/identity-field-budget.test.ts +1 -1
- package/src/tests/internal-ai/credentials.test.ts +62 -0
- package/src/tests/internal-ai/task-title.test.ts +283 -0
- package/src/tests/merge-gate-detect-changes.test.ts +145 -0
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/page-session-dev-cookie.test.ts +188 -0
- package/src/tests/pages-frame-ancestors.test.ts +28 -1
- package/src/tests/pages-head-v2.test.ts +534 -0
- package/src/tests/pages-http.test.ts +330 -0
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-storage.test.ts +182 -0
- package/src/tests/pages-versioning.test.ts +72 -0
- package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
- package/src/tests/profile-sync.test.ts +11 -15
- package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/seed-registry.test.ts +28 -6
- package/src/tests/seed-schedules.test.ts +61 -8
- package/src/tests/seed-scripts.test.ts +142 -2
- package/src/tests/swarm-diff.test.ts +121 -2
- package/src/tests/task-auto-title-config-guard.test.ts +37 -0
- package/src/tests/task-auto-title-listener.test.ts +165 -0
- package/src/tests/theme-import.test.ts +758 -0
- package/src/tests/themes-page-theme.test.ts +155 -0
- package/src/tests/themes-presets.test.ts +172 -0
- package/src/tests/workflow-script-principal.test.ts +2 -1
- package/src/themes/classics.ts +949 -0
- package/src/themes/import.ts +420 -0
- package/src/themes/page-fields.ts +40 -0
- package/src/themes/page-theme.ts +102 -0
- package/src/themes/presets.ts +561 -0
- package/src/themes/tokens.ts +218 -0
- package/src/tools/app-upsert.ts +1 -1
- package/src/tools/create-page.ts +118 -12
- package/src/tools/utils.ts +33 -0
- package/src/types.ts +45 -3
- package/src/utils/constants.ts +19 -0
- package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
- package/src/utils/internal-ai/credentials.ts +146 -93
- package/src/utils/internal-ai/index.ts +10 -0
- package/src/utils/internal-ai/models.ts +18 -0
- package/src/utils/internal-ai/task-title.ts +282 -0
- package/src/utils/page-session.ts +37 -0
- package/templates/schedules/daily-status-report/content.md +88 -2
- package/templates/schedules/daily-swarm-update-check/content.md +16 -40
- package/templates/skills/apps/SKILL.md +1 -1
- package/templates/skills/apps/content.md +1 -1
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/pages/config.json +1 -1
- package/templates/skills/pages/content.md +539 -86
- package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
- package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
- package/templates/skills/pages/files/examples/report-page.html +125 -124
- package/dist/cli-2pnswgh8.js +0 -189
- package/dist/handlers-by3sfxyx.js +0 -32
- package/dist/index-4f2pvxae.js +0 -37
- package/dist/index-98gz7tmp.js +0 -30
- package/dist/index-k08z2cak.js +0 -33
- package/dist/lead-enq35nqp.js +0 -70
- package/dist/worker-gg1576j3.js +0 -70
- package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
- package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<base target="_blank">
|
|
2
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
3
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
4
|
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
|
5
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
6
|
+
<script>
|
|
7
|
+
if (window.tailwind) {
|
|
8
|
+
window.tailwind.config = { corePlugins: { preflight: false } };
|
|
9
|
+
}
|
|
10
|
+
</script>
|
|
11
|
+
<style>
|
|
12
|
+
*, ::before, ::after { box-sizing: border-box; border: 0 solid #e5e7eb; }
|
|
13
|
+
hr { border-top-width: 1px; }
|
|
14
|
+
img, video { display: block; max-width: 100%; height: auto; }
|
|
15
|
+
button, input, optgroup, select, textarea, ::file-selector-button { font: inherit; }
|
|
16
|
+
:root {
|
|
17
|
+
color-scheme: light;
|
|
18
|
+
--swarm-bg: #fbfbfa;
|
|
19
|
+
--swarm-card: #ffffff;
|
|
20
|
+
--swarm-border: #e5e7eb;
|
|
21
|
+
--swarm-text: #111827;
|
|
22
|
+
--swarm-muted: #6b7280;
|
|
23
|
+
--swarm-primary: #3b82f6;
|
|
24
|
+
}
|
|
25
|
+
html, body { background: var(--swarm-bg); color: var(--swarm-text); }
|
|
26
|
+
body {
|
|
27
|
+
font-family: "Space Grotesk", system-ui, sans-serif;
|
|
28
|
+
margin: 0;
|
|
29
|
+
padding: 24px;
|
|
30
|
+
line-height: 1.5;
|
|
31
|
+
}
|
|
32
|
+
code, pre, kbd, samp { font-family: "Space Mono", ui-monospace, monospace; }
|
|
33
|
+
a { color: var(--swarm-primary); }
|
|
34
|
+
::selection { background: var(--swarm-primary); color: #fff; }
|
|
35
|
+
@media print {
|
|
36
|
+
/* Override theme variables so built-in primitives (swarm-diff, swarm-card)
|
|
37
|
+
that read var(--swarm-card) / var(--swarm-border) etc. inline-styled
|
|
38
|
+
backgrounds also flip to light. Without this, diff cards stay dark navy
|
|
39
|
+
on print. */
|
|
40
|
+
:root {
|
|
41
|
+
--swarm-bg: #ffffff;
|
|
42
|
+
--swarm-card: #ffffff;
|
|
43
|
+
--swarm-border: #cccccc;
|
|
44
|
+
--swarm-text: #000000;
|
|
45
|
+
--swarm-muted: #555555;
|
|
46
|
+
}
|
|
47
|
+
html, body { background: white !important; color: black !important; }
|
|
48
|
+
a { color: black !important; text-decoration: underline; }
|
|
49
|
+
/* Hide any swarm chrome the agent (or built-in primitives) tagged with
|
|
50
|
+
.no-print. Use this class on annotation badges, jump-list nav, anything
|
|
51
|
+
that shouldn't appear in the PDF export. */
|
|
52
|
+
.no-print { display: none !important; }
|
|
53
|
+
/* Avoid page-break inside cards / diff blocks. */
|
|
54
|
+
.swarm-card, swarm-diff { break-inside: avoid; }
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
closeDb,
|
|
4
|
+
createAgent,
|
|
5
|
+
createTaskExtended,
|
|
6
|
+
getDbClient,
|
|
7
|
+
getLogsByTaskIdAndEventType,
|
|
8
|
+
getTaskById,
|
|
9
|
+
initDb,
|
|
10
|
+
startTask,
|
|
11
|
+
} from "../be/db";
|
|
12
|
+
import { upsertScriptByName } from "../be/scripts/db";
|
|
13
|
+
import * as savedScriptRunner from "../be/scripts/run-saved";
|
|
14
|
+
import { validateConfigValue } from "../be/swarm-config-guard";
|
|
15
|
+
import type { RunScriptOutput } from "../scripts-runtime/loader";
|
|
16
|
+
import {
|
|
17
|
+
classifyFollowUpGate,
|
|
18
|
+
drainFollowUpGateShadowForTest,
|
|
19
|
+
type FollowUpGateDeps,
|
|
20
|
+
readFollowUpGateConfig,
|
|
21
|
+
setFollowUpGateDepsForTests,
|
|
22
|
+
setFollowUpGateTimeoutMsForTests,
|
|
23
|
+
} from "../tasks/follow-up-gate";
|
|
24
|
+
import { createWorkerTaskFollowUp } from "../tasks/worker-follow-up";
|
|
25
|
+
import type { AgentTask } from "../types";
|
|
26
|
+
|
|
27
|
+
const GATED = "11111111-1111-4111-8111-111111111111";
|
|
28
|
+
const NOT_GATED = "22222222-2222-4222-8222-222222222222";
|
|
29
|
+
const ENV_KEYS = [
|
|
30
|
+
"FOLLOW_UP_GATE_MODE",
|
|
31
|
+
"FOLLOW_UP_GATE_SCHEDULE_IDS",
|
|
32
|
+
"FOLLOW_UP_GATE_VERIFIER_SCRIPT",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
const VERIFIER = { name: "test-verifier", version: 3, contentHash: "abc123" };
|
|
36
|
+
|
|
37
|
+
let leadId: string;
|
|
38
|
+
let workerId: string;
|
|
39
|
+
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
initDb(":memory:");
|
|
42
|
+
leadId = (
|
|
43
|
+
await createAgent({
|
|
44
|
+
name: "Lead",
|
|
45
|
+
isLead: true,
|
|
46
|
+
status: "busy",
|
|
47
|
+
capabilities: [],
|
|
48
|
+
maxTasks: 50,
|
|
49
|
+
})
|
|
50
|
+
).id;
|
|
51
|
+
workerId = (
|
|
52
|
+
await createAgent({
|
|
53
|
+
name: "Worker",
|
|
54
|
+
isLead: false,
|
|
55
|
+
status: "busy",
|
|
56
|
+
capabilities: [],
|
|
57
|
+
maxTasks: 50,
|
|
58
|
+
})
|
|
59
|
+
).id;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(async () => {
|
|
63
|
+
await drainFollowUpGateShadowForTest();
|
|
64
|
+
setFollowUpGateDepsForTests(null);
|
|
65
|
+
setFollowUpGateTimeoutMsForTests(null);
|
|
66
|
+
for (const key of ENV_KEYS) delete process.env[key];
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterAll(() => closeDb());
|
|
70
|
+
|
|
71
|
+
function shadowEnv(overrides: Partial<Record<(typeof ENV_KEYS)[number], string>> = {}) {
|
|
72
|
+
process.env.FOLLOW_UP_GATE_MODE = "shadow";
|
|
73
|
+
process.env.FOLLOW_UP_GATE_SCHEDULE_IDS = GATED;
|
|
74
|
+
process.env.FOLLOW_UP_GATE_VERIFIER_SCRIPT = VERIFIER.name;
|
|
75
|
+
for (const [key, value] of Object.entries(overrides)) process.env[key] = value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Deps that record calls; `run` decides the verifier result. */
|
|
79
|
+
function stubDeps(run: FollowUpGateDeps["runVerifier"]) {
|
|
80
|
+
const calls = { getVerifier: 0, runVerifier: 0 };
|
|
81
|
+
const deps: FollowUpGateDeps = {
|
|
82
|
+
getVerifier: async () => {
|
|
83
|
+
calls.getVerifier++;
|
|
84
|
+
return VERIFIER;
|
|
85
|
+
},
|
|
86
|
+
runVerifier: async (...args) => {
|
|
87
|
+
calls.runVerifier++;
|
|
88
|
+
return run(...args);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
setFollowUpGateDepsForTests(deps);
|
|
92
|
+
return calls;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function workerTask(options: Parameters<typeof createTaskExtended>[1] = {}) {
|
|
96
|
+
const task = await createTaskExtended("scheduled work", {
|
|
97
|
+
agentId: workerId,
|
|
98
|
+
scheduleId: GATED,
|
|
99
|
+
...options,
|
|
100
|
+
});
|
|
101
|
+
await startTask(task.id);
|
|
102
|
+
return (await getTaskById(task.id)) as AgentTask;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function shadowRows(taskId: string) {
|
|
106
|
+
await drainFollowUpGateShadowForTest();
|
|
107
|
+
const rows = await getLogsByTaskIdAndEventType(taskId, "task_follow_up_gate_shadow");
|
|
108
|
+
return rows.map((row) => ({ ...row, meta: JSON.parse(row.metadata ?? "{}") }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function finish(task: AgentTask, status: "completed" | "failed" = "completed") {
|
|
112
|
+
const followUp = await createWorkerTaskFollowUp({ task, status, output: "done" });
|
|
113
|
+
expect(followUp).not.toBeNull();
|
|
114
|
+
expect(followUp?.parentTaskId).toBe(task.id);
|
|
115
|
+
expect(await getTaskById(followUp!.id)).not.toBeNull();
|
|
116
|
+
return followUp!;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
describe("classifyFollowUpGate", () => {
|
|
120
|
+
const shadow = readFollowUpGateConfig({
|
|
121
|
+
FOLLOW_UP_GATE_MODE: "shadow",
|
|
122
|
+
FOLLOW_UP_GATE_SCHEDULE_IDS: `${GATED}, not-a-uuid`,
|
|
123
|
+
});
|
|
124
|
+
const classify = (
|
|
125
|
+
task: Pick<AgentTask, "scheduleId" | "followUpConfig">,
|
|
126
|
+
status: "completed" | "failed" = "completed",
|
|
127
|
+
config = shadow,
|
|
128
|
+
) => classifyFollowUpGate({ task, status, config });
|
|
129
|
+
|
|
130
|
+
test("a completed run of a gated schedule is eligible", () => {
|
|
131
|
+
expect(classify({ scheduleId: GATED })).toEqual({ eligible: true });
|
|
132
|
+
expect(classify({ scheduleId: GATED.toUpperCase() })).toEqual({ eligible: true });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("a failed run is ineligible", () => {
|
|
136
|
+
expect(classify({ scheduleId: GATED }, "failed")).toEqual({
|
|
137
|
+
eligible: false,
|
|
138
|
+
reason: "status_failed",
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("follow-up instructions make a run ineligible", () => {
|
|
143
|
+
for (const followUpConfig of [{ onCompleted: "post it" }, { onFailed: "retry it" }]) {
|
|
144
|
+
expect(classify({ scheduleId: GATED, followUpConfig })).toEqual({
|
|
145
|
+
eligible: false,
|
|
146
|
+
reason: "follow_up_instructions",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("a task with no schedule is not scheduled", () => {
|
|
152
|
+
expect(classify({})).toEqual({ eligible: false, reason: "not_scheduled" });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("a schedule off the list is not gated", () => {
|
|
156
|
+
expect(classify({ scheduleId: NOT_GATED })).toEqual({
|
|
157
|
+
eligible: false,
|
|
158
|
+
reason: "schedule_not_gated",
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("mode off and an unknown mode read as off", () => {
|
|
163
|
+
for (const mode of [undefined, "off", "enforce", "SHADOWY"]) {
|
|
164
|
+
const config = readFollowUpGateConfig({
|
|
165
|
+
FOLLOW_UP_GATE_MODE: mode,
|
|
166
|
+
FOLLOW_UP_GATE_SCHEDULE_IDS: GATED,
|
|
167
|
+
});
|
|
168
|
+
expect(classify({ scheduleId: GATED }, "completed", config)).toEqual({
|
|
169
|
+
eligible: false,
|
|
170
|
+
reason: "mode_off",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("malformed ids in the list are ignored", () => {
|
|
176
|
+
expect([...shadow.scheduleIds]).toEqual([GATED]);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("follow-up gate config validation", () => {
|
|
181
|
+
test("mode accepts off and shadow and rejects enforce", () => {
|
|
182
|
+
expect(validateConfigValue("FOLLOW_UP_GATE_MODE", "off")).toBeNull();
|
|
183
|
+
expect(validateConfigValue("FOLLOW_UP_GATE_MODE", "shadow")).toBeNull();
|
|
184
|
+
expect(validateConfigValue("FOLLOW_UP_GATE_MODE", "enforce")).toContain("FOLLOW_UP_GATE_MODE");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("schedule ids accept empty and UUID lists and reject other ids", () => {
|
|
188
|
+
expect(validateConfigValue("FOLLOW_UP_GATE_SCHEDULE_IDS", "")).toBeNull();
|
|
189
|
+
expect(validateConfigValue("FOLLOW_UP_GATE_SCHEDULE_IDS", `${GATED}, ${NOT_GATED}`)).toBeNull();
|
|
190
|
+
expect(validateConfigValue("FOLLOW_UP_GATE_SCHEDULE_IDS", `${GATED},daily-report`)).toContain(
|
|
191
|
+
"FOLLOW_UP_GATE_SCHEDULE_IDS",
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("follow-up gate shadow runner", () => {
|
|
197
|
+
test("mode off never looks up the verifier and writes no rows", async () => {
|
|
198
|
+
const calls = stubDeps(async () => ({ ok: true, controls: { ok: true } }));
|
|
199
|
+
process.env.FOLLOW_UP_GATE_SCHEDULE_IDS = GATED;
|
|
200
|
+
process.env.FOLLOW_UP_GATE_VERIFIER_SCRIPT = VERIFIER.name;
|
|
201
|
+
const task = await workerTask();
|
|
202
|
+
const followUp = await finish(task);
|
|
203
|
+
expect(followUp.taskType).toBe("follow-up");
|
|
204
|
+
expect(followUp.agentId).toBe(leadId);
|
|
205
|
+
expect(await shadowRows(task.id)).toHaveLength(0);
|
|
206
|
+
expect(calls).toEqual({ getVerifier: 0, runVerifier: 0 });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("mode off registers no post-commit hook; shadow registers exactly one", async () => {
|
|
210
|
+
stubDeps(async () => ({ ok: true, controls: { ok: true } }));
|
|
211
|
+
const afterCommit = spyOn(getDbClient(), "afterCommit");
|
|
212
|
+
try {
|
|
213
|
+
const hooksFor = async () => {
|
|
214
|
+
const task = await workerTask();
|
|
215
|
+
const before = afterCommit.mock.calls.length;
|
|
216
|
+
await finish(task);
|
|
217
|
+
const count = afterCommit.mock.calls.length - before;
|
|
218
|
+
await drainFollowUpGateShadowForTest();
|
|
219
|
+
return count;
|
|
220
|
+
};
|
|
221
|
+
const off = await hooksFor();
|
|
222
|
+
shadowEnv();
|
|
223
|
+
expect(await hooksFor()).toBe(off + 1);
|
|
224
|
+
} finally {
|
|
225
|
+
afterCommit.mockRestore();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("ok:true with passing controls records would_skip", async () => {
|
|
230
|
+
shadowEnv();
|
|
231
|
+
const seen: unknown[] = [];
|
|
232
|
+
stubDeps(async (script, input, agentId) => {
|
|
233
|
+
seen.push({ script: script.name, input, agentId });
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
check: "page",
|
|
237
|
+
detail: "fresh",
|
|
238
|
+
controls: { ok: true },
|
|
239
|
+
unverified: ["archive"],
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
const task = await workerTask();
|
|
243
|
+
const followUp = await finish(task);
|
|
244
|
+
const rows = await shadowRows(task.id);
|
|
245
|
+
expect(rows).toHaveLength(1);
|
|
246
|
+
expect(rows[0]!.agentId).toBe(leadId);
|
|
247
|
+
expect(rows[0]!.meta).toMatchObject({
|
|
248
|
+
v: 1,
|
|
249
|
+
mode: "shadow",
|
|
250
|
+
followUpTaskId: followUp.id,
|
|
251
|
+
scheduleId: GATED,
|
|
252
|
+
status: "completed",
|
|
253
|
+
decision: "would_skip",
|
|
254
|
+
reason: null,
|
|
255
|
+
verdict: { ok: true, check: "page", detail: "fresh", controlsOk: true },
|
|
256
|
+
unverified: ["archive"],
|
|
257
|
+
verifier: VERIFIER,
|
|
258
|
+
});
|
|
259
|
+
expect(rows[0]!.meta.latencyMs).toBeGreaterThanOrEqual(0);
|
|
260
|
+
expect(seen).toEqual([{ script: VERIFIER.name, input: { taskId: task.id }, agentId: leadId }]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("ok:false, ok:null and failing controls record would_keep", async () => {
|
|
264
|
+
shadowEnv();
|
|
265
|
+
for (const result of [
|
|
266
|
+
{ ok: false, controls: { ok: true } },
|
|
267
|
+
{ ok: null, controls: { ok: true } },
|
|
268
|
+
{ ok: true, controls: { ok: false } },
|
|
269
|
+
{ ok: true },
|
|
270
|
+
]) {
|
|
271
|
+
stubDeps(async () => result);
|
|
272
|
+
const task = await workerTask();
|
|
273
|
+
await finish(task);
|
|
274
|
+
const rows = await shadowRows(task.id);
|
|
275
|
+
expect(rows.map((row) => row.meta.decision)).toEqual(["would_keep"]);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("a verifier throw records a scrubbed error", async () => {
|
|
280
|
+
shadowEnv();
|
|
281
|
+
stubDeps(async () => {
|
|
282
|
+
throw new Error("boom");
|
|
283
|
+
});
|
|
284
|
+
const task = await workerTask();
|
|
285
|
+
await finish(task);
|
|
286
|
+
const rows = await shadowRows(task.id);
|
|
287
|
+
expect(rows).toHaveLength(1);
|
|
288
|
+
expect(rows[0]!.meta).toMatchObject({
|
|
289
|
+
decision: "error",
|
|
290
|
+
reason: "verifier_error",
|
|
291
|
+
error: "boom",
|
|
292
|
+
verifier: VERIFIER,
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("a non-object result records verifier_error", async () => {
|
|
297
|
+
shadowEnv();
|
|
298
|
+
stubDeps(async () => undefined);
|
|
299
|
+
const task = await workerTask();
|
|
300
|
+
await finish(task);
|
|
301
|
+
const rows = await shadowRows(task.id);
|
|
302
|
+
expect(rows[0]!.meta).toMatchObject({ decision: "error", reason: "verifier_error" });
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("a verifier past the deadline records timeout and is aborted", async () => {
|
|
306
|
+
shadowEnv();
|
|
307
|
+
setFollowUpGateTimeoutMsForTests(20);
|
|
308
|
+
let aborted = false;
|
|
309
|
+
stubDeps(
|
|
310
|
+
(_script, _input, _agentId, signal) =>
|
|
311
|
+
new Promise((resolve) => {
|
|
312
|
+
signal.addEventListener("abort", () => {
|
|
313
|
+
aborted = true;
|
|
314
|
+
resolve({ ok: true, controls: { ok: true } });
|
|
315
|
+
});
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
const task = await workerTask();
|
|
319
|
+
await finish(task);
|
|
320
|
+
const rows = await shadowRows(task.id);
|
|
321
|
+
expect(rows).toHaveLength(1);
|
|
322
|
+
expect(rows[0]!.meta).toMatchObject({ decision: "error", reason: "timeout" });
|
|
323
|
+
expect(aborted).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("the follow-up returns before the verifier settles", async () => {
|
|
327
|
+
shadowEnv();
|
|
328
|
+
let release!: (value: unknown) => void;
|
|
329
|
+
let started = false;
|
|
330
|
+
stubDeps(() => {
|
|
331
|
+
started = true;
|
|
332
|
+
return new Promise((resolve) => {
|
|
333
|
+
release = resolve;
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
const task = await workerTask();
|
|
337
|
+
const followUp = await finish(task);
|
|
338
|
+
expect(followUp.id).toBeTruthy();
|
|
339
|
+
expect(await getLogsByTaskIdAndEventType(task.id, "task_follow_up_gate_shadow")).toHaveLength(
|
|
340
|
+
0,
|
|
341
|
+
);
|
|
342
|
+
while (!started) await Bun.sleep(1);
|
|
343
|
+
release({ ok: true, controls: { ok: true } });
|
|
344
|
+
const rows = await shadowRows(task.id);
|
|
345
|
+
expect(rows.map((row) => row.meta.decision)).toEqual(["would_skip"]);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("a third concurrent run hits the concurrency cap", async () => {
|
|
349
|
+
shadowEnv();
|
|
350
|
+
const releases: Array<(value: unknown) => void> = [];
|
|
351
|
+
const calls = stubDeps(
|
|
352
|
+
() =>
|
|
353
|
+
new Promise((resolve) => {
|
|
354
|
+
releases.push(resolve);
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
357
|
+
const tasks = [await workerTask(), await workerTask(), await workerTask()];
|
|
358
|
+
for (const task of tasks) await finish(task);
|
|
359
|
+
const allRows = async () =>
|
|
360
|
+
(await Promise.all(tasks.map((task) => shadowRows(task.id)))).flat();
|
|
361
|
+
while (releases.length < 2) await Bun.sleep(1);
|
|
362
|
+
// Let the third hook run while the first two are still blocked.
|
|
363
|
+
let rows = await getLogsByTaskIdAndEventType(tasks[2]!.id, "task_follow_up_gate_shadow");
|
|
364
|
+
while (rows.length === 0) {
|
|
365
|
+
await Bun.sleep(1);
|
|
366
|
+
rows = await getLogsByTaskIdAndEventType(tasks[2]!.id, "task_follow_up_gate_shadow");
|
|
367
|
+
}
|
|
368
|
+
for (const release of releases) release({ ok: true, controls: { ok: true } });
|
|
369
|
+
const all = await allRows();
|
|
370
|
+
expect(all.map((row) => row.meta.decision).sort()).toEqual([
|
|
371
|
+
"error",
|
|
372
|
+
"would_skip",
|
|
373
|
+
"would_skip",
|
|
374
|
+
]);
|
|
375
|
+
expect(all.find((row) => row.meta.decision === "error")!.meta.reason).toBe("concurrency_cap");
|
|
376
|
+
expect(calls.runVerifier).toBe(2);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("a gated failed run records ineligible without running the verifier", async () => {
|
|
380
|
+
shadowEnv();
|
|
381
|
+
const calls = stubDeps(async () => ({ ok: true, controls: { ok: true } }));
|
|
382
|
+
const task = await workerTask();
|
|
383
|
+
const followUp = await createWorkerTaskFollowUp({ task, status: "failed", failureReason: "x" });
|
|
384
|
+
expect(followUp).not.toBeNull();
|
|
385
|
+
const rows = await shadowRows(task.id);
|
|
386
|
+
expect(rows).toHaveLength(1);
|
|
387
|
+
expect(rows[0]!.meta).toMatchObject({
|
|
388
|
+
decision: "ineligible",
|
|
389
|
+
reason: "status_failed",
|
|
390
|
+
status: "failed",
|
|
391
|
+
followUpTaskId: followUp!.id,
|
|
392
|
+
verdict: null,
|
|
393
|
+
verifier: null,
|
|
394
|
+
});
|
|
395
|
+
expect(calls).toEqual({ getVerifier: 0, runVerifier: 0 });
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("a run with follow-up instructions records ineligible", async () => {
|
|
399
|
+
shadowEnv();
|
|
400
|
+
const calls = stubDeps(async () => ({ ok: true, controls: { ok: true } }));
|
|
401
|
+
const task = await workerTask({ followUpConfig: { onCompleted: "post the report" } });
|
|
402
|
+
await finish(task);
|
|
403
|
+
const rows = await shadowRows(task.id);
|
|
404
|
+
expect(rows.map((row) => row.meta.reason)).toEqual(["follow_up_instructions"]);
|
|
405
|
+
expect(calls.runVerifier).toBe(0);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("ungated and unscheduled runs write nothing", async () => {
|
|
409
|
+
shadowEnv();
|
|
410
|
+
const calls = stubDeps(async () => ({ ok: true, controls: { ok: true } }));
|
|
411
|
+
const ungated = await workerTask({ scheduleId: NOT_GATED });
|
|
412
|
+
const unscheduled = await workerTask({ scheduleId: undefined });
|
|
413
|
+
await finish(ungated);
|
|
414
|
+
await finish(unscheduled);
|
|
415
|
+
expect(await shadowRows(ungated.id)).toHaveLength(0);
|
|
416
|
+
expect(await shadowRows(unscheduled.id)).toHaveLength(0);
|
|
417
|
+
expect(calls).toEqual({ getVerifier: 0, runVerifier: 0 });
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("no verifier configured records no_verifier", async () => {
|
|
421
|
+
shadowEnv({ FOLLOW_UP_GATE_VERIFIER_SCRIPT: "" });
|
|
422
|
+
const calls = stubDeps(async () => ({ ok: true, controls: { ok: true } }));
|
|
423
|
+
const task = await workerTask();
|
|
424
|
+
await finish(task);
|
|
425
|
+
const rows = await shadowRows(task.id);
|
|
426
|
+
expect(rows[0]!.meta).toMatchObject({ decision: "error", reason: "no_verifier" });
|
|
427
|
+
expect(calls.getVerifier).toBe(0);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("a missing verifier script records no_verifier", async () => {
|
|
431
|
+
shadowEnv();
|
|
432
|
+
setFollowUpGateDepsForTests({
|
|
433
|
+
getVerifier: async () => null,
|
|
434
|
+
runVerifier: async () => {
|
|
435
|
+
throw new Error("must not run");
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
const task = await workerTask();
|
|
439
|
+
await finish(task);
|
|
440
|
+
const rows = await shadowRows(task.id);
|
|
441
|
+
expect(rows[0]!.meta).toMatchObject({
|
|
442
|
+
decision: "error",
|
|
443
|
+
reason: "no_verifier",
|
|
444
|
+
verifier: null,
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe("default verifier lookup and runner", () => {
|
|
449
|
+
const runOutput = (overrides: Partial<RunScriptOutput>) => ({
|
|
450
|
+
exitCode: 0,
|
|
451
|
+
result: undefined as unknown,
|
|
452
|
+
stdout: "",
|
|
453
|
+
stderr: "",
|
|
454
|
+
durationMs: 1,
|
|
455
|
+
truncated: { stdout: false, stderr: false },
|
|
456
|
+
...overrides,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
beforeAll(async () => {
|
|
460
|
+
await upsertScriptByName({
|
|
461
|
+
name: "gate-fixture",
|
|
462
|
+
scope: "global",
|
|
463
|
+
source: "export default () => ({ ok: true, controls: { ok: true } });",
|
|
464
|
+
description: "Follow-up gate fixture",
|
|
465
|
+
intent: "Exercise the default verifier path",
|
|
466
|
+
signatureJson: JSON.stringify({ args: { type: "object" }, result: { type: "object" } }),
|
|
467
|
+
typeChecked: true,
|
|
468
|
+
embeddingMode: "skip",
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("runs the global script as the Lead and records its version and hash", async () => {
|
|
473
|
+
shadowEnv({ FOLLOW_UP_GATE_VERIFIER_SCRIPT: "gate-fixture" });
|
|
474
|
+
const saved = spyOn(savedScriptRunner, "runSavedScriptAsAgent").mockResolvedValue(
|
|
475
|
+
runOutput({ result: { ok: true, controls: { ok: true } } }),
|
|
476
|
+
);
|
|
477
|
+
try {
|
|
478
|
+
const task = await workerTask();
|
|
479
|
+
await finish(task);
|
|
480
|
+
const rows = await shadowRows(task.id);
|
|
481
|
+
expect(rows[0]!.meta.decision).toBe("would_skip");
|
|
482
|
+
expect(rows[0]!.meta.verifier.name).toBe("gate-fixture");
|
|
483
|
+
expect(rows[0]!.meta.verifier.version).toBeGreaterThanOrEqual(1);
|
|
484
|
+
expect(rows[0]!.meta.verifier.contentHash).toMatch(/^[0-9a-f]{64}$/);
|
|
485
|
+
const call = saved.mock.calls[0]![0];
|
|
486
|
+
expect(call.agentId).toBe(leadId);
|
|
487
|
+
expect(call.input).toEqual({ taskId: task.id });
|
|
488
|
+
expect(call.timeoutMs).toBe(60_000);
|
|
489
|
+
} finally {
|
|
490
|
+
saved.mockRestore();
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("a non-zero exit records verifier_error", async () => {
|
|
495
|
+
shadowEnv({ FOLLOW_UP_GATE_VERIFIER_SCRIPT: "gate-fixture" });
|
|
496
|
+
const saved = spyOn(savedScriptRunner, "runSavedScriptAsAgent").mockResolvedValue(
|
|
497
|
+
runOutput({ exitCode: 1, error: "script failed" }),
|
|
498
|
+
);
|
|
499
|
+
try {
|
|
500
|
+
const task = await workerTask();
|
|
501
|
+
await finish(task);
|
|
502
|
+
const rows = await shadowRows(task.id);
|
|
503
|
+
expect(rows[0]!.meta).toMatchObject({
|
|
504
|
+
decision: "error",
|
|
505
|
+
reason: "verifier_error",
|
|
506
|
+
error: "script failed",
|
|
507
|
+
});
|
|
508
|
+
} finally {
|
|
509
|
+
saved.mockRestore();
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
});
|
|
@@ -179,7 +179,7 @@ describe("Stop-hook profile sync visibility", () => {
|
|
|
179
179
|
throw new Error("network unavailable");
|
|
180
180
|
}) as typeof fetch,
|
|
181
181
|
}),
|
|
182
|
-
).resolves.
|
|
182
|
+
).resolves.toBe(false);
|
|
183
183
|
expect(String(errorSpy.mock.calls[0]?.[0])).toContain(
|
|
184
184
|
"claudeMd profile sync errored: network unavailable",
|
|
185
185
|
);
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DEFAULT_MODEL,
|
|
4
4
|
type ResolveCredentialOptions,
|
|
5
5
|
resolveCredential,
|
|
6
|
+
resolveCredentials,
|
|
6
7
|
} from "../../utils/internal-ai/credentials.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -331,3 +332,64 @@ describe("resolveCredential", () => {
|
|
|
331
332
|
}
|
|
332
333
|
});
|
|
333
334
|
});
|
|
335
|
+
|
|
336
|
+
describe("resolveCredentials", () => {
|
|
337
|
+
test("returns every credential in precedence order", async () => {
|
|
338
|
+
const creds = await resolveCredentials(
|
|
339
|
+
makeOpts({
|
|
340
|
+
env: {
|
|
341
|
+
OPENROUTER_API_KEY: "example-or",
|
|
342
|
+
ANTHROPIC_API_KEY: "example-ant",
|
|
343
|
+
OPENAI_API_KEY: "example-oai",
|
|
344
|
+
CLAUDE_CODE_OAUTH_TOKEN: "example-claude",
|
|
345
|
+
},
|
|
346
|
+
}),
|
|
347
|
+
);
|
|
348
|
+
expect(creds.map((cred) => cred.kind)).toEqual([
|
|
349
|
+
"openrouter",
|
|
350
|
+
"anthropic",
|
|
351
|
+
"openai",
|
|
352
|
+
"claude-cli",
|
|
353
|
+
]);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("`kinds` restricts the lookup and `limit` stops early", async () => {
|
|
357
|
+
const env = { OPENROUTER_API_KEY: "example-or", OPENAI_API_KEY: "example-oai" };
|
|
358
|
+
const onlyOpenai = await resolveCredentials({ ...makeOpts({ env }), kinds: ["openai"] });
|
|
359
|
+
expect(onlyOpenai.map((cred) => cred.kind)).toEqual(["openai"]);
|
|
360
|
+
|
|
361
|
+
const first = await resolveCredentials({ ...makeOpts({ env }), limit: 1 });
|
|
362
|
+
expect(first.map((cred) => cred.kind)).toEqual(["openrouter"]);
|
|
363
|
+
|
|
364
|
+
const none = await resolveCredentials({ ...makeOpts({ env }), kinds: ["anthropic"] });
|
|
365
|
+
expect(none).toEqual([]);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("the codex probe runs only when its kind is requested", async () => {
|
|
369
|
+
let probed = 0;
|
|
370
|
+
const base = {
|
|
371
|
+
env: { OPENROUTER_API_KEY: "example-or" },
|
|
372
|
+
apiUrl: "http://localhost:3013",
|
|
373
|
+
apiKey: "k",
|
|
374
|
+
_getValidCodexOAuth: async () => {
|
|
375
|
+
probed++;
|
|
376
|
+
return { access: "a", refresh: "r", expires: Date.now() + 60_000, accountId: "acct" };
|
|
377
|
+
},
|
|
378
|
+
_getOAuthApiKey: async () => ({ apiKey: "codex-access" }),
|
|
379
|
+
};
|
|
380
|
+
const envOnly = await resolveCredentials({
|
|
381
|
+
...makeOpts(base),
|
|
382
|
+
kinds: ["openrouter", "openai"],
|
|
383
|
+
});
|
|
384
|
+
expect(envOnly.map((cred) => cred.kind)).toEqual(["openrouter"]);
|
|
385
|
+
expect(probed).toBe(0);
|
|
386
|
+
|
|
387
|
+
const codex = await resolveCredentials({ ...makeOpts(base), kinds: ["openai-codex"] });
|
|
388
|
+
expect(codex.map((cred) => cred.kind)).toEqual(["openai-codex"]);
|
|
389
|
+
expect(probed).toBe(1);
|
|
390
|
+
|
|
391
|
+
const all = await resolveCredentials(makeOpts(base));
|
|
392
|
+
expect(all.map((cred) => cred.kind)).toEqual(["openrouter", "openai-codex"]);
|
|
393
|
+
expect(probed).toBe(2);
|
|
394
|
+
});
|
|
395
|
+
});
|