import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { buildGraph, cloneBook, makeIndex, traceIngredientClosure, validateBook } from "./core.mjs"; const book = JSON.parse(await readFile(new URL("../../data/recipe-book.json", import.meta.url), "utf8")); const schema = JSON.parse(await readFile(new URL("../../data/recipe-book.schema.json", import.meta.url), "utf8")); const techCsv = await readFile(new URL("../../docs/tech-tree-nodes-v0.2.csv", import.meta.url), "utf8"); const technologyIds = new Set(techCsv.split(/\r?\n/).slice(1).map((line) => line.split(",", 1)[0].trim()).filter(Boolean)); assert.equal(book.schemaVersion, 1, "配方书版本必须为 1"); assert.equal(schema.properties.schemaVersion.const, 1, "Schema 与配方书版本必须一致"); for (const required of schema.required) assert.ok(Object.hasOwn(book, required), `缺少顶层字段 ${required}`); const issues = validateBook(book, { technologyIds }); const errors = issues.filter((entry) => entry.severity === "error"); const warnings = issues.filter((entry) => entry.severity === "warning"); assert.deepEqual(errors, [], `存在校验错误:${errors.map((entry) => entry.message).join(";")}`); assert.deepEqual(warnings, [], `存在非预期警告:${warnings.map((entry) => entry.message).join(";")}`); const index = makeIndex(book); assert.ok(index.recipes.has("perform_first_warp"), "缺少首次折跃配方"); assert.ok(index.items.has("first_warp_record"), "缺少首次折跃记录"); assert.ok(index.recipes.has("gather_guiding_dew"), "生机循环缺少显式启动配方"); const firstWarp = traceIngredientClosure(book, "first_warp_record"); assert.deepEqual(firstWarp.missing, [], "首次折跃上游必须全部可追溯"); for (const requiredRecipe of ["perform_first_warp", "assemble_walker_construct", "harvest_life_proof", "recall_death_proof", "prove_element_cycle", "separate_dimension_proof"]) { assert.ok(firstWarp.recipes.includes(requiredRecipe), `首次折跃追溯缺少 ${requiredRecipe}`); } for (const recipe of book.recipes.filter((entry) => entry.kind === "result_bag")) { const weighted = recipe.outputs.filter((entry) => entry.weight != null); if (weighted.length) assert.equal(weighted.reduce((sum, entry) => sum + entry.weight, 0), 100, `${recipe.id} 权重不等于 100`); const chance = recipe.outputs.filter((entry) => entry.chance != null); if (chance.length === recipe.outputs.length) assert.ok(Math.abs(chance.reduce((sum, entry) => sum + entry.chance, 0) - 1) < 1e-9, `${recipe.id} 概率不等于 1`); } const focused = buildGraph(book, { mode: "focus", depth: 2, selection: { type: "recipes", id: "perform_first_warp" } }); assert.ok(focused.nodes.some((node) => node.key === "recipes:perform_first_warp"), "聚焦图缺少选中配方"); assert.ok(focused.edges.length >= 10, "首次折跃聚焦图边数异常"); const broken = cloneBook(book); broken.recipes[0].inputs.push({ itemId: "missing_test_item", amount: 1, mode: "consumed" }); assert.ok(validateBook(broken, { technologyIds }).some((entry) => entry.code === "missing_item" && entry.severity === "error"), "缺失引用必须被识别为错误"); console.log(`配方树测试通过:${book.items.length} 物品 / ${book.recipes.length} 配方 / ${book.machines.length} 设施 / ${firstWarp.recipes.length} 道首次折跃上游配方。`);