47 lines
3.3 KiB
JavaScript
47 lines
3.3 KiB
JavaScript
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} 道首次折跃上游配方。`);
|