/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import * as http from 'http'; import type { Config } from '@vybestack/llxprt-code-core'; import type { Profile } from '@vybestack/llxprt-code-settings'; import { MessageBus } from '@vybestack/llxprt-code-core'; import { AgentClient, CoreToolScheduler, } from '@vybestack/llxprt-code-agents/internals.js'; import { createTaskToolRegistration } from '@vybestack/llxprt-code-agents'; /** * Creates a temporary directory for tests * @returns Path to the created temporary directory */ export async function createTempDirectory(): Promise { const prefix = 'llxprt-test-'; const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); return tempDir; } /** * Cleans up a temporary directory and all its contents * @param dir - Directory path to clean up */ export async function cleanupTempDirectory(dir: string): Promise { try { await fs.rm(dir, { recursive: true, force: true }); } catch (error: unknown) { // Ignore errors if directory doesn't exist if ( error !== null && typeof error === 'object' && 'code' in error && (error as { code: string }).code !== 'ENOENT' ) { throw error; } } } function attachTestAgentFactories(config: Config): void { Object.defineProperties(config, { agentClientFactory: { value: ( cfg: Config, runtimeState: ConstructorParameters[1], ) => new AgentClient(cfg, runtimeState), configurable: true, }, toolSchedulerFactory: { value: (options: ConstructorParameters[0]) => new CoreToolScheduler(options), configurable: true, }, taskToolRegistration: { value: createTaskToolRegistration(), configurable: true, }, }); } export async function initializeTestConfig( config: Config, ): Promise { attachTestAgentFactories(config); const sessionMessageBus = new MessageBus( config.getPolicyEngine(), config.getDebugMode(), ); await ( config as Config & { initialize(dependencies?: { messageBus?: MessageBus }): Promise; } ).initialize({ messageBus: sessionMessageBus }); return sessionMessageBus; } /** * Creates a profile file in the specified directory * @param dir - Directory to create the profile in * @param name - Name of the profile (without .json extension) * @param profile - Profile data to write */ export async function createTempProfile( dir: string, name: string, profile: Profile, ): Promise { const profilesDir = path.join(dir, '.llxprt', 'profiles'); await fs.mkdir(profilesDir, { recursive: true }); const profilePath = path.join(profilesDir, `${name}.json`); await fs.writeFile(profilePath, JSON.stringify(profile, null, 2), 'utf8'); } /** * Creates a keyfile with proper permissions (600) * @param dir - Directory to create the keyfile in * @param apiKey - API key content to write * @returns Path to the created keyfile */ export async function createTempKeyfile( dir: string, apiKey: string, ): Promise { const keysDir = path.join(dir, '.keys'); await fs.mkdir(keysDir, { recursive: true }); const keyfilePath = path.join(keysDir, 'api-key'); await fs.writeFile(keyfilePath, apiKey, { mode: 0o600 }); return keyfilePath; } /** * Reads and parses settings.json from the specified directory * @param dir - Directory containing .llxprt/settings.json * @returns Parsed settings object */ export async function readSettingsFile(dir: string): Promise { const settingsPath = path.join(dir, '.llxprt', 'settings.json'); try { const content = await fs.readFile(settingsPath, 'utf8'); return JSON.parse(content); } catch (error: unknown) { if ( error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT' ) { return {}; } throw error; } } /** * Writes settings.json to the specified directory * @param dir - Directory to write .llxprt/settings.json in * @param settings - Settings object to write */ export async function writeSettingsFile( dir: string, settings: unknown, ): Promise { const llxprtDir = path.join(dir, '.llxprt'); await fs.mkdir(llxprtDir, { recursive: true }); const settingsPath = path.join(llxprtDir, 'settings.json'); await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf8'); } /** * Waits for a file to exist with timeout * @param filepath - Path to the file to wait for * @param timeout - Maximum time to wait in milliseconds * @returns Promise that resolves when file exists or rejects on timeout */ export async function waitForFile( filepath: string, timeout: number, ): Promise { const startTime = Date.now(); const checkInterval = 100; // Check every 100ms while (Date.now() - startTime < timeout) { try { await fs.access(filepath); return; // File exists } catch { // File doesn't exist yet, wait and try again await new Promise((resolve) => setTimeout(resolve, checkInterval)); } } throw new Error(`Timeout waiting for file: ${filepath}`); } interface MockApiServerRequest { method?: string; url?: string; headers: http.IncomingHttpHeaders; body: string; } interface MockApiServer { server: http.Server; port: number; requests: MockApiServerRequest[]; close: () => Promise; } /** * Creates a simple HTTP server that logs requests * @returns Mock server object with server instance, port, and request log */ export async function createMockApiServer(): Promise { const requests: MockApiServerRequest[] = []; const server = http.createServer((req, res) => { let body = ''; req.on('data', (chunk) => { body += chunk.toString(); }); req.on('end', () => { requests.push({ method: req.method, url: req.url, headers: req.headers, body, }); // Send a simple success response res.writeHead(200, { 'Content-Type': 'application/json' }); res.end( JSON.stringify({ id: 'mock-chat-completion', object: 'chat.completion', created: Math.floor(Date.now() / 1000), model: 'gpt-4o-mini', choices: [ { index: 0, message: { role: 'assistant', content: 'Mock response from integration test', }, finish_reason: 'stop', }, ], usage: { prompt_tokens: 8, completion_tokens: 12, total_tokens: 20, }, }), ); }); }); // Find an available port await new Promise((resolve) => { server.listen(0, '127.0.0.1', () => { resolve(); }); }); const address = server.address(); if (address === null || typeof address === 'string') { throw new Error('Failed to get server port'); } return { server, port: address.port, requests, close: () => new Promise((resolve, reject) => { server.close((err) => { if (err) { reject(err); } else { resolve(); } }); }), }; }