Skip to main content
Glama

hypertool-mcp

parser.test.tsโ€ข25.8 kB
/** * PersonaParser Test Suite * * Comprehensive tests for YAML parsing functionality, including valid/invalid YAML, * schema validation integration, error handling, file system operations, * and edge cases with malformed content. */ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { promises as fs } from "fs"; import { join } from "path"; import { tmpdir } from "os"; import { parsePersonaYAML, parsePersonaYAMLFile, parseMultiplePersonaFiles, isPersonaConfigFile, getSupportedPersonaFiles, isValidYAMLSyntax, extractPersonaNameFromYAML, parseResultToValidationResult, type ParseResult, type YAMLError, type ParseOptions, } from "./parser.js"; import type { PersonaConfig, ValidationResult } from "./types.js"; describe("PersonaParser", () => { let tempDir: string; beforeEach(async () => { // Create temporary directory for test files tempDir = await fs.mkdtemp(join(tmpdir(), "persona-parser-test-")); }); afterEach(async () => { // Clean up temporary files try { await fs.rmdir(tempDir, { recursive: true }); } catch { // Ignore cleanup errors } }); describe("parsePersonaYAML", () => { describe("Valid YAML Content", () => { it("should parse minimal valid persona configuration", () => { const yamlContent = ` name: test-persona description: A test persona for unit testing purposes `; const result = parsePersonaYAML(yamlContent); expect(result.success).toBe(true); expect(result.data).toBeDefined(); expect(result.data?.name).toBe("test-persona"); expect(result.data?.description).toBe( "A test persona for unit testing purposes" ); expect(result.errors).toHaveLength(0); expect(result.warnings).toHaveLength(0); }); it("should parse complete persona configuration", () => { const yamlContent = ` name: complete-persona description: A complete persona configuration with all features version: "1.0.0" toolsets: - name: development toolIds: - git.status - docker.ps - npm.install - name: testing toolIds: - jest.run - coverage.report defaultToolset: development metadata: author: Test Suite tags: - development - testing - automation created: "2024-01-01T00:00:00Z" lastModified: "2024-01-01T12:00:00Z" `; const result = parsePersonaYAML(yamlContent); expect(result.success).toBe(true); expect(result.data?.name).toBe("complete-persona"); expect(result.data?.toolsets).toHaveLength(2); expect(result.data?.defaultToolset).toBe("development"); expect(result.data?.metadata?.author).toBe("Test Suite"); expect(result.data?.metadata?.tags).toContain("development"); expect(result.errors).toHaveLength(0); }); it("should handle empty optional fields", () => { const yamlContent = ` name: minimal-persona description: Persona with only required fields toolsets: [] metadata: {} `; const result = parsePersonaYAML(yamlContent); expect(result.success).toBe(true); expect(result.data?.toolsets).toEqual([]); expect(result.data?.metadata).toEqual({}); }); }); describe("Invalid YAML Syntax", () => { it("should handle malformed YAML syntax", () => { const yamlContent = ` name: test-persona description: [invalid yaml structure missing closing bracket `; const result = parsePersonaYAML(yamlContent); expect(result.success).toBe(false); expect(result.data).toBeUndefined(); expect(result.errors).toHaveLength(1); expect(result.errors[0].type).toBe("schema"); expect(result.errors[0].message).toMatch( /Flow sequence|YAML|syntax|parse/i ); expect(result.yamlErrors).toBeDefined(); expect(result.yamlErrors).toHaveLength(1); }); it("should handle indentation errors", () => { const yamlContent = ` name: test-persona description: Test description toolsets: # This line uses a tab instead of spaces - name: invalid toolIds: [] `; const result = parsePersonaYAML(yamlContent); expect(result.success).toBe(false); expect(result.errors[0].suggestion).toContain("spaces"); }); it("should handle duplicate keys", () => { const yamlContent = ` name: test-persona description: First description description: Duplicate key `; const result = parsePersonaYAML(yamlContent); expect(result.success).toBe(false); expect(result.errors[0].suggestion).toContain("duplicate"); }); it("should provide line numbers for syntax errors", () => { const yamlContent = ` name: test-persona description: Valid description invalid-yaml: [ missing closing bracket `; const result = parsePersonaYAML(yamlContent, "test.yaml"); expect(result.success).toBe(false); expect(result.yamlErrors).toBeDefined(); if (result.yamlErrors && result.yamlErrors[0]) { expect(result.yamlErrors[0].line).toBeDefined(); expect(result.yamlErrors[0].message).toMatch( /Flow sequence|missing.*bracket|syntax/i ); } }); }); describe("Schema Validation", () => { it("should validate against persona schema", () => { const yamlContent = ` name: Invalid_Name_Format description: Too short toolsets: - name: invalid-toolset toolIds: [] defaultToolset: non-existent `; const result = parsePersonaYAML(yamlContent); expect(result.success).toBe(false); expect(result.errors.length).toBeGreaterThan(1); // Check for specific validation errors const nameError = result.errors.find((e) => e.field === "name"); const descriptionError = result.errors.find( (e) => e.field === "description" ); const toolsetError = result.errors.find( (e) => e.message.includes("tool ID") || e.message.includes("toolIds") ); const defaultToolsetError = result.errors.find( (e) => e.field === "defaultToolset" ); expect(nameError).toBeDefined(); expect(descriptionError).toBeDefined(); expect(toolsetError).toBeDefined(); expect(defaultToolsetError).toBeDefined(); }); it("should skip schema validation when disabled", () => { const yamlContent = ` name: Invalid_Name_Format description: Short `; const result = parsePersonaYAML(yamlContent, undefined, { validateSchema: false, }); expect(result.success).toBe(true); expect(result.data?.name).toBe("Invalid_Name_Format"); expect(result.errors).toHaveLength(0); }); it("should handle validation warnings", () => { // Create content that might generate warnings (this depends on implementation) const yamlContent = ` name: test-persona description: A test persona that might generate warnings `; const result = parsePersonaYAML(yamlContent, undefined, { includeWarnings: true, }); expect(result.success).toBe(true); expect(result.warnings).toBeDefined(); // Warnings array might be empty, which is fine }); it("should exclude warnings when disabled", () => { const yamlContent = ` name: test-persona description: A test persona configuration `; const result = parsePersonaYAML(yamlContent, undefined, { includeWarnings: false, }); expect(result.warnings).toHaveLength(0); }); }); describe("Custom Validation", () => { it("should apply custom validation function", () => { const yamlContent = ` name: test-persona description: A test persona configuration `; const customValidation = (data: any): ValidationResult => ({ isValid: false, errors: [ { type: "business", message: "Custom validation failed", severity: "error", }, ], warnings: [ { type: "business", message: "Custom warning", severity: "warning", }, ], }); const result = parsePersonaYAML(yamlContent, undefined, { customValidation, }); expect(result.success).toBe(false); expect(result.errors.some((e) => e.type === "business")).toBe(true); expect(result.warnings.some((w) => w.type === "business")).toBe(true); }); it("should pass custom validation when successful", () => { const yamlContent = ` name: test-persona description: A test persona configuration `; const customValidation = (data: any): ValidationResult => ({ isValid: true, errors: [], warnings: [], }); const result = parsePersonaYAML(yamlContent, undefined, { customValidation, }); expect(result.success).toBe(true); }); }); describe("Edge Cases", () => { it("should handle empty YAML content", () => { const result = parsePersonaYAML(""); expect(result.success).toBe(false); expect(result.errors.length).toBeGreaterThan(0); }); it("should handle null/undefined YAML result", () => { const yamlContent = `~`; // YAML null const result = parsePersonaYAML(yamlContent); expect(result.success).toBe(false); }); it("should handle non-object YAML result", () => { const yamlContent = `"just a string"`; const result = parsePersonaYAML(yamlContent); expect(result.success).toBe(false); expect(result.errors[0].message).toContain("Expected object"); }); it("should provide helpful error suggestions", () => { const yamlContent = ` name: test-persona description: Short `; const result = parsePersonaYAML(yamlContent); expect(result.success).toBe(false); expect(result.errors[0].suggestion).toBeDefined(); expect(result.errors[0].suggestion).not.toBe(""); }); }); }); describe("parsePersonaYAMLFile", () => { describe("Valid Files", () => { it("should parse valid persona.yaml file", async () => { const filePath = join(tempDir, "persona.yaml"); const content = ` name: file-test-persona description: A persona loaded from file system `; await fs.writeFile(filePath, content); const result = await parsePersonaYAMLFile(filePath); expect(result.success).toBe(true); expect(result.data?.name).toBe("file-test-persona"); }); it("should parse valid persona.yml file", async () => { const filePath = join(tempDir, "persona.yml"); const content = ` name: yml-test-persona description: A persona with yml extension `; await fs.writeFile(filePath, content); const result = await parsePersonaYAMLFile(filePath); expect(result.success).toBe(true); expect(result.data?.name).toBe("yml-test-persona"); }); }); describe("File System Errors", () => { it("should handle non-existent files", async () => { const filePath = join(tempDir, "persona.yaml"); const result = await parsePersonaYAMLFile(filePath); expect(result.success).toBe(false); expect(result.errors[0].message).toContain("Failed to read file"); expect(result.errors[0].suggestion).toContain( "Verify that the file exists" ); }); it("should handle permission errors", async () => { const filePath = join(tempDir, "persona.yaml"); // Create file and remove read permissions (on Unix-like systems) await fs.writeFile(filePath, "content"); // Spy on fs.readFile to simulate permission error const readFileSpy = vi .spyOn(fs, "readFile") .mockRejectedValueOnce( Object.assign(new Error("Permission denied"), { code: "EACCES" }) ); const result = await parsePersonaYAMLFile(filePath); expect(result.success).toBe(false); expect(result.errors[0].suggestion).toMatch( /Check file permissions|Ensure.*correct type|Permission denied/i ); // Restore original function readFileSpy.mockRestore(); }); it("should handle unsupported file extensions", async () => { const filePath = join(tempDir, "persona.json"); const result = await parsePersonaYAMLFile(filePath); expect(result.success).toBe(false); expect(result.errors[0].message).toContain("Unsupported file type"); expect(result.errors[0].suggestion).toContain("supported extensions"); }); }); describe("Parse Options", () => { it("should pass parse options correctly", async () => { const filePath = join(tempDir, "persona.yaml"); const content = ` name: Invalid_Name description: Short `; await fs.writeFile(filePath, content); const result = await parsePersonaYAMLFile(filePath, { validateSchema: false, }); expect(result.success).toBe(true); expect(result.data?.name).toBe("Invalid_Name"); }); }); }); describe("parseMultiplePersonaFiles", () => { it("should parse multiple valid files concurrently", async () => { const files = [ { name: "persona.yaml", content: "name: persona-one\ndescription: First persona for testing multiple file parsing", dir: "dir1", }, { name: "persona.yaml", content: "name: persona-two\ndescription: Second persona for testing multiple file parsing", dir: "dir2", }, { name: "persona.yaml", content: "name: persona-three\ndescription: Third persona for testing multiple file parsing", dir: "dir3", }, ]; const filePaths = await Promise.all( files.map(async (file) => { const dirPath = join(tempDir, file.dir); await fs.mkdir(dirPath, { recursive: true }); const filePath = join(dirPath, file.name); await fs.writeFile(filePath, file.content); return filePath; }) ); const results = await parseMultiplePersonaFiles(filePaths); expect(results.size).toBe(3); filePaths.forEach((filePath, index) => { const result = results.get(filePath); if (!result?.success) { console.log(`File ${filePath} failed:`, result?.errors); } expect(result?.success).toBe(true); expect(result?.data?.name).toBe( `persona-${["one", "two", "three"][index]}` ); }); }); it("should handle mix of valid and invalid files", async () => { const files = [ { name: "persona.yaml", dir: "valid", content: "name: valid-persona\ndescription: Valid persona for testing mixed file parsing", }, { name: "persona.yaml", dir: "invalid", content: "name: [invalid yaml", }, { name: "persona.yaml", dir: "missing", exists: false }, ]; const filePaths = []; for (const file of files) { const dirPath = join(tempDir, file.dir); await fs.mkdir(dirPath, { recursive: true }); const filePath = join(dirPath, file.name); if (file.exists !== false) { await fs.writeFile(filePath, file.content); } filePaths.push(filePath); } const results = await parseMultiplePersonaFiles(filePaths); expect(results.size).toBe(3); // Valid file should succeed const validResult = results.get(filePaths[0]); expect(validResult?.success).toBe(true); // Invalid YAML should fail with parsing error const invalidResult = results.get(filePaths[1]); expect(invalidResult?.success).toBe(false); // Missing file should fail with file system error const missingResult = results.get(filePaths[2]); expect(missingResult?.success).toBe(false); }); it("should handle promise rejections gracefully", async () => { const filePath = join(tempDir, "persona.yaml"); // Create a file that will cause parsing issues await fs.writeFile(filePath, "invalid: yaml: syntax: [unclosed"); const results = await parseMultiplePersonaFiles([filePath]); expect(results.size).toBe(1); const result = results.get(filePath); expect(result?.success).toBe(false); expect(result?.errors[0].message).toContain( "Nested mappings are not allowed" ); }); }); describe("Utility Functions", () => { describe("isPersonaConfigFile", () => { it("should identify persona configuration files", () => { const validPaths = [ "persona.yaml", "persona.yml", "/path/to/persona.yaml", "/path/to/persona.yml", "C:\\path\\to\\persona.yaml", ]; validPaths.forEach((path) => { expect(isPersonaConfigFile(path)).toBe(true); }); }); it("should reject non-persona configuration files", () => { const invalidPaths = [ "config.yaml", "persona.json", "persona.txt", "PERSONA.YAML", "", "persona", ]; invalidPaths.forEach((path) => { expect(isPersonaConfigFile(path)).toBe(false); }); }); }); describe("getSupportedPersonaFiles", () => { it("should return supported file extensions", () => { const supported = getSupportedPersonaFiles(); expect(supported).toContain("persona.yaml"); expect(supported).toContain("persona.yml"); expect(supported).toHaveLength(2); }); it("should return readonly array", () => { const supported = getSupportedPersonaFiles(); // This is a compile-time check, but we can verify behavior expect(() => { (supported as any).push("persona.json"); }).toThrow(); }); }); describe("isValidYAMLSyntax", () => { it("should validate correct YAML syntax", () => { const validYaml = [ "key: value", "array:\n - item1\n - item2", "object:\n nested: value", "", "null", "42", '"string"', ]; validYaml.forEach((yaml) => { expect(isValidYAMLSyntax(yaml)).toBe(true); }); }); it("should reject invalid YAML syntax", () => { const invalidYaml = [ "key: [unclosed bracket", "duplicate:\nduplicate:", "\t\tindentation error", "invalid: yaml: [structure", ]; invalidYaml.forEach((yaml) => { expect(isValidYAMLSyntax(yaml)).toBe(false); }); }); }); describe("extractPersonaNameFromYAML", () => { it("should extract name from valid YAML", () => { const yamlContent = ` name: extracted-persona description: Test persona other: field `; const name = extractPersonaNameFromYAML(yamlContent); expect(name).toBe("extracted-persona"); }); it("should handle YAML without name field", () => { const yamlContent = ` description: Test persona without name other: field `; const name = extractPersonaNameFromYAML(yamlContent); expect(name).toBeUndefined(); }); it("should handle invalid YAML gracefully", () => { const yamlContent = "invalid: yaml: [structure"; const name = extractPersonaNameFromYAML(yamlContent); expect(name).toBeUndefined(); }); it("should handle non-string name values", () => { const yamlContent = ` name: 123 description: Test persona `; const name = extractPersonaNameFromYAML(yamlContent); expect(name).toBeUndefined(); }); it("should handle non-object YAML", () => { const yamlContent = '"just a string"'; const name = extractPersonaNameFromYAML(yamlContent); expect(name).toBeUndefined(); }); }); describe("parseResultToValidationResult", () => { it("should convert successful parse result", () => { const parseResult: ParseResult = { success: true, data: { name: "test-persona", description: "Test persona", } as PersonaConfig, errors: [], warnings: [], }; const validationResult = parseResultToValidationResult(parseResult); expect(validationResult.isValid).toBe(true); expect(validationResult.errors).toEqual([]); expect(validationResult.warnings).toEqual([]); }); it("should convert failed parse result", () => { const parseResult: ParseResult = { success: false, errors: [ { type: "schema", message: "Validation failed", severity: "error", }, ], warnings: [ { type: "business", message: "Warning message", severity: "warning", }, ], }; const validationResult = parseResultToValidationResult(parseResult); expect(validationResult.isValid).toBe(false); expect(validationResult.errors).toHaveLength(1); expect(validationResult.warnings).toHaveLength(1); expect(validationResult.errors[0].message).toBe("Validation failed"); }); }); }); describe("Error Handling", () => { it("should provide detailed error information", () => { const yamlContent = ` name: Invalid_Name description: Too short toolsets: - name: invalid-toolset toolIds: ["invalid-tool"] `; const result = parsePersonaYAML(yamlContent); expect(result.success).toBe(false); expect(result.errors.length).toBeGreaterThan(0); result.errors.forEach((error) => { expect(error.type).toBeDefined(); expect(error.message).toBeDefined(); expect(error.severity).toBeDefined(); expect(error.suggestion).toBeDefined(); }); }); it("should handle YAML parser errors gracefully", () => { const yamlContent = "invalid: yaml: [structure"; const result = parsePersonaYAML(yamlContent); expect(result.success).toBe(false); expect(result.yamlErrors).toHaveLength(1); expect(result.yamlErrors?.[0].originalError).toBeInstanceOf(Error); }); it("should extract line numbers from generic errors", () => { // Mock a generic error that contains line information in message const yamlContent = "invalid: yaml: [structure"; const result = parsePersonaYAML(yamlContent); expect(result.success).toBe(false); expect(result.yamlErrors).toBeDefined(); if (result.yamlErrors && result.yamlErrors[0]) { expect(result.yamlErrors[0].message).toBeDefined(); expect(result.yamlErrors[0].originalError).toBeDefined(); } }); }); describe("Performance and Edge Cases", () => { it("should handle large YAML files efficiently", () => { // Create a large but valid YAML content const toolIds = Array.from({ length: 100 }, (_, i) => `tool-${i}.action`); const yamlContent = ` name: large-persona description: A persona with many tools for performance testing toolsets: - name: large-toolset toolIds: ${toolIds.map((id) => ` - ${id}`).join("\n")} `; const startTime = Date.now(); const result = parsePersonaYAML(yamlContent); const duration = Date.now() - startTime; expect(result.success).toBe(true); expect(result.data?.toolsets?.[0].toolIds).toHaveLength(100); expect(duration).toBeLessThan(1000); // Should complete within 1 second }); it("should handle deeply nested YAML structures", () => { const yamlContent = ` name: nested-persona description: Persona with deeply nested structure for testing YAML parsing capabilities deeply: nested: yaml: structure: with: multiple: levels: - item1 - item2: nested: deep-value array: - sub1 - sub2 another: branch: test-value `; // Parse with schema validation disabled to test pure YAML parsing const result = parsePersonaYAML(yamlContent, undefined, { validateSchema: false, }); expect(result.success).toBe(true); expect( result.data?.deeply?.nested?.yaml?.structure?.with?.multiple ?.levels?.[1]?.item2?.nested ).toBe("deep-value"); expect( result.data?.deeply?.nested?.yaml?.structure?.with?.multiple?.another ?.branch ).toBe("test-value"); }); it("should handle YAML with special characters", () => { const yamlContent = ` name: special-chars-persona description: "Persona with special characters: !@#$%^&*()" metadata: author: "Author with spaces and symbols: <>{}[]" tags: - "tag-with-spaces" - "tag:with:colons" - "tag/with/slashes" `; const result = parsePersonaYAML(yamlContent); expect(result.success).toBe(true); expect(result.data?.description).toContain("!@#$%^&*()"); expect(result.data?.metadata?.author).toContain("<>{}[]"); }); }); });

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/toolprint/hypertool-mcp'

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