D&D creatures listed in data/bestiary/dnd-bundled.json are now merged into the search index and pre-loaded into creatureMap, so they appear alongside 5etools creatures with no "Load source" step. Source codes are derived from the JSON itself (each creature carries source + sourceDisplayName), so adding a new book is a pure data change. Bundled sources are excluded from getAllSourceCodes() so bulk-import skips them, and they never appear in the source manager (which only lists cached sources). Includes a reference extractor (scripts/extract-great-labors.py) for the 5.5e revised stat-block format and a /bundle-bestiary skill that future agents can follow to add monsters from other PDF books. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
getAllSourceCodes,
|
|
getDefaultFetchUrl,
|
|
getSourceDisplayName,
|
|
loadBestiaryIndex,
|
|
} from "../bestiary-index-adapter.js";
|
|
|
|
describe("loadBestiaryIndex", () => {
|
|
it("returns an object with sources and creatures", () => {
|
|
const index = loadBestiaryIndex();
|
|
expect(index.sources).toBeDefined();
|
|
expect(index.creatures).toBeDefined();
|
|
expect(Array.isArray(index.creatures)).toBe(true);
|
|
});
|
|
|
|
it("creatures have the expected shape", () => {
|
|
const index = loadBestiaryIndex();
|
|
expect(index.creatures.length).toBeGreaterThan(0);
|
|
const first = index.creatures[0];
|
|
expect(first).toHaveProperty("name");
|
|
expect(first).toHaveProperty("source");
|
|
expect(first).toHaveProperty("ac");
|
|
expect(first).toHaveProperty("hp");
|
|
expect(first).toHaveProperty("dex");
|
|
expect(first).toHaveProperty("cr");
|
|
expect(first).toHaveProperty("initiativeProficiency");
|
|
expect(first).toHaveProperty("size");
|
|
expect(first).toHaveProperty("type");
|
|
});
|
|
|
|
it("returns the same cached instance on subsequent calls", () => {
|
|
const a = loadBestiaryIndex();
|
|
const b = loadBestiaryIndex();
|
|
expect(a).toBe(b);
|
|
});
|
|
|
|
it("sources is a record of source code to display name", () => {
|
|
const index = loadBestiaryIndex();
|
|
const entries = Object.entries(index.sources);
|
|
expect(entries.length).toBeGreaterThan(0);
|
|
for (const [code, name] of entries) {
|
|
expect(typeof code).toBe("string");
|
|
expect(typeof name).toBe("string");
|
|
expect(code.length).toBeGreaterThan(0);
|
|
expect(name.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("getAllSourceCodes", () => {
|
|
it("returns all index sources except bundled ones", () => {
|
|
const codes = getAllSourceCodes();
|
|
expect(codes).not.toContain("TGL");
|
|
});
|
|
|
|
it("returns only strings", () => {
|
|
for (const code of getAllSourceCodes()) {
|
|
expect(typeof code).toBe("string");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("getDefaultFetchUrl", () => {
|
|
it("returns default GitHub URL when no baseUrl provided", () => {
|
|
const url = getDefaultFetchUrl("MM");
|
|
expect(url).toBe(
|
|
"https://raw.githubusercontent.com/5etools-mirror-3/5etools-src/main/data/bestiary/bestiary-mm.json",
|
|
);
|
|
});
|
|
|
|
it("constructs URL from baseUrl with trailing slash", () => {
|
|
const url = getDefaultFetchUrl("PHB", "https://example.com/data/");
|
|
expect(url).toBe("https://example.com/data/bestiary-phb.json");
|
|
});
|
|
|
|
it("normalizes baseUrl without trailing slash", () => {
|
|
const url = getDefaultFetchUrl("PHB", "https://example.com/data");
|
|
expect(url).toBe("https://example.com/data/bestiary-phb.json");
|
|
});
|
|
|
|
it("lowercases the source code in the filename", () => {
|
|
const url = getDefaultFetchUrl("XMM");
|
|
expect(url).toContain("bestiary-xmm.json");
|
|
});
|
|
|
|
it("applies filename override for Plane Shift sources", () => {
|
|
expect(getDefaultFetchUrl("PSA")).toContain("bestiary-ps-a.json");
|
|
expect(getDefaultFetchUrl("PSD")).toContain("bestiary-ps-d.json");
|
|
expect(getDefaultFetchUrl("PSK")).toContain("bestiary-ps-k.json");
|
|
});
|
|
});
|
|
|
|
describe("getSourceDisplayName", () => {
|
|
it("returns display name for a known source", () => {
|
|
const index = loadBestiaryIndex();
|
|
const [code, expectedName] = Object.entries(index.sources)[0];
|
|
expect(getSourceDisplayName(code)).toBe(expectedName);
|
|
});
|
|
|
|
it("falls back to source code for unknown source", () => {
|
|
expect(getSourceDisplayName("UNKNOWN_SOURCE_XYZ")).toBe(
|
|
"UNKNOWN_SOURCE_XYZ",
|
|
);
|
|
});
|
|
});
|