Skip to main content
Glama
template-resolver.test.ts10.7 kB
/** * Test suite for TemplateResolver * Tests for AC 6.10, 6.11, 6.12 - Template variable expansion and conditional logic * * TDD methodology: Failing tests first for template resolution system */ import { TemplateResolver } from './template-resolver'; import { EnvironmentValues } from './environment-value-provider'; describe('TemplateResolver', () => { let resolver: TemplateResolver; let mockEnvironmentValues: EnvironmentValues; beforeEach(() => { mockEnvironmentValues = { USER: 'testuser', PWD: '/home/testuser/project', HOSTNAME: 'testhost', HOME: '/home/testuser', LS_OUTPUT: ['file1.txt', 'dir1', 'README.md'], TIMESTAMP: '1703505600000' }; resolver = new TemplateResolver(mockEnvironmentValues); }); describe('AC 6.10: Template variable expansion system', () => { it('should resolve simple variable substitution', async () => { const template = '${USER}'; const result = await resolver.resolve(template); expect(result).toBe('testuser'); }); it('should resolve multiple variables in single template', async () => { const template = 'User: ${USER}, Directory: ${PWD}, Host: ${HOSTNAME}'; const result = await resolver.resolve(template); expect(result).toBe('User: testuser, Directory: /home/testuser/project, Host: testhost'); }); it('should resolve array variables as JSON', async () => { const template = 'Files: ${LS_OUTPUT}'; const result = await resolver.resolve(template); expect(result).toBe('Files: ["file1.txt","dir1","README.md"]'); }); it('should handle template with no variables', async () => { const template = 'No variables here'; const result = await resolver.resolve(template); expect(result).toBe('No variables here'); }); it('should handle empty template', async () => { const template = ''; const result = await resolver.resolve(template); expect(result).toBe(''); }); it('should handle nested variable references', async () => { const resolver = new TemplateResolver(mockEnvironmentValues, { BASE_PATH: '${PWD}', FULL_PATH: '${BASE_PATH}/subdir' }); const template = '${FULL_PATH}'; const result = await resolver.resolve(template); expect(result).toBe('/home/testuser/project/subdir'); }); it('should detect circular references', async () => { const resolver = new TemplateResolver(mockEnvironmentValues, { VAR_A: '${VAR_B}', VAR_B: '${VAR_A}' }); const template = '${VAR_A}'; await expect(resolver.resolve(template)) .rejects.toThrow('Circular reference detected'); }); }); describe('AC 6.11: Custom variable definition and usage', () => { it('should resolve custom variables', async () => { const resolver = new TemplateResolver(mockEnvironmentValues, { CUSTOM_VAR: 'custom_value' }); const template = 'Value: ${CUSTOM_VAR}'; const result = await resolver.resolve(template); expect(result).toBe('Value: custom_value'); }); it('should execute command-based custom variables', async () => { const resolver = new TemplateResolver(mockEnvironmentValues, { ECHO_TEST: 'exec:echo "hello world"' }); const template = 'Output: ${ECHO_TEST}'; const result = await resolver.resolve(template); expect(result).toBe('Output: hello world'); }); it('should handle command execution with complex output', async () => { const resolver = new TemplateResolver(mockEnvironmentValues, { NODE_VERSION: 'exec:node --version' }); const template = 'Node version: ${NODE_VERSION}'; const result = await resolver.resolve(template); expect(result).toMatch(/Node version: v\d+\.\d+\.\d+/); }); it('should handle command execution failures', async () => { const resolver = new TemplateResolver(mockEnvironmentValues, { FAILING_CMD: 'exec:nonexistent-command-xyz' }); const template = '${FAILING_CMD}'; await expect(resolver.resolve(template)) .rejects.toThrow('Command execution failed'); }); it('should support SSH session count variable', async () => { const resolver = new TemplateResolver(mockEnvironmentValues, { SSH_SESSION_COUNT: 'exec:who | wc -l' }); const template = 'SSH sessions: ${SSH_SESSION_COUNT}'; const result = await resolver.resolve(template); expect(result).toMatch(/SSH sessions: \d+/); }); it('should support git branch variable when in git repo', async () => { const resolver = new TemplateResolver(mockEnvironmentValues, { GIT_BRANCH: 'exec:git branch --show-current' }); const template = 'Branch: ${GIT_BRANCH}'; try { const result = await resolver.resolve(template); expect(result).toMatch(/Branch: .+/); } catch (error) { // Skip if not in git repo or git not available expect(error instanceof Error ? error.message : String(error)).toContain('Command execution failed'); } }); }); describe('AC 6.12: Conditional template resolution', () => { it('should resolve simple conditional expression', async () => { const template = '${USER === "testuser" ? "Admin" : "User"}'; const result = await resolver.resolve(template); expect(result).toBe('Admin'); }); it('should resolve false conditional expression', async () => { const template = '${USER === "root" ? "Admin" : "User"}'; const result = await resolver.resolve(template); expect(result).toBe('User'); }); it('should handle file existence conditional', async () => { // Mock file existence checker const resolver = new TemplateResolver(mockEnvironmentValues, { 'FILE_EXISTS:package.json': 'true' }); const template = '${FILE_EXISTS:package.json ? "Found" : "Missing"}'; const result = await resolver.resolve(template); expect(result).toBe('Found'); }); it('should handle file non-existence conditional', async () => { const resolver = new TemplateResolver(mockEnvironmentValues, { 'FILE_EXISTS:nonexistent.txt': 'false' }); const template = '${FILE_EXISTS:nonexistent.txt ? "Found" : "Missing"}'; const result = await resolver.resolve(template); expect(result).toBe('Missing'); }); it('should support multiple conditional operators', async () => { const template1 = '${USER !== "root" ? "non-root" : "root"}'; const result1 = await resolver.resolve(template1); expect(result1).toBe('non-root'); const template2 = '${LS_OUTPUT.length > 0 ? "has files" : "empty"}'; const result2 = await resolver.resolve(template2); expect(result2).toBe('has files'); }); it('should handle nested conditional expressions', async () => { const template = '${USER === "root" ? "Root user" : (USER === "testuser" ? "Test user" : "Other user")}'; const result = await resolver.resolve(template); expect(result).toBe('Test user'); }); it('should support contains operator', async () => { const template = '${PWD.includes("project") ? "In project dir" : "Not in project"}'; const result = await resolver.resolve(template); expect(result).toBe('In project dir'); }); it('should handle complex conditional with array operations', async () => { const template = '${LS_OUTPUT.includes("README.md") ? "Has README" : "No README"}'; const result = await resolver.resolve(template); expect(result).toBe('Has README'); }); }); describe('Template Syntax Validation', () => { it('should detect invalid template syntax', async () => { const template = '${INVALID_SYNTAX'; await expect(resolver.resolve(template)) .rejects.toThrow('Invalid template syntax'); }); it('should detect unmatched braces', async () => { const template = '${USER} ${HOSTNAME'; await expect(resolver.resolve(template)) .rejects.toThrow('Invalid template syntax'); }); it('should handle escaped braces', async () => { const template = 'This is not a variable: \\${USER}'; const result = await resolver.resolve(template); expect(result).toBe('This is not a variable: ${USER}'); }); it('should detect unknown variables', async () => { const template = '${UNKNOWN_VARIABLE}'; await expect(resolver.resolve(template)) .rejects.toThrow('Unknown template variable: UNKNOWN_VARIABLE'); }); }); describe('Performance and Edge Cases', () => { it('should handle large templates efficiently', async () => { const largeTemplate = Array(100).fill('${USER}').join(' '); const startTime = Date.now(); const result = await resolver.resolve(largeTemplate); const endTime = Date.now(); expect(result).toBe(Array(100).fill('testuser').join(' ')); expect(endTime - startTime).toBeLessThan(1000); // Should complete within 1 second }); it('should handle recursive template resolution up to limit', async () => { const resolver = new TemplateResolver(mockEnvironmentValues, { LEVEL_1: '${LEVEL_2}', LEVEL_2: '${LEVEL_3}', LEVEL_3: 'final_value' }); const template = '${LEVEL_1}'; const result = await resolver.resolve(template); expect(result).toBe('final_value'); }); it('should prevent infinite recursion', async () => { const resolver = new TemplateResolver(mockEnvironmentValues, { RECURSIVE_1: '${RECURSIVE_2}', RECURSIVE_2: '${RECURSIVE_3}', RECURSIVE_3: '${RECURSIVE_1}' }); const template = '${RECURSIVE_1}'; await expect(resolver.resolve(template)) .rejects.toThrow('Maximum recursion depth exceeded'); }); it('should handle special characters in variable values', async () => { const resolver = new TemplateResolver({ ...mockEnvironmentValues, SPECIAL_CHARS: 'value with "quotes" and $pecial ch@rs' }); const template = 'Value: ${SPECIAL_CHARS}'; const result = await resolver.resolve(template); expect(result).toBe('Value: value with "quotes" and $pecial ch@rs'); }); }); });

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