games-development-ai/game-runtime/test/harness/gate0-engine-takeover.cjs
zizi 11c8eafb42 feat(t1b-beta): A0 引擎掌帧接管落地——host-dev RAF桩→littlejsengine engineInit 五回调;本机门全绿,门0浏览器段待mini-desktop
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>
2026-06-13 06:33:27 +00:00

158 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);
});