import { describe, expect, it } from 'vitest'; import { buildContent } from '../schema'; const validResources = [{ id: 'gold', name: 'Gold' }]; const validActions = [ { id: 'forage', name: 'Forage', group: { id: 'camp', label: 'Camp' }, durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }], }, ]; describe('buildContent()', () => { it('validates definitions and indexes them by id', () => { const content = buildContent({ resources: validResources, actions: validActions }); expect(content.resourcesById.gold.name).toBe('Gold'); expect(content.actionsById.forage.durationMs).toBe(3000); }); it('applies the startAmount default of 0', () => { const content = buildContent({ resources: validResources, actions: validActions }); expect(content.resourcesById.gold.startAmount).toBe(0); }); it('rejects an action that yields an unknown resource', () => { const actions = [ { id: 'forage', name: 'Forage', group: { id: 'camp', label: 'Camp' }, durationMs: 3000, yields: [{ resourceId: 'ghost', amount: 1 }], }, ]; expect(() => buildContent({ resources: validResources, actions })).toThrow(/unknown resource/i); }); it('rejects duplicate resource ids', () => { const resources = [ { id: 'gold', name: 'Gold' }, { id: 'gold', name: 'Gold Again' }, ]; expect(() => buildContent({ resources, actions: validActions })).toThrow(/duplicate/i); }); it('rejects a structurally invalid definition', () => { const actions = [ { id: 'forage', name: 'Forage', group: { id: 'camp', label: 'Camp' }, durationMs: -1, yields: [{ resourceId: 'gold', amount: 1 }], }, ]; expect(() => buildContent({ resources: validResources, actions })).toThrow(); }); }); describe('costs and multi-yield', () => { it('accepts optional costs and multiple yields', () => { const resources = [ { id: 'gold', name: 'Gold' }, { id: 'wood', name: 'Wood' }, ]; const actions = [ { id: 'craft', name: 'Craft', group: { id: 'camp', label: 'Camp' }, durationMs: 1000, costs: [{ resourceId: 'wood', amount: 2 }], yields: [ { resourceId: 'gold', amount: 1 }, { resourceId: 'wood', amount: 1 }, ], }, ]; const content = buildContent({ resources, actions }); expect(content.actionsById.craft.costs).toEqual([{ resourceId: 'wood', amount: 2 }]); expect(content.actionsById.craft.yields).toHaveLength(2); }); it('rejects cost referencing unknown resource', () => { const actions = [ { id: 'craft', name: 'Craft', group: { id: 'camp', label: 'Camp' }, durationMs: 1000, costs: [{ resourceId: 'ghost', amount: 1 }], yields: [{ resourceId: 'gold', amount: 1 }], }, ]; expect(() => buildContent({ resources: validResources, actions })).toThrow(/unknown resource/i); }); it('defaults costs to empty array', () => { const content = buildContent({ resources: validResources, actions: validActions }); expect(content.actionsById.forage.costs).toEqual([]); }); }); describe('unlock conditions', () => { it('accepts optional minResources and requireStoryFlags', () => { const actions = [ { id: 'forage', name: 'Forage', group: { id: 'camp', label: 'Camp' }, durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }], unlock: { minResources: { gold: 10 }, requireStoryFlags: ['intro_complete'], }, }, ]; const content = buildContent({ resources: validResources, actions }); expect(content.actionsById.forage.unlock).toEqual({ minResources: { gold: 10 }, requireStoryFlags: ['intro_complete'], }); }); it('defaults unlock to undefined when omitted', () => { const content = buildContent({ resources: validResources, actions: validActions }); expect(content.actionsById.forage.unlock).toBeUndefined(); }); }); describe('action narrative fields', () => { it('accepts optional storyHint and storyTooltip', () => { const actions = [ { id: 'forage', name: 'Forage', group: { id: 'camp', label: 'Camp' }, durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }], storyHint: 'Gather what the forest offers.', storyTooltip: 'Your first step into the wild.', }, ]; const content = buildContent({ resources: validResources, actions }); expect(content.actionsById.forage.storyHint).toBe('Gather what the forest offers.'); expect(content.actionsById.forage.storyTooltip).toBe('Your first step into the wild.'); }); it('defaults storyHint and storyTooltip to undefined when omitted', () => { const content = buildContent({ resources: validResources, actions: validActions }); expect(content.actionsById.forage.storyHint).toBeUndefined(); expect(content.actionsById.forage.storyTooltip).toBeUndefined(); }); }); describe('action kind schema', () => { it('accepts kind, group, and storyChoiceId', () => { const content = buildContent({ resources: [{ id: 'supplies', name: 'Supplies' }], actions: [ { id: 'pick_high_road', name: 'Take the high road', kind: 'story', group: { id: 'fork', label: 'Crossroads' }, storyChoiceId: 'pick_a', yields: [], }, ], }); expect(content.actionsById.pick_high_road.kind).toBe('story'); expect(content.actionsById.pick_high_road.group.label).toBe('Crossroads'); }); it('defaults kind to timed and requires durationMs for timed actions', () => { expect(() => buildContent({ resources: [{ id: 'supplies', name: 'Supplies' }], actions: [ { id: 'broken', name: 'Broken', kind: 'timed', group: { id: 'camp', label: 'Camp' }, yields: [{ resourceId: 'supplies', amount: 1 }], }, ], }), ).toThrow(); }); it('requires durationMs for loop actions', () => { const content = buildContent({ resources: [{ id: 'supplies', name: 'Supplies' }], actions: [ { id: 'rest', name: 'Rest', kind: 'loop', group: { id: 'camp_loop', label: 'Camp activities' }, durationMs: 2000, yields: [{ resourceId: 'supplies', amount: 1 }], }, ], }); expect(content.actionsById.rest.kind).toBe('loop'); }); it('rejects loop action without durationMs', () => { expect(() => buildContent({ resources: [{ id: 'supplies', name: 'Supplies' }], actions: [ { id: 'broken_loop', name: 'Broken loop', kind: 'loop', group: { id: 'camp', label: 'Camp' }, yields: [], }, ], }), ).toThrow(/durationMs/); }); });