Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions apps/cli/src/__tests__/lib/validate-type-spec.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, test } from 'bun:test';
import { validateValueAgainstTypeSpec } from '../../lib/operation-args';
import { CliError } from '../../lib/errors';
import type { CliTypeSpec } from '../../cli/types';

describe('validateValueAgainstTypeSpec – oneOf const enumeration', () => {
const schema: CliTypeSpec = {
oneOf: [
{ const: 'headerRow' },
{ const: 'totalRow' },
{ const: 'firstColumn' },
{ const: 'lastColumn' },
{ const: 'bandedRows' },
{ const: 'bandedColumns' },
],
};

test('accepts a valid const value', () => {
expect(() => validateValueAgainstTypeSpec('headerRow', schema, 'flag')).not.toThrow();
expect(() => validateValueAgainstTypeSpec('bandedColumns', schema, 'flag')).not.toThrow();
});

test('rejects an invalid value and lists all allowed values', () => {
try {
validateValueAgainstTypeSpec('lastRow', schema, 'tables set-style-option:flag');
throw new Error('Expected CliError to be thrown');
} catch (error) {
expect(error).toBeInstanceOf(CliError);
const cliError = error as CliError;
expect(cliError.code).toBe('VALIDATION_ERROR');
expect(cliError.message).toBe(
'tables set-style-option:flag must be one of: headerRow, totalRow, firstColumn, lastColumn, bandedRows, bandedColumns.',
);
}
});

test('preserves per-variant errors in details', () => {
try {
validateValueAgainstTypeSpec('invalid', schema, 'flag');
throw new Error('Expected CliError to be thrown');
} catch (error) {
const cliError = error as CliError;
const details = cliError.details as { errors: string[] };
expect(details.errors).toBeArrayOfSize(6);
}
});
});

describe('validateValueAgainstTypeSpec – oneOf with mixed schemas', () => {
const mixedSchema: CliTypeSpec = {
oneOf: [{ const: 'block' }, { type: 'object', properties: { kind: { const: 'inline' } }, required: ['kind'] }],
};

test('falls back to generic message when variants are not all const', () => {
try {
validateValueAgainstTypeSpec('nope', mixedSchema, 'target');
throw new Error('Expected CliError to be thrown');
} catch (error) {
const cliError = error as CliError;
expect(cliError.message).toBe('target must match one of the allowed schema variants.');
}
});
});

describe('validateValueAgainstTypeSpec – enum branch', () => {
const enumSchema: CliTypeSpec = {
type: 'string',
enum: ['direct', 'tracked'],
} as CliTypeSpec & { enum: string[] };

test('accepts a valid enum value', () => {
expect(() => validateValueAgainstTypeSpec('direct', enumSchema, 'changeMode')).not.toThrow();
});

test('rejects an invalid enum value with allowed list', () => {
try {
validateValueAgainstTypeSpec('bogus', enumSchema, 'changeMode');
throw new Error('Expected CliError to be thrown');
} catch (error) {
const cliError = error as CliError;
expect(cliError.message).toBe('changeMode must be one of: direct, tracked.');
}
});
});
21 changes: 20 additions & 1 deletion apps/cli/src/lib/operation-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ function acceptsLegacyTextAddressTarget(
return docApiId === 'replace' || docApiId === 'delete' || docApiId?.startsWith('format.') === true;
}

/**
* If every variant in a `oneOf` is a `{ const: X }`, return the values as strings.
* Returns an empty array when the pattern doesn't hold (mixed / nested schemas).
*/
function extractConstValues(variants: CliTypeSpec[]): string[] {
const values: string[] = [];
for (const variant of variants) {
if (!('const' in variant)) return [];
values.push(String(variant.const));
}
return values;
}

export function validateValueAgainstTypeSpec(value: unknown, schema: CliTypeSpec, path: string): void {
if ('const' in schema) {
if (value !== schema.const) {
Expand All @@ -119,7 +132,13 @@ export function validateValueAgainstTypeSpec(value: unknown, schema: CliTypeSpec
errors.push(error instanceof Error ? error.message : String(error));
}
}
throw new CliError('VALIDATION_ERROR', `${path} must match one of the allowed schema variants.`, { errors });

const allowedValues = extractConstValues(variants);
const message =
allowedValues.length > 0
? `${path} must be one of: ${allowedValues.join(', ')}.`
: `${path} must match one of the allowed schema variants.`;
throw new CliError('VALIDATION_ERROR', message, { errors });
}

if (schema.type === 'json') return;
Expand Down
Loading