@abarcenas/ai-workflow-template 1.44.0 → 1.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.agents/config/README.md +43 -0
  2. package/.agents/config/domain.md +37 -0
  3. package/.agents/instructions/agent.instructions.md +10 -10
  4. package/.agents/instructions/learned-knowledge.instructions.md +170 -0
  5. package/.agents/instructions/memory-bank.instructions.md +11 -0
  6. package/.agents/skills/caveman-compress/README.md +2 -0
  7. package/.agents/skills/caveman-compress/SECURITY.md +3 -1
  8. package/.agents/skills/caveman-compress/SKILL.md +4 -0
  9. package/.agents/skills/caveman-compress/scripts/compress.py +9 -2
  10. package/.agents/skills/compress/SKILL.md +4 -0
  11. package/.agents/skills/compress/scripts/compress.py +9 -2
  12. package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
  13. package/.agents/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
  14. package/.agents/skills/domain-modeling/SKILL.md +74 -0
  15. package/.agents/skills/grill-me/SKILL.md +7 -0
  16. package/.agents/skills/grill-with-docs/SKILL.md +7 -0
  17. package/.agents/skills/grilling/SKILL.md +28 -0
  18. package/.agents/skills/prototype/LOGIC.md +67 -0
  19. package/.agents/skills/prototype/SKILL.md +26 -0
  20. package/.agents/skills/prototype/UI.md +112 -0
  21. package/.github/workflows/npm-publish.yml +10 -0
  22. package/.github/workflows/pr-test.yml +31 -0
  23. package/.opencode/agents/architect.agent.md +0 -1
  24. package/.opencode/agents/coder.agent.md +0 -1
  25. package/.opencode/agents/deployer.agent.md +0 -1
  26. package/.opencode/agents/designer.agent.md +0 -1
  27. package/.opencode/agents/e2e-tester.agent.md +0 -1
  28. package/.opencode/agents/implementer.agent.md +0 -1
  29. package/.opencode/agents/orchestrator/feature-pipeline.agent.md +0 -1
  30. package/.opencode/agents/orchestrator/orchestrator.agent.md +68 -4
  31. package/.opencode/agents/orchestrator/tdd-orchestrator.agent.md +0 -1
  32. package/.opencode/agents/plan.agent.md +0 -1
  33. package/.opencode/agents/researcher.agent.md +0 -1
  34. package/.opencode/agents/reviewer.agent.md +0 -1
  35. package/.opencode/agents/scope.agent.md +299 -0
  36. package/.opencode/agents/tracker.agent.md +0 -1
  37. package/.opencode/agents/unit-tester.agent.md +0 -1
  38. package/AGENTS.md +44 -5
  39. package/README.md +6 -3
  40. package/package.json +1 -1
  41. package/scripts/bump-version.js +7 -0
  42. package/scripts/bump-version.test.js +238 -0
  43. package/scripts/mcp/playwright-mcp-launcher.js +55 -29
  44. package/scripts/mcp/playwright-mcp-launcher.test.js +217 -0
  45. package/scripts/mcp-memory-server.js +93 -76
  46. package/scripts/mcp-memory-server.test.js +398 -0
  47. package/scripts/memory-cli.test.js +297 -0
  48. package/scripts/memory-index.js +119 -31
  49. package/scripts/memory-index.test.js +1173 -0
  50. package/scripts/normalize-memory.js +26 -1
  51. package/scripts/normalize-memory.test.js +753 -0
  52. package/scripts/setup/discover.test.js +124 -2
  53. package/scripts/setup/hooks.test.js +188 -1
  54. package/scripts/setup/husky-init.test.js +311 -0
  55. package/scripts/setup/index.js +35 -5
  56. package/scripts/setup/index.test.js +279 -0
  57. package/scripts/setup/knowledgebase.test.js +100 -0
  58. package/scripts/setup/skills-phase.js +568 -0
  59. package/scripts/setup/skills-phase.test.js +1274 -0
  60. package/scripts/setup/sync-phase.js +6 -1
  61. package/scripts/setup/sync-phase.test.js +180 -2
  62. package/scripts/setup/ui.test.js +579 -0
  63. package/scripts/setup/utils.test.js +586 -0
  64. package/scripts/sync-unit.test.js +1342 -0
  65. package/scripts/sync.js +540 -246
  66. package/scripts/sync.test.js +283 -0
  67. package/scripts/vocab-sync.test.js +88 -0
  68. package/skills-lock.json +90 -0
