Skip to main content
Glama
validateArchitectTool.test.ts21.6 kB
/** * ValidateArchitectTool Tests * * TESTING PATTERNS: * - Test tool metadata (name, description, schema) * - Test successful execution with valid inputs * - Test error handling with invalid inputs * - Test YAML syntax error handling with line numbers * - Test schema validation errors with fix suggestions * - Mock external dependencies for isolation * * CODING STANDARDS: * - Use describe blocks to group related tests * - Use it with 'should...' naming pattern * - Test input validation and edge cases * - Verify ToolResult structure * - Check both success and error paths * - Mock external dependencies */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { ValidateArchitectTool } from '../../src/tools'; import * as fs from 'node:fs/promises'; import { TemplatesManagerService } from '@agiflowai/aicode-utils'; // Mock fs/promises vi.mock('node:fs/promises', () => ({ access: vi.fn(), readFile: vi.fn(), stat: vi.fn(), })); // Mock TemplatesManagerService vi.mock('@agiflowai/aicode-utils', () => ({ TemplatesManagerService: { findTemplatesPath: vi.fn().mockResolvedValue('/mock/templates'), }, })); const mockFindTemplatesPath = vi.mocked(TemplatesManagerService.findTemplatesPath); interface ValidationFeature { name: string; design_pattern: string; includes_count: number; } interface ValidationError { type: string; message?: string; fix_suggestion?: string; } interface ValidationResult { valid: boolean; features_count?: number; features?: ValidationFeature[]; errors?: ValidationError[]; file_path?: string; } // Mock workspace root for path validation tests const MOCK_WORKSPACE_ROOT = '/mock/workspace'; describe('ValidateArchitectTool', () => { let tool: ValidateArchitectTool; const mockAccess = fs.access as ReturnType<typeof vi.fn>; const mockReadFile = fs.readFile as ReturnType<typeof vi.fn>; const mockStat = fs.stat as ReturnType<typeof vi.fn>; const originalCwd = process.cwd; beforeEach(() => { vi.clearAllMocks(); // Mock process.cwd to return a predictable path for validation process.cwd = (): string => MOCK_WORKSPACE_ROOT; tool = new ValidateArchitectTool(); }); afterEach(() => { vi.restoreAllMocks(); process.cwd = originalCwd; }); describe('metadata', () => { it('should have correct tool name', () => { const definition = tool.getDefinition(); expect(definition.name).toBe(ValidateArchitectTool.TOOL_NAME); expect(definition.name).toBe('validate-architect'); }); it('should have description containing validate', () => { const definition = tool.getDefinition(); expect(definition.description.toLowerCase()).toContain('validate'); }); it('should have input schema with file_path and template_name properties', () => { const definition = tool.getDefinition(); expect(definition.inputSchema).toBeDefined(); expect(definition.inputSchema.properties).toHaveProperty('file_path'); expect(definition.inputSchema.properties).toHaveProperty('template_name'); }); it('should not require any inputs (both are optional)', () => { const definition = tool.getDefinition(); expect(definition.inputSchema.required).toBeUndefined(); }); }); describe('constructor', () => { it('should create instance', () => { expect(tool).toBeInstanceOf(ValidateArchitectTool); }); }); describe('validate valid architect.yaml', () => { const validYaml = ` features: - name: service-pattern design_pattern: "Service Layer Pattern" includes: - "src/services/**/*.ts" description: | Services encapsulate business logic. - name: controller-pattern design_pattern: "Controller Pattern" includes: - "src/controllers/**/*.ts" `; it('should validate valid architect.yaml file', async () => { mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue(validYaml); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/test/architect.yaml` }); expect(result.isError).toBeFalsy(); const content = result.content[0]; expect(content.type).toBe('text'); if (content.type === 'text') { const data: ValidationResult = JSON.parse(content.text); expect(data.valid).toBe(true); expect(data.features_count).toBe(2); expect(data.features).toHaveLength(2); expect(data.features?.[0].name).toBe('service-pattern'); expect(data.features?.[0].design_pattern).toBe('Service Layer Pattern'); expect(data.features?.[0].includes_count).toBe(1); } }); it('should validate empty features array', async () => { mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue('features: []'); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/test/architect.yaml` }); expect(result.isError).toBeFalsy(); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.valid).toBe(true); expect(data.features_count).toBe(0); } }); it('should validate empty file (defaults to empty features)', async () => { mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue(''); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/test/architect.yaml` }); expect(result.isError).toBeFalsy(); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.valid).toBe(true); expect(data.features_count).toBe(0); } }); }); describe('YAML syntax errors with line numbers', () => { it('should catch YAML indentation errors', async () => { const invalidYaml = ` features: - name: test design_pattern: "Bad indentation" includes: - "src/**/*.ts" `; mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue(invalidYaml); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/test/architect.yaml` }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.valid).toBe(false); expect(data.errors).toHaveLength(1); expect(data.errors[0].type).toBe('yaml_syntax'); expect(data.errors[0].fix_suggestion.toLowerCase()).toContain('indentation'); } }); it('should catch YAML duplicate key errors', async () => { const invalidYaml = ` features: - name: test name: duplicate design_pattern: "Test" includes: - "src/**/*.ts" `; mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue(invalidYaml); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/test/architect.yaml` }); // Note: js-yaml may or may not catch duplicate keys depending on version // This test verifies our error handling works regardless const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); // Either valid (last value wins) or error - both are acceptable yaml behaviors expect(typeof data.valid).toBe('boolean'); } }); it('should catch YAML missing colon errors', async () => { const invalidYaml = ` features: - name test design_pattern: "Test" `; mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue(invalidYaml); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/test/architect.yaml` }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.valid).toBe(false); expect(data.errors[0].type).toBe('yaml_syntax'); } }); }); describe('schema validation errors', () => { it('should catch missing design_pattern field', async () => { const invalidYaml = ` features: - name: test-pattern includes: - "src/**/*.ts" `; mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue(invalidYaml); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/test/architect.yaml` }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.valid).toBe(false); expect(data.errors.length).toBeGreaterThan(0); expect(data.errors[0].type).toBe('schema_validation'); expect(data.errors[0].fix_suggestion).toContain('design_pattern'); } }); it('should catch empty includes array', async () => { const invalidYaml = ` features: - name: test-pattern design_pattern: "Test Pattern" includes: [] `; mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue(invalidYaml); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/test/architect.yaml` }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.valid).toBe(false); expect(data.errors.length).toBeGreaterThan(0); expect(data.errors[0].type).toBe('schema_validation'); expect(data.errors[0].fix_suggestion.toLowerCase()).toContain('includes'); } }); it('should provide fix suggestions for schema errors', async () => { const invalidYaml = ` features: - name: test design_pattern: "" includes: - "src/**/*.ts" `; mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue(invalidYaml); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/test/architect.yaml` }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.valid).toBe(false); expect(data.errors[0].fix_suggestion).toBeDefined(); expect(data.errors[0].fix_suggestion.length).toBeGreaterThan(0); } }); }); describe('file_not_found errors', () => { it('should handle file not found', async () => { mockAccess.mockRejectedValue(new Error('ENOENT')); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/nonexistent/architect.yaml` }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.valid).toBe(false); expect(data.errors[0].type).toBe('file_not_found'); expect(data.errors[0].fix_suggestion).toContain('Create the file'); } }); it('should handle missing templates directory when using template_name', async () => { mockFindTemplatesPath.mockResolvedValue(null); const result = await tool.execute({ template_name: 'nonexistent-template' }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.valid).toBe(false); expect(data.errors[0].type).toBe('file_not_found'); expect(data.errors[0].message).toContain('Templates directory not found'); } }); }); describe('resolve template names correctly', () => { it('should resolve template name to architect.yaml path', async () => { // Re-setup the mock for templates path after clearAllMocks mockFindTemplatesPath.mockResolvedValue('/mock/templates'); // First access for template dir, second for architect file, third for readFile check mockAccess .mockResolvedValueOnce(undefined) // template dir exists .mockResolvedValueOnce(undefined) // .architect.yaml exists .mockResolvedValue(undefined); // file access for parseArchitectFile mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue('features: []'); const result = await tool.execute({ template_name: 'nextjs-15' }); expect(result.isError).toBeFalsy(); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.valid).toBe(true); expect(data.file_path).toContain('nextjs-15'); } }); it('should handle template without architect.yaml', async () => { // Re-setup the mock for templates path after clearAllMocks mockFindTemplatesPath.mockResolvedValue('/mock/templates'); mockAccess .mockResolvedValueOnce(undefined) // template dir exists .mockRejectedValueOnce(new Error('ENOENT')) // .architect.yaml doesn't exist .mockRejectedValueOnce(new Error('ENOENT')); // architect.yaml doesn't exist const result = await tool.execute({ template_name: 'template-no-architect' }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.valid).toBe(false); expect(data.errors[0].type).toBe('file_not_found'); expect(data.errors[0].message).toContain('No architect.yaml'); } }); }); describe('missing input validation', () => { it('should require either file_path or template_name', async () => { const result = await tool.execute({}); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.valid).toBe(false); expect(data.errors[0].type).toBe('missing_input'); expect(data.errors[0].message).toContain('Either file_path or template_name'); } }); }); describe('edge cases', () => { it('should handle absolute file path', async () => { mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue('features: []'); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/absolute/path/to/architect.yaml`, }); expect(result.isError).toBeFalsy(); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.file_path).toBe(`${MOCK_WORKSPACE_ROOT}/absolute/path/to/architect.yaml`); } }); it('should handle relative file path', async () => { mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue('features: []'); const result = await tool.execute({ file_path: 'templates/nextjs-15/architect.yaml', }); expect(result.isError).toBeFalsy(); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.file_path).toContain('templates/nextjs-15/architect.yaml'); } }); it('should handle .architect.yaml filename', async () => { mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue('features: []'); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/test/.architect.yaml`, }); expect(result.isError).toBeFalsy(); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.valid).toBe(true); } }); it('should return JSON parseable content', async () => { mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue('features: []'); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/test/architect.yaml` }); const content = result.content[0]; if (content.type === 'text') { expect(() => JSON.parse(content.text)).not.toThrow(); } }); it('should include file_path in success result', async () => { mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue('features: []'); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/test/architect.yaml` }); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); expect(data.file_path).toBeDefined(); } }); it('should include file_path in error result when available', async () => { mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue('invalid: yaml: syntax:'); const result = await tool.execute({ file_path: `${MOCK_WORKSPACE_ROOT}/test/architect.yaml` }); const content = result.content[0]; if (content.type === 'text') { const data = JSON.parse(content.text); if (!data.valid) { expect(data.file_path).toBeDefined(); } } }); }); describe('path traversal security', () => { it('should reject paths outside workspace directory', async () => { // Path traversal attempt using .. const result = await tool.execute({ file_path: '../../../etc/passwd' }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === 'text') { const data: ValidationResult = JSON.parse(content.text); expect(data.valid).toBe(false); expect(data.errors?.[0].message).toContain('outside the workspace'); } }); it('should reject absolute paths outside workspace', async () => { // Attempt to access file outside workspace using absolute path const result = await tool.execute({ file_path: '/etc/passwd' }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === 'text') { const data: ValidationResult = JSON.parse(content.text); expect(data.valid).toBe(false); expect(data.errors?.[0].message).toContain('outside the workspace'); } }); it('should allow paths within templates directory even outside workspace', async () => { // Mock templates in a different location mockFindTemplatesPath.mockResolvedValue('/different/templates'); mockAccess.mockResolvedValue(undefined); mockStat.mockResolvedValue({ isFile: (): boolean => true }); mockReadFile.mockResolvedValue('features: []'); const result = await tool.execute({ file_path: '/different/templates/nextjs/architect.yaml' }); expect(result.isError).toBeFalsy(); const content = result.content[0]; if (content.type === 'text') { const data: ValidationResult = JSON.parse(content.text); expect(data.valid).toBe(true); } }); it('should reject nested path traversal attempts', async () => { // Multiple levels of .. to escape workspace const result = await tool.execute({ file_path: 'foo/../../bar/../../../etc/passwd' }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === 'text') { const data: ValidationResult = JSON.parse(content.text); expect(data.valid).toBe(false); expect(data.errors?.[0].message).toContain('outside the workspace'); } }); it('should reject paths that start inside but escape workspace', async () => { // Starts with valid directory but escapes via .. const result = await tool.execute({ file_path: 'templates/../../../etc/passwd' }); expect(result.isError).toBe(true); const content = result.content[0]; if (content.type === 'text') { const data: ValidationResult = JSON.parse(content.text); expect(data.valid).toBe(false); expect(data.errors?.[0].message).toContain('outside the workspace'); } }); // Note: Windows-specific path tests (D:\, UNC paths) are platform-dependent: // - On Unix: backslashes are valid filename characters, so 'D:\file' is a relative path // - On Windows: backslashes are separators, so 'D:\file' is absolute on D: drive // The path.isAbsolute() check in the implementation handles both cases correctly, // but we can only test the Unix behavior here. Windows CI would test Windows behavior. }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AgiFlow/aicode-toolkit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server