Files
memind/scripts/run-memind-tests.mjs
john 6b0c633a75 feat(page-data): complete Phase 4-5, ops UI, and publish integration
Add visitor roles, row-level scope, owner ops APIs, MySQL policy index,
Turnstile captcha, browser client SDK, publish-panel dataset binding,
acceptance tests, and usage documentation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 14:52:49 +08:00

377 lines
9.4 KiB
JavaScript

#!/usr/bin/env node
/**
* Memind scoped test runner.
* Usage: node scripts/run-memind-tests.mjs [--mode quick|changed|guards|release|full] [--base REF]
*/
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
const root = path.resolve(new URL('..', import.meta.url).pathname);
const MODES = new Set(['quick', 'changed', 'guards', 'release', 'full']);
const QUICK_TESTS = [
'capabilities.test.mjs',
'llm-providers.test.mjs',
'user-auth.test.mjs',
'wechat-mp.test.mjs',
];
const GUARD_SCRIPTS = [
'verify:mindspace-publish-guards',
'verify:mindspace-page-sync-guards',
];
const RELEASE_TESTS = [
...QUICK_TESTS,
'wechat-oauth.test.mjs',
'wechat-pay.test.mjs',
'mindspace-public-finish-sync.test.mjs',
'chat-finish-sync.test.mjs',
'conversation-display.test.mjs',
'mindspace-pages.test.mjs',
'mindspace-page-sync-service.test.mjs',
];
const RELEASE_CHECKS = [
'check:mindspace-public-links',
'verify:mindspace-publish-guards:full',
];
const SCOPE_RULES = [
{
id: 'publish-chat-finish',
patterns: [
/mindspace-public-finish/i,
/chat-finish-sync/i,
/conversation-display/i,
/useTKMindChat/i,
/message\.ts$/,
/mindspace-h5-html-finish/i,
],
tests: [
'mindspace-public-finish-sync.test.mjs',
'mindspace-h5-html-finish-guard.test.mjs',
'chat-finish-sync.test.mjs',
'conversation-display.test.mjs',
],
verify: ['verify:mindspace-publish-guards'],
},
{
id: 'page-sync-thumbnail',
patterns: [
/mindspace-page-sync/i,
/mindspace-pages/i,
/public-site-bases/i,
/publicUrl\.ts$/,
/mindspace-thumbnails/i,
/mindspace-workspace-thumbnails/i,
],
tests: [
'mindspace-pages.test.mjs',
'mindspace-page-sync-service.test.mjs',
'public-site-bases.test.mjs',
'mindspace-thumbnails.test.mjs',
'mindspace-workspace-thumbnails.test.mjs',
],
verify: ['verify:mindspace-page-sync-guards'],
},
{
id: 'wechat',
patterns: [/wechat/i],
tests: [
'wechat-mp.test.mjs',
'wechat-oauth.test.mjs',
'wechat-pay.test.mjs',
'user-auth.test.mjs',
],
verify: ['verify:wechat-channel-isolation'],
},
{
id: 'billing',
patterns: [/billing/i, /sse-billing/i],
tests: [
'billing.test.mjs',
'billing-recharge.test.mjs',
'billing-subscription.test.mjs',
'sse-billing.test.mjs',
],
},
{
id: 'agent-run',
patterns: [/agent-run/i, /tool-gateway/i, /goosed-proxy/i],
tests: [
'agent-run-gateway.test.mjs',
'agent-run-routes.test.mjs',
'agent-run-stream.test.mjs',
'tool-gateway.test.mjs',
'goosed-proxy-boundary.test.mjs',
'chat-agent-run-gate.test.mjs',
],
verify: ['verify:goosed-proxy-boundary'],
},
{
id: 'memory-v2',
patterns: [/memory-v2/i, /scripts\/check-memory-v2/i, /scripts\/smoke-memory-v2/i],
tests: [
'memory-v2.test.mjs',
'memory-v2-runtime.test.mjs',
'memory-v2-health.test.mjs',
'memory-v2-backend-contract.test.mjs',
],
},
{
id: 'plaza',
patterns: [/plaza/i],
tests: [
'plaza-posts.test.mjs',
'plaza-interactions.test.mjs',
'plaza-algorithm.test.mjs',
'plaza-seo.test.mjs',
'plaza-ops.test.mjs',
],
},
{
id: 'page-data',
patterns: [/page-data/i, /page-access-policy/i, /user-data-space-service/i, /mindspace-sandbox-mcp/i],
tests: [
'user-data-space-service.test.mjs',
'page-access-policy.test.mjs',
'page-data-routes.test.mjs',
'page-data-public-service.test.mjs',
'page-data-integration.test.mjs',
'page-data-acceptance.test.mjs',
'page-data-log-store.test.mjs',
'page-access-visitor.test.mjs',
'page-data-ops.test.mjs',
'page-data-session-store.test.mjs',
'page-data-browser-client.test.mjs',
'page-data-policy-index.test.mjs',
'page-data-captcha.test.mjs',
'mindspace-sandbox-mcp.test.mjs',
],
},
{
id: 'mindspace-core',
patterns: [/mindspace/i, /server\.mjs$/, /src\/hooks\/useTKMind/i],
tests: [
'mindspace.test.mjs',
'mindspace-public-route.test.mjs',
'mindspace-public-links.test.mjs',
'mindspace-runtime-config.test.mjs',
],
},
];
function usage() {
console.log(`Usage:
node scripts/run-memind-tests.mjs [--mode quick|changed|guards|release|full] [--base REF]
Modes:
changed Run tests matched to git diff (default)
quick Fast smoke for routine checks
guards Regression guard scripts only
release Pre-portal-release checklist
full Entire npm test suite`);
}
function parseArgs(argv) {
let mode = 'changed';
let base = 'HEAD';
for (let i = 2; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '--mode') {
mode = argv[++i] ?? '';
} else if (arg === '--base') {
base = argv[++i] ?? 'HEAD';
} else if (arg === '-h' || arg === '--help') {
usage();
process.exit(0);
} else {
throw new Error(`Unknown argument: ${arg}`);
}
}
if (!MODES.has(mode)) {
throw new Error(`Invalid mode: ${mode}`);
}
return { mode, base };
}
function runStep(label, fn) {
console.log(`\n==> ${label}`);
const ok = fn();
if (!ok) {
throw new Error(`${label} failed`);
}
}
function runNodeTest(files) {
const existing = files.filter((file) => fs.existsSync(path.join(root, file)));
if (existing.length === 0) {
console.log('(skip: no matching test files on disk)');
return true;
}
const result = spawnSync(process.execPath, ['--test', ...existing], {
cwd: root,
stdio: 'inherit',
});
return result.status === 0;
}
function runNpmScript(script) {
const result = spawnSync('npm', ['run', script], {
cwd: root,
stdio: 'inherit',
shell: process.platform === 'win32',
});
return result.status === 0;
}
function gitChangedFiles(base) {
const result = spawnSync('git', ['diff', '--name-only', `${base}...HEAD`], {
cwd: root,
encoding: 'utf8',
});
if (result.status !== 0) {
const fallback = spawnSync('git', ['diff', '--name-only', base], {
cwd: root,
encoding: 'utf8',
});
if (fallback.status !== 0) {
return [];
}
return fallback.stdout
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
}
return result.stdout
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
}
function gitStagedAndUnstaged() {
const result = spawnSync('git', ['status', '--porcelain'], {
cwd: root,
encoding: 'utf8',
});
if (result.status !== 0) {
return [];
}
return result.stdout
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.map((line) => line.replace(/^. [^ ]+ -> /, '').replace(/^.. /, ''));
}
function directTestForSource(file) {
const base = path.basename(file).replace(/\.(mjs|ts|tsx|js)$/, '');
const candidates = [
`${base}.test.mjs`,
path.join('scripts', `${base}.test.mjs`),
path.join('mindspace-service', `${base}.test.mjs`),
];
return candidates.filter((candidate) => fs.existsSync(path.join(root, candidate)));
}
function resolveChangedScope(files) {
const tests = new Set(QUICK_TESTS);
const verify = new Set();
for (const file of files) {
for (const test of directTestForSource(file)) {
tests.add(test);
}
for (const rule of SCOPE_RULES) {
if (rule.patterns.some((pattern) => pattern.test(file))) {
for (const test of rule.tests) {
tests.add(test);
}
for (const script of rule.verify ?? []) {
verify.add(script);
}
}
}
}
return {
files,
tests: [...tests],
verify: [...verify],
};
}
function summarize(scope) {
console.log('\n--- planned scope ---');
if (scope.files?.length) {
console.log(`changed files (${scope.files.length}):`);
for (const file of scope.files) {
console.log(` - ${file}`);
}
}
console.log(`tests (${scope.tests.length}):`);
for (const test of scope.tests) {
console.log(` - ${test}`);
}
if (scope.verify.length) {
console.log(`verify scripts (${scope.verify.length}):`);
for (const script of scope.verify) {
console.log(` - npm run ${script}`);
}
}
}
function main() {
const { mode, base } = parseArgs(process.argv);
let scope;
switch (mode) {
case 'quick':
scope = { tests: QUICK_TESTS, verify: [] };
break;
case 'guards':
scope = { tests: [], verify: GUARD_SCRIPTS };
break;
case 'release':
scope = { tests: RELEASE_TESTS, verify: RELEASE_CHECKS };
break;
case 'full':
runStep('full test suite (npm test)', () => runNpmScript('test'));
console.log('\nmemind tests ok (full)');
return;
case 'changed':
default: {
const changed = [...new Set([...gitChangedFiles(base), ...gitStagedAndUnstaged()])];
if (changed.length === 0) {
console.log('No changed files detected; falling back to quick smoke.');
scope = { tests: QUICK_TESTS, verify: [], files: [] };
} else {
scope = resolveChangedScope(changed);
}
break;
}
}
summarize(scope);
for (const test of scope.tests) {
runStep(`node --test ${test}`, () => runNodeTest([test]));
}
for (const script of scope.verify) {
runStep(`npm run ${script}`, () => runNpmScript(script));
}
console.log(`\nmemind tests ok (${mode})`);
}
try {
main();
} catch (error) {
console.error(`\n${error.message}`);
process.exit(1);
}