import { describe, expect, it } from 'vitest' import { UnsupportedArgValueError } from './errors.js' import { Args, Flags, parseArgv } from './flags.js' const meta = { flags: { output: Flags.string({ description: 'output format', char: 'o' }), verbose: Flags.boolean({ description: 'verbose mode', char: 'v' }), count: Flags.integer({ description: 'count', default: 5 }), format: Flags.string({ description: 'format', default: 'text' }), }, args: { name: Args.string({ description: 'name', required: true }), extra: Args.string({ description: 'extra' }), }, } describe('parseArgv', () => { describe('positional args', () => { it('parses required arg', () => { const { args } = parseArgv(['alice'], meta) expect(args.name).toBe('alice') }) it('parses optional arg when provided', () => { const { args } = parseArgv(['alice', 'bonus'], meta) expect(args.name).toBe('alice') expect(args.extra).toBe('bonus') }) it('leaves optional arg undefined when absent', () => { const { args } = parseArgv(['alice'], meta) expect(args.extra).toBeUndefined() }) it('throws on missing required arg', () => { expect(() => parseArgv([], meta)).toThrow('missing required argument: name') }) }) describe('long flags (--flag value)', () => { it('parses string flag with space separator', () => { const { flags } = parseArgv(['alice', '--output', 'json'], meta) expect(flags.output).toBe('json') }) it('parses string flag with = separator', () => { const { flags } = parseArgv(['alice', '--output=yaml'], meta) expect(flags.output).toBe('yaml') }) it('parses boolean flag as true when bare', () => { const { flags } = parseArgv(['alice', '--verbose'], meta) expect(flags.verbose).toBe(true) }) it('parses boolean flag with =true', () => { const { flags } = parseArgv(['alice', '--verbose=true'], meta) expect(flags.verbose).toBe(true) }) it('parses boolean flag with =false', () => { const { flags } = parseArgv(['alice', '--verbose=false'], meta) expect(flags.verbose).toBe(false) }) it('parses integer flag', () => { const { flags } = parseArgv(['alice', '--count', '10'], meta) expect(flags.count).toBe(10) }) it('parses integer flag with = separator', () => { const { flags } = parseArgv(['alice', '--count=42'], meta) expect(flags.count).toBe(42) }) it('throws on non-integer value for integer flag', () => { expect(() => parseArgv(['alice', '--count', 'abc'], meta)).toThrow('expected integer, got "abc"') }) it('throws on invalid boolean value', () => { expect(() => parseArgv(['alice', '--verbose=maybe'], meta)).toThrow('expected boolean, got "maybe"') }) it('throws when non-boolean flag has no value', () => { expect(() => parseArgv(['alice', '--output'], meta)).toThrow('flag --output expects a value') }) it('throws on unknown long flag', () => { expect(() => parseArgv(['alice', '--unknown', 'x'], meta)).toThrow('unknown flag: --unknown') }) }) describe('short flags (-x)', () => { it('parses short boolean flag', () => { const { flags } = parseArgv(['alice', '-v'], meta) expect(flags.verbose).toBe(true) }) it('parses short string flag with space-separated value', () => { const { flags } = parseArgv(['alice', '-o', 'json'], meta) expect(flags.output).toBe('json') }) it('throws when short non-boolean flag has no value', () => { expect(() => parseArgv(['alice', '-o'], meta)).toThrow('flag -o expects a value') }) it('throws on unknown short flag', () => { expect(() => parseArgv(['alice', '-z'], meta)).toThrow('unknown flag: -z') }) }) describe('multiple: true', () => { const multipleMeta = { flags: { label: Flags.stringArray({ description: 'labels' }), output: Flags.string({ description: 'output', char: 'o' }), }, args: {}, } it('collects repeated long flags into an array', () => { const { flags } = parseArgv(['--label', 'foo', '--label', 'bar'], multipleMeta) expect(flags.label).toEqual(['foo', 'bar']) }) it('collects repeated long flags with = separator', () => { const { flags } = parseArgv(['--label=foo', '--label=bar', '--label=baz'], multipleMeta) expect(flags.label).toEqual(['foo', 'bar', 'baz']) }) it('collects repeated short flags into an array', () => { const multipleShortMeta = { flags: { label: Flags.string({ description: 'labels', multiple: true, char: 'l' }) }, args: {}, } const { flags } = parseArgv(['-l', 'foo', '-l', 'bar'], multipleShortMeta) expect(flags.label).toEqual(['foo', 'bar']) }) it('single occurrence still produces array with one element', () => { const { flags } = parseArgv(['--label', 'only'], multipleMeta) expect(flags.label).toEqual(['only']) }) it('absent multiple flag is undefined', () => { const { flags } = parseArgv([], multipleMeta) expect(flags.label).toBeUndefined() }) it('non-multiple flag is not affected', () => { const { flags } = parseArgv(['--output', 'json'], multipleMeta) expect(flags.output).toBe('json') }) }) describe('double-dash (--) separator', () => { it('treats tokens after -- as positional args', () => { const { args, flags } = parseArgv(['alice', '--', '--output', 'json'], meta) expect(flags.output).toBeUndefined() expect(args.extra).toBe('--output') }) }) describe('defaults', () => { it('applies flag default when flag is absent', () => { const { flags } = parseArgv(['alice'], meta) expect(flags.count).toBe(5) expect(flags.format).toBe('text') }) it('does not apply default when flag is provided', () => { const { flags } = parseArgv(['alice', '--count', '99'], meta) expect(flags.count).toBe(99) }) }) describe('options validation', () => { const metaWithOptions = { flags: { mode: Flags.string({ description: 'app mode', options: ['chat', 'workflow', 'completion'] }), }, args: {}, } it('accepts a valid option value', () => { const { flags } = parseArgv(['--mode', 'chat'], metaWithOptions) expect(flags.mode).toBe('chat') }) it('rejects an invalid option value (space form)', () => { expect(() => parseArgv(['--mode', 'chatbot'], metaWithOptions)).toThrow( UnsupportedArgValueError, ) }) it('rejects an invalid option value (= form)', () => { expect(() => parseArgv(['--mode=chatbot'], metaWithOptions)).toThrow( UnsupportedArgValueError, ) }) }) describe('Flags and Args factory', () => { it('Flags.string produces string type definition', () => { const def = Flags.string({ description: 'test' }) expect(def.type).toBe('string') }) it('Flags.boolean produces boolean type definition', () => { const def = Flags.boolean({ description: 'test' }) expect(def.type).toBe('boolean') }) it('Flags.integer produces integer type definition', () => { const def = Flags.integer({ description: 'test' }) expect(def.type).toBe('integer') }) it('Args.string produces an arg definition with required when set', () => { const def = Args.string({ description: 'test', required: true }) expect(def.required).toBe(true) }) }) })