@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,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)
@@ -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
- })
@@ -1,114 +0,0 @@
1
- /**
2
- * Pulse — process-utils.js tests
3
- * run: node src/cli/process-utils.test.js
4
- *
5
- * killPulseServerOnPort must never kill a process it didn't spawn as a
6
- * Pulse dev/prod server — see its doc comment for the real user report
7
- * (an unrelated Next.js dev server on port 3000 was at risk of being
8
- * killed by a blind lsof|kill) this fixes.
9
- */
10
-
11
- import { test } from 'node:test'
12
- import assert from 'node:assert/strict'
13
- import { spawn } from 'node:child_process'
14
- import net from 'node:net'
15
- import { killPulseServerOnPort } from './process-utils.js'
16
-
17
- /** Find a free TCP port by binding to port 0 and reading it back. */
18
- async function freePort() {
19
- return new Promise((resolve, reject) => {
20
- const srv = net.createServer()
21
- srv.listen(0, () => {
22
- const { port } = srv.address()
23
- srv.close(() => resolve(port))
24
- })
25
- srv.on('error', reject)
26
- })
27
- }
28
-
29
- /** Spawn a plain (non-Pulse) process holding a port open, for negative tests. */
30
- function spawnPlainListener(port) {
31
- const proc = spawn(process.execPath, ['-e', `
32
- const net = require('net')
33
- net.createServer().listen(${port})
34
- setInterval(() => {}, 1000) // keep the process alive
35
- `])
36
- return proc
37
- }
38
-
39
- async function waitUntilListening(port, maxMs = 3000) {
40
- const deadline = Date.now() + maxMs
41
- while (Date.now() < deadline) {
42
- const alive = await new Promise(resolve => {
43
- const sock = net.createConnection(port, '127.0.0.1')
44
- sock.on('connect', () => { sock.destroy(); resolve(true) })
45
- sock.on('error', () => resolve(false))
46
- })
47
- if (alive) return true
48
- await new Promise(r => setTimeout(r, 50))
49
- }
50
- return false
51
- }
52
-
53
- async function waitUntilFree(port, maxMs = 3000) {
54
- const deadline = Date.now() + maxMs
55
- while (Date.now() < deadline) {
56
- const alive = await new Promise(resolve => {
57
- const sock = net.createConnection(port, '127.0.0.1')
58
- sock.on('connect', () => { sock.destroy(); resolve(true) })
59
- sock.on('error', () => resolve(false))
60
- })
61
- if (!alive) return true
62
- await new Promise(r => setTimeout(r, 50))
63
- }
64
- return false
65
- }
66
-
67
- test('killPulseServerOnPort does not kill a process whose command line has no Pulse marker', async () => {
68
- const port = await freePort()
69
- const proc = spawnPlainListener(port)
70
- try {
71
- assert.equal(await waitUntilListening(port), true, 'test listener never came up')
72
-
73
- const result = killPulseServerOnPort(port)
74
-
75
- assert.equal(result.killed.length, 0, 'must not report the unrelated process as killed')
76
- assert.equal(result.skipped.length, 1, 'must report exactly one skipped process')
77
- assert.equal(result.skipped[0].pid, String(proc.pid))
78
- // The process must actually still be alive — not just unreported as killed.
79
- assert.equal(await waitUntilFree(port, 300), false, 'the unrelated process must still be listening')
80
- } finally {
81
- proc.kill('SIGKILL')
82
- }
83
- })
84
-
85
- test('killPulseServerOnPort kills a process whose command line matches a Pulse marker', async () => {
86
- const port = await freePort()
87
- // Simulate a Pulse dev server by giving the child process's argv a marker
88
- // substring (cli/dev.js) without actually running the real dev server —
89
- // exercises the same ps-based matching path a real `node .../cli/dev.js`
90
- // invocation would hit.
91
- const proc = spawn(process.execPath, ['-e', `
92
- const net = require('net')
93
- net.createServer().listen(${port})
94
- setInterval(() => {}, 1000)
95
- `, '--', 'cli/dev.js'])
96
- try {
97
- assert.equal(await waitUntilListening(port), true, 'test listener never came up')
98
-
99
- const result = killPulseServerOnPort(port)
100
-
101
- assert.equal(result.skipped.length, 0, 'must not skip a process matching a Pulse marker')
102
- assert.equal(result.killed.length, 1)
103
- assert.equal(result.killed[0], String(proc.pid))
104
- assert.equal(await waitUntilFree(port), true, 'the matched process must actually be killed')
105
- } finally {
106
- try { proc.kill('SIGKILL') } catch { /* already dead */ }
107
- }
108
- })
109
-
110
- test('killPulseServerOnPort on a free port returns empty killed and skipped', async () => {
111
- const port = await freePort()
112
- const result = killPulseServerOnPort(port)
113
- assert.deepEqual(result, { killed: [], skipped: [] })
114
- })