@visa/cli 4.1.0-rc.26 → 4.1.0-rc.260

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 (83) hide show
  1. package/README.md +202 -46
  2. package/dist/checkout-engine/adapters/generic.d.ts +69 -0
  3. package/dist/checkout-engine/adapters/generic.js +383 -58
  4. package/dist/checkout-engine/adapters/index.d.ts +4 -1
  5. package/dist/checkout-engine/adapters/index.js +10 -3
  6. package/dist/checkout-engine/adapters/shopify.d.ts +80 -0
  7. package/dist/checkout-engine/adapters/shopify.js +688 -0
  8. package/dist/checkout-engine/amount.d.ts +15 -0
  9. package/dist/checkout-engine/amount.js +72 -0
  10. package/dist/checkout-engine/browser-launch.d.ts +9 -4
  11. package/dist/checkout-engine/browser-launch.js +19 -4
  12. package/dist/checkout-engine/browserbase-browser.d.ts +24 -0
  13. package/dist/checkout-engine/browserbase-browser.js +186 -0
  14. package/dist/checkout-engine/cli-engine.d.ts +206 -32
  15. package/dist/checkout-engine/cli-engine.js +772 -216
  16. package/dist/checkout-engine/confirmed-merchants.d.ts +31 -0
  17. package/dist/checkout-engine/confirmed-merchants.js +165 -0
  18. package/dist/checkout-engine/detect.d.ts +1 -1
  19. package/dist/checkout-engine/detect.js +6 -0
  20. package/dist/checkout-engine/evidence.d.ts +1 -1
  21. package/dist/checkout-engine/executor.d.ts +61 -3
  22. package/dist/checkout-engine/executor.js +550 -144
  23. package/dist/checkout-engine/hosted-approval.d.ts +69 -9
  24. package/dist/checkout-engine/hosted-approval.js +211 -21
  25. package/dist/checkout-engine/index.d.ts +6 -2
  26. package/dist/checkout-engine/index.js +5 -1
  27. package/dist/checkout-engine/instrument.d.ts +6 -0
  28. package/dist/checkout-engine/known-merchants.d.ts +10 -0
  29. package/dist/checkout-engine/known-merchants.js +38 -0
  30. package/dist/checkout-engine/live-fill-approval.d.ts +5 -11
  31. package/dist/checkout-engine/live-fill-approval.js +20 -34
  32. package/dist/checkout-engine/mandate/card-mandate.d.ts +6 -2
  33. package/dist/checkout-engine/mandate/card-mandate.js +10 -5
  34. package/dist/checkout-engine/mandate/mandate-ledger.d.ts +63 -23
  35. package/dist/checkout-engine/mandate/mandate-ledger.js +124 -17
  36. package/dist/checkout-engine/receipt-dir.d.ts +6 -0
  37. package/dist/checkout-engine/receipt-dir.js +8 -0
  38. package/dist/checkout-engine/receipt.d.ts +56 -2
  39. package/dist/checkout-engine/receipt.js +55 -16
  40. package/dist/checkout-engine/shopify-primary-domain.d.ts +25 -0
  41. package/dist/checkout-engine/shopify-primary-domain.js +96 -0
  42. package/dist/checkout-engine/trace-handles.d.ts +8 -0
  43. package/dist/checkout-engine/trace-handles.js +12 -0
  44. package/dist/checkout-engine/types.d.ts +15 -2
  45. package/dist/checkout-engine/unresolved-charges.d.ts +34 -0
  46. package/dist/checkout-engine/unresolved-charges.js +134 -0
  47. package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +26 -7
  48. package/dist/checkout-engine/vgs-gateway/server-mint-client.js +43 -3
  49. package/dist/checkout-engine/vgs-live-instrument.d.ts +11 -35
  50. package/dist/checkout-engine/vgs-live-instrument.js +14 -74
  51. package/dist/checkout-engine/vic-confirmation.d.ts +18 -0
  52. package/dist/checkout-engine/vic-confirmation.js +9 -3
  53. package/dist/checkout-engine/web-bot-auth.d.ts +98 -0
  54. package/dist/checkout-engine/web-bot-auth.js +218 -0
  55. package/dist/cli.js +899 -387
  56. package/dist/mcp-apps/ucp-checkout.html +280 -0
  57. package/dist/mcp-server/index.js +725 -254
  58. package/dist/merchant-ucp-mcp/index.js +6 -0
  59. package/dist/skills/pair-visa-agent/RUNTIMES.md +122 -79
  60. package/dist/skills/pair-visa-agent/SKILL.md +433 -318
  61. package/dist/skills/pair-visa-agent/scripts/__tests__/setup.test.mjs +407 -0
  62. package/dist/skills/pair-visa-agent/scripts/setup.mjs +310 -30
  63. package/dist/skills/visa-shopify-checkout/SKILL.md +122 -0
  64. package/dist/skills/visa-shopify-checkout/references/evidence-and-states.md +37 -0
  65. package/dist/skills/visa-ucp-shopping/SKILL.md +86 -0
  66. package/dist/subway-direct.mjs +1 -0
  67. package/install.ps1 +7 -6
  68. package/install.sh +3 -3
  69. package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
  70. package/package.json +31 -28
  71. package/server.json +4 -4
  72. package/dist/checkout-engine/inline-target.d.ts +0 -13
  73. package/dist/checkout-engine/inline-target.js +0 -37
  74. package/dist/checkout-engine/pay-args.d.ts +0 -14
  75. package/dist/checkout-engine/pay-args.js +0 -44
  76. package/dist/checkout-engine/pay.d.ts +0 -1
  77. package/dist/checkout-engine/pay.js +0 -13
  78. package/dist/checkout-engine/repo-env.d.ts +0 -11
  79. package/dist/checkout-engine/repo-env.js +0 -23
  80. package/dist/checkout-engine/run-live-fill.d.ts +0 -1
  81. package/dist/checkout-engine/run-live-fill.js +0 -493
  82. package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +0 -74
  83. package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +0 -248
@@ -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
+ })