Files
Portfolio/tests/unit/audio-intent-coordinator.test.ts

299 lines
9.9 KiB
TypeScript

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> = {},
): 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();
});
});