@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
|
@@ -15,6 +15,7 @@ import crypto from "node:crypto";
|
|
|
15
15
|
import { unlink } from "node:fs/promises";
|
|
16
16
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
17
|
import { closeDb, getPageBySlug, getPageVersions, initDb } from "../be/db";
|
|
18
|
+
import { COLOR_LITERAL_THRESHOLD, countColorLiterals } from "../pages/color-literals";
|
|
18
19
|
import { registerCreatePageTool } from "../tools/create-page";
|
|
19
20
|
|
|
20
21
|
const TEST_DB_PATH = "./test-create-page-tool.sqlite";
|
|
@@ -211,6 +212,506 @@ describe("create_page MCP tool", () => {
|
|
|
211
212
|
expect(row?.passwordHash).not.toBe("example-open-sesame");
|
|
212
213
|
expect(await Bun.password.verify("example-open-sesame", row!.passwordHash!)).toBe(true);
|
|
213
214
|
});
|
|
215
|
+
|
|
216
|
+
describe("design fields", () => {
|
|
217
|
+
type ToolResult = {
|
|
218
|
+
isError?: boolean;
|
|
219
|
+
content: Array<{ text: string }>;
|
|
220
|
+
structuredContent: {
|
|
221
|
+
id?: string;
|
|
222
|
+
message: string;
|
|
223
|
+
details?: string;
|
|
224
|
+
nudge?: string;
|
|
225
|
+
designVersion?: number;
|
|
226
|
+
ignoredTokens?: string[];
|
|
227
|
+
colorLiteralCount?: number;
|
|
228
|
+
usesDesignSystem?: boolean;
|
|
229
|
+
lightOnlyColors?: boolean;
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const NUDGE_TEXT = (count: number) =>
|
|
234
|
+
`This page hardcodes ${count} color values. Use theme tokens (var(--color-primary), bg-primary, text-muted-foreground) so theme and dark-mode switches restyle it. Until then the page stays light in a dark dashboard.`;
|
|
235
|
+
const V1_NUDGE =
|
|
236
|
+
"This page is on designVersion 1 (the original look), so design system classes, tokens and themes have no effect. Pass designVersion: 2.";
|
|
237
|
+
const LIGHT_ONLY_NUDGE =
|
|
238
|
+
"Your themeTokens set colors for light mode only. Dark mode keeps the preset's dark colors; add a dark map (or a .dark block) to restyle it too.";
|
|
239
|
+
const IGNORED_MESSAGE =
|
|
240
|
+
"Some theme tokens were ignored (outside the page token contract or referencing a variable outside it).";
|
|
241
|
+
|
|
242
|
+
const uniqueSlug = () => `design-${crypto.randomUUID().slice(0, 8)}`;
|
|
243
|
+
|
|
244
|
+
/** An HTML page whose <style> block holds exactly `count` color literals. */
|
|
245
|
+
const styledPage = (count: number) =>
|
|
246
|
+
`<h1>Report</h1><style>${Array.from(
|
|
247
|
+
{ length: count },
|
|
248
|
+
(_, i) => `.c${i}{color:rgb(${i} 0 0)}`,
|
|
249
|
+
).join("")}</style>`;
|
|
250
|
+
|
|
251
|
+
async function call(args: Record<string, unknown>): Promise<ToolResult> {
|
|
252
|
+
const tool = buildServer();
|
|
253
|
+
return (await tool.handler(
|
|
254
|
+
{
|
|
255
|
+
title: "Design Page",
|
|
256
|
+
slug: uniqueSlug(),
|
|
257
|
+
body: "<h1>hi</h1>",
|
|
258
|
+
contentType: "text/html",
|
|
259
|
+
authMode: "public",
|
|
260
|
+
...args,
|
|
261
|
+
},
|
|
262
|
+
fakeMeta,
|
|
263
|
+
)) as ToolResult;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
test("a new page gets design version 2 with no theme and no tokens", async () => {
|
|
267
|
+
const slug = uniqueSlug();
|
|
268
|
+
const result = await call({ slug });
|
|
269
|
+
expect(result.isError).toBe(false);
|
|
270
|
+
const row = await getPageBySlug(agentId, slug);
|
|
271
|
+
expect(row?.designVersion).toBe(2);
|
|
272
|
+
expect(row?.theme).toBeNull();
|
|
273
|
+
expect(row?.themeTokens).toBeNull();
|
|
274
|
+
expect(result.structuredContent.ignoredTokens).toBeUndefined();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("an explicit design version and a preset theme are stored on create", async () => {
|
|
278
|
+
const slug = uniqueSlug();
|
|
279
|
+
await call({ slug, theme: "paper", designVersion: 1 });
|
|
280
|
+
const row = await getPageBySlug(agentId, slug);
|
|
281
|
+
expect(row?.theme).toBe("paper");
|
|
282
|
+
expect(row?.designVersion).toBe(1);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("an unknown theme returns an error that lists the presets and saves nothing", async () => {
|
|
286
|
+
const slug = uniqueSlug();
|
|
287
|
+
const result = await call({ slug, theme: "neon" });
|
|
288
|
+
expect(result.isError).toBe(true);
|
|
289
|
+
expect(result.structuredContent.message).toContain('Unknown theme "neon"');
|
|
290
|
+
for (const id of ["hive", "paper", "github"]) {
|
|
291
|
+
expect(result.structuredContent.message).toContain(id);
|
|
292
|
+
}
|
|
293
|
+
expect(await getPageBySlug(agentId, slug)).toBeNull();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("shadcn-style tokens are stored normalized and the ignored names are reported", async () => {
|
|
297
|
+
const slug = uniqueSlug();
|
|
298
|
+
const result = await call({
|
|
299
|
+
slug,
|
|
300
|
+
themeTokens: {
|
|
301
|
+
light: {
|
|
302
|
+
"--primary": "oklch(0.55 0.2 260)",
|
|
303
|
+
background: "oklch(1 0 0)",
|
|
304
|
+
radius: "0.5rem",
|
|
305
|
+
"--sidebar": "oklch(0.9 0 0)",
|
|
306
|
+
"--shadow-2xs": "0 1px 2px #0000",
|
|
307
|
+
},
|
|
308
|
+
dark: { "--primary": "oklch(0.7 0.15 260)" },
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
expect(result.isError).toBe(false);
|
|
312
|
+
expect(result.structuredContent.ignoredTokens).toEqual(["--sidebar", "--shadow-2xs"]);
|
|
313
|
+
expect(result.structuredContent.message).toContain(IGNORED_MESSAGE);
|
|
314
|
+
expect(result.structuredContent.details).toContain(
|
|
315
|
+
"Ignored theme tokens: --sidebar, --shadow-2xs",
|
|
316
|
+
);
|
|
317
|
+
expect(result.content[0]!.text).toContain("--shadow-2xs");
|
|
318
|
+
|
|
319
|
+
const row = await getPageBySlug(agentId, slug);
|
|
320
|
+
expect(row?.themeTokens).toEqual({
|
|
321
|
+
light: {
|
|
322
|
+
"--color-primary": "oklch(0.55 0.2 260)",
|
|
323
|
+
"--color-background": "oklch(1 0 0)",
|
|
324
|
+
"--radius-sm": "calc(0.5rem - 4px)",
|
|
325
|
+
"--radius-md": "calc(0.5rem - 2px)",
|
|
326
|
+
"--radius-lg": "0.5rem",
|
|
327
|
+
"--radius-xl": "calc(0.5rem + 4px)",
|
|
328
|
+
},
|
|
329
|
+
dark: { "--color-primary": "oklch(0.7 0.15 260)" },
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("tokens with nothing to ignore report nothing", async () => {
|
|
334
|
+
const result = await call({
|
|
335
|
+
themeTokens: { light: { "--color-primary": "oklch(0.55 0.2 260)" } },
|
|
336
|
+
});
|
|
337
|
+
expect(result.isError).toBe(false);
|
|
338
|
+
expect(result.structuredContent.ignoredTokens).toBeUndefined();
|
|
339
|
+
expect(result.structuredContent.message).not.toContain("ignored");
|
|
340
|
+
expect(result.structuredContent.details).not.toContain("Ignored");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("the css format works", async () => {
|
|
344
|
+
const slug = uniqueSlug();
|
|
345
|
+
const result = await call({
|
|
346
|
+
slug,
|
|
347
|
+
themeTokens: {
|
|
348
|
+
format: "css",
|
|
349
|
+
css: ":root { --primary: 222.2 47.4% 11.2%; --tracking-normal: 0em; } .dark { --primary: 210 40% 98%; }",
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
expect(result.isError).toBe(false);
|
|
353
|
+
expect(result.structuredContent.ignoredTokens).toEqual(["--tracking-normal"]);
|
|
354
|
+
const row = await getPageBySlug(agentId, slug);
|
|
355
|
+
expect(row?.themeTokens).toEqual({
|
|
356
|
+
light: { "--color-primary": "hsl(222.2 47.4% 11.2%)" },
|
|
357
|
+
dark: { "--color-primary": "hsl(210 40% 98%)" },
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("bad themeTokens return an error that names the problem and save nothing", async () => {
|
|
362
|
+
const slug = uniqueSlug();
|
|
363
|
+
const unsafe = await call({
|
|
364
|
+
slug,
|
|
365
|
+
themeTokens: { light: { "--color-primary": "red; } body { display: none" } },
|
|
366
|
+
});
|
|
367
|
+
expect(unsafe.isError).toBe(true);
|
|
368
|
+
expect(unsafe.structuredContent.message).toContain("Invalid themeTokens");
|
|
369
|
+
expect(unsafe.structuredContent.message).toContain("--color-primary");
|
|
370
|
+
|
|
371
|
+
const missingCss = await call({ slug, themeTokens: { format: "css" } });
|
|
372
|
+
expect(missingCss.isError).toBe(true);
|
|
373
|
+
expect(missingCss.structuredContent.message).toContain("Invalid themeTokens");
|
|
374
|
+
|
|
375
|
+
expect(await getPageBySlug(agentId, slug)).toBeNull();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("an upsert keeps the design fields it omits and clears them with null", async () => {
|
|
379
|
+
const slug = uniqueSlug();
|
|
380
|
+
const tokens = { light: { "--color-ring": "oklch(0.6 0.2 30)" } };
|
|
381
|
+
await call({ slug, theme: "paper", themeTokens: tokens, designVersion: 1 });
|
|
382
|
+
const stored = {
|
|
383
|
+
theme: "paper",
|
|
384
|
+
themeTokens: { light: { "--color-ring": "oklch(0.6 0.2 30)" }, dark: {} },
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// Omitted fields keep the stored values, including an older design version.
|
|
388
|
+
await call({ slug, body: "<h1>v2</h1>" });
|
|
389
|
+
let row = await getPageBySlug(agentId, slug);
|
|
390
|
+
expect(row?.body).toBe("<h1>v2</h1>");
|
|
391
|
+
expect(row).toMatchObject({ ...stored, designVersion: 1 });
|
|
392
|
+
|
|
393
|
+
// A given version moves the page. The theme and the tokens stay.
|
|
394
|
+
await call({ slug, body: "<h1>v3</h1>", designVersion: 2 });
|
|
395
|
+
row = await getPageBySlug(agentId, slug);
|
|
396
|
+
expect(row).toMatchObject({ ...stored, designVersion: 2 });
|
|
397
|
+
|
|
398
|
+
// A new theme replaces the old one. The tokens stay.
|
|
399
|
+
await call({ slug, body: "<h1>v4</h1>", theme: "github" });
|
|
400
|
+
row = await getPageBySlug(agentId, slug);
|
|
401
|
+
expect(row).toMatchObject({ theme: "github", themeTokens: stored.themeTokens });
|
|
402
|
+
|
|
403
|
+
// null clears one field at a time.
|
|
404
|
+
await call({ slug, body: "<h1>v5</h1>", theme: null });
|
|
405
|
+
row = await getPageBySlug(agentId, slug);
|
|
406
|
+
expect(row?.theme).toBeNull();
|
|
407
|
+
expect(row?.themeTokens).toEqual(stored.themeTokens);
|
|
408
|
+
await call({ slug, body: "<h1>v6</h1>", themeTokens: null });
|
|
409
|
+
row = await getPageBySlug(agentId, slug);
|
|
410
|
+
expect(row?.themeTokens).toBeNull();
|
|
411
|
+
expect(row?.designVersion).toBe(2);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("a rejected upsert leaves the page and its history alone", async () => {
|
|
415
|
+
const slug = uniqueSlug();
|
|
416
|
+
const first = await call({ slug, body: "<h1>kept</h1>", theme: "paper" });
|
|
417
|
+
const result = await call({ slug, body: "<h1>lost</h1>", theme: "neon" });
|
|
418
|
+
expect(result.isError).toBe(true);
|
|
419
|
+
|
|
420
|
+
const row = await getPageBySlug(agentId, slug);
|
|
421
|
+
expect(row?.body).toBe("<h1>kept</h1>");
|
|
422
|
+
expect(row?.theme).toBe("paper");
|
|
423
|
+
expect(await getPageVersions(first.structuredContent.id!)).toHaveLength(0);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("a reference outside the contract is reported with the reference", async () => {
|
|
427
|
+
const result = await call({
|
|
428
|
+
themeTokens: {
|
|
429
|
+
format: "css",
|
|
430
|
+
css: ":root { --primary: oklch(0.55 0.2 260); --font-sans: var(--font-geist-sans); } .dark { --primary: oklch(0.7 0.15 260); }",
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
expect(result.isError).toBe(false);
|
|
434
|
+
expect(result.structuredContent.ignoredTokens).toEqual([
|
|
435
|
+
"--font-sans (references --font-geist-sans)",
|
|
436
|
+
]);
|
|
437
|
+
expect(result.structuredContent.message).toContain(IGNORED_MESSAGE);
|
|
438
|
+
expect(result.structuredContent.details).toContain(
|
|
439
|
+
"Ignored theme tokens: --font-sans (references --font-geist-sans)",
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("the result reports the saved design version in data and in the message", async () => {
|
|
444
|
+
const slug = uniqueSlug();
|
|
445
|
+
const created = await call({ slug });
|
|
446
|
+
expect(created.structuredContent.designVersion).toBe(2);
|
|
447
|
+
expect(created.structuredContent.message).toContain("designVersion=2)");
|
|
448
|
+
|
|
449
|
+
const legacy = await call({ designVersion: 1 });
|
|
450
|
+
expect(legacy.structuredContent.designVersion).toBe(1);
|
|
451
|
+
expect(legacy.structuredContent.message).toContain("designVersion=1)");
|
|
452
|
+
|
|
453
|
+
const json = await call({ body: '{"type":"text"}', contentType: "application/json" });
|
|
454
|
+
expect(json.structuredContent.designVersion).toBe(2);
|
|
455
|
+
expect(json.structuredContent.usesDesignSystem).toBeUndefined();
|
|
456
|
+
expect(json.structuredContent.lightOnlyColors).toBeUndefined();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("a design version 1 HTML page that uses the design system gets the version nudge", async () => {
|
|
460
|
+
const designSystemCalls: Array<Record<string, unknown>> = [
|
|
461
|
+
{ body: '<main class="swarm-page"><h1>Report</h1></main>' },
|
|
462
|
+
{ body: "<p class='lead swarm-lede'>x</p>" },
|
|
463
|
+
{ body: "<style>.a{color:var(--color-primary)}</style>" },
|
|
464
|
+
{ theme: "paper" },
|
|
465
|
+
{ themeTokens: { light: { "--radius-lg": "8px" } } },
|
|
466
|
+
];
|
|
467
|
+
for (const extra of designSystemCalls) {
|
|
468
|
+
const result = await call({ designVersion: 1, ...extra });
|
|
469
|
+
expect(result.isError).toBe(false);
|
|
470
|
+
expect(result.structuredContent.usesDesignSystem).toBe(true);
|
|
471
|
+
expect(result.structuredContent.nudge).toBe(V1_NUDGE);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("the version nudge stays quiet on plain v1 pages, on v2 pages and on other content types", async () => {
|
|
476
|
+
const plain = await call({
|
|
477
|
+
designVersion: 1,
|
|
478
|
+
// <swarm-diff> works on both design versions, and a cleared theme is not a theme.
|
|
479
|
+
body: '<h1>PR</h1><swarm-diff>{"files":[]}</swarm-diff><p class="not-swarm-x">x</p>',
|
|
480
|
+
theme: null,
|
|
481
|
+
themeTokens: null,
|
|
482
|
+
});
|
|
483
|
+
expect(plain.structuredContent.usesDesignSystem).toBe(false);
|
|
484
|
+
expect(plain.structuredContent.nudge).toBeUndefined();
|
|
485
|
+
|
|
486
|
+
const v2 = await call({ body: '<main class="swarm-page">x</main>', theme: "paper" });
|
|
487
|
+
expect(v2.structuredContent.usesDesignSystem).toBe(true);
|
|
488
|
+
expect(v2.structuredContent.nudge).toBeUndefined();
|
|
489
|
+
|
|
490
|
+
const svg = await call({
|
|
491
|
+
designVersion: 1,
|
|
492
|
+
body: '<svg xmlns="http://www.w3.org/2000/svg" class="swarm-page"/>',
|
|
493
|
+
contentType: "image/svg+xml",
|
|
494
|
+
theme: "paper",
|
|
495
|
+
});
|
|
496
|
+
expect(svg.structuredContent.usesDesignSystem).toBeUndefined();
|
|
497
|
+
expect(svg.structuredContent.nudge).toBeUndefined();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("an upsert that keeps a design version 1 page gets the version nudge", async () => {
|
|
501
|
+
const slug = uniqueSlug();
|
|
502
|
+
await call({ slug, body: "<h1>old</h1>", designVersion: 1 });
|
|
503
|
+
const rewritten = await call({ slug, body: '<main class="swarm-page">new</main>' });
|
|
504
|
+
expect(rewritten.structuredContent.designVersion).toBe(1);
|
|
505
|
+
expect(rewritten.structuredContent.nudge).toBe(V1_NUDGE);
|
|
506
|
+
|
|
507
|
+
const moved = await call({
|
|
508
|
+
slug,
|
|
509
|
+
body: '<main class="swarm-page">new</main>',
|
|
510
|
+
designVersion: 2,
|
|
511
|
+
});
|
|
512
|
+
expect(moved.structuredContent.designVersion).toBe(2);
|
|
513
|
+
expect(moved.structuredContent.nudge).toBeUndefined();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("stored tokens with light colors and no dark colors get the light-only nudge", async () => {
|
|
517
|
+
const slug = uniqueSlug();
|
|
518
|
+
const lightOnly = await call({
|
|
519
|
+
slug,
|
|
520
|
+
themeTokens: { light: { "--color-primary": "oklch(0.55 0.2 260)", "--radius-lg": "8px" } },
|
|
521
|
+
});
|
|
522
|
+
expect(lightOnly.structuredContent.lightOnlyColors).toBe(true);
|
|
523
|
+
expect(lightOnly.structuredContent.nudge).toBe(LIGHT_ONLY_NUDGE);
|
|
524
|
+
|
|
525
|
+
// The note follows the stored tokens, so an upsert that keeps them repeats it.
|
|
526
|
+
const kept = await call({ slug, body: "<h1>again</h1>" });
|
|
527
|
+
expect(kept.structuredContent.lightOnlyColors).toBe(true);
|
|
528
|
+
expect(kept.structuredContent.nudge).toBe(LIGHT_ONLY_NUDGE);
|
|
529
|
+
|
|
530
|
+
const quiet: Array<Record<string, unknown>> = [
|
|
531
|
+
{ themeTokens: { light: { "--font-sans": "Inter", "--radius-lg": "8px" } } },
|
|
532
|
+
{
|
|
533
|
+
themeTokens: {
|
|
534
|
+
light: { "--color-primary": "oklch(0.55 0.2 260)" },
|
|
535
|
+
dark: { "--color-primary": "oklch(0.7 0.15 260)" },
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
{ themeTokens: { dark: { "--color-primary": "oklch(0.7 0.15 260)" } } },
|
|
539
|
+
{},
|
|
540
|
+
];
|
|
541
|
+
for (const extra of quiet) {
|
|
542
|
+
const result = await call(extra);
|
|
543
|
+
expect(result.structuredContent.lightOnlyColors).toBe(false);
|
|
544
|
+
expect(result.structuredContent.nudge).toBeUndefined();
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("the nudge joins every sentence that applies, in a fixed order", async () => {
|
|
549
|
+
const lightOnly = { light: { "--color-primary": "oklch(0.55 0.2 260)" } };
|
|
550
|
+
const legacy = await call({ designVersion: 1, themeTokens: lightOnly });
|
|
551
|
+
expect(legacy.structuredContent.nudge).toBe(`${V1_NUDGE} ${LIGHT_ONLY_NUDGE}`);
|
|
552
|
+
|
|
553
|
+
const current = await call({ body: styledPage(9), themeTokens: lightOnly });
|
|
554
|
+
expect(current.structuredContent.nudge).toBe(`${LIGHT_ONLY_NUDGE} ${NUDGE_TEXT(9)}`);
|
|
555
|
+
expect(current.content[0]!.text).toContain(`${LIGHT_ONLY_NUDGE} ${NUDGE_TEXT(9)}`);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("the color nudge fires at 8 color literals and not at 7", async () => {
|
|
559
|
+
// The page server shares this threshold: from 8 on, dark mode keeps the page light.
|
|
560
|
+
expect(COLOR_LITERAL_THRESHOLD).toBe(8);
|
|
561
|
+
const seven = await call({ body: styledPage(7) });
|
|
562
|
+
expect(seven.structuredContent.colorLiteralCount).toBe(7);
|
|
563
|
+
expect(seven.structuredContent.nudge).toBeUndefined();
|
|
564
|
+
|
|
565
|
+
const eight = await call({ body: styledPage(8) });
|
|
566
|
+
expect(eight.structuredContent.colorLiteralCount).toBe(8);
|
|
567
|
+
expect(eight.structuredContent.nudge).toBe(NUDGE_TEXT(8));
|
|
568
|
+
expect(eight.content[0]!.text).toContain(NUDGE_TEXT(8));
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
test("the color count covers style attributes and ignores colors elsewhere", async () => {
|
|
572
|
+
const result = await call({
|
|
573
|
+
body: [
|
|
574
|
+
"<style>.a{color:#fff;background:#0f172a}</style>",
|
|
575
|
+
'<p style="color:#2563eb;border:1px solid hsl(0 0% 90%)">a</p>',
|
|
576
|
+
"<p style='fill:oklch(0.5 0.2 30)'>b</p>",
|
|
577
|
+
// Not counted: text, script, and attributes that are not style.
|
|
578
|
+
"<p>Use #fff or rgb(0 0 0) for white.</p>",
|
|
579
|
+
'<script>const c = "#ff0000";</script>',
|
|
580
|
+
'<svg fill="#fff"><circle stroke="rgb(0 0 0)"/></svg>',
|
|
581
|
+
].join(""),
|
|
582
|
+
});
|
|
583
|
+
expect(result.structuredContent.colorLiteralCount).toBe(5);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test("pages on design version 1, and pages that are not HTML, get no color count or nudge", async () => {
|
|
587
|
+
const legacy = await call({ body: styledPage(20), designVersion: 1 });
|
|
588
|
+
expect(legacy.structuredContent.colorLiteralCount).toBeUndefined();
|
|
589
|
+
expect(legacy.structuredContent.nudge).toBeUndefined();
|
|
590
|
+
|
|
591
|
+
const json = await call({
|
|
592
|
+
body: JSON.stringify({ note: styledPage(20) }),
|
|
593
|
+
contentType: "application/json",
|
|
594
|
+
});
|
|
595
|
+
expect(json.structuredContent.colorLiteralCount).toBeUndefined();
|
|
596
|
+
expect(json.structuredContent.nudge).toBeUndefined();
|
|
597
|
+
|
|
598
|
+
const svg = await call({
|
|
599
|
+
body: `<svg xmlns="http://www.w3.org/2000/svg">${styledPage(20)}</svg>`,
|
|
600
|
+
contentType: "image/svg+xml",
|
|
601
|
+
});
|
|
602
|
+
expect(svg.structuredContent.colorLiteralCount).toBeUndefined();
|
|
603
|
+
expect(svg.structuredContent.nudge).toBeUndefined();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("an upsert is judged by the design version the page keeps", async () => {
|
|
607
|
+
const slug = uniqueSlug();
|
|
608
|
+
await call({ slug, body: "<h1>old</h1>", designVersion: 1 });
|
|
609
|
+
const kept = await call({ slug, body: styledPage(20) });
|
|
610
|
+
expect(kept.structuredContent.colorLiteralCount).toBeUndefined();
|
|
611
|
+
expect(kept.structuredContent.nudge).toBeUndefined();
|
|
612
|
+
|
|
613
|
+
const moved = await call({ slug, body: styledPage(20), designVersion: 2 });
|
|
614
|
+
expect(moved.structuredContent.colorLiteralCount).toBe(20);
|
|
615
|
+
expect(moved.structuredContent.nudge).toBe(NUDGE_TEXT(20));
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("a failed save carries no nudge", async () => {
|
|
619
|
+
const result = await call({ body: styledPage(20), theme: "neon" });
|
|
620
|
+
expect(result.isError).toBe(true);
|
|
621
|
+
expect(result.structuredContent.nudge).toBeUndefined();
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
describe("countColorLiterals", () => {
|
|
627
|
+
test("counts every color literal form in a style block", () => {
|
|
628
|
+
const css =
|
|
629
|
+
".a{color:#fff;background:#ffffff;border-color:#ffffff80;outline-color:#fffa}" +
|
|
630
|
+
".b{color:rgb(0 0 0);fill:rgba(0,0,0,.5);stroke:hsl(0 0% 0%);x:hsla(0,0%,0%,.5);y:oklch(0.5 0.2 30)}";
|
|
631
|
+
expect(countColorLiterals(`<style>${css}</style>`)).toBe(9);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test("counts style attributes with either quote and every style block", () => {
|
|
635
|
+
const html =
|
|
636
|
+
"<p style=\"color:#fff\">a</p><p style='background:rgb(0 0 0)'>b</p>" +
|
|
637
|
+
"<style>.a{color:#000}</style><style media=print>.b{color:#111}</style>";
|
|
638
|
+
expect(countColorLiterals(html)).toBe(4);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test("is case-insensitive", () => {
|
|
642
|
+
expect(countColorLiterals("<STYLE>.a{color:#FFF;b:RGB(0 0 0)}</STYLE>")).toBe(2);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test("counts an unclosed style block to the end of the body", () => {
|
|
646
|
+
expect(countColorLiterals("<style>.a{color:#fff}.b{color:#000}")).toBe(2);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
test("ignores colors outside style regions", () => {
|
|
650
|
+
const html =
|
|
651
|
+
"<p>#fff rgb(0 0 0)</p><script>var c = '#ff0000'</script>" +
|
|
652
|
+
'<svg fill="#fff"></svg><p data-style="color:#fff"></p>';
|
|
653
|
+
expect(countColorLiterals(html)).toBe(0);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("ignores hex-like text that is not a color", () => {
|
|
657
|
+
const css = ".a{width:#12345}.b{x:#1234567}#header{}.c{content:'#xyz'}.d{color:var(--c)}";
|
|
658
|
+
expect(countColorLiterals(`<style>${css}</style>`)).toBe(0);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test("counts white and black as values in style regions, not inside names", () => {
|
|
662
|
+
const css =
|
|
663
|
+
".a{color:white;background:black;border:1px solid white}" +
|
|
664
|
+
".b{white-space:nowrap;--black:1;font-family:'Archivo Black'}.white{}#black{}";
|
|
665
|
+
expect(countColorLiterals(`<style>${css}</style>`)).toBe(3);
|
|
666
|
+
expect(countColorLiterals('<p style="background: black">x</p>')).toBe(1);
|
|
667
|
+
expect(countColorLiterals("<p>white on black</p>")).toBe(0);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
test("counts color classes in class attributes", () => {
|
|
671
|
+
const palette =
|
|
672
|
+
'<p class="bg-blue-500 hover:text-gray-600/80 dark:border-zinc-800 !ring-red-50">a</p>' +
|
|
673
|
+
"<p class='from-sky-400 via-violet-500 to-rose-950 shadow-slate-900 divide-stone-100'>b</p>" +
|
|
674
|
+
'<p class="outline-amber-300 decoration-lime-200 accent-teal-700 caret-cyan-600">c</p>' +
|
|
675
|
+
'<p class="placeholder-neutral-400 fill-emerald-500 stroke-indigo-500 text-fuchsia-500">d</p>' +
|
|
676
|
+
'<p class="bg-orange-100 text-yellow-800 border-green-300 bg-purple-50 text-pink-400">e</p>';
|
|
677
|
+
expect(countColorLiterals(palette)).toBe(22);
|
|
678
|
+
expect(countColorLiterals('<p class="bg-white text-black bg-white/80 text-white">x</p>')).toBe(
|
|
679
|
+
4,
|
|
680
|
+
);
|
|
681
|
+
expect(
|
|
682
|
+
countColorLiterals(
|
|
683
|
+
'<p class="bg-[#fff] text-[rgb(0_0_0)] border-[hsl(0_0%_90%)] ring-[oklch(0.5_0.2_30)]">x</p>',
|
|
684
|
+
),
|
|
685
|
+
).toBe(4);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("ignores token classes, other class names, and color classes outside class attributes", () => {
|
|
689
|
+
const html =
|
|
690
|
+
'<p class="bg-card text-muted-foreground border-border bg-primary swarm-panel">a</p>' +
|
|
691
|
+
'<p class="border-white ring-black my-bg-red-500 bg-red text-red bg-blue-500x">b</p>' +
|
|
692
|
+
'<p data-class="bg-red-500">c</p><p>bg-red-500 text-white</p>' +
|
|
693
|
+
"<style>.bg-white{display:block}</style>";
|
|
694
|
+
expect(countColorLiterals(html)).toBe(0);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test("returns 0 for a body without style regions", () => {
|
|
698
|
+
expect(countColorLiterals("")).toBe(0);
|
|
699
|
+
expect(countColorLiterals("<h1>plain</h1>")).toBe(0);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
test("stays fast on a body of many unterminated tags", () => {
|
|
703
|
+
// 20,000 "<style " tags with no ">" cost about a second with an unbounded
|
|
704
|
+
// attribute match and about 2 ms with the capped one.
|
|
705
|
+
const started = performance.now();
|
|
706
|
+
expect(countColorLiterals("<style ".repeat(20_000))).toBe(0);
|
|
707
|
+
expect(countColorLiterals(' style="'.repeat(20_000))).toBe(0);
|
|
708
|
+
expect(countColorLiterals(' class="'.repeat(20_000))).toBe(0);
|
|
709
|
+
expect(countColorLiterals(`<p class="${"bg-red-".repeat(20_000)}">`)).toBe(0);
|
|
710
|
+
expect(countColorLiterals(`<p class="bg-red-${"5".repeat(100_000)}x">`)).toBe(0);
|
|
711
|
+
expect(countColorLiterals(`<p class="${"-[".repeat(50_000)}">`)).toBe(0);
|
|
712
|
+
expect(countColorLiterals(`<style>${"whites ".repeat(20_000)}</style>`)).toBe(0);
|
|
713
|
+
expect(performance.now() - started).toBeLessThan(500);
|
|
714
|
+
});
|
|
214
715
|
});
|
|
215
716
|
|
|
216
717
|
describe("create_page MCP tool capability gating", () => {
|
|
@@ -306,4 +306,85 @@ describe("db-client SQLITE_BUSY retry", () => {
|
|
|
306
306
|
expect(rows.map((r) => r.name)).toEqual(["pre-existing"]);
|
|
307
307
|
await released;
|
|
308
308
|
});
|
|
309
|
+
test("a crowd of queued writers does not spin the event loop while a holder persists", async () => {
|
|
310
|
+
// Regression for the 31/10 stalls: a long external holder (a Litestream
|
|
311
|
+
// snapshot) with many writers queued. Each writer used to spin its full
|
|
312
|
+
// attemptSpinMs inside the FIFO lock on every attempt, so a round of
|
|
313
|
+
// attempts blocked the loop for writers x spin and the nominal retry
|
|
314
|
+
// budget stretched to minutes.
|
|
315
|
+
const crowdClient = createBunSqliteClient(() => main, {
|
|
316
|
+
maxWaitMs: 4_000,
|
|
317
|
+
backoffMs: [25, 50, 100, 250, ...Array<number>(20).fill(250)],
|
|
318
|
+
attemptSpinMs: 50,
|
|
319
|
+
finalAttemptSpinMs: 50,
|
|
320
|
+
});
|
|
321
|
+
const writers = 60;
|
|
322
|
+
const released = holdWriteLock(1_500);
|
|
323
|
+
|
|
324
|
+
let maxLagMs = 0;
|
|
325
|
+
let last = performance.now();
|
|
326
|
+
const ticker = setInterval(() => {
|
|
327
|
+
const now = performance.now();
|
|
328
|
+
maxLagMs = Math.max(maxLagMs, now - last - 10);
|
|
329
|
+
last = now;
|
|
330
|
+
}, 10);
|
|
331
|
+
|
|
332
|
+
const startedAt = performance.now();
|
|
333
|
+
const results = await Promise.allSettled(
|
|
334
|
+
Array.from({ length: writers }, (_, i) =>
|
|
335
|
+
crowdClient.run("INSERT INTO items (name) VALUES (?)", [`crowd-${i}`]),
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
const wallMs = performance.now() - startedAt;
|
|
339
|
+
clearInterval(ticker);
|
|
340
|
+
await released;
|
|
341
|
+
|
|
342
|
+
expect(results.every((r) => r.status === "fulfilled")).toBe(true);
|
|
343
|
+
// Ungated: 60 writers x 50ms = 3s of blocked loop per round of attempts.
|
|
344
|
+
expect(maxLagMs).toBeLessThan(500);
|
|
345
|
+
// Ungated the crowd needed several seconds of spin rounds after release.
|
|
346
|
+
expect(wallMs).toBeLessThan(3_000);
|
|
347
|
+
const rows = await crowdClient.query<{ n: number }>(
|
|
348
|
+
"SELECT COUNT(*) AS n FROM items WHERE name LIKE 'crowd-%'",
|
|
349
|
+
);
|
|
350
|
+
expect(rows[0]?.n).toBe(writers);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("a held lock past the budget fails fast for the whole crowd", async () => {
|
|
354
|
+
const crowdClient = createBunSqliteClient(() => main, {
|
|
355
|
+
maxWaitMs: 400,
|
|
356
|
+
backoffMs: [25, 50, 100, 250],
|
|
357
|
+
attemptSpinMs: 50,
|
|
358
|
+
finalAttemptSpinMs: 50,
|
|
359
|
+
});
|
|
360
|
+
const released = holdWriteLock(2_500);
|
|
361
|
+
const startedAt = performance.now();
|
|
362
|
+
const results = await Promise.allSettled(
|
|
363
|
+
Array.from({ length: 40 }, (_, i) =>
|
|
364
|
+
crowdClient.run("INSERT INTO items (name) VALUES (?)", [`doomed-${i}`]),
|
|
365
|
+
),
|
|
366
|
+
);
|
|
367
|
+
const wallMs = performance.now() - startedAt;
|
|
368
|
+
await released;
|
|
369
|
+
expect(results.every((r) => r.status === "rejected")).toBe(true);
|
|
370
|
+
for (const r of results) {
|
|
371
|
+
expect((r as PromiseRejectedResult).reason.code).toBe("SQLITE_BUSY");
|
|
372
|
+
}
|
|
373
|
+
// Ungated: 40 writers x 5 spins x 50ms = 10s. Gated: well under 2s.
|
|
374
|
+
expect(wallMs).toBeLessThan(2_000);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("the gate closes once a blocked write lands, so the next BUSY probes afresh", async () => {
|
|
378
|
+
const released = holdWriteLock(150);
|
|
379
|
+
await client.run("INSERT INTO items (name) VALUES (?)", ["after-hold"]);
|
|
380
|
+
await released;
|
|
381
|
+
// New episode: a second, short hold is still bridged.
|
|
382
|
+
const second = holdWriteLock(100);
|
|
383
|
+
await client.run("INSERT INTO items (name) VALUES (?)", ["after-second-hold"]);
|
|
384
|
+
await second;
|
|
385
|
+
const rows = await client.query<{ name: string }>(
|
|
386
|
+
"SELECT name FROM items WHERE name LIKE 'after-%' ORDER BY name",
|
|
387
|
+
);
|
|
388
|
+
expect(rows.map((r) => r.name)).toEqual(["after-hold", "after-second-hold"]);
|
|
389
|
+
});
|
|
309
390
|
});
|
|
@@ -566,6 +566,7 @@ value:setBudgetRefusalFollowUpTaskId
|
|
|
566
566
|
value:setFavorite
|
|
567
567
|
value:setScriptRunFollowUpGuard
|
|
568
568
|
value:setSlackMessageTracking
|
|
569
|
+
value:setTaskTitleIfUnset
|
|
569
570
|
value:setUserFavorite
|
|
570
571
|
value:settleSupersededTaskDependents
|
|
571
572
|
value:shouldBlockPolling
|
|
@@ -589,6 +589,7 @@ value:setBudgetRefusalFollowUpTaskId
|
|
|
589
589
|
value:setFavorite
|
|
590
590
|
value:setScriptRunFollowUpGuard
|
|
591
591
|
value:setSlackMessageTracking
|
|
592
|
+
value:setTaskTitleIfUnset
|
|
592
593
|
value:setUserFavorite
|
|
593
594
|
value:settleSupersededTaskDependents
|
|
594
595
|
value:shouldBlockPolling
|
|
@@ -659,6 +660,7 @@ value:pauseTask
|
|
|
659
660
|
value:recordTaskProviderIfUnset
|
|
660
661
|
value:resetOrphanedInProgressTasksForAgent
|
|
661
662
|
value:resumeTask
|
|
663
|
+
value:setTaskTitleIfUnset
|
|
662
664
|
value:settleSupersededTaskDependents
|
|
663
665
|
value:startTask
|
|
664
666
|
value:supersedeTask
|
|
@@ -36,6 +36,27 @@ beforeEach(async () => {
|
|
|
36
36
|
});
|
|
37
37
|
afterEach(() => db.closeDb());
|
|
38
38
|
|
|
39
|
+
test("unfiltered small pages use the lastUpdatedAt index without planner statistics", async () => {
|
|
40
|
+
const client = db.getDbClient();
|
|
41
|
+
for (const includeHeartbeat of [false, true]) {
|
|
42
|
+
for (const limit of [1, 5, 50]) {
|
|
43
|
+
const query = spyOn(client, "query");
|
|
44
|
+
let sql = "";
|
|
45
|
+
let params: unknown[] = [];
|
|
46
|
+
try {
|
|
47
|
+
await db.getAllTasks({ limit, includeHeartbeat });
|
|
48
|
+
[sql, params = []] = query.mock.calls.find(([s]) => s.includes("WITH page AS"))!;
|
|
49
|
+
} finally {
|
|
50
|
+
query.mockRestore();
|
|
51
|
+
}
|
|
52
|
+
const plan = await client.query<{ detail: string }>(`EXPLAIN QUERY PLAN ${sql}`, params);
|
|
53
|
+
expect(plan.map((row) => row.detail).join("\n")).toContain(
|
|
54
|
+
"SCAN agent_tasks USING INDEX idx_agent_tasks_last_updated",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
39
60
|
// Characterized against the pre-extraction facade at 8a8b7b53.
|
|
40
61
|
const cases: {
|
|
41
62
|
name: string;
|