契约先行(sdk-interface.d.ts+镜像 additive):白名单新增恰等键族 save:{gameId}:{versionId}(≤65536 码元,
写入必须 {schemaVersion≥1, data} 信封,宿主闸 fail-closed);idle: 族 4096 逐字不动;弃多 key 分片。
宿主闸抽纯函数模块 storageGate.ts(四闸合一可 node 直测);save-progress 插件 v1.0.0→v1.1.0 additive:
setVersioned/getVersioned 逐级迁移/缺级兜底/降档读新档不毁档/无版本旧档按 0 版进链+探针两计数。
证据:插件单测 18/18、game-runtime 31/31、宿主闸负向演示脚本入仓真跑全断言过、vue-tsc+vite build 绿。
三书 M 门影子存档降级口径解除条件就位(卡牌 M2/割草 M4/夜市 M5 切换)。设计页 docs/plans/2026-07-06-002。
84 lines
4.4 KiB
JavaScript
84 lines
4.4 KiB
JavaScript
/**
|
|
* storage-gate-demo.mjs — 宿主 storage 闸负向演示(W-SAVE-CAP,测真源非复制品)
|
|
*
|
|
* game-studio 无测试跑器,本脚本用 game-runtime 的 esbuild 把真源 src/host/storageGate.ts
|
|
* 转译成临时 ESM 后由 node 断言。运行(仓根):
|
|
* node game-studio/scripts/storage-gate-demo.mjs
|
|
* 断言面:idle 4096 边界行为不变 / save 合法信封放行 / 超限拒 / 坏信封拒 / 白名单外 key 拒。
|
|
*/
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join, dirname, resolve } from 'node:path';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
import assert from 'node:assert/strict';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = resolve(here, '..', '..');
|
|
const src = join(repoRoot, 'game-studio', 'src', 'host', 'storageGate.ts');
|
|
const esbuild = join(repoRoot, 'game-runtime', 'node_modules', '.bin', 'esbuild');
|
|
|
|
const outDir = mkdtempSync(join(tmpdir(), 'storage-gate-'));
|
|
const outFile = join(outDir, 'storageGate.mjs');
|
|
try {
|
|
// esbuild 纯转译(strip 类型),产物即真源逻辑。
|
|
execFileSync(esbuild, [src, '--format=esm', `--outfile=${outFile}`], { stdio: 'inherit' });
|
|
const gate = await import(pathToFileURL(outFile).href);
|
|
const { gateStorageKey, gateStorageWrite, IDLE_MAX_CHARS, SAVE_MAX_CHARS } = gate;
|
|
|
|
const G = 'g1', V = 'v1';
|
|
const idleKey = `idle:${G}:${V}`;
|
|
const saveKey = `save:${G}:${V}`;
|
|
|
|
// ① idle 存量行为:恰等 key 放行、4096 边界(=过 / >拒)、坏 JSON 拒、落盘键带 wxgame: 前缀。
|
|
assert.equal(gateStorageKey(idleKey, G, V).ok, true);
|
|
assert.equal(gateStorageKey(idleKey, G, V).storageKey, 'wxgame:' + idleKey);
|
|
const okIdle = gateStorageWrite(idleKey, JSON.stringify({ a: 1 }), G, V);
|
|
assert.equal(okIdle.ok, true);
|
|
const at4096 = '"' + 'x'.repeat(IDLE_MAX_CHARS - 2) + '"'; // 恰 4096 码元的合法 JSON 字符串
|
|
assert.equal(at4096.length, IDLE_MAX_CHARS);
|
|
assert.equal(gateStorageWrite(idleKey, at4096, G, V).ok, true); // 边界=过(沿现行 > 判定)
|
|
assert.equal(gateStorageWrite(idleKey, at4096 + 'x', G, V).ok, false); // 超一码元=拒
|
|
assert.equal(gateStorageWrite(idleKey, '{bad json', G, V).ok, false); // 坏 JSON=拒
|
|
// idle 不要求信封:任意合法 JSON 对象即可(存量语义)。
|
|
assert.equal(gateStorageWrite(idleKey, JSON.stringify({ noEnvelope: 1 }), G, V).ok, true);
|
|
|
|
// ② save 族:合法信封放行;非信封 fail-closed;上限 65536。
|
|
const env = JSON.stringify({ schemaVersion: 1, data: { coins: 1 } });
|
|
const okSave = gateStorageWrite(saveKey, env, G, V);
|
|
assert.equal(okSave.ok, true);
|
|
assert.equal(okSave.family, 'save');
|
|
assert.equal(okSave.storageKey, 'wxgame:' + saveKey);
|
|
// 坏信封五连拒:无版本 / 版本 0 / 版本小数 / data 数组 / 顶层数组。
|
|
for (const bad of [
|
|
JSON.stringify({ data: { a: 1 } }),
|
|
JSON.stringify({ schemaVersion: 0, data: { a: 1 } }),
|
|
JSON.stringify({ schemaVersion: 1.5, data: { a: 1 } }),
|
|
JSON.stringify({ schemaVersion: 1, data: [1, 2] }),
|
|
JSON.stringify([{ schemaVersion: 1, data: {} }]),
|
|
]) {
|
|
assert.equal(gateStorageWrite(saveKey, bad, G, V).ok, false, 'save 坏信封必须拒:' + bad.slice(0, 40));
|
|
}
|
|
// 上限:恰 65536 过 / 超一码元拒(用信封内填充串控制总长)。
|
|
const padTo = (total) => {
|
|
const shell = JSON.stringify({ schemaVersion: 1, data: { p: '' } });
|
|
return JSON.stringify({ schemaVersion: 1, data: { p: 'x'.repeat(total - shell.length) } });
|
|
};
|
|
const at65536 = padTo(SAVE_MAX_CHARS);
|
|
assert.equal(at65536.length, SAVE_MAX_CHARS);
|
|
assert.equal(gateStorageWrite(saveKey, at65536, G, V).ok, true);
|
|
assert.equal(gateStorageWrite(saveKey, padTo(SAVE_MAX_CHARS + 1), G, V).ok, false);
|
|
// save 的大值对 idle 族同样超限(4096 闸不动)。
|
|
assert.equal(gateStorageWrite(idleKey, at65536, G, V).ok, false);
|
|
|
|
// ③ 白名单外一律拒:错前缀 / 错 gameId / 前缀通配尝试 / 空 key。
|
|
for (const badKey of ['tycoon:' + G + ':' + V, 'save:other:' + V, saveKey + ':extra', 'idle:' + G, '']) {
|
|
assert.equal(gateStorageKey(badKey, G, V).ok, false, 'key 必须拒:' + badKey);
|
|
}
|
|
|
|
console.log('storage-gate-demo:全部断言通过(idle 存量行为不变 / save 信封 fail-closed / 分层上限 / 白名单恰等)');
|
|
} finally {
|
|
rmSync(outDir, { recursive: true, force: true });
|
|
}
|