@@ -0,0 +1,238 @@
1
+ // ═════════════════════════════════════════════════════════════════════════════
2
+ // bump-version.test.js — Unit tests for the pre-commit version bumper
3
+ // ═════════════════════════════════════════════════════════════════════════════
4
+ //
5
+ // bump-version.js is a top-level script: it reads package.json on import,
6
+ // derives the previous major version from `git show HEAD:package.json`, writes
7
+ // the new version, and stages the file. Tests therefore mock `fs` and
8
+ // `child_process` so the repository's real package.json is never touched, then
9
+ // re-import the module for each scenario with `vi.resetModules()`.
10
+ //
11
+ // Covered scenarios:
12
+ // 1. SKIP_BUMP=1 → logs skip, exits 0, no fs/git calls
13
+ // 2. SKIP_BUMP=yes → same (any non-empty value is truthy)
14
+ // 3. SKIP_BUMP=0 → same (string "0" is truthy in Node)
15
+ // 4. SKIP_BUMP="" → falsy, bump proceeds
16
+ // 5. Minor bump (same major) → 1.4.7 → 1.5.0
17
+ // 6. Minor bump with multi-digit minor → 1.10.3 → 1.11.0
18
+ // 7. No HEAD (git show fails) → falls back to current major, bumps minor
19
+ // 8. Major changed → 3.2.1 → 3.0.0
20
+ // 9. Exact serialisation → 2-space JSON + trailing newline, fields preserved
21
+ // 10. Git commands → `git show HEAD:package.json` then `git add package.json`
22
+ // ═════════════════════════════════════════════════════════════════════════════
23
+
24
+ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
25
+
26
+ vi.mock('fs', () => ({
27
+ readFileSync: vi.fn(),
28
+ writeFileSync: vi.fn(),
29
+ }))
30
+
31
+ vi.mock('child_process', () => ({
32
+ execSync: vi.fn(),
33
+ }))
34
+
35
+ import { readFileSync, writeFileSync } from 'fs'
36
+ import { execSync } from 'child_process'
37
+
38
+ const EXIT_SENTINEL = new Error('__process_exit__')
39
+
40
+ /**
41
+ * Re-imports bump-version.js with the current mock state.
42
+ *
43
+ * @returns {Promise<void>}
44
+ */
45
+ async function loadModule() {
46
+ vi.resetModules()
47
+ await import('./bump-version.js')
48
+ }
49
+
50
+ /**
51
+ * Wires the fs/git mocks for a normal (non-skip) run.
52
+ *
53
+ * @param {object} opts
54
+ * @param {object} opts.pkg - Contents returned for package.json.
55
+ * @param {object} [opts.prevPkg] - Contents returned for HEAD:package.json.
56
+ * @param {boolean} [opts.noHead] - When true, `git show` throws.
57
+ */
58
+ function mockNormalRun({ pkg, prevPkg, noHead = false }) {
59
+ readFileSync.mockReturnValue(JSON.stringify(pkg))
60
+ execSync.mockImplementation((cmd) => {
61
+ if (cmd === 'git show HEAD:package.json') {
62
+ if (noHead) throw new Error('fatal: invalid object name HEAD')
63
+ return JSON.stringify(prevPkg)
64
+ }
65
+ return ''
66
+ })
67
+ }
68
+
69
+ describe('bump-version.js', () => {
70
+ let logSpy
71
+ let exitSpy
72
+
73
+ beforeEach(() => {
74
+ vi.clearAllMocks()
75
+ delete process.env.SKIP_BUMP
76
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
77
+ exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {})
78
+ })
79
+
80
+ afterEach(() => {
81
+ delete process.env.SKIP_BUMP
82
+ logSpy.mockRestore()
83
+ exitSpy.mockRestore()
84
+ })
85
+
86
+ // ── SKIP_BUMP guard ──────────────────────────────────────────────────────
87
+
88
+ it('SKIP_BUMP=1: logs skip message, exits 0, touches nothing', async () => {
89
+ process.env.SKIP_BUMP = '1'
90
+ exitSpy.mockImplementation(() => {
91
+ throw EXIT_SENTINEL
92
+ })
93
+
94
+ await expect(loadModule()).rejects.toBe(EXIT_SENTINEL)
95
+
96
+ expect(logSpy).toHaveBeenCalledWith('SKIP_BUMP set — version bump skipped')
97
+ expect(exitSpy).toHaveBeenCalledWith(0)
98
+ expect(readFileSync).not.toHaveBeenCalled()
99
+ expect(writeFileSync).not.toHaveBeenCalled()
100
+ expect(execSync).not.toHaveBeenCalled()
101
+ })
102
+
103
+ it('SKIP_BUMP=yes: any non-empty value skips the bump', async () => {
104
+ process.env.SKIP_BUMP = 'yes'
105
+ exitSpy.mockImplementation(() => {
106
+ throw EXIT_SENTINEL
107
+ })
108
+
109
+ await expect(loadModule()).rejects.toBe(EXIT_SENTINEL)
110
+
111
+ expect(exitSpy).toHaveBeenCalledWith(0)
112
+ expect(writeFileSync).not.toHaveBeenCalled()
113
+ })
114
+
115
+ it('SKIP_BUMP=0: the string "0" is truthy, so the bump is skipped', async () => {
116
+ process.env.SKIP_BUMP = '0'
117
+ exitSpy.mockImplementation(() => {
118
+ throw EXIT_SENTINEL
119
+ })
120
+
121
+ await expect(loadModule()).rejects.toBe(EXIT_SENTINEL)
122
+
123
+ expect(exitSpy).toHaveBeenCalledWith(0)
124
+ expect(execSync).not.toHaveBeenCalled()
125
+ })
126
+
127
+ it('SKIP_BUMP="": empty string is falsy, so the bump proceeds', async () => {
128
+ process.env.SKIP_BUMP = ''
129
+ mockNormalRun({
130
+ pkg: { name: 'fixture', version: '1.4.7' },
131
+ prevPkg: { version: '1.4.0' },
132
+ })
133
+
134
+ await loadModule()
135
+
136
+ expect(exitSpy).not.toHaveBeenCalled()
137
+ expect(logSpy).toHaveBeenCalledWith('Version bumped: 1.4.7 → 1.5.0')
138
+ })
139
+
140
+ // ── Version arithmetic ───────────────────────────────────────────────────
141
+
142
+ it('same major: bumps the minor and resets the patch', async () => {
143
+ mockNormalRun({
144
+ pkg: { name: 'fixture', version: '1.4.7' },
145
+ prevPkg: { version: '1.4.0' },
146
+ })
147
+
148
+ await loadModule()
149
+
150
+ const written = JSON.parse(writeFileSync.mock.calls[0][1])
151
+ expect(written.version).toBe('1.5.0')
152
+ })
153
+
154
+ it('multi-digit minor: increments numerically (1.10.3 → 1.11.0)', async () => {
155
+ mockNormalRun({
156
+ pkg: { name: 'fixture', version: '1.10.3' },
157
+ prevPkg: { version: '1.9.9' },
158
+ })
159
+
160
+ await loadModule()
161
+
162
+ expect(logSpy).toHaveBeenCalledWith('Version bumped: 1.10.3 → 1.11.0')
163
+ })
164
+
165
+ it('no HEAD yet: catch path keeps the current major and bumps the minor', async () => {
166
+ mockNormalRun({
167
+ pkg: { name: 'fixture', version: '2.3.1' },
168
+ noHead: true,
169
+ })
170
+
171
+ await loadModule()
172
+
173
+ expect(logSpy).toHaveBeenCalledWith('Version bumped: 2.3.1 → 2.4.0')
174
+ })
175
+
176
+ it('major changed: normalises to N.0.0', async () => {
177
+ mockNormalRun({
178
+ pkg: { name: 'fixture', version: '3.2.1' },
179
+ prevPkg: { version: '2.9.9' },
180
+ })
181
+
182
+ await loadModule()
183
+
184
+ expect(logSpy).toHaveBeenCalledWith('Version bumped: 3.2.1 → 3.0.0')
185
+ })
186
+
187
+ // ── Serialisation and git side-effects ───────────────────────────────────
188
+
189
+ it('writes 2-space JSON with a trailing newline and preserves other fields', async () => {
190
+ const pkg = {
191
+ name: 'fixture',
192
+ version: '1.4.7',
193
+ scripts: { test: 'vitest' },
194
+ keywords: ['a', 'b'],
195
+ }
196
+ mockNormalRun({ pkg, prevPkg: { version: '1.0.0' } })
197
+
198
+ await loadModule()
199
+
200
+ const [path, contents] = writeFileSync.mock.calls[0]
201
+ expect(path).toMatch(/package\.json$/)
202
+ expect(contents.endsWith('\n')).toBe(true)
203
+ expect(contents).toBe(
204
+ JSON.stringify({ ...pkg, version: '1.5.0' }, null, 2) + '\n',
205
+ )
206
+ })
207
+
208
+ it('reads HEAD:package.json then stages package.json', async () => {
209
+ mockNormalRun({
210
+ pkg: { name: 'fixture', version: '0.9.2' },
211
+ prevPkg: { version: '0.9.0' },
212
+ })
213
+
214
+ await loadModule()
215
+
216
+ expect(execSync).toHaveBeenNthCalledWith(
217
+ 1,
218
+ 'git show HEAD:package.json',
219
+ { encoding: 'utf8' },
220
+ )
221
+ expect(execSync).toHaveBeenNthCalledWith(2, 'git add package.json')
222
+ expect(execSync).toHaveBeenCalledTimes(2)
223
+ })
224
+
225
+ it('reads package.json from the package root', async () => {
226
+ mockNormalRun({
227
+ pkg: { name: 'fixture', version: '1.0.0' },
228
+ prevPkg: { version: '1.0.0' },
229
+ })
230
+
231
+ await loadModule()
232
+
233
+ expect(readFileSync).toHaveBeenCalledWith(
234
+ expect.stringMatching(/package\.json$/),
235
+ 'utf8',
236
+ )
237
+ })
238
+ })
@@ -6,41 +6,67 @@
6
6
  * based on environment variables and project settings.
