Skip to main content
Glama
jest-matchers.ts8.66 kB
/** * Custom Jest Matchers for Dynamic Expected Values * Implementation for AC 6.13, 6.14 - Jest integration and error diagnostics * * Extends Jest with custom matchers for dynamic template and pattern matching. * Provides detailed error diagnostics with environment context. * * CRITICAL: No mocks - uses real template resolution and environment values. */ import { DynamicExpectedValueConstructor } from './dynamic-expected-value-constructor'; // Global constructor instance for matchers let globalConstructor: DynamicExpectedValueConstructor | null = null; /** * Get or create global constructor instance */ function getConstructor(): DynamicExpectedValueConstructor { if (!globalConstructor) { globalConstructor = new DynamicExpectedValueConstructor(); } return globalConstructor; } /** * Custom matcher for dynamic template matching * AC 6.13: Dynamic expected value integration with Jest matchers */ async function toMatchDynamicTemplate(this: jest.MatcherContext, received: string, template: string) { const constructor = getConstructor(); try { const resolved = await constructor.resolveTemplate(template); const pass = received === resolved; if (pass) { return { message: () => `Expected output NOT to match dynamic template`, pass: true }; } else { // AC 6.14: Assertion error diagnostics with dynamic values const environmentValues = await constructor.getEnvironmentValues(); const message = () => { let errorMsg = `Expected output to match dynamic template\n\n`; errorMsg += `Template: ${template}\n`; errorMsg += `Resolved: ${resolved}\n`; errorMsg += `Actual: ${received}\n\n`; // Show mismatch details errorMsg += `Mismatch detected:\n`; if (received.length !== resolved.length) { errorMsg += ` Length difference: expected ${resolved.length}, received ${received.length}\n`; } // Find first difference const minLength = Math.min(received.length, resolved.length); for (let i = 0; i < minLength; i++) { if (received[i] !== resolved[i]) { errorMsg += ` First difference at position ${i}: expected '${resolved[i]}', received '${received[i]}'\n`; break; } } // Environment context errorMsg += `\nEnvironment Context:\n`; errorMsg += ` USER: ${environmentValues.USER}\n`; errorMsg += ` HOSTNAME: ${environmentValues.HOSTNAME}\n`; errorMsg += ` PWD: ${environmentValues.PWD}\n`; errorMsg += ` HOME: ${environmentValues.HOME}\n`; return errorMsg; }; return { message, pass: false }; } } catch (error) { // Handle template resolution errors if (error instanceof Error && error.message.includes('Unknown template variable')) { const environmentValues = await constructor.getEnvironmentValues(); const availableVars = Object.keys(environmentValues).join(', '); const message = () => { let errorMsg = `Template resolution failed: ${error instanceof Error ? error.message : String(error)}\n\n`; errorMsg += `Template: ${template}\n`; errorMsg += `Available variables: ${availableVars}\n\n`; errorMsg += `Guidance: Check that all variables in the template are defined.\n`; errorMsg += `Use one of the available variables or define custom variables in the constructor configuration.\n`; return errorMsg; }; return { message, pass: false }; } else { throw error; } } } /** * Custom matcher for dynamic pattern matching with regex support * AC 6.7: Volatile output handling with regex patterns */ async function toMatchDynamicPattern(this: jest.MatcherContext, received: string, pattern: string) { const constructor = getConstructor(); try { let resolvedPattern = pattern; // Check if pattern contains template variables if (pattern.includes('${')) { resolvedPattern = await constructor.resolveTemplate(pattern); } // Handle regex patterns if (resolvedPattern.startsWith('${pattern:') && resolvedPattern.endsWith('}')) { const regexMatch = resolvedPattern.match(/\$\{pattern:(.+)\}/); if (regexMatch) { const regexStr = regexMatch[1]; // Extract regex and flags const regexParts = regexStr.match(/^\/(.+)\/([gimuy]*)$/); if (regexParts) { const regex = new RegExp(regexParts[1], regexParts[2]); const pass = regex.test(received); if (pass) { return { message: () => `Expected output NOT to match dynamic pattern`, pass: true }; } else { const message = () => { let errorMsg = `Expected output to match dynamic pattern\n\n`; errorMsg += `Pattern: ${pattern}\n`; errorMsg += `Resolved Pattern: ${regex}\n`; errorMsg += `Actual: ${received}\n\n`; errorMsg += `The output does not match the expected regex pattern.\n`; return errorMsg; }; return { message, pass: false }; } } } } // Standard string matching const pass = received === resolvedPattern; if (pass) { return { message: () => `Expected output NOT to match dynamic pattern`, pass: true }; } else { const message = () => { let errorMsg = `Expected output to match dynamic pattern\n\n`; errorMsg += `Pattern: ${pattern}\n`; errorMsg += `Resolved: ${resolvedPattern}\n`; errorMsg += `Actual: ${received}\n`; return errorMsg; }; return { message, pass: false }; } } catch (error) { throw new Error(`Pattern resolution failed: ${error instanceof Error ? error.message : String(error)}`); } } /** * Enhanced terminal history matcher with dynamic support */ function toHaveValidTerminalHistory(this: jest.MatcherContext, received: string) { // Basic validation for terminal history format const hasCRLF = received.includes('\r\n'); const hasPrompts = /\[[^@]+@[^@]+\s[^\]]+\]\$/.test(received); const pass = hasCRLF && hasPrompts; if (pass) { return { message: () => `Expected output NOT to have valid terminal history format`, pass: true }; } else { const message = () => { let errorMsg = `Expected output to have valid terminal history format\n\n`; errorMsg += `Issues found:\n`; if (!hasCRLF) { errorMsg += ` - Missing CRLF line endings (\\r\\n)\n`; } if (!hasPrompts) { errorMsg += ` - Missing proper prompt format [user@host directory]$\n`; } errorMsg += `\nActual output:\n${received}\n`; return errorMsg; }; return { message, pass: false }; } } /** * CRLF line endings matcher */ function toContainCRLFLineEndings(this: jest.MatcherContext, received: string) { const pass = received.includes('\r\n'); if (pass) { return { message: () => `Expected output NOT to contain CRLF line endings`, pass: true }; } else { const message = () => { let errorMsg = `Expected output to contain CRLF line endings (\\r\\n)\n\n`; errorMsg += `CRLF line endings are required for xterm.js browser terminal compatibility.\n`; errorMsg += `Found line ending types:\n`; if (received.includes('\n')) { errorMsg += ` - LF (\\n) found\n`; } if (received.includes('\r')) { errorMsg += ` - CR (\\r) found\n`; } errorMsg += `\nActual output:\n${JSON.stringify(received)}\n`; return errorMsg; }; return { message, pass: false }; } } // Extend Jest matchers declare global { namespace jest { interface Matchers<R> { toMatchDynamicTemplate(template: string): Promise<R>; toMatchDynamicPattern(pattern: string): Promise<R>; toHaveValidTerminalHistory(): R; toContainCRLFLineEndings(): R; } } } // Register matchers with Jest expect.extend({ toMatchDynamicTemplate, toMatchDynamicPattern, toHaveValidTerminalHistory, toContainCRLFLineEndings }); // Export for explicit imports export { toMatchDynamicTemplate, toMatchDynamicPattern, toHaveValidTerminalHistory, toContainCRLFLineEndings };

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/LightspeedDMS/ssh-mcp'

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