@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,334 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
advanceIdentityBaselines,
|
|
6
|
+
bootIdentityBaselines,
|
|
7
|
+
CLAUDE_MD_LINEAGE_PATH,
|
|
8
|
+
CLAUDE_MD_PATH,
|
|
9
|
+
collectProfilePayloads,
|
|
10
|
+
contentSha256,
|
|
11
|
+
type FileReader,
|
|
12
|
+
HEARTBEAT_MD_PATH,
|
|
13
|
+
IDENTITY_BASELINES_PATH,
|
|
14
|
+
IDENTITY_MD_PATH,
|
|
15
|
+
type ProfileFileIO,
|
|
16
|
+
SETUP_SCRIPT_PATH,
|
|
17
|
+
SOUL_MD_PATH,
|
|
18
|
+
syncProfileFilesToServer,
|
|
19
|
+
TOOLS_MD_PATH,
|
|
20
|
+
WORKSPACE_CLAUDE_MD_PATH,
|
|
21
|
+
} from "../commands/profile-sync";
|
|
22
|
+
import {
|
|
23
|
+
type SwarmHooksConfig,
|
|
24
|
+
syncIdentityFilesToServer,
|
|
25
|
+
syncSetupScriptToServer,
|
|
26
|
+
} from "../providers/pi-mono-extension";
|
|
27
|
+
|
|
28
|
+
// A file the agent never touched still holds the copy written at boot. A sync
|
|
29
|
+
// that sends it anyway writes that stale copy over any DB-side edit made since
|
|
30
|
+
// (Lead's update-profile, a nightly profile edit). These tests hold the DB
|
|
31
|
+
// edit in a fake profile API and check that it survives the agent's sync.
|
|
32
|
+
|
|
33
|
+
const BOOT_IDENTITY = `# Identity\n\n${"boot ".repeat(120)}\n\n## Self-Evolution\n\nold section\n`;
|
|
34
|
+
const EDITED_IDENTITY = `# Identity\n\n${"boot ".repeat(120)}\n`;
|
|
35
|
+
|
|
36
|
+
/** In-memory stand-in for the worker's workspace and /tmp baselines. */
|
|
37
|
+
function memoryIO(files: Record<string, string>): ProfileFileIO & {
|
|
38
|
+
files: Record<string, string>;
|
|
39
|
+
} {
|
|
40
|
+
return {
|
|
41
|
+
files,
|
|
42
|
+
readFile: async (path) => files[path],
|
|
43
|
+
writeFile: async (path, content) => {
|
|
44
|
+
files[path] = content;
|
|
45
|
+
},
|
|
46
|
+
baselinesLockPath: join(tmpdir(), `identity-baselines-${crypto.randomUUID()}.lock`),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Holds each read of the baselines file until a second read arrives (or a
|
|
52
|
+
* short timeout), after both have captured the file as it was: the
|
|
53
|
+
* interleaving in which two unserialized updates drop one another's hashes.
|
|
54
|
+
*/
|
|
55
|
+
function pairBaselineReads(io: ReturnType<typeof memoryIO>): typeof io {
|
|
56
|
+
let waiting: (() => void) | null = null;
|
|
57
|
+
return {
|
|
58
|
+
...io,
|
|
59
|
+
readFile: async (path) => {
|
|
60
|
+
const content = io.files[path];
|
|
61
|
+
if (path !== IDENTITY_BASELINES_PATH) return content;
|
|
62
|
+
if (waiting) {
|
|
63
|
+
const partner = waiting;
|
|
64
|
+
waiting = null;
|
|
65
|
+
partner();
|
|
66
|
+
return content;
|
|
67
|
+
}
|
|
68
|
+
await new Promise<void>((resolve) => {
|
|
69
|
+
waiting = resolve;
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
if (waiting === resolve) waiting = null;
|
|
72
|
+
resolve();
|
|
73
|
+
}, 300);
|
|
74
|
+
});
|
|
75
|
+
return content;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function baselinesOf(io: ReturnType<typeof memoryIO>): Record<string, string> {
|
|
81
|
+
return JSON.parse(io.files[IDENTITY_BASELINES_PATH] ?? "{}");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The workspace right after runner boot: files and baselines from the DB. */
|
|
85
|
+
function bootedWorkspace(profile: Record<string, string>) {
|
|
86
|
+
const baselines: Record<string, string> = {};
|
|
87
|
+
for (const [field, content] of Object.entries(profile)) {
|
|
88
|
+
baselines[field] = contentSha256(content);
|
|
89
|
+
}
|
|
90
|
+
return memoryIO({
|
|
91
|
+
[IDENTITY_MD_PATH]: profile.identityMd ?? "",
|
|
92
|
+
[IDENTITY_BASELINES_PATH]: JSON.stringify(baselines),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe("profile sync never writes an untouched file over a DB edit", () => {
|
|
97
|
+
let db: Record<string, string> = {};
|
|
98
|
+
let server: ReturnType<typeof Bun.serve>;
|
|
99
|
+
let config: SwarmHooksConfig;
|
|
100
|
+
|
|
101
|
+
beforeAll(() => {
|
|
102
|
+
server = Bun.serve({
|
|
103
|
+
port: 0,
|
|
104
|
+
async fetch(req) {
|
|
105
|
+
if (req.method !== "PUT") return new Response("not found", { status: 404 });
|
|
106
|
+
const body = (await req.json()) as Record<string, string>;
|
|
107
|
+
for (const [field, value] of Object.entries(body)) {
|
|
108
|
+
if (field !== "changeSource") db[field] = value;
|
|
109
|
+
}
|
|
110
|
+
return Response.json({ ok: true });
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
config = {
|
|
114
|
+
apiUrl: `http://localhost:${server.port}`,
|
|
115
|
+
apiKey: "test-key",
|
|
116
|
+
agentId: "agent-1",
|
|
117
|
+
taskId: "task-1",
|
|
118
|
+
isLead: false,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterAll(() => {
|
|
123
|
+
server.stop(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("opencode/pi session end keeps a DB edit made during the session when IDENTITY.md is untouched", async () => {
|
|
127
|
+
db = { identityMd: BOOT_IDENTITY };
|
|
128
|
+
const io = bootedWorkspace({ identityMd: BOOT_IDENTITY });
|
|
129
|
+
|
|
130
|
+
// Mid-session DB edit; the agent never touches /workspace/IDENTITY.md.
|
|
131
|
+
db.identityMd = EDITED_IDENTITY;
|
|
132
|
+
|
|
133
|
+
await syncIdentityFilesToServer(config, "session_sync", undefined, io);
|
|
134
|
+
|
|
135
|
+
expect(db.identityMd).toBe(EDITED_IDENTITY);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("a synced local edit advances the baseline, so a later DB edit survives the next session end", async () => {
|
|
139
|
+
db = { identityMd: BOOT_IDENTITY };
|
|
140
|
+
const io = bootedWorkspace({ identityMd: BOOT_IDENTITY });
|
|
141
|
+
const agentEdit = `${BOOT_IDENTITY}\n## Learned\n\nagent edit\n`;
|
|
142
|
+
|
|
143
|
+
io.files[IDENTITY_MD_PATH] = agentEdit;
|
|
144
|
+
await syncIdentityFilesToServer(config, "self_edit", undefined, io);
|
|
145
|
+
expect(db.identityMd).toBe(agentEdit);
|
|
146
|
+
|
|
147
|
+
db.identityMd = EDITED_IDENTITY;
|
|
148
|
+
await syncIdentityFilesToServer(config, "session_sync", undefined, io);
|
|
149
|
+
|
|
150
|
+
expect(db.identityMd).toBe(EDITED_IDENTITY);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("a real local edit still syncs", async () => {
|
|
154
|
+
db = { identityMd: BOOT_IDENTITY };
|
|
155
|
+
const io = bootedWorkspace({ identityMd: BOOT_IDENTITY });
|
|
156
|
+
io.files[IDENTITY_MD_PATH] = EDITED_IDENTITY;
|
|
157
|
+
|
|
158
|
+
await syncIdentityFilesToServer(config, "session_sync", undefined, io);
|
|
159
|
+
|
|
160
|
+
expect(db.identityMd).toBe(EDITED_IDENTITY);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("an untouched start-up.sh does not overwrite a DB setupScript edit", async () => {
|
|
164
|
+
db = { setupScript: "echo boot" };
|
|
165
|
+
const io = memoryIO({
|
|
166
|
+
[SETUP_SCRIPT_PATH]: "#!/bin/bash\necho boot\n",
|
|
167
|
+
[IDENTITY_BASELINES_PATH]: JSON.stringify({ setupScript: contentSha256("echo boot") }),
|
|
168
|
+
});
|
|
169
|
+
db.setupScript = "echo edited";
|
|
170
|
+
|
|
171
|
+
await syncSetupScriptToServer(config, "session_sync", io);
|
|
172
|
+
|
|
173
|
+
expect(db.setupScript).toBe("echo edited");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const runnerSync = (
|
|
177
|
+
io: ProfileFileIO,
|
|
178
|
+
opts: Partial<Parameters<typeof syncProfileFilesToServer>[0]>,
|
|
179
|
+
) =>
|
|
180
|
+
syncProfileFilesToServer({
|
|
181
|
+
agentId: config.agentId,
|
|
182
|
+
apiUrl: config.apiUrl,
|
|
183
|
+
apiKey: config.apiKey,
|
|
184
|
+
changeSource: "session_sync",
|
|
185
|
+
io,
|
|
186
|
+
...opts,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("an accepted workspace CLAUDE.md edit advances its baseline, so a later DB edit survives the next session end", async () => {
|
|
190
|
+
const boot = "# CLAUDE.md\n\nboot rules\n";
|
|
191
|
+
db = { claudeMd: boot };
|
|
192
|
+
const io = memoryIO({
|
|
193
|
+
[WORKSPACE_CLAUDE_MD_PATH]: boot,
|
|
194
|
+
[IDENTITY_BASELINES_PATH]: JSON.stringify({ claudeMd: contentSha256(boot) }),
|
|
195
|
+
});
|
|
196
|
+
const sync = () =>
|
|
197
|
+
runnerSync(io, { fields: ["claude"], claudeMdPath: WORKSPACE_CLAUDE_MD_PATH });
|
|
198
|
+
|
|
199
|
+
// Session 1: a codex/opencode agent edits /workspace/CLAUDE.md.
|
|
200
|
+
const agentEdit = `${boot}\n## Learned\n\nagent rule\n`;
|
|
201
|
+
io.files[WORKSPACE_CLAUDE_MD_PATH] = agentEdit;
|
|
202
|
+
await sync();
|
|
203
|
+
expect(db.claudeMd).toBe(agentEdit);
|
|
204
|
+
|
|
205
|
+
// Lead edits the DB; session 2 never touches the file.
|
|
206
|
+
db.claudeMd = "# CLAUDE.md\n\nlead rules\n";
|
|
207
|
+
await sync();
|
|
208
|
+
|
|
209
|
+
expect(db.claudeMd).toBe("# CLAUDE.md\n\nlead rules\n");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("an accepted personal CLAUDE.md sync keeps the hook's lineage and the boot baseline", async () => {
|
|
213
|
+
const boot = "# CLAUDE.md\n\nboot rules\n";
|
|
214
|
+
const lineage = JSON.stringify({ written: contentSha256(boot), base: contentSha256(boot) });
|
|
215
|
+
const bootBaselines = JSON.stringify({ claudeMd: contentSha256(boot) });
|
|
216
|
+
db = { claudeMd: boot };
|
|
217
|
+
const io = memoryIO({
|
|
218
|
+
[CLAUDE_MD_PATH]: `${boot}\nagent rule\n`,
|
|
219
|
+
[CLAUDE_MD_LINEAGE_PATH]: lineage,
|
|
220
|
+
[IDENTITY_BASELINES_PATH]: bootBaselines,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await runnerSync(io, {
|
|
224
|
+
fields: ["claude"],
|
|
225
|
+
claudeMdPath: CLAUDE_MD_PATH,
|
|
226
|
+
claudeMdLock: { path: join(tmpdir(), `claude-md-${crypto.randomUUID()}.lock`) },
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(db.claudeMd).toBe(`${boot}\nagent rule\n`);
|
|
230
|
+
expect(db.expectedHashes).toEqual({ claudeMd: contentSha256(boot) });
|
|
231
|
+
expect(io.files[CLAUDE_MD_LINEAGE_PATH]).toBe(lineage);
|
|
232
|
+
expect(io.files[IDENTITY_BASELINES_PATH]).toBe(bootBaselines);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("two concurrent baseline updates both persist their hashes", async () => {
|
|
236
|
+
const io = memoryIO({
|
|
237
|
+
[IDENTITY_BASELINES_PATH]: JSON.stringify({ soulMd: contentSha256("soul") }),
|
|
238
|
+
});
|
|
239
|
+
const paired = pairBaselineReads(io);
|
|
240
|
+
|
|
241
|
+
await Promise.all([
|
|
242
|
+
advanceIdentityBaselines({ identityMd: "identity, edited" }, paired),
|
|
243
|
+
advanceIdentityBaselines({ setupScript: "echo edited" }, paired),
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
expect(baselinesOf(io)).toEqual({
|
|
247
|
+
soulMd: contentSha256("soul"),
|
|
248
|
+
identityMd: contentSha256("identity, edited"),
|
|
249
|
+
setupScript: contentSha256("echo edited"),
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("concurrent runner and pi syncs both advance their baselines, so neither file is re-sent over a later DB edit", async () => {
|
|
254
|
+
db = { identityMd: BOOT_IDENTITY, setupScript: "echo boot" };
|
|
255
|
+
const io = bootedWorkspace({ identityMd: BOOT_IDENTITY });
|
|
256
|
+
io.files[IDENTITY_BASELINES_PATH] = JSON.stringify({
|
|
257
|
+
identityMd: contentSha256(BOOT_IDENTITY),
|
|
258
|
+
setupScript: contentSha256("echo boot"),
|
|
259
|
+
});
|
|
260
|
+
io.files[IDENTITY_MD_PATH] = EDITED_IDENTITY;
|
|
261
|
+
io.files[SETUP_SCRIPT_PATH] = "#!/bin/bash\necho edited\n";
|
|
262
|
+
const paired = pairBaselineReads(io);
|
|
263
|
+
|
|
264
|
+
// The runner's session-end sync races the pi extension's on-edit sync.
|
|
265
|
+
await Promise.all([
|
|
266
|
+
runnerSync(paired, { fields: ["identity"] }),
|
|
267
|
+
syncSetupScriptToServer(config, "self_edit", paired),
|
|
268
|
+
]);
|
|
269
|
+
expect(db).toEqual({ identityMd: EDITED_IDENTITY, setupScript: "echo edited" });
|
|
270
|
+
|
|
271
|
+
db = { identityMd: "lead identity", setupScript: "echo lead" };
|
|
272
|
+
await runnerSync(io, { fields: ["identity", "setup"] });
|
|
273
|
+
await syncIdentityFilesToServer(config, "session_sync", undefined, io);
|
|
274
|
+
await syncSetupScriptToServer(config, "session_sync", io);
|
|
275
|
+
|
|
276
|
+
expect(db).toEqual({ identityMd: "lead identity", setupScript: "echo lead" });
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("an empty HEARTBEAT.md that survives a restart is not sent over a later DB edit", async () => {
|
|
280
|
+
// Boot after a restart: the DB value is empty, so the runner leaves the
|
|
281
|
+
// surviving empty file in place and records the boot baselines.
|
|
282
|
+
db = { heartbeatMd: "" };
|
|
283
|
+
const io = memoryIO({ [HEARTBEAT_MD_PATH]: "" });
|
|
284
|
+
io.files[IDENTITY_BASELINES_PATH] = JSON.stringify(
|
|
285
|
+
bootIdentityBaselines({ heartbeatMd: db.heartbeatMd }, undefined),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
db.heartbeatMd = "- check the queue";
|
|
289
|
+
await runnerSync(io, { fields: ["identity"] });
|
|
290
|
+
expect(db.heartbeatMd).toBe("- check the queue");
|
|
291
|
+
|
|
292
|
+
// A real edit of the empty file still syncs.
|
|
293
|
+
io.files[HEARTBEAT_MD_PATH] = "- agent checklist";
|
|
294
|
+
await runnerSync(io, { fields: ["identity"] });
|
|
295
|
+
expect(db.heartbeatMd).toBe("- agent checklist");
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("collectProfilePayloads (claude hook + runner)", () => {
|
|
300
|
+
const reader =
|
|
301
|
+
(files: Record<string, string>): FileReader =>
|
|
302
|
+
async (path) =>
|
|
303
|
+
files[path];
|
|
304
|
+
|
|
305
|
+
test("Lead's self_edit of HEARTBEAT.md does not resend a stale IDENTITY.md", async () => {
|
|
306
|
+
const files = reader({
|
|
307
|
+
[SOUL_MD_PATH]: BOOT_IDENTITY,
|
|
308
|
+
[IDENTITY_MD_PATH]: BOOT_IDENTITY,
|
|
309
|
+
[TOOLS_MD_PATH]: "tools",
|
|
310
|
+
[HEARTBEAT_MD_PATH]: "heartbeat, edited",
|
|
311
|
+
[IDENTITY_BASELINES_PATH]: JSON.stringify({
|
|
312
|
+
soulMd: contentSha256(BOOT_IDENTITY),
|
|
313
|
+
identityMd: contentSha256(BOOT_IDENTITY),
|
|
314
|
+
toolsMd: contentSha256("tools"),
|
|
315
|
+
heartbeatMd: contentSha256("heartbeat"),
|
|
316
|
+
}),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const payloads = await collectProfilePayloads(["identity"], "self_edit", files);
|
|
320
|
+
|
|
321
|
+
expect(payloads.map((payload) => payload.body)).toEqual([
|
|
322
|
+
{ heartbeatMd: "heartbeat, edited", changeSource: "self_edit" },
|
|
323
|
+
]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("session_sync skips an untouched start-up.sh", async () => {
|
|
327
|
+
const files = reader({
|
|
328
|
+
[SETUP_SCRIPT_PATH]: "#!/bin/bash\necho boot\n",
|
|
329
|
+
[IDENTITY_BASELINES_PATH]: JSON.stringify({ setupScript: contentSha256("echo boot") }),
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(await collectProfilePayloads(["setup"], "session_sync", files)).toEqual([]);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
@@ -439,7 +439,7 @@ describe("postProfileUpdate (non-2xx is surfaced, not swallowed)", () => {
|
|
|
439
439
|
new Response("boom", { status: 500, statusText: "Server Error" })) as typeof fetch;
|
|
440
440
|
try {
|
|
441
441
|
// Must resolve (non-fatal), not reject.
|
|
442
|
-
await expect(postProfileUpdate({ ...opts, fetchImpl }, payload)).resolves.
|
|
442
|
+
await expect(postProfileUpdate({ ...opts, fetchImpl }, payload)).resolves.toBe(false);
|
|
443
443
|
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
444
444
|
const msg = String(warnSpy.mock.calls[0]?.[0]);
|
|
445
445
|
expect(msg).toContain("setup sync failed");
|
|
@@ -455,7 +455,7 @@ describe("postProfileUpdate (non-2xx is surfaced, not swallowed)", () => {
|
|
|
455
455
|
throw new Error("network down");
|
|
456
456
|
}) as typeof fetch;
|
|
457
457
|
try {
|
|
458
|
-
await expect(postProfileUpdate({ ...opts, fetchImpl }, payload)).resolves.
|
|
458
|
+
await expect(postProfileUpdate({ ...opts, fetchImpl }, payload)).resolves.toBe(false);
|
|
459
459
|
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
460
460
|
expect(String(warnSpy.mock.calls[0]?.[0])).toContain("setup sync errored");
|
|
461
461
|
} finally {
|
|
@@ -634,27 +634,23 @@ describe("collectProfilePayloads (baseline integration)", () => {
|
|
|
634
634
|
expect(payloads[0]?.body.identityMd).toBeUndefined();
|
|
635
635
|
});
|
|
636
636
|
|
|
637
|
-
test("self_edit
|
|
638
|
-
|
|
639
|
-
|
|
637
|
+
test("self_edit skips the identity files the agent did not edit", async () => {
|
|
638
|
+
// An edit to one file triggers a sync of all of them; the untouched ones
|
|
639
|
+
// still hold the boot-time copy, which may predate a DB-side edit.
|
|
640
640
|
const files = reader({
|
|
641
|
-
[SOUL_MD_PATH]:
|
|
642
|
-
[IDENTITY_MD_PATH]:
|
|
643
|
-
[TOOLS_MD_PATH]: "tools",
|
|
641
|
+
[SOUL_MD_PATH]: LONG,
|
|
642
|
+
[IDENTITY_MD_PATH]: LONG,
|
|
643
|
+
[TOOLS_MD_PATH]: "edited tools",
|
|
644
644
|
[IDENTITY_BASELINES_PATH]: JSON.stringify({
|
|
645
|
-
soulMd: contentSha256(
|
|
646
|
-
identityMd: contentSha256(
|
|
645
|
+
soulMd: contentSha256(LONG),
|
|
646
|
+
identityMd: contentSha256(LONG),
|
|
647
647
|
toolsMd: contentSha256("tools"),
|
|
648
648
|
}),
|
|
649
649
|
});
|
|
650
650
|
|
|
651
651
|
const payloads = await collectProfilePayloads(["identity"], "self_edit", files);
|
|
652
|
-
expect(payloads).toHaveLength(3);
|
|
653
|
-
// self_edit should include ALL files regardless of baselines, independently.
|
|
654
652
|
expect(payloads.map((payload) => payload.body)).toEqual([
|
|
655
|
-
{
|
|
656
|
-
{ identityMd: identityContent, changeSource: "self_edit" },
|
|
657
|
-
{ toolsMd: "tools", changeSource: "self_edit" },
|
|
653
|
+
{ toolsMd: "edited tools", changeSource: "self_edit" },
|
|
658
654
|
]);
|
|
659
655
|
});
|
|
660
656
|
|
|
@@ -104,7 +104,9 @@ describe("scheduled task required params repair", () => {
|
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
test("keeps every requirement on genuinely tokenized fresh templates", async () => {
|
|
107
|
-
|
|
107
|
+
// Migrations 141 and 151 still name these, but no template ships for them.
|
|
108
|
+
const unshipped = ["dream-daily", "daily-workflow-health-audit"];
|
|
109
|
+
for (const name of canonicalNames.filter((name) => !unshipped.includes(name))) {
|
|
108
110
|
const template = new URL(`../../templates/schedules/${name}/content.md`, import.meta.url);
|
|
109
111
|
insert(name, await Bun.file(template).text());
|
|
110
112
|
}
|
|
@@ -678,6 +678,8 @@ describe("script connections", () => {
|
|
|
678
678
|
markMigrationApplied(database, "264_slack_thread_cover_index.sql");
|
|
679
679
|
// 266 alters script_runs, which this migration-112-only fixture does not create.
|
|
680
680
|
markMigrationApplied(database, "266_script_runs_source_task.sql");
|
|
681
|
+
// 269 alters pages, which this migration-112-only fixture does not create.
|
|
682
|
+
markMigrationApplied(database, "269_pages_design_system.sql");
|
|
681
683
|
|
|
682
684
|
const id = crypto.randomUUID();
|
|
683
685
|
const now = new Date().toISOString();
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
closeDb,
|
|
5
|
+
getScheduledTaskByName,
|
|
6
|
+
getScheduledTasks,
|
|
7
|
+
initDb,
|
|
8
|
+
listWorkflows,
|
|
9
|
+
} from "../be/db";
|
|
10
|
+
import { getScript } from "../be/scripts/db";
|
|
4
11
|
import { runSeeder, SEEDERS } from "../be/seed";
|
|
5
12
|
import { schedulesSeeder } from "../be/seed/schedules-seeder";
|
|
6
13
|
import { workflowsSeeder } from "../be/seed/workflows-seeder";
|
|
14
|
+
import { scriptsSeeder } from "../be/seed-scripts";
|
|
7
15
|
|
|
8
16
|
const TEST_DB_PATH = "./test-seed-registry.sqlite";
|
|
9
17
|
|
|
@@ -58,15 +66,29 @@ describe("seeder registry", () => {
|
|
|
58
66
|
}
|
|
59
67
|
});
|
|
60
68
|
|
|
61
|
-
test("seeds all
|
|
69
|
+
test("seeds all 22 automation templates on a fresh database and re-runs as a no-op", async () => {
|
|
62
70
|
expect(await listWorkflows()).toHaveLength(0);
|
|
63
71
|
expect(await getScheduledTasks()).toHaveLength(0);
|
|
64
72
|
|
|
73
|
+
// Boot order: scripts seed before schedules, so a script-target schedule
|
|
74
|
+
// (daily-swarm-update-check) resolves its catalog script on a fresh install.
|
|
75
|
+
const order = SEEDERS.map((seeder) => seeder.kind);
|
|
76
|
+
expect(order.indexOf("script")).toBeLessThan(order.indexOf("schedule"));
|
|
77
|
+
expect(await getScript({ name: "swarm-update-check", scope: "global" })).toBeNull();
|
|
78
|
+
const scriptResult = await runSeeder(scriptsSeeder, {
|
|
79
|
+
quiet: true,
|
|
80
|
+
scriptEmbeddingMode: "skip",
|
|
81
|
+
});
|
|
65
82
|
const workflowResult = await runSeeder(workflowsSeeder, { quiet: true });
|
|
66
83
|
const scheduleResult = await runSeeder(schedulesSeeder, { quiet: true });
|
|
67
84
|
|
|
85
|
+
expect(scriptResult.failed).toEqual([]);
|
|
68
86
|
expect(workflowResult).toMatchObject({ created: 11, failed: [] });
|
|
69
|
-
expect(scheduleResult).toMatchObject({ created:
|
|
87
|
+
expect(scheduleResult).toMatchObject({ created: 11, failed: [] });
|
|
88
|
+
expect(await getScheduledTaskByName("daily-swarm-update-check")).toMatchObject({
|
|
89
|
+
targetType: "script",
|
|
90
|
+
scriptName: "swarm-update-check",
|
|
91
|
+
});
|
|
70
92
|
expect((await listWorkflows()).map((workflow) => workflow.name).sort()).toEqual(
|
|
71
93
|
workflowsSeeder.items().map((item) => item.key),
|
|
72
94
|
);
|
|
@@ -83,10 +105,10 @@ describe("seeder registry", () => {
|
|
|
83
105
|
expect(await runSeeder(schedulesSeeder, { quiet: true })).toMatchObject({
|
|
84
106
|
created: 0,
|
|
85
107
|
updated: 0,
|
|
86
|
-
skippedUnchanged:
|
|
108
|
+
skippedUnchanged: 11,
|
|
87
109
|
failed: [],
|
|
88
110
|
});
|
|
89
111
|
expect(await listWorkflows()).toHaveLength(11);
|
|
90
|
-
expect(await getScheduledTasks()).toHaveLength(
|
|
91
|
-
});
|
|
112
|
+
expect(await getScheduledTasks()).toHaveLength(11);
|
|
113
|
+
}, 60_000); // typechecks every catalog script with tsc
|
|
92
114
|
});
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
initDb,
|
|
9
9
|
updateScheduledTask,
|
|
10
10
|
} from "../be/db";
|
|
11
|
+
import { upsertScriptByName } from "../be/scripts/db";
|
|
11
12
|
import { runSeeder } from "../be/seed";
|
|
12
13
|
import {
|
|
13
14
|
createSchedulesSeeder,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
type ScheduleTemplateSource,
|
|
16
17
|
} from "../be/seed/schedules-seeder";
|
|
17
18
|
import { createWorkflowsSeeder } from "../be/seed/workflows-seeder";
|
|
19
|
+
import { SEED_SCRIPTS } from "../be/seed-scripts";
|
|
18
20
|
|
|
19
21
|
const TEST_DB_PATH = "./test-seed-schedules.sqlite";
|
|
20
22
|
|
|
@@ -95,6 +97,23 @@ function targetScheduleSource(target: Record<string, unknown>, prompt?: string)
|
|
|
95
97
|
} satisfies ScheduleTemplateSource;
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
/** Stand-in for the catalog script daily-swarm-update-check targets; boot seeds the real one first. */
|
|
101
|
+
async function seedUpdateCheckScript() {
|
|
102
|
+
await upsertScriptByName({
|
|
103
|
+
name: "swarm-update-check",
|
|
104
|
+
scope: "global",
|
|
105
|
+
scopeId: null,
|
|
106
|
+
source: "export default async function main() { return {}; }",
|
|
107
|
+
description: "test stand-in",
|
|
108
|
+
intent: "seed-schedules test fixture",
|
|
109
|
+
signatureJson: "{}",
|
|
110
|
+
fsMode: "none",
|
|
111
|
+
agentId: null,
|
|
112
|
+
isScratch: false,
|
|
113
|
+
typeChecked: true,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
98
117
|
async function makeWorkflow(name: string) {
|
|
99
118
|
return createWorkflow({ name, definition: { nodes: [] } });
|
|
100
119
|
}
|
|
@@ -118,9 +137,9 @@ afterEach(async () => {
|
|
|
118
137
|
});
|
|
119
138
|
|
|
120
139
|
describe("schedules seeder", () => {
|
|
121
|
-
test("loads all
|
|
140
|
+
test("loads all eleven schedule templates, including the unfenced schedule block", () => {
|
|
122
141
|
const schedules = loadSeedSchedules();
|
|
123
|
-
expect(schedules).toHaveLength(
|
|
142
|
+
expect(schedules).toHaveLength(11);
|
|
124
143
|
// Zero-config, requires:[], placeholders:[], template enabled:true -> auto-enabled
|
|
125
144
|
// under the default (unset -> on) SEED_AUTOMATIONS_ENABLED switch.
|
|
126
145
|
expect(schedules.find((schedule) => schedule.name === "daily-status-report")).toMatchObject({
|
|
@@ -135,6 +154,11 @@ describe("schedules seeder", () => {
|
|
|
135
154
|
enabled: true,
|
|
136
155
|
requiredParams: [],
|
|
137
156
|
requires: [],
|
|
157
|
+
// A script fire: no agent task, no prompt.
|
|
158
|
+
targetType: "script",
|
|
159
|
+
scriptName: "swarm-update-check",
|
|
160
|
+
scriptArgs: {},
|
|
161
|
+
taskTemplate: "",
|
|
138
162
|
});
|
|
139
163
|
expect(
|
|
140
164
|
schedules.find((schedule) => schedule.name === "weekly-harness-upgrade-check"),
|
|
@@ -149,9 +173,10 @@ describe("schedules seeder", () => {
|
|
|
149
173
|
"daily-blocker-digest",
|
|
150
174
|
"daily-status-report",
|
|
151
175
|
"daily-swarm-update-check",
|
|
152
|
-
"daily-workflow-health-audit",
|
|
153
176
|
"dreaming-daily",
|
|
154
177
|
]);
|
|
178
|
+
// The status report absorbed the workflow-health audit's six queries.
|
|
179
|
+
expect(schedules.map((schedule) => schedule.name)).not.toContain("daily-workflow-health-audit");
|
|
155
180
|
// dreaming replaces Daily Evolution, which seeds off even though it is zero-config.
|
|
156
181
|
expect(
|
|
157
182
|
schedules.find((schedule) => schedule.name === "daily-compounding-reflection"),
|
|
@@ -160,20 +185,22 @@ describe("schedules seeder", () => {
|
|
|
160
185
|
|
|
161
186
|
test("SEED_AUTOMATIONS_ENABLED=false still seeds all schedules disabled", async () => {
|
|
162
187
|
process.env.SEED_AUTOMATIONS_ENABLED = "false";
|
|
163
|
-
// dreaming-daily targets the seeded dreaming workflow.
|
|
188
|
+
// dreaming-daily targets the seeded dreaming workflow, daily-swarm-update-check a catalog script.
|
|
164
189
|
await runSeeder(createWorkflowsSeeder(), { quiet: true });
|
|
190
|
+
await seedUpdateCheckScript();
|
|
165
191
|
const result = await runSeeder(createSchedulesSeeder(), { quiet: true });
|
|
166
|
-
expect(result).toMatchObject({ created:
|
|
192
|
+
expect(result).toMatchObject({ created: 11, failed: [] });
|
|
167
193
|
const schedules = await getScheduledTasks();
|
|
168
|
-
expect(schedules).toHaveLength(
|
|
194
|
+
expect(schedules).toHaveLength(11);
|
|
169
195
|
expect(schedules.every((schedule) => schedule.enabled === false)).toBe(true);
|
|
170
196
|
});
|
|
171
197
|
|
|
172
198
|
test("seeds a disabled schedule whose timezone still needs setup", async () => {
|
|
173
|
-
// dreaming-daily targets the seeded dreaming workflow.
|
|
199
|
+
// dreaming-daily targets the seeded dreaming workflow, daily-swarm-update-check a catalog script.
|
|
174
200
|
await runSeeder(createWorkflowsSeeder(), { quiet: true });
|
|
201
|
+
await seedUpdateCheckScript();
|
|
175
202
|
const result = await runSeeder(createSchedulesSeeder(), { quiet: true });
|
|
176
|
-
expect(result).toMatchObject({ created:
|
|
203
|
+
expect(result).toMatchObject({ created: 11, failed: [] });
|
|
177
204
|
expect(await getScheduledTaskByName("weekly-dependabot-triage")).toMatchObject({
|
|
178
205
|
cronExpression: "40 3 * * 0",
|
|
179
206
|
timezone: "{{TIMEZONE}}",
|
|
@@ -313,6 +340,32 @@ describe("schedules seeder", () => {
|
|
|
313
340
|
expect(result.failed[0]?.error).toContain('Global script "no-such-script" not found');
|
|
314
341
|
});
|
|
315
342
|
|
|
343
|
+
test("daily-swarm-update-check runs the catalog's swarm-update-check script, no agent task", async () => {
|
|
344
|
+
// The scripts catalog ships the target, so boot's script seeder creates it first.
|
|
345
|
+
expect(SEED_SCRIPTS.map((script) => script.name)).toContain("swarm-update-check");
|
|
346
|
+
const updateCheck = loadSeedSchedules().filter(
|
|
347
|
+
(schedule) => schedule.name === "daily-swarm-update-check",
|
|
348
|
+
);
|
|
349
|
+
expect(updateCheck).toHaveLength(1);
|
|
350
|
+
|
|
351
|
+
// Control: without the script the item fails instead of seeding an agent task.
|
|
352
|
+
const seeder = createSchedulesSeeder();
|
|
353
|
+
const before = await runSeeder(seeder, { quiet: true });
|
|
354
|
+
expect(before.failed.map((failure) => failure.key)).toContain("daily-swarm-update-check");
|
|
355
|
+
expect(await getScheduledTaskByName("daily-swarm-update-check")).toBeNull();
|
|
356
|
+
|
|
357
|
+
await seedUpdateCheckScript();
|
|
358
|
+
await runSeeder(seeder, { quiet: true });
|
|
359
|
+
const row = await getScheduledTaskByName("daily-swarm-update-check");
|
|
360
|
+
expect(row).toMatchObject({
|
|
361
|
+
targetType: "script",
|
|
362
|
+
scriptName: "swarm-update-check",
|
|
363
|
+
scriptArgs: {},
|
|
364
|
+
enabled: true,
|
|
365
|
+
});
|
|
366
|
+
expect(row?.taskTemplate).toBeUndefined();
|
|
367
|
+
});
|
|
368
|
+
|
|
316
369
|
test("fails a workflow target whose workflow does not exist, without creating a row", async () => {
|
|
317
370
|
const result = await runSeeder(
|
|
318
371
|
createSchedulesSeeder([
|