import { describe, expect, test } from '@jest/globals';
import { ArgumentSchemaValidator } from '../../../../src/engine/execution/parsers/argument-schema.js';
import type { ConvertedPrompt } from '../../../../src/shared/types/index.js';
const basePrompt: ConvertedPrompt = {
id: 'schema_prompt',
name: 'Schema Prompt',
description: '',
category: 'general',
userMessageTemplate: 'Schema prompt',
arguments: [],
};
describe('ArgumentSchemaValidator', () => {
describe('basic validation', () => {
test('passes validation when arguments match metadata', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'topic',
required: true,
type: 'string',
validation: {
minLength: 3,
pattern: '^[A-Za-z ]+$',
},
},
{
name: 'iterations',
required: false,
type: 'number',
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, {
topic: 'Schema Validation',
iterations: 2,
});
expect(result.success).toBe(true);
expect(result.issues).toHaveLength(0);
});
test('returns schema issues when arguments violate constraints', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'topic',
required: true,
type: 'string',
validation: {
minLength: 2,
},
},
{
name: 'iterations',
required: true,
type: 'number',
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, {
topic: 'ok',
iterations: 'not-a-number',
});
expect(result.success).toBe(false);
expect(result.issues).toEqual(
expect.arrayContaining([expect.objectContaining({ argument: 'iterations' })])
);
});
test('passes validation for prompts without arguments', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, {});
expect(result.success).toBe(true);
expect(result.issues).toHaveLength(0);
});
test('passes validation for prompts without validation rules', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{ name: 'topic', required: true, type: 'string' },
{ name: 'count', required: false, type: 'number' },
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { topic: 'x', count: 5 });
expect(result.success).toBe(true);
expect(result.issues).toHaveLength(0);
});
});
describe('minLength validation', () => {
test('fails when input is too short', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'topic',
required: true,
type: 'string',
validation: { minLength: 10 },
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { topic: 'short' });
expect(result.success).toBe(false);
expect(result.issues).toHaveLength(1);
expect(result.issues[0].argument).toBe('topic');
expect(result.issues[0].message).toContain('at least 10 characters');
});
test('passes when input is at minimum length', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'topic',
required: true,
type: 'string',
validation: { minLength: 5 },
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { topic: 'exact' });
expect(result.success).toBe(true);
expect(result.issues).toHaveLength(0);
});
test('passes when input is above minimum length', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'topic',
required: true,
type: 'string',
validation: { minLength: 3 },
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { topic: 'well above minimum' });
expect(result.success).toBe(true);
expect(result.issues).toHaveLength(0);
});
});
describe('maxLength validation', () => {
test('fails when input is too long', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'topic',
required: true,
type: 'string',
validation: { maxLength: 5 },
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { topic: 'way too long' });
expect(result.success).toBe(false);
expect(result.issues).toHaveLength(1);
expect(result.issues[0].argument).toBe('topic');
expect(result.issues[0].message).toContain('no more than 5 characters');
});
test('passes when input is at maximum length', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'topic',
required: true,
type: 'string',
validation: { maxLength: 5 },
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { topic: 'exact' });
expect(result.success).toBe(true);
expect(result.issues).toHaveLength(0);
});
test('passes when input is below maximum length', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'topic',
required: true,
type: 'string',
validation: { maxLength: 100 },
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { topic: 'short' });
expect(result.success).toBe(true);
expect(result.issues).toHaveLength(0);
});
});
describe('pattern validation', () => {
test('fails when input does not match URL pattern', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'url',
required: true,
type: 'string',
validation: { pattern: '^https://' },
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { url: 'not-a-url' });
expect(result.success).toBe(false);
expect(result.issues).toHaveLength(1);
expect(result.issues[0].argument).toBe('url');
expect(result.issues[0].message).toContain('match pattern');
});
test('passes when input matches URL pattern', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'url',
required: true,
type: 'string',
validation: { pattern: '^https://' },
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { url: 'https://example.com' });
expect(result.success).toBe(true);
expect(result.issues).toHaveLength(0);
});
test('passes when input matches regex pattern', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'code',
required: true,
type: 'string',
validation: { pattern: '^[A-Z]{3}-[0-9]{4}$' },
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { code: 'ABC-1234' });
expect(result.success).toBe(true);
expect(result.issues).toHaveLength(0);
});
test('fails when input does not match regex pattern', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'code',
required: true,
type: 'string',
validation: { pattern: '^[A-Z]{3}-[0-9]{4}$' },
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { code: 'invalid' });
expect(result.success).toBe(false);
expect(result.issues).toHaveLength(1);
expect(result.issues[0].argument).toBe('code');
});
test('handles invalid regex gracefully', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'topic',
required: true,
type: 'string',
validation: { pattern: '[invalid(regex' },
},
],
};
const validator = new ArgumentSchemaValidator();
// Should not throw, just skip invalid regex
const result = validator.validate(prompt, { topic: 'anything' });
expect(result.success).toBe(true);
});
});
describe('combined validation', () => {
test('reports multiple errors when both minLength and pattern fail', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'url',
required: true,
type: 'string',
validation: { minLength: 20, pattern: '^https://' },
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { url: 'http://x' });
expect(result.success).toBe(false);
// At least one issue should be reported (may combine or report separately)
expect(result.issues.length).toBeGreaterThan(0);
});
test('reports single error when one passes, one fails', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'url',
required: true,
type: 'string',
validation: { minLength: 5, pattern: '^https://' },
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { url: 'http://example.com' });
expect(result.success).toBe(false);
expect(result.issues).toHaveLength(1);
expect(result.issues[0].message).toContain('pattern');
});
});
describe('allowedValues deprecation', () => {
test('ignores allowedValues constraint (deprecated in v3.0.0)', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'priority',
required: true,
type: 'string',
validation: { allowedValues: ['low', 'medium', 'high'] },
},
],
};
const validator = new ArgumentSchemaValidator();
// Should pass even with value not in allowedValues - LLM handles variation
const result = validator.validate(prompt, { priority: 'urgent' });
expect(result.success).toBe(true);
expect(result.issues).toHaveLength(0);
});
});
describe('schema caching', () => {
test('caches schema for repeated validations', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'topic',
required: true,
type: 'string',
validation: { minLength: 3 },
},
],
};
const validator = new ArgumentSchemaValidator();
// First validation
const result1 = validator.validate(prompt, { topic: 'first' });
expect(result1.success).toBe(true);
// Second validation should use cached schema
const result2 = validator.validate(prompt, { topic: 'second' });
expect(result2.success).toBe(true);
});
});
describe('error message format', () => {
test('includes argument name in issue', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'myArgument',
required: true,
type: 'string',
validation: { minLength: 100 },
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { myArgument: 'short' });
expect(result.success).toBe(false);
expect(result.issues[0].argument).toBe('myArgument');
});
test('includes clear failure reason in message', () => {
const prompt: ConvertedPrompt = {
...basePrompt,
arguments: [
{
name: 'topic',
required: true,
type: 'string',
validation: { minLength: 10 },
},
],
};
const validator = new ArgumentSchemaValidator();
const result = validator.validate(prompt, { topic: 'short' });
expect(result.success).toBe(false);
expect(result.issues[0].message).toContain('10');
expect(result.issues[0].message).toContain('characters');
});
});
});