1089 lines
40 KiB
JavaScript
1089 lines
40 KiB
JavaScript
import { describe, it, expect, vi } from "vitest";
|
|
import { cleanText } from "../../src/textUtils.js";
|
|
import {
|
|
parseList,
|
|
parseObjects,
|
|
parseEncounterText,
|
|
splitCombinedEncounters,
|
|
parseRandomEventsRaw,
|
|
parseMainContentSections,
|
|
} from "../../src/parsing.js";
|
|
import {
|
|
validateContentCompleteness,
|
|
validateContentQuality,
|
|
validateContentStructure,
|
|
validateNarrativeCoherence,
|
|
extractCanonicalNames,
|
|
} from "../../src/validation.js";
|
|
import {
|
|
validateAndFixContent,
|
|
fixStructureIssues,
|
|
fixMissingContent,
|
|
fixNarrativeCoherence,
|
|
validateNameConsistency,
|
|
standardizeEncounterLocations,
|
|
} from "../../src/contentFixes.js";
|
|
import {
|
|
deduplicateRoomsByName,
|
|
padNpcsToMinimum,
|
|
buildEncountersList,
|
|
mergeRandomEventsWithFallbacks,
|
|
fixRoomPlaceholderName,
|
|
limitIntermediateRooms,
|
|
} from "../../src/dungeonBuild.js";
|
|
|
|
describe("cleanText", () => {
|
|
it("returns empty string for empty input", () => {
|
|
expect(cleanText("")).toBe("");
|
|
expect(cleanText(null)).toBe("");
|
|
});
|
|
|
|
it("strips markdown and normalizes spaces", () => {
|
|
expect(cleanText("# Head")).toBe("Head");
|
|
expect(cleanText("**bold**")).toBe("bold");
|
|
expect(cleanText(" a b ")).toBe("a b");
|
|
});
|
|
});
|
|
|
|
describe("parseRandomEventsRaw", () => {
|
|
it("parses Event Name: Description format", () => {
|
|
const raw = "1. Rift Shift: Time skips forward.";
|
|
const got = parseRandomEventsRaw(raw);
|
|
expect(got).toHaveLength(1);
|
|
expect(got[0].name).toBe("Rift Shift");
|
|
expect(got[0].description).toContain("Time skips");
|
|
});
|
|
|
|
it("returns null for placeholder name in colon format", () => {
|
|
const raw = "1. Event Name: Placeholder event";
|
|
const got = parseRandomEventsRaw(raw);
|
|
expect(got).toHaveLength(0);
|
|
});
|
|
|
|
it("returns null when name contains 'placeholder' (colon format)", () => {
|
|
const raw = "1. My placeholder idea: Something happens.";
|
|
const got = parseRandomEventsRaw(raw);
|
|
expect(got).toHaveLength(0);
|
|
});
|
|
|
|
it("uses first two words as name when no colon and more than 3 words", () => {
|
|
const raw = "1. Rift Shift Time Skips Forward";
|
|
const got = parseRandomEventsRaw(raw);
|
|
expect(got).toHaveLength(1);
|
|
expect(got[0].name).toBe("Rift Shift");
|
|
expect(got[0].description).toContain("Time Skips");
|
|
});
|
|
|
|
it("uses Event N and full text when no colon and 3 or fewer words", () => {
|
|
const raw = "1. One two three";
|
|
const got = parseRandomEventsRaw(raw);
|
|
expect(got).toHaveLength(1);
|
|
expect(got[0].name).toBe("Event 1");
|
|
expect(got[0].description).toBe("One two three");
|
|
});
|
|
|
|
it("filters out short and placeholder-like entries", () => {
|
|
const raw = "1. Short\n2. A random event occurs.\n3. Real Event: A real description here.";
|
|
const got = parseRandomEventsRaw(raw);
|
|
expect(got).toHaveLength(1);
|
|
expect(got[0].name).toBe("Real Event");
|
|
});
|
|
});
|
|
|
|
describe("parseMainContentSections", () => {
|
|
it("returns five sections when content has all labels", () => {
|
|
const raw = "Locations:\n1. Hall: A hall.\n\nEncounters:\n1. Goblin: Hall: Attacks.\n\nNPCs:\n1. Guard: Stern.\n\nTreasures:\n1. Gold — coins.\n\nRandom Events:\n1. Event: Desc.";
|
|
const got = parseMainContentSections(raw);
|
|
expect(got.intermediateRoomsSection).toContain("Locations");
|
|
expect(got.encountersSection).toContain("Goblin");
|
|
expect(got.npcsSection).toContain("Guard");
|
|
expect(got.treasureSection).toContain("Gold");
|
|
expect(got.randomEventsSection).toContain("Event");
|
|
});
|
|
|
|
it("fills random section via regex when initial split has no fourth segment", () => {
|
|
const raw = "Encounters:\n\nNPCs:\n\nTreasures:\n\nRandom Events:\n1. Rift: Time skips.";
|
|
const got = parseMainContentSections(raw);
|
|
expect(got.randomEventsSection).toContain("Rift");
|
|
});
|
|
|
|
it("returns initialSplit when random is in content but Random Events regex does not match", () => {
|
|
const raw = "Encounters:\n\nNPCs:\n\nTreasures:\n\nrandom stuff";
|
|
const got = parseMainContentSections(raw);
|
|
expect(got.randomEventsSection).toBeUndefined();
|
|
});
|
|
|
|
it("extracts encounters from first block when encounters section empty but block contains Encounter N", () => {
|
|
const raw = "Locations:\n1. Corridor: A corridor.\nEncounter 1 Goblin Room Name Hall Details In the hall.\n\nEncounters:\n\nNPCs:\n1. Captain: Leader.\n\nTreasures:\n\nRandom Events:\n";
|
|
const got = parseMainContentSections(raw);
|
|
expect(got.encountersSection).toMatch(/Goblin.*Hall/);
|
|
expect(got.intermediateRoomsSection).not.toMatch(/Encounter 1/);
|
|
});
|
|
|
|
it("returns encountersSection as-is when first block has no Encounter text", () => {
|
|
const raw = "Locations:\n1. Hall: A hall.\n\nEncounters:\n1. Goblin: Hall: Attacks.\n\nNPCs:\n1. Guard: Stern.";
|
|
const got = parseMainContentSections(raw);
|
|
expect(got.encountersSection).toContain("Goblin");
|
|
expect(got.intermediateRoomsSection).toContain("Hall");
|
|
});
|
|
|
|
it("returns early when inter includes Encounter but regex match is null", () => {
|
|
const raw = "Locations:\n1. Hall: A hall.\nEncounter foo\n\nEncounters:\n\nNPCs:\n1. Guard: Stern.\n\nTreasures:\n\nRandom Events:\n";
|
|
const got = parseMainContentSections(raw);
|
|
expect(got.encountersSection).toBe("");
|
|
expect(got.intermediateRoomsSection).toContain("Encounter foo");
|
|
});
|
|
|
|
it("uses npcMatch when third segment is empty but content has npc", () => {
|
|
const raw = "Encounters:\n\nNPCs:\n\nTreasures:\n1. Gold.\n\nRandom Events:\n";
|
|
const got = parseMainContentSections(raw);
|
|
expect(got.npcsSection).toBeDefined();
|
|
expect(got.treasureSection).toContain("Gold");
|
|
});
|
|
|
|
it("uses npcMatch when NPCs: immediately followed by Treasures: (empty segment)", () => {
|
|
const raw = "Encounters:\n\nNPCs:Treasures:\n1. Gold.\n\nRandom Events:\n";
|
|
const got = parseMainContentSections(raw);
|
|
expect(got.npcsSection).toBe("");
|
|
expect(got.treasureSection).toContain("Gold");
|
|
});
|
|
|
|
it("returns withRandom when npc in content but npcMatch regex does not match", () => {
|
|
const raw = "Encounters:\n\nenpcTreasures:\n\n";
|
|
const got = parseMainContentSections(raw);
|
|
expect(got.encountersSection).toBe("enpc");
|
|
expect(got.npcsSection).toBe("\n\n");
|
|
});
|
|
|
|
it("extracts encounter using simpleMatch when no Room Name/Location in block", () => {
|
|
const raw = "Locations:\n1. Corridor: A corridor.\nEncounter 1 Goblin Grand Hall Details In the hall.\n\nEncounters:\n\nNPCs:\n1. Captain: Leader.\n\nTreasures:\n\nRandom Events:\n";
|
|
const got = parseMainContentSections(raw);
|
|
expect(got.encountersSection).toMatch(/Goblin|Grand Hall/);
|
|
});
|
|
|
|
it("uses fallback format when encounter block matches neither match nor simpleMatch", () => {
|
|
const raw = "Locations:\n1. Corridor: A corridor.\nEncounter 1 No details format here.\n\nEncounters:\n\nNPCs:\n1. Captain: Leader.\n\nTreasures:\n\nRandom Events:\n";
|
|
const got = parseMainContentSections(raw);
|
|
expect(got.encountersSection).toMatch(/^1\./);
|
|
});
|
|
});
|
|
|
|
describe("deduplicateRoomsByName", () => {
|
|
it("returns empty for empty or null input", () => {
|
|
expect(deduplicateRoomsByName([])).toEqual([]);
|
|
expect(deduplicateRoomsByName(null)).toEqual([]);
|
|
});
|
|
|
|
it("keeps first occurrence when names duplicate (case-insensitive)", () => {
|
|
const rooms = [
|
|
{ name: "Hall", description: "First" },
|
|
{ name: "hall", description: "Second" },
|
|
{ name: "HALL", description: "Third" },
|
|
];
|
|
const got = deduplicateRoomsByName(rooms);
|
|
expect(got).toHaveLength(1);
|
|
expect(got[0].description).toBe("First");
|
|
});
|
|
|
|
it("filters out rooms with no name", () => {
|
|
const rooms = [
|
|
{ name: "Hall", description: "x" },
|
|
{ name: "", description: "y" },
|
|
{ name: null, description: "z" },
|
|
];
|
|
const got = deduplicateRoomsByName(rooms);
|
|
expect(got).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe("padNpcsToMinimum", () => {
|
|
it("returns input when length >= minCount", () => {
|
|
const npcs = [{ name: "A", trait: "x" }, { name: "B", trait: "y" }, { name: "C", trait: "z" }, { name: "D", trait: "w" }];
|
|
expect(padNpcsToMinimum(npcs, "Primary Faction: Guard.", 4)).toEqual(npcs);
|
|
});
|
|
|
|
it("pads to minCount using faction from coreConcepts", () => {
|
|
const npcs = [{ name: "Captain", trait: "Leader." }];
|
|
const got = padNpcsToMinimum(npcs, "Primary Faction: The Guard.", 4);
|
|
expect(got).toHaveLength(4);
|
|
expect(got[1].name).toBe("NPC 2");
|
|
expect(got[1].trait).toContain("guard");
|
|
});
|
|
|
|
it("returns empty array when parsedNpcs null and minCount 4", () => {
|
|
const got = padNpcsToMinimum(null, "Primary Faction: Guard.", 4);
|
|
expect(got).toEqual([]);
|
|
});
|
|
|
|
it("uses default faction text when coreConcepts has no Primary Faction", () => {
|
|
const npcs = [{ name: "A", trait: "x" }];
|
|
const got = padNpcsToMinimum(npcs, "Central Conflict: War.", 3);
|
|
expect(got).toHaveLength(3);
|
|
expect(got[1].trait).toContain("primary faction");
|
|
});
|
|
});
|
|
|
|
describe("buildEncountersList", () => {
|
|
const rooms = [{ name: "Hall", description: "x" }, { name: "Corridor", description: "y" }];
|
|
const coreConcepts = "Dynamic Element: Magic. Central Conflict: War.";
|
|
|
|
it("returns 6 new encounters when parsedEncounters is empty", () => {
|
|
const got = buildEncountersList([], rooms, coreConcepts);
|
|
expect(got).toHaveLength(6);
|
|
expect(got[0].name).toMatch(/Encounter$/);
|
|
expect(got[0].details).toContain("magic");
|
|
});
|
|
|
|
it("pads when 0 < length < 6", () => {
|
|
const parsed = [
|
|
{ name: "Patrol", details: "Hall: A patrol." },
|
|
{ name: "Ambush", details: "Corridor: Bandits." },
|
|
];
|
|
const got = buildEncountersList(parsed, rooms, coreConcepts);
|
|
expect(got).toHaveLength(6);
|
|
expect(got[0].name).toBe("Patrol");
|
|
expect(got[2].details).toContain("magic");
|
|
});
|
|
|
|
it("returns input when length >= 6", () => {
|
|
const parsed = Array.from({ length: 6 }, (_, i) => ({ name: `E${i + 1}`, details: `Hall: Details ${i}.` }));
|
|
const got = buildEncountersList(parsed, rooms, coreConcepts);
|
|
expect(got).toHaveLength(6);
|
|
expect(got[0].name).toBe("E1");
|
|
});
|
|
|
|
it("uses Unknown Location when rooms array is empty", () => {
|
|
const got = buildEncountersList([], [], coreConcepts);
|
|
expect(got).toHaveLength(6);
|
|
expect(got[0].name).toContain("Unknown Location");
|
|
});
|
|
|
|
it("uses default dynamicElement and conflict when coreConcepts is null", () => {
|
|
const got = buildEncountersList([], [{ name: "Hall" }], null);
|
|
expect(got).toHaveLength(6);
|
|
expect(got[0].details).toContain("strange");
|
|
});
|
|
});
|
|
|
|
describe("mergeRandomEventsWithFallbacks", () => {
|
|
const coreConcepts = "Central Conflict: War. Dynamic Element: Magic.";
|
|
|
|
it("returns empty when parsedEvents empty (no fill)", () => {
|
|
const got = mergeRandomEventsWithFallbacks([], coreConcepts, 6);
|
|
expect(got).toHaveLength(0);
|
|
});
|
|
|
|
it("merges when 0 < length < maxCount", () => {
|
|
const parsed = [
|
|
{ name: "Rift", description: "Time skips." },
|
|
{ name: "Patrol", description: "Guard approaches." },
|
|
];
|
|
const got = mergeRandomEventsWithFallbacks(parsed, coreConcepts, 6);
|
|
expect(got).toHaveLength(6);
|
|
expect(got[0].name).toBe("Rift");
|
|
expect(got[2].name).toBe("Dungeon Shift");
|
|
});
|
|
|
|
it("returns truncated list when length >= maxCount", () => {
|
|
const parsed = Array.from({ length: 6 }, (_, i) => ({ name: `E${i + 1}`, description: "x" }));
|
|
const got = mergeRandomEventsWithFallbacks(parsed, coreConcepts, 6);
|
|
expect(got).toHaveLength(6);
|
|
expect(got[0].name).toBe("E1");
|
|
});
|
|
|
|
it("uses default conflict when coreConcepts is null", () => {
|
|
const parsed = [{ name: "E1", description: "x" }];
|
|
const got = mergeRandomEventsWithFallbacks(parsed, null, 6);
|
|
expect(got).toHaveLength(6);
|
|
expect(got[1].name).toBe("Conflict Manifestation");
|
|
});
|
|
});
|
|
|
|
describe("limitIntermediateRooms", () => {
|
|
it("returns slice when length > maxCount and warns", () => {
|
|
const rooms = [
|
|
{ name: "A", description: "x" },
|
|
{ name: "B", description: "y" },
|
|
{ name: "C", description: "z" },
|
|
{ name: "D", description: "w" },
|
|
];
|
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
const got = limitIntermediateRooms(rooms, 3);
|
|
expect(got).toHaveLength(3);
|
|
expect(got[0].name).toBe("A");
|
|
expect(warn).toHaveBeenCalledWith(expect.stringContaining("Expected exactly 3 intermediate"));
|
|
warn.mockRestore();
|
|
});
|
|
|
|
it("returns all when length <= maxCount", () => {
|
|
const rooms = [{ name: "A", description: "x" }, { name: "B", description: "y" }];
|
|
const got = limitIntermediateRooms(rooms, 3);
|
|
expect(got).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe("fixRoomPlaceholderName", () => {
|
|
it("returns room unchanged when name is not placeholder", () => {
|
|
const room = { name: "Grand Hall", description: "A big hall." };
|
|
fixRoomPlaceholderName(room);
|
|
expect(room.name).toBe("Grand Hall");
|
|
});
|
|
|
|
it("extracts name from description when name is Room Name and description has Name: prefix", () => {
|
|
const room = { name: "Room Name", description: "Whispering Gallery: Dim light." };
|
|
fixRoomPlaceholderName(room);
|
|
expect(room.name).toBe("Whispering Gallery");
|
|
expect(room.description).toContain("Dim");
|
|
});
|
|
|
|
it("extracts name and updates description when first regex matches (colon)", () => {
|
|
const room = { name: "Room Name", description: "Grand Hall: Big space." };
|
|
fixRoomPlaceholderName(room);
|
|
expect(room.name).toBe("Grand Hall");
|
|
expect(room.description).toBe("Big space.");
|
|
});
|
|
|
|
it("extracts name when second regex matches (is)", () => {
|
|
const room = { name: "Room Name", description: "The Chamber is dark." };
|
|
fixRoomPlaceholderName(room);
|
|
expect(room.name).toBe("Chamber");
|
|
});
|
|
|
|
it("uses fallback first words when no nameMatch", () => {
|
|
const room = { name: "room name", description: "No match here at all" };
|
|
fixRoomPlaceholderName(room);
|
|
expect(room.name).toBe("No match here at");
|
|
});
|
|
|
|
it("returns null for null input", () => {
|
|
expect(fixRoomPlaceholderName(null)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("parseList", () => {
|
|
it("returns empty array for empty input", () => {
|
|
expect(parseList("")).toEqual([]);
|
|
expect(parseList(null)).toEqual([]);
|
|
});
|
|
|
|
it("parses single numbered item", () => {
|
|
expect(parseList("1. First item")).toEqual(["First item"]);
|
|
});
|
|
|
|
it("uses fallback split when regex does not match", () => {
|
|
const raw = "1) Only paren style";
|
|
const got = parseList(raw);
|
|
expect(got.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it("parses multiple numbered items", () => {
|
|
const raw = "1. One\n2. Two\n3. Three";
|
|
expect(parseList(raw)).toEqual(["One", "Two", "Three"]);
|
|
});
|
|
|
|
it("handles fallback format", () => {
|
|
const raw = "1) Item A";
|
|
expect(parseList(raw).length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
describe("parseObjects", () => {
|
|
it("parses rooms with name and description", () => {
|
|
const raw = "1. Room Name - A dark room.";
|
|
const got = parseObjects(raw, "rooms");
|
|
expect(got).toHaveLength(1);
|
|
expect(got[0].name).toBe("Room Name");
|
|
expect(got[0].description).toContain("dark");
|
|
});
|
|
|
|
it("filters placeholder names for rooms", () => {
|
|
const raw = "1. Location Name - desc";
|
|
expect(parseObjects(raw, "rooms")).toHaveLength(0);
|
|
});
|
|
|
|
it("parses encounters with name and details", () => {
|
|
const raw = "1. Goblin: Hall: Attacks.";
|
|
const got = parseObjects(raw, "encounters");
|
|
expect(got).toHaveLength(1);
|
|
expect(got[0].name).toBe("Goblin");
|
|
expect(got[0].details).toContain("Hall");
|
|
});
|
|
|
|
it("parses treasure with em-dash", () => {
|
|
const raw = "1. Gold — Shiny coins.";
|
|
const got = parseObjects(raw, "treasure");
|
|
expect(got).toHaveLength(1);
|
|
expect(got[0].name).toBe("Gold");
|
|
expect(got[0].description).toContain("Shiny");
|
|
});
|
|
|
|
it("parses treasure with hyphen (generic path)", () => {
|
|
const raw = "1. Silver - A pile of silver.";
|
|
const got = parseObjects(raw, "treasure");
|
|
expect(got).toHaveLength(1);
|
|
expect(got[0].name).toBe("Silver");
|
|
expect(got[0].description).toContain("pile");
|
|
});
|
|
|
|
it("parses npcs with trait", () => {
|
|
const raw = "1. Guard: A stern guard.";
|
|
const got = parseObjects(raw, "npcs");
|
|
expect(got).toHaveLength(1);
|
|
expect(got[0].name).toBe("Guard");
|
|
expect(got[0].trait).toContain("stern");
|
|
});
|
|
|
|
it("filters encounter placeholder names (Location Name / Encounter Name)", () => {
|
|
const rawWithPlaceholder = "1. Location Name: Hall: Some details here for the encounter.";
|
|
const got = parseObjects(rawWithPlaceholder, "encounters");
|
|
expect(got).toHaveLength(0);
|
|
const rawEncName = "1. Encounter Name: Room: Details.";
|
|
expect(parseObjects(rawEncName, "encounters")).toHaveLength(0);
|
|
});
|
|
|
|
it("parses encounter with two-part format (Name: details)", () => {
|
|
const raw = "1. Goblin: Attacks in the hall.";
|
|
const got = parseObjects(raw, "encounters");
|
|
expect(got).toHaveLength(1);
|
|
expect(got[0].name).toBe("Goblin");
|
|
expect(got[0].details).toContain("Attacks");
|
|
});
|
|
|
|
it("filters out encounter with only one part (no colon)", () => {
|
|
const raw = "1. OnlyOnePart";
|
|
const got = parseObjects(raw, "encounters");
|
|
expect(got).toHaveLength(0);
|
|
});
|
|
|
|
it("filters treasure with placeholder name", () => {
|
|
const raw = "1. Treasure Name — A shiny thing.";
|
|
expect(parseObjects(raw, "treasure")).toHaveLength(0);
|
|
});
|
|
|
|
it("filters room with NPC Name placeholder", () => {
|
|
const raw = "1. NPC Name - A person.";
|
|
expect(parseObjects(raw, "rooms")).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("parseEncounterText", () => {
|
|
it("parses Encounter N Name Room Name Details format", () => {
|
|
const text = "Encounter 1 Goblin Room Name Hall Details In the hall.";
|
|
const got = parseEncounterText(text, 0);
|
|
expect(got).not.toBeNull();
|
|
expect(got.name).toBe("Goblin");
|
|
expect(got.details).toContain("Hall");
|
|
});
|
|
|
|
it("parses Encounter N Name: Location: Details format", () => {
|
|
const got = parseEncounterText("Encounter 1 Boss: Grand Hall: The boss waits here.", 0);
|
|
expect(got).not.toBeNull();
|
|
expect(got.name).toBe("Boss");
|
|
expect(got.details).toMatch(/Grand Hall.*boss/);
|
|
});
|
|
|
|
it("parses simple N Name: details format", () => {
|
|
const got = parseEncounterText("1. Boss: The boss appears.", 0);
|
|
expect(got).not.toBeNull();
|
|
expect(got.name).toContain("Boss");
|
|
expect(got.details).toContain("appears");
|
|
});
|
|
|
|
it("parses via colonSplit when text has colon but no leading number format", () => {
|
|
const got = parseEncounterText("Goblin: Hall: Attacks here.", 0);
|
|
expect(got).not.toBeNull();
|
|
expect(got.name).toBe("Goblin");
|
|
expect(got.details).toContain("Hall");
|
|
});
|
|
|
|
it("returns fallback name and trimmed details when no other format matches", () => {
|
|
const got = parseEncounterText("1. xyz short", 2);
|
|
expect(got.name).toBe("Encounter 3");
|
|
expect(got.details).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("splitCombinedEncounters", () => {
|
|
it("returns empty for empty array", () => {
|
|
expect(splitCombinedEncounters([])).toEqual([]);
|
|
});
|
|
|
|
it("returns same array when not combined", () => {
|
|
const list = [{ name: "E1", details: "Short." }];
|
|
expect(splitCombinedEncounters(list)).toEqual(list);
|
|
});
|
|
|
|
it("returns encounter unchanged when details are missing", () => {
|
|
const encounters = [{ name: "E1" }];
|
|
const rooms = [{ name: "Hall" }];
|
|
const { encounters: out } = standardizeEncounterLocations(encounters, rooms);
|
|
expect(out[0]).toEqual(encounters[0]);
|
|
});
|
|
|
|
it("splits when single encounter has combined text", () => {
|
|
const combined = "Encounter 1 A Hall Details First. Encounter 2 B Room Details Second.";
|
|
const list = [{ name: "1", details: combined }];
|
|
const got = splitCombinedEncounters(list);
|
|
expect(got.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it("splits when single encounter details contain number and capital (combined)", () => {
|
|
const list = [{ name: "E1", details: "Hall: First part. 2 Second Encounter in room." }];
|
|
const got = splitCombinedEncounters(list);
|
|
expect(got.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
describe("standardizeEncounterLocations", () => {
|
|
it("returns input when encounters or rooms missing", () => {
|
|
const enc = [{ name: "E1", details: "x" }];
|
|
expect(standardizeEncounterLocations(null, [])).toEqual({ encounters: null, fixes: [] });
|
|
expect(standardizeEncounterLocations(enc, null)).toEqual({ encounters: enc, fixes: [] });
|
|
});
|
|
|
|
it("adds colon after room name when missing", () => {
|
|
const encounters = [{ name: "E1", details: "Hall something" }];
|
|
const rooms = [{ name: "Hall" }];
|
|
const { encounters: out, fixes } = standardizeEncounterLocations(encounters, rooms);
|
|
expect(out[0].details).toMatch(/Hall:\s/);
|
|
expect(fixes.length).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it("leaves details unchanged when location already has colon", () => {
|
|
const encounters = [{ name: "E1", details: "Hall: already has colon." }];
|
|
const rooms = [{ name: "Hall" }];
|
|
const { encounters: out, fixes } = standardizeEncounterLocations(encounters, rooms);
|
|
expect(out[0].details).toBe("Hall: already has colon.");
|
|
expect(fixes).toHaveLength(0);
|
|
});
|
|
|
|
it("leaves details unchanged when no room match", () => {
|
|
const encounters = [{ name: "E1", details: "Unknown: text" }];
|
|
const rooms = [{ name: "Hall" }];
|
|
const { encounters: out } = standardizeEncounterLocations(encounters, rooms);
|
|
expect(out[0].details).toBe("Unknown: text");
|
|
});
|
|
|
|
it("applies standardization when second room name matches details prefix", () => {
|
|
const encounters = [{ name: "E1", details: "Corridor something" }];
|
|
const rooms = [{ name: "Hall" }, { name: "Corridor" }];
|
|
const { encounters: out } = standardizeEncounterLocations(encounters, rooms);
|
|
expect(out[0].details).toMatch(/Corridor:\s/);
|
|
});
|
|
});
|
|
|
|
describe("validateContentCompleteness", () => {
|
|
it("reports missing title", () => {
|
|
const issues = validateContentCompleteness({ flavor: "x".repeat(30) });
|
|
expect(issues.some((i) => i.toLowerCase().includes("title"))).toBe(true);
|
|
});
|
|
|
|
it("reports short flavor", () => {
|
|
const issues = validateContentCompleteness({ title: "X", flavor: "short" });
|
|
expect(issues.some((i) => i.toLowerCase().includes("flavor"))).toBe(true);
|
|
});
|
|
|
|
it("reports few hooks", () => {
|
|
const issues = validateContentCompleteness({
|
|
title: "X",
|
|
flavor: "a ".repeat(15),
|
|
hooksRumors: ["one"],
|
|
});
|
|
expect(issues.some((i) => i.toLowerCase().includes("hook"))).toBe(true);
|
|
});
|
|
|
|
it("reports when plotResolutions array is too short", () => {
|
|
const issues = validateContentCompleteness({
|
|
title: "T",
|
|
flavor: "a ".repeat(15),
|
|
hooksRumors: ["a", "b", "c", "d"],
|
|
rooms: [],
|
|
encounters: [],
|
|
npcs: [],
|
|
plotResolutions: ["one"],
|
|
});
|
|
expect(issues.some((i) => i.toLowerCase().includes("plot") || i.includes("resolution"))).toBe(true);
|
|
});
|
|
|
|
it("returns empty for valid full data", () => {
|
|
const roomDesc = "A room with enough description length here.";
|
|
const data = {
|
|
title: "Test",
|
|
flavor: "A ".repeat(15),
|
|
hooksRumors: ["a", "b", "c", "d"],
|
|
rooms: Array.from({ length: 5 }, (_, i) => ({ name: `R${i + 1}`, description: roomDesc })),
|
|
encounters: Array.from({ length: 6 }, (_, i) => ({ name: `E${i + 1}`, details: `R1: Encounter details long enough.` })),
|
|
npcs: Array.from({ length: 4 }, (_, i) => ({ name: `N${i + 1}`, trait: "Trait with enough length for validation." })),
|
|
treasure: Array.from({ length: 4 }, (_, i) => ({ name: `T${i + 1}`, description: "x" })),
|
|
randomEvents: Array.from({ length: 6 }, (_, i) => ({ name: `Ev${i + 1}`, description: "y" })),
|
|
plotResolutions: ["p1", "p2", "p3", "p4"],
|
|
};
|
|
const issues = validateContentCompleteness(data);
|
|
expect(issues.length).toBe(0);
|
|
});
|
|
|
|
it("does not push room description issues when rooms is undefined", () => {
|
|
const issues = validateContentCompleteness({
|
|
title: "T",
|
|
flavor: "A ".repeat(15),
|
|
hooksRumors: ["a", "b", "c", "d"],
|
|
});
|
|
expect(issues.some((i) => i.includes("Room") && i.includes("description"))).toBe(false);
|
|
});
|
|
|
|
it("reports room description too short", () => {
|
|
const data = {
|
|
title: "T",
|
|
flavor: "A ".repeat(15),
|
|
hooksRumors: ["a", "b", "c", "d"],
|
|
rooms: [{ name: "R1", description: "Short." }],
|
|
encounters: [],
|
|
npcs: [],
|
|
};
|
|
const issues = validateContentCompleteness(data);
|
|
expect(issues.some((i) => i.includes("description too short"))).toBe(true);
|
|
});
|
|
|
|
it("reports encounter details too short", () => {
|
|
const data = {
|
|
title: "T",
|
|
flavor: "A ".repeat(15),
|
|
hooksRumors: ["a", "b", "c", "d"],
|
|
rooms: [],
|
|
encounters: [{ name: "E1", details: "Short." }],
|
|
npcs: [],
|
|
};
|
|
const issues = validateContentCompleteness(data);
|
|
expect(issues.some((i) => i.includes("details too short"))).toBe(true);
|
|
});
|
|
|
|
it("reports NPC trait too short", () => {
|
|
const data = {
|
|
title: "T",
|
|
flavor: "A ".repeat(15),
|
|
hooksRumors: ["a", "b", "c", "d"],
|
|
rooms: [],
|
|
encounters: [],
|
|
npcs: [{ name: "N1", trait: "Short." }],
|
|
};
|
|
const issues = validateContentCompleteness(data);
|
|
expect(issues.some((i) => i.includes("description too short"))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("validateContentQuality", () => {
|
|
it("reports vague language when too many vague words", () => {
|
|
const data = {
|
|
flavor: "Some various several things stuff items here.",
|
|
};
|
|
const issues = validateContentQuality(data);
|
|
expect(issues.some((i) => i.includes("vague language"))).toBe(true);
|
|
});
|
|
|
|
it("reports short room description", () => {
|
|
const data = {
|
|
rooms: [{ name: "R1", description: "Short." }],
|
|
};
|
|
const issues = validateContentQuality(data);
|
|
expect(issues.some((i) => i.includes("too short"))).toBe(true);
|
|
});
|
|
|
|
it("skips vague check when text is missing", () => {
|
|
const data = { rooms: [{ name: "R1" }] };
|
|
const issues = validateContentQuality(data);
|
|
expect(issues.filter((i) => i.includes("vague"))).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("validateContentStructure", () => {
|
|
it("reports missing room name", () => {
|
|
const data = { rooms: [{ name: "", description: "x" }] };
|
|
const issues = validateContentStructure(data);
|
|
expect(issues.some((i) => i.includes("Room") && i.includes("name"))).toBe(true);
|
|
});
|
|
|
|
it("reports room name too long", () => {
|
|
const data = {
|
|
rooms: [{ name: "One Two Three Four Five Six Seven", description: "A room." }],
|
|
};
|
|
const issues = validateContentStructure(data);
|
|
expect(issues.some((i) => i.includes("name too long"))).toBe(true);
|
|
});
|
|
|
|
it("reports encounter missing location prefix", () => {
|
|
const data = {
|
|
encounters: [{ name: "E1", details: "no colon prefix" }],
|
|
};
|
|
const issues = validateContentStructure(data);
|
|
expect(issues.some((i) => i.includes("location"))).toBe(true);
|
|
});
|
|
|
|
it("reports encounter name too long", () => {
|
|
const data = {
|
|
encounters: [{ name: "A B C D E F G", details: "Hall: details" }],
|
|
};
|
|
const issues = validateContentStructure(data);
|
|
expect(issues.some((i) => i.includes("name too long"))).toBe(true);
|
|
});
|
|
|
|
it("reports no location-prefix issue when details have Location: prefix", () => {
|
|
const data = {
|
|
encounters: [{ name: "E1", details: "Hall: proper prefix." }],
|
|
};
|
|
const issues = validateContentStructure(data);
|
|
expect(issues.some((i) => i.includes("location"))).toBe(false);
|
|
});
|
|
|
|
it("reports NPC name too long", () => {
|
|
const data = {
|
|
npcs: [{ name: "Alice Bob Carol Dave Eve", trait: "Long trait here." }],
|
|
};
|
|
const issues = validateContentStructure(data);
|
|
expect(issues.some((i) => i.includes("name too long"))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("validateNarrativeCoherence", () => {
|
|
it("reports unknown location in encounter details", () => {
|
|
const data = {
|
|
rooms: [{ name: "Hall" }],
|
|
encounters: [{ name: "E1", details: "UnknownPlace: text" }],
|
|
};
|
|
const issues = validateNarrativeCoherence(data);
|
|
expect(issues.some((i) => i.includes("unknown location"))).toBe(true);
|
|
});
|
|
|
|
it("reports poorly integrated faction when few refs", () => {
|
|
const data = {
|
|
coreConcepts: "Primary Faction: The Guard.",
|
|
npcs: [{ name: "N1", trait: "unrelated" }],
|
|
encounters: [{ name: "E1", details: "Hall: no faction" }],
|
|
};
|
|
const issues = validateNarrativeCoherence(data);
|
|
expect(issues.some((i) => i.includes("Faction"))).toBe(true);
|
|
});
|
|
|
|
it("reports no issue when faction has enough refs", () => {
|
|
const data = {
|
|
coreConcepts: "Primary Faction: The Guard.",
|
|
rooms: [{ name: "Hall" }],
|
|
npcs: [{ name: "N1", trait: "Member of the Guard." }],
|
|
encounters: [
|
|
{ name: "E1", details: "Hall: The Guard patrols here." },
|
|
{ name: "E2", details: "Hall: Guard reinforcements." },
|
|
],
|
|
};
|
|
const issues = validateNarrativeCoherence(data);
|
|
expect(issues.filter((i) => i.includes("Faction"))).toHaveLength(0);
|
|
});
|
|
|
|
it("reports no issue when coreConcepts has no Primary Faction", () => {
|
|
const data = {
|
|
coreConcepts: "Central Conflict: War.",
|
|
rooms: [{ name: "Hall" }],
|
|
encounters: [{ name: "E1", details: "Hall: text" }],
|
|
};
|
|
const issues = validateNarrativeCoherence(data);
|
|
expect(issues).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("extractCanonicalNames", () => {
|
|
it("extracts npc and room names", () => {
|
|
const data = {
|
|
npcs: [{ name: "Alice" }],
|
|
rooms: [{ name: "Hall" }],
|
|
coreConcepts: "Primary Faction: The Guard.",
|
|
};
|
|
const names = extractCanonicalNames(data);
|
|
expect(names.npcs).toContain("Alice");
|
|
expect(names.rooms).toContain("Hall");
|
|
expect(names.factions).toContain("The Guard");
|
|
});
|
|
|
|
it("returns empty factions when coreConcepts has no Primary Faction", () => {
|
|
const data = { coreConcepts: "Central Conflict: War. Dynamic Element: Magic." };
|
|
const names = extractCanonicalNames(data);
|
|
expect(names.factions).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("validateNameConsistency", () => {
|
|
it("fixes NPC name in flavor and hooks when inconsistent", () => {
|
|
const data = {
|
|
flavor: "The alice is here.",
|
|
hooksRumors: ["alice said something."],
|
|
encounters: [{ name: "E1", details: "Hall: alice appears." }],
|
|
plotResolutions: ["alice wins."],
|
|
npcs: [{ name: "Alice" }],
|
|
rooms: [{ name: "Hall" }],
|
|
};
|
|
const fixes = validateNameConsistency(data);
|
|
expect(data.flavor).toContain("Alice");
|
|
expect(fixes.some((f) => f.includes("Flavor") || f.includes("flavor"))).toBe(true);
|
|
});
|
|
|
|
it("fixes room name in encounter details when inconsistent", () => {
|
|
const data = {
|
|
encounters: [{ name: "E1", details: "main hall: something." }],
|
|
rooms: [{ name: "Main Hall", description: "x" }],
|
|
};
|
|
const fixes = validateNameConsistency(data);
|
|
expect(data.encounters[0].details).toContain("Main Hall");
|
|
expect(fixes.some((f) => f.includes("room") || f.includes("Room"))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("fixStructureIssues", () => {
|
|
it("adds default room name when description has no extractable name", () => {
|
|
const data = {
|
|
rooms: [{ name: "", description: "x" }],
|
|
};
|
|
const fixes = fixStructureIssues(data);
|
|
expect(data.rooms[0].name).toBe("Room 1");
|
|
expect(fixes.some((f) => f.includes("Added default name for room"))).toBe(true);
|
|
});
|
|
|
|
it("adds default encounter name when details have no colon", () => {
|
|
const data = {
|
|
encounters: [{ name: "", details: "no colon here" }],
|
|
};
|
|
const fixes = fixStructureIssues(data);
|
|
expect(data.encounters[0].name).toBe("Encounter 1");
|
|
expect(fixes.some((f) => f.includes("Added default name for encounter"))).toBe(true);
|
|
});
|
|
|
|
it("extracts encounter name from details when details have Name: Rest", () => {
|
|
const data = {
|
|
encounters: [{ name: "", details: "Goblin King: Attacks from the shadows." }],
|
|
};
|
|
const fixes = fixStructureIssues(data);
|
|
expect(data.encounters[0].name).toBe("Goblin King");
|
|
expect(data.encounters[0].details).toContain("Attacks");
|
|
expect(fixes.some((f) => f.includes("Extracted encounter name"))).toBe(true);
|
|
});
|
|
|
|
it("adds default NPC name when trait has no extractable name", () => {
|
|
const data = {
|
|
npcs: [{ name: "", trait: "no capital word at start" }],
|
|
};
|
|
const fixes = fixStructureIssues(data);
|
|
expect(data.npcs[0].name).toBe("NPC 1");
|
|
expect(fixes.some((f) => f.includes("Added default name for NPC"))).toBe(true);
|
|
});
|
|
|
|
it("extracts NPC name from trait when trait starts with Capital Name", () => {
|
|
const data = {
|
|
npcs: [{ name: "", trait: "Captain Smith: leads the guard." }],
|
|
};
|
|
const fixes = fixStructureIssues(data);
|
|
expect(data.npcs[0].name).toBe("Captain Smith");
|
|
expect(fixes.some((f) => f.includes("Extracted NPC name"))).toBe(true);
|
|
});
|
|
|
|
it("truncates room name over 6 words", () => {
|
|
const data = {
|
|
rooms: [{ name: "A B C D E F G H", description: "Room." }],
|
|
};
|
|
const fixes = fixStructureIssues(data);
|
|
expect(data.rooms[0].name).toBe("A B C D E F");
|
|
expect(fixes.some((f) => f.includes("Truncated room name"))).toBe(true);
|
|
});
|
|
|
|
it("truncates encounter name over 6 words", () => {
|
|
const data = {
|
|
encounters: [{ name: "A B C D E F G", details: "Hall: x" }],
|
|
};
|
|
const fixes = fixStructureIssues(data);
|
|
expect(data.encounters[0].name).toBe("A B C D E F");
|
|
expect(fixes.some((f) => f.includes("Truncated encounter name"))).toBe(true);
|
|
});
|
|
|
|
it("truncates NPC name over 4 words", () => {
|
|
const data = {
|
|
npcs: [{ name: "A B C D E", trait: "Trait." }],
|
|
};
|
|
const fixes = fixStructureIssues(data);
|
|
expect(data.npcs[0].name).toBe("A B C D");
|
|
expect(fixes.some((f) => f.includes("Truncated NPC name"))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("fixMissingContent", () => {
|
|
it("pads encounters using rooms when encounters exist but under 6", () => {
|
|
const data = {
|
|
rooms: [{ name: "Hall", description: "A hall." }],
|
|
encounters: [{ name: "E1", details: "Hall: first." }],
|
|
coreConcepts: "Dynamic Element: Magic. Central Conflict: War.",
|
|
};
|
|
const fixes = fixMissingContent(data);
|
|
expect(data.encounters.length).toBe(6);
|
|
expect(fixes.some((f) => f.includes("fallback") || f.includes("Added"))).toBe(true);
|
|
});
|
|
|
|
it("pads NPCs to 4 when missing", () => {
|
|
const data = { npcs: [], coreConcepts: "Primary Faction: Guard." };
|
|
const fixes = fixMissingContent(data);
|
|
expect(data.npcs.length).toBe(4);
|
|
expect(fixes.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("pads treasure to 4 when missing", () => {
|
|
const data = { treasure: [] };
|
|
const fixes = fixMissingContent(data);
|
|
expect(data.treasure.length).toBe(4);
|
|
expect(fixes.some((f) => f.includes("treasure"))).toBe(true);
|
|
});
|
|
|
|
it("pads random events to 6 when some exist and coreConcepts set", () => {
|
|
const data = {
|
|
randomEvents: [{ name: "E1", description: "One." }],
|
|
coreConcepts: "Dynamic Element: Magic. Central Conflict: War.",
|
|
};
|
|
const fixes = fixMissingContent(data);
|
|
expect(data.randomEvents.length).toBe(6);
|
|
expect(fixes.some((f) => f.includes("random event"))).toBe(true);
|
|
});
|
|
|
|
it("pads plot resolutions to 4 when missing", () => {
|
|
const data = { plotResolutions: [] };
|
|
const fixes = fixMissingContent(data);
|
|
expect(data.plotResolutions.length).toBe(4);
|
|
expect(fixes.some((f) => f.includes("plot resolution"))).toBe(true);
|
|
});
|
|
|
|
it("does not add random event fallbacks when randomEvents is empty", () => {
|
|
const data = { randomEvents: [], coreConcepts: "Dynamic Element: Magic." };
|
|
fixMissingContent(data);
|
|
expect(data.randomEvents.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("fixNarrativeCoherence", () => {
|
|
it("fixes encounter with unknown location by assigning a room", () => {
|
|
const data = {
|
|
rooms: [{ name: "Hall", description: "A hall." }],
|
|
encounters: [{ name: "E1", details: "UnknownPlace: something happens." }],
|
|
};
|
|
const fixes = fixNarrativeCoherence(data);
|
|
expect(data.encounters[0].details).toMatch(/^Hall:\s/);
|
|
expect(fixes.some((f) => f.includes("Fixed unknown location"))).toBe(true);
|
|
});
|
|
|
|
it("leaves encounter unchanged when location matches a room name", () => {
|
|
const data = {
|
|
rooms: [{ name: "Hall", description: "A hall." }],
|
|
encounters: [{ name: "E1", details: "Hall: something." }],
|
|
};
|
|
const fixes = fixNarrativeCoherence(data);
|
|
expect(data.encounters[0].details).toBe("Hall: something.");
|
|
expect(fixes).toHaveLength(0);
|
|
});
|
|
|
|
it("does not assign room when rooms array is empty", () => {
|
|
const data = {
|
|
rooms: [],
|
|
encounters: [{ name: "E1", details: "Unknown: text." }],
|
|
};
|
|
const fixes = fixNarrativeCoherence(data);
|
|
expect(data.encounters[0].details).toBe("Unknown: text.");
|
|
expect(fixes).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("validateAndFixContent", () => {
|
|
it("applies fixes and returns dungeonData", () => {
|
|
const dungeonData = {
|
|
title: "Test",
|
|
flavor: "A ".repeat(20),
|
|
hooksRumors: ["a", "b", "c", "d"],
|
|
rooms: [
|
|
{ name: "", description: "Grand Hall. A big room." },
|
|
{ name: "Room2", description: "Second." },
|
|
],
|
|
encounters: [
|
|
{ name: "", details: "Something happens." },
|
|
],
|
|
npcs: [],
|
|
treasure: [],
|
|
randomEvents: [],
|
|
plotResolutions: ["One."],
|
|
coreConcepts: "Primary Faction: Guard. Dynamic Element: Magic.",
|
|
};
|
|
const result = validateAndFixContent(dungeonData);
|
|
expect(result).toBeDefined();
|
|
expect(result.title).toBe("Test");
|
|
expect(dungeonData.rooms[0].name).toBeTruthy();
|
|
expect(dungeonData.encounters[0].name).toBeTruthy();
|
|
});
|
|
|
|
it("adds location to encounter when details lack location prefix", () => {
|
|
const dungeonData = {
|
|
title: "Test",
|
|
flavor: "A ".repeat(20),
|
|
hooksRumors: ["a", "b", "c", "d"],
|
|
rooms: [{ name: "Hall", description: "A hall." }],
|
|
encounters: [{ name: "E1", details: "no colon prefix" }],
|
|
npcs: [],
|
|
coreConcepts: "Primary Faction: Guard.",
|
|
};
|
|
const result = validateAndFixContent(dungeonData);
|
|
expect(dungeonData.encounters[0].details).toMatch(/^Hall:\s/);
|
|
expect(result).toBeDefined();
|
|
});
|
|
|
|
it("skips encounter when details are missing", () => {
|
|
const dungeonData = {
|
|
title: "Test",
|
|
flavor: "A ".repeat(20),
|
|
hooksRumors: ["a", "b", "c", "d"],
|
|
rooms: [{ name: "Hall", description: "A hall." }],
|
|
encounters: [{ name: "E1" }],
|
|
npcs: [],
|
|
coreConcepts: "Primary Faction: Guard.",
|
|
};
|
|
const result = validateAndFixContent(dungeonData);
|
|
expect(result.encounters[0].details).toBeUndefined();
|
|
});
|
|
|
|
it("logs Content quality checks passed when no issues remain", () => {
|
|
const longDesc = "A room with enough descriptive text to pass length checks here.";
|
|
const longDetails = "Hall: Encounter details that are long enough for validation.";
|
|
const longTrait = "An NPC with a trait long enough to pass validation here.";
|
|
const dungeonData = {
|
|
title: "Valid Dungeon",
|
|
flavor: "A dungeon with enough flavor text to pass the minimum length.",
|
|
hooksRumors: ["a", "b", "c", "d"],
|
|
rooms: [
|
|
{ name: "Hall", description: longDesc },
|
|
{ name: "Room2", description: longDesc },
|
|
{ name: "Room3", description: longDesc },
|
|
{ name: "Room4", description: longDesc },
|
|
{ name: "Room5", description: longDesc },
|
|
],
|
|
encounters: [
|
|
{ name: "E1", details: longDetails },
|
|
{ name: "E2", details: longDetails },
|
|
{ name: "E3", details: longDetails },
|
|
{ name: "E4", details: longDetails },
|
|
{ name: "E5", details: longDetails },
|
|
{ name: "E6", details: longDetails },
|
|
],
|
|
npcs: [
|
|
{ name: "Guard", trait: longTrait + " Guard faction." },
|
|
{ name: "Captain", trait: longTrait + " Guard faction." },
|
|
{ name: "N3", trait: longTrait },
|
|
{ name: "N4", trait: longTrait },
|
|
],
|
|
treasure: [{ name: "T1", description: "d" }, { name: "T2", description: "d" }, { name: "T3", description: "d" }, { name: "T4", description: "d" }],
|
|
randomEvents: Array(6).fill({ name: "Event", description: "Desc" }),
|
|
plotResolutions: ["P1", "P2", "P3", "P4"],
|
|
coreConcepts: "Primary Faction: Guard. Dynamic Element: Magic.",
|
|
};
|
|
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
validateAndFixContent(dungeonData);
|
|
expect(logSpy).toHaveBeenCalledWith("\n[Validation] Content quality checks passed");
|
|
logSpy.mockRestore();
|
|
});
|
|
});
|