refactor: split soundtrack chapter and drive commands

This commit is contained in:
2026-09-04 15:43:41 +03:00
parent 5250a3174c
commit b427d101fa
9 changed files with 170 additions and 136 deletions

View File

@@ -3,7 +3,7 @@ import { createAudioController } from '@/audio/create-audio-controller';
import type {
AudioGraph,
AudioGraphFactory,
AudioJourneyState,
AudioInitialState,
} from '@/audio/types';
describe('audio controller lifecycle', () => {
@@ -196,25 +196,49 @@ describe('audio controller lifecycle', () => {
expect(controller.snapshot().kind).toBe('failed');
});
it('stores the latest semantic state before activation and forwards raw clamped updates', async () => {
it('stores and normalizes chapter and drive independently before activation', async () => {
const harness = createHarness();
const controller = createAudioController({ createGraph: harness.createGraph });
controller.setChapterIntent({
sectionId: 'contact',
mood: 'resolve',
atStart: false,
entryPhrase: 99,
generation: -3,
});
controller.setDriveEnergy({ energy: 2, source: 'navigation' });
controller.setJourneyState({ atStart: false, bias: 'resolve', entryPhrase: 7, intensity: 2 });
expect(harness.constructed).toBe(0);
await controller.startFromGesture();
expect(harness.initialStates).toEqual([{
chapter: {
sectionId: 'contact',
mood: 'resolve',
atStart: false,
entryPhrase: 7,
generation: 0,
},
drive: { energy: 1, source: 'navigation' },
}]);
expect(harness.initialStates).toEqual([
{ atStart: false, bias: 'resolve', entryPhrase: 7, intensity: 1 },
]);
controller.setJourneyState({ atStart: false, bias: 'shadow', entryPhrase: 2, intensity: 0.73 });
expect(harness.graph.setJourneyState).toHaveBeenLastCalledWith({
controller.setChapterIntent({
sectionId: 'experience',
mood: 'shadow',
atStart: false,
bias: 'shadow',
entryPhrase: 2,
intensity: 0.73,
generation: 4,
});
controller.setDriveEnergy({ energy: 0.73, source: 'physical' });
expect(harness.graph.setChapterIntent).toHaveBeenLastCalledWith({
sectionId: 'experience',
mood: 'shadow',
atStart: false,
entryPhrase: 2,
generation: 4,
});
expect(harness.graph.setDriveEnergy).toHaveBeenLastCalledWith({
energy: 0.73,
source: 'physical',
});
});
});
@@ -245,7 +269,8 @@ function createHarness(options: HarnessOptions = {}) {
connectOutput: vi.fn(),
disconnectOutput: vi.fn(),
setOutputGain: vi.fn(),
setJourneyState: vi.fn(),
setChapterIntent: vi.fn(),
setDriveEnergy: vi.fn(),
cancelPending: vi.fn(),
onContextStateChange: vi.fn((listener) => {
stateListeners.add(listener);
@@ -253,10 +278,10 @@ function createHarness(options: HarnessOptions = {}) {
}),
} satisfies AudioGraph;
let constructed = 0;
const initialStates: AudioJourneyState[] = [];
const createGraph: AudioGraphFactory = (initialState?: AudioJourneyState) => {
const initialStates: AudioInitialState[] = [];
const createGraph: AudioGraphFactory = (initialState: AudioInitialState) => {
constructed += 1;
if (initialState) initialStates.push(initialState);
initialStates.push(initialState);
return graph;
};
return {

View File

@@ -12,17 +12,19 @@ import {
describe('soundtrack transport decisions', () => {
it('starts at the intro only on Start and otherwise chooses the matching main phrase', () => {
expect(resolveInitialPlayback({
sectionId: 'start',
mood: 'open',
atStart: true,
bias: 'open',
entryPhrase: 0,
intensity: 0,
generation: 0,
})).toEqual({ kind: 'intro' });
expect(resolveInitialPlayback({
sectionId: 'infrastructure',
mood: 'shadow',
atStart: false,
bias: 'shadow',
entryPhrase: 3,
intensity: 0.5,
generation: 1,
})).toEqual({ kind: 'main', offsetSeconds: mainPhraseOffsetSeconds(3) });
expect(mainPhraseOffsetSeconds(Number.NaN)).toBe(0);