@visa/cli 4.1.0-rc.99 → 5.0.0-rc.335

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 (85) hide show
  1. package/README.md +282 -156
  2. package/dist/cli.js +333 -532
  3. package/dist/managed-runtime/resolve-and-update.mjs +268 -0
  4. package/dist/managed-runtime/runtime-readiness.mjs +126 -0
  5. package/dist/managed-runtime/update-and-restart.mjs +1079 -0
  6. package/dist/mcp-apps/ucp-checkout.html +280 -0
  7. package/dist/mcp-server/index.js +234 -387
  8. package/dist/merchant-ucp-mcp/index.js +7 -0
  9. package/dist/skills/pair-visa-agent/RUNTIMES.md +56 -26
  10. package/dist/skills/pair-visa-agent/SKILL.md +319 -320
  11. package/dist/skills/pair-visa-agent/scripts/__tests__/setup.test.mjs +407 -0
  12. package/dist/skills/pair-visa-agent/scripts/setup.mjs +310 -30
  13. package/dist/skills/visa-shopify-checkout/SKILL.md +122 -0
  14. package/dist/skills/visa-shopify-checkout/references/evidence-and-states.md +37 -0
  15. package/dist/skills/visa-ucp-shopping/SKILL.md +86 -0
  16. package/install.ps1 +10 -6
  17. package/install.sh +7 -2
  18. package/native/bin/darwin-arm64/visa-runtime-signer +0 -0
  19. package/native/bin/darwin-x64/visa-runtime-signer +0 -0
  20. package/native/bin/linux-arm64/visa-runtime-signer +0 -0
  21. package/native/bin/linux-x64/visa-runtime-signer +0 -0
  22. package/native/bin/win32-arm64/visa-runtime-signer.exe +0 -0
  23. package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
  24. package/native/bin/win32-x64/visa-runtime-signer.exe +0 -0
  25. package/package.json +23 -31
  26. package/server.json +3 -3
  27. package/dist/checkout-engine/adapters/generic.d.ts +0 -23
  28. package/dist/checkout-engine/adapters/generic.js +0 -216
  29. package/dist/checkout-engine/adapters/index.d.ts +0 -10
  30. package/dist/checkout-engine/adapters/index.js +0 -24
  31. package/dist/checkout-engine/adapters/shopify.d.ts +0 -31
  32. package/dist/checkout-engine/adapters/shopify.js +0 -423
  33. package/dist/checkout-engine/adapters/stripe-like.d.ts +0 -10
  34. package/dist/checkout-engine/adapters/stripe-like.js +0 -21
  35. package/dist/checkout-engine/amount.d.ts +0 -15
  36. package/dist/checkout-engine/amount.js +0 -72
  37. package/dist/checkout-engine/browser-launch.d.ts +0 -46
  38. package/dist/checkout-engine/browser-launch.js +0 -81
  39. package/dist/checkout-engine/ceremony.d.ts +0 -64
  40. package/dist/checkout-engine/ceremony.js +0 -261
  41. package/dist/checkout-engine/cli-engine.d.ts +0 -227
  42. package/dist/checkout-engine/cli-engine.js +0 -779
  43. package/dist/checkout-engine/detect.d.ts +0 -61
  44. package/dist/checkout-engine/detect.js +0 -398
  45. package/dist/checkout-engine/evidence.d.ts +0 -25
  46. package/dist/checkout-engine/evidence.js +0 -104
  47. package/dist/checkout-engine/executor.d.ts +0 -176
  48. package/dist/checkout-engine/executor.js +0 -1325
  49. package/dist/checkout-engine/hosted-approval.d.ts +0 -187
  50. package/dist/checkout-engine/hosted-approval.js +0 -478
  51. package/dist/checkout-engine/index.d.ts +0 -6
  52. package/dist/checkout-engine/index.js +0 -8
  53. package/dist/checkout-engine/inline-target.d.ts +0 -13
  54. package/dist/checkout-engine/inline-target.js +0 -37
  55. package/dist/checkout-engine/instrument.d.ts +0 -61
  56. package/dist/checkout-engine/instrument.js +0 -87
  57. package/dist/checkout-engine/live-fill-approval.d.ts +0 -43
  58. package/dist/checkout-engine/live-fill-approval.js +0 -90
  59. package/dist/checkout-engine/mandate/card-mandate.d.ts +0 -121
  60. package/dist/checkout-engine/mandate/card-mandate.js +0 -227
  61. package/dist/checkout-engine/mandate/mandate-ledger.d.ts +0 -142
  62. package/dist/checkout-engine/mandate/mandate-ledger.js +0 -338
  63. package/dist/checkout-engine/mandate.d.ts +0 -25
  64. package/dist/checkout-engine/mandate.js +0 -100
  65. package/dist/checkout-engine/outcome.d.ts +0 -30
  66. package/dist/checkout-engine/outcome.js +0 -225
  67. package/dist/checkout-engine/owner-only-file.d.ts +0 -19
  68. package/dist/checkout-engine/owner-only-file.js +0 -41
  69. package/dist/checkout-engine/package.json +0 -3
  70. package/dist/checkout-engine/receipt.d.ts +0 -81
  71. package/dist/checkout-engine/receipt.js +0 -109
  72. package/dist/checkout-engine/repo-env.d.ts +0 -11
  73. package/dist/checkout-engine/repo-env.js +0 -23
  74. package/dist/checkout-engine/trace-handles.d.ts +0 -8
  75. package/dist/checkout-engine/trace-handles.js +0 -12
  76. package/dist/checkout-engine/types.d.ts +0 -44
  77. package/dist/checkout-engine/types.js +0 -2
  78. package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +0 -74
  79. package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +0 -248
  80. package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +0 -82
  81. package/dist/checkout-engine/vgs-gateway/server-mint-client.js +0 -180
  82. package/dist/checkout-engine/vgs-live-instrument.d.ts +0 -179
  83. package/dist/checkout-engine/vgs-live-instrument.js +0 -296
  84. package/dist/checkout-engine/vic-confirmation.d.ts +0 -34
  85. package/dist/checkout-engine/vic-confirmation.js +0 -39
