Skip to main content
Glama

CTS MCP Server

by EricA1019
signal_extractor.test.tsโ€ข11.1 kB
/** * Signal Extractor Tests * * Validates signal extraction accuracy, performance, and compatibility. * * @module signal_extractor.test */ import { describe, it, expect, beforeEach } from '@jest/globals'; import { SignalExtractor, SignalDefinitionSchema } from '../artifacts/parsers/signal_extractor'; import { SignalDefinition } from '../artifacts/parsers/gdscript_parser'; import { resolve } from 'path'; import * as fs from 'fs'; describe('SignalExtractor', () => { let extractor: SignalExtractor; beforeEach(() => { extractor = new SignalExtractor(); extractor.resetStats(); }); describe('Basic Signal Extraction', () => { it('should extract all signals from simple_signals.gd', async () => { const fixturePath = resolve(__dirname, 'fixtures/simple_signals.gd'); const signals = await extractor.extractSignals(fixturePath); expect(signals.length).toBe(4); expect(signals.map(s => s.name)).toEqual([ 'health_changed', 'damage_taken', 'player_ready', 'item_collected' ]); }); it('should extract parameter names correctly', async () => { const fixturePath = resolve(__dirname, 'fixtures/simple_signals.gd'); const signals = await extractor.extractSignals(fixturePath); const healthSignal = signals.find(s => s.name === 'health_changed'); expect(healthSignal).toBeDefined(); expect(healthSignal!.params).toEqual(['new_value', 'old_value']); }); it('should handle signals with no parameters', async () => { const fixturePath = resolve(__dirname, 'fixtures/simple_signals.gd'); const signals = await extractor.extractSignals(fixturePath); const readySignal = signals.find(s => s.name === 'player_ready'); expect(readySignal).toBeDefined(); expect(readySignal!.params).toEqual([]); }); }); describe('Typed Parameter Extraction', () => { it('should extract all typed signals', async () => { const fixturePath = resolve(__dirname, 'fixtures/typed_signals.gd'); const signals = await extractor.extractSignals(fixturePath); expect(signals.length).toBeGreaterThanOrEqual(6); expect(signals.every(s => s.paramTypes !== undefined)).toBe(true); }); it('should correctly parse parameter types', async () => { const fixturePath = resolve(__dirname, 'fixtures/typed_signals.gd'); const signals = await extractor.extractSignals(fixturePath); const healthSignal = signals.find(s => s.name === 'health_changed'); expect(healthSignal).toBeDefined(); expect(healthSignal!.paramTypes).toEqual({ new_value: 'int', old_value: 'int' }); }); it('should handle mixed type annotations', async () => { const fixturePath = resolve(__dirname, 'fixtures/typed_signals.gd'); const signals = await extractor.extractSignals(fixturePath); const damageSignal = signals.find(s => s.name === 'damage_taken'); expect(damageSignal).toBeDefined(); expect(damageSignal!.paramTypes).toEqual({ amount: 'float', source: 'Node' }); }); it('should extract Array type parameters', async () => { const fixturePath = resolve(__dirname, 'fixtures/typed_signals.gd'); const signals = await extractor.extractSignals(fixturePath); const questSignal = signals.find(s => s.name === 'quest_completed'); expect(questSignal).toBeDefined(); expect(questSignal!.paramTypes?.rewards).toBe('Array'); }); }); describe('EventBus vs Local Signal Differentiation', () => { it('should identify EventBus signals correctly', async () => { const fixturePath = resolve(__dirname, 'fixtures/eventbus_signals.gd'); const signals = await extractor.extractSignals(fixturePath); // Source field contains filename expect(signals.every(s => s.source === 'eventbus_signals')).toBe(true); expect(signals.length).toBeGreaterThanOrEqual(6); }); it('should identify local signals correctly', async () => { const fixturePath = resolve(__dirname, 'fixtures/simple_signals.gd'); const signals = await extractor.extractSignals(fixturePath); const stats = extractor.getStats(); expect(stats.localSignals).toBe(4); expect(signals.every(s => s.source === 'simple_signals')).toBe(true); }); }); describe('Complex Signal Patterns', () => { it('should handle Callable and Dictionary types', async () => { const fixturePath = resolve(__dirname, 'fixtures/complex_signals.gd'); const signals = await extractor.extractSignals(fixturePath); const callbackSignal = signals.find(s => s.name === 'nested_callback'); expect(callbackSignal).toBeDefined(); expect(callbackSignal!.params).toContain('callback'); expect(callbackSignal!.params).toContain('data'); }); it('should extract multiline signal declarations', async () => { const fixturePath = resolve(__dirname, 'fixtures/complex_signals.gd'); const signals = await extractor.extractSignals(fixturePath); // Note: Phase 1 regex parser doesn't handle multiline declarations // This is a known limitation documented for tree-sitter upgrade const stateSignal = signals.find(s => s.name === 'state_changed'); // Test documents current behavior: multiline signals may not be extracted // or may be extracted with only first line's params if (stateSignal) { // If found, should have at least signal name expect(stateSignal.name).toBe('state_changed'); } // Not finding multiline signals is acceptable for Phase 1 regex parser }); }); describe('Edge Cases', () => { it('should ignore commented signals', async () => { const fixturePath = resolve(__dirname, 'fixtures/edge_cases.gd'); const signals = await extractor.extractSignals(fixturePath); const commentedSignal = signals.find(s => s.name === 'commented_signal'); expect(commentedSignal).toBeUndefined(); }); it('should handle unusual whitespace', async () => { const fixturePath = resolve(__dirname, 'fixtures/edge_cases.gd'); const signals = await extractor.extractSignals(fixturePath); const weirdSignal = signals.find(s => s.name === 'weird_spacing'); expect(weirdSignal).toBeDefined(); expect(weirdSignal!.params).toContain('param1'); expect(weirdSignal!.params).toContain('param2'); }); it('should handle signals with no params and empty parens', async () => { const fixturePath = resolve(__dirname, 'fixtures/edge_cases.gd'); const signals = await extractor.extractSignals(fixturePath); const noParamsSignals = signals.filter(s => s.name === 'no_params' || s.name === 'no_params_with_parens' ); expect(noParamsSignals.length).toBe(2); noParamsSignals.forEach(sig => { expect(sig.params).toEqual([]); }); }); }); describe('Batch Processing', () => { it('should extract from multiple files', async () => { const files = [ resolve(__dirname, 'fixtures/simple_signals.gd'), resolve(__dirname, 'fixtures/typed_signals.gd'), ]; const signals = await extractor.extractFromFiles(files); const stats = extractor.getStats(); expect(signals.length).toBeGreaterThanOrEqual(10); expect(stats.filesProcessed).toBe(2); }); it('should handle missing files gracefully', async () => { const files = [ resolve(__dirname, 'fixtures/simple_signals.gd'), resolve(__dirname, 'fixtures/nonexistent.gd'), ]; // Should not throw, just skip missing file const signals = await extractor.extractFromFiles(files); expect(signals.length).toBeGreaterThan(0); }); }); describe('Performance', () => { it('should extract from 1000 LOC file in <200ms', async () => { // Generate synthetic 1000-line file const tempPath = resolve(__dirname, 'fixtures/large_test.gd'); const lines: string[] = ['extends Node', '']; for (let i = 0; i < 250; i++) { lines.push(`signal test_signal_${i}(value: int)`); lines.push(`func test_func_${i}():`); lines.push(`\tpass`); lines.push(''); } fs.writeFileSync(tempPath, lines.join('\n')); const startTime = Date.now(); const signals = await extractor.extractSignals(tempPath); const duration = Date.now() - startTime; expect(signals.length).toBe(250); expect(duration).toBeLessThan(200); // Cleanup fs.unlinkSync(tempPath); }); it('should track extraction statistics', async () => { const fixturePath = resolve(__dirname, 'fixtures/simple_signals.gd'); await extractor.extractSignals(fixturePath); const stats = extractor.getStats(); expect(stats.filesProcessed).toBe(1); expect(stats.signalCount).toBe(4); expect(stats.durationMs).toBeGreaterThanOrEqual(0); // Can be 0ms for small files expect(stats.localSignals).toBe(4); }); }); describe('Schema Validation', () => { it('should validate extracted signals against Zod schema', async () => { const fixturePath = resolve(__dirname, 'fixtures/simple_signals.gd'); const signals = await extractor.extractSignals(fixturePath); signals.forEach(signal => { const result = SignalDefinitionSchema.safeParse(signal); expect(result.success).toBe(true); }); }); it('should match Phase 1 SignalDefinition interface exactly', async () => { const fixturePath = resolve(__dirname, 'fixtures/typed_signals.gd'); const signals = await extractor.extractSignals(fixturePath); signals.forEach((signal: SignalDefinition) => { expect(signal).toHaveProperty('name'); expect(signal).toHaveProperty('params'); expect(signal).toHaveProperty('filePath'); expect(signal).toHaveProperty('line'); expect(signal).toHaveProperty('source'); expect(typeof signal.name).toBe('string'); expect(Array.isArray(signal.params)).toBe(true); expect(typeof signal.line).toBe('number'); }); }); }); describe('Integration with Phase 1', () => { it('should produce identical output to Phase 1 regex parser', async () => { const { parseGDScriptSignals } = await import('../artifacts/parsers/gdscript_parser.js'); const fixturePath = resolve(__dirname, 'fixtures/simple_signals.gd'); // Extract via new service const newSignals = await extractor.extractSignals(fixturePath); // Extract via Phase 1 parser const phase1Signals = parseGDScriptSignals(fixturePath); expect(newSignals).toEqual(phase1Signals); }); }); });

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/EricA1019/CTS_MCP'

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