Files
ccs/tests/unit/commands/cliproxy-catalog-json.test.ts
T
Sergey Galuza 22acd96ba4 feat(cliproxy): add thinking config to catalog --json and test false values
- Include thinking support configuration (type, min, max, levels) in
  JSON output for reasoning-capable models
- Add test verifying explicit false booleans are preserved (not omitted)
- Add test verifying thinking config appears for thinking models

Built [OnSteroids](https://onsteroids.ai)
2026-04-11 20:12:46 +02:00

147 lines
4.8 KiB
TypeScript

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<string, unknown[]>;
// 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<Record<string, unknown>>) {
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<Record<string, unknown>>
>;
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<Record<string, unknown>>
>;
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<Record<string, unknown>>
>;
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<Record<string, unknown>>
>;
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<string, unknown>;
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');
});
});