Skip to main content
Glama

Context Pods

by conorluddy
wrapper-tester.tsโ€ข12 kB
/** * Script wrapper testing implementation */ import { promises as fs } from 'fs'; import { dirname } from 'path'; import { logger } from '@context-pods/core'; import { execa } from 'execa'; import type { WrapperTestConfig, WrapperTestCase, TestResult, TestSuiteResult } from '../types.js'; import { TestStatus } from '../types.js'; import { OutputValidator } from './output-validator.js'; import { ParameterValidator } from './parameter-validator.js'; /** * Script wrapper tester * * Tests wrapped scripts to ensure they work correctly through MCP interface */ export class ScriptWrapperTester { private config: WrapperTestConfig; private paramValidator: ParameterValidator; private outputValidator: OutputValidator; constructor(config: WrapperTestConfig) { this.config = config; this.paramValidator = new ParameterValidator(); this.outputValidator = new OutputValidator(); } /** * Run all test cases */ async runTests(): Promise<TestSuiteResult> { const startTime = Date.now(); const tests: TestResult[] = []; logger.info(`Running wrapper tests for: ${this.config.scriptPath}`); // Validate script exists try { await fs.access(this.config.scriptPath); } catch { tests.push({ name: 'Script Validation', status: TestStatus.FAILED, duration: 0, error: `Script not found: ${this.config.scriptPath}`, }); return this.createSuiteResult(tests, startTime); } // Run each test case for (const testCase of this.config.testCases) { const result = await this.runTestCase(testCase); tests.push(result); } // Additional validation tests tests.push(await this.testParameterPassing()); tests.push(await this.testErrorHandling()); tests.push(await this.testOutputFormat()); tests.push(await this.testTypeConversion()); return this.createSuiteResult(tests, startTime); } /** * Run a single test case */ private async runTestCase(testCase: WrapperTestCase): Promise<TestResult> { const startTime = Date.now(); try { logger.info(`Running test: ${testCase.name}`); // Execute the wrapped script const result = await this.executeScript(testCase.input, testCase.timeout); // Validate output if (testCase.expectedOutput !== undefined) { this.outputValidator.validateOutput(result.output, testCase.expectedOutput); } // Validate error if (testCase.expectedError) { if (!result.error) { throw new Error('Expected error but none occurred'); } if (testCase.expectedError instanceof RegExp) { if (!testCase.expectedError.test(result.error)) { throw new Error(`Error message doesn't match pattern: ${result.error}`); } } else { if (!result.error.includes(testCase.expectedError)) { throw new Error(`Error message doesn't match: ${result.error}`); } } } else if (result.error) { throw new Error(`Unexpected error: ${result.error}`); } return { name: testCase.name, status: TestStatus.PASSED, duration: Date.now() - startTime, }; } catch (error) { return { name: testCase.name, status: TestStatus.FAILED, duration: Date.now() - startTime, error: error instanceof Error ? error.message : String(error), }; } } /** * Execute wrapped script */ private async executeScript( input: Record<string, unknown>, timeout?: number, ): Promise<{ output?: unknown; error?: string; }> { try { const command = this.getExecutionCommand(); const args = this.buildArguments(input); const execResult = await execa(command.cmd, [...command.args, ...args], { cwd: dirname(this.config.scriptPath), timeout: timeout || this.config.timeout || 30000, env: { ...process.env, NODE_ENV: 'test', }, }); // Parse output try { return { output: JSON.parse(execResult.stdout) as unknown, }; } catch { // If not JSON, return raw output return { output: execResult.stdout, }; } } catch (error: unknown) { return { error: error instanceof Error ? error.message : String(error), }; } } /** * Get execution command based on language */ private getExecutionCommand(): { cmd: string; args: string[] } { switch (this.config.language) { case 'typescript': return { cmd: 'npx', args: ['tsx'] }; case 'python': return { cmd: 'python', args: [] }; case 'rust': return { cmd: 'cargo', args: ['run', '--'] }; case 'shell': return { cmd: 'bash', args: [] }; default: throw new Error(`Unsupported language: ${this.config.language as string}`); } } /** * Build command arguments from input */ private buildArguments(input: Record<string, unknown>): string[] { const args: string[] = [this.config.scriptPath]; // Convert input to command-line arguments for (const [key, value] of Object.entries(input)) { if (value === true) { args.push(`--${key}`); } else if (value !== false && value !== null && value !== undefined) { args.push(`--${key}`, JSON.stringify(value)); } } return args; } /** * Test parameter passing */ private async testParameterPassing(): Promise<TestResult> { const startTime = Date.now(); try { // Test various parameter types const testCases = [ { name: 'string-param', value: 'test value' }, { name: 'number-param', value: 42 }, { name: 'boolean-param', value: true }, { name: 'array-param', value: ['a', 'b', 'c'] }, { name: 'object-param', value: { nested: 'value' } }, ]; for (const test of testCases) { const input = { [test.name]: test.value }; const result = await this.executeScript(input); if (result.error) { throw new Error(`Parameter test failed for ${test.name}: ${result.error}`); } // Validate parameter was received correctly if (!this.paramValidator.validateParameter(test.name, test.value, result.output)) { throw new Error(`Parameter validation failed for ${test.name}`); } } return { name: 'Parameter Passing', status: TestStatus.PASSED, duration: Date.now() - startTime, }; } catch (error) { return { name: 'Parameter Passing', status: TestStatus.FAILED, duration: Date.now() - startTime, error: error instanceof Error ? error.message : String(error), }; } } /** * Test error handling */ private async testErrorHandling(): Promise<TestResult> { const startTime = Date.now(); try { // Test various error scenarios const errorTests = [ { name: 'missing-required-param', input: {}, expectedError: /required parameter/i, }, { name: 'invalid-type', input: { number: 'not-a-number' }, expectedError: /invalid type/i, }, { name: 'out-of-range', input: { value: -1 }, expectedError: /out of range/i, }, ]; for (const test of errorTests) { const result = await this.executeScript(test.input); if (!result.error) { throw new Error(`Expected error for ${test.name} but none occurred`); } if (test.expectedError && !test.expectedError.test(result.error)) { throw new Error(`Error doesn't match pattern for ${test.name}: ${result.error}`); } } return { name: 'Error Handling', status: TestStatus.PASSED, duration: Date.now() - startTime, }; } catch (error) { return { name: 'Error Handling', status: TestStatus.FAILED, duration: Date.now() - startTime, error: error instanceof Error ? error.message : String(error), }; } } /** * Test output format */ private async testOutputFormat(): Promise<TestResult> { const startTime = Date.now(); try { // Test different output formats const formatTests = [ { name: 'json-output', input: { format: 'json' }, validator: (output: unknown): void => { if (typeof output !== 'object') { throw new Error('Expected JSON object output'); } }, }, { name: 'text-output', input: { format: 'text' }, validator: (output: unknown): void => { if (typeof output !== 'string') { throw new Error('Expected text output'); } }, }, { name: 'array-output', input: { format: 'array' }, validator: (output: unknown): void => { if (!Array.isArray(output)) { throw new Error('Expected array output'); } }, }, ]; for (const test of formatTests) { const result = await this.executeScript(test.input); if (result.error) { throw new Error(`Format test failed for ${test.name}: ${result.error}`); } test.validator(result.output); } return { name: 'Output Format', status: TestStatus.PASSED, duration: Date.now() - startTime, }; } catch (error) { return { name: 'Output Format', status: TestStatus.FAILED, duration: Date.now() - startTime, error: error instanceof Error ? error.message : String(error), }; } } /** * Test type conversion */ private async testTypeConversion(): Promise<TestResult> { const startTime = Date.now(); try { // Test type conversion for different languages const conversionTests = [ { name: 'string-to-number', input: { value: '123' }, expectedType: 'number', }, { name: 'string-to-boolean', input: { flag: 'true' }, expectedType: 'boolean', }, { name: 'csv-to-array', input: { list: 'a,b,c' }, expectedType: 'array', }, ]; for (const test of conversionTests) { const result = await this.executeScript(test.input); if (result.error) { throw new Error(`Conversion test failed for ${test.name}: ${result.error}`); } // Validate type conversion const outputType = Array.isArray(result.output) ? 'array' : typeof result.output; if (outputType !== test.expectedType) { throw new Error( `Type mismatch for ${test.name}: expected ${test.expectedType}, got ${outputType}`, ); } } return { name: 'Type Conversion', status: TestStatus.PASSED, duration: Date.now() - startTime, }; } catch (error) { return { name: 'Type Conversion', status: TestStatus.FAILED, duration: Date.now() - startTime, error: error instanceof Error ? error.message : String(error), }; } } /** * Create test suite result */ private createSuiteResult(tests: TestResult[], startTime: number): TestSuiteResult { const passed = tests.filter((t) => t.status === TestStatus.PASSED).length; const failed = tests.filter((t) => t.status === TestStatus.FAILED).length; const skipped = tests.filter((t) => t.status === TestStatus.SKIPPED).length; return { name: `Wrapper Tests: ${this.config.scriptPath}`, tests, duration: Date.now() - startTime, passed, failed, skipped, }; } }

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/conorluddy/ContextPods'

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