#!/usr/bin/env node import { spawn } from 'node:child_process'; import path from 'node:path'; import { createLocalGateStack, selectBackendLlmProvider, seedSelectedProviderKeys, } from '../release-gate/local-stack.mjs'; const root = path.resolve(new URL('..', import.meta.url).pathname); const runtimeRoot = path.join(root, '.runtime', 'portal'); const allScenarioIds = [ 'ai-usage-survey', 'customer-order-system', 'supplier-data-report', 'tkmind-feature-survey', ]; const requestedScenarioIds = String(process.env.RELEASE_GATE_SCENARIO_IDS ?? '').trim(); const scenarioIds = requestedScenarioIds ? requestedScenarioIds.split(',').map((value) => value.trim()).filter((value) => allScenarioIds.includes(value)) : [...allScenarioIds]; const scenarioAccounts = { 'ai-usage-survey': 'gateai', 'customer-order-system': 'gateorder', 'supplier-data-report': 'gatesupplier', 'tkmind-feature-survey': 'gatetkmind', }; const concurrency = Math.max( 1, // The backend provider is stateful across tool rounds. Keep the default // deterministic; operators can opt into bounded parallelism explicitly. Math.min(scenarioIds.length, Number(process.env.RELEASE_GATE_SCENARIO_CONCURRENCY ?? 1) || 1), ); const childTimeoutMs = Math.max( 30_000, Number(process.env.RELEASE_GATE_SCENARIO_PROCESS_TIMEOUT_MS ?? 360_000) || 360_000, ); const stepTimeoutMs = Math.max( 30_000, Number(process.env.RELEASE_GATE_SCENARIO_TIMEOUT_MS ?? 300_000) || 300_000, ); async function runScenario(scenarioId, port, scenarioRoot) { const startedAt = Date.now(); const result = await new Promise((resolve, reject) => { const child = spawn( process.execPath, ['scripts/run-scenario-test.mjs', '--scenario', scenarioId, '--port', String(port)], { cwd: root, env: { ...process.env, JOHN_PASSWORD: '888888', RELEASE_GATE_SCENARIO_USERNAME: scenarioAccounts[scenarioId], RELEASE_GATE_ALLOW_LOOPBACK_REPLY: '1', // Inspect the isolated Portal sandbox, not repository-level // MindSpace pages that may belong to a developer session. MEMIND_SCENARIO_H5_ROOT: scenarioRoot, RELEASE_GATE_SCENARIO_TIMEOUT_MS: String(stepTimeoutMs), }, stdio: 'inherit', }, ); const timeout = setTimeout(() => { child.kill('SIGTERM'); setTimeout(() => child.kill('SIGKILL'), 3_000).unref(); }, childTimeoutMs); timeout.unref(); child.once('error', reject); child.once('close', (status, signal) => { clearTimeout(timeout); resolve({ code: status ?? 1, signal, timedOut: signal === 'SIGTERM' || signal === 'SIGKILL', }); }); }); return { scenarioId, ...result, elapsedMs: Date.now() - startedAt, }; } const runId = `page-data-${new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}`; const stack = await createLocalGateStack({ root, runId, port: Number(process.env.MEMIND_RELEASE_GATE_PAGE_DATA_PORT ?? 19086), portalRoot: runtimeRoot, nodeEnv: 'test', runtimeProfile: 'local', }); await seedSelectedProviderKeys({ sourceUrl: stack.sourceDatabaseUrl, targetUrl: stack.mysqlUrl }); await selectBackendLlmProvider({ targetUrl: stack.mysqlUrl }); async function register(username) { const response = await fetch(`${stack.baseUrl}/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password: '888888', displayName: `Release Gate ${username}`, email: `${username}@example.invalid`, }), }); if (!response.ok) { throw new Error(`register ${username} failed: ${response.status}`); } } try { for (const username of Object.values(scenarioAccounts)) await register(username); const claimed = new Set(); const activeAccounts = new Set(); const results = []; async function worker() { while (true) { const availableIndex = scenarioIds.findIndex((scenarioId, index) => ( !claimed.has(scenarioId) && !activeAccounts.has(scenarioAccounts[scenarioId]) )); if (availableIndex < 0) { if (claimed.size >= scenarioIds.length) return; await new Promise((resolve) => setTimeout(resolve, 100)); continue; } const scenarioId = scenarioIds[availableIndex]; claimed.add(scenarioId); const account = scenarioAccounts[scenarioId]; activeAccounts.add(account); results.push(await runScenario( scenarioId, new URL(stack.baseUrl).port, stack.sandboxRoot, )); activeAccounts.delete(account); } } await Promise.all(Array.from({ length: concurrency }, () => worker())); results.sort( (left, right) => scenarioIds.indexOf(left.scenarioId) - scenarioIds.indexOf(right.scenarioId), ); console.log('\nPage Data scenario timing:'); for (const result of results) { console.log( `${result.code === 0 ? 'PASS' : 'FAIL'} ${result.scenarioId} ` + `${Math.ceil(result.elapsedMs / 1000)}s${result.timedOut ? ' timeout' : ''}`, ); } if (results.some((result) => result.code !== 0)) process.exitCode = 1; } finally { await stack.cleanup(); }