up
This commit is contained in:
+213
-29
@@ -1,47 +1,231 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
import { Controller, parseCommand } from './controller.js'
|
||||
import { buildScreenPackage } from './screen-package.js'
|
||||
import { packageKey, type LocalPackage } from './package-store.js'
|
||||
import { parseRange } from './range.js'
|
||||
import type { Channel, Intent, Report } from '../src/types/screen.js'
|
||||
|
||||
const command = { id: 'one', action: 'play', media: 'audio', position: 0 }
|
||||
test('валидирует команды на границе IPC', () => {
|
||||
function testPackage(): LocalPackage {
|
||||
const pkg = buildScreenPackage('https://example.test', 7, {
|
||||
game: {
|
||||
game: {
|
||||
ID: 7,
|
||||
name: 'Тест',
|
||||
scenario: {
|
||||
story: { introduction: { audio: 'intro.wav' }, conclusion: { video: 'ending.mp4' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
plan: {},
|
||||
music: {
|
||||
gamePlaylistId: 1,
|
||||
playlists: [
|
||||
{
|
||||
ID: 1,
|
||||
name: 'Музыка',
|
||||
tracks: [
|
||||
{ ID: 1, name: 'Первый', position: 0, filename: 'one.wav' },
|
||||
{ ID: 2, name: 'Второй', position: 1, filename: 'two.wav' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
return {
|
||||
formatVersion: 1,
|
||||
key: packageKey(pkg.server, pkg.game.id),
|
||||
version: 'a'.repeat(64),
|
||||
package: pkg,
|
||||
files: [],
|
||||
}
|
||||
}
|
||||
function send(c: Controller, intent: Intent) {
|
||||
c.command({ session: c.state.session, sequence: c.state.sequence + 1, ...intent })
|
||||
}
|
||||
function ack(
|
||||
c: Controller,
|
||||
channel: Channel,
|
||||
status: Report['status'],
|
||||
position = 0,
|
||||
duration = 120,
|
||||
) {
|
||||
c.report({
|
||||
session: c.state.session,
|
||||
channel,
|
||||
operation: c.state.channels[channel].operation,
|
||||
status,
|
||||
position,
|
||||
duration,
|
||||
})
|
||||
}
|
||||
const controller = () => {
|
||||
const c = new Controller(() => {})
|
||||
c.output(true)
|
||||
return c
|
||||
}
|
||||
|
||||
test('валидирует команды IPC, включая канал, номер, позицию и громкость', () => {
|
||||
const c = controller()
|
||||
const command = { session: c.state.session, sequence: 1, action: 'play', channel: 'main' }
|
||||
assert.deepEqual(parseCommand(command), command)
|
||||
for (const value of [
|
||||
null,
|
||||
{},
|
||||
{ ...command, id: '../file' },
|
||||
{ ...command, session: '../' },
|
||||
{ ...command, sequence: 1.5 },
|
||||
{ ...command, sequence: 0 },
|
||||
{ ...command, action: ['play'] },
|
||||
{ ...command, media: ['audio'] },
|
||||
{ ...command, position: NaN },
|
||||
{ ...command, position: -1 },
|
||||
{ ...command, action: 'delete' },
|
||||
{ ...command, media: '/etc/passwd' },
|
||||
]) {
|
||||
{ ...command, channel: '/etc/passwd' },
|
||||
{ ...command, action: 'seek', position: NaN },
|
||||
{ ...command, action: 'seek', position: -1 },
|
||||
{ ...command, action: 'volume', volume: 101 },
|
||||
{ ...command, action: 'next' },
|
||||
{ ...command, action: 'mute', enabled: 'true' },
|
||||
])
|
||||
assert.throws(() => parseCommand(value))
|
||||
assert.equal(parseCommand({ ...command, action: 'seek', position: 900 }).action, 'seek')
|
||||
})
|
||||
test('не повторяет команды после 4096 операций, отклоняет старую сессию', () => {
|
||||
const c = controller()
|
||||
const old = { session: c.state.session, sequence: 1, action: 'play', channel: 'main' }
|
||||
c.command(old)
|
||||
for (let i = 0; i < 4100; i++) send(c, { action: 'volume', channel: 'main', volume: 35 })
|
||||
const revision = c.state.revision
|
||||
c.command(old)
|
||||
assert.equal(c.state.revision, revision)
|
||||
c.output(false)
|
||||
c.output(true)
|
||||
const session = c.state.session
|
||||
c.command({ ...old, sequence: 999999 })
|
||||
assert.equal(c.state.session, session)
|
||||
assert.equal(c.state.sequence, 0)
|
||||
assert.equal(c.state.channels.main.desired, 'stopped')
|
||||
})
|
||||
test('устаревшие подтверждения не влияют на новую операцию или пакет', () => {
|
||||
const c = controller()
|
||||
send(c, { action: 'play', channel: 'main' })
|
||||
const old = {
|
||||
session: c.state.session,
|
||||
channel: 'main',
|
||||
operation: c.state.channels.main.operation,
|
||||
status: 'playing',
|
||||
position: 9,
|
||||
duration: 120,
|
||||
}
|
||||
send(c, { action: 'pause', channel: 'main' })
|
||||
c.report(old)
|
||||
assert.equal(c.state.channels.main.status, 'pending')
|
||||
ack(c, 'main', 'paused', 12)
|
||||
assert.equal(c.state.channels.main.position, 12)
|
||||
c.selectPackage(testPackage())
|
||||
c.report(old)
|
||||
assert.equal(c.state.channels.main.desired, 'stopped')
|
||||
assert.equal(c.state.channels.main.position, 0)
|
||||
})
|
||||
test('повтор не применяется; устаревшее подтверждение не меняет новую команду', () => {
|
||||
let broadcasts = 0
|
||||
const c = new Controller(() => broadcasts++)
|
||||
c.command(command)
|
||||
c.command(command)
|
||||
assert.equal(broadcasts, 1)
|
||||
c.command({ ...command, id: 'two', action: 'pause' })
|
||||
c.report({ id: 'one', status: 'playing' })
|
||||
assert.equal(c.state.status, 'pending')
|
||||
c.report({ id: 'two', status: 'paused' })
|
||||
assert.equal(c.state.status, 'paused')
|
||||
assert.equal(c.state.revision, 3)
|
||||
test('play/replay/stop сбрасывают позицию, resume сохраняет, seek использует длительность', () => {
|
||||
const c = controller()
|
||||
send(c, { action: 'play', channel: 'main' })
|
||||
ack(c, 'main', 'playing', 42, 180)
|
||||
send(c, { action: 'pause', channel: 'main' })
|
||||
ack(c, 'main', 'paused', 42.5, 180)
|
||||
send(c, { action: 'resume', channel: 'main' })
|
||||
assert.equal(c.state.channels.main.position, 42.5)
|
||||
ack(c, 'main', 'playing', 43, 180)
|
||||
send(c, { action: 'seek', channel: 'main', position: 150 })
|
||||
assert.equal(c.state.channels.main.position, 150)
|
||||
assert.equal(c.state.channels.main.desired, 'playing')
|
||||
send(c, { action: 'seek', channel: 'main', position: 999 })
|
||||
assert.equal(c.state.channels.main.position, 180)
|
||||
send(c, { action: 'pause', channel: 'main' })
|
||||
ack(c, 'main', 'paused', 100, 180)
|
||||
send(c, { action: 'seek', channel: 'main', position: 90 })
|
||||
assert.equal(c.state.channels.main.desired, 'paused')
|
||||
send(c, { action: 'replay', channel: 'main' })
|
||||
assert.equal(c.state.channels.main.position, 0)
|
||||
assert.equal(c.state.channels.main.desired, 'playing')
|
||||
send(c, { action: 'stop', channel: 'main' })
|
||||
assert.equal(c.state.channels.main.position, 0)
|
||||
assert.equal(c.state.channels.main.desired, 'stopped')
|
||||
})
|
||||
test('playing подтверждается один раз, ошибки сохраняются', () => {
|
||||
const c = new Controller(() => {})
|
||||
c.command(command)
|
||||
c.report({ id: 'one', status: 'playing' })
|
||||
c.report({ id: 'one', status: 'playing' })
|
||||
test('музыка независима от основного канала, mute и громкость не перематывают', () => {
|
||||
const c = controller()
|
||||
c.selectPackage(testPackage())
|
||||
send(c, { action: 'play', channel: 'music' })
|
||||
ack(c, 'music', 'playing', 15)
|
||||
send(c, { action: 'select', channel: 'main', sourceId: 'conclusion' })
|
||||
send(c, { action: 'play', channel: 'main' })
|
||||
ack(c, 'main', 'playing', 20)
|
||||
const operation = c.state.channels.main.operation
|
||||
send(c, { action: 'volume', channel: 'main', volume: 80 })
|
||||
send(c, { action: 'mute', enabled: true })
|
||||
assert.equal(c.state.channels.main.operation, operation)
|
||||
assert.equal(c.state.channels.main.position, 20)
|
||||
assert.equal(c.state.channels.music.desired, 'playing')
|
||||
assert.equal(c.state.channels.music.position, 15)
|
||||
assert.equal(c.state.channels.music.volume, 35)
|
||||
send(c, { action: 'mute', enabled: false })
|
||||
assert.equal(c.state.channels.main.volume, 80)
|
||||
assert.throws(
|
||||
() => send(c, { action: 'select', channel: 'main', sourceId: 'arbitrary-file' }),
|
||||
/Ресурс/,
|
||||
)
|
||||
})
|
||||
test('музыка переходит вперёд ровно один раз; повторяет плейлист и сохраняет паузу', () => {
|
||||
const c = controller()
|
||||
c.selectPackage(testPackage())
|
||||
send(c, { action: 'play', channel: 'music' })
|
||||
ack(c, 'music', 'playing')
|
||||
const ended = {
|
||||
session: c.state.session,
|
||||
channel: 'music',
|
||||
operation: c.state.channels.music.operation,
|
||||
status: 'ended',
|
||||
position: 120,
|
||||
duration: 120,
|
||||
}
|
||||
c.report(ended)
|
||||
c.report(ended)
|
||||
assert.equal(c.state.channels.music.trackIndex, 1)
|
||||
assert.equal(c.state.channels.music.desired, 'playing')
|
||||
ack(c, 'music', 'playing')
|
||||
ack(c, 'music', 'ended', 120)
|
||||
assert.equal(c.state.channels.music.status, 'ended')
|
||||
assert.equal(c.state.channels.music.desired, 'paused')
|
||||
send(c, { action: 'loop', channel: 'music', enabled: true })
|
||||
send(c, { action: 'next', channel: 'music' })
|
||||
assert.equal(c.state.channels.music.trackIndex, 0)
|
||||
assert.equal(c.state.channels.music.desired, 'paused')
|
||||
send(c, { action: 'resume', channel: 'music' })
|
||||
ack(c, 'music', 'playing')
|
||||
send(c, { action: 'track', channel: 'music', trackIndex: 1 })
|
||||
ack(c, 'music', 'playing')
|
||||
ack(c, 'music', 'ended', 120)
|
||||
assert.equal(c.state.channels.music.trackIndex, 0)
|
||||
assert.equal(c.state.channels.music.desired, 'playing')
|
||||
})
|
||||
test('ошибки и закрытие окна не возобновляют звук; замер playing записывается один раз', () => {
|
||||
const c = controller()
|
||||
send(c, { action: 'play', channel: 'main' })
|
||||
ack(c, 'main', 'playing')
|
||||
ack(c, 'main', 'playing', 1)
|
||||
assert.equal(c.state.samples.length, 1)
|
||||
c.report({ id: 'one', status: 'error', error: 'Нет файла' })
|
||||
assert.equal(c.state.error, 'Нет файла')
|
||||
assert.throws(() => c.report({ id: 'one', status: 'anything' }))
|
||||
c.report({
|
||||
session: c.state.session,
|
||||
channel: 'main',
|
||||
operation: c.state.channels.main.operation,
|
||||
status: 'error',
|
||||
position: 1,
|
||||
duration: null,
|
||||
error: 'Нет файла',
|
||||
})
|
||||
assert.equal(c.state.channels.main.error, 'Нет файла')
|
||||
assert.equal(c.state.channels.main.desired, 'paused')
|
||||
c.output(false)
|
||||
c.output(true)
|
||||
assert.equal(c.state.channels.main.desired, 'stopped')
|
||||
assert.equal(c.state.channels.music.desired, 'stopped')
|
||||
assert.throws(() => c.report({ status: 'anything' }))
|
||||
})
|
||||
test('Range: полный файл, границы, суффикс и отказ для недопустимых диапазонов', () => {
|
||||
assert.equal(parseRange(null, 0), null)
|
||||
|
||||
Reference in New Issue
Block a user