158 lines
5.1 KiB
JavaScript
158 lines
5.1 KiB
JavaScript
#!/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();
|
|
}
|