@cparkerwebm/webmonterey 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +21 -0
  3. package/README.md +104 -0
  4. package/agents/.gitkeep +0 -0
  5. package/dist/webm.mjs +2381 -0
  6. package/hooks/.gitkeep +0 -0
  7. package/package.json +101 -0
  8. package/schema/design.json +118 -0
  9. package/skills/launch/SKILL.md +183 -0
  10. package/skills/new-component/SKILL.md +85 -0
  11. package/skills/start/SKILL.md +117 -0
  12. package/skills/traps/SKILL.md +333 -0
  13. package/skills/upgrade/SKILL.md +42 -0
  14. package/src/actions/index.ts +348 -0
  15. package/src/cli/checks.test.ts +711 -0
  16. package/src/cli/checks.ts +822 -0
  17. package/src/cli/codemods.test.ts +42 -0
  18. package/src/cli/codemods.ts +51 -0
  19. package/src/cli/compare.test.ts +144 -0
  20. package/src/cli/compare.ts +222 -0
  21. package/src/cli/design-extract.test.ts +96 -0
  22. package/src/cli/design-extract.ts +229 -0
  23. package/src/cli/doctor.ts +187 -0
  24. package/src/cli/mcp.test.ts +57 -0
  25. package/src/cli/mcp.ts +110 -0
  26. package/src/cli/new.ts +148 -0
  27. package/src/cli/package-root.ts +58 -0
  28. package/src/cli/scaffold.test.ts +230 -0
  29. package/src/cli/scaffold.ts +424 -0
  30. package/src/cli/seed.ts +133 -0
  31. package/src/cli/slug.test.ts +70 -0
  32. package/src/cli/slug.ts +109 -0
  33. package/src/cli/sync.test.ts +137 -0
  34. package/src/cli/sync.ts +266 -0
  35. package/src/cli/upgrade.ts +93 -0
  36. package/src/design/__fixtures__/tokens-v1.4.1.css +227 -0
  37. package/src/design/brand.ts +49 -0
  38. package/src/design/compile.test.ts +98 -0
  39. package/src/design/compile.ts +155 -0
  40. package/src/design/defaults.ts +315 -0
  41. package/src/design/index.ts +18 -0
  42. package/src/design/resolve.test.ts +80 -0
  43. package/src/design/resolve.ts +108 -0
  44. package/src/design/types.ts +96 -0
  45. package/src/emails/autoresponse.test.ts +82 -0
  46. package/src/emails/autoresponse.ts +83 -0
  47. package/src/emails/footer.test.ts +102 -0
  48. package/src/emails/footer.ts +91 -0
  49. package/src/emails/index.ts +24 -0
  50. package/src/emails/subject.test.ts +66 -0
  51. package/src/emails/subject.ts +80 -0
  52. package/src/emails/submission-notification.ts +76 -0
  53. package/src/env.d.ts +12 -0
  54. package/src/includes/cloudflare/d1/client.ts +86 -0
  55. package/src/includes/cloudflare/r2/README.md +68 -0
  56. package/src/includes/cloudflare/r2/media.ts +41 -0
  57. package/src/includes/cloudflare/r2/url.test.ts +44 -0
  58. package/src/includes/cloudflare/r2/url.ts +34 -0
  59. package/src/includes/cloudflare/turnstile/Turnstile.astro +161 -0
  60. package/src/includes/cloudflare/turnstile/verify.ts +157 -0
  61. package/src/includes/cloudflare/workers/env.ts +56 -0
  62. package/src/includes/google/tag-manager/TagManager.astro +73 -0
  63. package/src/includes/sinch/mailgun/redirect.test.ts +96 -0
  64. package/src/includes/sinch/mailgun/redirect.ts +96 -0
  65. package/src/includes/sinch/mailgun/send.ts +186 -0
  66. package/src/includes/webmonterey/animations/animations.css +347 -0
  67. package/src/includes/webmonterey/animations/observe.test.ts +88 -0
  68. package/src/includes/webmonterey/animations/observe.ts +209 -0
  69. package/src/includes/webmonterey/compliance/ConsentInit.astro +74 -0
  70. package/src/includes/webmonterey/compliance/CookieConsent.astro +558 -0
  71. package/src/includes/webmonterey/compliance/consent-styles.test.ts +83 -0
  72. package/src/includes/webmonterey/compliance/consent.ts +142 -0
  73. package/src/includes/webmonterey/config.test.ts +94 -0
  74. package/src/includes/webmonterey/config.ts +346 -0
  75. package/src/includes/webmonterey/copy-defaults.ts +148 -0
  76. package/src/includes/webmonterey/copy.ts +13 -0
  77. package/src/includes/webmonterey/credits/Credit.astro +80 -0
  78. package/src/includes/webmonterey/credits/credit.test.ts +111 -0
  79. package/src/includes/webmonterey/credits/credit.ts +59 -0
  80. package/src/includes/webmonterey/forms/honeypot.test.ts +40 -0
  81. package/src/includes/webmonterey/forms/honeypot.ts +66 -0
  82. package/src/includes/webmonterey/prose/inline.test.ts +94 -0
  83. package/src/includes/webmonterey/prose/inline.ts +71 -0
  84. package/src/includes/webmonterey/scroll-top/ScrollTop.astro +209 -0
  85. package/src/includes/webmonterey/site.ts +136 -0
  86. package/src/includes/webmonterey/structured-data/nodes.ts +315 -0
  87. package/src/includes/webmonterey/zoned-hour.test.ts +49 -0
  88. package/src/integration/adapter.ts +53 -0
  89. package/src/integration/app-middleware.ts +43 -0
  90. package/src/integration/config.ts +96 -0
  91. package/src/integration/content.ts +66 -0
  92. package/src/integration/image-size.test.ts +83 -0
  93. package/src/integration/image-size.ts +100 -0
  94. package/src/integration/index.ts +386 -0
  95. package/src/integration/virtual.d.ts +101 -0
  96. package/src/layouts/base.astro +402 -0
  97. package/src/package.test.ts +121 -0
  98. package/src/pages/404.astro +33 -0
  99. package/src/pages/[...slug].astro +126 -0
  100. package/src/pages/robots.txt.ts +41 -0
  101. package/src/pages/webm.astro +23 -0
  102. package/src/scripts/.gitkeep +0 -0
  103. package/src/styles/base.css +185 -0
  104. package/src/styles/global.css +47 -0
  105. package/src/styles/layers.test.ts +32 -0
  106. package/src/styles/layers.ts +21 -0
  107. package/src/styles/layout.css +235 -0
  108. package/src/styles/reset.css +135 -0
  109. package/src/styles/utilities.css +90 -0
  110. package/src/worker.ts +76 -0
  111. package/template/assets/logo.svg +14 -0
  112. package/template/assets/open-graph.png +0 -0
  113. package/template/migrations/0001_create_submissions.sql +40 -0
  114. package/template/migrations/README.md +41 -0
  115. package/template/public/android-chrome-192x192.png +0 -0
  116. package/template/public/android-chrome-512x512.png +0 -0
  117. package/template/public/apple-touch-icon.png +0 -0
  118. package/template/public/favicon-16x16.png +0 -0
  119. package/template/public/favicon-32x32.png +0 -0
  120. package/template/public/favicon.ico +0 -0
  121. package/template/public/favicon.svg +4 -0
  122. package/template/public/open-graph.png +0 -0
  123. package/template/scripts/check-node.mjs +72 -0
  124. package/template/scripts/test-hooks.mjs +96 -0
  125. package/template/site/.editorconfig +22 -0
  126. package/template/site/.prettierignore +15 -0
  127. package/template/site/.prettierrc.json +17 -0
  128. package/template/site/CLAUDE.md +245 -0
  129. package/template/site/CONTENT.md +135 -0
  130. package/template/site/PRIVACY-POLICY-DRAFT.md +81 -0
  131. package/template/site/public/_headers +70 -0
  132. package/template/site/src/forms/contact.json +45 -0
  133. package/template/workflows/ci.yml +52 -0
