import { readFileSync, readdirSync, statSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { join, relative, extname } from 'node:path'; import test from 'node:test'; import assert from 'node:assert/strict'; const repoRoot = fileURLToPath(new URL('..', import.meta.url)); // Overseas build: every download source must be reachable without routing through // China. These patterns are slower from Singapore, and the GitHub proxies put an // unaccountable third party in the middle of the supply chain. const FORBIDDEN = [ { pattern: /npmmirror\.com/, use: 'registry.npmjs.org / nodejs.org' }, { pattern: /mirrors\.(tuna\.tsinghua|ustc|aliyun|cloud\.tencent|huaweicloud)/, use: 'the upstream project mirror' }, { pattern: /ghfast\.top|ghproxy\.net|gh\.idayer\.com/, use: 'github.com directly' }, ]; // Executable surfaces only. Docs and the Config model list are handled by the // content pass (they need rewriting, not a URL swap). const SCANNED_EXTENSIONS = new Set(['.sh', '.ps1', '.bat', '.command', '.yml', '.yaml', '.mjs', '.js']); const SKIPPED_DIRS = new Set(['.git', 'node_modules', 'dist', '.download-cache']); function* walk(dir) { for (const entry of readdirSync(dir)) { if (SKIPPED_DIRS.has(entry)) continue; const full = join(dir, entry); if (statSync(full).isDirectory()) yield* walk(full); else yield full; } } test('no China-routed download sources in scripts, launchers or CI', () => { const offenders = []; for (const file of walk(repoRoot)) { if (!SCANNED_EXTENSIONS.has(extname(file))) continue; const lines = readFileSync(file, 'utf8').split(/\r?\n/); lines.forEach((line, i) => { for (const { pattern, use } of FORBIDDEN) { if (pattern.test(line)) { offenders.push(`${relative(repoRoot, file)}:${i + 1} matches ${pattern} — use ${use}`); } } }); } assert.deepEqual(offenders, [], `China-routed sources found:\n${offenders.join('\n')}`); }); test('u-claw-app lockfile resolves every package from the official npm registry', () => { const lock = JSON.parse(readFileSync(join(repoRoot, 'u-claw-app', 'package-lock.json'), 'utf8')); const offenders = Object.entries(lock.packages ?? {}) .filter(([, meta]) => meta.resolved && !meta.resolved.startsWith('https://registry.npmjs.org/')) .map(([name, meta]) => `${name} -> ${meta.resolved}`); // A lockfile bakes the mirror host into every `resolved` field, so `npm ci` // keeps hitting it no matter what --registry the scripts pass. Regenerating // requires deleting the lockfile first: npm will not rewrite existing entries. assert.deepEqual(offenders, [], `lockfile pins non-official sources:\n${offenders.join('\n')}`); }); test('lockfile openclaw version matches OPENCLAW_VERSION', () => { const declared = readFileSync(join(repoRoot, 'OPENCLAW_VERSION'), 'utf8').trim(); const lock = JSON.parse(readFileSync(join(repoRoot, 'u-claw-app', 'package-lock.json'), 'utf8')); const locked = lock.packages?.['node_modules/openclaw']?.version; assert.equal(locked, declared, `OPENCLAW_VERSION says ${declared} but the lockfile pins ${locked}`); }); test('no user-facing Chinese is left in the English build surfaces', () => { // Chinese belongs in lib/messages/zh-CN.json and in the two deliberately // bilingual pages — not hardcoded in a script an English-speaking user sees. // Comments are fine: they are for whoever maintains this next. const CJK = /[一-鿿]/; const ALLOWED = new Set([ 'portable/lib/messages/zh-CN.json', // the Chinese catalogue itself 'portable/lib/i18n/messages.js', // generated from it 'portable/lib/language.html', // bilingual by design 'portable/advanced/Config.html', // bilingual by design ]); // Strip comments before looking, so a Chinese note at the end of a code line // or inside a multi-line block does not read as a user-facing string. function stripComments(text, file) { let out = text .replace(/\/\*[\s\S]*?\*\//g, '') // /* ... */ .replace(//g, ''); // return out .split(/\r?\n/) .map((line) => { if (/\.(sh|command)$/.test(file)) return line.replace(/(^|\s)#.*$/, ''); if (/\.(bat|ps1)$/.test(file)) return line.replace(/(^|\s)(REM\b|::|#).*$/i, ''); return line.replace(/(^|[^:])\/\/.*$/, '$1'); }) .join('\n'); } const offenders = []; for (const file of walk(join(repoRoot, 'portable'))) { const rel = relative(repoRoot, file); if (ALLOWED.has(rel)) continue; if (!/\.(sh|ps1|bat|command|mjs|js|html)$/.test(file)) continue; stripComments(readFileSync(file, 'utf8'), file) .split(/\r?\n/) .forEach((line, i) => { if (CJK.test(line)) offenders.push(`${rel}:${i + 1} ${line.trim().slice(0, 70)}`); }); } assert.deepEqual(offenders, [], `Chinese left in user-facing strings:\n${offenders.join('\n')}`); });