const fs = require('node:fs/promises'); const path = require('node:path'); const { spawn } = require('node:child_process'); const ROOT_DIR = path.resolve(__dirname, '..', '..'); const WORKSPACE_ROOT = path.resolve(ROOT_DIR, '..'); const LOG_ROOT = path.join(ROOT_DIR, 'logs'); const SEARCH_LOG_ROOT = path.join(LOG_ROOT, 'searches'); const FACEAI_BASE_URL = process.env.FACEAI_E2E_BASE_URL || 'http://127.0.0.1:3001'; const SIMULATOR_URL = process.env.FACEAI_E2E_SIMULATOR_URL || 'http://127.0.0.1:8080/faceai_simulator.php?raceId=202&lang=it'; const LEGACY_BASE_URL = process.env.FACEAI_E2E_LEGACY_BASE_URL || 'http://127.0.0.1:8080'; const LEGACY_HOME_URL = process.env.FACEAI_E2E_LEGACY_HOME_URL || `${LEGACY_BASE_URL}/index.jsp`; const SELFIE_NAME = process.env.FACEAI_E2E_SELFIE || 'DSC_1960.JPG'; const EXPECTED_MATCH_COUNT = Number(process.env.FACEAI_E2E_EXPECTED_MATCH_COUNT || '6'); function quoteShellArg(value) { if (!/[\s"]/u.test(value)) { return value; } return `"${value.replace(/"/g, '\\"')}"`; } function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } function runCommand(command, args, options = {}) { const { cwd = ROOT_DIR, allowFailure = false } = options; const executable = process.platform === 'win32' && command === 'npm' ? 'npm.cmd' : command; const useShell = process.platform === 'win32'; return new Promise((resolve, reject) => { const child = useShell ? spawn([executable, ...args].map(quoteShellArg).join(' '), { cwd, env: process.env, shell: true, stdio: ['ignore', 'pipe', 'pipe'] }) : spawn(executable, args, { cwd, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; child.stdout.on('data', (chunk) => { stdout += chunk.toString(); }); child.stderr.on('data', (chunk) => { stderr += chunk.toString(); }); child.on('error', reject); child.on('close', (code) => { const result = { code, stdout, stderr }; if (code === 0 || allowFailure) { resolve(result); return; } const error = new Error(`Command failed: ${executable} ${args.join(' ')}`); error.result = result; reject(error); }); }); } function dockerCompose(args, options) { return runCommand('docker', ['compose', ...args], options); } async function prepareHostState() { await fs.rm(LOG_ROOT, { recursive: true, force: true }); await fs.mkdir(LOG_ROOT, { recursive: true }); } async function waitForHttp(url, validate, timeoutMs = 3 * 60 * 1000) { const deadline = Date.now() + timeoutMs; let lastError = null; while (Date.now() < deadline) { try { const response = await fetch(url); const bodyText = await response.text(); let parsedBody = null; try { parsedBody = JSON.parse(bodyText); } catch { parsedBody = null; } if (validate({ response, bodyText, parsedBody })) { return; } lastError = new Error(`Readiness check did not pass for ${url}.`); } catch (error) { lastError = error; } await sleep(1000); } throw lastError || new Error(`Timed out waiting for ${url}`); } function getSelfiePath(fileName = SELFIE_NAME) { return path.join(WORKSPACE_ROOT, 'test_pkl', 'test_images', fileName); } function buildSimulatorUrl({ raceId = '202', lang = 'it', raceSlug = 'mezza-di-pisa', raceName = 'Mezza di Pisa', raceYear = '2026', raceMonthFolder = '04.APRILE', raceFolder = 'PISA' } = {}) { const url = new URL('/faceai_simulator.php', LEGACY_BASE_URL); url.searchParams.set('raceId', raceId); url.searchParams.set('lang', lang); url.searchParams.set('raceSlug', raceSlug); url.searchParams.set('raceName', raceName); url.searchParams.set('raceYear', raceYear); url.searchParams.set('raceMonthFolder', raceMonthFolder); url.searchParams.set('raceFolder', raceFolder); return url.toString(); } function buildHandoffUrl({ raceId = '202', lang = 'it', raceSlug = 'mezza-di-pisa', raceName = 'Mezza di Pisa', raceYear = '2026', raceMonthFolder = '04.APRILE', raceFolder = 'PISA', userId = '1', displayName = `Local Test User ${userId}`, email = `local-test-${userId}@example.invalid`, membershipStatus = 'active', returnUrl = buildSimulatorUrl({ raceId, lang, raceSlug, raceName, raceYear, raceMonthFolder, raceFolder }) } = {}) { const url = new URL('/faceai_handoff.php', LEGACY_BASE_URL); url.searchParams.set('raceId', raceId); url.searchParams.set('raceSlug', raceSlug); url.searchParams.set('raceName', raceName); url.searchParams.set('raceYear', raceYear); url.searchParams.set('raceMonthFolder', raceMonthFolder); url.searchParams.set('raceFolder', raceFolder); url.searchParams.set('lang', lang); url.searchParams.set('returnUrl', returnUrl); url.searchParams.set('devUserId', userId); url.searchParams.set('devDisplayName', displayName); url.searchParams.set('devEmail', email); url.searchParams.set('devMembershipStatus', membershipStatus); return url.toString(); } function getSearchArtifacts(searchId) { const searchRoot = path.join(SEARCH_LOG_ROOT, searchId); return { searchRoot, backendLogPath: path.join(LOG_ROOT, 'backend.log'), processorLogPath: path.join(LOG_ROOT, 'processor.log'), workerLogPath: path.join(searchRoot, 'worker.log'), matcherLogPath: path.join(searchRoot, 'matcher.log') }; } async function readUtf8(filePath) { return fs.readFile(filePath, 'utf8'); } module.exports = { ROOT_DIR, LOG_ROOT, SEARCH_LOG_ROOT, FACEAI_BASE_URL, LEGACY_BASE_URL, LEGACY_HOME_URL, SIMULATOR_URL, SELFIE_NAME, EXPECTED_MATCH_COUNT, buildHandoffUrl, buildSimulatorUrl, dockerCompose, getSearchArtifacts, getSelfiePath, prepareHostState, readUtf8, runCommand, waitForHttp };