@@ -0,0 +1,711 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { CHECKS, onDemandRoutes, type CheckContext } from './checks.ts';
4
+ import { MCP_NAMES, MCP_SERVERS, mcpConfig } from './mcp.ts';
5
+
6
+ const base = (over: Partial<CheckContext> = {}): CheckContext => ({
7
+ site: { client: 'Acme', domain: 'acme.com' },
8
+ wrangler: { assets: { run_worker_first: ['/_actions/*'] } },
9
+ pages: new Map(),
10
+ components: new Map(),
11
+ today: '2026-08-26',
12
+ workerEntry: null,
13
+ contentPages: [],
14
+ content: new Map(),
15
+ actions: new Map(),
16
+ includes: new Map(),
17
+ emails: new Map(),
18
+ migrations: new Map(),
19
+ registry: null,
20
+ present: {},
21
+ placeholders: [],
22
+ sync: { version: '1.0.0', skills: ['launch'] },
23
+ mcp: { declared: mcpConfig().mcpServers, enabled: [...MCP_NAMES] },
24
+ version: '1.0.0',
25
+ ...over,
26
+ });
27
+
28
+ const runCheck = (id: string, ctx: CheckContext) => CHECKS.find((c) => c.id === id)!.run(ctx);
29
+
30
+ test('an on-demand route not in run_worker_first fails, naming both slash forms', () => {
31
+ const ctx = base({
32
+ pages: new Map([['src/pages/contact.astro', 'export const prerender = false;']]),
33
+ });
34
+ const r = runCheck('run-worker-first', ctx);
35
+ assert.equal(r.status, 'fail');
36
+ assert.match(r.detail!, /\/contact/);
37
+ assert.match(r.detail!, /\/contact\//);
38
+ });
39
+
40
+ test('a wildcard entry covers the route it prefixes', () => {
41
+ const ctx = base({
42
+ pages: new Map([['src/pages/api/lead.ts', 'export const prerender = false;']]),
43
+ wrangler: { assets: { run_worker_first: ['/api/*'] } },
44
+ });
45
+ assert.equal(runCheck('run-worker-first', ctx).status, 'pass');
46
+ });
47
+
48
+ test('a prerendered route needs no entry', () => {
49
+ const ctx = base({ pages: new Map([['src/pages/about.astro', '<h1>About</h1>']]) });
50
+ assert.equal(runCheck('run-worker-first', ctx).status, 'pass');
51
+ });
52
+
53
+ test('CHANGEME anywhere in the config fails, naming the fields', () => {
54
+ const r = runCheck('changeme', base({ site: { client: 'CHANGEME', domain: 'CHANGEME' } }));
55
+ assert.equal(r.status, 'fail');
56
+ assert.match(r.detail!, /client/);
57
+ assert.match(r.detail!, /domain/);
58
+ });
59
+
60
+ test('Pacific/LA is caught before it reaches a Worker', () => {
61
+ const r = runCheck(
62
+ 'timezone',
63
+ base({ site: { client: 'A', domain: 'a.com', timeZone: 'Pacific/LA' } }),
64
+ );
65
+ assert.equal(r.status, 'fail');
66
+ assert.match(r.detail!, /Pacific\/LA/);
67
+ });
68
+
69
+ test('a valid zone passes and an unset one only warns', () => {
70
+ assert.equal(
71
+ runCheck(
72
+ 'timezone',
73
+ base({ site: { client: 'A', domain: 'a.com', timeZone: 'America/Los_Angeles' } }),
74
+ ).status,
75
+ 'pass',
76
+ );
77
+ assert.equal(runCheck('timezone', base()).status, 'warn');
78
+ });
79
+
80
+ test('a missing sync directory fails and names --ignore-scripts', () => {
81
+ const r = runCheck('skills-synced', base({ sync: null }));
82
+ assert.equal(r.status, 'fail');
83
+ assert.match(r.detail!, /ignore-scripts/);
84
+ });
85
+
86
+ test('a stale sync warns rather than fails - the skills still work', () => {
87
+ assert.equal(
88
+ runCheck('skills-synced', base({ sync: { version: '0.9.0', skills: [] } })).status,
89
+ 'warn',
90
+ );
91
+ });
92
+
93
+ test('querySelector<HTMLSelectElement> is caught, and the message says to cast', () => {
94
+ const ctx = base({
95
+ components: new Map([
96
+ ['src/components/x.astro', 'document.querySelector<HTMLSelectElement>("#t")'],
97
+ ]),
98
+ });
99
+ const r = runCheck('select-element', ctx);
100
+ assert.equal(r.status, 'fail');
101
+ assert.match(r.detail!, /Cast instead/);
102
+ });
103
+
104
+ test('<Image> on an on-demand route fails unless it branches on isPrerendered', () => {
105
+ const bad = base({
106
+ pages: new Map([['src/pages/x.astro', 'export const prerender = false;\n<Image src={a} />']]),
107
+ });
108
+ assert.equal(runCheck('image-on-demand', bad).status, 'fail');
109
+
110
+ const guarded = base({
111
+ pages: new Map([
112
+ [
113
+ 'src/pages/x.astro',
114
+ 'export const prerender = false;\nAstro.isPrerendered ? <Image src={a}/> : <img/>',
115
+ ],
116
+ ]),
117
+ });
118
+ assert.equal(runCheck('image-on-demand', guarded).status, 'pass');
119
+ });
120
+
121
+ test('a literal color in component CSS warns, but a token declaration does not', () => {
122
+ const bad = base({
123
+ components: new Map([['src/components/x.astro', '<style>.a{color:#ff0000}</style>']]),
124
+ });
125
+ assert.equal(runCheck('literal-values', bad).status, 'warn');
126
+
127
+ const ok = base({
128
+ components: new Map([['src/components/x.astro', '<style>.a{color:var(--webm-text)}</style>']]),
129
+ });
130
+ assert.equal(runCheck('literal-values', ok).status, 'pass');
131
+ });
132
+
133
+ test('onDemandRoutes maps file paths to url paths', () => {
134
+ const pages = new Map([
135
+ ['src/pages/contact.astro', 'export const prerender = false'],
136
+ ['src/pages/api/lead.ts', 'export const prerender = false'],
137
+ ['src/pages/about.astro', 'static'],
138
+ ]);
139
+ assert.deepEqual(onDemandRoutes(pages).sort(), ['/api/lead', '/contact']);
140
+ });
141
+
142
+ test('a block type missing from the registry FAILS, naming the file it is in', () => {
143
+ /*
144
+ * The one that costs an hour. A block whose type is not registered renders as nothing: 200 on
145
+ * the route, clean build, section simply absent - which looks exactly like a CSS bug.
146
+ */
147
+ const ctx = base({
148
+ registry: `export const blocks = { 'content-000001': Content };`,
149
+ content: new Map([['src/content/pages/home.json', '{"blocks":[{"type":"content-000002"}]}']]),
150
+ });
151
+ const result = runCheck('block-types-registered', ctx);
152
+ assert.equal(result.status, 'fail');
153
+ assert.match(result.detail!, /content-000002/);
154
+ assert.match(result.detail!, /home\.json/, 'the message names the file to open');
155
+ });
156
+
157
+ test('a registered block type passes', () => {
158
+ const ctx = base({
159
+ registry: `export const blocks = { 'content-000001': Content, 'regions-000004': Hero };`,
160
+ content: new Map([
161
+ [
162
+ 'src/content/pages/home.json',
163
+ '{"blocks":[{"type":"content-000001"},{"type":"regions-000004"}]}',
164
+ ],
165
+ ]),
166
+ });
167
+ assert.equal(runCheck('block-types-registered', ctx).status, 'pass');
168
+ });
169
+
170
+ test('a freshly scaffolded site passes - empty registry, no content, not a failure', () => {
171
+ // `webm new` produces exactly this, and a doctor that fails on a brand new site trains people
172
+ // to ignore it.
173
+ const ctx = base({ registry: `export const blocks = {};`, content: new Map() });
174
+ assert.equal(runCheck('block-types-registered', ctx).status, 'pass');
175
+ });
176
+
177
+ test('a missing _headers warns rather than fails, and says how to get it back', () => {
178
+ const ctx = base({ present: { 'public/_headers': false, 'public/favicon.ico': true } });
179
+ const result = runCheck('seeded-files', ctx);
180
+ assert.equal(result.status, 'warn');
181
+ assert.match(result.detail!, /public\/_headers/);
182
+ assert.match(result.detail!, /webm sync/);
183
+ });
184
+
185
+ test('a compatibility_date past the measured breaking point FAILS, naming the symptom', () => {
186
+ /*
187
+ * 2026-01-01 against 2026-08-26 is 237 days, and that exact pair was observed rendering
188
+ * [object Object] on wrangler 4.126. It has to FAIL, not warn - the first draft of this check
189
+ * warned here, which is the check being wrong in the only direction that matters.
190
+ */
191
+ const ctx = base({
192
+ today: '2026-08-26',
193
+ wrangler: { compatibility_date: '2026-01-01', assets: { run_worker_first: ['/_actions/*'] } },
194
+ });
195
+ const result = runCheck('compatibility-date', ctx);
196
+ assert.equal(result.status, 'fail');
197
+ assert.match(result.detail!, /\[object Object\]/, 'names the symptom someone would google');
198
+ });
199
+
200
+ test('a four-month-stale date warns - not broken yet, but one cycle from it', () => {
201
+ const ctx = base({
202
+ today: '2026-08-26',
203
+ wrangler: { compatibility_date: '2026-04-20', assets: { run_worker_first: ['/_actions/*'] } },
204
+ });
205
+ assert.equal(runCheck('compatibility-date', ctx).status, 'warn');
206
+ });
207
+
208
+ test('a current compatibility_date passes', () => {
209
+ const ctx = base({
210
+ today: '2026-08-26',
211
+ wrangler: { compatibility_date: '2026-08-01', assets: { run_worker_first: ['/_actions/*'] } },
212
+ });
213
+ assert.equal(runCheck('compatibility-date', ctx).status, 'pass');
214
+ });
215
+
216
+ test('a site with components but no credit import warns', () => {
217
+ const ctx = base({
218
+ components: new Map([['src/components/regions/footer/footer.astro', '<footer>hi</footer>']]),
219
+ });
220
+ const result = runCheck('agency-credit', ctx);
221
+ assert.equal(result.status, 'warn');
222
+ assert.match(result.detail!, /footer component/);
223
+ });
224
+
225
+ test('a footer that imports the credit passes', () => {
226
+ const ctx = base({
227
+ components: new Map([
228
+ [
229
+ 'src/components/regions/footer/footer.astro',
230
+ `import Credit from '@cparkerwebm/webmonterey/webmonterey/credits/Credit.astro';`,
231
+ ],
232
+ ]),
233
+ });
234
+ assert.equal(runCheck('agency-credit', ctx).status, 'pass');
235
+ });
236
+
237
+ test('a site with no components yet is not nagged', () => {
238
+ assert.equal(runCheck('agency-credit', base()).status, 'pass');
239
+ });
240
+
241
+ test('a comment explaining a trap does not trip the check that enforces it', () => {
242
+ /*
243
+ * Three checks have now fired on their own documentation. Good code explains its traps using
244
+ * the exact words the trap is about, so a naive substring scan is guaranteed to hit the prose.
245
+ */
246
+ const ctx = base({
247
+ components: new Map([
248
+ [
249
+ 'src/components/regions/header/header.astro',
250
+ `/*\n * No querySelector<HTMLSelectElement> generic - see CLAUDE.md.\n */\nconst el = root.querySelector('.thing');`,
251
+ ],
252
+ ]),
253
+ pages: new Map([
254
+ [
255
+ 'src/pages/thing.astro',
256
+ `// querySelector<HTMLSelectElement> is what this file avoids\nconst x = 1;`,
257
+ ],
258
+ ]),
259
+ });
260
+ assert.equal(runCheck('select-element', ctx).status, 'pass');
261
+ });
262
+
263
+ test('the real thing is still caught once the comments are gone', () => {
264
+ const ctx = base({
265
+ components: new Map([
266
+ ['src/components/a.astro', `const el = root.querySelector<HTMLSelectElement>('.thing');`],
267
+ ]),
268
+ });
269
+ assert.equal(runCheck('select-element', ctx).status, 'fail');
270
+ });
271
+
272
+ test("WebMonterey's own site is not asked to credit itself", () => {
273
+ const ctx = base({
274
+ site: { client: 'WebMonterey', domain: 'webmonterey.com' },
275
+ components: new Map([['src/components/regions/footer/footer.astro', '<footer>x</footer>']]),
276
+ });
277
+ assert.equal(runCheck('agency-credit', ctx).status, 'pass');
278
+ });
279
+
280
+ test('a cron with no custom entrypoint FAILS, and names the fix', () => {
281
+ /*
282
+ * Measured against @astrojs/cloudflare 14.2.5: triggers.crons merges into the generated
283
+ * wrangler.json and deploys clean, and the generated entry exports fetch only. Firing
284
+ * /__scheduled against that build returns 404 from the asset router.
285
+ */
286
+ const ctx = base({
287
+ wrangler: { triggers: { crons: ['0 * * * *'] }, assets: { run_worker_first: ['/_actions/*'] } },
288
+ });
289
+ const result = runCheck('cron-without-handler', ctx);
290
+ assert.equal(result.status, 'fail');
291
+ assert.match(result.detail!, /main/);
292
+ assert.match(result.detail!, /@astrojs\/cloudflare\/handler/);
293
+ });
294
+
295
+ test('a cron WITH a proper custom entrypoint passes - this is the supported path', () => {
296
+ // friendsofthemarinalibrary.org has run its evening summary sweep this way since generation 2.
297
+ const ctx = base({
298
+ wrangler: {
299
+ main: './src/worker.ts',
300
+ triggers: { crons: ['0 * * * *'] },
301
+ assets: { run_worker_first: ['/_actions/*'] },
302
+ },
303
+ workerEntry: `import { handle } from '@astrojs/cloudflare/handler';
304
+ export default {
305
+ async fetch(request, env, ctx) { return handle(request, env, ctx); },
306
+ async scheduled(controller, env, ctx) { ctx.waitUntil(sweep(env)); },
307
+ } satisfies ExportedHandler<Env>;`,
308
+ });
309
+ assert.equal(runCheck('cron-without-handler', ctx).status, 'pass');
310
+ });
311
+
312
+ test('an entrypoint that forgets the adapter handler FAILS - it would unserve the whole site', () => {
313
+ const ctx = base({
314
+ wrangler: {
315
+ main: './src/worker.ts',
316
+ triggers: { crons: ['0 * * * *'] },
317
+ assets: { run_worker_first: [] },
318
+ },
319
+ workerEntry: `export default { async scheduled(c, env, ctx) { ctx.waitUntil(sweep(env)); } };`,
320
+ });
321
+ const result = runCheck('cron-without-handler', ctx);
322
+ assert.equal(result.status, 'fail');
323
+ assert.match(result.detail!, /stops being served|@astrojs\/cloudflare\/handler/);
324
+ });
325
+
326
+ test('the Pages-era main value is refused by name', () => {
327
+ const ctx = base({
328
+ wrangler: {
329
+ main: './dist/_worker.js/index.js',
330
+ triggers: { crons: ['0 * * * *'] },
331
+ assets: { run_worker_first: [] },
332
+ },
333
+ });
334
+ assert.match(runCheck('cron-without-handler', ctx).detail!, /build output|SOURCE/);
335
+ });
336
+
337
+ test('a compatibility_date in the FUTURE fails - the site would not build at all', () => {
338
+ /*
339
+ * The direction the check originally missed. A date newer than the runtime bundled with the
340
+ * installed wrangler is refused outright: miniflare throws ERR_FUTURE_COMPATIBILITY_DATE. Two
341
+ * client sites hit it and it was diagnosed twice, independently, because the check only ever
342
+ * looked for a date that was too old.
343
+ */
344
+ const ctx = base({
345
+ today: '2026-08-27',
346
+ wrangler: { compatibility_date: '2026-12-31', assets: { run_worker_first: ['/_actions/*'] } },
347
+ });
348
+ const result = runCheck('compatibility-date', ctx);
349
+ assert.equal(result.status, 'fail');
350
+ assert.match(result.detail!, /FUTURE/);
351
+ assert.match(result.detail!, /ERR_FUTURE_COMPATIBILITY_DATE/);
352
+ });
353
+
354
+ test('a nested schema.org type is not mistaken for a block type', () => {
355
+ /*
356
+ * A page describing a physical place carries `place.type: "BookStore"`, and a form field
357
+ * carries its own `type`. Matching every "type" key in the file reported a real site as broken
358
+ * for having a bookshop on a page.
359
+ */
360
+ const ctx = base({
361
+ registry: `export const blocks = { 'content-000001': C };`,
362
+ content: new Map([
363
+ [
364
+ 'src/content/pages/bookstore.json',
365
+ JSON.stringify({
366
+ place: { type: 'BookStore', name: 'The Shop' },
367
+ blocks: [{ type: 'content-000001', fields: [{ type: 'email' }] }],
368
+ }),
369
+ ],
370
+ ]),
371
+ });
372
+ assert.equal(runCheck('block-types-registered', ctx).status, 'pass');
373
+ });
374
+
375
+ test('an unregistered BLOCK type is still caught', () => {
376
+ const ctx = base({
377
+ registry: `export const blocks = { 'content-000001': C };`,
378
+ content: new Map([
379
+ ['src/content/pages/home.json', JSON.stringify({ blocks: [{ type: 'content-000009' }] })],
380
+ ]),
381
+ });
382
+ assert.match(runCheck('block-types-registered', ctx).detail!, /content-000009/);
383
+ });
384
+
385
+ test('a worker entry using defineWorker satisfies the cron check', () => {
386
+ // defineWorker supplies the adapter's fetch; requiring the raw import flagged every site
387
+ // using the package helper as broken.
388
+ const ctx = base({
389
+ wrangler: {
390
+ main: './src/worker.ts',
391
+ triggers: { crons: ['0 * * * *'] },
392
+ assets: { run_worker_first: [] },
393
+ },
394
+ workerEntry: `import { defineWorker } from '@cparkerwebm/webmonterey/worker';
395
+ export default defineWorker({ scheduled: (c, env, ctx) => ctx.waitUntil(sweep(env)) });`,
396
+ });
397
+ assert.equal(runCheck('cron-without-handler', ctx).status, 'pass');
398
+ });
399
+
400
+ test("a placeholder favicon still in public/ fails - it is the agency's mark on a client site", () => {
401
+ /*
402
+ * Caught on a rebuild: the client had .ico and PNG icons but no SVG, the scaffold seeds one,
403
+ * and browsers PREFER SVG - so WebMonterey's own mark became the client's icon. Nobody looks
404
+ * at a favicon, which is exactly why it would have stayed there.
405
+ */
406
+ const ctx = base({
407
+ site: { client: 'Acme', domain: 'acme.com', launched: '2026-03-01' },
408
+ placeholders: ['public/favicon.svg', 'public/open-graph.png'],
409
+ });
410
+ const result = runCheck('placeholder-branding', ctx);
411
+ assert.equal(result.status, 'fail', 'a LAUNCHED site shipping the agency mark is a fault');
412
+ assert.match(result.detail!, /favicon\.svg/);
413
+ });
414
+
415
+ test('a site that replaced the artwork passes', () => {
416
+ assert.equal(runCheck('placeholder-branding', base()).status, 'pass');
417
+ });
418
+
419
+ test('a registry keyed by name rather than by number is understood', () => {
420
+ /*
421
+ * Most of the fleet numbers its components; autire names them "hero.standard", "article.feed".
422
+ * Requiring the numbered form meant none of its 39 registrations matched and every block on
423
+ * the site was reported unregistered. A convention is not a syntax.
424
+ */
425
+ const ctx = base({
426
+ registry: `export const blocks = { 'hero.standard': H, 'article.feed': A, faq: F };`,
427
+ content: new Map([
428
+ [
429
+ 'src/content/pages/home.json',
430
+ JSON.stringify({ blocks: [{ type: 'hero.standard' }, { type: 'article.feed' }] }),
431
+ ],
432
+ ]),
433
+ });
434
+ assert.equal(runCheck('block-types-registered', ctx).status, 'pass');
435
+ });
436
+
437
+ test('a site that has not launched only gets a warning about placeholders', () => {
438
+ /*
439
+ * `webm new` seeds every one of these, so a freshly scaffolded site has the full set. The
440
+ * check first shipped as a hard failure and the end-to-end test caught it immediately: it
441
+ * scaffolds a site and the doctor failed it. A check that fails on the scaffold's own output
442
+ * is one people learn to skip.
443
+ */
444
+ const ctx = base({
445
+ site: { client: 'Acme', domain: 'acme.com' },
446
+ placeholders: ['public/favicon.svg'],
447
+ });
448
+ assert.equal(runCheck('placeholder-branding', ctx).status, 'warn');
449
+ });
450
+
451
+ /*
452
+ * THE THREE CHECKS THAT EXIST BECAUSE OF THE AUGUST 2026 REBUILD AUDIT.
453
+ *
454
+ * Each one reproduces a real failure that shipped and stayed quiet: an action the rebuild
455
+ * dropped while the button kept calling it, migrations left behind in the old repo while
456
+ * production ran on tables that already existed, and a D1 feature flag with nothing bound.
457
+ *
458
+ * The negative cases matter as much as the positive ones. `actions` is an ordinary English word
459
+ * and SQL keywords are ordinary English words; a check that cries wolf on either gets ignored,
460
+ * and an ignored check is worse than no check.
461
+ */
462
+ test('actions-exist catches a call to an action nothing exports', () => {
463
+ const r = runCheck(
464
+ 'actions-exist',
465
+ base({
466
+ components: new Map([
467
+ [
468
+ 'src/components/x.astro',
469
+ `import { actions } from 'astro:actions';\nactions.registerForEvent(fd);`,
470
+ ],
471
+ ]),
472
+ actions: new Map([
473
+ ['src/actions/index.ts', `export { server } from '@cparkerwebm/webmonterey/actions';`],
474
+ ]),
475
+ }),
476
+ );
477
+ assert.equal(r.status, 'fail');
478
+ assert.match(r.detail!, /registerForEvent/);
479
+ });
480
+
481
+ test('actions-exist accepts an action the site adds beside the package server', () => {
482
+ const r = runCheck(
483
+ 'actions-exist',
484
+ base({
485
+ components: new Map([
486
+ [
487
+ 'src/components/x.astro',
488
+ `import { actions } from 'astro:actions';\nactions.registerForEvent(fd);`,
489
+ ],
490
+ ]),
491
+ actions: new Map([
492
+ [
493
+ 'src/actions/index.ts',
494
+ `import { server as webm } from '@cparkerwebm/webmonterey/actions';\nimport { registerForEvent } from './events';\nexport const server = {\n ...webm,\n registerForEvent,\n}`,
495
+ ],
496
+ ['src/actions/events.ts', `export const registerForEvent = defineAction({});`],
497
+ ]),
498
+ }),
499
+ );
500
+ assert.equal(r.status, 'pass');
501
+ });
502
+
503
+ test('actions-exist accepts the package action with no site actions file at all', () => {
504
+ const r = runCheck(
505
+ 'actions-exist',
506
+ base({
507
+ components: new Map([
508
+ [
509
+ 'src/components/x.astro',
510
+ `import { actions } from 'astro:actions';\nactions.submitForm(fd);`,
511
+ ],
512
+ ]),
513
+ }),
514
+ );
515
+ assert.equal(r.status, 'pass');
516
+ });
517
+
518
+ test('actions-exist ignores a variable that merely happens to be called actions', () => {
519
+ /* webmonterey.com really has this: a DOM node named `actions`, and no astro:actions import. */
520
+ const r = runCheck(
521
+ 'actions-exist',
522
+ base({
523
+ components: new Map([
524
+ [
525
+ 'src/components/header.astro',
526
+ `const actions = header.querySelector('[data-actions]');\nactions.getBoundingClientRect();`,
527
+ ],
528
+ ]),
529
+ }),
530
+ );
531
+ assert.equal(r.status, 'pass');
532
+ });
533
+
534
+ test('migrations-cover-tables catches a table no migration creates', () => {
535
+ const r = runCheck(
536
+ 'migrations-cover-tables',
537
+ base({
538
+ site: { client: 'Acme', domain: 'acme.com', features: { d1: true } },
539
+ includes: new Map([
540
+ ['src/includes/events/store.ts', 'const q = `SELECT id FROM events WHERE x = ?`;'],
541
+ ]),
542
+ migrations: new Map([
543
+ ['migrations/0001_create_submissions.sql', 'CREATE TABLE submissions (id INTEGER);'],
544
+ ]),
545
+ }),
546
+ );
547
+ assert.equal(r.status, 'fail');
548
+ assert.match(r.detail!, /events/);
549
+ });
550
+
551
+ test('migrations-cover-tables passes once the migration is there', () => {
552
+ const r = runCheck(
553
+ 'migrations-cover-tables',
554
+ base({
555
+ site: { client: 'Acme', domain: 'acme.com', features: { d1: true } },
556
+ includes: new Map([['src/includes/events/store.ts', 'const q = `SELECT id FROM events`;']]),
557
+ migrations: new Map([
558
+ ['migrations/0001.sql', 'CREATE TABLE submissions (id INTEGER);'],
559
+ ['migrations/0002.sql', 'CREATE TABLE IF NOT EXISTS events (id INTEGER);'],
560
+ ]),
561
+ }),
562
+ );
563
+ assert.equal(r.status, 'pass');
564
+ });
565
+
566
+ test('migrations-cover-tables does not read an import statement as SQL', () => {
567
+ /* Lowercase `from` is every import in the repo. Only uppercase SQL counts. */
568
+ const r = runCheck(
569
+ 'migrations-cover-tables',
570
+ base({
571
+ site: { client: 'Acme', domain: 'acme.com', features: { d1: true } },
572
+ includes: new Map([
573
+ ['src/includes/a.ts', `import { all } from 'astro:content';\nimport x from './y';`],
574
+ ]),
575
+ migrations: new Map([['migrations/0001.sql', 'CREATE TABLE submissions (id INTEGER);']]),
576
+ }),
577
+ );
578
+ assert.equal(r.status, 'pass');
579
+ });
580
+
581
+ test('d1-binding catches features.d1 with nothing bound', () => {
582
+ const r = runCheck(
583
+ 'd1-binding',
584
+ base({
585
+ site: { client: 'Acme', domain: 'acme.com', features: { d1: true } },
586
+ wrangler: {},
587
+ }),
588
+ );
589
+ assert.equal(r.status, 'fail');
590
+ });
591
+
592
+ test('d1-binding stays quiet when the site does not use D1', () => {
593
+ const r = runCheck(
594
+ 'd1-binding',
595
+ base({
596
+ site: { client: 'Acme', domain: 'acme.com', features: { d1: false } },
597
+ wrangler: {},
598
+ }),
599
+ );
600
+ assert.equal(r.status, 'pass');
601
+ });
602
+
603
+ test('an enabled app with a custom path needs the PUBLIC path in run_worker_first', () => {
604
+ const site = { client: 'A', domain: 'a.com', app: { enabled: true, path: 'portal' } };
605
+ const pages = new Map([['src/pages/webapp/dashboard.astro', 'export const prerender = false;']]);
606
+
607
+ const bad = base({ site, pages, wrangler: { assets: { run_worker_first: ['/_actions/*'] } } });
608
+ const r = runCheck('app-namespace', bad);
609
+ assert.equal(r.status, 'fail');
610
+ assert.match(r.detail!, /\/portal\/\*/);
611
+ /* And the generic route check asks for the public route, not the folder route. */
612
+ assert.match(runCheck('run-worker-first', bad).detail!, /\/portal\/dashboard/);
613
+
614
+ const good = base({
615
+ site,
616
+ pages,
617
+ wrangler: { assets: { run_worker_first: ['/_actions/*', '/portal/*', '/portal', '/portal/'] } },
618
+ });
619
+ assert.equal(runCheck('app-namespace', good).status, 'pass');
620
+ assert.equal(runCheck('run-worker-first', good).status, 'pass');
621
+ });
622
+
623
+ test('a page JSON named like the app path is a collision, and a prerendered app page is a fault', () => {
624
+ const r = runCheck(
625
+ 'app-namespace',
626
+ base({
627
+ site: { client: 'A', domain: 'a.com', app: { enabled: true, path: 'portal' } },
628
+ contentPages: ['home', 'portal'],
629
+ pages: new Map([['src/pages/webapp/index.astro', '<h1>static</h1>']]),
630
+ wrangler: {
631
+ assets: { run_worker_first: ['/_actions/*', '/portal/*', '/portal', '/portal/'] },
632
+ },
633
+ }),
634
+ );
635
+ assert.equal(r.status, 'fail');
636
+ assert.match(r.detail!, /collides/);
637
+ assert.match(r.detail!, /prerender = false/);
638
+ });
639
+
640
+ test('app routes with the app switched off only warn, and no routes is silence', () => {
641
+ assert.equal(runCheck('app-namespace', base()).status, 'pass');
642
+ const r = runCheck(
643
+ 'app-namespace',
644
+ base({ pages: new Map([['src/pages/webapp/x.astro', 'export const prerender = false;']]) }),
645
+ );
646
+ assert.equal(r.status, 'warn');
647
+ assert.match(r.detail!, /app\.enabled/);
648
+ });
649
+
650
+ test('a staging site with no stagingEmail FAILS - every send would throw', () => {
651
+ const r = runCheck(
652
+ 'staging-email',
653
+ base({ site: { client: 'A', domain: 'a.com', environment: 'staging', stagingEmail: '' } }),
654
+ );
655
+ assert.equal(r.status, 'fail');
656
+ assert.match(r.detail!, /stagingEmail/);
657
+ });
658
+
659
+ test('a staging site with an address passes, and production never needs one', () => {
660
+ assert.equal(
661
+ runCheck(
662
+ 'staging-email',
663
+ base({
664
+ site: { client: 'A', domain: 'a.com', environment: 'staging', stagingEmail: 'me@a.com' },
665
+ }),
666
+ ).status,
667
+ 'pass',
668
+ );
669
+ assert.equal(
670
+ runCheck('staging-email', base({ site: { client: 'A', domain: 'a.com' } })).status,
671
+ 'pass',
672
+ );
673
+ });
674
+
675
+ test('a fully wired site passes the docs-server check', () => {
676
+ assert.equal(runCheck('mcp-docs', base()).status, 'pass');
677
+ });
678
+
679
+ test('a site with no .mcp.json at all fails rather than passing quietly', () => {
680
+ const r = runCheck('mcp-docs', base({ mcp: { declared: null, enabled: null } }));
681
+ assert.equal(r.status, 'fail');
682
+ assert.match(r.detail!, /no \.mcp\.json/);
683
+ });
684
+
685
+ /*
686
+ * The regression that motivated the check: MDN was added to the scaffold after sites existed, so
687
+ * a site can hold a perfectly valid .mcp.json that simply predates a server.
688
+ */
689
+ test('a server missing from .mcp.json is named, not just counted', () => {
690
+ const { mdn, ...rest } = mcpConfig().mcpServers;
691
+ void mdn;
692
+ const r = runCheck('mcp-docs', base({ mcp: { declared: rest, enabled: ['astro-docs'] } }));
693
+ assert.equal(r.status, 'fail');
694
+ assert.match(r.detail!, /missing from \.mcp\.json: mdn/);
695
+ });
696
+
697
+ test('a declared but un-approved server fails, because it is inert until someone clicks', () => {
698
+ const r = runCheck('mcp-docs', base({ mcp: { declared: mcpConfig().mcpServers, enabled: [] } }));
699
+ assert.equal(r.status, 'fail');
700
+ assert.match(r.detail!, /enabledMcpjsonServers/);
701
+ assert.match(r.detail!, /astro-docs/);
702
+ assert.match(r.detail!, /mdn/);
703
+ });
704
+
705
+ test('a server pointed at the wrong url is caught, and both urls are shown', () => {
706
+ const declared = { ...mcpConfig().mcpServers, mdn: { type: 'http', url: 'https://example.com' } };
707
+ const r = runCheck('mcp-docs', base({ mcp: { declared, enabled: [...MCP_NAMES] } }));
708
+ assert.equal(r.status, 'fail');
709
+ assert.match(r.detail!, /example\.com/);
710
+ assert.ok(r.detail!.includes(MCP_SERVERS.mdn.url));
711
+ });