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 0000000..f76dd23 Binary files /dev/null and b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiDmXiatCA.obj differ diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiJxp8B9Kq.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiJxp8B9Kq.obj new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiJxp8B9Kq.obj differ diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiLLu0gNbd.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiLLu0gNbd.obj new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiLLu0gNbd.obj differ diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpicDn7Eum6.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpicDn7Eum6.obj new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpicDn7Eum6.obj differ diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpicRQyHjEH.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpicRQyHjEH.obj new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpicRQyHjEH.obj differ diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpieG6u1dRg.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpieG6u1dRg.obj new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpieG6u1dRg.obj differ diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpirkZ3Fkt7.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpirkZ3Fkt7.obj new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpirkZ3Fkt7.obj differ diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpisFCKDH5a.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpisFCKDH5a.obj new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpisFCKDH5a.obj differ diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpisWr9s80U.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpisWr9s80U.obj new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpisWr9s80U.obj differ diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpix6zWF2Pa.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpix6zWF2Pa.obj new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpix6zWF2Pa.obj differ diff --git a/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiyPgXN468.obj b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiyPgXN468.obj new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/tests/fixtures/extracted/Mods/Models/httppastebincomrawphpiyPgXN468.obj differ 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 0000000..f76dd23 Binary files /dev/null and b/tests/fixtures/extracted/Mods/Workshop/Thumbnails/379104394.png differ diff --git a/tests/fixtures/s_dial.ttsmod b/tests/fixtures/s_dial.ttsmod new file mode 100644 index 0000000..178566e Binary files /dev/null and b/tests/fixtures/s_dial.ttsmod differ diff --git a/tests/fixtures/s_hex.ttsmod b/tests/fixtures/s_hex.ttsmod new file mode 100644 index 0000000..0a5e30b Binary files /dev/null and b/tests/fixtures/s_hex.ttsmod differ diff --git a/tests/fixtures/sample_diceset.ttsmod b/tests/fixtures/sample_diceset.ttsmod new file mode 100644 index 0000000..a904ee3 Binary files /dev/null and b/tests/fixtures/sample_diceset.ttsmod differ diff --git a/tests/unit/archive-cachekey.test.ts b/tests/unit/archive-cachekey.test.ts index efa9006..440b94f 100644 --- a/tests/unit/archive-cachekey.test.ts +++ b/tests/unit/archive-cachekey.test.ts @@ -7,15 +7,19 @@ * 靠文件名命中本地缓存),错一个字符接收方就命中不了缓存。 * * 验证策略(全部用真实 URL 钉死): - * - 3 份第三方真实 `.ttsmod` 样本(**仓库不自带**,见下方"夹具获取")里的实测 - * 条目名作为金标准:`sanitize(url) + 扩展名` 必须与样本里的条目名 **逐字符相等**; + * - 3 份第三方真实 `.ttsmod` 样本里的实测条目名作为金标准: + * `sanitize(url) + 扩展名` 必须与样本里的条目名 **逐字符相等**; * - 直接读真实样本 ZIP,断言「已知 URL 算出的条目名」确实出现在样本条目里; * - 字符类全覆盖 + 幂等性 + 非法输入边界的纯函数行为。 * - * 夹具获取: - * - 设环境变量 `TTS_FIXTURE_DIR` 指向含 `s_dial.ttsmod` 等样本的目录;或 - * - 在 `/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';