// tests/api/utils.ts import { writeFile, mkdir } from 'fs/promises'; import { join } from 'path'; import { config, endpoints } from './config'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Colors for console output const colors = { reset: '\x1b[0m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[34m', gray: '\x1b[90m', cyan: '\x1b[36m', }; // Logging utilities export const log = { success: (msg: string) => console.log(`${colors.green}✓${colors.reset} ${msg}`), error: (msg: string) => console.log(`${colors.red}✗${colors.reset} ${msg}`), info: (msg: string) => console.log(`${colors.blue}→${colors.reset} ${msg}`), warning: (msg: string) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`), section: (msg: string) => console.log(`\n${colors.cyan}━━━ ${msg} ━━━${colors.reset}`), detail: (key: string, value: any) => { const valueStr = typeof value === 'object' ? JSON.stringify(value, null, 2) : value; console.log(` ${colors.gray}${key}:${colors.reset} ${valueStr}`); }, }; // API fetch wrapper export async function api( endpoint: string, options: RequestInit & { expectError?: boolean; timeout?: number; } = {} ): Promise<{ data: T; status: number; headers: Headers; duration: number; }> { const { expectError = false, timeout = config.requestTimeout, ...fetchOptions } = options; const url = `${config.baseURL}${endpoint}`; const startTime = Date.now(); try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); const response = await fetch(url, { ...fetchOptions, headers: { 'X-API-Key': config.apiKey, ...fetchOptions.headers, }, signal: controller.signal, }); clearTimeout(timeoutId); const duration = Date.now() - startTime; let data: any; const contentType = response.headers.get('content-type'); if (contentType?.includes('application/json')) { data = await response.json(); } else if (contentType?.includes('image/')) { data = await response.arrayBuffer(); } else { data = await response.text(); } if (!response.ok && !expectError) { throw new Error(`HTTP ${response.status}: ${JSON.stringify(data)}`); } if (config.verbose) { const method = fetchOptions.method || 'GET'; log.detail('Request', `${method} ${endpoint}`); log.detail('Status', response.status); log.detail('Duration', `${duration}ms`); } return { data, status: response.status, headers: response.headers, duration, }; } catch (error) { const duration = Date.now() - startTime; if (!expectError) { log.error(`Request failed: ${error}`); log.detail('Endpoint', endpoint); log.detail('Duration', `${duration}ms`); } throw error; } } // Save image to results directory export async function saveImage( buffer: ArrayBuffer, filename: string ): Promise { const resultsPath = join(__dirname, config.resultsDir); try { await mkdir(resultsPath, { recursive: true }); } catch (err) { // Directory exists, ignore } const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); const fullFilename = `${timestamp}_${filename}`; const filepath = join(resultsPath, fullFilename); await writeFile(filepath, Buffer.from(buffer)); if (config.saveImages) { log.info(`Saved image: ${fullFilename}`); } return filepath; } // Upload file helper export async function uploadFile( filepath: string, fields: Record = {} ): Promise { const formData = new FormData(); // Read file and detect MIME type from extension const fs = await import('fs/promises'); const path = await import('path'); const fileBuffer = await fs.readFile(filepath); const ext = path.extname(filepath).toLowerCase(); const mimeTypes: Record = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', }; const mimeType = mimeTypes[ext] || 'application/octet-stream'; const filename = path.basename(filepath); const blob = new Blob([fileBuffer], { type: mimeType }); formData.append('file', blob, filename); // Add other fields for (const [key, value] of Object.entries(fields)) { formData.append(key, value); } const result = await api(endpoints.images + '/upload', { method: 'POST', body: formData, headers: { // Don't set Content-Type, let fetch set it with boundary }, }); return result.data.data; } // Wait helper export async function wait(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } // Poll for generation completion export async function waitForGeneration( generationId: string, maxAttempts = 20 ): Promise { for (let i = 0; i < maxAttempts; i++) { const result = await api(`${endpoints.generations}/${generationId}`); const generation = result.data.data; if (generation.status === 'success' || generation.status === 'failed') { return generation; } await wait(1000); } throw new Error('Generation timeout'); } // Test context to share data between tests export const testContext: { imageId?: string; generationId?: string; flowId?: string; uploadedImageId?: string; [key: string]: any; // Allow dynamic properties } = {}; // Test tracking state let failedTests = 0; let totalTests = 0; // Test runner helper export async function runTest( name: string, fn: () => Promise ): Promise { totalTests++; try { const startTime = Date.now(); await fn(); const duration = Date.now() - startTime; log.success(`${name} (${duration}ms)`); return true; } catch (error) { failedTests++; log.error(`${name}`); console.error(error); return false; } } // Get test statistics export function getTestStats() { return { total: totalTests, failed: failedTests, passed: totalTests - failedTests }; } // Exit with appropriate code based on test results export function exitWithTestResults() { const stats = getTestStats(); if (stats.failed > 0) { log.error(`${stats.failed}/${stats.total} tests failed`); process.exit(1); } log.success(`${stats.passed}/${stats.total} tests passed`); process.exit(0); } // Verify image is accessible at URL export async function verifyImageAccessible(url: string): Promise { try { const response = await fetch(url); if (!response.ok) { return false; } const contentType = response.headers.get('content-type'); if (!contentType?.includes('image/')) { log.warning(`URL returned non-image content type: ${contentType}`); return false; } const buffer = await response.arrayBuffer(); return buffer.byteLength > 0; } catch (error) { log.warning(`Failed to access image: ${error}`); return false; } } // Helper to expect an error response export async function expectError( fn: () => Promise, expectedStatus?: number ): Promise { try { const result = await fn(); if (result.status >= 400) { // Error status returned if (expectedStatus && result.status !== expectedStatus) { throw new Error(`Expected status ${expectedStatus}, got ${result.status}`); } return result; } throw new Error(`Expected error but got success: ${result.status}`); } catch (error) { // If it's a fetch error or our assertion error, re-throw throw error; } } // Helper to create a test image via generation export async function createTestImage( prompt: string, options: { aspectRatio?: string; alias?: string; flowId?: string | null; flowAlias?: string; } = {} ): Promise { const result = await api(endpoints.generations, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt, aspectRatio: options.aspectRatio || '1:1', alias: options.alias, flowId: options.flowId, flowAlias: options.flowAlias, }), }); if (!result.data.data) { throw new Error('No generation returned'); } // Wait for completion const generation = await waitForGeneration(result.data.data.id); if (generation.status !== 'success') { throw new Error(`Generation failed: ${generation.errorMessage}`); } return generation; } // Helper to resolve alias // Returns format compatible with old /resolve/ endpoint: { imageId, scope, alias, image } export async function resolveAlias( alias: string, flowId?: string ): Promise { // Section 6.2: Use direct alias identifier instead of /resolve/ endpoint const endpoint = flowId ? `${endpoints.images}/${alias}?flowId=${flowId}` : `${endpoints.images}/${alias}`; const result = await api(endpoint); const image = result.data.data; // Determine scope based on alias type and context const technicalAliases = ['@last', '@first', '@upload']; let scope: string; if (technicalAliases.includes(alias)) { scope = 'technical'; } else if (flowId) { scope = 'flow'; } else { scope = 'project'; } // Adapt response to match old /resolve/ format for test compatibility return { imageId: image.id, alias: image.alias || alias, scope, flowId: image.flowId, image, }; }