mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
151 lines
5.3 KiB
TypeScript
151 lines
5.3 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import assert from 'node:assert/strict';
|
|
import {transitionVoiceEngineV2} from '@fluxer/voice_engine_v2/src/core/reducer';
|
|
import {selectVoiceEngineV2Model} from '@fluxer/voice_engine_v2/src/core/selectors';
|
|
import {
|
|
availableVoiceEngineV2Capabilities,
|
|
createVoiceEngineV2InitialSnapshot,
|
|
type VoiceEngineV2Snapshot,
|
|
} from '@fluxer/voice_engine_v2/src/core/state';
|
|
import type {VoiceEngineV2Command} from '@fluxer/voice_engine_v2/src/protocol/commands';
|
|
import type {VoiceEngineV2Event} from '@fluxer/voice_engine_v2/src/protocol/events';
|
|
import type {VoiceEngineV2Capabilities, VoiceEngineV2Model} from '@fluxer/voice_engine_v2/src/protocol/types';
|
|
|
|
export const VOICE_ENGINE_V2_EVENT_LOG_FIXTURE_VERSION = 1;
|
|
|
|
export interface VoiceEngineV2EventLogFixtureStep {
|
|
name?: string;
|
|
event: VoiceEngineV2Event;
|
|
commands: Array<VoiceEngineV2Command>;
|
|
}
|
|
|
|
export interface VoiceEngineV2EventLogFixtureExpected {
|
|
finalSnapshot: VoiceEngineV2Snapshot;
|
|
finalModel?: VoiceEngineV2Model;
|
|
}
|
|
|
|
export interface VoiceEngineV2EventLogFixture {
|
|
version: typeof VOICE_ENGINE_V2_EVENT_LOG_FIXTURE_VERSION;
|
|
name: string;
|
|
description?: string;
|
|
capabilities?: VoiceEngineV2Capabilities;
|
|
initialSnapshot?: VoiceEngineV2Snapshot;
|
|
steps: Array<VoiceEngineV2EventLogFixtureStep>;
|
|
expected: VoiceEngineV2EventLogFixtureExpected;
|
|
}
|
|
|
|
export interface VoiceEngineV2EventLogReplayStepResult {
|
|
index: number;
|
|
name: string | null;
|
|
event: VoiceEngineV2Event;
|
|
previousSnapshot: VoiceEngineV2Snapshot;
|
|
snapshot: VoiceEngineV2Snapshot;
|
|
commands: Array<VoiceEngineV2Command>;
|
|
}
|
|
|
|
export interface VoiceEngineV2EventLogReplayResult {
|
|
fixtureName: string;
|
|
initialSnapshot: VoiceEngineV2Snapshot;
|
|
steps: Array<VoiceEngineV2EventLogReplayStepResult>;
|
|
finalSnapshot: VoiceEngineV2Snapshot;
|
|
finalModel: VoiceEngineV2Model;
|
|
commandBatches: Array<Array<VoiceEngineV2Command>>;
|
|
}
|
|
|
|
export function replayVoiceEngineV2EventLogFixture(
|
|
fixture: VoiceEngineV2EventLogFixture,
|
|
): VoiceEngineV2EventLogReplayResult {
|
|
assertVoiceEngineV2EventLogFixture(fixture);
|
|
|
|
const initialSnapshot = fixture.initialSnapshot
|
|
? cloneReplayValue(fixture.initialSnapshot)
|
|
: createVoiceEngineV2InitialSnapshot(fixture.capabilities ?? availableVoiceEngineV2Capabilities());
|
|
const initialSnapshotForResult = cloneReplayValue(initialSnapshot);
|
|
let snapshot = initialSnapshot;
|
|
const steps: Array<VoiceEngineV2EventLogReplayStepResult> = [];
|
|
|
|
for (const [index, step] of fixture.steps.entries()) {
|
|
const previousSnapshot = cloneReplayValue(snapshot);
|
|
const previousSnapshotBeforeTransition = cloneReplayValue(snapshot);
|
|
deepFreeze(snapshot);
|
|
|
|
const transition = transitionVoiceEngineV2(snapshot, step.event);
|
|
|
|
assert.deepStrictEqual(
|
|
snapshot,
|
|
previousSnapshotBeforeTransition,
|
|
`${fixture.name} step ${index + 1} mutated its previous snapshot`,
|
|
);
|
|
assert.deepStrictEqual(
|
|
transition.commands,
|
|
step.commands,
|
|
`${fixture.name} step ${index + 1} emitted an unexpected command batch`,
|
|
);
|
|
|
|
snapshot = transition.snapshot;
|
|
steps.push({
|
|
index,
|
|
name: step.name ?? null,
|
|
event: cloneReplayValue(step.event),
|
|
previousSnapshot,
|
|
snapshot: cloneReplayValue(snapshot),
|
|
commands: cloneReplayValue(transition.commands),
|
|
});
|
|
}
|
|
|
|
assert.deepStrictEqual(snapshot, fixture.expected.finalSnapshot, `${fixture.name} final snapshot drifted`);
|
|
|
|
const finalModel = selectVoiceEngineV2Model(snapshot);
|
|
if (fixture.expected.finalModel) {
|
|
assert.deepStrictEqual(finalModel, fixture.expected.finalModel, `${fixture.name} final model drifted`);
|
|
}
|
|
|
|
return {
|
|
fixtureName: fixture.name,
|
|
initialSnapshot: initialSnapshotForResult,
|
|
steps,
|
|
finalSnapshot: cloneReplayValue(snapshot),
|
|
finalModel: cloneReplayValue(finalModel),
|
|
commandBatches: steps.map((step) => cloneReplayValue(step.commands)),
|
|
};
|
|
}
|
|
|
|
export function assertVoiceEngineV2EventLogFixture(fixture: VoiceEngineV2EventLogFixture): void {
|
|
assert.equal(
|
|
fixture.version,
|
|
VOICE_ENGINE_V2_EVENT_LOG_FIXTURE_VERSION,
|
|
`${fixture.name} uses an unsupported voice engine v2 event-log fixture version`,
|
|
);
|
|
assert.ok(fixture.name.length > 0, 'voice engine v2 event-log fixtures must have a name');
|
|
assert.ok(Array.isArray(fixture.steps), `${fixture.name} must provide replay steps`);
|
|
|
|
for (const [index, step] of fixture.steps.entries()) {
|
|
assert.ok(step.event && typeof step.event.type === 'string', `${fixture.name} step ${index + 1} has no event`);
|
|
assert.ok(Array.isArray(step.commands), `${fixture.name} step ${index + 1} must declare commands`);
|
|
}
|
|
}
|
|
|
|
function cloneReplayValue<Value>(value: Value): Value {
|
|
if (Array.isArray(value)) return value.map((entry) => cloneReplayValue(entry)) as Value;
|
|
if (!isFreezable(value)) return value;
|
|
const clone: Record<PropertyKey, unknown> = {};
|
|
for (const property of Reflect.ownKeys(value)) {
|
|
clone[property] = cloneReplayValue((value as Record<PropertyKey, unknown>)[property]);
|
|
}
|
|
return clone as Value;
|
|
}
|
|
|
|
function deepFreeze<Value>(value: Value): Value {
|
|
if (!isFreezable(value) || Object.isFrozen(value)) return value;
|
|
for (const property of Reflect.ownKeys(value)) {
|
|
deepFreeze((value as Record<PropertyKey, unknown>)[property]);
|
|
}
|
|
Object.freeze(value);
|
|
return value;
|
|
}
|
|
|
|
function isFreezable(value: unknown): value is Record<PropertyKey, unknown> {
|
|
return (typeof value === 'object' && value !== null) || typeof value === 'function';
|
|
}
|