host.js+entry.js:engineMode 分流。real=引擎掌帧(engineInit 五回调 engGameInit/ Update/UpdatePost/Render/RenderPost 接帧驱动+渲染)+getContext2d backing 切引擎 mainContext(setGLEnable(false) 纯2D渲进 mainContext);stub=α RAF桩原样保留(可回滚点)。 本机已验(主会话亲跑): - 155单测绿(基线持平,受控面/插件零改故计数不变) - 受控面零改实证:src/core/api.d.ts+plugin.js 未触(七项 backing 全在 host 层切换) - Q4边界:引擎 import 仅 host.js+entry.js(metafile importers 反查证),未泄进受控面/插件 - 真接线门①:esbuild metafile 实证 littlejsengine 真入产物(163691 bytesInOutput), box2d 惰性(0 wasm input),产物 205KB min 8-agent workflow(源码取证→设计→对抗复审→实现):3/3 concerns 0 fatal。 17 mustFix(P0×1/P1×9/P2×7)全采纳:P0=real像素门 overshoot(引擎追帧 N tick/RAF 墙钟抖动 污染采样帧)→改 stub 通道独占逐像素回归,real 只证掌帧;P1 源码级纠偏=debug水印每帧画 averageFPS入mainContext(setDebugWatermark(false))/clearColor默认alpha=0不铺底 (setCanvasClearColor(BLACK))/P75须读timeReal非mockNowMs(real模式退化逻辑帧钟)。 门0 G0-1~G0-7(引擎真掌帧/真渲2D mainContext/双时钟/无uncaught/水印已除)须 mini-desktop chrome 实测(6c6g 禁 chrome)——未实测不宣布 A0 完成。 新:docs/agent-specs/2026-06-13-A0-引擎掌帧接管-edit-plan.md(编辑计划+门0 runbook) +game-runtime/test/harness/gate0-engine-takeover.cjs(门0 CDP 驱动,独立闭合不复用未改造的 integration-run) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
158 lines
9.3 KiB
JavaScript
158 lines
9.3 KiB
JavaScript
/**
|
||
* test/harness/gate0-engine-takeover.cjs — A0「引擎掌帧接管」门0 独立 driver(仅 mini-desktop,连本地 chrome via CDP)
|
||
* owner:T1b-β A0(地基)
|
||
*
|
||
* 【为何独立 driver(不复用 integration-run.cjs)】
|
||
* P2 复审 mustFix:A0 门0 不得依赖 A6 尚未改造的 integration-run.cjs(其 CANVAS='#game' 仍指退役画布、
|
||
* real 通道无就绪等待)。本 driver 只读 window.__engine/__hostdev/__probe 做 G0-1~G0-8,**不碰逐像素哈希**
|
||
* (real 通道不承担逐像素确定性,由 stub 通道兜——见 host.js vfxFrame/paletteFrame real 分支注释),
|
||
* 使 A0 自验闭合(不被 A6 阻塞)。逐像素回归仍走 integration-run.cjs 的 stub 通道(desktop-4)。
|
||
*
|
||
* 【前置】(同 integration-run.cjs)
|
||
* 外层 launcher(systemd-run)须已起好:static-serve serve 构建产物于 http://localhost:<PORT>/;
|
||
* chrome --headless=new --remote-debugging-port=9222 --no-sandbox --no-zygote --remote-allow-origins=*。
|
||
* 构建产物须为「引擎真入包」的 host-bundle.js(entry.js 经 esbuild,含 littlejsengine)。
|
||
*
|
||
* 【产出】(cwd 须为 game-runtime/)
|
||
* · evidence/gate0/gate0.json 门0 全断言机读汇总(G0-1~G0-8 各通过与否 + 采样数据)
|
||
* 退出码:0=门0 全过;非 0=任一断言失败(不掩盖)。
|
||
*
|
||
* 【用法】node test/harness/gate0-engine-takeover.cjs [PORT] [CDP_HTTP]
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
const H = require('./browser-evidence.cjs');
|
||
|
||
const PORT = parseInt(process.argv[2] || '4321', 10);
|
||
const CDP_HTTP = process.argv[3] || H.DEFAULT_CDP_HTTP;
|
||
const BASE = `http://localhost:${PORT}`;
|
||
const SEED = 0x1234abcd; // 固定主种子(与 host.js 默认一致;URL 也显式带)
|
||
const CANVAS = '#game-engine'; // 【R-C】real 模式引擎自建 mainCanvas 的 id(host.js setupAfterEngine 打)
|
||
const OUT_DIR = path.resolve('evidence/gate0');
|
||
|
||
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
|
||
// 门0 阈值(A6 可在 harness 收紧;此处给 A0 判据骨架的保守区间)。
|
||
const FRAME_WINDOW_MS = 500; // G0-2/G0-4 两次采样间隔
|
||
const FRAME_DELTA_MIN = 20; // G0-2 帧增量下界(@60fps 理论 ~30;排除「单步假推进 delta=1」与卡死)
|
||
const FRAME_DELTA_MAX = 45; // G0-2 帧增量上界(排除失控空转)
|
||
const TIME_FRAME_TOL = 0.05; // G0-3 |time - frame/60| 容忍(秒)
|
||
|
||
const result = {
|
||
meta: { ts: new Date().toISOString(), base: BASE, seed: SEED, engineMode: 'real' },
|
||
gates: {}, // G0-1..G0-8 → { pass, detail }
|
||
};
|
||
const failures = [];
|
||
function gate(id, cond, detail) {
|
||
result.gates[id] = { pass: !!cond, detail };
|
||
if (!cond) failures.push(`${id}: ${typeof detail === 'string' ? detail : JSON.stringify(detail)}`);
|
||
return !!cond;
|
||
}
|
||
|
||
async function main() {
|
||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||
|
||
// ── 通道 1:?engine=real&mode=evidence(接管断言 + 渲染落 2D)──
|
||
const urlEvidence = `${BASE}/index.html?seed=${SEED}&engine=real&mode=evidence`;
|
||
const cdp = await H.connectCdp({ url: urlEvidence, seed: SEED });
|
||
try {
|
||
// G0-1:real 通道引擎真起(engineInit().then 跑过 → __hostBootedReal;gameInit 回调触发 → __engineGameInitFired)。
|
||
// connectCdp 的 waitForBoot 已等到 __hostdev 挂载(=engineInit resolve),这里再显式核对两标记。
|
||
const boot = await cdp.evaluate(
|
||
'({ bootedReal: window.__hostBootedReal===true, gameInit: window.__engineGameInitFired===true, hasEngine: !!window.__engine, hasHostdev: !!window.__hostdev, engErr: window.__engineInitError||null })'
|
||
);
|
||
gate('G0-1', boot && boot.bootedReal && boot.gameInit && boot.hasEngine, boot);
|
||
|
||
// G0-2:引擎内部 RAF 真掌帧(frame 单调推进,且增量落合理区间——排除「假桩单步推进」与失控空转)。
|
||
// 【P1 修正】不止 t1>t0:加帧增量下界(≥20)+ 上界(≤45),抗「gameUpdate 只跑 1 次」骗过。
|
||
const s0 = await cdp.evaluate('window.__engine.snapshot()');
|
||
await delay(FRAME_WINDOW_MS);
|
||
const s1 = await cdp.evaluate('window.__engine.snapshot()');
|
||
const dFrame = (s1 && s0) ? (s1.frame - s0.frame) : -1;
|
||
gate('G0-2', dFrame >= FRAME_DELTA_MIN && dFrame <= FRAME_DELTA_MAX,
|
||
{ s0, s1, dFrame, expectRange: [FRAME_DELTA_MIN, FRAME_DELTA_MAX] });
|
||
|
||
// G0-3:time 与 frame 关系自检(time≈frame/60)。
|
||
// 【P1 修正】G0-3 是 time=frame++/frameRate 的定义式恒等,**仅作一致性自检,不作掌帧证据**(掌帧以 G0-2 为准)。
|
||
const tfDelta = (s1 && typeof s1.time === 'number') ? Math.abs(s1.time - s1.frame / 60) : 999;
|
||
gate('G0-3', tfDelta < TIME_FRAME_TOL, { time: s1 && s1.time, frame: s1 && s1.frame, delta: tfDelta, note: 'consistency-self-check-only' });
|
||
|
||
// G0-4:timeReal 纯墙钟推进(P75 埋点候选钟可用)。
|
||
const dTimeReal = (s1 && s0) ? (s1.timeReal - s0.timeReal) : -1;
|
||
gate('G0-4', dTimeReal > 0, { t0: s0 && s0.timeReal, t1: s1 && s1.timeReal, dTimeReal });
|
||
|
||
// G0-5:双时钟物理隔离——引擎 time(污染钟,LJS.time)与 probe P75 钟(ctx.time=host mockNowMs,经 probe JSONL tMono)
|
||
// 读自不同变量、不联动。这里核对:__engine.time 来自 LJS.time;probe 记录存在(tMono 走 mockNowMs)。
|
||
const probe = await cdp.evaluate('window.__probe()');
|
||
const hasProbeChain = !!(probe && Array.isArray(probe.anchors) && probe.anchors.length >= 6);
|
||
gate('G0-5', hasProbeChain && (s1 && typeof s1.time === 'number'),
|
||
{ engineTimeFromLJS: s1 && s1.time, probeAnchors: probe && probe.anchors, note: 'engine time≠probe tMono(mockNowMs), 物理隔离' });
|
||
|
||
// G0-7:渲染落在 2D mainContext(非空 + 非全黑/透明)。
|
||
// 【debugWatermark 修正】host 已 setDebugWatermark(false),右上角无 FPS 文字污染;
|
||
// 【clearColor 修正】host 已 setCanvasClearColor(BLACK),底为不透明黑(alpha=255)。
|
||
// 先 spawn 一发 burst(real 分支只推进 host 状态,引擎下一拍 gameRender 落像素),再抓 #game-engine。
|
||
await cdp.evaluate('window.__hostdev.reset()');
|
||
await cdp.evaluate(`window.__hostdev.vfxFrame('burst', 1001, 12)`);
|
||
await delay(120); // 等引擎 RAF 至少渲一拍,把粒子场落到 mainContext
|
||
const img = await H.captureImageData(cdp, CANVAS); // 返回 { data:Uint8Array(RGBA), width, height }
|
||
const nonEmpty = !!(img && img.width > 0 && img.height > 0 && img.data && img.data.length > 0);
|
||
// 非全黑/透明:扫像素,统计「有色(粒子覆盖区,主色 #7fd0ff 系:B 通道高、R 低)」与「右上角白文字」。
|
||
const px = (img && img.data) ? img.data : null;
|
||
let coloredHits = 0, whiteTextHits = 0;
|
||
if (px && nonEmpty) {
|
||
const W = img.width, Hh = img.height;
|
||
// 全屏抽样找粒子主色(B 高 R 低 → 青蓝)。
|
||
for (let i = 0; i + 3 < px.length; i += 4 * 97) { // 步进抽样降成本
|
||
const r = px[i], g = px[i + 1], b = px[i + 2];
|
||
if (b > 120 && b > r + 30) coloredHits++;
|
||
}
|
||
// 右上角子区(宽 240×高 40,物理像素)扫白色文字像素(debugWatermark 残留检测)。
|
||
const subW = Math.min(240, W), subH = Math.min(40, Hh);
|
||
for (let y = 0; y < subH; y++) {
|
||
for (let x = W - subW; x < W; x++) {
|
||
const i = (y * W + x) * 4;
|
||
if (i + 3 < px.length && px[i] > 200 && px[i + 1] > 200 && px[i + 2] > 200 && px[i + 3] > 200) whiteTextHits++;
|
||
}
|
||
}
|
||
}
|
||
gate('G0-7', nonEmpty && coloredHits > 0 && whiteTextHits === 0,
|
||
{ width: img && img.width, height: img && img.height, coloredHits, whiteTextHits, note: 'whiteTextHits>0 ⇒ debugWatermark 未关' });
|
||
} finally {
|
||
cdp.close();
|
||
}
|
||
|
||
// ── 通道 2:?engine=real&mode=smoke(受控面零改回归 G0-6)──
|
||
const urlSmoke = `${BASE}/index.html?seed=${SEED}&engine=real&mode=smoke`;
|
||
const cdp2 = await H.connectCdp({ url: urlSmoke, seed: SEED });
|
||
try {
|
||
const p = await cdp2.evaluate('window.__probe()');
|
||
const anchorsOk = !!(p && Array.isArray(p.anchors) && p.anchors.length >= 6);
|
||
gate('G0-6', p && p.initOk === true && p.verify && p.verify.ok === true && Array.isArray(p.uncaught) && p.uncaught.length === 0 && anchorsOk,
|
||
{ initOk: p && p.initOk, verifyOk: p && p.verify && p.verify.ok, uncaught: p && p.uncaught, anchors: p && p.anchors });
|
||
} finally {
|
||
cdp2.close();
|
||
}
|
||
|
||
// G0-8(回滚桩活):本 driver 不重复 integration-run;标记「由 desktop-4 的 integration-run stub 通道核对」。
|
||
result.gates['G0-8'] = { pass: null, detail: 'deferred: 由 `node test/harness/integration-run.cjs <PORT>`(?engine=stub 通道)核对,须与 α 收口报告等价' };
|
||
|
||
fs.writeFileSync(path.join(OUT_DIR, 'gate0.json'), JSON.stringify(result, null, 2));
|
||
const passed = Object.entries(result.gates).filter(([, v]) => v.pass === true).map(([k]) => k);
|
||
const failed = failures;
|
||
console.log('[gate0] gates passed:', passed.join(','));
|
||
if (failed.length) {
|
||
console.log('[gate0] GATE0 FAIL:\n - ' + failed.join('\n - '));
|
||
process.exit(1);
|
||
}
|
||
console.log('[gate0] GATE0 PASS (G0-8 deferred to integration-run stub channel)');
|
||
}
|
||
|
||
main().catch((e) => {
|
||
console.error('[gate0] driver error:', e && e.stack ? e.stack : e);
|
||
process.exit(2);
|
||
});
|