裁定三落地:accepted = 机械预筛 ∧ 独立模型玩法判定,九门 pass 降为「未见明显死」预筛。 - 波2a 玩法判定器(cheap_verify.py):读 brief+run-summary+全程截图(含新增局中连拍 midplay-1..N,play.cdp.cjs 只读并发拍、不进任何门判据),布尔裁 broken/hollow/off_brief, fail-closed;判定真相层落 evidence/judge.json + verdict.json 写回 accepted/judge 段; 金标夹具 fixtures/judge-golden/ + judge_golden.py 校准跑批 - 波2b 拆残笼:check 词法 tokenizer 替换 stripCode 正则(字符串内 // 假阴根修,292 在册 源零差异);prompt v1.6.3 外科清洗(两层奖励→纯设计语/删盲驱动器围设计/删熔断恐吓), registry 热取==内置逐字节一致(cheap_roles 内置回退同步);edit_file 空白归一+近似片段提示 - 判定器补修:空输出有界重试一次(glm 思考吃光 max_tokens 时 content 空)+重试放大公式 反缩 bug 修(旧 min(×2,6000) 大 base 反缩)+finishReason 落盘;imagesSeen 模型自报+ 盲检微扰重掷、两掷均盲=仪器故障 degraded(闲鱼中转静默丢图实锤,不再错杀 hollow) - 判定档切 MiniMax-M3(创始人 2026-07-10 拍板,裁定三①边界随 v2 plan 修订): generation.yaml judge.model+max_tokens=202752(512K 网关实探 400 拒,取实探上限); 3 例盲案 M3 重判全 accept、¥0.026/局 - 三路消费对齐:CLI 预筛语义修(cheap_studio 喂 verdict.pass 而非 finished,违裁定三的 放行已纠)/Service 路 apply_gameplay_judge 显式 prefilter 参数/result_out 权威改读 accepted(无键回落预筛,旧产物兼容)+trace.gameplayJudge additive 透传; tier2 gate_judge/genconfig 同步判定配置与消费 测试:test_gameplay_judge 33 项新增,全仓 pytest 480 绿;真判定闭环 accept ¥0.061。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
182 lines
10 KiB
JavaScript
182 lines
10 KiB
JavaScript
/**
|
||
* check-banned.test.mjs — amodel-gen 红线判定 _bannedLineErrors() 单测(死圈修复 2026-07-08 fix②-check)
|
||
* owner:便宜档生成质量线 | 跑法:node --test tools/amodel-gen/tests/check-banned.test.mjs(cwd 任意)
|
||
*
|
||
* 【守的不变量】
|
||
* ① 裸时间/随机/DOM(Math.random/Date.now/performance.now/setTimeout/setInterval/rAF/AudioContext/addEventListener)命中即报;
|
||
* ② 报错带【确切行号】——跨行块注释在前也不漂(旧 stripCode 把块注释塌成单空格会致行号漂,本测正是回归它);
|
||
* ③ 函数内的回退裸调、typeof 守卫后的裸调、任意嵌套层同样命中(正则按纯文本扫);
|
||
* ④ 零误杀:注释 / 字符串里的 API 名(经 stripCode)不触发;合规 ctx 写法不报。
|
||
*/
|
||
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { _bannedLineErrors, _bootCtxLine, _getInputLine, lexTokens } from '../tools.mjs';
|
||
|
||
/** 是否报了某红线(按正则 source 关键片段匹配)。 */
|
||
const hasBanned = (errs, frag) => errs.some((e) => e.includes(frag));
|
||
|
||
// ───────── ① 基本命中 + 行号 ─────────
|
||
|
||
test('① Math.random 命中并报出确切行号', () => {
|
||
const src = `export function createGame(){\n function rng(){ return Math.random(); }\n return { _forensicsView(){}, handleTap(){} };\n}`;
|
||
const errs = _bannedLineErrors(src, 'game-logic.js');
|
||
assert.equal(errs.length, 1, JSON.stringify(errs));
|
||
assert.ok(errs[0].includes('第 2 行'), `应报第 2 行:${errs[0]}`);
|
||
assert.ok(errs[0].includes('Math\\.random'), '应含被禁正则');
|
||
assert.ok(errs[0].includes('edit_file'), '应给 edit_file 补救指向');
|
||
});
|
||
|
||
test('① Date.now / setTimeout / addEventListener 各自命中', () => {
|
||
const cases = [
|
||
[`const t = Date.now();`, 'Date\\.now'],
|
||
[`setTimeout(() => {}, 100);`, 'setTimeout'],
|
||
[`window.addEventListener('click', h);`, 'addEventListener'],
|
||
[`const c = new AudioContext();`, 'AudioContext'],
|
||
];
|
||
for (const [line, frag] of cases) {
|
||
const errs = _bannedLineErrors(`export function createGame(){\n ${line}\n}`, 'render.js');
|
||
assert.ok(hasBanned(errs, frag), `应命中 ${frag}:${line}`);
|
||
}
|
||
});
|
||
|
||
// ───────── ② 行号不因跨行块注释而漂(回归旧 stripCode 塌行 bug) ─────────
|
||
|
||
test('② 跨行块注释在违规行之前 → 行号不漂', () => {
|
||
// 块注释占第 1-3 行(旧实现塌成单空格会让后续行号整体上移);Math.random 在第 5 行。
|
||
const src = [
|
||
'/* 多行', // 1
|
||
' 块', // 2
|
||
' 注释 */', // 3
|
||
'export function createGame(){', // 4
|
||
' const r = Math.random();', // 5
|
||
' return { _forensicsView(){}, handleTap(){} };', // 6
|
||
'}', // 7
|
||
].join('\n');
|
||
const errs = _bannedLineErrors(src, 'game-logic.js');
|
||
assert.equal(errs.length, 1, JSON.stringify(errs));
|
||
assert.ok(errs[0].includes('第 5 行'), `应报第 5 行(不漂):${errs[0]}`);
|
||
});
|
||
|
||
test('② 多处命中 → 逐行列出', () => {
|
||
const src = [
|
||
'export function createGame(){', // 1
|
||
' const a = Math.random();', // 2
|
||
' const b = 1;', // 3
|
||
' const c = Math.random();', // 4
|
||
' return {};', // 5
|
||
'}',
|
||
].join('\n');
|
||
const errs = _bannedLineErrors(src, 'game-logic.js');
|
||
assert.equal(errs.length, 1, '同一红线合并为一条');
|
||
assert.ok(errs[0].includes('第 2/4 行'), `应列 2/4:${errs[0]}`);
|
||
});
|
||
|
||
// ───────── ③ 函数内 / typeof 守卫后的回退裸调同样命中 ─────────
|
||
|
||
test('③ typeof 守卫后的回退裸调仍命中(别靠守卫绕)', () => {
|
||
const src = `export function createGame(){\n function rng(){ return (typeof ctx !== 'undefined' && ctx.random) ? ctx.random.next() : Math.random(); }\n}`;
|
||
const errs = _bannedLineErrors(src, 'game-logic.js');
|
||
assert.ok(hasBanned(errs, 'Math\\.random'), '守卫后的回退裸调应命中');
|
||
assert.ok(errs[0].includes('第 2 行'), errs[0]);
|
||
});
|
||
|
||
// ───────── ④ 零误杀 ─────────
|
||
|
||
test('④ 注释 / 字符串里的 API 名不触发', () => {
|
||
const src = [
|
||
'// 别写 Math.random(),要用 ctx.random.next()', // 行注释
|
||
'/* 反例:const t = Date.now() 也禁 */', // 块注释
|
||
'export function createGame(){',
|
||
" const tip = '禁用 setTimeout(fn, 100)';", // 字符串
|
||
' const r = ctx.random.next();', // 合规
|
||
' return { _forensicsView(){}, handleTap(){} };',
|
||
'}',
|
||
].join('\n');
|
||
const errs = _bannedLineErrors(src, 'game-logic.js');
|
||
assert.equal(errs.length, 0, `注释/字符串里的 API 名不应触发:${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('④ 合规 ctx 写法零报', () => {
|
||
const src = `export function createGame(){\n const r = ctx.random.next();\n const t = ctx.time.nowMs();\n return { _forensicsView(){}, handleTap(){} };\n}`;
|
||
const errs = _bannedLineErrors(src, 'game-logic.js');
|
||
assert.equal(errs.length, 0, JSON.stringify(errs));
|
||
});
|
||
|
||
// ───────── ⑤ 字符串内的 // 不 desync stripCode(2026-07-08 trpg-cyber 假阴性根治回归) ─────────
|
||
|
||
test('⑤ 字符串内含 // 不吞后续真代码(赛博主题回归)', () => {
|
||
// 赛博主题实测:'> JACK IN // FLOOR 01' 字符串里的 //,旧 stripCode 行注释先剥、吞掉闭合引号 →
|
||
// 引号失衡 → 后续字符串正则跨行匹配 → 级联误抹真代码(实测抹 169 行,连 bundle.tick/handleTap 一起抹)
|
||
// → check 误报红线缺失 → 模型明明写对却被反复拒 → thrash 到步数顶熔断失败。回归:字符串后的真
|
||
// Math.random 仍须命中且报对行号(证明字符串内 // 未 desync 后续码)。
|
||
const src = [
|
||
'export function createGame(){', // 1
|
||
" const banner = '> JACK IN // FLOOR 01';", // 2 // 在字符串内
|
||
' const r = Math.random();', // 3 真违规,须命中
|
||
' return { _forensicsView(){}, handleTap(){} };', // 4
|
||
'}', // 5
|
||
].join('\n');
|
||
const errs = _bannedLineErrors(src, 'game-logic.js');
|
||
assert.ok(hasBanned(errs, 'Math\\.random'), `字符串内 // 不应吞掉后续真代码:${JSON.stringify(errs)}`);
|
||
assert.ok(errs.some((e) => e.includes('第 3 行')), `Math.random 应报第 3 行:${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('⑤ 字符串内的 // 后接注释形态的文本零误杀', () => {
|
||
// 双引号串含 //,且串内出现禁用 API 名(Date.now);既不该吞后续码、串内 API 名也不该误报。
|
||
const src = [
|
||
'export function createGame(){', // 1
|
||
' const url = "sys://net/Date.now() 只是文案 // 提示";', // 2 串内 // + API 名
|
||
' const t = ctx.time.nowMs();', // 3 合规
|
||
' return { _forensicsView(){}, handleTap(){} };', // 4
|
||
'}', // 5
|
||
].join('\n');
|
||
const errs = _bannedLineErrors(src, 'game-logic.js');
|
||
assert.equal(errs.length, 0, `串内 // 与 API 名都不该报,后续合规码也不该被误伤:${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
// ───────── ⑥ 词法真实版新增能力(W-AXIS 波2:模板插值裸调 / boot.ctx / getInput 行号)─────────
|
||
|
||
test('⑥ 模板插值 ${} 内的裸调照样命中(词法真实,非纯文本近似)', () => {
|
||
// 旧 stripCode 把整段模板(含 ${})抹成空白 → 插值里的 Math.random 被漏报;词法版把 ${} 内按码扫描 → 命中。
|
||
const src = [
|
||
'export function createGame(){', // 1
|
||
' const label = `种子:${Math.random()}`;', // 2 插值内真实裸调
|
||
' return { _forensicsView(){}, handleTap(){} };', // 3
|
||
'}', // 4
|
||
].join('\n');
|
||
const errs = _bannedLineErrors(src, 'game-logic.js');
|
||
assert.ok(errs.some((e) => e.includes('Math\\.random')), `模板插值内的裸调应命中:${JSON.stringify(errs)}`);
|
||
assert.ok(errs.some((e) => e.includes('第 2 行')), `应报第 2 行:${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('⑥ 模板纯文本(非插值)里的 API 名不误命中', () => {
|
||
const src = 'export function createGame(){\n const tip = `别写 Math.random() 或 Date.now()`;\n const r = ctx.random.next();\n return { _forensicsView(){}, handleTap(){} };\n}';
|
||
const errs = _bannedLineErrors(src, 'game-logic.js');
|
||
assert.equal(errs.length, 0, `模板纯文本里的 API 名不该报:${JSON.stringify(errs)}`);
|
||
});
|
||
|
||
test('⑥ _bootCtxLine:.boot.ctx 双层误用命中并给行号,正确 boot.ctx 零命中', () => {
|
||
const bad = 'export function createGame(){\n init(boot){ const ctx = boot.boot.ctx; }\n}';
|
||
assert.equal(_bootCtxLine(bad), 2, '.boot.ctx 应命中第 2 行');
|
||
const good = 'export function createGame(){\n init(boot){ const ctx = boot.ctx; ctx.log("t","x"); }\n}';
|
||
assert.equal(_bootCtxLine(good), 0, '正确 boot.ctx 不该命中');
|
||
// 字符串里的 .boot.ctx 文案不误命中
|
||
const strCase = 'export function createGame(){\n const tip = "别写 x.boot.ctx";\n}';
|
||
assert.equal(_bootCtxLine(strCase), 0, '字符串里的 .boot.ctx 不该命中');
|
||
});
|
||
|
||
test('⑥ _getInputLine:getInput( 命中并给行号,字符串里的 getInput 不误命中', () => {
|
||
const bad = 'export function createGame(){\n update(dt){ const i = ctx.getInput(); }\n}';
|
||
assert.equal(_getInputLine(bad), 2, 'getInput( 应命中第 2 行');
|
||
const strCase = 'export function createGame(){\n const tip = "不要调 getInput()";\n}';
|
||
assert.equal(_getInputLine(strCase), 0, '字符串里的 getInput 不该命中');
|
||
});
|
||
|
||
test('⑥ lexTokens:字符串/注释不产 token,码区标识符产 token', () => {
|
||
const toks = lexTokens('const a = "Math.random"; // Date.now\n foo.bar');
|
||
const ids = toks.filter((t) => t.k === 'id').map((t) => t.v);
|
||
assert.ok(ids.includes('const') && ids.includes('a') && ids.includes('foo') && ids.includes('bar'), JSON.stringify(ids));
|
||
assert.ok(!ids.includes('Math') && !ids.includes('random') && !ids.includes('Date') && !ids.includes('now'), `字符串/注释内的名不该成 token:${JSON.stringify(ids)}`);
|
||
});
|