@@ -0,0 +1,407 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { mkdirSync, writeFileSync, rmSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { tmpdir } from 'node:os'
5
+ import { randomUUID } from 'node:crypto'
6
+
7
+ import {
8
+ parseSemver,
9
+ isSemverCompatible,
10
+ findCliBinary,
11
+ detectCliVersion,
12
+ probeMcpTools,
13
+ verifyCliReadiness,
14
+ runSetup,
15
+ REQUIRED_MCP_TOOLS,
16
+ } from '../setup.mjs'
17
+
18
+ describe('setup.mjs portable provisioner', () => {
19
+ let tempDir
20
+
21
+ beforeEach(() => {
22
+ tempDir = join(tmpdir(), `setup-test-${randomUUID()}`)
23
+ mkdirSync(tempDir, { recursive: true })
24
+ })
25
+
26
+ afterEach(() => {
27
+ rmSync(tempDir, { recursive: true, force: true })
28
+ })
29
+
30
+ describe('parseSemver', () => {
31
+ it('parses release and prerelease versions', () => {
32
+ expect(parseSemver('4.0.1')).toEqual({ major: 4, minor: 0, patch: 1, pre: null })
33
+ expect(parseSemver('v4.1.0-rc.159')).toEqual({ major: 4, minor: 1, patch: 0, pre: 'rc.159' })
34
+ expect(parseSemver('4.1.0-rc.166+build123')).toEqual({
35
+ major: 4,
36
+ minor: 1,
37
+ patch: 0,
38
+ pre: 'rc.166',
39
+ })
40
+ expect(parseSemver('not-a-version')).toBeNull()
41
+ expect(parseSemver(null)).toBeNull()
42
+ })
43
+ })
44
+
45
+ describe('isSemverCompatible', () => {
46
+ it('rejects stale 4.0.1 against 4.1.0-rc.159 floor', () => {
47
+ expect(isSemverCompatible('4.0.1', '4.1.0-rc.159')).toBe(false)
48
+ expect(isSemverCompatible('3.9.0', '4.1.0-rc.159')).toBe(false)
49
+ })
50
+
51
+ it('rejects older RC versions', () => {
52
+ expect(isSemverCompatible('4.1.0-rc.70', '4.1.0-rc.159')).toBe(false)
53
+ expect(isSemverCompatible('4.1.0-rc.158', '4.1.0-rc.159')).toBe(false)
54
+ })
55
+
56
+ it('accepts exact matching or newer RC versions', () => {
57
+ expect(isSemverCompatible('4.1.0-rc.159', '4.1.0-rc.159')).toBe(true)
58
+ expect(isSemverCompatible('4.1.0-rc.166', '4.1.0-rc.159')).toBe(true)
59
+ expect(isSemverCompatible('4.1.0-rc.200', '4.1.0-rc.159')).toBe(true)
60
+ })
61
+
62
+ it('accepts stable promotion (4.1.0 outranks 4.1.0-rc.159)', () => {
63
+ expect(isSemverCompatible('4.1.0', '4.1.0-rc.159')).toBe(true)
64
+ expect(isSemverCompatible('4.2.0', '4.1.0-rc.159')).toBe(true)
65
+ expect(isSemverCompatible('5.0.0', '4.1.0-rc.159')).toBe(true)
66
+ })
67
+ })
68
+
69
+ describe('findCliBinary', () => {
70
+ it('returns visa when visa --version succeeds', () => {
71
+ const mockExec = vi.fn((cmd) => {
72
+ if (cmd === 'visa') return 'visa 4.1.0-rc.166'
73
+ throw new Error('command not found')
74
+ })
75
+ expect(findCliBinary(mockExec)).toBe('visa')
76
+ })
77
+
78
+ it('returns visa-cli when only visa-cli succeeds', () => {
79
+ const mockExec = vi.fn((cmd) => {
80
+ if (cmd === 'visa-cli') return 'visa-cli 4.1.0-rc.166'
81
+ throw new Error('command not found')
82
+ })
83
+ expect(findCliBinary(mockExec)).toBe('visa-cli')
84
+ })
85
+
86
+ it('returns null when neither binary succeeds', () => {
87
+ const mockExec = vi.fn(() => {
88
+ throw new Error('command not found')
89
+ })
90
+ expect(findCliBinary(mockExec)).toBeNull()
91
+ })
92
+ })
93
+
94
+ describe('detectCliVersion', () => {
95
+ it('parses version from capabilities JSON output', () => {
96
+ const mockExec = vi.fn((cmd, args) => {
97
+ if (args.includes('capabilities')) {
98
+ return JSON.stringify({
99
+ data: { build: { version: '4.1.0-rc.166' } },
100
+ })
101
+ }
102
+ return '4.1.0-rc.166'
103
+ })
104
+ expect(detectCliVersion('visa', mockExec)).toBe('4.1.0-rc.166')
105
+ })
106
+
107
+ it('falls back to --version when capabilities fails', () => {
108
+ const mockExec = vi.fn((cmd, args) => {
109
+ if (args.includes('capabilities')) throw new Error('unknown command')
110
+ if (args.includes('--version')) return 'v4.1.0-rc.166\n'
111
+ throw new Error('unknown')
112
+ })
113
+ expect(detectCliVersion('visa', mockExec)).toBe('4.1.0-rc.166')
114
+ })
115
+ })
116
+
117
+ describe('probeMcpTools', () => {
118
+ it('returns ok: true when all required tools exist in bundle', () => {
119
+ const bundlePath = join(tempDir, 'index.js')
120
+ writeFileSync(
121
+ bundlePath,
122
+ `
123
+ const tools = ['agent_handoff_claim', 'agent_connect', 'agent_connect_poll', 'setup_start'];
124
+ `
125
+ )
126
+ const probe = probeMcpTools(bundlePath, REQUIRED_MCP_TOOLS)
127
+ expect(probe.ok).toBe(true)
128
+ expect(probe.missingTools).toEqual([])
129
+ })
130
+
131
+ it('returns missing tools when bundle lacks handoff tools (e.g. 4.0.1 bundle)', () => {
132
+ const bundlePath = join(tempDir, 'index.js')
133
+ writeFileSync(
134
+ bundlePath,
135
+ `
136
+ const tools = ['enroll_agent', 'pay_merchant'];
137
+ `
138
+ )
139
+ const probe = probeMcpTools(bundlePath, REQUIRED_MCP_TOOLS)
140
+ expect(probe.ok).toBe(false)
141
+ expect(probe.missingTools).toEqual([
142
+ 'agent_handoff_claim',
143
+ 'agent_connect',
144
+ 'agent_connect_poll',
145
+ ])
146
+ })
147
+
148
+ it('returns false when bundle file is missing', () => {
149
+ const probe = probeMcpTools(join(tempDir, 'nonexistent.js'), REQUIRED_MCP_TOOLS)
150
+ expect(probe.ok).toBe(false)
151
+ expect(probe.missingTools).toEqual([...REQUIRED_MCP_TOOLS])
152
+ })
153
+
154
+ it('does not treat agent_connect_poll as a match for agent_connect', () => {
155
+ const bundlePath = join(tempDir, 'index.js')
156
+ writeFileSync(bundlePath, `const tools = ['agent_handoff_claim', 'agent_connect_poll'];`)
157
+ const probe = probeMcpTools(bundlePath, REQUIRED_MCP_TOOLS)
158
+ expect(probe.ok).toBe(false)
159
+ expect(probe.missingTools).toContain('agent_connect')
160
+ expect(probe.missingTools).not.toContain('agent_connect_poll')
161
+ })
162
+ })
163
+
164
+ describe('verifyCliReadiness', () => {
165
+ it('reports not_installed when no binary is found', () => {
166
+ const mockExec = vi.fn(() => {
167
+ throw new Error('command not found')
168
+ })
169
+ const result = verifyCliReadiness(mockExec)
170
+ expect(result.ready).toBe(false)
171
+ expect(result.reason).toBe('not_installed')
172
+ })
173
+
174
+ it('reports stale_or_incomplete when installed version is 4.0.1', () => {
175
+ const bundlePath = join(tempDir, 'index.js')
176
+ writeFileSync(bundlePath, `const tools = ['enroll_agent'];`)
177
+
178
+ const mockExec = vi.fn((cmd, args) => {
179
+ if (args.includes('--version')) return '4.0.1'
180
+ if (args.includes('capabilities'))
181
+ return JSON.stringify({ data: { build: { version: '4.0.1' } } })
182
+ throw new Error('unknown')
183
+ })
184
+ const mockNpmRoot = vi.fn(() => tempDir)
185
+
186
+ const result = verifyCliReadiness(mockExec, mockNpmRoot)
187
+ expect(result.ready).toBe(false)
188
+ expect(result.reason).toBe('stale_or_incomplete')
189
+ expect(result.version).toBe('4.0.1')
190
+ })
191
+
192
+ it('reports ready when version is >= 4.1.0-rc.159 and required tools exist', () => {
193
+ const bundleDir = join(tempDir, '@visa', 'cli', 'dist', 'mcp-server')
194
+ mkdirSync(bundleDir, { recursive: true })
195
+ writeFileSync(
196
+ join(bundleDir, 'index.js'),
197
+ `const tools = ['agent_handoff_claim', 'agent_connect', 'agent_connect_poll'];`
198
+ )
199
+
200
+ const mockExec = vi.fn((cmd, args) => {
201
+ if (args.includes('--version')) return '4.1.0-rc.166'
202
+ if (args.includes('capabilities')) {
203
+ return JSON.stringify({ data: { build: { version: '4.1.0-rc.166' } } })
204
+ }
205
+ throw new Error('unknown')
206
+ })
207
+ const mockNpmRoot = vi.fn(() => tempDir)
208
+
209
+ const result = verifyCliReadiness(mockExec, mockNpmRoot)
210
+ expect(result.ready).toBe(true)
211
+ expect(result.version).toBe('4.1.0-rc.166')
212
+ expect(result.missingTools).toEqual([])
213
+ })
214
+
215
+ it('reports stale_or_incomplete when version is compatible but the MCP bundle is missing', () => {
216
+ const mockExec = vi.fn((cmd, args) => {
217
+ if (args.includes('--version')) return '4.1.0-rc.166'
218
+ if (args.includes('capabilities')) {
219
+ return JSON.stringify({ data: { build: { version: '4.1.0-rc.166' } } })
220
+ }
221
+ throw new Error('unknown')
222
+ })
223
+ const mockNpmRoot = vi.fn(() => join(tempDir, 'no-such-npm-root'))
224
+
225
+ const result = verifyCliReadiness(mockExec, mockNpmRoot, () => null)
226
+ expect(result.ready).toBe(false)
227
+ expect(result.reason).toBe('stale_or_incomplete')
228
+ expect(result.version).toBe('4.1.0-rc.166')
229
+ expect(result.missingTools).toEqual([...REQUIRED_MCP_TOOLS])
230
+ })
231
+ })
232
+
233
+ describe('runSetup end-to-end scenarios', () => {
234
+ it('scenario 1: fresh box (no CLI installed) -> installs @visa/cli@rc and succeeds', async () => {
235
+ const bundleDir = join(tempDir, '@visa', 'cli', 'dist', 'mcp-server')
236
+ let installed = false
237
+
238
+ const mockExec = vi.fn((cmd, args) => {
239
+ if (cmd === 'npm' && args.includes('install')) {
240
+ installed = true
241
+ mkdirSync(bundleDir, { recursive: true })
242
+ writeFileSync(
243
+ join(bundleDir, 'index.js'),
244
+ `const tools = ['agent_handoff_claim', 'agent_connect', 'agent_connect_poll'];`
245
+ )
246
+ return ''
247
+ }
248
+ if (cmd === 'visa') {
249
+ if (!installed) throw new Error('command not found')
250
+ if (args.includes('--version')) return '4.1.0-rc.166'
251
+ if (args.includes('capabilities')) {
252
+ return JSON.stringify({ data: { build: { version: '4.1.0-rc.166' } } })
253
+ }
254
+ }
255
+ throw new Error(`unknown command ${cmd}`)
256
+ })
257
+ const mockNpmRoot = vi.fn(() => tempDir)
258
+
259
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
260
+ const result = await runSetup(
261
+ { VISA_RELEASE_CHANNEL: 'rc' },
262
+ { execFileSync: mockExec, npmRootFn: mockNpmRoot }
263
+ )
264
+ logSpy.mockRestore()
265
+
266
+ expect(mockExec).toHaveBeenCalledWith(
267
+ 'npm',
268
+ ['install', '-g', '@visa/cli@rc'],
269
+ expect.any(Object)
270
+ )
271
+ expect(result.status).toBe('installed')
272
+ expect(result.version).toBe('4.1.0-rc.166')
273
+ })
274
+
275
+ it('scenario 2: stale CLI installed (4.0.1) -> upgrades to @visa/cli@rc and succeeds', async () => {
276
+ const bundleDir = join(tempDir, '@visa', 'cli', 'dist', 'mcp-server')
277
+ mkdirSync(bundleDir, { recursive: true })
278
+ writeFileSync(join(bundleDir, 'index.js'), `const tools = ['enroll_agent'];`)
279
+
280
+ let upgraded = false
281
+ const mockExec = vi.fn((cmd, args) => {
282
+ if (cmd === 'npm' && args.includes('install')) {
283
+ upgraded = true
284
+ writeFileSync(
285
+ join(bundleDir, 'index.js'),
286
+ `const tools = ['agent_handoff_claim', 'agent_connect', 'agent_connect_poll'];`
287
+ )
288
+ return ''
289
+ }
290
+ if (cmd === 'visa') {
291
+ const version = upgraded ? '4.1.0-rc.166' : '4.0.1'
292
+ if (args.includes('--version')) return version
293
+ if (args.includes('capabilities')) {
294
+ return JSON.stringify({ data: { build: { version } } })
295
+ }
296
+ }
297
+ throw new Error(`unknown command ${cmd}`)
298
+ })
299
+ const mockNpmRoot = vi.fn(() => tempDir)
300
+
301
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
302
+ const result = await runSetup(
303
+ { VISA_RELEASE_CHANNEL: 'rc' },
304
+ { execFileSync: mockExec, npmRootFn: mockNpmRoot }
305
+ )
306
+ logSpy.mockRestore()
307
+
308
+ expect(mockExec).toHaveBeenCalledWith(
309
+ 'npm',
310
+ ['install', '-g', '@visa/cli@rc'],
311
+ expect.any(Object)
312
+ )
313
+ expect(result.status).toBe('upgraded')
314
+ expect(result.version).toBe('4.1.0-rc.166')
315
+ })
316
+
317
+ it('scenario 3: current CLI installed -> idempotent no-op', async () => {
318
+ const bundleDir = join(tempDir, '@visa', 'cli', 'dist', 'mcp-server')
319
+ mkdirSync(bundleDir, { recursive: true })
320
+ writeFileSync(
321
+ join(bundleDir, 'index.js'),
322
+ `const tools = ['agent_handoff_claim', 'agent_connect', 'agent_connect_poll'];`
323
+ )
324
+
325
+ const mockExec = vi.fn((cmd, args) => {
326
+ if (cmd === 'visa') {
327
+ if (args.includes('--version')) return '4.1.0-rc.166'
328
+ if (args.includes('capabilities')) {
329
+ return JSON.stringify({ data: { build: { version: '4.1.0-rc.166' } } })
330
+ }
331
+ }
332
+ throw new Error(`unexpected execution ${cmd}`)
333
+ })
334
+ const mockNpmRoot = vi.fn(() => tempDir)
335
+
336
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
337
+ const result = await runSetup(
338
+ { VISA_RELEASE_CHANNEL: 'rc' },
339
+ { execFileSync: mockExec, npmRootFn: mockNpmRoot }
340
+ )
341
+ logSpy.mockRestore()
342
+
343
+ expect(mockExec).not.toHaveBeenCalledWith('npm', expect.anything(), expect.anything())
344
+ expect(result.status).toBe('already_ready')
345
+ expect(result.version).toBe('4.1.0-rc.166')
346
+ })
347
+
348
+ it('scenario 4: install failure reports one explicit upgrade action and exits 1', async () => {
349
+ const mockExec = vi.fn((cmd) => {
350
+ if (cmd === 'npm') throw new Error('EACCES: permission denied')
351
+ throw new Error('not installed')
352
+ })
353
+ const mockNpmRoot = vi.fn(() => tempDir)
354
+
355
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
356
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
357
+ throw new Error(`process.exit(${code})`)
358
+ })
359
+
360
+ await expect(
361
+ runSetup({ VISA_RELEASE_CHANNEL: 'rc' }, { execFileSync: mockExec, npmRootFn: mockNpmRoot })
362
+ ).rejects.toThrow('process.exit(1)')
363
+
364
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('npm install -g @visa/cli@rc'))
365
+
366
+ errorSpy.mockRestore()
367
+ exitSpy.mockRestore()
368
+ })
369
+
370
+ it('scenario 5: version string appears current but tools missing after install -> fails readiness', async () => {
371
+ const bundleDir = join(tempDir, '@visa', 'cli', 'dist', 'mcp-server')
372
+ mkdirSync(bundleDir, { recursive: true })
373
+ writeFileSync(join(bundleDir, 'index.js'), `const tools = ['other_tool'];`)
374
+
375
+ const mockExec = vi.fn((cmd, args) => {
376
+ if (cmd === 'npm') return ''
377
+ if (cmd === 'visa') {
378
+ if (args.includes('--version')) return '4.1.0-rc.166'
379
+ if (args.includes('capabilities')) {
380
+ return JSON.stringify({ data: { build: { version: '4.1.0-rc.166' } } })
381
+ }
382
+ }
383
+ throw new Error(`unknown command ${cmd}`)
384
+ })
385
+ const mockNpmRoot = vi.fn(() => tempDir)
386
+
387
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
388
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
389
+ throw new Error(`process.exit(${code})`)
390
+ })
391
+
392
+ await expect(
393
+ runSetup({ VISA_RELEASE_CHANNEL: 'rc' }, { execFileSync: mockExec, npmRootFn: mockNpmRoot })
394
+ ).rejects.toThrow('process.exit(1)')
395
+
396
+ expect(errorSpy).toHaveBeenCalledWith(
397
+ expect.stringContaining(
398
+ 'Readiness check failed: installed CLI v4.1.0-rc.166 is missing required tools'
399
+ )
400
+ )
401
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('npm install -g @visa/cli@rc'))
402
+
403
+ errorSpy.mockRestore()
404
+ exitSpy.mockRestore()
405
+ })
406
+ })
407
+ })