import { describe, expect, it } from 'vitest'; import { createAudioIntentCoordinator, type AudioIntentSample, } from '@/audio/audio-intent-coordinator'; import type { SectionId } from '@/content/types'; const sectionIds = [ 'start', 'profile', 'experience', 'infrastructure', 'selected-builds', 'additional-work', 'toolkit', 'contact', ] as const satisfies readonly SectionId[]; const baseSample = ( overrides: Partial = {}, ): AudioIntentSample => ({ now: 0, visible: true, scrollY: 0, activeSectionId: 'start', acceleration: 0, sectionActivation: { layoutRevision: 1, activeHeadingOffsetVh: -0.1, nextHeadingOffsetVh: 0.8, }, navigation: { epoch: 1, source: 'passive-scroll', phase: 'idle', destinationId: null, signDestinationId: null, }, ...overrides, }); describe('audio intent coordinator', () => { it('commits once only after 6vh hysteresis and 400 ms dwell', () => { const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0); expect(coordinator.sample(baseSample({ now: 10, scrollY: 10, activeSectionId: 'profile', sectionActivation: { layoutRevision: 1, activeHeadingOffsetVh: -0.059, nextHeadingOffsetVh: 0.8, }, }))).toMatchObject({ chapter: null, needsFrame: false }); expect(coordinator.sample(baseSample({ now: 100, scrollY: 10, activeSectionId: 'profile' }))) .toMatchObject({ chapter: null, needsFrame: true }); expect(coordinator.sample(baseSample({ now: 499, scrollY: 10, activeSectionId: 'profile' }))) .toMatchObject({ chapter: null }); expect(coordinator.sample(baseSample({ now: 500, scrollY: 10, activeSectionId: 'profile' })).chapter) .toMatchObject({ sectionId: 'profile', mood: 'open', generation: 1 }); expect(coordinator.sample(baseSample({ now: 900, scrollY: 10, activeSectionId: 'profile' })).chapter) .toBeNull(); }); it('clears a passive candidate on reversal or structural remeasurement', () => { const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0); coordinator.sample(baseSample({ now: 0, scrollY: 10, activeSectionId: 'profile' })); expect(coordinator.sample(baseSample({ now: 250, scrollY: 9, activeSectionId: 'start' })).needsFrame) .toBe(false); coordinator.sample(baseSample({ now: 300, scrollY: 12, activeSectionId: 'profile' })); expect(coordinator.sample(baseSample({ now: 800, scrollY: 12, activeSectionId: 'profile', sectionActivation: { layoutRevision: 2, activeHeadingOffsetVh: -0.2, nextHeadingOffsetVh: 0.7, }, }))).toMatchObject({ chapter: null, needsFrame: false }); }); it('commits only the latest settled explicit destination', () => { const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0); coordinator.sample(baseSample({ navigation: { epoch: 2, source: 'explicit-navigation', phase: 'travelling', destinationId: 'contact', signDestinationId: null, }, })); coordinator.sample(baseSample({ activeSectionId: 'toolkit', navigation: { epoch: 3, source: 'explicit-navigation', phase: 'travelling', destinationId: 'toolkit', signDestinationId: null, }, })); expect(coordinator.sample(baseSample({ now: 600, activeSectionId: 'toolkit', navigation: { epoch: 3, source: 'explicit-navigation', phase: 'settled', destinationId: 'toolkit', signDestinationId: null, }, }))).toMatchObject({ chapter: { sectionId: 'toolkit', mood: 'resolve', generation: 1 }, }); }); it('dead-bands tiny input and caps programmatic travel below lift entry', () => { const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0); expect(coordinator.sample(baseSample({ now: 0, acceleration: 0.07 })).drive).toBeNull(); expect(coordinator.sample(baseSample({ now: 40, acceleration: 0.7 })).drive) .toEqual({ energy: expect.closeTo((0.7 - 0.08) / 0.92), source: 'physical' }); expect(coordinator.sample(baseSample({ now: 80, acceleration: 1, navigation: { epoch: 2, source: 'explicit-navigation', phase: 'travelling', destinationId: 'contact', signDestinationId: null, }, })).drive).toEqual({ energy: 0.35, source: 'navigation' }); expect(coordinator.sample(baseSample({ now: 120, acceleration: 0 })).drive) .toEqual({ energy: 0, source: 'idle' }); }); it('rate-limits small drive changes but emits exact threshold, source, and zero crossings', () => { const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0); const physicalAcceleration = (energy: number): number => 0.08 + energy * 0.92; expect(coordinator.sample(baseSample({ now: 0, acceleration: physicalAcceleration(0.3) })).drive) .toEqual({ energy: expect.closeTo(0.3), source: 'physical' }); expect(coordinator.sample(baseSample({ now: 10, acceleration: physicalAcceleration(0.31) })).drive) .toBeNull(); expect(coordinator.sample(baseSample({ now: 11, acceleration: physicalAcceleration(0.32) })).drive) .toEqual({ energy: expect.closeTo(0.32), source: 'physical' }); expect(coordinator.sample(baseSample({ now: 12, acceleration: physicalAcceleration(0.62) })).drive) .toEqual({ energy: expect.closeTo(0.62), source: 'physical' }); expect(coordinator.sample(baseSample({ now: 13, acceleration: 0.2, navigation: { epoch: 2, source: 'explicit-navigation', phase: 'travelling', destinationId: 'contact', signDestinationId: null, }, })).drive).toEqual({ energy: 0.2, source: 'navigation' }); expect(coordinator.sample(baseSample({ now: 14, acceleration: 0 })).drive) .toEqual({ energy: 0, source: 'idle' }); }); it('cannot turn a layout-only active-section shift into a passive commit', () => { const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0); expect(coordinator.sample(baseSample({ now: 100, activeSectionId: 'profile', sectionActivation: { layoutRevision: 2, activeHeadingOffsetVh: -0.2, nextHeadingOffsetVh: 0.7, }, }))).toMatchObject({ chapter: null, needsFrame: false }); expect(coordinator.sample(baseSample({ now: 600, activeSectionId: 'profile', sectionActivation: { layoutRevision: 2, activeHeadingOffsetVh: -0.2, nextHeadingOffsetVh: 0.7, }, })).chapter).toBeNull(); expect(coordinator.sample(baseSample({ now: 650, scrollY: 2, activeSectionId: 'profile', sectionActivation: { layoutRevision: 2, activeHeadingOffsetVh: -0.2, nextHeadingOffsetVh: 0.7, }, })).needsFrame).toBe(true); expect(coordinator.sample(baseSample({ now: 1_050, scrollY: 2, activeSectionId: 'profile', sectionActivation: { layoutRevision: 2, activeHeadingOffsetVh: -0.2, nextHeadingOffsetVh: 0.7, }, })).chapter).toMatchObject({ sectionId: 'profile' }); }); it('defers a genuine settlement across remeasurement but ignores old settled state', () => { const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0); coordinator.sample(baseSample({ navigation: { epoch: 2, source: 'explicit-navigation', phase: 'travelling', destinationId: 'contact', signDestinationId: null, }, })); expect(coordinator.sample(baseSample({ now: 100, activeSectionId: 'contact', sectionActivation: { layoutRevision: 2, activeHeadingOffsetVh: -0.2, nextHeadingOffsetVh: null, }, navigation: { epoch: 2, source: 'explicit-navigation', phase: 'settled', destinationId: 'contact', signDestinationId: null, }, }))).toMatchObject({ chapter: null, needsFrame: true }); expect(coordinator.sample(baseSample({ now: 116, activeSectionId: 'contact', sectionActivation: { layoutRevision: 2, activeHeadingOffsetVh: -0.2, nextHeadingOffsetVh: null, }, navigation: { epoch: 2, source: 'explicit-navigation', phase: 'settled', destinationId: 'contact', signDestinationId: null, }, })).chapter).toMatchObject({ sectionId: 'contact' }); }); it('starts one final dwell after wheel events advance passive epochs', () => { const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0); const passive = (epoch: number) => ({ epoch, source: 'passive-scroll' as const, phase: 'idle' as const, destinationId: null, signDestinationId: null, }); coordinator.sample(baseSample({ now: 100, scrollY: 24, activeSectionId: 'profile', navigation: passive(2), })); expect(coordinator.sample(baseSample({ now: 180, scrollY: 48, activeSectionId: 'profile', navigation: passive(3), })).needsFrame).toBe(true); expect(coordinator.sample(baseSample({ now: 580, scrollY: 48, activeSectionId: 'profile', navigation: passive(3), })).chapter).toMatchObject({ sectionId: 'profile' }); }); it('suppresses a navigation that settled while hidden', () => { const coordinator = createAudioIntentCoordinator(sectionIds, 'start', 1, 0); const settled = { epoch: 2, source: 'explicit-navigation', phase: 'settled', destinationId: 'contact', signDestinationId: null, } as const; coordinator.reset({ navigation: settled, scrollY: 0, sectionActivation: baseSample().sectionActivation, }); expect(coordinator.sample(baseSample({ now: 1_000, activeSectionId: 'contact', navigation: settled, })).chapter).toBeNull(); }); });