Adding the lab project

This commit is contained in:
2026-07-01 12:10:12 +02:00
commit 48ce52b52c
56 changed files with 22289 additions and 0 deletions
+483
View File
@@ -0,0 +1,483 @@
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([
'life-random-soup',
'von-neumann-traffic'
])
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 })
})
})