Skip to main content
Glama
sandbox-executor.test.ts30.9 kB
/** * CE-MCP Sandbox Executor Tests * * Tests for the sandbox execution engine that processes orchestration directives * and state machine directives in an isolated environment. * * @see ADR-014: CE-MCP Architecture */ import { vi as _vi } from 'vitest'; import { join } from 'path'; import { mkdtemp, rm, mkdir, writeFile } from 'fs/promises'; import { tmpdir } from 'os'; import { SandboxExecutor, DEFAULT_CEMCP_CONFIG, getSandboxExecutor, resetSandboxExecutor, } from '../../src/utils/sandbox-executor'; import { OrchestrationDirective, StateMachineDirective, SandboxOperation as _SandboxOperation, CEMCPConfig, } from '../../src/types/ce-mcp'; describe('SandboxExecutor', () => { let executor: SandboxExecutor; let testProjectPath: string; beforeEach(async () => { // Reset global singleton resetSandboxExecutor(); // Create a temporary test project directory testProjectPath = await mkdtemp(join(tmpdir(), 'sandbox-test-')); // Create project structure await mkdir(join(testProjectPath, 'src', 'prompts'), { recursive: true }); await mkdir(join(testProjectPath, 'src', 'utils'), { recursive: true }); await mkdir(join(testProjectPath, '.github', 'workflows'), { recursive: true }); // Create package.json await writeFile( join(testProjectPath, 'package.json'), JSON.stringify({ name: 'test-project', version: '1.0.0', dependencies: { typescript: '^5.0.0' }, devDependencies: { jest: '^29.0.0' }, }) ); // Create tsconfig.json await writeFile( join(testProjectPath, 'tsconfig.json'), JSON.stringify({ compilerOptions: { target: 'ES2022' } }) ); // Create sample TypeScript files await writeFile( join(testProjectPath, 'src', 'index.ts'), 'export const main = () => console.log("hello");' ); await writeFile( join(testProjectPath, 'src', 'utils', 'helper.ts'), 'export const helper = (x: number) => x * 2;' ); // Create a prompt file for testing await writeFile( join(testProjectPath, 'src', 'prompts', 'test-prompt.ts'), `// SECTION: test_section\nexport const TEST_PROMPT = 'test prompt content';\n// SECTION: another_section\nexport const ANOTHER = 'another content';` ); executor = new SandboxExecutor(); }); afterEach(async () => { // Cleanup temp directory await rm(testProjectPath, { recursive: true, force: true }); resetSandboxExecutor(); }); describe('Configuration', () => { it('should use default configuration', () => { expect(executor['config']).toEqual(DEFAULT_CEMCP_CONFIG); }); it('should merge custom configuration with defaults', () => { const customConfig: Partial<CEMCPConfig> = { sandbox: { enabled: true, timeout: 60000, memoryLimit: 512 * 1024 * 1024, fsOperationsLimit: 2000, networkAllowed: false, }, }; const customExecutor = new SandboxExecutor(customConfig); expect(customExecutor['config'].sandbox.timeout).toBe(60000); expect(customExecutor['config'].sandbox.memoryLimit).toBe(512 * 1024 * 1024); // Other defaults preserved expect(customExecutor['config'].prompts.lazyLoading).toBe(true); }); }); describe('Orchestration Directive Execution', () => { it('should execute simple orchestration directive', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'test_tool', description: 'Test directive', sandbox_operations: [ { op: 'loadKnowledge', args: { domain: 'architecture' }, store: 'knowledge' }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); expect(result.data).toBeDefined(); expect(result.metadata.operationsExecuted).toBe(1); }); it('should execute multiple operations in sequence', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'multi_op_tool', description: 'Multi-operation directive', sandbox_operations: [ { op: 'loadKnowledge', args: { domain: 'testing' }, store: 'knowledge' }, { op: 'scanEnvironment', store: 'env' }, { op: 'analyzeFiles', args: { patterns: ['**/*.ts'] }, store: 'files' }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); expect(result.metadata.operationsExecuted).toBe(3); expect(result.data).toHaveProperty('knowledge'); expect(result.data).toHaveProperty('env'); expect(result.data).toHaveProperty('files'); }); it('should handle composition directive for result building', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'compose_tool', description: 'Composition test', sandbox_operations: [ { op: 'loadKnowledge', args: { domain: 'test' }, store: 'data1' }, { op: 'scanEnvironment', store: 'data2' }, ], compose: { sections: [ { source: 'data1', key: 'knowledge' }, { source: 'data2', key: 'environment' }, ], template: 'analysis_result', format: 'json', }, }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); expect(result.data).toHaveProperty('knowledge'); expect(result.data).toHaveProperty('environment'); expect(result.data).toHaveProperty('template', 'analysis_result'); }); it('should handle return flag for early termination', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'early_return_tool', description: 'Early return test', sandbox_operations: [ { op: 'loadKnowledge', args: { domain: 'test' }, store: 'result', return: true }, { op: 'scanEnvironment', store: 'env' }, // Should not execute ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); expect(result.metadata.operationsExecuted).toBe(1); // Second operation should not have executed }); it('should handle conditional operations', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'conditional_tool', description: 'Conditional operation test', sandbox_operations: [ { op: 'loadKnowledge', args: { domain: 'test' }, store: 'knowledge' }, { op: 'scanEnvironment', store: 'env', condition: { key: 'knowledge', operator: 'exists' }, }, { op: 'analyzeFiles', store: 'files', condition: { key: 'nonexistent', operator: 'exists' }, // Should skip }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); expect(result.data).toHaveProperty('knowledge'); expect(result.data).toHaveProperty('env'); expect(result.data).not.toHaveProperty('files'); }); }); describe('State Machine Directive Execution', () => { it('should execute simple state machine directive', async () => { const directive: StateMachineDirective = { type: 'state_machine_directive', version: '1.0', initial_state: { input: 'test' }, transitions: [ { name: 'process', from: 'initial', operation: { op: 'loadKnowledge', args: { domain: 'test' }, store: 'result' }, next_state: 'result', }, ], final_state: 'result', }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); expect(result.metadata.operationsExecuted).toBe(1); }); it('should execute multi-step state machine', async () => { const directive: StateMachineDirective = { type: 'state_machine_directive', version: '1.0', initial_state: { step: 0 }, transitions: [ { name: 'step1', from: 'initial', operation: { op: 'loadKnowledge', args: { domain: 'step1' }, store: 'step1_result' }, next_state: 'step1_done', }, { name: 'step2', from: 'step1_done', operation: { op: 'scanEnvironment', store: 'step2_result' }, next_state: 'step2_done', }, { name: 'finalize', from: 'step2_done', operation: { op: 'composeResult', store: 'final' }, next_state: 'final', }, ], final_state: 'final', }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); expect(result.metadata.operationsExecuted).toBe(3); }); it('should handle state machine error with skip', async () => { const directive: StateMachineDirective = { type: 'state_machine_directive', version: '1.0', initial_state: {}, transitions: [ { name: 'failing_step', from: 'initial', operation: 'nonexistent_tool', // This will fail (tool invocation not supported) next_state: 'result', on_error: 'skip', }, ], final_state: 'result', }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); // Skipped, so continued }); it('should handle state machine error with abort', async () => { const directive: StateMachineDirective = { type: 'state_machine_directive', version: '1.0', initial_state: {}, transitions: [ { name: 'failing_step', from: 'initial', operation: 'nonexistent_tool', next_state: 'result', on_error: 'abort', }, ], final_state: 'result', }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); }); describe('Operation Handlers', () => { describe('loadKnowledge', () => { it('should load knowledge for specified domain', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'knowledge_tool', description: 'Load knowledge test', sandbox_operations: [ { op: 'loadKnowledge', args: { domain: 'architecture', scope: 'global' }, store: 'k' }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); const knowledge = (result.data as Record<string, unknown>)['k']; expect(knowledge).toHaveProperty('domain', 'architecture'); expect(knowledge).toHaveProperty('scope', 'global'); }); }); describe('loadPrompt', () => { it('should load prompt file', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'prompt_tool', description: 'Load prompt test', sandbox_operations: [ { op: 'loadPrompt', args: { name: 'test-prompt' }, store: 'prompt' }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); }); it('should fail when prompt name is missing', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'prompt_tool', description: 'Missing prompt test', sandbox_operations: [{ op: 'loadPrompt', args: {}, store: 'prompt' }], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(false); expect(result.error).toContain('requires "name" argument'); }); }); describe('analyzeFiles', () => { it('should analyze files matching patterns', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'analyze_tool', description: 'Analyze files test', sandbox_operations: [ { op: 'analyzeFiles', args: { patterns: ['**/*.ts'], maxFiles: 10 }, store: 'files' }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); const files = (result.data as Record<string, unknown>)['files'] as Record<string, unknown>; expect(files).toHaveProperty('totalFiles'); expect(files).toHaveProperty('files'); expect(files['totalFiles'] as number).toBeGreaterThan(0); }); it('should respect maxFiles limit', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'analyze_tool', description: 'Max files test', sandbox_operations: [ { op: 'analyzeFiles', args: { patterns: ['**/*.ts'], maxFiles: 1 }, store: 'files' }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); const files = (result.data as Record<string, unknown>)['files'] as Record<string, unknown>; expect(files['totalFiles'] as number).toBeLessThanOrEqual(1); }); }); describe('scanEnvironment', () => { it('should scan environment configuration', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'scan_tool', description: 'Scan environment test', sandbox_operations: [{ op: 'scanEnvironment', store: 'env' }], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); const env = (result.data as Record<string, unknown>)['env'] as Record<string, unknown>; expect(env).toHaveProperty('configFiles'); expect(env).toHaveProperty('dependencies'); expect(env).toHaveProperty('hasTypeScript', true); }); }); describe('generateContext', () => { it('should generate context from inputs', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'context_tool', description: 'Generate context test', sandbox_operations: [ { op: 'loadKnowledge', args: { domain: 'test' }, store: 'input1' }, { op: 'scanEnvironment', store: 'input2' }, { op: 'generateContext', args: { type: 'analysis' }, inputs: ['input1', 'input2'], store: 'ctx', }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); const ctx = (result.data as Record<string, unknown>)['ctx'] as Record<string, unknown>; expect(ctx).toHaveProperty('type', 'analysis'); expect(ctx).toHaveProperty('inputCount', 2); }); }); describe('composeResult', () => { it('should compose result from state', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'compose_tool', description: 'Compose result test', sandbox_operations: [ { op: 'loadKnowledge', args: { domain: 'test' }, store: 'data' }, { op: 'composeResult', args: { template: 'report', format: 'markdown' }, store: 'r' }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); const composed = (result.data as Record<string, unknown>)['r'] as Record<string, unknown>; expect(composed).toHaveProperty('template', 'report'); expect(composed).toHaveProperty('format', 'markdown'); }); }); describe('validateOutput', () => { it('should validate non-null output', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'validate_tool', description: 'Validate output test', sandbox_operations: [ { op: 'loadKnowledge', args: { domain: 'test' }, store: 'data' }, { op: 'validateOutput', input: 'data', store: 'valid' }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); const valid = (result.data as Record<string, unknown>)['valid'] as Record<string, unknown>; expect(valid).toHaveProperty('valid', true); }); }); describe('cacheResult and retrieveCache', () => { it('should cache and retrieve results', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'cache_tool', description: 'Cache test', sandbox_operations: [ { op: 'loadKnowledge', args: { domain: 'test' }, store: 'data' }, { op: 'cacheResult', args: { key: 'test-cache', ttl: 60 }, input: 'data', store: 'cached', }, { op: 'retrieveCache', args: { key: 'test-cache' }, store: 'retrieved' }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); expect(result.data).toHaveProperty('retrieved'); }); it('should return null for non-existent cache', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'cache_tool', description: 'Missing cache test', sandbox_operations: [ { op: 'retrieveCache', args: { key: 'nonexistent' }, store: 'retrieved' }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); const retrieved = (result.data as Record<string, unknown>)['retrieved']; expect(retrieved).toBeNull(); }); }); }); describe('Condition Evaluation', () => { it('should evaluate exists condition', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'cond_tool', description: 'Exists condition test', sandbox_operations: [ { op: 'loadKnowledge', args: { domain: 'test' }, store: 'data' }, { op: 'scanEnvironment', store: 'env', condition: { key: 'data', operator: 'exists' }, }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); expect(result.data).toHaveProperty('env'); }); it('should evaluate truthy condition', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'cond_tool', description: 'Truthy condition test', sandbox_operations: [ { op: 'loadKnowledge', args: { domain: 'test' }, store: 'data' }, { op: 'scanEnvironment', store: 'env', condition: { key: 'data', operator: 'truthy' }, }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); expect(result.data).toHaveProperty('env'); }); it('should skip operation when condition fails', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'cond_tool', description: 'Failed condition test', sandbox_operations: [ { op: 'scanEnvironment', store: 'env', condition: { key: 'nonexistent', operator: 'exists' }, }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); expect(result.data).not.toHaveProperty('env'); }); }); describe('Resource Limits', () => { it('should have configurable timeout setting', () => { // Verify timeout configuration works const shortTimeoutExecutor = new SandboxExecutor({ sandbox: { enabled: true, timeout: 100, memoryLimit: 256 * 1024 * 1024, fsOperationsLimit: 1000, networkAllowed: false, }, }); expect(shortTimeoutExecutor['config'].sandbox.timeout).toBe(100); }); it('should track execution time in metadata', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'time_tool', description: 'Time tracking test', sandbox_operations: [ { op: 'loadKnowledge', args: { domain: 'test' }, store: 'k' }, { op: 'scanEnvironment', store: 'env' }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); expect(result.metadata.executionTime).toBeGreaterThanOrEqual(0); expect(typeof result.metadata.executionTime).toBe('number'); }); it('should enforce fs operations limit configuration', () => { const limitedExecutor = new SandboxExecutor({ sandbox: { enabled: true, timeout: 30000, memoryLimit: 256 * 1024 * 1024, fsOperationsLimit: 50, networkAllowed: false, }, }); expect(limitedExecutor['config'].sandbox.fsOperationsLimit).toBe(50); }); it('should enforce memory limit configuration', () => { const limitedExecutor = new SandboxExecutor({ sandbox: { enabled: true, timeout: 30000, memoryLimit: 128 * 1024 * 1024, fsOperationsLimit: 1000, networkAllowed: false, }, }); expect(limitedExecutor['config'].sandbox.memoryLimit).toBe(128 * 1024 * 1024); }); }); describe('Caching', () => { it('should cache operation results when cacheable is true', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'cache_tool', description: 'Caching test', sandbox_operations: [{ op: 'loadKnowledge', args: { domain: 'cache-test' }, store: 'k' }], metadata: { cacheable: true, }, }; // Execute twice await executor.executeDirective(directive, testProjectPath); const result2 = await executor.executeDirective(directive, testProjectPath); expect(result2.success).toBe(true); expect(result2.metadata.cachedOperations.length).toBeGreaterThan(0); }); it('should track cache statistics', () => { const stats = executor.getCacheStats(); expect(stats).toHaveProperty('operations'); expect(stats).toHaveProperty('prompts'); }); it('should clear caches', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'cache_tool', description: 'Cache clear test', sandbox_operations: [ { op: 'cacheResult', args: { key: 'test' }, input: undefined, store: 'cached' }, ], }; await executor.executeDirective(directive, testProjectPath); executor.clearCaches(); const stats = executor.getCacheStats(); expect(stats.operations).toBe(0); expect(stats.prompts).toBe(0); }); }); describe('Error Handling', () => { it('should handle unknown directive type', async () => { const invalidDirective = { type: 'invalid_type', version: '1.0', } as unknown as OrchestrationDirective; const result = await executor.executeDirective(invalidDirective, testProjectPath); expect(result.success).toBe(false); expect(result.error).toContain('Unknown directive type'); }); it('should handle unknown operation type', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'error_tool', description: 'Unknown op test', sandbox_operations: [{ op: 'unknownOperation' as any, store: 'result' }], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(false); expect(result.error).toContain('Unknown operation'); }); it('should handle state machine with missing transition', async () => { const directive: StateMachineDirective = { type: 'state_machine_directive', version: '1.0', initial_state: {}, transitions: [ { name: 'step1', from: 'initial', operation: { op: 'loadKnowledge', store: 'result' }, next_state: 'step2', // No transition from step2 }, ], final_state: 'final', }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(false); expect(result.error).toContain('No transition found'); }); it('should return execution metadata on error', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'error_tool', description: 'Error metadata test', sandbox_operations: [ { op: 'loadKnowledge', store: 'k' }, { op: 'unknownOperation' as any, store: 'err' }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(false); expect(result.metadata).toBeDefined(); expect(result.metadata.executionTime).toBeGreaterThanOrEqual(0); expect(result.metadata.operationsExecuted).toBe(1); // First op succeeded }); }); describe('Global Singleton', () => { it('should return same instance from getSandboxExecutor', () => { resetSandboxExecutor(); const exec1 = getSandboxExecutor(); const exec2 = getSandboxExecutor(); expect(exec1).toBe(exec2); }); it('should reset global instance', () => { const exec1 = getSandboxExecutor(); resetSandboxExecutor(); const exec2 = getSandboxExecutor(); expect(exec1).not.toBe(exec2); }); it('should accept config on first call only', () => { resetSandboxExecutor(); const exec1 = getSandboxExecutor({ sandbox: { ...DEFAULT_CEMCP_CONFIG.sandbox, timeout: 99999 }, }); const exec2 = getSandboxExecutor({ sandbox: { ...DEFAULT_CEMCP_CONFIG.sandbox, timeout: 11111 }, }); // Second config should be ignored expect(exec1).toBe(exec2); expect(exec1['config'].sandbox.timeout).toBe(99999); }); }); describe('Security', () => { it('should restrict file access to project path', async () => { const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'security_tool', description: 'Security test', sandbox_operations: [ { op: 'analyzeFiles', args: { patterns: ['**/*.ts'] }, store: 'files' }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); const files = (result.data as Record<string, unknown>)['files'] as Record<string, unknown>; const fileList = files['files'] as Array<{ path: string }>; // All paths should be relative to project for (const file of fileList) { expect(file.path.startsWith('/')).toBe(true); // Paths start with / (relative from project root) expect(file.path).not.toContain('..'); } }); it('should exclude node_modules and hidden directories', async () => { // Create node_modules and hidden directory await mkdir(join(testProjectPath, 'node_modules', 'dep'), { recursive: true }); await mkdir(join(testProjectPath, '.hidden'), { recursive: true }); await writeFile( join(testProjectPath, 'node_modules', 'dep', 'index.ts'), 'export default {};' ); await writeFile(join(testProjectPath, '.hidden', 'secret.ts'), 'const secret = "hidden";'); const directive: OrchestrationDirective = { type: 'orchestration_directive', version: '1.0', tool: 'security_tool', description: 'Exclusion test', sandbox_operations: [ { op: 'analyzeFiles', args: { patterns: ['**/*.ts'] }, store: 'files' }, ], }; const result = await executor.executeDirective(directive, testProjectPath); expect(result.success).toBe(true); const files = (result.data as Record<string, unknown>)['files'] as Record<string, unknown>; const fileList = files['files'] as Array<{ path: string }>; // No files from node_modules or hidden directories for (const file of fileList) { expect(file.path).not.toContain('node_modules'); expect(file.path).not.toContain('.hidden'); } }); it('should not allow network access by default', () => { expect(DEFAULT_CEMCP_CONFIG.sandbox.networkAllowed).toBe(false); }); }); });

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/tosin2013/mcp-adr-analysis-server'

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