二七王:前端单测框架与 shared 同步守卫

测试侧用 vm.runInThisContext 加载浏览器全局脚本,正式代码不为测试加任何导出。
test_shared_sync 断言前后端 shared 逐字节一致,已做反向验证(改一个空格即红)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 19:51:31 +08:00
co-authored by Claude Opus 5
parent 147a28805e
commit 656066b1a5
4 changed files with 82 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
// 极简断言助手(测试代码不受严格 ES5 限制,dev-guide 05 §10)
module.exports = function () {
let pass = true;
let count = 0;
function eq(name, got, exp) {
const p = JSON.stringify(got) === JSON.stringify(exp);
count++;
if (!p) pass = false;
console.log((p ? 'PASS' : 'FAIL') + ' ' + name + ' got=' + JSON.stringify(got) + ' exp=' + JSON.stringify(exp));
}
function done(label) {
console.log((pass ? 'ALL PASS' : 'HAS FAILURE') + ' [' + label + '] ' + count + ' checks');
return pass;
}
return { eq, done };
};
+27
View File
@@ -0,0 +1,27 @@
// 把前端 ES5 全局脚本加载进 Node global。
// 正式代码是浏览器脚本(var X = {...},无 module.exports),
// 这里用 vm.runInThisContext 还原浏览器语义——顶层 var 挂到 globalThis。
// 【关键】不得为了测试而给正式代码加 module.exports(client 05 §10)。
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const ROOT = path.resolve(__dirname, '..', '..'); // 仓库根
function load(rel) {
const abs = path.join(ROOT, rel);
const code = fs.readFileSync(abs, 'utf8');
vm.runInThisContext(code, { filename: rel });
}
function loadAll(rels) {
rels.forEach(load);
}
// 断言抛错的小工具:期望 fn 抛异常
function throws(fn) {
try { fn(); } catch (e) { return true; }
return false;
}
module.exports = { ROOT, load, loadAll, throws };
+16
View File
@@ -0,0 +1,16 @@
// 二七王前端单测总运行器:逐个 spawn test_*.js(各自独立进程/全局),汇总结果
// 用法:node client/tests/run.js
const fs = require('fs');
const path = require('path');
const cp = require('child_process');
const dir = __dirname;
const files = fs.readdirSync(dir).filter(f => /^test_.*\.js$/.test(f)).sort();
let allOk = true;
for (const f of files) {
console.log('\n===== ' + f + ' =====');
const r = cp.spawnSync(process.execPath, [path.join(dir, f)], { stdio: 'inherit' });
if (r.status !== 0) allOk = false;
}
console.log('\n' + (allOk ? '===== 全部单测通过 =====' : '===== 存在失败单测 ====='));
process.exit(allOk ? 0 : 1);
+23
View File
@@ -0,0 +1,23 @@
// shared 同步守卫:服务端权威源与前端只读副本必须逐字节一致。
// 忘了跑 sync_shared.cmd、或有人手改了前端副本,这条立刻红。
const fs = require('fs');
const path = require('path');
const { ROOT } = require('./_load');
const t = require('./_assert')();
const srcDir = path.join(ROOT, 'server', 'games', 'erqiwang', 'shared');
const dstDir = path.join(ROOT, 'client', 'js', '01_SubGame', 'codes', 'shared');
const srcFiles = fs.readdirSync(srcDir).filter(f => f.endsWith('.js')).sort();
const dstFiles = fs.readdirSync(dstDir).filter(f => f.endsWith('.js')).sort();
t.eq('文件清单一致', dstFiles, srcFiles);
t.eq('源目录非空', srcFiles.length > 0, true);
srcFiles.forEach(f => {
const a = fs.readFileSync(path.join(srcDir, f));
const b = fs.existsSync(path.join(dstDir, f)) ? fs.readFileSync(path.join(dstDir, f)) : Buffer.alloc(0);
t.eq('字节一致: ' + f, a.equals(b), true);
});
process.exit(t.done('shared_sync') ? 0 : 1);