@invisibleloop/pulse 0.20.37 → 0.20.39

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.
@@ -1,617 +0,0 @@
1
- /**
2
- * MCP integrity tests — run with: node src/mcp/mcp.test.js
3
- *
4
- * Guards against guide file drift: every guide-*.md file under src/agent/
5
- * must be registered as a named MCP resource in server.js, and every
6
- * readFileSync call in server.js that references a guide-*.md must point
7
- * to a file that actually exists.
8
- */
9
-
10
- import fs from 'node:fs'
11
- import path from 'node:path'
12
- import { fileURLToPath } from 'node:url'
13
- import { execFileSync } from 'node:child_process'
14
-
15
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
16
- // Package root (packages/pulse/) — canonical source for src/agent/*.
17
- const ROOT = path.resolve(__dirname, '../..')
18
- // Repo root (pulse2/) — where distributed copies live: .claude/, docs/,
19
- // examples/, CLAUDE.md. Package root moved into packages/pulse/ when the
20
- // framework became a workspace member; these distributed-copy targets did
21
- // not move, so they need one extra level up from the package root.
22
- const REPO_ROOT = path.resolve(ROOT, '..', '..')
23
-
24
- let passed = 0
25
- let failed = 0
26
-
27
- function test(label, fn) {
28
- try {
29
- fn()
30
- console.log(` ✓ ${label}`)
31
- passed++
32
- } catch (e) {
33
- console.log(` ✗ ${label}`)
34
- console.log(` ${e.message}`)
35
- failed++
36
- }
37
- }
38
-
39
- function assert(condition, msg) {
40
- if (!condition) throw new Error(msg || 'Assertion failed')
41
- }
42
-
43
- // ── Read source files ────────────────────────────────────────────────────────
44
-
45
- const serverSrc = fs.readFileSync(path.join(__dirname, 'server.js'), 'utf8')
46
- // validateContent's real logic — extracted from server.js into cli/validate.js
47
- // so `pulse validate` (CLI) and pulse_validate (MCP) share one implementation.
48
- const validateSrc = fs.readFileSync(path.join(ROOT, 'src/cli/validate.js'), 'utf8')
49
- // checkBundles' real logic — extracted into cli/check-bundles.js so
50
- // `pulse check-bundles` (CLI) and pulse_check_bundles (MCP) share one implementation.
51
- const checkBundlesSrc = fs.readFileSync(path.join(ROOT, 'src/cli/check-bundles.js'), 'utf8')
52
- // pulse_review's real logic — extracted into cli/review.js so `pulse review`
53
- // (CLI) and pulse_review (MCP) share one implementation.
54
- const reviewSrc = fs.readFileSync(path.join(ROOT, 'src/cli/review.js'), 'utf8')
55
- const agentDir = path.join(ROOT, 'src/agent')
56
-
57
- // All guide-*.md files that exist on disk
58
- const guideFiles = fs.readdirSync(agentDir)
59
- .filter(f => f.startsWith('guide-') && f.endsWith('.md'))
60
- .sort()
61
-
62
- // All guide-*.md files referenced via readFileSync in server.js
63
- // Pattern: new URL('../agent/guide-*.md', import.meta.url)
64
- const referencedFiles = [...serverSrc.matchAll(/new URL\(['"`][^'"`]*?(guide-[^'"`/]+\.md)['"`]/g)]
65
- .map(m => m[1])
66
-
67
- // All guide-*.md filenames registered as MCP resource URIs
68
- const registeredUris = [...serverSrc.matchAll(/uri:\s*['"`](pulse:\/\/guide\/[^'"`]+)['"`]/g)]
69
- .map(m => m[1])
70
-
71
- // ── Tests ─────────────────────────────────────────────────────────────────────
72
-
73
- console.log('\nGuide file coverage\n')
74
-
75
- test('guide-routing.md exists on disk', () => {
76
- assert(fs.existsSync(path.join(agentDir, 'guide-routing.md')), 'guide-routing.md not found')
77
- })
78
-
79
- test('guide-components.md exists on disk', () => {
80
- assert(fs.existsSync(path.join(agentDir, 'guide-components.md')), 'guide-components.md not found')
81
- })
82
-
83
- for (const file of guideFiles) {
84
- test(`${file} is referenced by readFileSync in server.js`, () => {
85
- assert(
86
- referencedFiles.includes(file),
87
- `${file} exists in src/agent/ but is not loaded by server.js via readFileSync. ` +
88
- `Either register it as a guide resource or delete it.`
89
- )
90
- })
91
- }
92
-
93
- for (const file of referencedFiles) {
94
- test(`${file} (referenced in server.js) exists on disk`, () => {
95
- assert(
96
- fs.existsSync(path.join(agentDir, file)),
97
- `server.js references ${file} via readFileSync but the file does not exist in src/agent/`
98
- )
99
- })
100
- }
101
-
102
- test('every guide-*.md on disk has a corresponding pulse://guide/* MCP resource', () => {
103
- // Map file names to expected URI slugs: guide-design-references.md → pulse://guide/design-references
104
- const fileToUri = (f) => 'pulse://guide/' + f.replace(/^guide-/, '').replace(/\.md$/, '')
105
- const missing = guideFiles.filter(f => !registeredUris.includes(fileToUri(f)))
106
- assert(
107
- missing.length === 0,
108
- `These guide files have no registered MCP resource URI:\n ${missing.join('\n ')}\n` +
109
- `Add them to GUIDE_RESOURCES in server.js or delete them.`
110
- )
111
- })
112
-
113
- test('no orphaned guide.md (monolithic legacy file should not exist)', () => {
114
- assert(
115
- !fs.existsSync(path.join(agentDir, 'guide.md')),
116
- 'guide.md exists in src/agent/ — this is the old monolithic guide and should be deleted. ' +
117
- 'Content should live in the split guide-*.md files.'
118
- )
119
- })
120
-
121
- // ── Duplicated doc sync ──────────────────────────────────────────────────────
122
- // Several agent docs exist in two places (a canonical source plus a copy that is
123
- // scaffolded into consumer projects or kept in this repo's own .claude/). These
124
- // pairs must stay byte-identical or the two audiences receive drifting rules.
125
-
126
- console.log('\nDuplicated doc sync\n')
127
-
128
- const SYNC_PAIRS = [
129
- ['src/agent/checklist.md', '.claude/pulse-checklist.md'],
130
- ['src/agent/checklist.md', 'docs/.claude/pulse-checklist.md'],
131
- ['src/agent/checklist.md', 'examples/.claude/pulse-checklist.md'],
132
- ['src/agent/commands/verify.md', '.claude/commands/verify.md'],
133
- ['src/agent/commands/verify.md', 'docs/.claude/commands/verify.md'],
134
- ['src/agent/commands/verify.md', 'examples/.claude/commands/verify.md'],
135
- ['src/agent/coverage-check.js', 'docs/.claude/coverage-check.js'],
136
- ['src/agent/coverage-check.js', 'examples/.claude/coverage-check.js'],
137
- ]
138
-
139
- for (const [a, b] of SYNC_PAIRS) {
140
- test(`${a} is in sync with ${b}`, () => {
141
- const fa = path.join(ROOT, a) // canonical source — package-relative
142
- const fb = path.join(REPO_ROOT, b) // distributed copy — repo-root-relative
143
- assert(fs.existsSync(fa), `${a} does not exist`)
144
- assert(fs.existsSync(fb), `${b} does not exist`)
145
- assert(
146
- fs.readFileSync(fa, 'utf8') === fs.readFileSync(fb, 'utf8'),
147
- `${a} and ${b} have drifted apart. Edit the canonical file and copy it over the other ` +
148
- `(cp ${a} ${b} or vice versa) so both audiences get the same rules.`
149
- )
150
- })
151
- }
152
-
153
- // ── Agent doc consistency guards ─────────────────────────────────────────────
154
- // Contradictions between agent docs leave the agent unable to know what it can
155
- // and cannot do. These guards pin the resolved decisions:
156
- // - The Lighthouse pass bar is THREE gated scores (Accessibility, Best
157
- // Practices, SEO) — Performance is reported, never gated. "100/100/100/100"
158
- // reintroduces a four-score bar the audit tooling cannot verify.
159
- // - "ask_user" is not a real tool in any host — docs must not name it.
160
- // - Emoji are banned in UI output — design guides must not suggest them.
161
-
162
- console.log('\nAgent doc consistency\n')
163
-
164
- // Each entry is resolved against ROOT (package root) or REPO_ROOT (repo
165
- // root) depending on where it actually lives — see the ROOT/REPO_ROOT
166
- // comment above. Package-relative entries carry their own root so callers
167
- // don't have to guess which base a given path needs.
168
- const agentDocs = [
169
- ...fs.readdirSync(agentDir).filter(f => f.endsWith('.md')).map(f => ({ root: ROOT, file: path.join('src/agent', f) })),
170
- ...fs.readdirSync(path.join(agentDir, 'commands')).filter(f => f.endsWith('.md')).map(f => ({ root: ROOT, file: path.join('src/agent/commands', f) })),
171
- ...fs.readdirSync(path.join(agentDir, 'skills')).flatMap(d => {
172
- const p = path.join(agentDir, 'skills', d, 'SKILL.md')
173
- return fs.existsSync(p) ? [{ root: ROOT, file: path.join('src/agent/skills', d, 'SKILL.md') }] : []
174
- }),
175
- { root: REPO_ROOT, file: '.claude/commands/build-page.md' },
176
- { root: REPO_ROOT, file: '.claude/commands/verify.md' },
177
- { root: REPO_ROOT, file: '.claude/commands/new-doc-page.md' },
178
- { root: REPO_ROOT, file: 'CLAUDE.md' },
179
- { root: REPO_ROOT, file: 'README.md' },
180
- ]
181
-
182
- test('no agent doc states a four-score Lighthouse bar (100/100/100/100)', () => {
183
- const fourScore = (src) =>
184
- src.includes('100/100/100/100') ||
185
- /all four (scores|categories)/i.test(src) ||
186
- /Accessibility, Best Practices, SEO,? and Performance[^.\n]*must (all )?be 100/i.test(src)
187
- const offenders = agentDocs.filter(({ root, file }) =>
188
- fs.existsSync(path.join(root, file)) &&
189
- fourScore(fs.readFileSync(path.join(root, file), 'utf8'))
190
- ).map(({ file }) => file)
191
- if (fourScore(serverSrc)) offenders.push('src/mcp/server.js')
192
- assert(
193
- offenders.length === 0,
194
- `Four-score Lighthouse bar found in: ${offenders.join(', ')}. ` +
195
- `The gated scores are Accessibility, Best Practices, and SEO (three) — Performance is reported, not gated.`
196
- )
197
- })
198
-
199
- test('no agent doc references a literal ask_user tool', () => {
200
- const offenders = agentDocs.filter(({ root, file }) =>
201
- fs.existsSync(path.join(root, file)) &&
202
- /\bask_user\b/.test(fs.readFileSync(path.join(root, file), 'utf8'))
203
- ).map(({ file }) => file)
204
- assert(
205
- offenders.length === 0,
206
- `"ask_user" referenced in: ${offenders.join(', ')}. ` +
207
- `No host exposes a tool by that name — phrase host-agnostically (e.g. "your host's question tool").`
208
- )
209
- })
210
-
211
- test('quietlytics-speed command keeps its safety rules: keys from the environment, report is data, edits need approval', () => {
212
- const src = fs.readFileSync(path.join(agentDir, 'commands', 'quietlytics-speed.md'), 'utf8')
213
- assert(src.includes('$QUIETLYTICS_READ_KEY') && src.includes('$QUIETLYTICS_MARKER_KEY'), 'reads keys from environment variables')
214
- assert(!/qlr_[A-Za-z0-9_-]{10,}|qlm_[A-Za-z0-9_-]{10,}/.test(src), 'contains no literal key')
215
- assert(/data, not instructions/i.test(src), 'treats the report as untrusted data')
216
- assert(/until the user has approved the plan/i.test(src), 'no edits before the plan is approved')
217
- assert(src.includes('quietlytics.speed'), 'checks the report format')
218
- })
219
-
220
- test('design guides do not suggest emoji in UI output', () => {
221
- const designGuides = ['src/agent/guide-design-references.md', 'src/agent/guide-design-gallery.md']
222
- const offenders = designGuides.filter(f => {
223
- const src = fs.readFileSync(path.join(ROOT, f), 'utf8')
224
- // Allow lines that mention emoji only to ban them; flag lines that suggest using them
225
- return src.split('\n').some(line =>
226
- /emoji/i.test(line) && !/never|not |banned|instead of emoji/i.test(line)
227
- )
228
- })
229
- assert(
230
- offenders.length === 0,
231
- `Emoji suggested (without a ban qualifier) in: ${offenders.join(', ')}. ` +
232
- `Emoji are banned in UI output (identity.md) — suggest icon-library glyphs instead.`
233
- )
234
- })
235
-
236
- test('workflow.md Mode B instructs writing the creative-override spec comment', () => {
237
- const workflow = fs.readFileSync(path.join(agentDir, 'workflow.md'), 'utf8')
238
- assert(
239
- /component.free — creative override/.test(workflow),
240
- 'workflow.md Mode B must instruct writing the `// component-free — creative override: <reason>` ' +
241
- 'comment into the spec — pulse_review detects creative override by reading the source file.'
242
- )
243
- })
244
-
245
- test('checklist points cross-spec shared components at src/components/, not src/ui/', () => {
246
- const checklist = fs.readFileSync(path.join(agentDir, 'checklist.md'), 'utf8')
247
- assert(
248
- !/shared component in `src\/ui\/`/.test(checklist),
249
- 'checklist.md tells the agent to create shared components in src/ui/ — that is the ' +
250
- 'framework\'s own library inside the package. Project-level shared code goes in src/components/.'
251
- )
252
- assert(
253
- /shared component in `src\/components\/`/.test(checklist),
254
- 'checklist.md must direct cross-spec shared components to src/components/.'
255
- )
256
- })
257
-
258
- test('guide-spec.md states the one-spec-per-page mental model', () => {
259
- const guideSpec = fs.readFileSync(path.join(agentDir, 'guide-spec.md'), 'utf8')
260
- assert(
261
- /one spec per page/i.test(guideSpec) && /dynamic route is still one spec/i.test(guideSpec),
262
- 'guide-spec.md must state the mental model explicitly: one spec = one route = one file, ' +
263
- 'and a dynamic route is still one spec serving many URLs.'
264
- )
265
- })
266
-
267
- test('approval pause mechanism is wired end to end', () => {
268
- // The design-approval gate requires the agent to end its turn and wait for the
269
- // user — but the scaffolded Stop hooks block any turn ending with unverified
270
- // edits. Without an escape hatch the hooks steamroll the approval gate (the
271
- // agent gets blocked, reads "Run /verify", and proceeds without an answer).
272
- const scaffoldSrc = fs.readFileSync(path.join(ROOT, 'src/cli/scaffold.js'), 'utf8')
273
- const coverageSrc = fs.readFileSync(path.join(agentDir, 'coverage-check.js'), 'utf8')
274
- const workflow = fs.readFileSync(path.join(agentDir, 'workflow.md'), 'utf8')
275
-
276
- // 1. The MCP server must expose the tool that writes the marker
277
- assert(/registerTool\(\s*'pulse_await_approval'/.test(serverSrc),
278
- 'server.js must register pulse_await_approval')
279
-
280
- // 2. Every inline Stop hook in the scaffold must early-exit on the marker
281
- const stopHookCount = (scaffoldSrc.match(/decision:'block'/g) || []).length
282
- const markerCheckCount = (scaffoldSrc.match(/\.pulse-awaiting-approval'\)\)process\.exit\(0\)/g) || []).length
283
- assert(stopHookCount >= 2 && markerCheckCount >= stopHookCount,
284
- `scaffold.js has ${stopHookCount} blocking Stop hooks but only ${markerCheckCount} check ` +
285
- `.pulse-awaiting-approval — every blocking Stop hook must allow the approval pause.`)
286
-
287
- // 3. coverage-check.js (the third Stop hook) must early-exit on the marker
288
- assert(coverageSrc.includes('.pulse-awaiting-approval'),
289
- 'coverage-check.js must early-exit when .pulse-awaiting-approval exists')
290
-
291
- // 4. The marker must be consumed when the user replies — one turn-end only
292
- assert(/UserPromptSubmit/.test(scaffoldSrc) && /unlinkSync\('\.pulse-awaiting-approval'\)/.test(scaffoldSrc),
293
- 'scaffold.js must register a UserPromptSubmit hook that deletes .pulse-awaiting-approval')
294
-
295
- // 5. The workflow must teach the mechanism at the design-approval gate
296
- assert(workflow.includes('pulse_await_approval'),
297
- 'workflow.md Phase 5a must explain how to pause: always pulse_await_approval before asking')
298
-
299
- // 6. No doc may claim a host question tool avoids ending the turn — field
300
- // testing showed AskUserQuestion in Claude Code still ends the turn and
301
- // fires the Stop hooks. The safe instruction is: always write the marker
302
- // first, regardless of how the question is asked.
303
- const claimDocs = agentDocs.filter(({ root, file }) =>
304
- fs.existsSync(path.join(root, file)) &&
305
- /without ending the turn/i.test(fs.readFileSync(path.join(root, file), 'utf8'))
306
- ).map(({ file }) => file)
307
- if (/without ending the turn.*Stop hooks never fire/is.test(serverSrc)) claimDocs.push('src/mcp/server.js')
308
- assert(claimDocs.length === 0,
309
- `These docs claim a question tool avoids ending the turn (empirically false in Claude Code): ${claimDocs.join(', ')}. ` +
310
- `Instruct: always call pulse_await_approval before asking, whichever way the question is asked.`)
311
- })
312
-
313
- test('pulse_validate prop-alias check excludes attrs blocks (false-positive fix)', () => {
314
- // Regression: input({ attrs: { autocomplete } }) — the RECOMMENDED pattern —
315
- // was flagged as a wrong top-level prop because the match crossed into the
316
- // attrs object. The exclusion regex must be present in the alias loop.
317
- // Logic lives in cli/validate.js — shared by pulse_validate (MCP) and
318
- // `pulse validate` (CLI), not duplicated in server.js.
319
- assert(validateSrc.includes('attrs\\s*:\\s*\\{[^}]*$'),
320
- 'The PROP_ALIASES loop must exclude matches that fall inside an open attrs: { … } block')
321
- })
322
-
323
- test('pulse_validate CSP check matches config by host, not full URL', () => {
324
- // Regression: CSP sources are valid without a scheme (images.unsplash.com),
325
- // but the config check required https?:// — flagging correctly configured
326
- // projects on every validate.
327
- assert(validateSrc.includes('configImgSrc.includes(host)'),
328
- 'External-image CSP check must test the config for the bare host')
329
- assert(validateSrc.includes(`host: 'images.unsplash.com'`),
330
- 'External image host entries must carry a bare host field')
331
- })
332
-
333
- test('validator resolves relative imports from the spec file\'s own directory', () => {
334
- // Regression: the temp validation file was always written into src/pages/
335
- // root, so subdirectory pages (src/pages/news/index.js) had their relative
336
- // imports (../../components/layout.js) resolve from the wrong depth.
337
- assert(/export async function validateContent\(content, root, tmpDir = null\)/.test(validateSrc),
338
- 'validateContent must accept a tmpDir parameter')
339
- assert(validateSrc.includes('validateFile(file, root)'),
340
- 'validateFile must exist and take (file, root)')
341
- assert(serverSrc.includes('validateContent(content, ROOT, path.dirname(fullPath))'),
342
- 'pulse_create_page must validate from the file\'s own directory')
343
- assert(serverSrc.includes('validateFile(file, ROOT)'),
344
- 'pulse_validate file mode must validate from the file\'s own directory (via validateFile, which uses path.dirname internally)')
345
- })
346
-
347
- test('pulse validate CLI command exists and shares validate.js with the MCP tool', () => {
348
- const cliSrc = fs.readFileSync(path.join(ROOT, 'src/cli/index.js'), 'utf8')
349
- assert(cliSrc.includes("case 'validate':"), 'pulse validate must be a registered CLI command')
350
- assert(cliSrc.includes('validateFile(file, root)'), 'the CLI command must call the shared validateFile() logic, not reimplement it')
351
- })
352
-
353
- test('pulse diagnose / resolve-error CLI commands exist and share diagnose.js with the MCP tools', () => {
354
- const cliSrc = fs.readFileSync(path.join(ROOT, 'src/cli/index.js'), 'utf8')
355
- assert(cliSrc.includes("case 'diagnose':"), 'pulse diagnose must be a registered CLI command')
356
- assert(cliSrc.includes("case 'resolve-error':"), 'pulse resolve-error must be a registered CLI command')
357
- assert(cliSrc.includes('readJournal(root'), 'the CLI diagnose command must call the shared readJournal() logic, not reimplement it')
358
- assert(cliSrc.includes('resolveEntries(root'), 'the CLI resolve-error command must call the shared resolveEntries() logic, not reimplement it')
359
- assert(serverSrc.includes('readJournal(ROOT'), 'the MCP pulse_diagnose tool must call the shared readJournal() logic')
360
- assert(serverSrc.includes('resolveEntries(ROOT'), 'the MCP pulse_resolve_error tool must call the shared resolveEntries() logic')
361
- })
362
-
363
- test('package-root import is stripped from client bundles in build and dev', () => {
364
- // Regression: import { pushStore } from '@invisibleloop/pulse' (the documented
365
- // live-push pattern) pulled the entire server — http, fs, zlib — into the
366
- // browser bundle and broke `pulse build`.
367
- const buildSrc = fs.readFileSync(path.join(ROOT, 'scripts/build.js'), 'utf8')
368
- const devSrc = fs.readFileSync(path.join(ROOT, 'src/cli/dev.js'), 'utf8')
369
- assert(buildSrc.includes(`'@invisibleloop/pulse/md', '@invisibleloop/pulse'`),
370
- 'build.js SERVER_ONLY_IMPORTS must include the package root')
371
- assert(devSrc.includes(`@invisibleloop\\/pulse)['"]`),
372
- 'dev.js serveFile must strip package-root imports before serving specs to the browser')
373
- })
374
-
375
- test('pulse_check_contrast extracts variables from [data-theme="light"] blocks', () => {
376
- // Regression: the light-theme block pattern was an ungrouped alternation
377
- // (a|b) + '\\s*\\{([^}]+)\\}' — the body matcher bound only to the second
378
- // alternative, so [data-theme="light"] blocks matched with no body capture
379
- // and the checker silently reported nothing for the light theme.
380
- // Recreate extractVars with the exact pattern strings from server.js source.
381
- const patterns = [...serverSrc.matchAll(/extractVars\(source,\s*'((?:[^'\\]|\\.)*)'\)/g)].map(m =>
382
- m[1].replace(/\\\\/g, '\\').replace(/\\'/g, "'")
383
- )
384
- assert(patterns.length === 2, `Expected 2 extractVars call patterns in server.js, found ${patterns.length}`)
385
- const lightPattern = patterns.find(p => p.includes('data-theme'))
386
- assert(lightPattern, 'No data-theme pattern found in extractVars calls')
387
-
388
- const sampleCss = `:root { --accent: #112233; }\n[data-theme="light"] { --accent: #445566; --bg: #ffffff; }`
389
- const blockRegex = new RegExp(lightPattern + '\\s*\\{([^}]+)\\}', 'gi')
390
- const m = blockRegex.exec(sampleCss)
391
- assert(m && m[1] && m[1].includes('--accent'),
392
- `The light-theme block pattern in pulse_check_contrast fails to capture the block body. ` +
393
- `Pattern: ${lightPattern} — alternations must be wrapped in (?:...).`)
394
- })
395
-
396
- test('dark theme default is declared at every spec-writing entry point', () => {
397
- // Agents repeatedly assumed the default theme is light, built the page, then
398
- // discovered dark at the screenshot — costing an edit → restart → re-approval
399
- // cycle. The dark default must be stated wherever a spec is first written:
400
- // the spec skeleton, the plan/build brief, and the checklist.
401
- const guideSpec = fs.readFileSync(path.join(agentDir, 'guide-spec.md'), 'utf8')
402
- const workflow = fs.readFileSync(path.join(agentDir, 'workflow.md'), 'utf8')
403
- const checklist = fs.readFileSync(path.join(agentDir, 'checklist.md'), 'utf8')
404
- const claudeMd = fs.readFileSync(path.join(REPO_ROOT, 'CLAUDE.md'), 'utf8')
405
-
406
- assert(/theme:/.test(guideSpec) && /DEFAULT IS DARK/i.test(guideSpec),
407
- 'guide-spec.md meta skeleton must include the theme field with the dark-default warning')
408
- assert(/Theme:\s+light \| dark/.test(workflow),
409
- 'workflow.md build brief template must include a Theme line')
410
- assert(/default theme is DARK/i.test(checklist),
411
- 'checklist.md Critical section must state the dark default')
412
- assert(/theme:\s+'light'/.test(claudeMd) && /default is DARK/i.test(claudeMd),
413
- 'CLAUDE.md spec meta block must show theme with the dark-default warning')
414
- })
415
-
416
- test('identity.md declares the creative override (Design Freedom) carve-out', () => {
417
- const identity = fs.readFileSync(path.join(agentDir, 'identity.md'), 'utf8')
418
- assert(
419
- /creative override/i.test(identity),
420
- 'identity.md must include the creative-override carve-out — without it the persona ' +
421
- 'unconditionally bans raw HTML that workflow.md Mode B explicitly permits.'
422
- )
423
- })
424
-
425
- test('pulse://start reads design:"freeform" from pulse.config.js and ties it to the Mode B override comment', () => {
426
- // pulse.config.js's `design: 'freeform'` flag is a project-wide default for
427
- // Mode B (see identity.md's "Design Freedom" rule / workflow.md 3a) — not a
428
- // separate, unenforced concept. pulse_review still detects Mode B per-file
429
- // by regexing the `// component-free — creative override` comment out of
430
- // each spec's source, so the freeform banner must tell the agent to keep
431
- // writing that comment into every new file even though the mode itself is
432
- // set once, project-wide.
433
- assert(/readPulseConfig/.test(serverSrc),
434
- 'server.js must define/use a readPulseConfig() helper to read pulse.config.js')
435
- assert(/config\.design === 'freeform'/.test(serverSrc),
436
- 'pulse://start must check config.design === "freeform" from pulse.config.js')
437
- assert(/component-free — creative override/.test(serverSrc) && /isFreeform/.test(serverSrc),
438
- 'the freeform banner must instruct writing the `// component-free — creative override` ' +
439
- 'comment into each spec file — pulse_review detects Mode B from that comment, not from config')
440
- })
441
-
442
- test('scaffolded pulse.config.js documents the design:"freeform" flag', () => {
443
- const scaffoldSrc = fs.readFileSync(path.join(ROOT, 'src/cli/scaffold.js'), 'utf8')
444
- assert(/design: 'freeform'/.test(scaffoldSrc),
445
- 'scaffold.js should mention design: "freeform" in the generated pulse.config.js as a commented hint')
446
- })
447
-
448
- // ── pulse_review mechanized checks ────────────────────────────────────────────
449
- // These three rules used to live only as prose in checklist.md (unenforced).
450
- // They're now regex-checked in both pulse_review's quick and full modes —
451
- // tested here against fixture strings, not just "the pattern exists somewhere",
452
- // so a future edit that narrows/breaks the regex is actually caught.
453
-
454
- test('modal-state check catches state.modalOpen and misses unrelated state', () => {
455
- const modalRegex = /state\.modalOpen|modalOpen\s*:/
456
- assert(modalRegex.test(`state: { modalOpen: false }`), 'must catch modalOpen: in state block')
457
- assert(modalRegex.test(`\${state.modalOpen ? modal() : ''}`), 'must catch state.modalOpen read in view')
458
- assert(!modalRegex.test(`state: { dialogId: null }`), 'must not flag unrelated state fields')
459
- })
460
-
461
- test('CSRF check catches a POST form missing ${server.csrf} on a submit-handling page', () => {
462
- const hasSubmit = (src) => /\bsubmit\s*:\s*async/.test(src)
463
- const hasPostForm = (src) => /<form[^>]*method=["']POST["']/i.test(src)
464
- const hasCsrf = (src) => src.includes('server.csrf')
465
-
466
- const missing = `submit: async (ctx) => {}, view: () => \`<form method="POST"><input name="email"></form>\``
467
- assert(hasSubmit(missing) && hasPostForm(missing) && !hasCsrf(missing),
468
- 'must flag a submit-page POST form with no ${server.csrf}')
469
-
470
- const present = `submit: async (ctx) => {}, view: () => \`<form method="POST">\${server.csrf}<input name="email"></form>\``
471
- assert(hasSubmit(present) && hasPostForm(present) && hasCsrf(present),
472
- 'must not flag a submit-page POST form that includes ${server.csrf}')
473
-
474
- const noSubmit = `view: () => \`<form method="POST"><input name="email"></form>\``
475
- assert(!hasSubmit(noSubmit),
476
- 'must not run the CSRF check at all on a page with no spec.submit (client-only action forms don\'t need it)')
477
- })
478
-
479
- test('_storeUpdate check catches a primitive and misses a real object literal', () => {
480
- const badStoreUpdate = /_storeUpdate\s*:\s*(true|false|\d|['"`])/
481
- assert(badStoreUpdate.test(`onSuccess: (state) => ({ _storeUpdate: true })`), 'must catch a boolean')
482
- assert(badStoreUpdate.test(`onSuccess: (state) => ({ _storeUpdate: 'theme' })`), 'must catch a string literal')
483
- assert(!badStoreUpdate.test(`onSuccess: (state, theme) => ({ _storeUpdate: { settings: { theme } } })`),
484
- 'must not flag a correctly-shaped object literal')
485
- })
486
-
487
- test('review.js defines all three new auto-checks in both quick and full pulse_review modes', () => {
488
- // Logic lives in cli/review.js — shared by pulse_review (MCP) and
489
- // `pulse review` (CLI), not duplicated in server.js.
490
- const modalCount = (reviewSrc.match(/state\\\.modalOpen\|modalOpen\\s\*:/g) || []).length
491
- assert(modalCount >= 2, `modal-state check must appear in both quick and full review modes (found ${modalCount})`)
492
- assert(reviewSrc.split("source.includes('server.csrf')").length - 1 >= 2,
493
- 'CSRF check must appear in both quick and full review modes')
494
- assert(reviewSrc.split('_storeUpdate\\s*:\\s*(true|false|\\d').length - 1 >= 2,
495
- '_storeUpdate check must appear in both quick and full review modes')
496
- })
497
-
498
- test('pulse review CLI command exists and shares review.js with the MCP tool', () => {
499
- const cliSrc = fs.readFileSync(path.join(ROOT, 'src/cli/index.js'), 'utf8')
500
- assert(cliSrc.includes("case 'review':"), 'pulse review must be a registered CLI command')
501
- assert(cliSrc.includes('runQuickReview(source, file)'), 'the CLI command must call the shared runQuickReview() logic')
502
- assert(cliSrc.includes('runFullReview(source, file)'), 'the CLI command must call the shared runFullReview() logic')
503
- assert(serverSrc.includes('runQuickReview(source, file)'), 'the MCP tool must call the shared runQuickReview() logic')
504
- assert(serverSrc.includes('runFullReview(source, file)'), 'the MCP tool must call the shared runFullReview() logic')
505
- })
506
-
507
- test('the CLI-facing review formatter never carries agent-only framing', () => {
508
- // The MCP tool's report talks directly to an agent ("you are now a senior
509
- // code reviewer", "continue to the verification workflow"). That framing
510
- // must not leak into the CLI output a human reads — formatFullReview and
511
- // formatQuickReview must branch on agentFacing rather than hardcode it.
512
- assert(reviewSrc.includes('agentFacing'), 'formatters must accept an agentFacing option')
513
- assert(reviewSrc.includes("you are now a"), 'the agent-facing intro text must exist somewhere in review.js')
514
- assert(/agentFacing\s*\?[\s\S]{0,200}you are now a/i.test(reviewSrc),
515
- 'the "you are now a reviewer" framing must be conditional on agentFacing, not unconditional')
516
- })
517
-
518
- // ── validate-worker.js path handling ────────────────────────────────────────
519
- // Regression: a relative path passed to the worker resolves against the
520
- // worker's own location (src/mcp/), not the caller's cwd, and fails with a
521
- // confusing "Cannot find package 'src'" error that reads as a bug in the
522
- // spec's imports rather than what it actually is — the worker was called
523
- // wrong. Found via a real dogfooding session where an agent shelled out to
524
- // the worker directly (bypassing the MCP tool's own absolute-path
525
- // normalisation) and lost several minutes to the misleading error.
526
-
527
- const validateWorkerPath = path.join(ROOT, 'src/mcp/validate-worker.js')
528
-
529
- function runValidateWorker(arg) {
530
- try {
531
- return { output: execFileSync(process.execPath, [validateWorkerPath, arg], { encoding: 'utf8' }), code: 0 }
532
- } catch (err) {
533
- return { output: err.stdout || err.message, code: err.status ?? 1 }
534
- }
535
- }
536
-
537
- test('validate-worker.js gives a precise error for a relative path, not the raw Node resolver error', () => {
538
- const { output } = runValidateWorker('src/pages/home.js')
539
- assert(output.includes('requires an absolute path'),
540
- `Expected a clear absolute-path error, got: ${output}`)
541
- // The explanatory text is allowed to mention the phrase for context, but the
542
- // raw Node stack-trace signature ("imported from ...") must not leak through —
543
- // that's the actual confusing artifact this fix replaces.
544
- assert(!output.includes('imported from'),
545
- `Should not leak the raw Node resolver stack trace to the caller: ${output}`)
546
- })
547
-
548
- test('validate-worker.js still gives the file-not-found message for a missing absolute path', () => {
549
- const { output } = runValidateWorker('/tmp/definitely-does-not-exist-pulse-test.js')
550
- assert(output.includes('file not found'), `Expected a file-not-found message, got: ${output}`)
551
- })
552
-
553
- // ── pulse_intent dashboard-aware inspiration question ───────────────────────
554
- // Regression: found via dogfooding — the mandatory design-inspiration question
555
- // ("a site you love, a screenshot, a mood board") has no branch for internal/
556
- // functional tools, where it lands oddly and risks an agent reinterpreting or
557
- // skipping a mandatory step. pulse_intent's dashboard archetype now asks a
558
- // reframed question (visual reference OR clean functional default, plus
559
- // information-density questions) instead of the generic aesthetic one.
560
-
561
- test('pulse_intent reframes the inspiration question for the dashboard archetype', () => {
562
- assert(/if \(key === 'dashboard'\)/.test(serverSrc),
563
- 'pulse_intent must branch on the dashboard archetype for the inspiration question')
564
- assert(/reframed for a functional\/internal tool/.test(serverSrc),
565
- 'the dashboard branch must explain why the question is reframed')
566
- assert(/is not skippable/.test(serverSrc),
567
- 'the reframed question must state it remains mandatory, not an excuse to skip a required step')
568
- })
569
-
570
- test('workflow.md documents the same internal-tool exception for Step 0', () => {
571
- const workflow = fs.readFileSync(path.join(agentDir, 'workflow.md'), 'utf8')
572
- assert(/Internal\/functional tools/.test(workflow),
573
- 'workflow.md must carry the same internal-tool branch as pulse_intent, since an agent following workflow.md directly (without calling pulse_intent) needs the same guidance')
574
- assert(/the question itself is still mandatory/.test(workflow),
575
- 'must be explicit that the internal-tool branch reframes the question, it does not remove the requirement to ask it')
576
- })
577
-
578
- // ── pulse_check_bundles ──────────────────────────────────────────────────────
579
- // Verified live (build a real project, reproduce the exact bug it catches by
580
- // temporarily reverting the build.js fix, confirm the tool flags it, restore
581
- // the fix, confirm clean) during development — not re-run here since that
582
- // needs a full esbuild pass. This locks in the tool's structure so future
583
- // edits can't silently drop a check without a source-level test noticing.
584
-
585
- test('pulse_check_bundles checks both bundle-existence and node-builtin-leak', () => {
586
- assert(/'pulse_check_bundles'/.test(serverSrc), 'pulse_check_bundles must be registered')
587
- assert(serverSrc.includes("checkBundles(ROOT)"), 'the MCP tool must call the shared checkBundles() logic, not reimplement it')
588
- assert(/mutations\|actions\|persist/.test(checkBundlesSrc),
589
- 'must check for mutations/actions/persist — same test as discover.js\'s needsHydration()')
590
- assert(/NODE_BUILTINS/.test(checkBundlesSrc), 'must scan bundles for leaked Node built-in references')
591
- assert(/node:fs/.test(checkBundlesSrc.slice(checkBundlesSrc.indexOf('NODE_BUILTINS'))),
592
- 'node:fs must be in the leak-detection list — the exact module a real persistence layer uses')
593
- })
594
-
595
- test('pulse check-bundles CLI command exists and shares check-bundles.js with the MCP tool', () => {
596
- const cliSrc = fs.readFileSync(path.join(ROOT, 'src/cli/index.js'), 'utf8')
597
- assert(cliSrc.includes("case 'check-bundles':"), 'pulse check-bundles must be a registered CLI command')
598
- assert(cliSrc.includes("checkBundles(root)"), 'the CLI command must call the shared checkBundles() logic, not reimplement it')
599
- })
600
-
601
- test('verify.md calls pulse_check_bundles as part of the full Lighthouse pre-flight', () => {
602
- const verify = fs.readFileSync(path.join(agentDir, 'commands', 'verify.md'), 'utf8')
603
- assert(/pulse_check_bundles/.test(verify), 'verify.md must call pulse_check_bundles')
604
- // Must appear in the Lighthouse pre-flight section (after pulse_build, before
605
- // the actual lighthouse_audit call) — not buried somewhere it won't run.
606
- const buildIdx = verify.indexOf('pulse_build` to produce a production build')
607
- const checkIdx = verify.indexOf('pulse_check_bundles')
608
- const auditIdx = verify.indexOf('lighthouse_audit` with `{ "device": "desktop" }')
609
- assert(buildIdx !== -1 && checkIdx !== -1 && auditIdx !== -1, 'all three anchors must exist in verify.md')
610
- assert(buildIdx < checkIdx && checkIdx < auditIdx,
611
- 'pulse_check_bundles must run after pulse_build and before the Lighthouse audit')
612
- })
613
-
614
- // ── Result ───────────────────────────────────────────────────────────────────
615
-
616
- console.log(`\n${passed} passed, ${failed} failed\n`)
617
- if (failed > 0) process.exit(1)