import { describe, expect, it } from 'vitest' import { createDeck, createInitialConditionGenerator, createPresetNode, createPresetTree, createScene, deleteDeck, deletePresetNode, deleteScene, getDeck, getScene, listInitialConditionGenerators, listDecks, listPresetNodes, listScenes, resolveDeck, resolvePresetNode, resolveScene, updateDeck, updatePresetNode, updateScene } from '../src/caStudioRepository.js' import { createTestDb } from './testDb.js' describe('CA Studio repository', () => { it('creates preset nodes, resolves inherited params, and treats null as unset', async () => { const db = await createTestDb() const tree = await createPresetTree(db, { slug: 'ca-series', name: 'CA Series' }) const root = await createPresetNode(db, { treeId: tree.id, slug: 'root', name: 'Root', kind: 'preset_root', params: { caClass: { neighborhoodId: 'moore', dimensions: 2, states: 2 }, simulation: { grid: { size: [32, 32, 1], wrap: true }, ruleId: 'life-3d' }, renderer: { id: 'voxel-instanced', quality: 'high' }, overlays: ['generation-counter'] } }) const chapter = await createPresetNode(db, { treeId: tree.id, parentId: root.id, slug: 'game-of-life', name: 'Game of Life', kind: 'chapter', params: { simulation: { ruleId: 'game-of-life' }, overlays: ['minimal-title'] } }) const shot = await createPresetNode(db, { treeId: tree.id, parentId: chapter.id, slug: 'blinker', name: 'Blinker', kind: 'shot', params: { simulation: { grid: { wrap: null }, initialCondition: { patternId: 'blinker' } }, camera: { mode: '2d' } } }) const resolved = await resolvePresetNode(db, tree.id, shot.id) expect(resolved?.ancestry.map((node) => node.slug)).toEqual(['root', 'game-of-life', 'blinker']) expect(resolved?.params).toEqual({ simulation: { grid: { size: [32, 32, 1] }, ruleId: 'game-of-life', initialCondition: { patternId: 'blinker' } }, caClass: { neighborhoodId: 'moore', dimensions: 2, states: 2 }, renderer: { id: 'voxel-instanced', quality: 'high' }, overlays: ['minimal-title'], camera: { mode: '2d' } }) }) it('registers initial condition generators and filters them by CA class', async () => { const db = await createTestDb() await createInitialConditionGenerator(db, { slug: 'life-random-soup', name: 'Life Random Soup', generatorKind: 'random-soup', supportedClasses: [{ neighborhoodId: 'moore', dimensions: 2, states: 2 }], defaultParams: { density: 0.28 } }) await createInitialConditionGenerator(db, { slug: 'von-neumann-traffic', name: 'Traffic Seed', generatorKind: 'traffic-lanes', supportedClasses: [{ neighborhoodId: 'von-neumann', dimensions: 2, states: 3 }] }) expect((await listInitialConditionGenerators(db)).map((generator) => generator.slug)).toEqual( expect.arrayContaining(['life-random-soup', 'von-neumann-traffic', 'naga-markov-chains']) ) expect( (await listInitialConditionGenerators(db, { caClass: { neighborhoodId: 'moore', dimensions: 2, states: 2 } })).map((generator) => generator.slug) ).toEqual(['life-random-soup']) }) it('rejects cross-tree parents and duplicate sibling slugs', async () => { const db = await createTestDb() const firstTree = await createPresetTree(db, { slug: 'first', name: 'First' }) const secondTree = await createPresetTree(db, { slug: 'second', name: 'Second' }) const root = await createPresetNode(db, { treeId: firstTree.id, slug: 'root', name: 'Root', kind: 'preset_root' }) await expect( createPresetNode(db, { treeId: secondTree.id, parentId: root.id, slug: 'bad-parent', name: 'Bad Parent' }) ).rejects.toThrow() await createPresetNode(db, { treeId: firstTree.id, parentId: root.id, slug: 'child', name: 'Child A' }) await expect( createPresetNode(db, { treeId: firstTree.id, parentId: root.id, slug: 'child', name: 'Child B' }) ).rejects.toThrow() }) it('prevents parent cycles', async () => { const db = await createTestDb() const tree = await createPresetTree(db, { slug: 'cycles', name: 'Cycles' }) const root = await createPresetNode(db, { treeId: tree.id, slug: 'root', name: 'Root', kind: 'preset_root' }) const child = await createPresetNode(db, { treeId: tree.id, parentId: root.id, slug: 'child', name: 'Child' }) await expect(updatePresetNode(db, tree.id, root.id, { parentId: child.id })).rejects.toThrow( /cycle/i ) }) it('cascades child preset nodes when deleting a parent', async () => { const db = await createTestDb() const tree = await createPresetTree(db, { slug: 'delete-tree', name: 'Delete Tree' }) const root = await createPresetNode(db, { treeId: tree.id, slug: 'root', name: 'Root', kind: 'preset_root' }) await createPresetNode(db, { treeId: tree.id, parentId: root.id, slug: 'child', name: 'Child' }) expect(await deletePresetNode(db, tree.id, root.id)).toBe(true) expect(await listPresetNodes(db, tree.id)).toEqual([]) }) it('creates ordered scenes and resolves scene params over preset params', async () => { const db = await createTestDb() const tree = await createPresetTree(db, { slug: 'deck-tree', name: 'Deck Tree' }) const root = await createPresetNode(db, { treeId: tree.id, slug: 'root', name: 'Root', kind: 'preset_root', params: { simulation: { ruleId: 'life-3d', grid: { size: [16, 16, 16] } }, renderer: { id: 'classic-voxels' } } }) const deck = await createDeck(db, { slug: 'ca-001', title: 'CA 001' }) const intro = await createScene(db, { deckId: deck.id, orderIndex: 1, title: 'Intro', presetTreeId: tree.id, presetNodeId: root.id, params: { camera: { mode: '2d' } } }) await createScene(db, { deckId: deck.id, orderIndex: 2, title: 'Patch', presetTreeId: tree.id, presetNodeId: root.id, applyMode: 'patch_existing', params: { simulation: { speed: 2 } } }) await expect( createScene(db, { deckId: deck.id, orderIndex: 1, title: 'Duplicate Order', presetTreeId: tree.id, presetNodeId: root.id }) ).rejects.toThrow() expect((await listScenes(db, deck.id)).map((scene) => scene.title)).toEqual(['Intro', 'Patch']) expect((await listScenes(db, deck.id))[1].requires_previous_scene).toBe(true) expect(await resolveScene(db, intro.id)).toMatchObject({ params: { simulation: { ruleId: 'life-3d', grid: { size: [16, 16, 16] } }, renderer: { id: 'classic-voxels' }, camera: { mode: '2d' } } }) }) it('loads scenes through preset references and reflects preset updates on the next resolve', async () => { const db = await createTestDb() const tree = await createPresetTree(db, { slug: 'studio-tree', name: 'Studio Tree' }) const root = await createPresetNode(db, { treeId: tree.id, slug: 'root', name: 'Root', kind: 'preset_root', params: { simulation: { grid: { size: [24, 24, 1], wrap: true }, ruleId: 'life-3d' }, renderer: { id: 'classic-voxels', quality: 'medium' }, overlays: ['title', 'generation-counter'] } }) const gliderPreset = await createPresetNode(db, { treeId: tree.id, parentId: root.id, slug: 'glider', name: 'Glider', kind: 'shot', params: { simulation: { initialCondition: { patternId: 'glider' }, speed: 1 }, camera: { mode: '2d', zoom: 1 } } }) const deck = await createDeck(db, { slug: 'studio-deck', title: 'Studio Deck' }) const scene = await createScene(db, { deckId: deck.id, orderIndex: 1, title: 'Glider Shot', presetTreeId: tree.id, presetNodeId: gliderPreset.id, params: { camera: { zoom: 1.25 }, renderer: { quality: null } } }) expect(await resolveScene(db, scene.id)).toMatchObject({ params: { simulation: { grid: { size: [24, 24, 1], wrap: true }, ruleId: 'life-3d', initialCondition: { patternId: 'glider' }, speed: 1 }, renderer: { id: 'classic-voxels' }, overlays: ['title', 'generation-counter'], camera: { mode: '2d', zoom: 1.25 } } }) await updatePresetNode(db, tree.id, gliderPreset.id, { params: { simulation: { initialCondition: { patternId: 'lightweight-spaceship' }, speed: 1.75 }, camera: { mode: '2d', zoom: 0.85 } } }) expect(await resolveScene(db, scene.id)).toMatchObject({ params: { simulation: { grid: { size: [24, 24, 1], wrap: true }, ruleId: 'life-3d', initialCondition: { patternId: 'lightweight-spaceship' }, speed: 1.75 }, renderer: { id: 'classic-voxels' }, camera: { mode: '2d', zoom: 1.25 } } }) }) it('updates deck and scene records used by the editor CRUD flow', async () => { const db = await createTestDb() const tree = await createPresetTree(db, { slug: 'crud-tree', name: 'CRUD Tree' }) const root = await createPresetNode(db, { treeId: tree.id, slug: 'root', name: 'Root', kind: 'preset_root', params: { simulation: { ruleId: 'life-3d' } } }) const deck = await createDeck(db, { slug: 'crud-deck', title: 'CRUD Deck', params: { recording: { fps: 30 } } }) const scene = await createScene(db, { deckId: deck.id, orderIndex: 1, title: 'Original Scene', presetTreeId: tree.id, presetNodeId: root.id, params: { camera: { mode: '3d' } } }) const updatedDeck = await updateDeck(db, deck.id, { title: 'Updated Deck', params: { recording: { fps: 60, resolution: [1920, 1080] } } }) expect(updatedDeck).toMatchObject({ title: 'Updated Deck', params: { recording: { fps: 60, resolution: [1920, 1080] } } }) expect(await getDeck(db, deck.id)).toMatchObject({ title: 'Updated Deck' }) expect((await listDecks(db)).map((item) => item.slug)).toEqual(['crud-deck']) const updatedScene = await updateScene(db, scene.id, { orderIndex: 2, title: 'Updated Scene', applyMode: 'patch_existing', params: { camera: { mode: '2d' }, simulation: { speed: 2 } }, transition: { type: 'cut' } }) expect(updatedScene).toMatchObject({ order_index: 2, title: 'Updated Scene', apply_mode: 'patch_existing', requires_previous_scene: true, params: { camera: { mode: '2d' }, simulation: { speed: 2 } }, transition: { type: 'cut' } }) expect(await getScene(db, scene.id)).toMatchObject({ title: 'Updated Scene' }) expect(await resolveScene(db, scene.id)).toMatchObject({ params: { simulation: { ruleId: 'life-3d', speed: 2 }, camera: { mode: '2d' } } }) expect(await deleteScene(db, scene.id)).toBe(true) expect(await listScenes(db, deck.id)).toEqual([]) expect(await deleteDeck(db, deck.id)).toBe(true) expect(await getDeck(db, deck.id)).toBeNull() }) it('resolves a full deck as ordered playback scenes', async () => { const db = await createTestDb() const tree = await createPresetTree(db, { slug: 'playback-tree', name: 'Playback Tree' }) const root = await createPresetNode(db, { treeId: tree.id, slug: 'root', name: 'Root', kind: 'preset_root', params: { simulation: { ruleId: 'life-3d', grid: { size: [40, 40, 1] } }, renderer: { id: 'classic-voxels' }, recording: { fps: 60 } } }) const blinker = await createPresetNode(db, { treeId: tree.id, parentId: root.id, slug: 'blinker', name: 'Blinker', kind: 'shot', params: { simulation: { initialCondition: { patternId: 'blinker' } }, camera: { mode: '2d', zoom: 1 } } }) const glider = await createPresetNode(db, { treeId: tree.id, parentId: root.id, slug: 'glider', name: 'Glider', kind: 'shot', params: { simulation: { initialCondition: { patternId: 'glider' } }, camera: { mode: '2d', zoom: 0.9 } } }) const deck = await createDeck(db, { slug: 'playback-deck', title: 'Playback Deck' }) await createScene(db, { deckId: deck.id, orderIndex: 2, title: 'Glider Scene', presetTreeId: tree.id, presetNodeId: glider.id, applyMode: 'patch_existing', params: { camera: { zoom: 1.2 } } }) await createScene(db, { deckId: deck.id, orderIndex: 1, title: 'Blinker Scene', presetTreeId: tree.id, presetNodeId: blinker.id }) expect(await resolveDeck(db, deck.id)).toMatchObject({ deck: { title: 'Playback Deck' }, scenes: [ { scene: { order_index: 1, title: 'Blinker Scene' }, params: { simulation: { ruleId: 'life-3d', grid: { size: [40, 40, 1] }, initialCondition: { patternId: 'blinker' } }, renderer: { id: 'classic-voxels' }, recording: { fps: 60 }, camera: { mode: '2d', zoom: 1 } } }, { scene: { order_index: 2, title: 'Glider Scene', apply_mode: 'patch_existing', requires_previous_scene: true }, params: { simulation: { ruleId: 'life-3d', grid: { size: [40, 40, 1] }, initialCondition: { patternId: 'glider' } }, renderer: { id: 'classic-voxels' }, recording: { fps: 60 }, camera: { mode: '2d', zoom: 1.2 } } } ] }) await updatePresetNode(db, tree.id, root.id, { params: { simulation: { ruleId: 'life-3d', grid: { size: [48, 48, 1] } }, renderer: { id: 'neon-voxels' }, recording: { fps: 30 } } }) const resolved = await resolveDeck(db, deck.id) expect(resolved?.scenes.map((entry) => entry.params.renderer)).toEqual([ { id: 'neon-voxels' }, { id: 'neon-voxels' } ]) expect(resolved?.scenes.map((entry) => entry.params.recording)).toEqual([{ fps: 30 }, { fps: 30 }]) expect(resolved?.scenes[1].params.camera).toEqual({ mode: '2d', zoom: 1.2 }) }) })