7
7
  */
8
8
 
9
- const { spawn } = require('child_process');
10
- const path = require('path');
9
+ import { spawn } from 'node:child_process'
10
+ import { resolve } from 'node:path'
11
+ import { fileURLToPath } from 'node:url'
11
12
 
12
- // Get environment variables
13
- const headless = process.env.HEADLESS === 'true';
14
- const slowMo = process.env.SLOW_MO ? parseInt(process.env.SLOW_MO) : 0;
13
+ /**
14
+ * Builds the `npx @playwright/mcp@latest` argument list from the environment.
15
+ *
16
+ * @param {NodeJS.ProcessEnv} [env=process.env] - Environment to read.
17
+ * @returns {string[]} Arguments passed to `npx`.
18
+ */
19
+ export function buildArgs(env = process.env) {
20
+ const args = ['@playwright/mcp@latest']
15
21
 
16
- // Build command arguments
17
- const args = ['@playwright/mcp@latest'];
22
+ if (env.HEADLESS === 'true') {
23
+ args.push('--headless')
24
+ }
18
25
 
19
- // Add headless flag if needed
20
- if (headless) {
21
- args.push('--headless');
22
- }
26
+ if (env.VIEWPORT) {
27
+ args.push('--viewport', env.VIEWPORT)
28
+ }
23
29
 
24
- // Add viewport size if specified
25
- if (process.env.VIEWPORT) {
26
- args.push('--viewport', process.env.VIEWPORT);
30
+ return args
27
31
  }
28
32
 
29
- // Launch the MCP server
30
- const child = spawn('npx', args, {
31
- stdio: 'inherit',
32
- shell: true
33
- });
33
+ /**
34
+ * Spawns the Playwright MCP server and forwards signals / exit codes.
35
+ *
36
+ * @param {object} [options]
37
+ * @param {object} [options.env=process.env] - Environment to read.
38
+ * @param {object} [options.proc=process] - Process-like emitter for signals.
39
+ * @param {Function} [options.exit=process.exit] - Exit function.
40
+ * @returns {import('node:child_process').ChildProcess} The spawned child.
41
+ */
42
+ export function launch({
43
+ env = process.env,
44
+ proc = process,
45
+ exit = process.exit,
46
+ } = {}) {
47
+ const child = spawn('npx', buildArgs(env), {
48
+ stdio: 'inherit',
49
+ shell: true,
50
+ })
51
+
52
+ proc.on('SIGINT', () => {
53
+ child.kill('SIGINT')
54
+ })
55
+
56
+ proc.on('SIGTERM', () => {
57
+ child.kill('SIGTERM')
58
+ })
34
59
 
35
- // Handle process signals
36
- process.on('SIGINT', () => {
37
- child.kill('SIGINT');
38
- });
60
+ child.on('exit', (code) => {
61
+ exit(code || 0)
62
+ })
39
63
 
40
- process.on('SIGTERM', () => {
41
- child.kill('SIGTERM');
42
- });
64
+ return child
65
+ }
43
66
 
44
- child.on('exit', (code) => {
45
- process.exit(code || 0);
46
- });
67
+ const isDirectRun =
68
+ process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
69
+
70
+ if (isDirectRun) {
71
+ launch()
72
+ }
@@ -0,0 +1,217 @@
1
+ // ═════════════════════════════════════════════════════════════════════════════
2
+ // playwright-mcp-launcher.test.js — Unit tests for the MCP launcher
3
+ // ═════════════════════════════════════════════════════════════════════════════
4
+ //
5
+ // `spawn` is mocked so no MCP server is ever launched. Signal handling and
6
+ // exit-code propagation are exercised through injected fakes (an EventEmitter
7
+ // double for the process and a fake child), plus a direct-run import that
8
+ // verifies the CLI bootstrap branch.
9
+ //
10
+ // Covered scenarios:
11
+ // 1. default args (no env flags)
12
+ // 2. HEADLESS=true adds --headless
13
+ // 3. HEADLESS=false does not
14
+ // 4. VIEWPORT adds --viewport <size>
15
+ // 5. HEADLESS + VIEWPORT together
16
+ // 6. spawn is called as `npx <args>` with inherited stdio + shell
17
+ // 7. exit code 0 is forwarded
18
+ // 8. non-zero exit code is forwarded
19
+ // 9. missing exit code falls back to 0
20
+ // 10. SIGINT / SIGTERM are forwarded to the child
21
+ // 11. launch returns the child
22
+ // 12. direct run auto-launches; imported elsewhere it does not
23
+ // ═════════════════════════════════════════════════════════════════════════════
24
+
25
+ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
26
+ import { EventEmitter } from 'node:events'
27
+ import { fileURLToPath } from 'node:url'
28
+
29
+ const cp = vi.hoisted(() => ({ spawn: vi.fn() }))
30
+
31
+ vi.mock('node:child_process', () => ({
32
+ spawn: cp.spawn,
33
+ }))
34
+
35
+ import { spawn } from 'node:child_process'
36
+ import { buildArgs, launch } from './playwright-mcp-launcher.js'
37
+
38
+ /**
39
+ * Creates a fake ChildProcess.
40
+ *
41
+ * @returns {object} Fake child with kill/on and a helper to emit 'exit'.
42
+ */
43
+ function makeChild() {
44
+ const handlers = {}
45
+ return {
46
+ kill: vi.fn(),
47
+ on: vi.fn((event, handler) => {
48
+ handlers[event] = handler
49
+ }),
50
+ emitExit(code) {
51
+ handlers.exit?.(code)
52
+ },
53
+ }
54
+ }
55
+
56
+ describe('playwright-mcp-launcher', () => {
57
+ let child
58
+
59
+ beforeEach(() => {
60
+ vi.clearAllMocks()
61
+ child = makeChild()
62
+ spawn.mockReturnValue(child)
63
+ })
64
+
65
+ afterEach(() => {
66
+ vi.restoreAllMocks()
67
+ })
68
+
69
+ // ── buildArgs ────────────────────────────────────────────────────────────
70
+
71
+ it('buildArgs: returns only the package when no env flags are set', () => {
72
+ expect(buildArgs({})).toEqual(['@playwright/mcp@latest'])
73
+ })
74
+
75
+ it('buildArgs: HEADLESS=true adds --headless', () => {
76
+ expect(buildArgs({ HEADLESS: 'true' })).toEqual([
77
+ '@playwright/mcp@latest',
78
+ '--headless',
79
+ ])
80
+ })
81
+
82
+ it('buildArgs: HEADLESS=false does not add --headless', () => {
83
+ expect(buildArgs({ HEADLESS: 'false' })).toEqual(['@playwright/mcp@latest'])
84
+ })
85
+
86
+ it('buildArgs: VIEWPORT adds a two-token viewport flag', () => {
87
+ expect(buildArgs({ VIEWPORT: '1280x720' })).toEqual([
88
+ '@playwright/mcp@latest',
89
+ '--viewport',
90
+ '1280x720',
91
+ ])
92
+ })
93
+
94
+ it('buildArgs: combines headless and viewport flags', () => {
95
+ expect(buildArgs({ HEADLESS: 'true', VIEWPORT: '800x600' })).toEqual([
96
+ '@playwright/mcp@latest',
97
+ '--headless',
98
+ '--viewport',
99
+ '800x600',
100
+ ])
101
+ })
102
+
103
+ // ── launch ───────────────────────────────────────────────────────────────
104
+
105
+ it('launch: spawns npx with inherited stdio and shell', () => {
106
+ launch({ env: {}, proc: new EventEmitter(), exit: vi.fn() })
107
+
108
+ expect(spawn).toHaveBeenCalledWith('npx', ['@playwright/mcp@latest'], {
109
+ stdio: 'inherit',
110
+ shell: true,
111
+ })
112
+ })
113
+
114
+ it('launch: returns the spawned child', () => {
115
+ const returned = launch({
116
+ env: {},
117
+ proc: new EventEmitter(),
118
+ exit: vi.fn(),
119
+ })
120
+
121
+ expect(returned).toBe(child)
122
+ })
123
+
124
+ it('launch: forwards exit code 0', () => {
125
+ const exit = vi.fn()
126
+ launch({ env: {}, proc: new EventEmitter(), exit })
127
+
128
+ child.emitExit(0)
129
+
130
+ expect(exit).toHaveBeenCalledWith(0)
131
+ })
132
+
133
+ it('launch: forwards a non-zero exit code', () => {
134
+ const exit = vi.fn()
135
+ launch({ env: {}, proc: new EventEmitter(), exit })
136
+
137
+ child.emitExit(5)
138
+
139
+ expect(exit).toHaveBeenCalledWith(5)
140
+ })
141
+
142
+ it('launch: falls back to 0 when no exit code is provided', () => {
143
+ const exit = vi.fn()
144
+ launch({ env: {}, proc: new EventEmitter(), exit })
145
+
146
+ child.emitExit(undefined)
147
+
148
+ expect(exit).toHaveBeenCalledWith(0)
149
+ })
150
+
151
+ it('launch: forwards SIGINT to the child', () => {
152
+ const proc = new EventEmitter()
153
+ launch({ env: {}, proc, exit: vi.fn() })
154
+
155
+ proc.emit('SIGINT')
156
+
157
+ expect(child.kill).toHaveBeenCalledWith('SIGINT')
158
+ })
159
+
160
+ it('launch: forwards SIGTERM to the child', () => {
161
+ const proc = new EventEmitter()
162
+ launch({ env: {}, proc, exit: vi.fn() })
163
+
164
+ proc.emit('SIGTERM')
165
+
166
+ expect(child.kill).toHaveBeenCalledWith('SIGTERM')
167
+ })
168
+
169
+ // ── Direct-run guard ─────────────────────────────────────────────────────
170
+
171
+ it('direct run: auto-launches when argv[1] is this module', async () => {
172
+ const modulePath = fileURLToPath(
173
+ new URL('./playwright-mcp-launcher.js', import.meta.url),
174
+ )
175
+ const originalArgv1 = process.argv[1]
176
+ const intListeners = process.listeners('SIGINT')
177
+ const termListeners = process.listeners('SIGTERM')
178
+ process.argv[1] = modulePath
179
+
180
+ try {
181
+ vi.resetModules()
182
+ await import('./playwright-mcp-launcher.js')
183
+
184
+ expect(spawn).toHaveBeenCalledWith(
185
+ 'npx',
186
+ expect.arrayContaining(['@playwright/mcp@latest']),
187
+ { stdio: 'inherit', shell: true },
188
+ )
189
+ } finally {
190
+ process.argv[1] = originalArgv1
191
+ for (const listener of process.listeners('SIGINT')) {
192
+ if (!intListeners.includes(listener)) {
193
+ process.removeListener('SIGINT', listener)
194
+ }
195
+ }
196
+ for (const listener of process.listeners('SIGTERM')) {
197
+ if (!termListeners.includes(listener)) {
198
+ process.removeListener('SIGTERM', listener)
199
+ }
200
+ }
201
+ }
202
+ })
203
+
204
+ it('imported with an empty argv[1]: does not launch', async () => {
205
+ const originalArgv1 = process.argv[1]
206
+ process.argv[1] = ''
207
+
208
+ try {
209
+ vi.resetModules()
210
+ await import('./playwright-mcp-launcher.js')
211
+
212
+ expect(spawn).not.toHaveBeenCalled()
213
+ } finally {
214
+ process.argv[1] = originalArgv1
215
+ }
216
+ })
217
+ })