Files
dify/cli/src/framework/flags.test.ts

227 lines
7.4 KiB
TypeScript

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)
})
})
})