Skip to main content
Glama
integration.test.ts13.4 kB
import { describe, expect, it, beforeAll, afterAll, beforeEach } from 'vitest'; import * as grpc from '@grpc/grpc-js'; import { existsSync, rmSync, unlinkSync } from 'node:fs'; import { startServer } from '../server/index.js'; import { SandboxClient } from '../client/index.js'; // Generate unique ID for test isolation const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; describe('Integration Tests - Client/Server Communication', () => { const testSocketPath = `/tmp/prodisco-sandbox-test-${uniqueId}.sock`; const testCacheDir = `/tmp/prodisco-cache-integration-${uniqueId}`; let server: grpc.Server; let client: SandboxClient; beforeAll(async () => { // Start the server server = await startServer({ socketPath: testSocketPath, cacheDir: testCacheDir, }); // Create client client = new SandboxClient({ socketPath: testSocketPath }); // Wait for server to be healthy const healthy = await client.waitForHealthy(5000); expect(healthy).toBe(true); }); afterAll(async () => { // Close client client.close(); // Shutdown server await new Promise<void>((resolve) => { server.tryShutdown((err) => { if (err) { server.forceShutdown(); } resolve(); }); }); // Clean up socket file if (existsSync(testSocketPath)) { try { unlinkSync(testSocketPath); } catch { // Ignore } } // Clean up cache directory if (existsSync(testCacheDir)) { rmSync(testCacheDir, { recursive: true }); } }); describe('Health Check', () => { it('returns healthy status', async () => { const result = await client.healthCheck(); expect(result.healthy).toBe(true); }); it('returns kubernetes context', async () => { const result = await client.healthCheck(); expect(result.kubernetesContext).toBeDefined(); expect(typeof result.kubernetesContext).toBe('string'); }); }); describe('Code Execution', () => { it('executes simple code', async () => { const result = await client.execute({ code: 'console.log("hello from integration test")', }); expect(result.success).toBe(true); expect(result.output).toBe('hello from integration test'); expect(result.error).toBeUndefined(); }); it('handles multiple console outputs', async () => { const result = await client.execute({ code: ` console.log("line 1"); console.log("line 2"); console.log("line 3"); `, }); expect(result.success).toBe(true); expect(result.output).toBe('line 1\nline 2\nline 3'); }); it('handles async code', async () => { const result = await client.execute({ code: ` await new Promise(r => setTimeout(r, 10)); console.log("async complete"); `, }); expect(result.success).toBe(true); expect(result.output).toBe('async complete'); }); it('handles TypeScript code', async () => { const result = await client.execute({ code: ` interface User { name: string; } const user: User = { name: "Test" }; console.log(user.name); `, }); expect(result.success).toBe(true); expect(result.output).toBe('Test'); }); it('handles execution errors', async () => { const result = await client.execute({ code: 'throw new Error("intentional error")', }); expect(result.success).toBe(false); expect(result.error).toBe('intentional error'); }); it('respects timeout', async () => { const result = await client.execute({ code: 'await new Promise(r => setTimeout(r, 10000))', timeoutMs: 100, }); expect(result.success).toBe(false); expect(result.error).toBe('Script execution timed out'); }); it('returns execution time', async () => { const result = await client.execute({ code: 'console.log("timing test")', }); expect(result.success).toBe(true); expect(result.executionTimeMs).toBeGreaterThanOrEqual(0); }); }); describe('Caching', () => { it('caches successful executions and returns CacheEntry', async () => { const code = `console.log("cache integration test ${Date.now()}")`; const result = await client.execute({ code }); expect(result.success).toBe(true); expect(result.cached).toBeDefined(); expect(result.cached!.name).toMatch(/^script-.*\.ts$/); expect(result.cached!.description).toBeDefined(); expect(result.cached!.contentHash).toBeDefined(); }); it('can execute cached scripts', async () => { const uniqueValue = `unique-${Date.now()}`; const code = `console.log("${uniqueValue}")`; // First execution - gets cached const firstResult = await client.execute({ code }); expect(firstResult.success).toBe(true); expect(firstResult.cached).toBeDefined(); // Execute from cache const cachedResult = await client.execute({ cached: firstResult.cached!.name, }); expect(cachedResult.success).toBe(true); expect(cachedResult.output).toBe(uniqueValue); // When running from cache, cached is not returned (not a new cache) expect(cachedResult.cached).toBeUndefined(); }); it('returns error for non-existent cached script', async () => { const result = await client.execute({ cached: 'non-existent-script-12345', }); expect(result.success).toBe(false); expect(result.error).toContain('not found'); }); it('deduplicates cache for identical code', async () => { const code = `console.log("dedupe test ${Date.now()}")`; const result1 = await client.execute({ code }); const result2 = await client.execute({ code }); expect(result1.success).toBe(true); expect(result2.success).toBe(true); expect(result1.cached).toBeDefined(); // Second execution of same code should not create new cache entry expect(result2.cached).toBeUndefined(); }); }); describe('Convenience Globals', () => { it('does not provide any Kubernetes convenience globals (k8s/kc)', async () => { const result = await client.execute({ code: 'console.log(typeof k8s, typeof kc)', }); expect(result.success).toBe(true); expect(result.output).toBe('undefined undefined'); }); }); describe('Require Support', () => { it('allows require of @kubernetes/client-node', async () => { const result = await client.execute({ code: ` const k8s = require('@kubernetes/client-node'); console.log(typeof k8s.KubeConfig); `, }); expect(result.success).toBe(true); expect(result.output).toBe('function'); }); it('allows require of @prodisco/prometheus-client', async () => { const result = await client.execute({ code: ` const prom = require('@prodisco/prometheus-client'); console.log(typeof prom.PrometheusClient); `, }); expect(result.success).toBe(true); expect(result.output).toBe('function'); }); it('blocks require of fs', async () => { const result = await client.execute({ code: 'const fs = require("fs")', }); expect(result.success).toBe(false); expect(result.error).toContain("Module 'fs' not available"); }); }); describe('Concurrent Requests', () => { it('handles multiple concurrent executions', async () => { const requests = Array.from({ length: 10 }, (_, i) => ({ code: `console.log("concurrent ${i}")`, })); const results = await Promise.all( requests.map(req => client.execute(req)) ); expect(results.every(r => r.success)).toBe(true); results.forEach((result, i) => { expect(result.output).toBe(`concurrent ${i}`); }); }); it('handles mixed success/failure concurrent requests', async () => { const requests = [ { code: 'console.log("success")' }, { code: 'throw new Error("fail")' }, { code: 'console.log("success 2")' }, ]; const results = await Promise.all( requests.map(req => client.execute(req)) ); expect(results[0].success).toBe(true); expect(results[1].success).toBe(false); expect(results[2].success).toBe(true); }); }); describe('Error Handling', () => { it('handles syntax errors', async () => { const result = await client.execute({ code: 'const x = {', }); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); it('handles runtime errors', async () => { const result = await client.execute({ code: 'undefinedVariable.property', }); expect(result.success).toBe(false); expect(result.error).toContain('undefinedVariable'); }); it('handles async rejection', async () => { const result = await client.execute({ code: 'await Promise.reject(new Error("async fail"))', }); expect(result.success).toBe(false); expect(result.error).toBe('async fail'); }); it('preserves output before error', async () => { const result = await client.execute({ code: ` console.log("before"); throw new Error("after"); `, }); expect(result.success).toBe(false); expect(result.output).toBe('before'); expect(result.error).toBe('after'); }); }); describe('Edge Cases', () => { it('handles empty code (treated as no source by client)', async () => { // Note: The client treats empty string as falsy, so source is undefined // This results in "Either code or cached must be provided" error const result = await client.execute({ code: '' }); expect(result.success).toBe(false); expect(result.error).toBe('Either code or cached must be provided'); }); it('handles code with only whitespace', async () => { // Whitespace is truthy, so this gets executed const result = await client.execute({ code: ' ' }); expect(result.success).toBe(true); expect(result.output).toBe(''); }); it('handles code with Unicode', async () => { const result = await client.execute({ code: 'console.log("Hello 世界 🌍")', }); expect(result.success).toBe(true); expect(result.output).toBe('Hello 世界 🌍'); }); it('handles large output', async () => { const result = await client.execute({ code: ` for (let i = 0; i < 100; i++) { console.log("line " + i); } `, }); expect(result.success).toBe(true); expect(result.output.split('\n').length).toBe(100); }); }); }); // Generate unique ID for SandboxClient tests const clientUniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; describe('SandboxClient', () => { const testSocketPath = `/tmp/prodisco-client-test-${clientUniqueId}.sock`; const testCacheDir = `/tmp/prodisco-cache-client-${clientUniqueId}`; let server: grpc.Server; beforeAll(async () => { server = await startServer({ socketPath: testSocketPath, cacheDir: testCacheDir, }); }); afterAll(async () => { await new Promise<void>((resolve) => { server.tryShutdown((err) => { if (err) { server.forceShutdown(); } resolve(); }); }); if (existsSync(testSocketPath)) { try { unlinkSync(testSocketPath); } catch { // Ignore } } if (existsSync(testCacheDir)) { rmSync(testCacheDir, { recursive: true }); } }); describe('Constructor', () => { it('uses provided socket path', () => { const client = new SandboxClient({ socketPath: testSocketPath }); expect(client).toBeInstanceOf(SandboxClient); client.close(); }); it('uses environment variable for socket path', () => { const originalEnv = process.env.SANDBOX_SOCKET_PATH; process.env.SANDBOX_SOCKET_PATH = testSocketPath; const client = new SandboxClient(); expect(client).toBeInstanceOf(SandboxClient); client.close(); if (originalEnv) { process.env.SANDBOX_SOCKET_PATH = originalEnv; } else { delete process.env.SANDBOX_SOCKET_PATH; } }); }); describe('waitForHealthy', () => { it('returns true when server is healthy', async () => { const client = new SandboxClient({ socketPath: testSocketPath }); const result = await client.waitForHealthy(5000); expect(result).toBe(true); client.close(); }); it('returns false when timeout expires', async () => { const client = new SandboxClient({ socketPath: '/tmp/non-existent-socket.sock' }); const result = await client.waitForHealthy(100, 50); expect(result).toBe(false); client.close(); }); }); describe('close', () => { it('closes the client connection', () => { const client = new SandboxClient({ socketPath: testSocketPath }); expect(() => client.close()).not.toThrow(); }); }); });

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/harche/ProDisco'

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