feat(characters): add catalog boundary and capabilities

This commit is contained in:
2026-07-17 19:11:09 +08:00
parent a30e9937b8
commit 2fc312b5c7
9 changed files with 235 additions and 31 deletions
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
import {
CHARACTERS,
createCharacterCatalog,
DEFAULT_CHARACTER,
getCharacterById,
getCharacterBySlug,
@@ -22,9 +23,22 @@ describe("local character catalog", () => {
expect(Object.isFrozen(CHARACTERS)).toBe(true);
expect(Object.isFrozen(DEFAULT_CHARACTER)).toBe(true);
expect(Object.isFrozen(DEFAULT_CHARACTER.assets)).toBe(true);
expect(Object.isFrozen(DEFAULT_CHARACTER.capabilities)).toBe(true);
expect(Object.isFrozen(DEFAULT_CHARACTER.copy)).toBe(true);
});
it("sorts catalog entries and rejects duplicate identities", () => {
const catalog = createCharacterCatalog([...CHARACTERS].reverse());
expect(catalog.characters.map((character) => character.slug)).toEqual([
"elio",
"maya",
"nayeli",
]);
expect(() =>
createCharacterCatalog([CHARACTERS[0], CHARACTERS[0]]),
).toThrow("Duplicate character identity");
});
it("resolves characters by id and normalized slug", () => {
expect(getCharacterById("character_maya")?.displayName).toBe("Maya Tan");
expect(getCharacterBySlug(" NAYELI ")?.displayName).toBe(
@@ -34,6 +48,21 @@ describe("local character catalog", () => {
expect(getCharacterBySlug("missing")).toBeNull();
});
it("describes route capabilities and character-specific chat copy", () => {
for (const character of CHARACTERS) {
expect(character.capabilities).toEqual({
chat: true,
privateRoom: true,
tip: true,
});
expect(character.tagline.length).toBeGreaterThan(0);
expect(character.emptyChatGreeting.length).toBeGreaterThan(0);
expect(character.assets.chatBackground).toBe(
"/images/chat/bg-chatpage.png",
);
}
});
it("references local assets that exist under public", () => {
for (const character of CHARACTERS) {
for (const assetPath of Object.values(character.assets)) {