UNPKG

meld

Version:

Meld: A template language for LLM prompts

430 lines (384 loc) 12.1 kB
import { expect } from 'vitest'; import { MeldError, ErrorSeverity } from '@core/errors/MeldError.js'; import { MeldResolutionError } from '@core/errors/MeldResolutionError.js'; import { MeldDirectiveError } from '@core/errors/MeldDirectiveError.js'; import { MeldInterpreterError } from '@core/errors/MeldInterpreterError.js'; import { DirectiveError, DirectiveErrorCode } from '@services/pipeline/DirectiveService/errors/DirectiveError.js'; /** * Error handler that collects errors for testing */ export class ErrorCollector { public errors: MeldError[] = []; public warnings: MeldError[] = []; /** * Error handler function that can be passed to services */ public handleError = (error: MeldError): void => { if (error.severity === ErrorSeverity.Warning || error.severity === ErrorSeverity.Recoverable) { this.warnings.push(error); } else { this.errors.push(error); } }; /** * Reset the collector */ public reset(): void { this.errors = []; this.warnings = []; } /** * Get all collected errors and warnings */ public getAllErrors(): MeldError[] { return [...this.errors, ...this.warnings]; } /** * Get errors of a specific type */ public getErrorsOfType<T extends MeldError>(errorType: new (...args: any[]) => T): T[] { return this.getAllErrors().filter(error => error instanceof errorType) as T[]; } /** * Get warnings of a specific type */ public getWarningsOfType<T extends MeldError>(errorType: new (...args: any[]) => T): T[] { return this.warnings.filter(error => error instanceof errorType) as T[]; } } /** * Test options for running tests in different error modes */ export interface ErrorModeTestOptions { strict?: boolean; errorHandler?: (error: MeldError) => void; } /** * Create test options for strict mode */ export function createStrictModeOptions(): ErrorModeTestOptions { return { strict: true }; } /** * Create test options for permissive mode with an error collector */ export function createPermissiveModeOptions(collector: ErrorCollector): ErrorModeTestOptions { return { strict: false, errorHandler: collector.handleError }; } /** * Assert that an error has the expected severity */ export function expectErrorSeverity(error: MeldError, severity: ErrorSeverity): void { expect(error.severity).toBe(severity); } /** * Assert that an error is a specific type with the expected severity */ export function expectErrorTypeAndSeverity<T extends MeldError>( error: unknown, errorType: new (...args: any[]) => T, severity: ErrorSeverity ): void { expect(error).toBeInstanceOf(errorType); expectErrorSeverity(error as MeldError, severity); } /** * Assert that a function throws an error with the expected severity */ export async function expectThrowsWithSeverity<T extends MeldError>( fn: () => Promise<any> | any, errorType: new (...args: any[]) => T, severity: ErrorSeverity ): Promise<void> { try { await fn(); throw new Error(`Expected function to throw ${errorType.name} with severity ${severity}`); } catch (error) { expectErrorTypeAndSeverity(error, errorType, severity); } } /** * Assert that a function does not throw but generates warnings in permissive mode */ export async function expectWarningsInPermissiveMode<T extends MeldError>( fn: (options: ErrorModeTestOptions) => Promise<any> | any, errorType: new (...args: any[]) => T, expectedWarningCount = 1 ): Promise<void> { const collector = new ErrorCollector(); const options = createPermissiveModeOptions(collector); // Should not throw in permissive mode await fn(options); // Should have generated warnings expect(collector.warnings.length).toBe(expectedWarningCount); expect(collector.getWarningsOfType(errorType).length).toBeGreaterThan(0); } /** * Assert that a function throws in strict mode but only warns in permissive mode */ export async function expectThrowsInStrictButWarnsInPermissive<T extends MeldError>( fn: (options: ErrorModeTestOptions) => Promise<any> | any, errorType: new (...args: any[]) => T, severity: ErrorSeverity = ErrorSeverity.Recoverable ): Promise<void> { // Should throw in strict mode const strictOptions = createStrictModeOptions(); await expectThrowsWithSeverity( () => fn(strictOptions), errorType, severity ); // Should only warn in permissive mode await expectWarningsInPermissiveMode(fn, errorType); } /** * Helper to test DirectiveError with specific error code */ export function expectDirectiveErrorWithCode( error: unknown, errorCode: string, severity: ErrorSeverity ): void { expect(error).toBeInstanceOf(DirectiveError); const directiveError = error as DirectiveError; expect(directiveError.code).toBe(errorCode); expectErrorSeverity(directiveError, severity); } /** * Helper to test MeldResolutionError with specific details */ export function expectResolutionErrorWithDetails( error: unknown, details: Record<string, any>, severity: ErrorSeverity = ErrorSeverity.Recoverable ): void { expect(error).toBeInstanceOf(MeldResolutionError); const resolutionError = error as MeldResolutionError; expectErrorSeverity(resolutionError, severity); // Check that all expected details are present for (const [key, value] of Object.entries(details)) { expect(resolutionError.details?.[key]).toEqual(value); } } /** * Error testing configuration options */ export interface ErrorTestOptions { /** * The expected error type (class name) */ type?: string; /** * The expected error code */ code?: string; /** * The expected error severity */ severity?: ErrorSeverity; /** * Whether the error message should contain this substring */ messageContains?: string; /** * The exact error message to match */ exactMessage?: string; /** * The directive kind for MeldDirectiveError */ directiveKind?: string; /** * Whether the error should have a location */ hasLocation?: boolean; } /** * Checks if an error matches the expected configuration * This provides more resilient error testing by focusing on properties * rather than exact message strings * * @param error The error to check * @param options The expected error configuration * @returns void * @throws Test assertion error if the error doesn't match expectations */ export function checkError(error: unknown, options: ErrorTestOptions): void { // Verify the error is an object expect(error).toBeInstanceOf(Object); // Check error type if (options.type) { expect(error.constructor.name).toBe(options.type); } // Handle MeldError specific checks if (error instanceof MeldError) { if (options.code) { expect(error.code).toBe(options.code); } if (options.severity) { expect(error.severity).toBe(options.severity); } if (options.messageContains) { expect(error.message).toContain(options.messageContains); } if (options.exactMessage) { expect(error.message).toBe(options.exactMessage); } } // Handle MeldDirectiveError specific checks if (error instanceof MeldDirectiveError) { if (options.directiveKind) { expect(error.directiveKind).toBe(options.directiveKind); } if (options.hasLocation) { expect(error.location).toBeDefined(); expect(error.location?.line).toBeGreaterThanOrEqual(0); expect(error.location?.column).toBeGreaterThanOrEqual(0); } } } /** * Convenience function to check if an error is a validation error */ export function expectValidationError(error: unknown): void { checkError(error, { type: 'MeldDirectiveError', code: DirectiveErrorCode.VALIDATION_FAILED, severity: ErrorSeverity.Fatal }); } /** * Helper to easily assert a specific directive validation error */ export function expectDirectiveValidationError( error: unknown, directiveKind: string, messageContains?: string ): void { checkError(error, { type: 'MeldDirectiveError', code: DirectiveErrorCode.VALIDATION_FAILED, severity: ErrorSeverity.Fatal, directiveKind, messageContains, hasLocation: true }); } /** * Async wrapper to test functions that should throw errors * This makes tests more readable than try/catch blocks * * @param fn Function that should throw an error * @param options Options to verify the thrown error * @returns Promise that resolves when the test passes */ export async function expectToThrowWithConfig( fn: () => Promise<any>, options: ErrorTestOptions ): Promise<void> { try { await fn(); // If we get here, the function didn't throw expect.fail('Expected function to throw an error'); } catch (error) { // Check if this is an assertion error from expect.fail if (error.name === 'AssertionError') { throw error; } // Otherwise check the actual error checkError(error, options); } } /** * Sync version of expectToThrowWithConfig */ export function expectToThrowWithConfigSync( fn: () => any, options: ErrorTestOptions ): void { try { fn(); // If we get here, the function didn't throw expect.fail('Expected function to throw an error'); } catch (error) { // Check if this is an assertion error from expect.fail if (error.name === 'AssertionError') { throw error; } // Otherwise check the actual error checkError(error, options); } } /** * Specialized validation error checking for ValidationService tests * This makes tests more resilient to error message changes * * @param error The error to check * @param directiveKind The directive kind (e.g., 'text', 'data', 'path') * @param code The error code (e.g., DirectiveErrorCode.VALIDATION_FAILED) * @param propertyName Optional property name that caused the validation error * @param severity The error severity (defaults to Fatal) */ export function expectValidationErrorWithDetails( error: unknown, directiveKind: string, code: string, propertyName?: string, severity: ErrorSeverity = ErrorSeverity.Fatal ): void { // Check basic error type expect(error).toBeInstanceOf(MeldDirectiveError); const directiveError = error as MeldDirectiveError; // Check properties regardless of exact message expect(directiveError.directiveKind).toBe(directiveKind); expect(directiveError.code).toBe(code); expect(directiveError.severity).toBe(severity); // If property name is specified, check that it's mentioned in the message if (propertyName) { expect(directiveError.message.toLowerCase()).toContain(propertyName.toLowerCase()); } } /** * Test a validation function expecting it to throw an error with specific attributes * * @param validationFn The validation function to test * @param directiveKind The expected directive kind in the error * @param code The expected error code * @param propertyName Optional property name that caused the validation error * @param severity The expected severity */ export function expectValidationToThrowWithDetails( validationFn: () => any, directiveKind: string, code: string, propertyName?: string, severity: ErrorSeverity = ErrorSeverity.Fatal ): void { try { validationFn(); throw new Error(`Expected validation function to throw MeldDirectiveError for ${directiveKind}`); } catch (error) { expectValidationErrorWithDetails(error, directiveKind, code, propertyName, severity); } } /** * Asynchronous version of expectValidationToThrowWithDetails */ export async function expectValidationToThrowWithDetailsAsync( validationFn: () => Promise<any>, directiveKind: string, code: string, propertyName?: string, severity: ErrorSeverity = ErrorSeverity.Fatal ): Promise<void> { try { await validationFn(); throw new Error(`Expected validation function to throw MeldDirectiveError for ${directiveKind}`); } catch (error) { expectValidationErrorWithDetails(error, directiveKind, code, propertyName, severity); } }