Files
KoalaSync/extension/canonical-media-state.test.mjs
T
2026-08-20 22:39:38 +02:00

160 lines
6.7 KiB
JavaScript

import { describe, expect, it } from 'vitest';
import {
canonicalMediaStateFromRoomData,
createCanonicalMediaStateTracker,
projectCanonicalMediaState,
validateCanonicalMediaState
} from './canonical-media-state.js';
const state = (revision, currentTime = revision * 10, playbackState = 'playing') => ({
revision,
currentTime,
playbackState,
updatedBy: 'peer-a'
});
describe('canonical media state validation', () => {
it('accepts a bounded canonical snapshot', () => {
expect(validateCanonicalMediaState(state(2))).toEqual(state(2));
});
it.each([
null,
[],
{},
{ revision: 0, playbackState: 'playing', currentTime: 1 },
{ revision: 1.5, playbackState: 'playing', currentTime: 1 },
{ revision: 1, playbackState: 'buffering', currentTime: 1 },
{ revision: 1, playbackState: 'playing', currentTime: NaN },
{ revision: 1, playbackState: 'playing', currentTime: Infinity },
{ revision: 1, playbackState: 'playing', currentTime: -1 },
{ revision: 1, playbackState: 'playing', currentTime: 86401 },
{ revision: 1, playbackState: 'playing', currentTime: '1' }
])('rejects malformed snapshot %#', value => {
expect(validateCanonicalMediaState(value)).toBeNull();
});
});
describe('ROOM_DATA capability compatibility', () => {
const legacyRoomData = {
roomId: 'room-a',
peers: [],
activeLobby: null,
hostPeerId: 'peer-a',
controlMode: 'everyone',
controllers: ['peer-a'],
capabilities: ['host-control', 'co-host', 'chat-v1']
};
it('treats an old relay without capability or mediaState as the unchanged fallback', () => {
expect(canonicalMediaStateFromRoomData(legacyRoomData)).toEqual({
status: 'unsupported',
mediaState: null
});
expect(canonicalMediaStateFromRoomData({ ...legacyRoomData, capabilities: undefined })).toEqual({
status: 'unsupported',
mediaState: null
});
});
it('does not consume a stray mediaState unless the relay advertises support', () => {
expect(canonicalMediaStateFromRoomData({ ...legacyRoomData, mediaState: state(4) })).toEqual({
status: 'unsupported',
mediaState: null
});
});
it('accepts null as a valid capable-relay state without creating pending recovery', () => {
expect(canonicalMediaStateFromRoomData({
...legacyRoomData,
capabilities: [...legacyRoomData.capabilities, 'media-state-v1'],
mediaState: null
})).toEqual({ status: 'empty', mediaState: null });
});
it('returns a validated snapshot only for a capable relay', () => {
expect(canonicalMediaStateFromRoomData({
...legacyRoomData,
capabilities: [...legacyRoomData.capabilities, 'media-state-v1'],
mediaState: state(4)
})).toEqual({ status: 'available', mediaState: state(4) });
});
});
describe('canonical media state tracker', () => {
it('projects a deferred playing snapshot from local receipt time and clamps it', () => {
expect(projectCanonicalMediaState(state(1, 100, 'playing'), 1_000, 31_000))
.toMatchObject({ currentTime: 130, playbackState: 'playing' });
expect(projectCanonicalMediaState(state(1, 100, 'paused'), 1_000, 31_000))
.toMatchObject({ currentTime: 100, playbackState: 'paused' });
expect(projectCanonicalMediaState(state(1, 86_390, 'playing'), 1_000, 31_000).currentTime)
.toBe(86_400);
});
it('accepts a valid snapshot and applies each revision once', () => {
const tracker = createCanonicalMediaStateTracker();
expect(tracker.receive('room-a', state(1)).status).toBe('pending');
expect(tracker.markHandled('room-a', 1)).toBe(true);
expect(tracker.receive('room-a', state(1)).status).toBe('duplicate');
});
it('ignores stale revisions and lets a newer snapshot replace pending state', () => {
const tracker = createCanonicalMediaStateTracker();
tracker.receive('room-a', state(3));
expect(tracker.receive('room-a', state(2)).status).toBe('stale');
expect(tracker.receive('room-a', state(4)).status).toBe('pending');
expect(tracker.getPending('room-a').mediaState).toEqual(state(4));
});
it('never exposes room A state after switching to room B or leaving', () => {
const tracker = createCanonicalMediaStateTracker();
tracker.receive('room-a', state(5));
tracker.adoptRoom('room-b');
expect(tracker.getPending('room-a')).toBeNull();
expect(tracker.getPending('room-b')).toBeNull();
tracker.receive('room-b', state(1));
tracker.clear();
expect(tracker.snapshot()).toEqual({ roomId: null, knownRevision: 0, appliedRevision: 0, pending: null });
});
it('allows the same revision once in a new reconnect recovery cycle', () => {
const tracker = createCanonicalMediaStateTracker();
tracker.receive('room-a', state(8, 80));
tracker.markHandled('room-a', 8);
tracker.beginRecovery('room-a');
expect(tracker.receive('room-a', state(8, 100)).status).toBe('pending');
expect(tracker.getPending().mediaState.currentTime).toBe(100);
});
it('accepts a lower revision from a new relay or room epoch after reconnect', () => {
const tracker = createCanonicalMediaStateTracker();
tracker.receive('room-a', state(12), 1_000);
tracker.markHandled('room-a', 12);
tracker.beginRecovery('room-a');
expect(tracker.receive('room-a', state(1), 2_000).status).toBe('pending');
expect(tracker.getPending('room-a').mediaState.revision).toBe(1);
});
it('persists receipt time so MV3 recovery projects only playing snapshots', () => {
const first = createCanonicalMediaStateTracker();
first.receive('room-a', state(4, 50, 'playing'), 10_000);
const restored = createCanonicalMediaStateTracker();
expect(restored.restore(first.snapshot(), 'room-a')).toBe(true);
expect(restored.getPendingProjected('room-a', 15_000).mediaState.currentTime).toBe(55);
});
it('restores only room-scoped session state', () => {
const first = createCanonicalMediaStateTracker();
first.receive('room-a', state(9));
const stored = first.snapshot();
const sameRoom = createCanonicalMediaStateTracker();
expect(sameRoom.restore(stored, 'room-a')).toBe(true);
expect(sameRoom.getPending('room-a').mediaState.revision).toBe(9);
const otherRoom = createCanonicalMediaStateTracker();
expect(otherRoom.restore(stored, 'room-b')).toBe(false);
expect(otherRoom.getPending('room-b')).toBeNull();
});
});