/** * CLIResult Tests * * Tests for rich assertion API and error handling. */ import { describe, expect, test } from 'bun:test'; import { z } from 'zod'; import { CLIResultImpl } from './cli-result'; describe('CLIResult', () => { describe('expectSuccess', () => { test('passes when exit code is 0', () => { const result = new CLIResultImpl('test', 'output', '', 0, 100, false); expect(() => result.expectSuccess()).not.toThrow(); }); test('throws when exit code is non-zero', () => { const result = new CLIResultImpl('test', 'output', 'error', 1, 100, false); expect(() => result.expectSuccess()).toThrow('Expected command to succeed'); expect(() => result.expectSuccess()).toThrow('exit code 1'); }); test('returns this for chaining', () => { const result = new CLIResultImpl('test', 'output', '', 0, 100, false); expect(result.expectSuccess()).toBe(result); }); }); describe('expectFailure', () => { test('passes when exit code is non-zero', () => { const result = new CLIResultImpl('test', '', 'error', 1, 100, false); expect(() => result.expectFailure()).not.toThrow(); }); test('throws when exit code is 0', () => { const result = new CLIResultImpl('test', 'output', '', 0, 100, false); expect(() => result.expectFailure()).toThrow('Expected command to fail'); }); test('returns this for chaining', () => { const result = new CLIResultImpl('test', '', 'error', 1, 100, false); expect(result.expectFailure()).toBe(result); }); }); describe('expectStdout', () => { test('passes when stdout contains string', () => { const result = new CLIResultImpl('test', 'hello world', '', 0, 100, false); expect(() => result.expectStdout('hello')).not.toThrow(); }); test('passes when stdout matches regex', () => { const result = new CLIResultImpl('test', 'hello world', '', 0, 100, false); expect(() => result.expectStdout(/w[o]rld/)).not.toThrow(); }); test('throws when stdout does not match', () => { const result = new CLIResultImpl('test', 'hello world', '', 0, 100, false); expect(() => result.expectStdout('goodbye')).toThrow('Expected stdout to match'); }); test('returns this for chaining', () => { const result = new CLIResultImpl('test', 'hello', '', 0, 100, false); expect(result.expectStdout('hello')).toBe(result); }); }); describe('expectStderr', () => { test('passes when stderr contains string', () => { const result = new CLIResultImpl('test', '', 'error message', 1, 100, false); expect(() => result.expectStderr('error')).not.toThrow(); }); test('passes when stderr matches regex', () => { const result = new CLIResultImpl('test', '', 'error message', 1, 100, false); expect(() => result.expectStderr(/err[o]r/)).not.toThrow(); }); test('throws when stderr does not match', () => { const result = new CLIResultImpl('test', '', 'error message', 1, 100, false); expect(() => result.expectStderr('warning')).toThrow('Expected stderr to match'); }); }); describe('expectJson', () => { test('parses and validates JSON', () => { const json = JSON.stringify({ name: 'test', count: 42 }); const result = new CLIResultImpl('test', json, '', 0, 100, false); const schema = z.object({ name: z.string(), count: z.number(), }); const data = result.expectJson(schema); expect(data.name).toBe('test'); expect(data.count).toBe(42); }); test('throws on invalid JSON', () => { const result = new CLIResultImpl('test', 'not json', '', 0, 100, false); const schema = z.object({ name: z.string() }); expect(() => result.expectJson(schema)).toThrow('Failed to parse JSON'); }); test('throws on schema validation failure', () => { const json = JSON.stringify({ name: 123 }); // Wrong type const result = new CLIResultImpl('test', json, '', 0, 100, false); const schema = z.object({ name: z.string() }); expect(() => result.expectJson(schema)).toThrow(); }); }); describe('expectNoOutput', () => { test('passes when stdout and stderr are empty', () => { const result = new CLIResultImpl('test', '', '', 0, 100, false); expect(() => result.expectNoOutput()).not.toThrow(); }); test('passes when stdout and stderr contain only whitespace', () => { const result = new CLIResultImpl('test', ' \n ', '\t\n', 0, 100, false); expect(() => result.expectNoOutput()).not.toThrow(); }); test('throws when stdout has content', () => { const result = new CLIResultImpl('test', 'output', '', 0, 100, false); expect(() => result.expectNoOutput()).toThrow('Expected no output'); }); test('throws when stderr has content', () => { const result = new CLIResultImpl('test', '', 'error', 0, 100, false); expect(() => result.expectNoOutput()).toThrow('Expected no output'); }); }); describe('expectDuration', () => { test('passes when duration is under limit', () => { const result = new CLIResultImpl('test', '', '', 0, 100, false); expect(() => result.expectDuration(200)).not.toThrow(); }); test('throws when duration exceeds limit', () => { const result = new CLIResultImpl('test', '', '', 0, 300, false); expect(() => result.expectDuration(200)).toThrow('Expected command to complete in 200ms'); expect(() => result.expectDuration(200)).toThrow('took 300ms'); }); }); describe('lines', () => { test('splits output into lines', () => { const result = new CLIResultImpl('test', 'line1\nline2\nline3', '', 0, 100, false); const lines = result.lines(); expect(lines).toEqual(['line1', 'line2', 'line3']); }); test('filters empty lines', () => { const result = new CLIResultImpl('test', 'line1\n\nline2\n\n', '', 0, 100, false); const lines = result.lines(); expect(lines).toEqual(['line1', 'line2']); }); test('trims trailing whitespace', () => { const result = new CLIResultImpl('test', 'line1 \nline2\t\n', '', 0, 100, false); const lines = result.lines(); expect(lines).toEqual(['line1', 'line2']); }); test('handles empty output', () => { const result = new CLIResultImpl('test', '', '', 0, 100, false); const lines = result.lines(); expect(lines).toEqual([]); }); }); describe('asyncIterator', () => { test('iterates over lines', async () => { const result = new CLIResultImpl('test', 'line1\nline2\nline3', '', 0, 100, false); const lines: string[] = []; for await (const line of result) { lines.push(line); } expect(lines).toEqual(['line1', 'line2', 'line3']); }); }); describe('toJSON', () => { test('exports all properties', () => { const result = new CLIResultImpl( 'module list', 'stdout output', 'stderr output', 1, 1234, false, ); const json = result.toJSON(); expect(json).toEqual({ command: 'module list', exitCode: 1, duration: 1234, timedOut: false, stdout: 'stdout output', stderr: 'stderr output', }); }); test('includes timeout flag when true', () => { const result = new CLIResultImpl('test', '', '', 0, 30000, true); const json = result.toJSON(); expect(json).toMatchObject({ timedOut: true }); }); }); describe('fluent chaining', () => { test('chains multiple assertions', () => { const result = new CLIResultImpl( 'module add homebridge', 'Module added: homebridge\nReady to configure', '', 0, 500, false, ); expect(() => { result .expectSuccess() .expectStdout(/added/) .expectStdout('homebridge') .expectDuration(1000); }).not.toThrow(); }); test('stops at first failed assertion', () => { const result = new CLIResultImpl('test', 'output', '', 1, 100, false); expect(() => { result .expectSuccess() // This will throw .expectStdout('test'); // This won't run }).toThrow('Expected command to succeed'); }); }); describe('error context (execa-inspired)', () => { test('includes command in error messages', () => { const result = new CLIResultImpl('module add homebridge', '', '', 1, 100, false); try { result.expectSuccess(); } catch (error) { expect((error as Error).message).toContain('module add homebridge'); } }); test('includes duration in error messages', () => { const result = new CLIResultImpl('test', '', '', 1, 1234, false); try { result.expectSuccess(); } catch (error) { expect((error as Error).message).toContain('1234ms'); } }); test('includes stdout in error messages', () => { const result = new CLIResultImpl('test', 'actual output', '', 0, 100, false); try { result.expectStdout('expected output'); } catch (error) { expect((error as Error).message).toContain('actual output'); } }); }); });