@invisibleloop/pulse 0.20.38 → 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.
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/scripts/css-purge.test.js +0 -147
- package/scripts/strip-server.test.js +0 -491
- package/src/cli/check-bundles.test.js +0 -137
- package/src/cli/cli.test.js +0 -341
- package/src/cli/diagnose.test.js +0 -124
- package/src/cli/process-utils.test.js +0 -114
- package/src/cli/review.test.js +0 -143
- package/src/cli/stamp.test.js +0 -149
- package/src/cli/validate.test.js +0 -153
- package/src/mcp/guards.test.js +0 -90
- package/src/mcp/mcp.test.js +0 -617
- package/src/md/md.test.js +0 -217
- package/src/runtime/image.test.js +0 -111
- package/src/runtime/morph.test.js +0 -317
- package/src/runtime/navigate.test.js +0 -304
- package/src/runtime/runtime.test.js +0 -1069
- package/src/runtime/ssr.test.js +0 -750
- package/src/server/server.test.js +0 -2474
- package/src/spec/schema.test.js +0 -467
- package/src/store/store.test.js +0 -210
- package/src/testing/testing.test.js +0 -508
- package/src/ui/ui.test.js +0 -1157
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse — check-bundles.js tests
|
|
3
|
-
* run: node src/cli/check-bundles.test.js
|
|
4
|
-
*
|
|
5
|
-
* Shared logic behind pulse_check_bundles (MCP tool) and `pulse check-bundles`
|
|
6
|
-
* (CLI command). These tests build against a real manifest.json + real boot
|
|
7
|
-
* file content (not a mocked build) since the whole point of this check is
|
|
8
|
-
* reading what a real build actually produced.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { test } from 'node:test'
|
|
12
|
-
import assert from 'node:assert/strict'
|
|
13
|
-
import fs from 'node:fs'
|
|
14
|
-
import os from 'node:os'
|
|
15
|
-
import path from 'node:path'
|
|
16
|
-
import { checkBundles, formatBundleCheck } from './check-bundles.js'
|
|
17
|
-
|
|
18
|
-
function tmpProject() {
|
|
19
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-check-bundles-test-'))
|
|
20
|
-
fs.mkdirSync(path.join(root, 'src', 'pages'), { recursive: true })
|
|
21
|
-
fs.mkdirSync(path.join(root, 'public', 'dist'), { recursive: true })
|
|
22
|
-
return root
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function writeManifest(root, entries) {
|
|
26
|
-
fs.writeFileSync(path.join(root, 'public', 'dist', 'manifest.json'), JSON.stringify(entries, null, 2))
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function writeBootFile(root, name, content = 'var x = 1;') {
|
|
30
|
-
fs.writeFileSync(path.join(root, 'public', 'dist', name), content)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
test('reports an error when no manifest.json exists', () => {
|
|
34
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-check-bundles-test-'))
|
|
35
|
-
const result = checkBundles(root)
|
|
36
|
-
assert.match(result.error, /No public\/dist\/manifest\.json found/)
|
|
37
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('clean pass: a page with mutations correctly has a bundle', () => {
|
|
41
|
-
const root = tmpProject()
|
|
42
|
-
fs.writeFileSync(path.join(root, 'src', 'pages', 'counter.js'),
|
|
43
|
-
`export default {\n route: '/counter',\n state: { count: 0 },\n mutations: { inc: (s) => ({ count: s.count + 1 }) },\n view: (state) => \`<button>\${state.count}</button>\`,\n}`)
|
|
44
|
-
writeManifest(root, { '/src/pages/counter.js': '/dist/counter.boot-ABC123.js' })
|
|
45
|
-
writeBootFile(root, 'counter.boot-ABC123.js')
|
|
46
|
-
|
|
47
|
-
const result = checkBundles(root)
|
|
48
|
-
assert.equal(result.error, null)
|
|
49
|
-
assert.equal(result.issues.length, 0, `Expected no issues, got: ${JSON.stringify(result.issues)}`)
|
|
50
|
-
assert.equal(result.confirms.length, 1)
|
|
51
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('flags a boot bundle for a page with no mutations/actions/persist', () => {
|
|
55
|
-
const root = tmpProject()
|
|
56
|
-
fs.writeFileSync(path.join(root, 'src', 'pages', 'redirect.js'),
|
|
57
|
-
`export default {\n route: '/:code',\n guard: async (ctx) => { if (!ctx.params.code) return { status: 404 } },\n view: () => '<main id="main-content">redirecting</main>',\n}`)
|
|
58
|
-
// Simulate the exact historical bug: a bundle exists for a page that needs none.
|
|
59
|
-
writeManifest(root, { '/src/pages/redirect.js': '/dist/redirect.boot-XYZ789.js' })
|
|
60
|
-
writeBootFile(root, 'redirect.boot-XYZ789.js')
|
|
61
|
-
|
|
62
|
-
const result = checkBundles(root)
|
|
63
|
-
assert.equal(result.issues.length, 1)
|
|
64
|
-
assert.match(result.issues[0], /\/:code/)
|
|
65
|
-
assert.match(result.issues[0], /no mutations\/actions\/persist/)
|
|
66
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test('issue message uses the spec\'s real declared route, not the filename-derived fallback', () => {
|
|
70
|
-
// Regression: discoverPages' derivedRoute is filename-based only (it doesn't
|
|
71
|
-
// parse spec.route) — a file named redirect.js with route: '/:code' was
|
|
72
|
-
// reported as route "/redirect" instead of the route actually registered.
|
|
73
|
-
const root = tmpProject()
|
|
74
|
-
fs.writeFileSync(path.join(root, 'src', 'pages', 'redirect.js'),
|
|
75
|
-
`export default {\n route: '/:code',\n guard: async (ctx) => { if (!ctx.params.code) return { status: 404 } },\n view: () => '<main id="main-content">redirecting</main>',\n}`)
|
|
76
|
-
writeManifest(root, { '/src/pages/redirect.js': '/dist/redirect.boot-XYZ789.js' })
|
|
77
|
-
writeBootFile(root, 'redirect.boot-XYZ789.js')
|
|
78
|
-
|
|
79
|
-
const result = checkBundles(root)
|
|
80
|
-
assert.equal(result.issues.length, 1)
|
|
81
|
-
assert.match(result.issues[0], /Route "\/:code"/,
|
|
82
|
-
`Expected the real declared route /:code, not the filename-derived /redirect — got: ${result.issues[0]}`)
|
|
83
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
test('does not flag a page with no mutations/actions/persist that correctly has no bundle', () => {
|
|
87
|
-
const root = tmpProject()
|
|
88
|
-
fs.writeFileSync(path.join(root, 'src', 'pages', 'redirect.js'),
|
|
89
|
-
`export default {\n route: '/:code',\n guard: async (ctx) => { if (!ctx.params.code) return { status: 404 } },\n view: () => '<main id="main-content">redirecting</main>',\n}`)
|
|
90
|
-
// No manifest entry for this page — correct behaviour, nothing to flag.
|
|
91
|
-
writeManifest(root, {})
|
|
92
|
-
|
|
93
|
-
const result = checkBundles(root)
|
|
94
|
-
assert.equal(result.issues.length, 0)
|
|
95
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
test('flags a boot bundle containing a literal Node built-in reference', () => {
|
|
99
|
-
const root = tmpProject()
|
|
100
|
-
writeManifest(root, {})
|
|
101
|
-
writeBootFile(root, 'leaky.boot-DEF456.js', `var x=require("node:fs");x.readFileSync("/tmp/y")`)
|
|
102
|
-
|
|
103
|
-
const result = checkBundles(root)
|
|
104
|
-
assert.equal(result.issues.length, 1)
|
|
105
|
-
assert.match(result.issues[0], /leaky\.boot-DEF456\.js/)
|
|
106
|
-
assert.match(result.issues[0], /node:fs/)
|
|
107
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test('a clean boot file with no Node built-ins is confirmed, not flagged', () => {
|
|
111
|
-
const root = tmpProject()
|
|
112
|
-
writeManifest(root, {})
|
|
113
|
-
writeBootFile(root, 'clean.boot-GHI999.js', `var mount=function(){};mount()`)
|
|
114
|
-
|
|
115
|
-
const result = checkBundles(root)
|
|
116
|
-
assert.equal(result.issues.length, 0)
|
|
117
|
-
assert.equal(result.confirms.some(c => c.includes('clean.boot-GHI999.js')), true)
|
|
118
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
test('formatBundleCheck reports all-clear when there are no issues', () => {
|
|
122
|
-
const root = tmpProject()
|
|
123
|
-
writeManifest(root, {})
|
|
124
|
-
const output = formatBundleCheck(checkBundles(root))
|
|
125
|
-
assert.match(output, /All clear/)
|
|
126
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
test('formatBundleCheck lists issues under their own heading', () => {
|
|
130
|
-
const root = tmpProject()
|
|
131
|
-
writeManifest(root, {})
|
|
132
|
-
writeBootFile(root, 'leaky.boot-JKL111.js', `require("node:crypto")`)
|
|
133
|
-
const output = formatBundleCheck(checkBundles(root))
|
|
134
|
-
assert.match(output, /### Issues found/)
|
|
135
|
-
assert.match(output, /node:crypto/)
|
|
136
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
137
|
-
})
|
package/src/cli/cli.test.js
DELETED
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse — CLI module smoke tests
|
|
3
|
-
*
|
|
4
|
-
* Imports every CLI module to catch syntax errors and bad exports.
|
|
5
|
-
* These modules aren't exercised by unit tests but are loaded at runtime
|
|
6
|
-
* by the dev server and `pulse new` — a parse error breaks both.
|
|
7
|
-
*
|
|
8
|
-
* run: node src/cli/cli.test.js
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { existsSync, readFileSync } from 'fs'
|
|
12
|
-
import path from 'path'
|
|
13
|
-
import { fileURLToPath } from 'url'
|
|
14
|
-
|
|
15
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
16
|
-
|
|
17
|
-
let passed = 0
|
|
18
|
-
let failed = 0
|
|
19
|
-
|
|
20
|
-
function test(label, fn) {
|
|
21
|
-
try {
|
|
22
|
-
fn()
|
|
23
|
-
console.log(` ✓ ${label}`)
|
|
24
|
-
passed++
|
|
25
|
-
} catch (e) {
|
|
26
|
-
console.log(` ✗ ${label}`)
|
|
27
|
-
console.log(` ${e.message}`)
|
|
28
|
-
failed++
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
console.log('\nCLI module imports\n')
|
|
35
|
-
|
|
36
|
-
const modules = await Promise.allSettled([
|
|
37
|
-
import('./scaffold.js'),
|
|
38
|
-
import('./discover.js'),
|
|
39
|
-
])
|
|
40
|
-
|
|
41
|
-
const names = ['scaffold.js', 'discover.js']
|
|
42
|
-
|
|
43
|
-
modules.forEach((result, i) => {
|
|
44
|
-
test(`${names[i]} parses and imports without error`, () => {
|
|
45
|
-
if (result.status === 'rejected') {
|
|
46
|
-
throw new Error(result.reason?.message || String(result.reason))
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('scaffold exports a scaffold function', () => {
|
|
52
|
-
const mod = modules[0].value
|
|
53
|
-
if (typeof mod?.scaffold !== 'function') {
|
|
54
|
-
throw new Error(`Expected scaffold export to be a function, got ${typeof mod?.scaffold}`)
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test('discover exports a loadPages function', () => {
|
|
59
|
-
const mod = modules[1].value
|
|
60
|
-
if (typeof mod?.loadPages !== 'function') {
|
|
61
|
-
throw new Error(`Expected loadPages export to be a function, got ${typeof mod?.loadPages}`)
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
test('discover: discoverPages skips .test.js files', () => {
|
|
66
|
-
const { discoverPages } = modules[1].value
|
|
67
|
-
// discoverPages on this very directory must not include cli.test.js
|
|
68
|
-
const pages = discoverPages(new URL('../../', import.meta.url).pathname)
|
|
69
|
-
const testFiles = pages.filter(p => p.filePath.endsWith('.test.js'))
|
|
70
|
-
if (testFiles.length > 0) {
|
|
71
|
-
throw new Error(`discoverPages included test files: ${testFiles.map(p => p.filePath).join(', ')}`)
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test('discover: nested pages get unique names (collision prevention)', () => {
|
|
76
|
-
const { deriveRoute } = modules[1].value
|
|
77
|
-
// Two files with the same basename in different subdirs must derive different routes
|
|
78
|
-
const r1 = deriveRoute('products.js')
|
|
79
|
-
const r2 = deriveRoute('api/products.js')
|
|
80
|
-
if (r1 === r2) {
|
|
81
|
-
throw new Error(`Route collision: both products.js and api/products.js derived '${r1}'`)
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
test('discover: a store-only spec (no local mutations/actions/persist) still gets a hydrate URL — regression: mount() is what wires up data-store-event dispatch AND the live SSE store-push subscription; without this a store-only page silently ships zero JS and never receives pushStore() broadcasts', async () => {
|
|
86
|
-
const fs = await import('fs')
|
|
87
|
-
const os = await import('os')
|
|
88
|
-
const path = await import('path')
|
|
89
|
-
const { loadPages } = modules[1].value
|
|
90
|
-
|
|
91
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-discover-store-test-'))
|
|
92
|
-
fs.mkdirSync(path.join(root, 'src', 'pages'), { recursive: true })
|
|
93
|
-
fs.writeFileSync(
|
|
94
|
-
path.join(root, 'src', 'pages', 'storey.js'),
|
|
95
|
-
`export default {\n route: '/storey',\n store: ['count'],\n state: {},\n view: (state, server) => \`\${server.count}\`,\n}\n`
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
const specs = await loadPages(root)
|
|
99
|
-
const storey = specs.find(s => s.route === '/storey')
|
|
100
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
101
|
-
|
|
102
|
-
if (!storey) throw new Error('loadPages did not find the /storey spec')
|
|
103
|
-
if (!storey.hydrate) throw new Error('store-only spec should have gotten a hydrate URL, got: ' + storey.hydrate)
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
// Skill source files
|
|
108
|
-
|
|
109
|
-
console.log('\nCopilot skill source files\n')
|
|
110
|
-
|
|
111
|
-
const skillsDir = new URL('../agent/skills', import.meta.url).pathname
|
|
112
|
-
const requiredSkills = ['build-page', 'verify', 'new-doc-page']
|
|
113
|
-
|
|
114
|
-
for (const skill of requiredSkills) {
|
|
115
|
-
const skillMd = path.join(skillsDir, skill, 'SKILL.md')
|
|
116
|
-
|
|
117
|
-
test(`${skill}/SKILL.md exists`, () => {
|
|
118
|
-
if (!existsSync(skillMd)) throw new Error(`Missing: ${skillMd}`)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
test(`${skill}/SKILL.md has valid frontmatter`, () => {
|
|
122
|
-
const content = readFileSync(skillMd, 'utf8')
|
|
123
|
-
if (!content.startsWith('---\n')) throw new Error('Missing opening YAML frontmatter ---')
|
|
124
|
-
const end = content.indexOf('\n---\n', 4)
|
|
125
|
-
if (end === -1) throw new Error('Missing closing YAML frontmatter ---')
|
|
126
|
-
const frontmatter = content.slice(4, end)
|
|
127
|
-
if (!frontmatter.includes('name:')) throw new Error('Frontmatter missing name:')
|
|
128
|
-
if (!frontmatter.includes('description:')) throw new Error('Frontmatter missing description:')
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ---------------------------------------------------------------------------
|
|
133
|
-
// Store auto-discovery (loadStore)
|
|
134
|
-
// ---------------------------------------------------------------------------
|
|
135
|
-
// Regression: pulse.store.js was documented but never loaded by the CLI —
|
|
136
|
-
// dev/start servers got no store, so spec.store keys were always empty.
|
|
137
|
-
|
|
138
|
-
console.log('\nStore auto-discovery\n')
|
|
139
|
-
|
|
140
|
-
{
|
|
141
|
-
const { mkdtempSync, writeFileSync, rmSync } = await import('node:fs')
|
|
142
|
-
const { tmpdir } = await import('node:os')
|
|
143
|
-
const { loadStore } = await import('./discover.js')
|
|
144
|
-
|
|
145
|
-
const dir = mkdtempSync(path.join(tmpdir(), 'pulse-store-'))
|
|
146
|
-
try {
|
|
147
|
-
// No store file → null
|
|
148
|
-
const none = await loadStore(dir)
|
|
149
|
-
test('loadStore returns null when pulse.store.js is absent', () => {
|
|
150
|
-
if (none !== null) throw new Error(`Expected null, got ${JSON.stringify(none)}`)
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
// Valid store file → definition with browser hydrate path set
|
|
154
|
-
writeFileSync(path.join(dir, 'pulse.store.js'),
|
|
155
|
-
`export default { state: { basket: [] }, server: { nav: async () => ['Home'] }, mutations: { add: (s, i) => ({ basket: [...s.basket, i] }) } }`)
|
|
156
|
-
const def = await loadStore(dir)
|
|
157
|
-
test('loadStore loads pulse.store.js and sets the browser hydrate path', () => {
|
|
158
|
-
if (!def) throw new Error('Expected a store definition')
|
|
159
|
-
if (def.hydrate !== '/pulse.store.js') throw new Error(`Expected hydrate /pulse.store.js, got ${def.hydrate}`)
|
|
160
|
-
if (typeof def.server?.nav !== 'function') throw new Error('server fetchers missing')
|
|
161
|
-
if (typeof def.mutations?.add !== 'function') throw new Error('mutations missing')
|
|
162
|
-
if (!Array.isArray(def.state?.basket)) throw new Error('state missing')
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
// Cache-busted re-import returns the edited module (dev hot reload)
|
|
166
|
-
writeFileSync(path.join(dir, 'pulse.store.js'),
|
|
167
|
-
`export default { state: { basket: [], promo: true }, mutations: {} }`)
|
|
168
|
-
const fresh = await loadStore(dir, Date.now())
|
|
169
|
-
test('loadStore cache-busts for hot reload', () => {
|
|
170
|
-
if (fresh?.state?.promo !== true) throw new Error('Expected fresh store definition after edit')
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
// Bad export → clear error
|
|
174
|
-
writeFileSync(path.join(dir, 'pulse.store.js'), `export default 42`)
|
|
175
|
-
let threw = false
|
|
176
|
-
try { await loadStore(dir, Date.now() + 1) } catch (e) { threw = /store object/.test(e.message) }
|
|
177
|
-
test('loadStore throws a clear error for a non-object export', () => {
|
|
178
|
-
if (!threw) throw new Error('Expected a descriptive throw for bad default export')
|
|
179
|
-
})
|
|
180
|
-
} finally {
|
|
181
|
-
rmSync(dir, { recursive: true, force: true })
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ---------------------------------------------------------------------------
|
|
186
|
-
// Coverage-check exemptions
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
// Regression: the stop hook blocked every session on uncovered guard/submit/
|
|
189
|
-
// sitemap/meta lines (server-only, untestable), and c8 ignore comments were
|
|
190
|
-
// not honoured because Node's built-in coverage ignores them.
|
|
191
|
-
|
|
192
|
-
console.log('\nCoverage-check exemptions\n')
|
|
193
|
-
|
|
194
|
-
{
|
|
195
|
-
const { computeExemptLines } = await import('../agent/coverage-check.js')
|
|
196
|
-
|
|
197
|
-
const src = [
|
|
198
|
-
/* 1 */ `export default {`,
|
|
199
|
-
/* 2 */ ` route: '/x',`,
|
|
200
|
-
/* 3 */ ` guard: async (ctx) => {`,
|
|
201
|
-
/* 4 */ ` if (!ctx.cookies.session) return { redirect: '/login' }`,
|
|
202
|
-
/* 5 */ ` },`,
|
|
203
|
-
/* 6 */ ` submit: async (ctx) => {`,
|
|
204
|
-
/* 7 */ ` return { redirect: '/done' }`,
|
|
205
|
-
/* 8 */ ` },`,
|
|
206
|
-
/* 9 */ ` sitemap: async () => ['/a'],`,
|
|
207
|
-
/* 10 */ ` meta: {`,
|
|
208
|
-
/* 11 */ ` title: async (ctx) => fetchTitle(ctx),`,
|
|
209
|
-
/* 12 */ ` },`,
|
|
210
|
-
/* 13 */ ` view: (state) => state.n,`,
|
|
211
|
-
/* 14 */ ` helpers: () => {`,
|
|
212
|
-
/* 15 */ ` /* c8 ignore next 2 */`,
|
|
213
|
-
/* 16 */ ` impossibleBranch()`,
|
|
214
|
-
/* 17 */ ` alsoImpossible()`,
|
|
215
|
-
/* 18 */ ` normalCode()`,
|
|
216
|
-
/* 19 */ ` },`,
|
|
217
|
-
/* 20 */ `}`,
|
|
218
|
-
].join('\n')
|
|
219
|
-
|
|
220
|
-
const exempt = computeExemptLines(src)
|
|
221
|
-
|
|
222
|
-
test('guard, submit, sitemap, and meta blocks are exempt', () => {
|
|
223
|
-
for (const n of [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) {
|
|
224
|
-
if (!exempt.has(n)) throw new Error(`Expected line ${n} to be exempt`)
|
|
225
|
-
}
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
test('view and ordinary helper code are NOT exempt', () => {
|
|
229
|
-
for (const n of [13, 18]) {
|
|
230
|
-
if (exempt.has(n)) throw new Error(`Line ${n} must not be exempt`)
|
|
231
|
-
}
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
test('c8 ignore next N exempts exactly the following N lines', () => {
|
|
235
|
-
for (const n of [15, 16, 17]) {
|
|
236
|
-
if (!exempt.has(n)) throw new Error(`Expected line ${n} to be exempt via c8 ignore`)
|
|
237
|
-
}
|
|
238
|
-
if (exempt.has(18)) throw new Error('Line 18 is beyond the ignore count and must not be exempt')
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
test('c8 ignore start/stop exempts the whole range', () => {
|
|
242
|
-
const ranged = computeExemptLines([
|
|
243
|
-
`const a = 1`, // 1
|
|
244
|
-
`/* c8 ignore start */`, // 2
|
|
245
|
-
`untestable1()`, // 3
|
|
246
|
-
`untestable2()`, // 4
|
|
247
|
-
`/* c8 ignore stop */`, // 5
|
|
248
|
-
`const b = 2`, // 6
|
|
249
|
-
].join('\n'))
|
|
250
|
-
for (const n of [2, 3, 4, 5]) {
|
|
251
|
-
if (!ranged.has(n)) throw new Error(`Expected line ${n} exempt in range`)
|
|
252
|
-
}
|
|
253
|
-
if (ranged.has(6)) throw new Error('Line after stop must not be exempt')
|
|
254
|
-
})
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
// pulse start — PORT env var override warning
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
// Regression: found via a real dogfooding session. A `PORT` env var set on the
|
|
261
|
-
// local machine for unrelated reasons (common in sandboxes/CI) silently
|
|
262
|
-
// overrode --port with no explanation, even though this exact --port usage is
|
|
263
|
-
// the documented local /verify convention (pulse-start.md). The behavior
|
|
264
|
-
// (PORT always wins — correct for real PaaS deployment) is unchanged; this
|
|
265
|
-
// only adds a visible warning when the override actually happens.
|
|
266
|
-
|
|
267
|
-
{
|
|
268
|
-
console.log('\npulse start — PORT env var override\n')
|
|
269
|
-
|
|
270
|
-
const { spawnSync } = await import('child_process')
|
|
271
|
-
const os = (await import('os')).default
|
|
272
|
-
const fsMod = await import('fs')
|
|
273
|
-
|
|
274
|
-
function scaffoldMinimalBuiltProject() {
|
|
275
|
-
const dir = fsMod.mkdtempSync(path.join(os.tmpdir(), 'pulse-start-test-'))
|
|
276
|
-
fsMod.mkdirSync(path.join(dir, 'src', 'pages'), { recursive: true })
|
|
277
|
-
fsMod.mkdirSync(path.join(dir, 'public', 'dist'), { recursive: true })
|
|
278
|
-
fsMod.writeFileSync(path.join(dir, 'src', 'pages', 'home.js'),
|
|
279
|
-
`export default { route: '/', state: {}, view: () => '<main id="main-content">hi</main>' }`)
|
|
280
|
-
// start.js only needs public/dist/ + manifest.json to exist to pass its
|
|
281
|
-
// pre-flight checks — a real build isn't necessary to test port selection.
|
|
282
|
-
fsMod.writeFileSync(path.join(dir, 'public', 'dist', 'manifest.json'), '{}')
|
|
283
|
-
return dir
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const startScriptPath = path.join(__dirname, 'start.js')
|
|
287
|
-
|
|
288
|
-
function runStart(dir, portFlag, env) {
|
|
289
|
-
const args = ['--root', dir]
|
|
290
|
-
if (portFlag) args.push('--port', String(portFlag))
|
|
291
|
-
return spawnSync(process.execPath, [startScriptPath, ...args], {
|
|
292
|
-
encoding: 'utf8',
|
|
293
|
-
timeout: 5000,
|
|
294
|
-
env: { ...process.env, ...env },
|
|
295
|
-
})
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
test('warns to stderr when PORT env var overrides an explicit --port flag', () => {
|
|
299
|
-
const dir = scaffoldMinimalBuiltProject()
|
|
300
|
-
try {
|
|
301
|
-
const result = runStart(dir, 3001, { PORT: '9999' })
|
|
302
|
-
if (!result.stderr.includes('PORT=9999') || !result.stderr.includes('overrides --port 3001'))
|
|
303
|
-
throw new Error(`Expected an override warning naming both ports, got stderr: ${result.stderr}`)
|
|
304
|
-
} finally {
|
|
305
|
-
cleanupTmpDir(dir)
|
|
306
|
-
}
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
test('no warning when PORT env var matches the requested --port', () => {
|
|
310
|
-
const dir = scaffoldMinimalBuiltProject()
|
|
311
|
-
try {
|
|
312
|
-
const result = runStart(dir, 3001, { PORT: '3001' })
|
|
313
|
-
if (result.stderr.includes('overrides --port'))
|
|
314
|
-
throw new Error(`Should not warn when PORT matches --port, got stderr: ${result.stderr}`)
|
|
315
|
-
} finally {
|
|
316
|
-
cleanupTmpDir(dir)
|
|
317
|
-
}
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
test('no warning when PORT is unset and only --port is used', () => {
|
|
321
|
-
const dir = scaffoldMinimalBuiltProject()
|
|
322
|
-
const env = { ...process.env }
|
|
323
|
-
delete env.PORT
|
|
324
|
-
try {
|
|
325
|
-
const result = spawnSync(process.execPath, [startScriptPath, '--root', dir, '--port', '3001'], {
|
|
326
|
-
encoding: 'utf8', timeout: 5000, env,
|
|
327
|
-
})
|
|
328
|
-
if (result.stderr.includes('overrides --port'))
|
|
329
|
-
throw new Error(`Should not warn when PORT is unset, got stderr: ${result.stderr}`)
|
|
330
|
-
} finally {
|
|
331
|
-
cleanupTmpDir(dir)
|
|
332
|
-
}
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
function cleanupTmpDir(dir) {
|
|
336
|
-
fsMod.rmSync(dir, { recursive: true, force: true })
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed\n`)
|
|
341
|
-
if (failed > 0) process.exit(1)
|
package/src/cli/diagnose.test.js
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse — diagnose.js tests
|
|
3
|
-
* run: node src/cli/diagnose.test.js
|
|
4
|
-
*
|
|
5
|
-
* Shared logic behind pulse_diagnose/pulse_resolve_error (MCP tools) and
|
|
6
|
-
* `pulse diagnose`/`pulse resolve-error` (CLI commands) — a human running
|
|
7
|
-
* the CLI must see the exact same journal an agent sees.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { test } from 'node:test'
|
|
11
|
-
import assert from 'node:assert/strict'
|
|
12
|
-
import fs from 'node:fs'
|
|
13
|
-
import os from 'node:os'
|
|
14
|
-
import path from 'node:path'
|
|
15
|
-
import { readJournal, formatJournal, resolveEntries, formatResolveResult } from './diagnose.js'
|
|
16
|
-
|
|
17
|
-
function tmpProject() {
|
|
18
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-diagnose-test-'))
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function writeJournal(root, entries) {
|
|
22
|
-
fs.mkdirSync(path.join(root, '.pulse'), { recursive: true })
|
|
23
|
-
fs.writeFileSync(path.join(root, '.pulse', 'errors.json'), JSON.stringify(entries, null, 2))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function sampleEntry(overrides = {}) {
|
|
27
|
-
return {
|
|
28
|
-
id: 'abc-123', ts: '2026-01-01T00:00:00.000Z', route: '/dashboard',
|
|
29
|
-
phase: 'view', message: 'boom', stack: 'Error: boom', resolved: false,
|
|
30
|
-
...overrides,
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
test('readJournal reports exists:false when no journal file exists', () => {
|
|
35
|
-
const root = tmpProject()
|
|
36
|
-
const result = readJournal(root)
|
|
37
|
-
assert.equal(result.exists, false)
|
|
38
|
-
assert.deepEqual(result.entries, [])
|
|
39
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('readJournal returns unresolved entries by default', () => {
|
|
43
|
-
const root = tmpProject()
|
|
44
|
-
writeJournal(root, [sampleEntry({ id: 'a', resolved: false }), sampleEntry({ id: 'b', resolved: true })])
|
|
45
|
-
const result = readJournal(root)
|
|
46
|
-
assert.equal(result.entries.length, 1)
|
|
47
|
-
assert.equal(result.entries[0].id, 'a')
|
|
48
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('readJournal with includeResolved:true returns both', () => {
|
|
52
|
-
const root = tmpProject()
|
|
53
|
-
writeJournal(root, [sampleEntry({ id: 'a', resolved: false }), sampleEntry({ id: 'b', resolved: true })])
|
|
54
|
-
const result = readJournal(root, { includeResolved: true })
|
|
55
|
-
assert.equal(result.entries.length, 2)
|
|
56
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('readJournal filters by route', () => {
|
|
60
|
-
const root = tmpProject()
|
|
61
|
-
writeJournal(root, [
|
|
62
|
-
sampleEntry({ id: 'a', route: '/dashboard' }),
|
|
63
|
-
sampleEntry({ id: 'b', route: '/other' }),
|
|
64
|
-
])
|
|
65
|
-
const result = readJournal(root, { route: '/dashboard' })
|
|
66
|
-
assert.equal(result.entries.length, 1)
|
|
67
|
-
assert.equal(result.entries[0].id, 'a')
|
|
68
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('formatJournal reports a clean message when nothing is recorded', () => {
|
|
72
|
-
const root = tmpProject()
|
|
73
|
-
const result = readJournal(root)
|
|
74
|
-
assert.match(formatJournal(result), /does not exist/)
|
|
75
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
test('formatJournal includes the route, phase, message, and stack for each entry', () => {
|
|
79
|
-
const root = tmpProject()
|
|
80
|
-
writeJournal(root, [sampleEntry()])
|
|
81
|
-
const result = readJournal(root)
|
|
82
|
-
const output = formatJournal(result)
|
|
83
|
-
assert.match(output, /\/dashboard/)
|
|
84
|
-
assert.match(output, /view/)
|
|
85
|
-
assert.match(output, /boom/)
|
|
86
|
-
assert.match(output, /Error: boom/)
|
|
87
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
test('resolveEntries by id marks only the matching entry', () => {
|
|
91
|
-
const root = tmpProject()
|
|
92
|
-
writeJournal(root, [sampleEntry({ id: 'a' }), sampleEntry({ id: 'b' })])
|
|
93
|
-
const result = resolveEntries(root, { id: 'a' })
|
|
94
|
-
assert.equal(result.count, 1)
|
|
95
|
-
const after = JSON.parse(fs.readFileSync(path.join(root, '.pulse', 'errors.json'), 'utf8'))
|
|
96
|
-
assert.equal(after.find(e => e.id === 'a').resolved, true)
|
|
97
|
-
assert.equal(after.find(e => e.id === 'b').resolved, false)
|
|
98
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
test('resolveEntries by route marks every unresolved entry on that route', () => {
|
|
102
|
-
const root = tmpProject()
|
|
103
|
-
writeJournal(root, [
|
|
104
|
-
sampleEntry({ id: 'a', route: '/x' }),
|
|
105
|
-
sampleEntry({ id: 'b', route: '/x' }),
|
|
106
|
-
sampleEntry({ id: 'c', route: '/y' }),
|
|
107
|
-
])
|
|
108
|
-
const result = resolveEntries(root, { route: '/x' })
|
|
109
|
-
assert.equal(result.count, 2)
|
|
110
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
test('resolveEntries with neither id nor route returns an error', () => {
|
|
114
|
-
const root = tmpProject()
|
|
115
|
-
const result = resolveEntries(root, {})
|
|
116
|
-
assert.match(result.error, /pass either id or route/)
|
|
117
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
test('formatResolveResult reports the count or "no matching entries"', () => {
|
|
121
|
-
assert.match(formatResolveResult({ count: 2, error: null }), /Resolved 2 entries/)
|
|
122
|
-
assert.match(formatResolveResult({ count: 1, error: null }), /Resolved 1 entry\./)
|
|
123
|
-
assert.match(formatResolveResult({ count: 0, error: null }), /No matching unresolved entries/)
|
|
124
|
-
})
|