Files
memind/scripts/run-release-gate-page-data-scenarios.mjs
T
john 441703de32
Memind CI / Test, build, and release guards (push) Successful in 1m58s
fix: stabilize page data release gate scenarios
2026-07-26 17:41:45 +08:00

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();
}