fix: stabilize page data release gate scenarios
Memind CI / Test, build, and release guards (push) Successful in 1m58s
Memind CI / Test, build, and release guards (push) Successful in 1m58s
This commit is contained in:
@@ -188,6 +188,21 @@ async function waitForPortal(baseUrl, child, timeoutMs = 60_000) {
|
||||
throw new Error(`isolated Portal did not become ready: ${baseUrl}`);
|
||||
}
|
||||
|
||||
async function waitForHttpHealth(baseUrl, child, label, timeoutMs = 30_000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (child.exitCode !== null) throw new Error(`${label} exited with code ${child.exitCode}`);
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(1_000) });
|
||||
if (response.ok) return;
|
||||
} catch {
|
||||
// The child can take a moment to bind its listener.
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
}
|
||||
throw new Error(`${label} did not become ready: ${baseUrl}`);
|
||||
}
|
||||
|
||||
async function createMysqlDatabase(baseUrl, database) {
|
||||
const parsed = new URL(baseUrl);
|
||||
assertNonProductionTarget(baseUrl, 'MySQL URL');
|
||||
@@ -278,6 +293,14 @@ export async function createLocalGateStack({
|
||||
const storageRoot = path.join(sandboxRoot, 'data', 'mindspace');
|
||||
const publishRoot = path.join(sandboxRoot, 'MindSpace');
|
||||
const logPath = path.join(runRoot, 'portal.log');
|
||||
const deepseekProxyLogPath = path.join(runRoot, 'deepseek-no-think.log');
|
||||
// Each isolated stack gets its own compatibility-proxy port so parallel
|
||||
// suites never share mutable provider state. The proxy talks directly to
|
||||
// the configured backend API; it is not the H5 relay service.
|
||||
const deepseekNoThinkPort = Number(
|
||||
process.env.MEMIND_RELEASE_GATE_DEEPSEEK_PROXY_PORT
|
||||
?? (18000 + (Number(port) % 1000)),
|
||||
);
|
||||
await Promise.all([
|
||||
fsp.mkdir(usersRoot, { recursive: true }),
|
||||
fsp.mkdir(storageRoot, { recursive: true }),
|
||||
@@ -300,11 +323,31 @@ export async function createLocalGateStack({
|
||||
let mysqlUrl;
|
||||
let pgUrl;
|
||||
let child;
|
||||
let deepseekProxyChild;
|
||||
let logFd;
|
||||
let deepseekProxyLogFd;
|
||||
let childEnv;
|
||||
let baseUrl;
|
||||
|
||||
async function stopPortal() {
|
||||
const proxyToStop = deepseekProxyChild;
|
||||
const waitForProxyExit = async (timeoutMs) => {
|
||||
if (!proxyToStop || proxyToStop.exitCode !== null) return true;
|
||||
return Promise.race([
|
||||
new Promise((resolve) => proxyToStop.once('exit', () => resolve(true))),
|
||||
new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs)),
|
||||
]);
|
||||
};
|
||||
if (proxyToStop && proxyToStop.exitCode === null) {
|
||||
proxyToStop.kill('SIGTERM');
|
||||
const exited = await waitForProxyExit(3_000);
|
||||
if (!exited && proxyToStop.exitCode === null) proxyToStop.kill('SIGKILL');
|
||||
}
|
||||
deepseekProxyChild = undefined;
|
||||
if (deepseekProxyLogFd !== undefined) {
|
||||
fs.closeSync(deepseekProxyLogFd);
|
||||
deepseekProxyLogFd = undefined;
|
||||
}
|
||||
const processToStop = child;
|
||||
const waitForExit = async (timeoutMs) => {
|
||||
if (!processToStop || processToStop.exitCode !== null) return true;
|
||||
@@ -334,6 +377,25 @@ export async function createLocalGateStack({
|
||||
async function startPortal() {
|
||||
if (!childEnv || !baseUrl) throw new Error('isolated Portal environment is not initialized');
|
||||
if (child && child.exitCode === null) throw new Error('isolated Portal is already running');
|
||||
deepseekProxyLogFd = fs.openSync(deepseekProxyLogPath, 'a');
|
||||
deepseekProxyChild = spawn(
|
||||
process.execPath,
|
||||
[path.join(root, 'deepseek-no-think-proxy.mjs')],
|
||||
{
|
||||
cwd: root,
|
||||
env: {
|
||||
...childEnv,
|
||||
MEMIND_DEEPSEEK_PROXY_ENTRYPOINT: '1',
|
||||
MEMIND_DEEPSEEK_NO_THINK_PORT: String(deepseekNoThinkPort),
|
||||
},
|
||||
stdio: ['ignore', deepseekProxyLogFd, deepseekProxyLogFd],
|
||||
},
|
||||
);
|
||||
await waitForHttpHealth(
|
||||
`http://127.0.0.1:${deepseekNoThinkPort}`,
|
||||
deepseekProxyChild,
|
||||
'DeepSeek no-thinking proxy',
|
||||
);
|
||||
logFd = fs.openSync(logPath, 'a');
|
||||
child = spawn(process.execPath, ['server.mjs'], {
|
||||
cwd: resolvedPortalRoot,
|
||||
@@ -367,11 +429,17 @@ export async function createLocalGateStack({
|
||||
MEMIND_SHARED_PUBLISH_ROOT: publishRoot,
|
||||
MEMIND_WORKSPACE_MAINTENANCE: '0',
|
||||
H5_RELAY_BOOTSTRAP_DISABLED: '1',
|
||||
// The gate must exercise the selected backend provider directly. The
|
||||
// local runtime normally enables the DeepSeek no-think compatibility
|
||||
// proxy, which is a separate relay-like hop and may be unavailable in a
|
||||
// clean gate process.
|
||||
MEMIND_DEEPSEEK_DISABLE_THINKING: '0',
|
||||
// Keep the selected backend provider direct while enabling the local
|
||||
// DeepSeek protocol compatibility shim. This is not the H5 relay: it
|
||||
// only prevents dropped reasoning_content from turning tool rounds into
|
||||
// provider HTTP 400s.
|
||||
MEMIND_DEEPSEEK_DISABLE_THINKING: '1',
|
||||
MEMIND_DEEPSEEK_NO_THINK_PORT: String(deepseekNoThinkPort),
|
||||
// The isolated gate must exercise the same shadow validation contract
|
||||
// used by production release checks; the database remains isolated.
|
||||
MEMIND_ORCHESTRATOR_MODE: 'shadow',
|
||||
MEMIND_ORCHESTRATOR_SHADOW_OBSERVATION_ENABLED: '1',
|
||||
MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED: '1',
|
||||
MEMIND_RUNTIME_REDIS_NAMESPACE: `memind:release-gate:${runId}`,
|
||||
H5_ADMIN_USERNAME: 'gate_admin',
|
||||
H5_ADMIN_PASSWORD: `Gate-${runId}-Admin!`,
|
||||
@@ -445,7 +513,11 @@ export async function createLocalGateStack({
|
||||
};
|
||||
} catch (error) {
|
||||
if (child && child.exitCode === null) child.kill('SIGKILL');
|
||||
if (deepseekProxyChild && deepseekProxyChild.exitCode === null) {
|
||||
deepseekProxyChild.kill('SIGKILL');
|
||||
}
|
||||
if (logFd !== undefined) fs.closeSync(logFd);
|
||||
if (deepseekProxyLogFd !== undefined) fs.closeSync(deepseekProxyLogFd);
|
||||
if (pgUrl) await dropPgDatabase(baseEnv.MINDSPACE_USERDATA_PG_URL, pgDatabase).catch(() => {});
|
||||
if (mysqlUrl) await dropMysqlDatabase(baseEnv.DATABASE_URL, mysqlDatabase).catch(() => {});
|
||||
await fsp.rm(sandboxRoot, { recursive: true, force: true }).catch(() => {});
|
||||
|
||||
Reference in New Issue
Block a user