import { afterEach, describe, expect, it, vi } from 'vitest'; import { gardenAudioConfig } from './audio/garden-audio-config'; import { getInitialVibe, hexToRgb, VIBE_PRESETS } from './vibes'; const originalLocalStorage = globalThis.localStorage; const setBrowserVibeState = ({ storedVibeId = null, }: { storedVibeId?: string | null; }) => { Object.defineProperty(globalThis, 'localStorage', { configurable: true, value: { getItem: vi.fn((key: string) => key === 'fleeting-garden:vibe' ? storedVibeId : null ), }, }); }; describe('vibe selection', () => { afterEach(() => { Object.defineProperty(globalThis, 'localStorage', { configurable: true, value: originalLocalStorage, }); }); it('uses a valid stored vibe id', () => { setBrowserVibeState({ storedVibeId: 'sunlit-moss' }); expect(getInitialVibe().id).toBe('sunlit-moss'); }); it('falls back to the default preset for an unknown stored vibe id', () => { setBrowserVibeState({ storedVibeId: 'unknown' }); expect(getInitialVibe()).toBe(VIBE_PRESETS[0]); }); }); describe('vibe and audio config contract', () => { it('keeps preset ids unique and URL-safe', () => { const vibeIds = VIBE_PRESETS.map((vibe) => vibe.id); expect(new Set(vibeIds).size).toBe(vibeIds.length); expect(vibeIds.every((id) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id))).toBe(true); }); it('keeps each vibe palette and audio profile complete', () => { VIBE_PRESETS.forEach((vibe) => { expect(vibe.colors).toHaveLength(3); vibe.colors.forEach((color) => { expect(color).toMatch(/^#[0-9a-f]{6}$/i); hexToRgb(color).forEach((channel) => { expect(channel).toBeGreaterThanOrEqual(0); expect(channel).toBeLessThanOrEqual(1); }); }); const profile = vibe.audio; expect(Number.isFinite(profile.rootMidi)).toBe(true); expect(profile.scale.length).toBeGreaterThan(0); expect(profile.scale.every((degree) => Number.isFinite(degree))).toBe(true); expect(profile.brightness).toBeGreaterThan(0); expect(profile.delayTimeMultiplier).toBeGreaterThan(0); expect(profile.progression.length).toBeGreaterThan(0); profile.progression.forEach((chord) => { expect(Number.isFinite(chord.rootOffset)).toBe(true); expect(['major', 'minor']).toContain(chord.quality); }); }); }); it('keeps audio style voices aligned with the rotating style pools', () => { expect(gardenAudioConfig.styleVoices).toHaveLength( gardenAudioConfig.generativePiano.stylePools.length ); gardenAudioConfig.styleVoices.forEach((voice) => { expect(Number.isFinite(voice.scaleDegreeOffset)).toBe(true); expect(voice.velocityMultiplier).toBeGreaterThan(0); expect(Math.abs(voice.panOffset)).toBeLessThanOrEqual(1); }); }); it('keeps audio timing, graph, and density settings bounded', () => { const { delay, generativePiano, graph, piano, rhythm } = gardenAudioConfig; expect(rhythm.bpm).toBeGreaterThan(0); expect(rhythm.stepsPerBeat).toBeGreaterThan(0); expect(rhythm.stepsPerBar).toBeGreaterThanOrEqual(rhythm.stepsPerBeat); expect(rhythm.lookaheadSeconds).toBeGreaterThanOrEqual(piano.scheduleAheadSeconds); expect(delay.feedbackMin).toBeLessThanOrEqual(delay.feedback); expect(delay.feedback).toBeLessThanOrEqual(delay.feedbackMax); expect(delay.feedbackHighPassHz).toBeLessThan(delay.feedbackLowPassHz); expect(delay.returnLowPassHz).toBeGreaterThan(delay.feedbackHighPassHz); generativePiano.stylePools.forEach((register) => { expect(register.midiMin).toBeLessThan(register.preferredMidi); expect(register.preferredMidi).toBeLessThan(register.midiMax); }); generativePiano.padRegisters.forEach((register) => { expect(register.midiMin).toBeLessThan(register.preferredMidi); expect(register.preferredMidi).toBeLessThan(register.midiMax); }); expect(generativePiano.brushStreamIdleIntervalBeats).toBeGreaterThanOrEqual( generativePiano.brushStreamActiveIntervalBeats ); expect(generativePiano.brushStreamActiveIntervalBeats).toBeGreaterThanOrEqual( generativePiano.brushStreamIntenseIntervalBeats ); expect(generativePiano.brushStreamIntenseIntervalBeats).toBeGreaterThanOrEqual( generativePiano.brushStreamManicIntervalBeats ); expect(generativePiano.maxBrushPhraseLayers).toBeLessThanOrEqual(3); expect(generativePiano.maxBrushStreamNotesPerBar).toBeLessThanOrEqual( rhythm.stepsPerBar ); Object.values(graph.pianoBusGains).forEach((gain) => { expect(gain).toBeGreaterThan(0); expect(gain).toBeLessThanOrEqual(1.2); }); }); it('falls back to finite RGB channels for malformed hex colors', () => { expect(hexToRgb('not-a-color')).toEqual([0, 0, 0]); expect(hexToRgb('#abcdzz')).toEqual([0, 0, 0]); }); });