/** * engine-caps.test.mjs — A1 引擎能力门面单测(node --test 直跑,零真引擎依赖) * owner:T1b-β A1 | 运行:node --test host-dev/test/engine-caps.test.mjs * * ════════════════════════════════════════════════════════════════════════════ * 【测什么 / 为什么这样测】 * A1 落地:① host-dev/engine-math.js 的 buildEngineMath(包装引擎 lerp/smoothStep/Ease 成 math 门面, * 含 easing 11 条等价曲线);② host.js 工厂 makeEngineCaps 注入 createHostDevContext.engineFactory * → getEngine() 在 real 通道返三面、stub/无工厂返 null(plugin.js getEngineShared 已就绪,A1 只接线)。 * * 【F7 铁律】`import * as LJS from 'littlejsengine'` 在 node 顶层抛 `window is not defined`(已实测) * → 本测**不 import 真引擎、不 import host.js**(host.js 顶层 import 引擎,node 同样 import 不了)。 * 故: * · 测门面映射正确性 → 注入**内联引擎 Ease 曲线的 mock ljs**(逐字节复制 littlejs.esm.js:15118, * 下方 makeEngineOracle 标注源行号)作 oracle,比对 buildEngineMath 产出的门面逐点等价。 * 这不是掏空——是把「门面映射对不对 + 字节恒等/ULP 边界 + backInOut/elasticInOut 排除」钉死的正面锚。 * · 测工厂注入/getEngine 接线 → 用真 createHostDevContext(plugin.js,零引擎),注入「返回 buildEngineMath * 门面的 mock 工厂」验 getEngine().math.easing 通;无工厂验 getEngine()==null(real runtime 契约)。 * 集成段 real 通道「门面真调真引擎」的证据由门②(mini-desktop CDP __engineCalls)兜(A1 不做,主会话事后)。 * ════════════════════════════════════════════════════════════════════════════ */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { buildEngineMath, clampUnit } from '../engine-math.js'; import { createHostDevContext } from '../../src/core/plugin.js'; // gamefeel 内置 easing(A4 的 null-engine fallback):T-A1-c 用它比对门面越界钳制一致。 import { easing as gamefeelEasing } from '../../src/plugins/gamefeel/impl.js'; /* ────────────────────────────────────────────────────────────────────────── * 引擎 Ease oracle(逐字节复制 littlejs.esm.js:15118 `const Ease`,不 import 真引擎,规避 F7)。 * lerp/smoothStep 同复制 esm.js:1444/:1500。每处标源行号,复审可逐字节复验。 * ────────────────────────────────────────────────────────────────────────── */ function makeEngineOracle() { // esm.js:1444 lerp(自带 percent 钳制:clamp(percent)) const clamp = (v, min = 0, max = 1) => (v < min ? min : v > max ? max : v); const lerp = (valueA, valueB, percent) => valueA + clamp(percent) * (valueB - valueA); // esm.js:1500 smoothStep const smoothStep = (percent) => percent * percent * (3 - 2 * percent); // esm.js:15118 const Ease(仅复制 A1 门面用到的曲线 + 修饰器) const PI = Math.PI; const sin = Math.sin; const Ease = { LINEAR: (x) => x, POWER: (n) => (x) => x ** n, BACK: (x) => x * x * (2.70158 * x - 1.70158), ELASTIC: (x) => x === 0 ? 0 : x === 1 ? 1 : -(2 ** (10 * x - 10)) * sin(((37 - 40 * x) * PI) / 6), IN: (f) => f, OUT: (f) => (x) => 1 - f(1 - x), IN_OUT: (f) => Ease.PIECEWISE(f, Ease.OUT(f)), PIECEWISE: (...fns) => { const n = fns.length; return (x) => { const i = (x * n - 1e-9) >> 0; return (fns[i]((x - i / n) * n) + i) / n; }; }, }; return { lerp, smoothStep, Ease }; } /** 门面 easing 键 → oracle 引擎曲线(同 A1 映射;用于逐点比对)。 */ function oracleEasingMap(oracle) { const E = oracle.Ease; return { linear: E.LINEAR, quadIn: E.POWER(2), quadOut: E.OUT(E.POWER(2)), quadInOut: E.IN_OUT(E.POWER(2)), cubicIn: E.POWER(3), cubicOut: E.OUT(E.POWER(3)), cubicInOut: E.IN_OUT(E.POWER(3)), backIn: E.BACK, backOut: E.OUT(E.BACK), elasticIn: E.ELASTIC, elasticOut: E.OUT(E.ELASTIC), }; } const EASING_11 = [ 'linear', 'quadIn', 'quadOut', 'quadInOut', 'cubicIn', 'cubicOut', 'cubicInOut', 'backIn', 'backOut', 'elasticIn', 'elasticOut', ]; /* ════════════════════════════════════════════════════════════════════════════ * T-A1-a:工厂注入 / getEngine 接线(真 createHostDevContext,零引擎) * ════════════════════════════════════════════════════════════════════════════ */ test('T-A1-a|无 engineFactory(stub/node 短路语义):getEngine() 返 null(plugin.js 容错降级路径成立)', () => { // host.js makeEngineCaps 在 stub 通道短路返 null ≡「createHostDevContext 不收工厂」→ getEngine()==null。 const bundle = createHostDevContext({ seed: 1 }); assert.equal(bundle.context.getEngine(), null, '无工厂时 getEngine() 必须返 null(不抛错)'); // 再调一次仍 null(lazy 单例 + 告警闸不影响返回值)。 assert.equal(bundle.context.getEngine(), null); }); test('T-A1-a|有 engineFactory(real 通道注入语义):getEngine() 返三面、math.easing 门面在', () => { const oracle = makeEngineOracle(); // 模拟 host.js makeEngineCaps real 分支:math=buildEngineMath(LJS),particles/audio.synth 空骨架占位。 const fakeFactory = () => ({ math: buildEngineMath(oracle), particles: { spawnEmitter: () => ({ isActive: () => false, stop: () => {} }) }, audio: { synth: { synthSfx: () => null, synthSong: () => null } }, }); const bundle = createHostDevContext({ seed: 1, engineFactory: fakeFactory }); const eng = bundle.context.getEngine(); assert.ok(eng, 'getEngine() 应返真能力面'); assert.ok(eng.math && typeof eng.math.lerp === 'function', 'math.lerp 在'); assert.ok(eng.math.easing && typeof eng.math.easing.quadIn === 'function', 'math.easing.quadIn 在'); // 工厂只调一次(lazy 单例):第二次 getEngine 返同一对象。 assert.equal(bundle.context.getEngine(), eng, 'getEngine 应 lazy 单例(同一实例)'); }); /* ════════════════════════════════════════════════════════════════════════════ * T-A1-b:easing 门面 11 条曲线逐点等价引擎 Ease oracle + backInOut/elasticInOut 排除 * ════════════════════════════════════════════════════════════════════════════ */ test('T-A1-b|easing 门面 11 条逐点等价引擎 Ease(quad 字节恒等 / 余 ULP)+ 二条 inOut 不在门面', () => { const oracle = makeEngineOracle(); const math = buildEngineMath(oracle); const oracleMap = oracleEasingMap(oracle); // 逐点扫 x∈[0,1] 共 201 点。门面内部钳 t,与 oracle 在 [0,1] 内同曲线 → 比内点。 for (const name of EASING_11) { const fac = math.easing[name]; const orc = oracleMap[name]; assert.equal(typeof fac, 'function', `门面应有 ${name}`); let maxDiff = 0; let byteEqualAll = true; for (let i = 0; i <= 200; i++) { const x = i / 200; const a = fac(x); const b = orc(x); const d = Math.abs(a - b); if (d > maxDiff) maxDiff = d; if (!Object.is(a, b)) byteEqualAll = false; } // 全 11 条至少 ULP 等价(<1e-9,§0.1 实测 maxAbsDiff ≤ 2.1e-15)。 assert.ok(maxDiff < 1e-9, `${name} 门面与引擎 oracle 应 ULP 等价,实测 maxDiff=${maxDiff}`); // quadIn/quadOut 应逐字节恒等(§0.1 实测 2001/2001 字节恒等)。 if (name === 'quadIn' || name === 'quadOut') { assert.ok(byteEqualAll, `${name} 应与引擎 oracle 逐字节恒等`); } } // backInOut/elasticInOut 刻意不入门面(引擎 IN_OUT 拼接是另一条曲线,差 6.6%/17%,非 ULP)。 assert.equal(math.easing.backInOut, undefined, 'backInOut 不应在门面(引擎无此等价曲线)'); assert.equal(math.easing.elasticInOut, undefined, 'elasticInOut 不应在门面(引擎无此等价曲线)'); }); /* ════════════════════════════════════════════════════════════════════════════ * T-A1-c:easing 门面越界钳制 == gamefeel 内置(fallback 行为一致,防 A4 改门面后回归) * ════════════════════════════════════════════════════════════════════════════ */ test('T-A1-c|easing 门面越界钳制:fn(-0.5)===fn(0)、fn(1.5)===fn(1),且与 gamefeel 内置越界逐值一致', () => { const oracle = makeEngineOracle(); const math = buildEngineMath(oracle); for (const name of EASING_11) { const fn = math.easing[name]; // 门面自身越界钳制:越下界 ≡ f(0),越上界 ≡ f(1)。 assert.ok(Object.is(fn(-0.5), fn(0)), `${name}(-0.5) 应 === ${name}(0)`); assert.ok(Object.is(fn(1.5), fn(1)), `${name}(1.5) 应 === ${name}(1)`); // 与 gamefeel 内置 fallback 的越界行为逐值一致(gamefeel 同名曲线对越界亦 clamp01)。 const gf = gamefeelEasing[name]; assert.equal(typeof gf, 'function', `gamefeel 应有同名 ${name}`); // 越界点门面与内置应同值(二者都钳到端点,端点值 ULP 等价)。 assert.ok(Math.abs(fn(-0.5) - gf(-0.5)) < 1e-9, `${name} 门面与内置在 t=-0.5 应等价`); assert.ok(Math.abs(fn(1.5) - gf(1.5)) < 1e-9, `${name} 门面与内置在 t=1.5 应等价`); } }); /* ════════════════════════════════════════════════════════════════════════════ * T-A1-d:easing 门面端点恒等 f(0)=0 / f(1)=1(11 条全) * ════════════════════════════════════════════════════════════════════════════ */ test('T-A1-d|easing 门面端点恒等:11 条全 f(0)=0、f(1)=1(与 gamefeel 测同口径 <1e-9)', () => { const oracle = makeEngineOracle(); const math = buildEngineMath(oracle); for (const name of EASING_11) { const fn = math.easing[name]; assert.ok(Math.abs(fn(0) - 0) < 1e-9, `${name}(0) 应 ≈ 0,实测 ${fn(0)}`); assert.ok(Math.abs(fn(1) - 1) < 1e-9, `${name}(1) 应 ≈ 1,实测 ${fn(1)}`); } }); /* ════════════════════════════════════════════════════════════════════════════ * T-A1-e:lerp/smoothStep 门面口径(含引擎 lerp 自带 percent 钳制) * ════════════════════════════════════════════════════════════════════════════ */ test('T-A1-e|lerp/smoothStep 门面逐值等价引擎 + lerp 自钳 percent', () => { const oracle = makeEngineOracle(); const math = buildEngineMath(oracle); // smoothStep 逐点比 oracle。 for (let i = 0; i <= 100; i++) { const p = i / 100; assert.ok(Math.abs(math.smoothStep(p) - oracle.smoothStep(p)) < 1e-12, `smoothStep(${p}) 应等价`); } // lerp 内点逐值比。 assert.equal(math.lerp(0, 10, 0.5), 5, 'lerp(0,10,0.5)=5'); assert.equal(math.lerp(2, 4, 0), 2, 'lerp 起点'); assert.equal(math.lerp(2, 4, 1), 4, 'lerp 终点'); // 引擎 lerp 自带 percent 钳制(clamp(percent)):越界 percent 被钳到 [0,1]。 assert.equal(math.lerp(0, 10, -1), 0, 'lerp percent<0 钳到 0 → 起点'); assert.equal(math.lerp(0, 10, 2), 10, 'lerp percent>1 钳到 1 → 终点'); }); /* ════════════════════════════════════════════════════════════════════════════ * 辅助:clampUnit 自身(门面钳制基元) * ════════════════════════════════════════════════════════════════════════════ */ test('辅助|clampUnit 钳到 [0,1]', () => { assert.equal(clampUnit(-0.5), 0); assert.equal(clampUnit(1.5), 1); assert.equal(clampUnit(0.3), 0.3); assert.equal(clampUnit(0), 0); assert.equal(clampUnit(1), 1); });