Files
memind/scripts/run-release-gate-local-smoke.mjs
john b1577a16e9
Memind CI / Test, build, and release guards (push) Failing after 12m3s
fix: harden release gate and page delivery
2026-07-26 14:32:01 +08:00

232 lines
8.2 KiB
JavaScript

#!/usr/bin/env node
import fs from 'node:fs/promises';
import path from 'node:path';
import mysql from 'mysql2/promise';
import { createLocalGateStack } from '../release-gate/local-stack.mjs';
const root = path.resolve(new URL('..', import.meta.url).pathname);
const runId = `gate-${new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}`;
const stack = await createLocalGateStack({ root, runId });
const checks = [];
function record(id, passed, detail) {
checks.push({ id, passed, detail });
console.log(`${passed ? 'PASS' : 'FAIL'} ${id} ${detail}`);
}
async function register(username, password) {
const response = await fetch(`${stack.baseUrl}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
password,
displayName: `Gate ${username}`,
email: `${username}@example.invalid`,
}),
});
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(`register ${username}: ${response.status} ${JSON.stringify(body)}`);
return body;
}
async function login(username, password) {
const response = await fetch(`${stack.baseUrl}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
const body = await response.json().catch(() => ({}));
const cookie = response.headers.get('set-cookie')?.split(';')[0] ?? '';
return { response, body, cookie };
}
let failed = false;
try {
const userA = `${runId.replaceAll('-', '').slice(-12)}a`;
const userB = `${runId.replaceAll('-', '').slice(-12)}b`;
const rateUser = `${runId.replaceAll('-', '').slice(-12)}r`;
const password = 'Gate-Local-Only-2026!';
const registeredA = await register(userA, password);
const registeredB = await register(userB, password);
await register(rateUser, password);
const userAId = registeredA.user?.id;
const userBId = registeredB.user?.id;
if (!userAId || !userBId) throw new Error('registered local users have no stable ids');
const rejectedStatuses = [];
for (let attempt = 0; attempt < 5; attempt += 1) {
const rejected = await login(rateUser, `wrong-password-${attempt}`);
rejectedStatuses.push(rejected.response.status);
}
const rateLimited = await login(rateUser, password);
const retryAfter = Number(rateLimited.response.headers.get('retry-after') ?? 0);
record(
'AUTH-02',
rejectedStatuses.every((status) => status === 401)
&& rateLimited.response.status === 429
&& retryAfter > 0,
`wrong=${rejectedStatuses.join(',')} limited=${rateLimited.response.status} retry_after=${retryAfter}`,
);
const authA = await login(userA, password);
const authB = await login(userB, password);
const authenticatedStatus = await fetch(`${stack.baseUrl}/auth/status`, {
headers: { Cookie: authA.cookie },
});
const authenticatedBody = await authenticatedStatus.json().catch(() => ({}));
record(
'AUTH-01',
authA.response.ok
&& authA.body.authenticated
&& Boolean(authA.cookie)
&& authenticatedStatus.ok
&& authenticatedBody.authenticated,
`login=${authA.response.status} cookie=${Boolean(authA.cookie)} status_api=${authenticatedStatus.status}`,
);
await stack.restartPortal();
const statusAfterRestart = await fetch(`${stack.baseUrl}/auth/status`, {
headers: { Cookie: authA.cookie },
});
const restartBody = await statusAfterRestart.json().catch(() => ({}));
const ghostStatus = await fetch(`${stack.baseUrl}/auth/status`, {
headers: { Cookie: 'h5_user_session=synthetic-invalid-session' },
});
const ghostBody = await ghostStatus.json().catch(() => ({}));
record(
'AUTH-03',
statusAfterRestart.ok
&& restartBody.authenticated === true
&& ghostBody.authenticated === false,
`valid_after_restart=${Boolean(restartBody.authenticated)} invalid_after_restart=${Boolean(ghostBody.authenticated)}`,
);
const syntheticSessionId = `gate-session-${runId}`;
const sessionConnection = await mysql.createConnection(stack.mysqlUrl);
try {
await sessionConnection.execute(
`INSERT INTO h5_user_sessions
(agent_session_id, user_id, goosed_node, goosed_target, created_at)
VALUES (?, ?, 0, NULL, ?)`,
[syntheticSessionId, userAId, Date.now()],
);
} finally {
await sessionConnection.end();
}
const crossUserSession = await fetch(
`${stack.baseUrl}/api/sessions/${encodeURIComponent(syntheticSessionId)}`,
{ headers: { Cookie: authB.cookie } },
);
const createPage = await fetch(`${stack.baseUrl}/api/mindspace/v1/pages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Cookie: authA.cookie,
},
body: JSON.stringify({
title: 'Gate isolation page',
summary: 'Synthetic local-only fixture',
content: '<p>release gate</p>',
template_id: 'article',
page_type: 'article',
}),
});
const pageBody = await createPage.json().catch(() => ({}));
const pageId = pageBody?.data?.id;
const crossUser = pageId
? await fetch(`${stack.baseUrl}/api/mindspace/v1/pages/${encodeURIComponent(pageId)}`, {
headers: { Cookie: authB.cookie },
})
: null;
const categoriesResponse = await fetch(`${stack.baseUrl}/api/mindspace/v1/space/categories`, {
headers: { Cookie: authA.cookie },
});
const categoriesBody = await categoriesResponse.json().catch(() => ({}));
const uploadCategory = (categoriesBody.data ?? []).find(
(category) => category.code === 'oa' || category.categoryCode === 'oa',
) ?? (categoriesBody.data ?? []).find(
(category) => ['oa', 'public'].includes(category.category_code),
);
const uploadContent = Buffer.from('local release gate attachment');
const createUpload = uploadCategory
? await fetch(`${stack.baseUrl}/api/mindspace/v1/uploads`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Cookie: authA.cookie,
},
body: JSON.stringify({
category_id: uploadCategory.id,
filename: 'gate-note.txt',
size_bytes: uploadContent.length,
declared_mime_type: 'text/plain',
session_id: syntheticSessionId,
}),
})
: null;
const uploadBody = await createUpload?.json().catch(() => ({}));
const uploadId = uploadBody?.data?.id;
const writeUpload = uploadId
? await fetch(`${stack.baseUrl}/api/mindspace/v1/uploads/${encodeURIComponent(uploadId)}/content`, {
method: 'PUT',
headers: {
'Content-Type': 'application/octet-stream',
Cookie: authA.cookie,
},
body: uploadContent,
})
: null;
const completeUpload = uploadId && writeUpload?.ok
? await fetch(`${stack.baseUrl}/api/mindspace/v1/uploads/${encodeURIComponent(uploadId)}/complete`, {
method: 'POST',
headers: { Cookie: authA.cookie },
})
: null;
const assetBody = await completeUpload?.json().catch(() => ({}));
const assetId = assetBody?.data?.id;
const crossUserAsset = assetId
? await fetch(`${stack.baseUrl}/api/mindspace/v1/assets/${encodeURIComponent(assetId)}/download`, {
headers: { Cookie: authB.cookie },
})
: null;
record(
'AUTH-05',
crossUserSession.status === 403
&& createPage.ok
&& Boolean(pageId)
&& [403, 404].includes(crossUser?.status)
&& completeUpload?.status === 201
&& Boolean(assetId)
&& [403, 404].includes(crossUserAsset?.status),
`session=${crossUserSession.status} page_create=${createPage.status} page_cross_user=${crossUser?.status ?? 'not-run'} asset_create=${completeUpload?.status ?? 'not-run'} asset_cross_user=${crossUserAsset?.status ?? 'not-run'}`,
);
const logout = await fetch(`${stack.baseUrl}/auth/logout`, {
method: 'POST',
headers: { Cookie: authA.cookie },
});
const afterLogout = await fetch(`${stack.baseUrl}/api/me`, {
headers: { Cookie: authA.cookie },
});
record(
'AUTH-04',
logout.ok && [401, 403].includes(afterLogout.status),
`logout=${logout.status} old_cookie=${afterLogout.status}`,
);
failed = checks.some((check) => !check.passed);
await fs.writeFile(
path.join(stack.runRoot, 'smoke.json'),
`${JSON.stringify({ run_id: runId, checks }, null, 2)}\n`,
);
} finally {
await stack.cleanup();
}
if (failed) process.exit(1);