import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; import { handleCatalogJson } from '../../../src/commands/cliproxy/catalog-subcommand'; import { handleCliproxyCommand } from '../../../src/commands/cliproxy/index'; let originalConsoleLog: typeof console.log; let capturedOutput: string[]; beforeEach(() => { originalConsoleLog = console.log; capturedOutput = []; console.log = (...args: unknown[]) => { capturedOutput.push(args.map(String).join(' ')); }; }); afterEach(() => { console.log = originalConsoleLog; }); describe('cliproxy catalog --json output', () => { it('outputs valid JSON mapping provider names to model arrays', () => { handleCatalogJson(); expect(capturedOutput).toHaveLength(1); const parsed = JSON.parse(capturedOutput[0]) as Record; // Must be a non-empty object (static catalog always has providers) expect(typeof parsed).toBe('object'); expect(Object.keys(parsed).length).toBeGreaterThan(0); // Every provider entry must be an array of objects with at least id and name for (const models of Object.values(parsed)) { expect(Array.isArray(models)).toBe(true); expect(models.length).toBeGreaterThan(0); for (const model of models as Array>) { expect(typeof model.id).toBe('string'); expect(typeof model.name).toBe('string'); } } }); it('includes metadata fields when present on model entries', () => { handleCatalogJson(); const parsed = JSON.parse(capturedOutput[0]) as Record< string, Array> >; const allModels = Object.values(parsed).flat(); // At least some models in the static catalog have tier set const withTier = allModels.filter((m) => m.tier !== undefined); expect(withTier.length).toBeGreaterThan(0); // Tier values must be one of the allowed strings for (const model of withTier) { expect(['free', 'pro', 'ultra']).toContain(model.tier); } }); it('omits undefined optional fields instead of including nulls', () => { handleCatalogJson(); const parsed = JSON.parse(capturedOutput[0]) as Record< string, Array> >; const allModels = Object.values(parsed).flat(); for (const model of allModels) { for (const value of Object.values(model)) { expect(value).not.toBeNull(); expect(value).not.toBeUndefined(); } } }); it('includes explicit false boolean values in output', () => { handleCatalogJson(); const parsed = JSON.parse(capturedOutput[0]) as Record< string, Array> >; const allModels = Object.values(parsed).flat(); // Static catalog has models with extendedContext: false const withExplicitFalse = allModels.filter((m) => m.extendedContext === false); expect(withExplicitFalse.length).toBeGreaterThan(0); }); it('includes thinking configuration when present on models', () => { handleCatalogJson(); const parsed = JSON.parse(capturedOutput[0]) as Record< string, Array> >; const allModels = Object.values(parsed).flat(); // Static catalog has thinking models (e.g. Claude Opus 4.6 Thinking) const withThinking = allModels.filter((m) => m.thinking !== undefined); expect(withThinking.length).toBeGreaterThan(0); for (const model of withThinking) { const thinking = model.thinking as Record; expect(['budget', 'levels', 'none']).toContain(thinking.type); } }); it('outputs minified JSON (single line, no whitespace formatting)', () => { handleCatalogJson(); const output = capturedOutput[0]; expect(output.includes('\n')).toBe(false); expect(JSON.stringify(JSON.parse(output))).toBe(output); }); }); describe('cliproxy catalog --json routing', () => { it('routes catalog --json through handleCliproxyCommand', async () => { await handleCliproxyCommand(['catalog', '--json']); expect(capturedOutput).toHaveLength(1); const parsed = JSON.parse(capturedOutput[0]); expect(typeof parsed).toBe('object'); expect(Object.keys(parsed).length).toBeGreaterThan(0); }); it('--json takes priority over refresh subcommand', async () => { await handleCliproxyCommand(['catalog', 'refresh', '--json']); expect(capturedOutput).toHaveLength(1); // Should output JSON, not refresh output const parsed = JSON.parse(capturedOutput[0]); expect(typeof parsed).toBe('object'); }); it('--json takes priority when placed before subcommand', async () => { await handleCliproxyCommand(['catalog', '--json', 'reset']); expect(capturedOutput).toHaveLength(1); const parsed = JSON.parse(capturedOutput[0]); expect(typeof parsed).toBe('object'); }); });