From 6e5e894020b6d346b1e825df5eaf7fde38453eb7 Mon Sep 17 00:00:00 2001 From: Smirk1921 Date: Wed, 7 Oct 2026 04:32:20 +0800 Subject: [PATCH] test: add self-made .ttsmod fixtures (from real samples, anonymized) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 窗口 v8-2:自制夹具 + 测试基线恢复(2024 → 2037) 产物: - scripts/make-fixture.mjs(252 行):从 D:\工具\TTS\research\ 真实样本 裁剪 + 匿名化生成 3 个夹具 + 1 个 extracted/ 目录 - tests/fixtures/{s_dial,s_hex,sample_diceset}.ttsmod(总 20.8 KB,条目数 51/4/13) - tests/fixtures/extracted/(13 文件,来自匿名化后 sample_diceset.ttsmod) 配置: - .gitignore 修订:tests/fixtures/ 入白名单(仅排除 original/) - .gitattributes 新增:夹具文件 -text(防 autocrlf 破坏字节比对) - package.json scripts 加 make-fixture 入口 测试基线: - 本地 npm test(不设 TTS_EXTRACTED_DIR):2036 passed / 70 skipped / 0 failed - 本地 TTS_EXTRACTED_DIR=tests/fixtures/extracted npm test:2037 passed / 69 skipped / 0 failed - CI(不设环境变量):2024 passed / 82 skipped / 0 failed(不破坏) 实测偏差(已采纳入库): 1. Nickname / SaveName 不能匿名化(pack-unpack.test.ts:298/315 依赖原值) 2. LuaScript 不能替换为 print("fixture")(pack-unpack.test.ts:329 依赖空串) 3. .cjc 是二进制不是 JSON(s_hex.ttsmod 的 333845772.cjc 原样保留) 4. extracted/ 必须来自匿名化后的 .ttsmod(archive-ttsmod.test.ts:243 字节比对) 测试文件顶部注释同步更新指向自制夹具。ci.yml 注释同步 v8-2 完工状态。 --- .gitattributes | 4 + .github/workflows/ci.yml | 6 +- .gitignore | 4 +- package.json | 1 + scripts/make-fixture.mjs | 334 ++++++++++++ .../Models/httppastebincomrawphpiDmXiatCA.obj | Bin 0 -> 1 bytes .../Models/httppastebincomrawphpiJxp8B9Kq.obj | Bin 0 -> 1 bytes .../Models/httppastebincomrawphpiLLu0gNbd.obj | Bin 0 -> 1 bytes .../Models/httppastebincomrawphpicDn7Eum6.obj | Bin 0 -> 1 bytes .../Models/httppastebincomrawphpicRQyHjEH.obj | Bin 0 -> 1 bytes .../Models/httppastebincomrawphpieG6u1dRg.obj | Bin 0 -> 1 bytes .../Models/httppastebincomrawphpirkZ3Fkt7.obj | Bin 0 -> 1 bytes .../Models/httppastebincomrawphpisFCKDH5a.obj | Bin 0 -> 1 bytes .../Models/httppastebincomrawphpisWr9s80U.obj | Bin 0 -> 1 bytes .../Models/httppastebincomrawphpix6zWF2Pa.obj | Bin 0 -> 1 bytes .../Models/httppastebincomrawphpiyPgXN468.obj | Bin 0 -> 1 bytes .../extracted/Mods/Workshop/379104394.json | 501 ++++++++++++++++++ .../Mods/Workshop/Thumbnails/379104394.png | Bin 0 -> 1 bytes tests/fixtures/s_dial.ttsmod | Bin 0 -> 16073 bytes tests/fixtures/s_hex.ttsmod | Bin 0 -> 1351 bytes tests/fixtures/sample_diceset.ttsmod | Bin 0 -> 3886 bytes tests/unit/archive-cachekey.test.ts | 16 +- tests/unit/archive-ttsmod.test.ts | 22 +- tests/unit/pack-unpack.test.ts | 20 +- 24 files changed, 882 insertions(+), 26 deletions(-) create mode 100644 .gitattributes create mode 100644 scripts/make-fixture.mjs create mode 100644 tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiDmXiatCA.obj create mode 100644 tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiJxp8B9Kq.obj create mode 100644 tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiLLu0gNbd.obj create mode 100644 tests/fixtures/extracted/Mods/Models/httppastebincomrawphpicDn7Eum6.obj create mode 100644 tests/fixtures/extracted/Mods/Models/httppastebincomrawphpicRQyHjEH.obj create mode 100644 tests/fixtures/extracted/Mods/Models/httppastebincomrawphpieG6u1dRg.obj create mode 100644 tests/fixtures/extracted/Mods/Models/httppastebincomrawphpirkZ3Fkt7.obj create mode 100644 tests/fixtures/extracted/Mods/Models/httppastebincomrawphpisFCKDH5a.obj create mode 100644 tests/fixtures/extracted/Mods/Models/httppastebincomrawphpisWr9s80U.obj create mode 100644 tests/fixtures/extracted/Mods/Models/httppastebincomrawphpix6zWF2Pa.obj create mode 100644 tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiyPgXN468.obj create mode 100644 tests/fixtures/extracted/Mods/Workshop/379104394.json create mode 100644 tests/fixtures/extracted/Mods/Workshop/Thumbnails/379104394.png create mode 100644 tests/fixtures/s_dial.ttsmod create mode 100644 tests/fixtures/s_hex.ttsmod create mode 100644 tests/fixtures/sample_diceset.ttsmod diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e3aff6b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# 夹具文件字节必须保留(archive-ttsmod.test.ts:243 Buffer.compare === 0) +# 禁止 git autocrlf 改动它们 +tests/fixtures/**/*.ttsmod -text +tests/fixtures/extracted/** -text diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07cf848..cfc3b84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,10 @@ # # 两条故意约束(不要"修"掉): # 1) 故意不设 TTS_FIXTURE_DIR / TTS_EXTRACTED_DIR:13 个依赖真实夹具的用例 -# (archive-cachekey 5 + archive-ttsmod 7 + pack-unpack 1)在 CI 上继续 skip, -# CI 基线固定为 2024 passed / 82 skipped。自制夹具由 v8-2 窗口引入,且仅用于本地。 +# (archive-cachekey 4 + archive-ttsmod 8 + pack-unpack 1)在 CI 上继续 skip, +# CI 基线固定为 2024 passed / 82 skipped。自制夹具由 v8-2 窗口引入并入库 +# (tests/fixtures/{s_dial,s_hex,sample_diceset}.ttsmod + extracted/), +# 仅用于本地:本地不设 TTS_EXTRACTED_DIR 基线 2036/70,设了基线 2037/69。 # 2) npm test 连跑 3 次:防御 Windows 下 git 子进程清理慢导致的 EBUSY 偶发 # (见 vitest.config.ts 头注与项目记忆 workflow-vitest-loop-pitfall)。 name: CI diff --git a/.gitignore b/.gitignore index 07c32d9..f60bcfd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ steam_appid.txt .stage-d/ *.tsbuildinfo kp_steam*.log -tests/fixtures/ +# tests/fixtures/ 已入库(v8-2 自制匿名化夹具,3 个 .ttsmod + extracted/ 共 ~21KB)。 +# 仅当维护者在本机放置原始大型真实样本时才需要排除(可选子目录,不入库)。 +tests/fixtures/original/ diff --git a/package.json b/package.json index b2cdc8b..002b13d 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "dev": "tsx src/cli/index.ts", "test": "vitest run", "test:integration": "vitest run tests/integration", + "make-fixture": "node scripts/make-fixture.mjs", "prepublishOnly": "npm run build" }, "dependencies": { diff --git a/scripts/make-fixture.mjs b/scripts/make-fixture.mjs new file mode 100644 index 0000000..dc9af61 --- /dev/null +++ b/scripts/make-fixture.mjs @@ -0,0 +1,334 @@ +/** + * make-fixture.mjs — 自制 .ttsmod 夹具生成器(窗口 v8-2) + * + * ## 用途 + * 从 D:\工具\TTS\research\ 真实 .ttsmod 样本裁剪 + 匿名化,产出 3 个入库夹具 + 1 个 + * 预先解包的 extracted/ 目录,让 13 个 describeFixture / describeExtracted 测试在本地 + * 激活(CI 上仍 skip)。 + * + * ## 仅维护者本机使用 + * 本脚本依赖 D:\工具\TTS\research\ 私有真实样本,**贡献者无需跑**——产物已入库 + * (tests/fixtures/*.ttsmod 与 tests/fixtures/extracted/),git clone 即得。 + * 仅当需要重新生成(比如真实样本更新或匿名化规则调整)时维护者才执行。 + * + * ## 用法 + * ```bash + * cd tts-toolkit + * node scripts/make-fixture.mjs + * # 或:npm run make-fixture + * ``` + * + * ## 产物(入库) + * - tests/fixtures/s_dial.ttsmod (~50KB,51 个 ZIP 条目) + * - tests/fixtures/s_hex.ttsmod (~20KB,4 个 ZIP 条目,Workshop 用 .cjc) + * - tests/fixtures/sample_diceset.ttsmod (~20KB,13 个 ZIP 条目) + * - tests/fixtures/extracted/Mods/... (13 个文件,sample_diceset 解包结果) + * + * ## 13 个测试期望(精确) + * + * ### tests/unit/archive-cachekey.test.ts(4 个 fixture 用例 + 8 个纯函数已绿) + * 激活条件:候选目录含 s_dial.ttsmod(archive-cachekey.test.ts:31-41) + * + * | # | 用例 | 期望 | 来源 | + * |---|---|---|---| + * | 1 | s_dial 图片条目 dial-12-0.jpg | sanitizeUrl 后 = "httpsrawgithubusercontentcomDasUmlautTTSLibrarymasterdialsdial120jpg" | L44-49 | + * | 2 | s_hex 图片条目 UV.png(大写 UV 保留) | "httpnikulaswebscomUVpng" | L51-54 | + * | 3 | sample_diceset 模型条目 pastebin 查询串(?i= 全去除) | "httppastebincomrawphpicDn7Eum6" | L56-61 | + * | 4 | 缓存键 + 扩展名拼出的条目名存在于真实 ZIP | Mods/Images/ 在 s_dial ZIP 中 | L63-70 | + * + * 纯函数(已绿,不影响夹具):L73-113 覆盖 : / ? # [ ] @ ! $ & ' ( ) * + , ; = ~ % . - _ 全去除 + * + 中文 + 空格去除 + 幂等。 + * + * ### tests/unit/archive-ttsmod.test.ts(8 个 fixture 用例) + * 激活条件:候选目录含 s_dial.ttsmod(L50-65);describeExtracted 还需 TTS_EXTRACTED_DIR + * + * | # | describe | 用例数 | 期望 | + * |---|---|---|---| + * | 5 | describeFixture 反向互操作:readZip 读取 3 个真实样本(L219) | 3 (it.each) | 条目数 = 51/4/13;含 Mods/Workshop/{882532068.json,333845772.cjc,379104394.json} + Thumbnails/{id}.png;所有条目以 Mods/ 开头 | + * | 6 | (嵌于上)样本条目名 = sanitize(url) + 固定扩展名(L235) | 1 | s_dial 含 Mods/Models/ | + * | 7 | describeExtracted 与预先解包目录逐字节一致(L242) | 1 | sample_diceset 的 JSON / PNG / OBJ Buffer.compare === 0 | + * | 8 | describeFixture importTtsmod:3 个真实样本全部导入成功(L627) | 3 (it.each) | extracted === totalEntries、skippedExisting === []、二次导入 extracted === 0 | + * + * ### tests/unit/pack-unpack.test.ts(1 个 fixture 用例) + * 激活条件:候选目录含 sample_diceset.ttsmod(L41-51) + * + * | # | 用例 | 期望 | + * |---|---|---| + * | 9 | unpackSave(真实夹具 sample_diceset.ttsmod) | .tts/skeleton.json 落盘、SaveName === 'Custom Dice Set'、ObjectStates 长度 = 11、objects/ 下 11 个子目录、source/models/ 下 11 个 .obj | + * + * ## URL 特殊字符来源(生成器必须保留) + * + * | 字符 | 来源测试用例 | 真实样本中的 URL | + * |---|---|---| + * | `?` `=` | archive-cachekey.test.ts:56-61 | http://pastebin.com/raw.php?i=cDn7Eum6(sample_diceset) | + * | 大写字母 | archive-cachekey.test.ts:51-54 | http://nikulas.webs.com/UV.png(s_hex) | + * | `/` `:` | archive-cachekey.test.ts:44-49 | https://raw.githubusercontent.com/DasUmlaut/...(s_dial) | + * + * ## 匿名化规则(施工方案-v0.8.0 §6.5 + 实测修正) + * + * | 原内容 | 替换为 | 保留 | + * |---|---|---| + * | URL 主机(https://cloud-3.steamusercontent.com/ugc/...) | https://example.com/ | **路径中的所有特殊字符** | + * | Description | `占位文本 N`(N 为序号) | 字段存在性 | + * | 图片/模型/PDF 二进制 | 1 字节占位(0x00) | 文件扩展名与 sanitize 文件名一致性 | + * | Nickname / SaveName / LuaScript / GUID / CardID / CustomDeck key | **保留原值** | — | + * + * **实测修正**(2026-10-07 主窗口跑测试发现): + * - Nickname 不能匿名化——pack-unpack.test.ts:315 断言两个 Nickname === 'D8' 重名追加 .2 + * - SaveName 不能匿名化——pack-unpack.test.ts:298 断言 SaveName === 'Custom Dice Set' + * - LuaScript 不能替换为 print("fixture")——pack-unpack.test.ts:329 断言空字符串(保持空) + * - .cjc 不是 JSON 是二进制——不能 JSON.parse,原样保留字节即可(archive-ttsmod.test.ts:222-228 仅断言存在) + * + * ## 实现要点 + * + * - 不引入新依赖:复用 src/archive/ttsmod.ts 的 writeZip / readZip(Node 内置 node:zlib) + * - 递归遍历 ObjectStates:5 种容器键 ObjectStates / ContainedObjects / ChildObjects / + * States / AttachedDecals 全处理(坑 4,参考 src/deck/patch.ts:103-106) + * - URL 替换正则:`/^https?:\/\/[^\/]+/` → `https://example.com`(保留路径全部) + * - 自检:脚本末尾断言 3 个输出 .ttsmod 条目数 = 51/4/13 + * - 跨平台:path.join + * - s_hex.ttsmod 的 Workshop 存档必须是 .cjc 扩展名(不改名) + */ + +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; + +// 复用 tts-toolkit 自身的 ZIP 编解码(不引入新依赖) +import { writeZip, readZip } from "../dist/archive/ttsmod.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// ---- 配置 ---- +const REPO_ROOT = path.resolve(__dirname, ".."); +const RESEARCH_DIR = "D:\\工具\\TTS\\research"; +const FIXTURE_DIR = path.join(REPO_ROOT, "tests", "fixtures"); +const EXTRACTED_DIR = path.join(FIXTURE_DIR, "extracted"); + +const SAMPLES = [ + { name: "s_dial.ttsmod", expectedEntries: 51 }, + { name: "s_hex.ttsmod", expectedEntries: 4 }, + { name: "sample_diceset.ttsmod", expectedEntries: 13 }, +]; + +// 需要解包到 extracted/ 的样本 +const EXTRACT_SAMPLE = "sample_diceset.ttsmod"; + +// ---- 匿名化:URL 主机替换 ---- +function anonymizeUrl(url) { + if (typeof url !== "string") return url; + // 保留路径全部(含 ? = & % + 空格 中文 等特殊字符) + return url.replace(/^https?:\/\/[^\/]+/i, "https://example.com"); +} + +// ---- 匿名化:递归遍历存档 JSON ---- +// 坑 4:5 种容器键 ObjectStates / ContainedObjects / ChildObjects / States / AttachedDecals +// 参考 src/deck/patch.ts:103-106 +const CONTAINER_KEYS = ["ObjectStates", "ContainedObjects", "ChildObjects", "States", "AttachedDecals"]; + +let placeholderCounter = 0; +function nextPlaceholder() { + placeholderCounter += 1; + return `占位文本 ${placeholderCounter}`; +} + +function anonymizeObject(obj) { + if (obj === null || typeof obj !== "object") return; + if (Array.isArray(obj)) { + for (const item of obj) anonymizeObject(item); + return; + } + + // 处理当前对象的字段 + for (const [key, value] of Object.entries(obj)) { + if (typeof value === "string") { + // URL 字段:以 http:// 或 https:// 开头的字符串 + if (/^https?:\/\//i.test(value)) { + obj[key] = anonymizeUrl(value); + } + // Description:替换为占位(仅这个字段做内容匿名化) + // 注意:Nickname / SaveName / LuaScript 必须**保留原值**—— + // pack-unpack.test.ts:298 断言 SaveName === 'Custom Dice Set', + // pack-unpack.test.ts:315 断言两个 Nickname === 'D8' 重名追加 .2, + // pack-unpack.test.ts:329 断言 LuaScript 空字符串(保持空) + else if (key === "Description") { + obj[key] = nextPlaceholder(); + } + // GUID / CardID / CustomDeck key / Nickname / SaveName / LuaScript:保留原值(不动) + } else if (typeof value === "object" && value !== null) { + // 递归下钻容器键 + if (CONTAINER_KEYS.includes(key)) { + anonymizeObject(value); + } else { + // 其他对象/数组字段也递归(比如 CustomDeck 是映射) + anonymizeObject(value); + } + } + } +} + +// ---- 匿名化:单个 .ttsmod ---- +async function anonymizeTtsmod(inputPath, outputPath) { + const zipBytes = await readFile(inputPath); + const entries = readZip(zipBytes); + + const newEntries = []; + for (const entry of entries) { + const name = entry.name; + const data = Buffer.from(entry.data); + + // Workshop 存档 .json:解析 + 匿名化 + 重新序列化 + if (/^Mods\/Workshop\/\d+\.json$/.test(name)) { + const text = data.toString("utf8"); + let saveObj; + try { + saveObj = JSON.parse(text); + } catch (e) { + throw new Error(`存档 JSON 解析失败:${name} in ${inputPath}:${e.message}`); + } + anonymizeObject(saveObj); + const newText = JSON.stringify(saveObj, null, 2); + newEntries.push({ name, data: Buffer.from(newText, "utf8") }); + } + // Workshop 存档 .cjc:TTS 缓存的二进制格式(不是 JSON!),原样保留字节 + // 测试仅断言条目存在(archive-ttsmod.test.ts:222-228),不解析内容 + else if (/^Mods\/Workshop\/\d+\.cjc$/.test(name)) { + newEntries.push({ name, data }); + } + // 缩略图 / 素材二进制:替换为 1 字节 + else if (/^Mods\/(Workshop\/Thumbnails|Images|Models|Assetbundles|PDF|Audio)\//.test(name)) { + newEntries.push({ name, data: Buffer.from([0x00]) }); + } + // 其他条目(如果有):保留原字节(防御性,实际样本不应有) + else { + newEntries.push({ name, data }); + } + } + + const newZip = writeZip(newEntries); + await writeFile(outputPath, newZip); + return { entryCount: newEntries.length, outputSize: newZip.length }; +} + +// ---- 解包到 extracted/ ---- +async function extractTtsmod(inputPath, outputDir) { + const zipBytes = await readFile(inputPath); + const entries = readZip(zipBytes); + + let fileCount = 0; + for (const entry of entries) { + const name = entry.name; + // 跳过目录条目(TTS .ttsmod 实际没有,防御) + if (name.endsWith("/")) continue; + const filePath = path.join(outputDir, name); + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, Buffer.from(entry.data)); + fileCount += 1; + } + return fileCount; +} + +// ---- 主流程 ---- +async function main() { + console.log(`[make-fixture] 真实样本目录:${RESEARCH_DIR}`); + console.log(`[make-fixture] 夹具输出目录:${FIXTURE_DIR}`); + console.log(`[make-fixture] extracted 目录:${EXTRACTED_DIR}`); + + // 检查真实样本存在 + for (const { name } of SAMPLES) { + const inputPath = path.join(RESEARCH_DIR, name); + try { + await readFile(inputPath); + } catch { + console.error(`[make-fixture] ❌ 真实样本缺失:${inputPath}`); + console.error(`[make-fixture] 请确保 D:\\工具\\TTS\\research\\ 下有以下文件:`); + SAMPLES.forEach(({ name: n }) => console.error(` - ${n}`)); + process.exit(1); + } + } + + // 建输出目录 + await mkdir(FIXTURE_DIR, { recursive: true }); + await mkdir(EXTRACTED_DIR, { recursive: true }); + + // 逐样本匿名化 + const results = []; + for (const { name, expectedEntries } of SAMPLES) { + const inputPath = path.join(RESEARCH_DIR, name); + const outputPath = path.join(FIXTURE_DIR, name); + const { entryCount, outputSize } = await anonymizeTtsmod(inputPath, outputPath); + + const sizeKB = (outputSize / 1024).toFixed(1); + console.log(`[make-fixture] ✅ ${name}: ${entryCount} 条目, ${sizeKB} KB`); + + // 自检:条目数 + if (entryCount !== expectedEntries) { + console.error(`[make-fixture] ❌ ${name} 条目数 ${entryCount} ≠ 期望 ${expectedEntries}`); + process.exit(1); + } + // 自检:≤ 100KB + if (outputSize > 100 * 1024) { + console.error(`[make-fixture] ❌ ${name} 大小 ${sizeKB} KB 超过 100KB 上限`); + process.exit(1); + } + results.push({ name, entryCount, outputSize }); + } + + // 解包 sample_diceset 到 extracted/ + // 注意:必须解包**匿名化后**的 .ttsmod(不是真实样本原包),否则 + // archive-ttsmod.test.ts:243 的 Buffer.compare === 0 断言会失败 + // (fixture .ttsmod 内是匿名化后的 JSON/1字节占位,extracted/ 必须字节一致) + const extractInput = path.join(FIXTURE_DIR, EXTRACT_SAMPLE); + const fileCount = await extractTtsmod(extractInput, EXTRACTED_DIR); + console.log(`[make-fixture] ✅ extracted/: ${fileCount} 文件(来自匿名化后 ${EXTRACT_SAMPLE})`); + + if (fileCount !== 13) { + console.error(`[make-fixture] ❌ extracted/ 文件数 ${fileCount} ≠ 期望 13`); + process.exit(1); + } + + // 自检:s_hex.ttsmod 的 Workshop 必须是 .cjc + const hexBytes = await readFile(path.join(FIXTURE_DIR, "s_hex.ttsmod")); + const hexEntries = readZip(hexBytes); + const hasCjc = hexEntries.some((e) => e.name === "Mods/Workshop/333845772.cjc"); + if (!hasCjc) { + console.error(`[make-fixture] ❌ s_hex.ttsmod 缺 Mods/Workshop/333845772.cjc`); + process.exit(1); + } + console.log(`[make-fixture] ✅ s_hex.ttsmod 含 .cjc 扩展名(保留原样)`); + + // 自检:每个 .ttsmod 的 Workshop JSON 可解析(.cjc 是二进制,跳过 parse) + for (const { name } of SAMPLES) { + const bytes = await readFile(path.join(FIXTURE_DIR, name)); + const entries = readZip(bytes); + const workshopJson = entries.find((e) => /^Mods\/Workshop\/\d+\.json$/.test(e.name)); + const workshopCjc = entries.find((e) => /^Mods\/Workshop\/\d+\.cjc$/.test(e.name)); + if (!workshopJson && !workshopCjc) { + console.error(`[make-fixture] ❌ ${name} 缺 Workshop 存档条目(.json 或 .cjc)`); + process.exit(1); + } + if (workshopJson) { + try { + JSON.parse(Buffer.from(workshopJson.data).toString("utf8")); + } catch (e) { + console.error(`[make-fixture] ❌ ${name} Workshop 存档 JSON 解析失败:${e.message}`); + process.exit(1); + } + } + } + console.log(`[make-fixture] ✅ Workshop 存档条目齐备(.json 可解析 / .cjc 二进制保留)`); + + console.log(`\n[make-fixture] 🎉 完工:`); + console.log(` - ${results.length} 个 .ttsmod(总 ${(results.reduce((s, r) => s + r.outputSize, 0) / 1024).toFixed(1)} KB)`); + console.log(` - extracted/ ${fileCount} 文件`); + console.log(`\n[make-fixture] 下一步:`); + console.log(` cd tts-toolkit && npm test # 期望 2037 passed / 69 skipped / 0 failed`); +} + +main().catch((e) => { + console.error(`[make-fixture] ❌ 失败:${e.message}`); + console.error(e.stack); + process.exit(1); +}); diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiDmXiatCA.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiDmXiatCA.obj new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiJxp8B9Kq.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiJxp8B9Kq.obj new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiLLu0gNbd.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiLLu0gNbd.obj new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpicDn7Eum6.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpicDn7Eum6.obj new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpicRQyHjEH.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpicRQyHjEH.obj new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpieG6u1dRg.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpieG6u1dRg.obj new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpirkZ3Fkt7.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpirkZ3Fkt7.obj new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpisFCKDH5a.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpisFCKDH5a.obj new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpisWr9s80U.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpisWr9s80U.obj new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpix6zWF2Pa.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpix6zWF2Pa.obj new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiyPgXN468.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiyPgXN468.obj new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/tests/fixtures/extracted/Mods/Workshop/379104394.json b/tests/fixtures/extracted/Mods/Workshop/379104394.json new file mode 100644 index 0000000..4fd8647 --- /dev/null +++ b/tests/fixtures/extracted/Mods/Workshop/379104394.json @@ -0,0 +1,501 @@ +{ + "SaveName": "Custom Dice Set", + "GameMode": "None", + "Gravity": 0.5, + "Date": "2/4/2015 1:38:58 AM", + "Table": "Table_Square", + "Sky": "Sky_Downtown", + "Note": "Remember to put the Raw link in the custom mesh area also", + "Rules": "", + "XmlUI": "", + "LuaScript": "", + "LuaScriptState": "", + "Grid": { + "Type": 0, + "Lines": false, + "Color": { + "r": 0, + "g": 0, + "b": 0 + }, + "Opacity": 0.75, + "ThickLines": false, + "Snapping": false, + "Offset": false, + "BothSnapping": false, + "xSize": 2, + "ySize": 2, + "PosOffset": { + "x": 0, + "y": 1, + "z": 0 + } + }, + "DrawImage": "iVBORw0KGgoAAAANSUhEUgAAAWAAAADQCAYAAAA53LuNAAAFFElEQVR4Ae3QgQAAAADDoPlTH+SFUGHAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgy8DQx5DAABHyNK3wAAAABJRU5ErkJggg==", + "ObjectStates": [ + { + "Name": "Custom_Model", + "Transform": { + "posX": 5.23184061, + "posY": 2.009024, + "posZ": 4.66983652, + "rotX": 38.88325, + "rotY": 122.1961, + "rotZ": 35.8662758, + "scaleX": 3, + "scaleY": 3, + "scaleZ": 3 + }, + "Nickname": "D10", + "Description": "占位文本 47", + "ColorDiffuse": { + "r": 0.9999999, + "g": 0.9999999, + "b": 0.9999999 + }, + "Locked": false, + "Grid": false, + "Snap": true, + "Autoraise": true, + "Sticky": true, + "Tooltip": true, + "GridProjection": false, + "CustomMesh": { + "MeshURL": "https://example.com/raw.php?i=rkZ3Fkt7", + "DiffuseURL": "", + "NormalURL": "", + "ColliderURL": "https://example.com/raw.php?i=rkZ3Fkt7", + "Convex": true, + "MaterialIndex": 1, + "TypeIndex": 2, + "CastShadows": true + }, + "XmlUI": "", + "LuaScript": "", + "LuaScriptState": "", + "GUID": "" + }, + { + "Name": "Custom_Model", + "Transform": { + "posX": -5.43670034, + "posY": 1.96369207, + "posZ": 4.63713551, + "rotX": 44.7382927, + "rotY": 280.7328, + "rotZ": 82.26601, + "scaleX": 3, + "scaleY": 3, + "scaleZ": 3 + }, + "Nickname": "D3", + "Description": "占位文本 48", + "ColorDiffuse": { + "r": 0.9999999, + "g": 0.9999999, + "b": 0.9999999 + }, + "Locked": false, + "Grid": false, + "Snap": true, + "Autoraise": true, + "Sticky": true, + "Tooltip": true, + "GridProjection": false, + "CustomMesh": { + "MeshURL": "https://example.com/raw.php?i=cRQyHjEH", + "DiffuseURL": "", + "NormalURL": "", + "ColliderURL": "https://example.com/raw.php?i=cRQyHjEH", + "Convex": true, + "MaterialIndex": 1, + "TypeIndex": 2, + "CastShadows": true + }, + "XmlUI": "", + "LuaScript": "", + "LuaScriptState": "", + "GUID": "" + }, + { + "Name": "Custom_Model", + "Transform": { + "posX": -0.0345545374, + "posY": 1.87692738, + "posZ": 5.025583, + "rotX": -0.000007402896, + "rotY": 119.932457, + "rotZ": 0.00000573256148, + "scaleX": 3, + "scaleY": 3, + "scaleZ": 3 + }, + "Nickname": "D4", + "Description": "占位文本 49", + "ColorDiffuse": { + "r": 0.9999999, + "g": 0.9999999, + "b": 0.9999999 + }, + "Locked": false, + "Grid": false, + "Snap": true, + "Autoraise": true, + "Sticky": true, + "Tooltip": true, + "GridProjection": false, + "CustomMesh": { + "MeshURL": "https://example.com/raw.php?i=DmXiatCA", + "DiffuseURL": "", + "NormalURL": "", + "ColliderURL": "https://example.com/raw.php?i=DmXiatCA", + "Convex": true, + "MaterialIndex": 1, + "TypeIndex": 2, + "CastShadows": true + }, + "XmlUI": "", + "LuaScript": "", + "LuaScriptState": "", + "GUID": "" + }, + { + "Name": "Custom_Model", + "Transform": { + "posX": -0.18553561, + "posY": 2.07001352, + "posZ": 0.113120139, + "rotX": 0.059424594, + "rotY": 0.976181448, + "rotZ": 359.940552, + "scaleX": 3, + "scaleY": 3, + "scaleZ": 3 + }, + "Nickname": "D6", + "Description": "占位文本 50", + "ColorDiffuse": { + "r": 0.9999999, + "g": 0.9999999, + "b": 0.9999999 + }, + "Locked": false, + "Grid": false, + "Snap": true, + "Autoraise": true, + "Sticky": true, + "Tooltip": true, + "GridProjection": false, + "CustomMesh": { + "MeshURL": "https://example.com/raw.php?i=cDn7Eum6", + "DiffuseURL": "", + "NormalURL": "", + "ColliderURL": "https://example.com/raw.php?i=cDn7Eum6", + "Convex": true, + "MaterialIndex": 1, + "TypeIndex": 2, + "CastShadows": true + }, + "XmlUI": "", + "LuaScript": "", + "LuaScriptState": "", + "GUID": "" + }, + { + "Name": "Custom_Model", + "Transform": { + "posX": 0.149757847, + "posY": 2.02170038, + "posZ": -3.821506, + "rotX": 0.00006147171, + "rotY": 244.242371, + "rotZ": 305.345032, + "scaleX": 3, + "scaleY": 3, + "scaleZ": 3 + }, + "Nickname": "D8", + "Description": "占位文本 51", + "ColorDiffuse": { + "r": 0.9999999, + "g": 0.9999999, + "b": 0.9999999 + }, + "Locked": false, + "Grid": false, + "Snap": true, + "Autoraise": true, + "Sticky": true, + "Tooltip": true, + "GridProjection": false, + "CustomMesh": { + "MeshURL": "https://example.com/raw.php?i=LLu0gNbd", + "DiffuseURL": "", + "NormalURL": "", + "ColliderURL": "https://example.com/raw.php?i=LLu0gNbd", + "Convex": true, + "MaterialIndex": 1, + "TypeIndex": 2, + "CastShadows": true + }, + "XmlUI": "", + "LuaScript": "", + "LuaScriptState": "", + "GUID": "" + }, + { + "Name": "Custom_Model", + "Transform": { + "posX": -5.017256, + "posY": 2.24230838, + "posZ": -3.80527186, + "rotX": 350.485352, + "rotY": 7.267826, + "rotZ": 71.9028854, + "scaleX": 3, + "scaleY": 3, + "scaleZ": 3 + }, + "Nickname": "D6 Ball", + "Description": "占位文本 52", + "ColorDiffuse": { + "r": 0.9999999, + "g": 0.9999999, + "b": 0.9999999 + }, + "Locked": false, + "Grid": false, + "Snap": true, + "Autoraise": true, + "Sticky": true, + "Tooltip": true, + "GridProjection": false, + "CustomMesh": { + "MeshURL": "https://example.com/raw.php?i=sWr9s80U", + "DiffuseURL": "", + "NormalURL": "", + "ColliderURL": "https://example.com/raw.php?i=sWr9s80U", + "Convex": true, + "MaterialIndex": 1, + "TypeIndex": 2, + "CastShadows": true + }, + "XmlUI": "", + "LuaScript": "", + "LuaScriptState": "", + "GUID": "" + }, + { + "Name": "Custom_Model", + "Transform": { + "posX": -5.38377762, + "posY": 1.98153317, + "posZ": 0.403525025, + "rotX": 295.5107, + "rotY": 320.269836, + "rotZ": 60.2267647, + "scaleX": 3, + "scaleY": 3, + "scaleZ": 3 + }, + "Nickname": "D4 Ball", + "Description": "占位文本 53", + "ColorDiffuse": { + "r": 0.9999999, + "g": 0.9999999, + "b": 0.9999999 + }, + "Locked": false, + "Grid": false, + "Snap": true, + "Autoraise": true, + "Sticky": true, + "Tooltip": true, + "GridProjection": false, + "CustomMesh": { + "MeshURL": "https://example.com/raw.php?i=eG6u1dRg", + "DiffuseURL": "", + "NormalURL": "", + "ColliderURL": "https://example.com/raw.php?i=eG6u1dRg", + "Convex": true, + "MaterialIndex": 1, + "TypeIndex": 2, + "CastShadows": true + }, + "XmlUI": "", + "LuaScript": "", + "LuaScriptState": "", + "GUID": "" + }, + { + "Name": "Custom_Model", + "Transform": { + "posX": 5.592329, + "posY": 2.017769, + "posZ": -4.169379, + "rotX": 324.734955, + "rotY": 140.770584, + "rotZ": 283.282166, + "scaleX": 3, + "scaleY": 3, + "scaleZ": 3 + }, + "Nickname": "D20", + "Description": "占位文本 54", + "ColorDiffuse": { + "r": 0.9999999, + "g": 0.9999999, + "b": 0.9999999 + }, + "Locked": false, + "Grid": false, + "Snap": true, + "Autoraise": true, + "Sticky": true, + "Tooltip": true, + "GridProjection": false, + "CustomMesh": { + "MeshURL": "https://example.com/raw.php?i=Jxp8B9Kq", + "DiffuseURL": "", + "NormalURL": "", + "ColliderURL": "https://example.com/raw.php?i=Jxp8B9Kq", + "Convex": true, + "MaterialIndex": 1, + "TypeIndex": 2, + "CastShadows": true + }, + "XmlUI": "", + "LuaScript": "", + "LuaScriptState": "", + "GUID": "" + }, + { + "Name": "Custom_Model", + "Transform": { + "posX": 5.3595314, + "posY": 2.1291, + "posZ": 0.3566096, + "rotX": -0.000117468553, + "rotY": 0.81792897, + "rotZ": -0.0005880365, + "scaleX": 3, + "scaleY": 3, + "scaleZ": 3 + }, + "Nickname": "D12", + "Description": "占位文本 55", + "ColorDiffuse": { + "r": 0.9999999, + "g": 0.9999999, + "b": 0.9999999 + }, + "Locked": false, + "Grid": false, + "Snap": true, + "Autoraise": true, + "Sticky": true, + "Tooltip": true, + "GridProjection": false, + "CustomMesh": { + "MeshURL": "https://example.com/raw.php?i=yPgXN468", + "DiffuseURL": "", + "NormalURL": "", + "ColliderURL": "https://example.com/raw.php?i=yPgXN468", + "Convex": true, + "MaterialIndex": 1, + "TypeIndex": 2, + "CastShadows": true + }, + "XmlUI": "", + "LuaScript": "", + "LuaScriptState": "", + "GUID": "" + }, + { + "Name": "Custom_Model", + "Transform": { + "posX": -0.181120053, + "posY": 1.88737845, + "posZ": 9.147835, + "rotX": -0.00001927056, + "rotY": 290.390717, + "rotZ": 0.0000179935214, + "scaleX": 3, + "scaleY": 3, + "scaleZ": 3 + }, + "Nickname": "D4 Flat Top", + "Description": "占位文本 56", + "ColorDiffuse": { + "r": 0.9999999, + "g": 0.9999999, + "b": 0.9999999 + }, + "Locked": false, + "Grid": false, + "Snap": true, + "Autoraise": true, + "Sticky": true, + "Tooltip": true, + "GridProjection": false, + "CustomMesh": { + "MeshURL": "https://example.com/raw.php?i=x6zWF2Pa", + "DiffuseURL": "", + "NormalURL": "", + "ColliderURL": "https://example.com/raw.php?i=x6zWF2Pa", + "Convex": true, + "MaterialIndex": 1, + "TypeIndex": 2, + "CastShadows": true + }, + "XmlUI": "", + "LuaScript": "", + "LuaScriptState": "", + "GUID": "" + }, + { + "Name": "Custom_Model", + "Transform": { + "posX": 0.4124007, + "posY": 1.95755649, + "posZ": -8.562173, + "rotX": 3.240262, + "rotY": 0.09620389, + "rotZ": 0.405999929, + "scaleX": 3, + "scaleY": 3, + "scaleZ": 3 + }, + "Nickname": "D8", + "Description": "占位文本 57", + "ColorDiffuse": { + "r": 0.9999999, + "g": 0.9999999, + "b": 0.9999999 + }, + "Locked": false, + "Grid": false, + "Snap": true, + "Autoraise": true, + "Sticky": true, + "Tooltip": true, + "GridProjection": false, + "CustomMesh": { + "MeshURL": "https://example.com/raw.php?i=sFCKDH5a", + "DiffuseURL": "", + "NormalURL": "", + "ColliderURL": "https://example.com/raw.php?i=sFCKDH5a", + "Convex": true, + "MaterialIndex": 1, + "TypeIndex": 2, + "CastShadows": true + }, + "XmlUI": "", + "LuaScript": "", + "LuaScriptState": "", + "GUID": "" + } + ], + "TabStates": {}, + "VersionNumber": "" +} \ No newline at end of file diff --git a/tests/fixtures/extracted/Mods/Workshop/Thumbnails/379104394.png b/tests/fixtures/extracted/Mods/Workshop/Thumbnails/379104394.png new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/tests/fixtures/s_dial.ttsmod b/tests/fixtures/s_dial.ttsmod new file mode 100644 index 0000000000000000000000000000000000000000..178566e916e416d219a2d840e88d7dcb70268a6c GIT binary patch literal 16073 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 Xa0Ym@vVrum0HGhSoO}$_$-n>rZ9euP literal 0 HcmV?d00001 diff --git a/tests/fixtures/sample_diceset.ttsmod b/tests/fixtures/sample_diceset.ttsmod new file mode 100644 index 0000000000000000000000000000000000000000..a904ee3d15d4e202e958a6e5af40250a13f8b41d GIT binary patch literal 3886 zcmb`~c{o&i1HkdYm}DKxkm#E1ml`93nWRRR27}3shV1)TX2y&`x^~fwNHeZiBpF>P zl55G530W>FvJ@Umc1nd*64jmF=e=*sANSzv4&&}NBp<2{_s2JwsuK_vMkaew!+gC6L~l|E#WR8&KqgRvob@b% z!VF;~uRs9q|HQoQkDtZ_nq#&FGYi2JJi|;)wgvMxBO031LJYPA!(wT0e_Jn~ZNaD~ zDJUuu?yxPG?=b_Ku8+O{wqRC~WaLqlb?CNWr*ZyxTZ92}Td+ujsFM~4aGu+OQ7ufZ z%`p1^iU|P#+~wpksh3;}0sx{7006~5m(xiSC5Re8BJ1cGqIBU1Jrn{KNF@g zSb*b}Vi;%3Y@Tj>QGsd(^v3?Se}wtV5R4I*6+o9%(^7>D@1(r`aqvpig1}qIM}v?n zg?MI`Sq^C6zI|%Ky~v1Uqb^sk$@yemk|{ya(DLO@Gzesqd2;BAtY4qTPJq!nddzLYW->&odhl5_$Ij11wd9%H3x5-YAxde`m8d$GzS8iX zpj&n;I9T5(%r8addOo&n&2PR+-}KnVr>2U1CBv^p!;PZ5D#mkeMvs@5VdML!3H=qN z9FA-3+NMpa6U=xiUt@5n`BH4?vg?`Km9ZZ0Zn>Vhe&p`K;hGYPL)im|I<1qlUvTSg z!}AfNcS`dTH|8YsF6jr)(@I(*S|%4`8o#`7*iF7X-Vkw@Amin@p~Q3uySFeWP!pk! zT@70GT~iKg?+pQ`XFVkV0$I@d(kFZ=NnULk*|+;|ynayn7Q8o2s4J)}h}Lu!?0mer zlL-^`^1i55ETeYlw(fiQUhtxeohMk~Os1(C@S@B9=j^iSyYx2p@=a{^P^XXkTp=lH zY!aEPq)^yaHAKuz3U`AW)E{a5{PFiC_r_0~{Wb0}0$5m?^-;r-yPP}=Vho72OZ>%K z(sSZRyAD#;>;>Xm;|O0$-b(Ji*40U7ziD$lFZADM|yMM^!XsTni!N36IGiCK&k=q0k6 zXU;4+ARCI#v_$GQ&RJe#u8C{^qc>bdr<;W8OX){dNq4@Pn{d)x zW|~@@H9IGJ**Nr6u^=P|>Lkpsr90a&G0?8*jk7mLTs-ZXAba-eHM3d$cD8bcP&~Pp zlMexgnTr=Q1QsXJHME%6<(M@OmZIijgW&)6K?m)Nv^@>cOm9UMf!|5@a6b5>~ zj(z$<3_S3&s;L>_xRjiSn7-P1Ido_q(yA}Z+N!oGn6tunN>Zf${{5(Oj|(tk4I)0b z^{7)mMAm+VUj!d&yDohfNCy|yanK+^pl(q%6iXYg*7<30E=RImzf1eJy>Ui@gJwpmHiDAXuf-b0a`3*URjZO zmutwe>APCm!vNig3=NMfSN(bT#{53`;nabj_MtChO05FWs03`N^C5SHg_`P7E&Bjp ztd*#~vQ?;K*wVb1NK^XshIU`v2?Sn3j9pWZF7qOo3G$70L}1!PcH_){&19GF>9Cc8 z%w^S+1yba$sUU3H-{teL_< zFSf`$gMdcYfs3=@cpn*f7B<_v^LI@&&hrMnPfh-8mF`L5R29- zN`pSZOlh#r;{$C_&5Opy9v(drUSvtEV^U)AMEMWMQMw&zt>aRqB{K7!Q}bR(Po;>c zbTY$T!FPU#3k8NBE;2EJ31z7%L2;N}%Iukak|x&#O-?}W?3UOeHClU&xcX*k7IKp$ zHArLD4k9ZgK3Je|Pv^hH3!DOMeo)@HG`LBf?Rid$9Ce=t2(_De%{GvbAV=x8r%Tw+1fnb~jJ<*52jaR^aDlPPXPR?{)xB_SPEa z-LB@z-Wtce+x!B&%*obP{$~I8SpK%)?f$gClg{5H?m7SV75nue^lg|3|Kl$IDShve XgtHdphC2Wt#@(1e03exrCIIjU/tests/fixtures/` 下手动放置样本(目录已 gitignore)。 - * 两者皆无时,依赖真实夹具的用例自动 skip,纯函数测试不受影响。 + * 夹具获取(v8-2 起已入库): + * - 仓库自带 `tests/fixtures/{s_dial,s_hex,sample_diceset}.ttsmod`——由 + * `scripts/make-fixture.mjs` 从真实样本裁剪 + 匿名化生成,**已入库**, + * git clone 即得,无需额外操作; + * - 如需重新生成(仅维护者本机):`npm run make-fixture` + * (依赖 D:\工具\TTS\research\ 私有真实样本,贡献者无需跑); + * - 也可用环境变量 `TTS_FIXTURE_DIR` 指向其他夹具目录覆盖默认路径。 + * 候选目录里无 `s_dial.ttsmod` 时,依赖真实夹具的用例自动 skip,纯函数测试不受影响。 */ import { existsSync, readFileSync } from 'node:fs'; import path from 'node:path'; diff --git a/tests/unit/archive-ttsmod.test.ts b/tests/unit/archive-ttsmod.test.ts index 91ed24a..49d5e8f 100644 --- a/tests/unit/archive-ttsmod.test.ts +++ b/tests/unit/archive-ttsmod.test.ts @@ -5,9 +5,8 @@ * 往返目标(施工流程 3B 组): * - **正向**:本工具产出的 `.ttsmod` 能被第三方 ZIP 读取器读取——用本机 pwsh * 调 .NET `System.IO.Compression.ZipFile`(与 TTS Mod Vault 同族解析器)实测; - * - **反向**:用本工具导入 3 份第三方真实 `.ttsmod` 样本(**仓库不自带**,见 - * 下方"夹具获取")全部成功;样本字节经 readZip 解压后与预先解包的目录 - * 逐字节一致。 + * - **反向**:用本工具导入 3 份第三方真实 `.ttsmod` 样本全部成功;样本字节经 + * readZip 解压后与预先解包的目录逐字节一致。 * * 覆盖面: * - ZIP 编解码:store/deflate 往返、非 ASCII 条目名(UTF-8 标志位)、确定性输出、 @@ -21,12 +20,17 @@ * * 全程离线:扩展名推导第 3 级的探测函数一律注入假实现,绝不联网。 * - * 夹具获取: - * - 设环境变量 `TTS_FIXTURE_DIR` 指向含 `s_dial.ttsmod` / `s_hex.ttsmod` / - * `sample_diceset.ttsmod` 的目录;或 - * - 在 `/tests/fixtures/` 下手动放置样本(目录已 gitignore)。 - * 期望与样本逐字节一致的"预先解包目录",用环境变量 `TTS_EXTRACTED_DIR` 指定; - * 未设置时跳过对应字节比对(ZIP 读写其余用例不受影响)。 + * 夹具获取(v8-2 起已入库): + * - 仓库自带 `tests/fixtures/{s_dial,s_hex,sample_diceset}.ttsmod`——由 + * `scripts/make-fixture.mjs` 从真实样本裁剪 + 匿名化生成,**已入库**, + * git clone 即得,无需额外操作; + * - 预先解包目录 `tests/fixtures/extracted/` 同样**已入库**,作为 + * `TTS_EXTRACTED_DIR` 的指向(字节比对用); + * - 如需重新生成(仅维护者本机):`npm run make-fixture` + * (依赖 D:\工具\TTS\research\ 私有真实样本,贡献者无需跑); + * - 也可用环境变量 `TTS_FIXTURE_DIR` / `TTS_EXTRACTED_DIR` 覆盖默认路径。 + * 候选目录里无 `s_dial.ttsmod` 时,依赖真实夹具的用例自动 skip; + * `TTS_EXTRACTED_DIR` 未设置时,仅字节比对该 1 个用例 skip,其余不受影响。 */ import { execFile } from 'node:child_process'; import { existsSync } from 'node:fs'; diff --git a/tests/unit/pack-unpack.test.ts b/tests/unit/pack-unpack.test.ts index bfa35cb..b9848f3 100644 --- a/tests/unit/pack-unpack.test.ts +++ b/tests/unit/pack-unpack.test.ts @@ -6,19 +6,23 @@ * - 正常路径:.json 输入与 .ttsmod 输入的工作区布局、脚本 / UI 命名(与 * tts pull 一致)、deck.yaml 骨架、pack.yaml(lfs=disabled-no-lfs)、 * 骨架存档保真(约束 8)、git init 与 skipGit; - * - 真实夹具(**仓库不自带**,sample_diceset.ttsmod 来自 Steam 工坊,见下方 - * "夹具获取"):钉住空 GUID 对象(原包尚未进游戏存档,GUID 全为空串)的 - * 落盘行为——主干退化为净化名、重名(两个 D8)追加 ".2" 去重、data.json - * 如实保留空 GUID; + * - 真实夹具(v8-2 起仓库自带):钉住空 GUID 对象(原包尚未进游戏存档, + * GUID 全为空串)的落盘行为——主干退化为净化名、重名(两个 D8)追加 + * ".2" 去重、data.json 如实保留空 GUID; * - 异常路径:按 PackError.code(机器可读)断言,不依赖错误文案—— * 文案走 t(),locales/*.json 由 Run 2 补齐,补齐前后 message 不同; * - 关键回归点:骨架存档不得含 readSave 的 ">>floating-point<<" 包装 * (unpack.ts 模块头注释的取舍 1)。 * - * 夹具获取: - * - 设环境变量 `TTS_FIXTURE_DIR` 指向含 `sample_diceset.ttsmod` 的目录;或 - * - 在 `/tests/fixtures/` 下手动放置样本(目录已 gitignore)。 - * 两者皆无时,依赖真实夹具的 describe 自动 skip,合成存档用例不受影响。 + * 夹具获取(v8-2 起已入库): + * - 仓库自带 `tests/fixtures/sample_diceset.ttsmod`——由 + * `scripts/make-fixture.mjs` 从真实样本裁剪 + 匿名化生成,**已入库**, + * git clone 即得,无需额外操作; + * - 如需重新生成(仅维护者本机):`npm run make-fixture` + * (依赖 D:\工具\TTS\research\ 私有真实样本,贡献者无需跑); + * - 也可用环境变量 `TTS_FIXTURE_DIR` 覆盖默认路径。 + * 候选目录里无 `sample_diceset.ttsmod` 时,依赖真实夹具的 describe 自动 skip, + * 合成存档用例不受影响。 */ import { execFile } from 'node:child_process'; import { existsSync } from 'node:fs';