Skip to content
Open
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
21 changes: 21 additions & 0 deletions src/cli/commands/add/__tests__/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@ describe('validate', () => {
expect(result.error?.includes('Invalid language')).toBeTruthy();
});

// Case-insensitive flag values
it('accepts lowercase flag values and normalizes them', () => {
const result = validateAddAgentOptions({
...validAgentOptionsByo,
framework: 'strands' as any,
modelProvider: 'bedrock' as any,
language: 'python' as any,
});
expect(result.valid).toBe(true);
});

it('accepts uppercase flag values and normalizes them', () => {
const result = validateAddAgentOptions({
...validAgentOptionsByo,
framework: 'STRANDS' as any,
modelProvider: 'BEDROCK' as any,
language: 'PYTHON' as any,
});
expect(result.valid).toBe(true);
});

// AC3: Framework/model provider compatibility
it('returns error for incompatible framework and model provider', () => {
const result = validateAddAgentOptions({
Expand Down
19 changes: 19 additions & 0 deletions src/cli/commands/add/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SDKFrameworkSchema,
TargetLanguageSchema,
getSupportedModelProviders,
matchEnumValue,
} from '../../../schema';
import { validateVpcOptions } from '../shared/vpc-utils';
import type {
Expand All @@ -28,6 +29,19 @@ const VALID_STRATEGIES = ['SEMANTIC', 'SUMMARIZATION', 'USER_PREFERENCE'];

// Agent validation
export function validateAddAgentOptions(options: AddAgentOptions): ValidationResult {
// Normalize enum flag values (case-insensitive matching)
if (options.framework)
options.framework =
(matchEnumValue(SDKFrameworkSchema, options.framework) as typeof options.framework) ?? options.framework;
if (options.modelProvider)
options.modelProvider =
(matchEnumValue(ModelProviderSchema, options.modelProvider) as typeof options.modelProvider) ??
options.modelProvider;
if (options.language)
options.language =
(matchEnumValue(TargetLanguageSchema, options.language) as typeof options.language) ?? options.language;
if (options.build) options.build = matchEnumValue(BuildTypeSchema, options.build) ?? options.build;

if (!options.name) {
return { valid: false, error: '--name is required' };
}
Expand Down Expand Up @@ -159,6 +173,11 @@ export function validateAddGatewayOptions(options: AddGatewayOptions): Validatio

// MCP Tool validation
export function validateAddMcpToolOptions(options: AddMcpToolOptions): ValidationResult {
// Normalize enum flag values (case-insensitive matching)
if (options.language)
options.language =
(matchEnumValue(TargetLanguageSchema, options.language) as typeof options.language) ?? options.language;

if (!options.name) {
return { valid: false, error: '--name is required' };
}
Expand Down
16 changes: 16 additions & 0 deletions src/cli/commands/create/__tests__/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ describe('validateCreateOptions', () => {
expect(result.valid).toBe(true);
});

it('accepts lowercase flag values and normalizes them', () => {
const result = validateCreateOptions(
{ name: 'TestProjLower', language: 'python', framework: 'strands', modelProvider: 'bedrock', memory: 'none' },
testDir
);
expect(result.valid).toBe(true);
});

it('accepts uppercase flag values and normalizes them', () => {
const result = validateCreateOptions(
{ name: 'TestProjUpper', language: 'PYTHON', framework: 'STRANDS', modelProvider: 'BEDROCK', memory: 'none' },
testDir
);
expect(result.valid).toBe(true);
});

// VPC validation tests
it('rejects invalid network mode', () => {
const result = validateCreateOptions(
Expand Down
8 changes: 8 additions & 0 deletions src/cli/commands/create/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
SDKFrameworkSchema,
TargetLanguageSchema,
getSupportedModelProviders,
matchEnumValue,
} from '../../../schema';
import { validateVpcOptions } from '../shared/vpc-utils';
import type { CreateOptions } from './types';
Expand Down Expand Up @@ -51,6 +52,13 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val
return { valid: true };
}

// Normalize enum flag values (case-insensitive matching)
if (options.language) options.language = matchEnumValue(TargetLanguageSchema, options.language) ?? options.language;
if (options.framework) options.framework = matchEnumValue(SDKFrameworkSchema, options.framework) ?? options.framework;
if (options.modelProvider)
options.modelProvider = matchEnumValue(ModelProviderSchema, options.modelProvider) ?? options.modelProvider;
if (options.build) options.build = matchEnumValue(BuildTypeSchema, options.build) ?? options.build;

// Validate build type if provided
if (options.build) {
const buildResult = BuildTypeSchema.safeParse(options.build);
Expand Down
23 changes: 23 additions & 0 deletions src/schema/__tests__/constants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,35 @@ import {
RESERVED_PROJECT_NAMES,
RuntimeVersionSchema,
SDKFrameworkSchema,
TargetLanguageSchema,
getSupportedModelProviders,
isModelProviderSupported,
isReservedProjectName,
matchEnumValue,
} from '../constants.js';
import { describe, expect, it } from 'vitest';

describe('matchEnumValue', () => {
it('returns canonical value for case-insensitive match', () => {
expect(matchEnumValue(SDKFrameworkSchema, 'strands')).toBe('Strands');
expect(matchEnumValue(SDKFrameworkSchema, 'STRANDS')).toBe('Strands');
expect(matchEnumValue(SDKFrameworkSchema, 'Strands')).toBe('Strands');
expect(matchEnumValue(ModelProviderSchema, 'bedrock')).toBe('Bedrock');
expect(matchEnumValue(TargetLanguageSchema, 'python')).toBe('Python');
});

it('returns undefined for non-matching input', () => {
expect(matchEnumValue(SDKFrameworkSchema, 'nonexistent')).toBeUndefined();
expect(matchEnumValue(ModelProviderSchema, 'azure')).toBeUndefined();
});

it('handles multi-word enum values', () => {
expect(matchEnumValue(SDKFrameworkSchema, 'langchain_langgraph')).toBe('LangChain_LangGraph');
expect(matchEnumValue(SDKFrameworkSchema, 'openaiagents')).toBe('OpenAIAgents');
expect(matchEnumValue(SDKFrameworkSchema, 'googleadk')).toBe('GoogleADK');
});
});

describe('SDKFrameworkSchema', () => {
it.each(['Strands', 'LangChain_LangGraph', 'CrewAI', 'GoogleADK', 'OpenAIAgents'])('accepts "%s"', framework => {
expect(SDKFrameworkSchema.safeParse(framework).success).toBe(true);
Expand Down
9 changes: 9 additions & 0 deletions src/schema/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ export type TargetLanguage = z.infer<typeof TargetLanguageSchema>;
export const ModelProviderSchema = z.enum(['Bedrock', 'Gemini', 'OpenAI', 'Anthropic']);
export type ModelProvider = z.infer<typeof ModelProviderSchema>;

/**
* Case-insensitively match a user-provided value against a Zod enum's options.
* Returns the canonical (correctly-cased) value, or undefined if no match.
*/
export function matchEnumValue(schema: { options: readonly string[] }, input: string): string | undefined {
const lower = input.toLowerCase();
return schema.options.find(v => v.toLowerCase() === lower);
}

/**
* Default model IDs used for each provider.
* These are the models generated in agent templates.
Expand Down
Loading