/** * @license * Copyright 2025 Qwen * SPDX-License-Identifier: Apache-2.0 */ import { describe, it, expect, beforeEach } from 'vitest'; import { SubagentValidator } from './validation.js'; import { type SubagentConfig, SubagentError } from './types.js'; describe('SubagentValidator', () => { let validator: SubagentValidator; beforeEach(() => { validator = new SubagentValidator(); }); describe('validateName', () => { it('should accept valid names', () => { const validNames = [ 'test-agent', 'code_reviewer', 'agent123', 'my-helper', ]; for (const name of validNames) { const result = validator.validateName(name); expect(result.isValid).toBe(true); expect(result.errors).toHaveLength(0); } }); it('should reject empty or whitespace names', () => { const invalidNames = ['', ' ', '\t', '\n']; for (const name of invalidNames) { const result = validator.validateName(name); expect(result.isValid).toBe(false); expect(result.errors).toContain('Name is required and cannot be empty'); } }); it('should reject names that are too short', () => { const result = validator.validateName('a'); expect(result.isValid).toBe(false); expect(result.errors).toContain( 'Name must be at least 2 characters long', ); }); it('should reject names that are too long', () => { const longName = 'a'.repeat(51); const result = validator.validateName(longName); expect(result.isValid).toBe(false); expect(result.errors).toContain('Name must be 50 characters or less'); }); it('should reject names with invalid characters', () => { const invalidNames = ['test@agent', 'agent.name', 'test agent', 'agent!']; for (const name of invalidNames) { const result = validator.validateName(name); expect(result.isValid).toBe(false); expect(result.errors).toContain( 'Name can only contain letters, numbers, hyphens, and underscores', ); } }); it('should reject names starting with special characters', () => { const invalidNames = ['-agent', '_agent']; for (const name of invalidNames) { const result = validator.validateName(name); expect(result.isValid).toBe(false); expect(result.errors).toContain( 'Name cannot start with a hyphen or underscore', ); } }); it('should reject names ending with special characters', () => { const invalidNames = ['agent-', 'agent_']; for (const name of invalidNames) { const result = validator.validateName(name); expect(result.isValid).toBe(false); expect(result.errors).toContain( 'Name cannot end with a hyphen or underscore', ); } }); it('should reject reserved names', () => { const reservedNames = [ 'self', 'system', 'user', 'model', 'tool', 'config', 'default', ]; for (const name of reservedNames) { const result = validator.validateName(name); expect(result.isValid).toBe(false); expect(result.errors).toContain( `"${name}" is a reserved name and cannot be used`, ); } }); it('should warn about naming conventions', () => { const result = validator.validateName('TestAgent'); expect(result.isValid).toBe(true); expect(result.warnings).toContain( 'Consider using lowercase names for consistency', ); }); it('should warn about mixed separators', () => { const result = validator.validateName('test-agent_helper'); expect(result.isValid).toBe(true); expect(result.warnings).toContain( 'Consider using either hyphens or underscores consistently, not both', ); }); }); describe('validateSystemPrompt', () => { it('should accept valid system prompts', () => { const validPrompts = [ 'You are a helpful assistant.', 'You are a code reviewer. Analyze the provided code and suggest improvements.', 'Help the user with ${task} by using available tools.', ]; for (const prompt of validPrompts) { const result = validator.validateSystemPrompt(prompt); expect(result.isValid).toBe(true); expect(result.errors).toHaveLength(0); } }); it('should reject empty prompts', () => { const invalidPrompts = ['', ' ', '\t\n']; for (const prompt of invalidPrompts) { const result = validator.validateSystemPrompt(prompt); expect(result.isValid).toBe(false); expect(result.errors).toContain( 'System prompt is required and cannot be empty', ); } }); it('should reject prompts that are too short', () => { const result = validator.validateSystemPrompt('Short'); expect(result.isValid).toBe(false); expect(result.errors).toContain( 'System prompt must be at least 10 characters long', ); }); it('should reject prompts that are too long', () => { const longPrompt = 'a'.repeat(10001); const result = validator.validateSystemPrompt(longPrompt); expect(result.isValid).toBe(false); expect(result.errors).toContain( 'System prompt is too long (>10,000 characters)', ); }); it('should warn about long prompts', () => { const longPrompt = 'a'.repeat(5001); const result = validator.validateSystemPrompt(longPrompt); expect(result.isValid).toBe(true); expect(result.warnings).toContain( 'System prompt is quite long (>5,000 characters), consider shortening', ); }); }); describe('validateTools', () => { it('should accept valid tool arrays', () => { const result = validator.validateTools(['read_file', 'write_file']); expect(result.isValid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should reject non-array inputs', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = validator.validateTools('not-an-array' as any); expect(result.isValid).toBe(false); expect(result.errors).toContain('Tools must be an array of strings'); }); it('should warn about empty arrays', () => { const result = validator.validateTools([]); expect(result.isValid).toBe(true); expect(result.warnings).toContain( 'Empty tools array - subagent will inherit all available tools', ); }); it('should warn about duplicate tools', () => { const result = validator.validateTools([ 'read_file', 'read_file', 'write_file', ]); expect(result.isValid).toBe(true); expect(result.warnings).toContain( 'Duplicate tool names found in tools array', ); }); it('should reject non-string tool names', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = validator.validateTools([123, 'read_file'] as any); expect(result.isValid).toBe(false); expect(result.errors).toContain( 'Tool name must be a string, got: number', ); }); it('should reject empty tool names', () => { const result = validator.validateTools(['', 'read_file']); expect(result.isValid).toBe(false); expect(result.errors).toContain('Tool name cannot be empty'); }); }); describe('validateModelConfig', () => { it('should accept valid model configurations', () => { const validConfigs = [ { model: 'gemini-1.5-pro', temp: 0.7, top_p: 0.9 }, { temp: 0.5 }, { top_p: 1.0 }, {}, ]; for (const config of validConfigs) { const result = validator.validateModelConfig(config); expect(result.isValid).toBe(true); expect(result.errors).toHaveLength(0); } }); it('should reject invalid model names', () => { const result = validator.validateModelConfig({ model: '' }); expect(result.isValid).toBe(false); expect(result.errors).toContain('Model name must be a non-empty string'); }); it('should reject invalid temperature values', () => { const invalidTemps = [-0.1, 2.1, 'not-a-number']; for (const temp of invalidTemps) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = validator.validateModelConfig({ temp: temp as any }); expect(result.isValid).toBe(false); } }); it('should warn about high temperature', () => { const result = validator.validateModelConfig({ temp: 1.5 }); expect(result.isValid).toBe(true); expect(result.warnings).toContain( 'High temperature (>1) may produce very creative but unpredictable results', ); }); it('should reject invalid top_p values', () => { const invalidTopP = [-0.1, 1.1, 'not-a-number']; for (const top_p of invalidTopP) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = validator.validateModelConfig({ top_p: top_p as any }); expect(result.isValid).toBe(false); } }); }); describe('validateRunConfig', () => { it('should accept valid run configurations', () => { const validConfigs = [ { max_time_minutes: 10, max_turns: 20 }, { max_time_minutes: 5 }, { max_turns: 10 }, {}, ]; for (const config of validConfigs) { const result = validator.validateRunConfig(config); expect(result.isValid).toBe(true); expect(result.errors).toHaveLength(0); } }); it('should reject invalid max_time_minutes', () => { const invalidTimes = [0, -1, 'not-a-number']; for (const time of invalidTimes) { const result = validator.validateRunConfig({ // eslint-disable-next-line @typescript-eslint/no-explicit-any max_time_minutes: time as any, }); expect(result.isValid).toBe(false); } }); it('should warn about very long execution times', () => { const result = validator.validateRunConfig({ max_time_minutes: 120 }); expect(result.isValid).toBe(true); expect(result.warnings).toContain( 'Very long execution time (>60 minutes) may cause resource issues', ); }); it('should reject invalid max_turns', () => { const invalidTurns = [0, -1, 1.5, 'not-a-number']; for (const turns of invalidTurns) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = validator.validateRunConfig({ max_turns: turns as any }); expect(result.isValid).toBe(false); } }); it('should warn about high turn limits', () => { const result = validator.validateRunConfig({ max_turns: 150 }); expect(result.isValid).toBe(true); expect(result.warnings).toContain( 'Very high turn limit (>100) may cause long execution times', ); }); }); describe('validateConfig', () => { const validConfig: SubagentConfig = { name: 'test-agent', description: 'A test subagent', systemPrompt: 'You are a helpful assistant.', level: 'project', filePath: '/path/to/test-agent.md', }; it('should accept valid configurations', () => { const result = validator.validateConfig(validConfig); expect(result.isValid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should collect errors from all validation steps', () => { const invalidConfig: SubagentConfig = { name: '', description: '', systemPrompt: '', level: 'project', filePath: '/path/to/invalid.md', }; const result = validator.validateConfig(invalidConfig); expect(result.isValid).toBe(false); expect(result.errors.length).toBeGreaterThan(0); }); it('should collect warnings from all validation steps', () => { const configWithWarnings: SubagentConfig = { ...validConfig, name: 'TestAgent', // Will generate warning about case description: 'A'.repeat(501), // Will generate warning about long description }; const result = validator.validateConfig(configWithWarnings); expect(result.isValid).toBe(true); expect(result.warnings.length).toBeGreaterThan(0); }); }); describe('validateOrThrow', () => { const validConfig: SubagentConfig = { name: 'test-agent', description: 'A test subagent', systemPrompt: 'You are a helpful assistant.', level: 'project', filePath: '/path/to/test-agent.md', }; it('should not throw for valid configurations', () => { expect(() => validator.validateOrThrow(validConfig)).not.toThrow(); }); it('should throw SubagentError for invalid configurations', () => { const invalidConfig: SubagentConfig = { ...validConfig, name: '', }; expect(() => validator.validateOrThrow(invalidConfig)).toThrow( SubagentError, ); expect(() => validator.validateOrThrow(invalidConfig)).toThrow( /Validation failed/, ); }); it('should include subagent name in error', () => { const invalidConfig: SubagentConfig = { ...validConfig, name: '', }; try { validator.validateOrThrow(invalidConfig, 'custom-name'); expect.fail('Should have thrown'); } catch (error) { expect(error).toBeInstanceOf(SubagentError); expect((error as SubagentError).subagentName).toBe('custom-name'); } }); }); });