execution.test.ts
1 import { describe, expect, it, vi } from 'vitest'; 2 import type { CliCommand } from './registry.js'; 3 import { executeCommand, prepareCommandArgs } from './execution.js'; 4 import { TimeoutError } from './errors.js'; 5 import { cli, Strategy } from './registry.js'; 6 import { withTimeoutMs } from './runtime.js'; 7 import * as runtime from './runtime.js'; 8 import * as capRouting from './capabilityRouting.js'; 9 10 describe('executeCommand — non-browser timeout', () => { 11 it('applies timeoutSeconds to non-browser commands', async () => { 12 const cmd = cli({ 13 site: 'test-execution', 14 name: 'non-browser-timeout', 15 description: 'test non-browser timeout', 16 browser: false, 17 strategy: Strategy.PUBLIC, 18 timeoutSeconds: 0.01, 19 func: () => new Promise(() => {}), 20 }); 21 22 // Sentinel timeout at 200ms — if the inner 10ms timeout fires first, 23 // the error will be a TimeoutError with the command label, not 'sentinel'. 24 const error = await withTimeoutMs(executeCommand(cmd, {}), 200, 'sentinel timeout') 25 .catch((err) => err); 26 27 expect(error).toBeInstanceOf(TimeoutError); 28 expect(error).toMatchObject({ 29 code: 'TIMEOUT', 30 message: 'test-execution/non-browser-timeout timed out after 0.01s', 31 }); 32 }); 33 34 it('skips timeout when timeoutSeconds is 0', async () => { 35 const cmd = cli({ 36 site: 'test-execution', 37 name: 'non-browser-zero-timeout', 38 description: 'test zero timeout bypasses wrapping', 39 browser: false, 40 strategy: Strategy.PUBLIC, 41 timeoutSeconds: 0, 42 func: () => new Promise(() => {}), 43 }); 44 45 // With timeout guard skipped, the sentinel fires instead. 46 await expect( 47 withTimeoutMs(executeCommand(cmd, {}), 50, 'sentinel timeout'), 48 ).rejects.toThrow('sentinel timeout'); 49 }); 50 51 it('calls closeWindow on browser command failure', async () => { 52 const closeWindow = vi.fn().mockResolvedValue(undefined); 53 const mockPage = { closeWindow } as any; 54 55 // Mock shouldUseBrowserSession to return true 56 vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true); 57 58 // Mock browserSession to invoke the callback with our mock page 59 vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => { 60 return fn(mockPage); 61 }); 62 63 const cmd = cli({ 64 site: 'test-execution', 65 name: 'browser-close-on-error', 66 description: 'test closeWindow on failure', 67 browser: true, 68 strategy: Strategy.PUBLIC, 69 func: async () => { throw new Error('adapter failure'); }, 70 }); 71 72 await expect(executeCommand(cmd, {})).rejects.toThrow('adapter failure'); 73 expect(closeWindow).toHaveBeenCalledTimes(1); 74 75 vi.restoreAllMocks(); 76 }); 77 78 it('skips closeWindow when OPENCLI_LIVE=1 (success path)', async () => { 79 const closeWindow = vi.fn().mockResolvedValue(undefined); 80 const mockPage = { closeWindow } as any; 81 82 vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true); 83 vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage)); 84 85 const prev = process.env.OPENCLI_LIVE; 86 process.env.OPENCLI_LIVE = '1'; 87 try { 88 const cmd = cli({ 89 site: 'test-execution', 90 name: 'browser-live-success', 91 description: 'test closeWindow skipped with --live on success', 92 browser: true, 93 strategy: Strategy.PUBLIC, 94 func: async () => [{ ok: true }], 95 }); 96 97 await executeCommand(cmd, {}); 98 expect(closeWindow).not.toHaveBeenCalled(); 99 } finally { 100 if (prev === undefined) delete process.env.OPENCLI_LIVE; 101 else process.env.OPENCLI_LIVE = prev; 102 vi.restoreAllMocks(); 103 } 104 }); 105 106 it('skips closeWindow when OPENCLI_LIVE=1 (failure path)', async () => { 107 const closeWindow = vi.fn().mockResolvedValue(undefined); 108 const mockPage = { closeWindow } as any; 109 110 vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true); 111 vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage)); 112 113 const prev = process.env.OPENCLI_LIVE; 114 process.env.OPENCLI_LIVE = '1'; 115 try { 116 const cmd = cli({ 117 site: 'test-execution', 118 name: 'browser-live-failure', 119 description: 'test closeWindow skipped with --live on failure', 120 browser: true, 121 strategy: Strategy.PUBLIC, 122 func: async () => { throw new Error('adapter failure'); }, 123 }); 124 125 await expect(executeCommand(cmd, {})).rejects.toThrow('adapter failure'); 126 expect(closeWindow).not.toHaveBeenCalled(); 127 } finally { 128 if (prev === undefined) delete process.env.OPENCLI_LIVE; 129 else process.env.OPENCLI_LIVE = prev; 130 vi.restoreAllMocks(); 131 } 132 }); 133 134 it('does not re-run custom validation when args are already prepared', async () => { 135 const validateArgs = vi.fn(); 136 const cmd: CliCommand = { 137 site: 'test-execution', 138 name: 'prepared-validation', 139 description: 'test prepared validation path', 140 browser: false, 141 strategy: Strategy.PUBLIC, 142 args: [], 143 validateArgs, 144 func: async () => [], 145 }; 146 147 const kwargs = prepareCommandArgs(cmd, {}); 148 await executeCommand(cmd, kwargs, false, { prepared: true }); 149 150 expect(validateArgs).toHaveBeenCalledTimes(1); 151 }); 152 });