feat(content): add Zod story graph schema and validation

This commit is contained in:
ginnoir
2026-06-11 18:49:28 -05:00
parent dc7b87eab2
commit a66a7db066
2 changed files with 198 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
import { describe, expect, it } from 'vitest';
import { buildStoryContent } from '../storySchema';
const resourcesById = {
supplies: { id: 'supplies', name: 'Supplies', startAmount: 0 },
coin: { id: 'coin', name: 'Coin', startAmount: 0 },
};
const actionsById = {
scout_path: { id: 'scout_path', name: 'Scout', durationMs: 1000, costs: [], yields: [{ resourceId: 'coin', amount: 1 }] },
};
const validNodes = [
{
id: 'boot_intro',
prose: 'You wake at the crossroads.',
triggers: [{ type: 'boot', targetNodeId: 'boot_intro' }],
},
{
id: 'fork_choice',
prose: 'Which way?',
choices: [
{
id: 'pick_a',
label: 'High road',
outcomes: [{ type: 'setFlag', flag: 'route_a' }],
targetNodeId: 'route_a_beat',
},
{
id: 'pick_b',
label: 'Low road',
outcomes: [{ type: 'setFlag', flag: 'route_b' }],
targetNodeId: 'route_b_beat',
},
],
},
{ id: 'route_a_beat', prose: 'The high road.' },
{ id: 'route_b_beat', prose: 'The low road.' },
];
describe('buildStoryContent()', () => {
it('indexes nodes and finds boot trigger', () => {
const story = buildStoryContent(validNodes, actionsById, resourcesById);
expect(story.storyNodesById.fork_choice.prose).toContain('Which way');
expect(story.bootTargetNodeId).toBe('boot_intro');
});
it('rejects dangling targetNodeId on choices', () => {
expect(() =>
buildStoryContent(
[{ id: 'n', prose: 'x', choices: [{ id: 'c', label: 'y', outcomes: [], targetNodeId: 'missing' }] }],
actionsById,
resourcesById,
),
).toThrow(/unknown story node/i);
});
it('rejects unknown actionId in actionComplete trigger', () => {
expect(() =>
buildStoryContent(
[
{
id: 't',
prose: 'x',
triggers: [{ type: 'actionComplete', actionId: 'ghost', targetNodeId: 'boot_intro' }],
},
{ id: 'boot_intro', prose: 'hi' },
],
actionsById,
resourcesById,
),
).toThrow(/unknown action/i);
});
it('requires exactly one boot trigger', () => {
expect(() =>
buildStoryContent([{ id: 'n', prose: 'no boot' }], actionsById, resourcesById),
).toThrow(/boot trigger/i);
});
});