mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-12 13:38:31 +09:00
155 lines
5.2 KiB
TypeScript
155 lines
5.2 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import type {
|
|
VoiceEngineV2CommandResult,
|
|
VoiceEngineV2Implementation,
|
|
} from '@fluxer/voice_engine_v2/src/implementations';
|
|
import type {VoiceEngineV2Command} from '@fluxer/voice_engine_v2/src/protocol';
|
|
import {createVoiceEngineV2MemoryEventLogSpillSink} from '@fluxer/voice_engine_v2/src/runtime/eventLogRing';
|
|
import {VoiceEngineV2Controller} from '@fluxer/voice_engine_v2/src/runtime/VoiceEngineV2Controller';
|
|
import {VoiceEngineV2Runtime} from '@fluxer/voice_engine_v2/src/runtime/VoiceEngineV2Runtime';
|
|
import {
|
|
FakeVoiceEngineV2Driver,
|
|
VoiceEngineV2TestImplementation,
|
|
waitForRuntime,
|
|
} from '@fluxer/voice_engine_v2/src/testing';
|
|
import {describe, expect, it} from 'vitest';
|
|
|
|
interface DeferredCommand {
|
|
command: VoiceEngineV2Command;
|
|
resolve(result?: VoiceEngineV2CommandResult): void;
|
|
}
|
|
|
|
class DeferredVoiceEngineV2Implementation implements VoiceEngineV2Implementation {
|
|
readonly kind = 'js' as const;
|
|
readonly started: Array<VoiceEngineV2Command> = [];
|
|
readonly deferred: Array<DeferredCommand> = [];
|
|
|
|
execute(command: VoiceEngineV2Command): Promise<VoiceEngineV2CommandResult> {
|
|
this.started.push(command);
|
|
if (command.type !== 'microphone.publish') return Promise.resolve({ok: true});
|
|
return new Promise<VoiceEngineV2CommandResult>((resolve) => {
|
|
this.deferred.push({
|
|
command,
|
|
resolve: (result = {ok: true}) => {
|
|
resolve(result);
|
|
},
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
interface ControllerRuntimeTestOptions {
|
|
clock?: {now(): number};
|
|
}
|
|
|
|
function createControllerTestRuntime(
|
|
implementation: VoiceEngineV2Implementation,
|
|
options: ControllerRuntimeTestOptions = {},
|
|
): VoiceEngineV2Runtime {
|
|
return new VoiceEngineV2Runtime(implementation, {
|
|
eventLogSpillSink: createVoiceEngineV2MemoryEventLogSpillSink(),
|
|
verifyEventLogInvariantsOnDispatch: true,
|
|
...options,
|
|
});
|
|
}
|
|
|
|
describe('VoiceEngineV2Controller', () => {
|
|
it('exposes explicit methods over the v2 event protocol', async () => {
|
|
const driver = new FakeVoiceEngineV2Driver();
|
|
const controller = new VoiceEngineV2Controller(
|
|
createControllerTestRuntime(new VoiceEngineV2TestImplementation(driver)),
|
|
);
|
|
|
|
controller.connect({url: 'wss://voice', token: 'token'});
|
|
await waitForRuntime();
|
|
controller.publishMicrophone({deviceId: 'default'});
|
|
controller.setMicrophoneEnabled(false);
|
|
await waitForRuntime();
|
|
|
|
expect(controller.model.connection.connected).toBe(true);
|
|
expect(controller.model.media.microphone).toBe('published');
|
|
expect(driver.calls).toEqual([
|
|
{type: 'connect', options: {url: 'wss://voice', token: 'token'}},
|
|
{type: 'publishMicrophone', options: {deviceId: 'default'}},
|
|
{type: 'setMicrophoneEnabled', enabled: false},
|
|
]);
|
|
});
|
|
|
|
it('records an append-only event log with the host clock', () => {
|
|
let now = 1000;
|
|
const driver = new FakeVoiceEngineV2Driver();
|
|
const runtime = createControllerTestRuntime(new VoiceEngineV2TestImplementation(driver), {
|
|
clock: {
|
|
now: () => {
|
|
now += 10;
|
|
return now;
|
|
},
|
|
},
|
|
});
|
|
|
|
runtime.dispatch({type: 'implementation.prewarmRequested'});
|
|
|
|
expect(runtime.eventLog).toHaveLength(1);
|
|
expect(runtime.eventLog[0]).toMatchObject({
|
|
sequence: 1,
|
|
atMs: 1010,
|
|
event: {type: 'implementation.prewarmRequested'},
|
|
commands: [{type: 'implementation.prewarm', operationId: 1}],
|
|
});
|
|
});
|
|
|
|
it('queries hardware encoder capabilities through the v2 command pipeline', async () => {
|
|
const driver = new FakeVoiceEngineV2Driver({
|
|
hardwareEncoderCapabilities: {
|
|
available: true,
|
|
backend: 'nvenc',
|
|
compiled: true,
|
|
runtime: true,
|
|
codecs: ['h264'],
|
|
zeroCopy: true,
|
|
nativeInputs: ['d3d11-texture'],
|
|
},
|
|
});
|
|
const controller = new VoiceEngineV2Controller(
|
|
createControllerTestRuntime(new VoiceEngineV2TestImplementation(driver)),
|
|
);
|
|
|
|
controller.queryHardwareEncoderCapabilities();
|
|
await waitForRuntime();
|
|
|
|
expect(driver.calls).toEqual([{type: 'getHardwareEncoderCapabilities'}]);
|
|
expect(controller.snapshot.hardwareEncoder.capabilities).toMatchObject({
|
|
available: true,
|
|
backend: 'nvenc',
|
|
zeroCopy: true,
|
|
});
|
|
});
|
|
|
|
it('serializes commands by resource and rejects stale completions', async () => {
|
|
const implementation = new DeferredVoiceEngineV2Implementation();
|
|
const runtime = createControllerTestRuntime(implementation);
|
|
|
|
runtime.dispatch({type: 'connection.connectRequested', options: {url: 'wss://voice', token: 'token'}});
|
|
await waitForRuntime();
|
|
runtime.dispatch({type: 'microphone.publishRequested', options: {deviceId: 'first'}});
|
|
await waitForRuntime();
|
|
runtime.dispatch({type: 'microphone.unpublishRequested'});
|
|
await waitForRuntime();
|
|
|
|
expect(implementation.started.map((command) => command.type)).toEqual(['connection.connect', 'microphone.publish']);
|
|
expect(runtime.commandQueue.map((command) => command.type)).toEqual(['microphone.unpublish']);
|
|
|
|
implementation.deferred[0]?.resolve();
|
|
await waitForRuntime();
|
|
|
|
expect(implementation.started.map((command) => command.type)).toEqual([
|
|
'connection.connect',
|
|
'microphone.publish',
|
|
'microphone.unpublish',
|
|
]);
|
|
expect(runtime.eventLog.some((entry) => entry.event.type === 'command.staleCompletionRejected')).toBe(true);
|
|
expect(runtime.snapshot.microphone.status).toBe('idle');
|
|
});
|
|
});
|