6b0c633a75
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>
377 lines
9.4 KiB
JavaScript
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);
|
|
}
|