mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 08:53:22 +00:00
278 lines
11 KiB
TypeScript
278 lines
11 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { join } from 'node:path';
|
|
import { parseGlobalFlags, cliOptsToProgressOptions, DEFAULT_CLI_OPTIONS, setCliOptions, getCliOptions, _resetCliOptionsForTest } from '../src/core/cli-options.ts';
|
|
|
|
describe('parseGlobalFlags', () => {
|
|
test('empty argv → defaults, empty rest', () => {
|
|
const r = parseGlobalFlags([]);
|
|
expect(r.cliOpts).toEqual(DEFAULT_CLI_OPTIONS);
|
|
expect(r.rest).toEqual([]);
|
|
});
|
|
|
|
test('strips --quiet from argv and sets quiet=true', () => {
|
|
// Per-command handlers that historically parsed their own --quiet
|
|
// (skillpack-check) now read the resolved CliOptions singleton via
|
|
// getCliOptions() — see src/core/cli-options.ts.
|
|
const r = parseGlobalFlags(['--quiet', 'doctor', '--fast']);
|
|
expect(r.cliOpts.quiet).toBe(true);
|
|
expect(r.cliOpts.progressJson).toBe(false);
|
|
expect(r.rest).toEqual(['doctor', '--fast']);
|
|
});
|
|
|
|
test('strips --progress-json from argv', () => {
|
|
const r = parseGlobalFlags(['--progress-json', 'doctor']);
|
|
expect(r.cliOpts.progressJson).toBe(true);
|
|
expect(r.rest).toEqual(['doctor']);
|
|
});
|
|
|
|
test('--progress-interval=500 form', () => {
|
|
const r = parseGlobalFlags(['--progress-interval=500', 'embed']);
|
|
expect(r.cliOpts.progressInterval).toBe(500);
|
|
expect(r.rest).toEqual(['embed']);
|
|
});
|
|
|
|
test('--progress-interval 500 space-separated form', () => {
|
|
const r = parseGlobalFlags(['--progress-interval', '500', 'embed']);
|
|
expect(r.cliOpts.progressInterval).toBe(500);
|
|
expect(r.rest).toEqual(['embed']);
|
|
});
|
|
|
|
test('global flag interleaved mid-argv still stripped', () => {
|
|
const r = parseGlobalFlags(['doctor', '--progress-json', '--fast']);
|
|
expect(r.cliOpts.progressJson).toBe(true);
|
|
expect(r.rest).toEqual(['doctor', '--fast']);
|
|
});
|
|
|
|
test('invalid --progress-interval value passes through (per-command parser can handle it)', () => {
|
|
const r = parseGlobalFlags(['--progress-interval=abc', 'doctor']);
|
|
// Unparseable value → leave the flag in rest, default interval kept.
|
|
expect(r.cliOpts.progressInterval).toBe(DEFAULT_CLI_OPTIONS.progressInterval);
|
|
expect(r.rest).toEqual(['--progress-interval=abc', 'doctor']);
|
|
});
|
|
|
|
test('negative --progress-interval rejected', () => {
|
|
const r = parseGlobalFlags(['--progress-interval=-1', 'doctor']);
|
|
expect(r.cliOpts.progressInterval).toBe(DEFAULT_CLI_OPTIONS.progressInterval);
|
|
expect(r.rest).toContain('--progress-interval=-1');
|
|
});
|
|
|
|
test('unknown flags pass through unchanged', () => {
|
|
const r = parseGlobalFlags(['doctor', '--fast', '--json', '--foo=bar']);
|
|
expect(r.rest).toEqual(['doctor', '--fast', '--json', '--foo=bar']);
|
|
expect(r.cliOpts).toEqual(DEFAULT_CLI_OPTIONS);
|
|
});
|
|
|
|
test('all global flags combined', () => {
|
|
const r = parseGlobalFlags(['--quiet', '--progress-json', '--progress-interval=250', 'sync']);
|
|
expect(r.cliOpts).toEqual({ quiet: true, progressJson: true, progressInterval: 250, timeoutMs: null, explain: false, brain: null });
|
|
expect(r.rest).toEqual(['sync']);
|
|
});
|
|
|
|
// v0.40.4 — --explain flag
|
|
test('--explain sets cliOpts.explain', () => {
|
|
const r = parseGlobalFlags(['--explain', 'search', 'test query']);
|
|
expect(r.cliOpts.explain).toBe(true);
|
|
expect(r.rest).toEqual(['search', 'test query']);
|
|
});
|
|
|
|
test('--explain absent → false default', () => {
|
|
const r = parseGlobalFlags(['search', 'test query']);
|
|
expect(r.cliOpts.explain).toBe(false);
|
|
});
|
|
|
|
test('--explain works in any argv position', () => {
|
|
const r = parseGlobalFlags(['search', '--explain', 'test query']);
|
|
expect(r.cliOpts.explain).toBe(true);
|
|
expect(r.rest).toEqual(['search', 'test query']);
|
|
});
|
|
});
|
|
|
|
describe('getCliOptions / setCliOptions singleton', () => {
|
|
test('defaults when never set', () => {
|
|
_resetCliOptionsForTest();
|
|
expect(getCliOptions()).toEqual(DEFAULT_CLI_OPTIONS);
|
|
});
|
|
|
|
test('setCliOptions applies + getCliOptions returns a copy', () => {
|
|
_resetCliOptionsForTest();
|
|
setCliOptions({ quiet: false, progressJson: true, progressInterval: 250, timeoutMs: null, explain: false, brain: null });
|
|
expect(getCliOptions().progressJson).toBe(true);
|
|
expect(getCliOptions().progressInterval).toBe(250);
|
|
});
|
|
});
|
|
|
|
describe('cli.ts global-flag stripping (integration)', () => {
|
|
const CLI = join(import.meta.dir, '..', 'src', 'cli.ts');
|
|
|
|
test('gbrain --progress-json --version works (global flag stripped before dispatch)', () => {
|
|
const res = spawnSync('bun', [CLI, '--progress-json', '--version'], {
|
|
encoding: 'utf-8',
|
|
env: { ...process.env, NO_COLOR: '1' },
|
|
});
|
|
expect(res.status).toBe(0);
|
|
expect(res.stdout).toContain('gbrain ');
|
|
});
|
|
|
|
test('gbrain --quiet --progress-interval=500 version works (flags interleaved, all stripped)', () => {
|
|
const res = spawnSync('bun', [CLI, '--quiet', '--progress-interval=500', 'version'], {
|
|
encoding: 'utf-8',
|
|
env: { ...process.env, NO_COLOR: '1' },
|
|
});
|
|
expect(res.status).toBe(0);
|
|
expect(res.stdout).toContain('gbrain ');
|
|
});
|
|
});
|
|
|
|
describe('CLI integration: progress streams to the right channel', () => {
|
|
const CLI = join(import.meta.dir, '..', 'src', 'cli.ts');
|
|
|
|
test('gbrain --progress-json --version emits only the version on stdout', () => {
|
|
// `version` is a single-shot command that goes through the main()
|
|
// dispatch path. We want to confirm --progress-json doesn't force
|
|
// stray progress onto stdout for commands that don't use a reporter.
|
|
const res = spawnSync('bun', [CLI, '--progress-json', '--version'], {
|
|
encoding: 'utf-8',
|
|
env: { ...process.env, NO_COLOR: '1' },
|
|
});
|
|
expect(res.status).toBe(0);
|
|
expect(res.stdout.trim()).toMatch(/^gbrain /);
|
|
// No JSON progress object should end up on stdout.
|
|
expect(res.stdout).not.toContain('"event":"start"');
|
|
});
|
|
|
|
test('gbrain --quiet skillpack-check returns exit code with no stdout', () => {
|
|
// Regression guard for the flag-collision that skillpack-check hit
|
|
// when --quiet briefly passed through argv. Now it reads the singleton.
|
|
const res = spawnSync('bun', [CLI, '--quiet', 'skillpack-check'], {
|
|
encoding: 'utf-8',
|
|
env: { ...process.env, NO_COLOR: '1' },
|
|
});
|
|
// Exit may be 0 or 1 depending on whether a brain is configured;
|
|
// what matters is stdout stays empty.
|
|
expect(res.stdout).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('cliOptsToProgressOptions', () => {
|
|
test('--quiet → quiet mode', () => {
|
|
const opts = cliOptsToProgressOptions({ quiet: true, progressJson: false, progressInterval: 1000, timeoutMs: null, explain: false, brain: null });
|
|
expect(opts.mode).toBe('quiet');
|
|
});
|
|
|
|
test('--progress-json → json mode with interval', () => {
|
|
const opts = cliOptsToProgressOptions({ quiet: false, progressJson: true, progressInterval: 500, timeoutMs: null, explain: false, brain: null });
|
|
expect(opts.mode).toBe('json');
|
|
expect(opts.minIntervalMs).toBe(500);
|
|
});
|
|
|
|
test('defaults → auto mode', () => {
|
|
const opts = cliOptsToProgressOptions(DEFAULT_CLI_OPTIONS);
|
|
expect(opts.mode).toBe('auto');
|
|
expect(opts.minIntervalMs).toBe(1000);
|
|
});
|
|
|
|
test('quiet takes priority over progressJson', () => {
|
|
const opts = cliOptsToProgressOptions({ quiet: true, progressJson: true, progressInterval: 1000, timeoutMs: null, explain: false, brain: null });
|
|
expect(opts.mode).toBe('quiet');
|
|
});
|
|
});
|
|
|
|
// v0.31.1: --timeout flag tests
|
|
describe('--timeout flag', () => {
|
|
test('--timeout=30s → 30000ms', () => {
|
|
const r = parseGlobalFlags(['--timeout=30s', 'search', 'X']);
|
|
expect(r.cliOpts.timeoutMs).toBe(30_000);
|
|
expect(r.rest).toEqual(['search', 'X']);
|
|
});
|
|
|
|
test('--timeout 1.5s → 1500ms', () => {
|
|
const r = parseGlobalFlags(['--timeout', '1.5s', 'search']);
|
|
expect(r.cliOpts.timeoutMs).toBe(1500);
|
|
expect(r.rest).toEqual(['search']);
|
|
});
|
|
|
|
test('--timeout=2m → 120000ms', () => {
|
|
const r = parseGlobalFlags(['--timeout=2m']);
|
|
expect(r.cliOpts.timeoutMs).toBe(120_000);
|
|
});
|
|
|
|
test('--timeout=500ms → 500ms', () => {
|
|
const r = parseGlobalFlags(['--timeout=500ms']);
|
|
expect(r.cliOpts.timeoutMs).toBe(500);
|
|
});
|
|
|
|
test('--timeout=500 (bare number, default ms)', () => {
|
|
const r = parseGlobalFlags(['--timeout=500']);
|
|
expect(r.cliOpts.timeoutMs).toBe(500);
|
|
});
|
|
|
|
test('--timeout=garbage → falls through, timeoutMs stays null', () => {
|
|
const r = parseGlobalFlags(['--timeout=garbage', 'search']);
|
|
expect(r.cliOpts.timeoutMs).toBe(null);
|
|
expect(r.rest).toContain('--timeout=garbage');
|
|
});
|
|
|
|
test('--timeout=0 rejected (must be positive)', () => {
|
|
const r = parseGlobalFlags(['--timeout=0']);
|
|
expect(r.cliOpts.timeoutMs).toBe(null);
|
|
expect(r.rest).toContain('--timeout=0');
|
|
});
|
|
|
|
test('default timeoutMs is null (per-command default applies)', () => {
|
|
const r = parseGlobalFlags(['search', 'X']);
|
|
expect(r.cliOpts.timeoutMs).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe('--brain flag (brain axis routing)', () => {
|
|
test('--brain <id> space form: parsed + stripped from rest', () => {
|
|
const r = parseGlobalFlags(['query', 'X', '--brain', 'media-team']);
|
|
expect(r.cliOpts.brain).toBe('media-team');
|
|
expect(r.rest).toEqual(['query', 'X']);
|
|
});
|
|
|
|
test('--brain=<id> equals form: parsed + stripped from rest', () => {
|
|
const r = parseGlobalFlags(['--brain=media-team', 'query', 'X']);
|
|
expect(r.cliOpts.brain).toBe('media-team');
|
|
expect(r.rest).toEqual(['query', 'X']);
|
|
});
|
|
|
|
test('--brain host is a valid explicit value', () => {
|
|
const r = parseGlobalFlags(['stats', '--brain', 'host']);
|
|
expect(r.cliOpts.brain).toBe('host');
|
|
});
|
|
|
|
test('missing value throws (loud, never a silent host fallback)', () => {
|
|
expect(() => parseGlobalFlags(['query', 'X', '--brain'])).toThrow(/--brain requires a value/);
|
|
expect(() => parseGlobalFlags(['--brain=', 'query'])).toThrow(/--brain requires a value/);
|
|
// A following flag is not a value.
|
|
expect(() => parseGlobalFlags(['--brain', '--quiet'])).toThrow(/--brain requires a value/);
|
|
});
|
|
|
|
test('malformed id throws (validated at parse time)', () => {
|
|
expect(() => parseGlobalFlags(['--brain', 'Bad_Id!'])).toThrow(/Invalid --brain value/);
|
|
expect(() => parseGlobalFlags(['--brain=$(rm -rf /)'])).toThrow(/Invalid --brain value/);
|
|
});
|
|
|
|
test('--brain-* per-command flags pass through untouched (skillopt collision guard)', () => {
|
|
const r = parseGlobalFlags(['skillopt', '--brain-wide-max-cost-usd', '5']);
|
|
expect(r.cliOpts.brain).toBe(null);
|
|
expect(r.rest).toEqual(['skillopt', '--brain-wide-max-cost-usd', '5']);
|
|
});
|
|
|
|
test('default brain is null (ambient resolution applies)', () => {
|
|
const r = parseGlobalFlags(['query', 'X']);
|
|
expect(r.cliOpts.brain).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe('childGlobalFlags propagates --brain', () => {
|
|
test('explicit brain rides into child gbrain subprocess commands', async () => {
|
|
const { childGlobalFlags } = await import('../src/core/cli-options.ts');
|
|
expect(childGlobalFlags({ ...DEFAULT_CLI_OPTIONS, brain: 'media-team' }))
|
|
.toContain('--brain=media-team');
|
|
expect(childGlobalFlags({ ...DEFAULT_CLI_OPTIONS })).not.toContain('--brain');
|
|
});
|
|
});
|