const { describe, test, expect, beforeEach, afterEach, mock } = require('bun:test') const { spawn } = require('child_process') const { randomUUID, randomBytes } = require('crypto') const path = require('path') const OPENROUTER_STATIC_FALLBACK_MODELS = [ 'anthropic/claude-3.5-sonnet', 'openai/gpt-4o-mini', 'mistralai/mistral-large-latest', 'google/gemini-flash-1.5', ] const DEFAULT_PROVIDER_SEEDS = ['openrouter', 'mistral', 'google', 'groq', 'nvidia', 'chutes', 'cerebras', 'ollama', 'opencode', 'cohere', 'kilo'] const KIB = 1024 const MIB = KIB * 1024 const GIB = MIB * 1024 const TIB = GIB * 1024 function parseMemoryValue(raw) { if (raw === undefined || raw === null) return 0 const str = String(raw).trim() const match = str.match(/^([0-9.]+)\s*([kKmMgGtT]i?)?(?:[bB])?$/) if (!match) return Number(str) || 0 const value = parseFloat(match[1]) const unit = (match[2] || '').toLowerCase() const multipliers = { '': 1, k: KIB, ki: KIB, m: MIB, mi: MIB, g: GIB, gi: GIB, t: TIB, ti: TIB } if (!Number.isFinite(value) || value <= 0) return 0 return Math.round(value * (multipliers[unit] || 1)) } function resolvePositiveInt(raw, fallback) { const parsed = Number(raw) if (!Number.isFinite(parsed) || parsed <= 0) return fallback return Math.round(parsed) } function resolveIntervalMs(raw, fallback, min) { const parsed = Number(raw) if (!Number.isFinite(parsed) || parsed <= 0) return fallback return Math.max(parsed, min) } function formatDuration(ms) { if (ms < 1000) return Math.round(ms) + 'ms' if (ms < 60000) return (ms / 1000).toFixed(1) + 's' if (ms < 3600000) return (ms / 60000).toFixed(1) + 'm' if (ms < 86400000) return (ms / 3600000).toFixed(1) + 'h' return (ms / 86400000).toFixed(1) + 'd' } describe('OpenCode Integration', () => { describe('Provider Configuration', () => { test('has default provider seeds defined', () => { expect(DEFAULT_PROVIDER_SEEDS.length).toBeGreaterThan(0) expect(DEFAULT_PROVIDER_SEEDS).toContain('openrouter') expect(DEFAULT_PROVIDER_SEEDS).toContain('mistral') expect(DEFAULT_PROVIDER_SEEDS).toContain('google') }) test('opencode is the default fallback provider', () => { const DEFAULT_PROVIDER_FALLBACK = 'opencode' expect(DEFAULT_PROVIDER_SEEDS).toContain(DEFAULT_PROVIDER_FALLBACK) }) test('openrouter has fallback models', () => { expect(OPENROUTER_STATIC_FALLBACK_MODELS.length).toBeGreaterThan(0) }) }) describe('Model Discovery', () => { const modelDiscoveryConfig = { openrouter: { url: 'https://openrouter.ai/api/v1/models', requiresAuth: true, }, mistral: { url: 'https://api.mistral.ai/v1/models', requiresAuth: true, }, google: { url: 'https://generativelanguage.googleapis.com/v1beta2/models', requiresAuth: true, }, } test('has discovery configuration for providers', () => { expect(modelDiscoveryConfig.openrouter.url).toBeDefined() expect(modelDiscoveryConfig.mistral.url).toBeDefined() expect(modelDiscoveryConfig.google.url).toBeDefined() }) test('openrouter discovery requires authentication', () => { expect(modelDiscoveryConfig.openrouter.requiresAuth).toBe(true) }) test('validates model ID format', () => { const validModelId = 'anthropic/claude-3.5-sonnet' const invalidModelId = 'invalid-model-format' const modelIdPattern = /^[a-z0-9-]+\/[a-z0-9.-]+$/i expect(modelIdPattern.test(validModelId)).toBe(true) expect(modelIdPattern.test(invalidModelId)).toBe(false) }) test('extracts provider from model ID', () => { function extractProvider(modelId) { const parts = modelId?.split('/') return parts?.length === 2 ? parts[0] : null } expect(extractProvider('anthropic/claude-3.5-sonnet')).toBe('anthropic') expect(extractProvider('openai/gpt-4o')).toBe('openai') expect(extractProvider('invalid')).toBe(null) }) }) describe('Message Building', () => { function buildOpenRouterMessages(history, currentMessage) { const prior = history .filter((m) => m && m.id !== currentMessage?.id && m.status === 'done') .slice(-8) const mapped = prior.map((m) => { if (m.role === 'assistant') { return { role: 'assistant', content: String(m.reply || m.partialOutput || '') } } return { role: 'user', content: String(m.content || '') } }).filter((m) => (m.content || '').trim().length) const text = String(currentMessage?.content || '').trim() const userMsg = { role: 'user', content: text } return mapped.concat([userMsg]) } test('builds message history for OpenRouter', () => { const history = [ { id: '1', role: 'user', content: 'Hello', status: 'done' }, { id: '2', role: 'assistant', reply: 'Hi there!', status: 'done' }, { id: '3', role: 'user', content: 'How are you?', status: 'done' }, ] const currentMessage = { id: '4', content: 'Good thanks' } const messages = buildOpenRouterMessages(history, currentMessage) expect(messages.length).toBe(4) expect(messages[0].role).toBe('user') expect(messages[0].content).toBe('Hello') expect(messages[1].role).toBe('assistant') expect(messages[3].content).toBe('Good thanks') }) test('filters out non-done messages', () => { const history = [ { id: '1', role: 'user', content: 'Hello', status: 'done' }, { id: '2', role: 'assistant', reply: 'Hi', status: 'running' }, { id: '3', role: 'user', content: 'Test', status: 'queued' }, ] const currentMessage = { id: '4', content: 'Current' } const messages = buildOpenRouterMessages(history, currentMessage) expect(messages.length).toBe(2) expect(messages[0].content).toBe('Hello') }) test('limits history to last 8 messages', () => { const history = Array.from({ length: 15 }, (_, i) => ({ id: String(i), role: 'user', content: `Message ${i}`, status: 'done', })) const currentMessage = { id: 'current', content: 'Current' } const messages = buildOpenRouterMessages(history, currentMessage) expect(messages.length).toBe(9) }) test('filters empty content', () => { const history = [ { id: '1', role: 'user', content: '', status: 'done' }, { id: '2', role: 'assistant', reply: ' ', status: 'done' }, { id: '3', role: 'user', content: 'Valid', status: 'done' }, ] const currentMessage = { id: '4', content: 'Current' } const messages = buildOpenRouterMessages(history, currentMessage) expect(messages.length).toBe(2) }) }) describe('Process Management', () => { const processConfig = { maxConcurrency: 5, timeout: 300000, retryAttempts: 3, retryDelay: 1000, } test('enforces maximum concurrency', () => { const runningProcesses = new Map() const maxConcurrency = processConfig.maxConcurrency function canStartNewProcess() { return runningProcesses.size < maxConcurrency } for (let i = 0; i < maxConcurrency; i++) { runningProcesses.set(`process-${i}`, { started: Date.now() }) } expect(canStartNewProcess()).toBe(false) runningProcesses.delete('process-0') expect(canStartNewProcess()).toBe(true) }) test('tracks process start time', () => { const processInfo = { id: randomUUID(), started: Date.now(), sessionId: 'session-1', messageId: 'message-1', } expect(processInfo.started).toBeDefined() expect(processInfo.started).toBeLessThanOrEqual(Date.now()) }) test('calculates process duration', () => { const startTime = Date.now() - 5000 const duration = Date.now() - startTime expect(duration).toBeGreaterThanOrEqual(5000) }) test('generates unique process IDs', () => { const ids = new Set() for (let i = 0; i < 100; i++) { ids.add(randomUUID()) } expect(ids.size).toBe(100) }) }) describe('Error Handling', () => { const ERROR_DETAIL_LIMIT = 400 function truncateError(message, limit = ERROR_DETAIL_LIMIT) { if (!message || typeof message !== 'string') return '' if (message.length <= limit) return message return message.slice(0, limit) + '...' } test('truncates long error messages', () => { const longError = 'x'.repeat(500) const truncated = truncateError(longError) expect(truncated.length).toBe(ERROR_DETAIL_LIMIT + 3) expect(truncated.endsWith('...')).toBe(true) }) test('keeps short error messages intact', () => { const shortError = 'Error: something went wrong' const result = truncateError(shortError) expect(result).toBe(shortError) }) test('handles null/undefined errors', () => { expect(truncateError(null)).toBe('') expect(truncateError(undefined)).toBe('') }) test('classifies error types', () => { function classifyError(error) { const msg = String(error || '').toLowerCase() if (msg.includes('timeout')) return 'timeout' if (msg.includes('rate limit')) return 'rate_limit' if (msg.includes('auth')) return 'auth' if (msg.includes('invalid')) return 'validation' return 'unknown' } expect(classifyError('Request timeout')).toBe('timeout') expect(classifyError('Rate limit exceeded')).toBe('rate_limit') expect(classifyError('Authentication failed')).toBe('auth') expect(classifyError('Invalid input')).toBe('validation') expect(classifyError('Something else')).toBe('unknown') }) }) describe('Stream Processing', () => { function parseSSELine(line) { if (!line || typeof line !== 'string') return null if (line.startsWith('data: ')) { const data = line.slice(6).trim() if (data === '[DONE]') return { done: true } try { return { data: JSON.parse(data) } } catch { return { data } } } return null } test('parses SSE data lines', () => { const line = 'data: {"content": "Hello"}' const parsed = parseSSELine(line) expect(parsed.data.content).toBe('Hello') }) test('handles SSE done signal', () => { const line = 'data: [DONE]' const parsed = parseSSELine(line) expect(parsed.done).toBe(true) }) test('handles malformed JSON in SSE', () => { const line = 'data: not valid json' const parsed = parseSSELine(line) expect(parsed.data).toBe('not valid json') }) test('ignores non-data lines', () => { expect(parseSSELine(': comment')).toBe(null) expect(parseSSELine('')).toBe(null) expect(parseSSELine('event: message')).toBe(null) }) test('accumulates stream chunks', () => { const chunks = [ { choices: [{ delta: { content: 'Hello' } }] }, { choices: [{ delta: { content: ' world' } }] }, { choices: [{ delta: { content: '!' } }] }, ] const content = chunks .map(c => c.choices?.[0]?.delta?.content || '') .join('') expect(content).toBe('Hello world!') }) }) describe('Resource Limits', () => { test('parses memory values correctly', () => { expect(parseMemoryValue('512M')).toBe(512 * MIB) expect(parseMemoryValue('1G')).toBe(GIB) expect(parseMemoryValue('2GB')).toBe(2 * GIB) expect(parseMemoryValue('1024')).toBe(1024) }) test('handles null/undefined memory values', () => { expect(parseMemoryValue(null)).toBe(0) expect(parseMemoryValue(undefined)).toBe(0) }) test('resolves positive integers with fallback', () => { expect(resolvePositiveInt('100', 50)).toBe(100) expect(resolvePositiveInt('invalid', 50)).toBe(50) expect(resolvePositiveInt(-5, 50)).toBe(50) }) test('enforces minimum interval', () => { expect(resolveIntervalMs('100', 1000, 500)).toBe(500) expect(resolveIntervalMs('2000', 1000, 500)).toBe(2000) expect(resolveIntervalMs('invalid', 1000, 500)).toBe(1000) }) test('formats durations correctly', () => { expect(formatDuration(500)).toBe('500ms') expect(formatDuration(5000)).toBe('5.0s') expect(formatDuration(90000)).toBe('1.5m') expect(formatDuration(7200000)).toBe('2.0h') expect(formatDuration(86400000)).toBe('1.0d') }) }) describe('Provider Fallback', () => { function selectNextProvider(currentProvider, failedProviders = new Set()) { const available = DEFAULT_PROVIDER_SEEDS.filter(p => !failedProviders.has(p)) const currentIndex = DEFAULT_PROVIDER_SEEDS.indexOf(currentProvider) for (let i = 1; i < DEFAULT_PROVIDER_SEEDS.length; i++) { const nextIndex = (currentIndex + i) % DEFAULT_PROVIDER_SEEDS.length const nextProvider = DEFAULT_PROVIDER_SEEDS[nextIndex] if (!failedProviders.has(nextProvider)) { return nextProvider } } return null } test('selects next available provider on failure', () => { const next = selectNextProvider('openrouter', new Set(['openrouter'])) expect(next).toBeDefined() expect(next).not.toBe('openrouter') }) test('returns null when all providers failed', () => { const allFailed = new Set(DEFAULT_PROVIDER_SEEDS) const next = selectNextProvider('openrouter', allFailed) expect(next).toBe(null) }) test('selects next model from fallback list', () => { const usedModels = new Set(['anthropic/claude-3.5-sonnet']) const nextModel = OPENROUTER_STATIC_FALLBACK_MODELS.find(m => !usedModels.has(m)) expect(nextModel).toBeDefined() expect(nextModel).not.toBe('anthropic/claude-3.5-sonnet') }) test('returns undefined when all fallback models used', () => { const usedModels = new Set(OPENROUTER_STATIC_FALLBACK_MODELS) const nextModel = OPENROUTER_STATIC_FALLBACK_MODELS.find(m => !usedModels.has(m)) expect(nextModel).toBeUndefined() }) }) describe('External Directory Restriction', () => { const ENABLE_EXTERNAL_DIR_RESTRICTION = true function validateExternalDirectory(sessionId, requestedPath) { if (!ENABLE_EXTERNAL_DIR_RESTRICTION) return { allowed: true } const normalizedPath = path.normalize(requestedPath || '') const blockedPatterns = [ /^(?:[a-zA-Z]:|\\\\|\/\/)/, /\.\./, /^\/etc\//, /^\/var\//, /^\/home\//, ] for (const pattern of blockedPatterns) { if (pattern.test(normalizedPath)) { return { allowed: false, reason: 'Path not allowed' } } } return { allowed: true } } test('blocks absolute paths on Windows', () => { const result = validateExternalDirectory('session-1', 'C:\\Windows\\System32') expect(result.allowed).toBe(false) }) test('blocks UNC paths', () => { const result = validateExternalDirectory('session-1', '\\\\server\\share') expect(result.allowed).toBe(false) }) test('blocks parent directory traversal', () => { const result = validateExternalDirectory('session-1', '../../../etc/passwd') expect(result.allowed).toBe(false) }) test('allows relative paths within workspace', () => { const result = validateExternalDirectory('session-1', './src/file.js') expect(result.allowed).toBe(true) }) }) }) describe('OpenCode CLI Integration', () => { describe('CLI Path Resolution', () => { test('resolves opencode CLI path', () => { const REPO_ROOT = process.cwd() const OPENCODE_REPO_ROOT = path.join(REPO_ROOT, 'opencode') const OPENCODE_REPO_CLI = path.join(OPENCODE_REPO_ROOT, 'packages', 'opencode', 'bin', 'opencode') expect(OPENCODE_REPO_CLI).toContain('opencode') expect(OPENCODE_REPO_CLI).toContain('bin') }) test('validates CLI existence pattern', () => { const cliPath = 'packages/opencode/bin/opencode' expect(cliPath.endsWith('opencode')).toBe(true) }) }) describe('Environment Configuration', () => { test('has required environment variable patterns', () => { const envPatterns = { OPENROUTER_API_KEY: /^sk-or-v1-/, MISTRAL_API_KEY: /^[a-zA-Z0-9]+$/, GOOGLE_API_KEY: /^AIza/, } expect(Object.keys(envPatterns).length).toBeGreaterThan(0) }) test('generates default values for missing env vars', () => { const defaults = { PORT: 4000, HOST: '0.0.0.0', DEFAULT_PLAN: 'hobby', DEFAULT_PROVIDER_FALLBACK: 'opencode', } expect(defaults.PORT).toBe(4000) expect(defaults.DEFAULT_PLAN).toBe('hobby') }) }) })