/ src / execution.test.ts
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  });