import { test } from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { validate, compile, SpecError } from "../src/core/loader.js"; const example = JSON.parse(readFileSync(new URL("../example/game.json", import.meta.url), "utf8")); test("the shipped example validates clean", () => { const { errors, warnings } = validate(example); assert.deepEqual(errors, []); // exit symmetry: jetty<->tavern, so no symmetry warnings either assert.deepEqual(warnings, []); }); test("compile indexes everything", () => { const c = compile(example); assert.equal(c.rooms.tavern.id, "tavern"); assert.equal(c.objects.mug.name, "a suspicious mug"); assert.equal(c.characters.player.room, "jetty"); assert.equal(c.verbs.look.label, "Look"); assert.ok(c.interactions.every((it) => typeof it._order === "number")); }); test("every problem reported in one pass", () => { const bad = structuredClone(example); bad.meta = {}; // missing name bad.startRoom = "nowhere"; // dangling start bad.rooms[0].walkable = []; // no polygons bad.characters[0].id = "walker"; // player missing bad.interactions[0].verb = "telepathy"; // unknown verb bad.interactions[1].noun = "ghost-mug"; // unknown noun const { errors } = validate(bad); assert.ok(errors.length >= 6, `expected >=6 errors, got ${errors.length}: ${errors.join("|")}`); const joined = errors.join("\n"); for (const needle of ["meta.name", "startRoom", "walkable", "'player'", "telepathy", "ghost-mug"]) assert.ok(joined.includes(needle), `missing error about ${needle}: ${joined}`); }); test("compile throws SpecError listing everything", () => { assert.throws(() => compile({ meta: {} }), SpecError); try { compile({ meta: {} }); } catch (e) { assert.ok(e.errors.length >= 3); } }); test("dangling structural refs are errors, unknown flags are not checked", () => { const bad = structuredClone(example); bad.interactions.push({ verb: "use", noun: "mug", effects: [{ give: "nonexistent-item" }] }); bad.interactions.push({ verb: "look", noun: "post", effects: [{ playMusic: "nope-track" }] }); bad.interactions.push({ verb: "look", noun: "post", conditions: { flag: "createdAtRuntime" }, response: "ok" }); const { errors } = validate(bad); const joined = errors.join("\n"); assert.ok(joined.includes("nonexistent-item")); assert.ok(joined.includes("nope-track")); assert.ok(!joined.includes("createdAtRuntime")); }); test("dialogue nodes must connect", () => { const bad = structuredClone(example); bad.dialogues.barkeep_intro.nodes.greet.choices[0].to = "phantom"; const { errors } = validate(bad); assert.ok(errors.some((e) => e.includes("phantom"))); }); test("scene steps validated", () => { const bad = structuredClone(example); bad.scenes.intro = [{ explode: true }, { goto: "nowhere" }, { slide: "no-such-asset" }]; const { errors } = validate(bad); const joined = errors.join("\n"); assert.ok(joined.includes("explode")); assert.ok(joined.includes("nowhere")); assert.ok(joined.includes("no-such-asset")); }); test("duplicate ids caught", () => { const bad = structuredClone(example); bad.rooms.push({ ...bad.rooms[0] }); const { errors } = validate(bad); assert.ok(errors.some((e) => e.includes("duplicate id 'jetty'"))); }); test("missing return exit is a warning, not an error", () => { const bad = structuredClone(example); bad.rooms[1].exits = []; // tavern no longer leads back const { errors, warnings } = validate(bad); assert.deepEqual(errors, []); assert.ok(warnings.some((w) => w.includes("no return exit"))); });