// Behavioral timer contracts using the actual browser functions, offline only. const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const source = fs.readFileSync(path.join(__dirname, '../ui_builder/static/app.js'), 'utf8'); const plain = value => JSON.parse(JSON.stringify(value)); function definition(name) { const start = source.search(new RegExp('^ (?:async )?function ' + name + '\\(', 'm')); assert.ok(start >= 0, `Production function missing: ${name}`); const end = source.indexOf('\n }', start); assert.ok(end > start, `Production function boundary missing: ${name}`); return source.slice(start, end + 4); } // Load actual transitive production dependencies while retaining explicit I/O stubs. function productionClosure(context, roots) { const functions = new Map(), pattern = /^( {0,2})(?:async )?function (\w+)\(/gm; let match; while ((match = pattern.exec(source))) { const start = match.index, lineEnd = source.indexOf('\n', start); const singleLine = /\}\s*;?\s*$/.test(source.slice(start, lineEnd)); const end = singleLine ? lineEnd : source.indexOf('\n' + match[1] + '}', start) + match[1].length + 2; assert.ok(end > start, 'Unclosed production helper ' + match[2]); functions.set(match[2], source.slice(start, end)); } const needed = new Set(roots); function visit(name) { assert.ok(functions.has(name), 'Missing actual production helper ' + name); for (const candidate of functions.keys()) { if (needed.has(candidate) || typeof context[candidate] === 'function') continue; if (new RegExp('\\b' + candidate + '\\b').test(functions.get(name))) { needed.add(candidate); visit(candidate); } } } roots.forEach(visit); return [...needed].map(name => functions.get(name)).join('\n'); } function runtime(names, extra = {}) { const state = { config: { components: [], shortcut_sequences: [] }, data: {}, pressedShortcutKeys: new Set(), pressedShortcutModifiers: new Set(), activeHockeyVmixTimerSteps: new Set(), vmixPenaltyTargetAssignments: new Map(), vmixPenaltyMirrors: new Map(), vmixGameCountdownSeededTargets: new Set(), runningShortcutSequences: new Set(), pendingShortcutSequenceRuns: new Map(), vmixCommandQueue: new Promise(() => {}), }; const context = { state, console: { warn() {}, error() {} }, window: {}, Promise, setTimeout, clearTimeout, queueMicrotask, padTimer: value => String(value).padStart(2, '0'), hockeyCanControl: () => true, currentRuntimeVmixDeviceId: () => 'LIVE-DEVICE', currentRuntimeHockeySessionToken: () => 'match-session', trackRuntimeOverlayCommands() {}, applyServerRuntimeOverlayState() {}, ...extra, }; vm.createContext(context); const presentation = ['getByPath', 'hockeyTimerSelectedGameId', 'hockeyScoreboardPresentationContext', 'hockeyObserveScoreboardPresentation', 'hockeySuppressScoreboardAuxiliaries', // Load the production dependency closure added to the transport/shortcut. 'hockeyGameControlLanguage', 'hockeyVmixCommandIsUrgent', 'hockeyVmixUrgentGroup', 'hockeyVmixTraceNow', 'hockeyVmixTransportContext', 'hockeyVmixTrace', 'hockeyVmixPhysicalInput', 'hockeyVmixVisibilityIntent', 'hockeyRequireTitleMappingReady', 'hockeyTrackVmixDelivery', 'vmixOverlayLayer', 'hockeyShortcutOutTargets', 'hockeyShortcutExecution', 'hockeyRunShortcutSequenceOut', 'hockeyHandleAuxiliaryShortcut', 'hockeyAuxiliaryGateContext', 'hockeyScoreboardAuxiliariesEnabled', 'hockeyMatchFlags', 'hockeyAuxiliaryVisibilityCommands', 'hockeyScoreboardAuxiliaryCommand']; vm.runInContext(productionClosure(context, [...new Set([...presentation, ...names])]), context); return context; } const nativeNames = ['vmixCountdownValue', 'vmixGameCountdownControlCommands']; const scenarios = { async key_edges_web_first() { const game = { running: false, startedOnce: false, currentMs: 1200000 }; const component = { action_id: 'game' }; const step = { id: 'timer', type: 'hockey_vmix_timers_start', enabled: true, hockey_timer_command: 'toggle', game_timer_action_id: 'game', start_web_game: true, sync_vmix_game: true, game_vmix_input: 'SCORE', game_vmix_selected_name: 'CLOCK.Text', sync_vmix_penalties: false, start_web_penalties: false }; const sequence = { id: 'space', combo: 'Space', enabled: true, steps: [step] }; const sends = [], runs = []; const c = runtime([...nativeNames, 'normalizeShortcutCombo', 'shortcutModifierFromEvent', 'shortcutKeyFromEvent', 'shortcutPhysicalKeyToken', 'shortcutFromKeyboardEvent', 'handleConfiguredShortcuts', 'handleConfiguredShortcutCombo', 'runShortcutSequenceStep'], { shortcutModifierOrder: ['Ctrl', 'Alt', 'Shift', 'Meta'], buildShortcutRuntimeContext: () => ({}), sequenceConditionMatches: () => true, componentByActionId: () => component, isTimerComponent: () => true, ensureTimerState: () => game, sequencePenaltyTargets: () => [], currentHockeyPenaltyEntries: () => [], shortcutSequenceAllowed: () => true, controlTimer: (_id, command, _value, options) => { assert.equal(options.syncVmix, false, 'web transition must not recursively send vMix'); game.running = command !== 'pause'; game.startedOnce = true; return true; }, sendRuntimeVmixTimerSequence: commands => { sends.push({ commands: plain(commands), running: game.running, started: game.startedOnce }); return new Promise(() => {}); // A slow Agent must not block the next physical press. }, }); c.state.config.shortcut_sequences = [sequence]; c.runShortcutSequence = (seq, meta) => { runs.push(meta); return c.runShortcutSequenceStep(seq, step); }; const keyUpStart = source.indexOf('window.addEventListener("keyup", (event) => {'); assert.ok(keyUpStart >= 0); const keyUpEnd = source.indexOf('\n }, true);', keyUpStart); let keyUp; c.window = { addEventListener: (_type, listener) => { keyUp = listener; } }; vm.runInContext(source.slice(keyUpStart, keyUpEnd + '\n }, true);'.length), c); let swallowed = 0; const key = (repeat = false) => ({ key: ' ', code: 'Space', repeat, preventDefault() { swallowed++; }, stopPropagation() {}, stopImmediatePropagation() {} }); assert.equal(c.handleConfiguredShortcuts(key()), true); assert.equal(runs.length, 1); assert.equal(game.running, true); assert.equal(sends[0].running, true, 'web state must change before any transport'); assert.deepEqual(sends[0].commands.map(row => row.Function), ['SetCountdown', 'StartCountdown']); assert.equal(sends[0].commands[0].Value, '00:20:00'); assert.equal(c.handleConfiguredShortcuts(key()), true, 'duplicate physical keydown is swallowed'); assert.equal(c.handleConfiguredShortcuts(key(true)), true, 'browser repeat is swallowed'); assert.equal(runs.length, 1); keyUp(key()); assert.equal(c.state.pressedShortcutKeys.size, 0, 'actual production keyup releases the latch'); assert.equal(c.handleConfiguredShortcuts(key()), true); assert.equal(runs.length, 2); assert.equal(game.running, false); assert.equal(sends[1].running, false, 'pause changes web state before transport'); assert.deepEqual(sends[1].commands.map(row => row.Function), ['SuspendCountdown']); keyUp(key()); assert.equal(c.handleConfiguredShortcuts(key(true)), false, 'orphan auto-repeat cannot trigger a shortcut'); assert.equal(runs.length, 2); assert.ok(swallowed >= 4); }, async ordered_transport() { const calls = []; let release; const pending = new Promise(resolve => { release = resolve; }); const c = runtime(['compactVmixCommand', 'sendRuntimeVmixTimerSequence'], { fetch: (url, options) => { calls.push({ url, options, body: JSON.parse(options.body) }); return pending; }, }); let value = '00:18:42'; const transport = c.sendRuntimeVmixTimerSequence(() => [ { Function: 'SetCountdown', Input: 'SCORE', SelectedName: 'CLOCK.Text', Value: () => value }, { Function: 'StartCountdown', Input: 'SCORE', SelectedName: 'CLOCK.Text' }, ]); assert.equal(calls.length, 1, 'timer must dispatch without waiting for the permanently blocked generic queue'); const sent = calls[0]; assert.equal(sent.url, '/api/hockey/vmix/sequence'); assert.equal(sent.options.keepalive, true); assert.equal(sent.body.delivery_mode, 'timer-fast-ordered'); assert.equal(sent.body.device_id, 'LIVE-DEVICE'); assert.equal(sent.body.session_token, 'match-session'); assert.equal('sequence_id' in sent.body, false); assert.equal('button_id' in sent.body, false); assert.deepEqual(sent.body.commands.map(row => row.Function), ['SetCountdown', 'StartCountdown']); assert.equal(sent.body.commands[0].Value, value); release({ ok: true, json: async () => ({ ok: true, results: [{ ok: true }, { ok: true }] }) }); assert.equal((await transport).ok, true); }, async blank_device_fails_closed() { let fetchCount = 0; const web = { running: false, startedOnce: false, currentMs: 1200000 }; const component = { action_id: 'game' }; const step = { id: 'timer', type: 'hockey_vmix_timers_start', enabled: true, hockey_timer_command: 'toggle', game_timer_action_id: 'game', start_web_game: true, sync_vmix_game: true, game_vmix_input: 'SCORE', game_vmix_selected_name: 'CLOCK.Text', sync_vmix_penalties: false, start_web_penalties: false }; const c = runtime([...nativeNames, 'compactVmixCommand', 'sendRuntimeVmixTimerSequence', 'runShortcutSequenceStep'], { currentRuntimeVmixDeviceId: () => '', currentRuntimeHockeySessionToken: () => 'stale-session-with-old-device', fetch: async () => { fetchCount++; return { ok: true, json: async () => ({ ok: true }) }; }, buildShortcutRuntimeContext: () => ({}), sequenceConditionMatches: () => true, componentByActionId: () => component, isTimerComponent: () => true, ensureTimerState: () => web, sequencePenaltyTargets: () => [], currentHockeyPenaltyEntries: () => [], controlTimer: (_id, action) => { web.running = action !== 'pause'; web.startedOnce = true; return true; }, }); const unsent = { input: 'SCORE', eventKey: 'unsent-penalty' }; const unrelated = { input: 'OTHER', eventKey: 'unrelated-penalty' }; c.state.vmixPenaltyTargetAssignments.set('unsent', unsent); c.state.vmixPenaltyTargetAssignments.set('unrelated', unrelated); const result = await c.sendRuntimeVmixTimerSequence([{ Function: 'StartCountdown', Input: 'SCORE', SelectedName: 'CLOCK.Text' }]); assert.equal(c.state.vmixPenaltyTargetAssignments.has('unsent'), false, 'blank output cannot confirm an unsent target'); assert.equal(c.state.vmixPenaltyTargetAssignments.get('unrelated'), unrelated, 'unrelated targets remain intact'); assert.equal(fetchCount, 0, 'blank Agent selection must never fall back through the session'); assert.equal(result.ok, false); assert.equal(result.reason, 'device_selection_required'); await c.runShortcutSequenceStep({ id: 'space' }, step); assert.equal(web.running, true, 'blank output target cannot roll back a local start'); assert.equal(web.currentMs, 1200000); await c.runShortcutSequenceStep({ id: 'space' }, step); assert.equal(web.running, false, 'the next local pause must remain usable'); assert.equal(fetchCount, 0); }, async failed_assignment_cleanup_preserves_newer_delivery() { let reject; const c = runtime(['compactVmixCommand', 'sendRuntimeVmixTimerSequence'], { fetch: () => new Promise((_resolve, fail) => { reject = fail; }), }); const older = { input: 'PENALTY', eventKey: 'old' }; const newer = { input: 'PENALTY', eventKey: 'new' }; c.state.vmixPenaltyTargetAssignments.set('slot', older); const pending = c.sendRuntimeVmixTimerSequence([{ Function: 'SetCountdown', Input: 'PENALTY', SelectedName: 'Clock.Text', Value: '00:02:00' }]); c.state.vmixPenaltyTargetAssignments.set('slot', newer); reject(new Error('older network request failed')); assert.equal((await pending).ok, false); assert.equal(c.state.vmixPenaltyTargetAssignments.get('slot'), newer); }, async queued_values_sampled_at_dispatch() { let release; const blocker = new Promise(resolve => { release = resolve; }); const calls = []; const c = runtime(['compactVmixCommand', 'sendRuntimeVmixSequence'], { AbortController, window: { setTimeout }, fetch: async (url, options) => { calls.push({ url, body: JSON.parse(options.body) }); return { ok: true, json: async () => ({ ok: true, confirmation: 'ack', results: [{ ok: true, ack_waited: true }] }) }; }, }); c.state.vmixCommandQueue = blocker; c.state.vmixCommandQueueDepth = 0; let commandReads = 0, valueReads = 0, liveValue = '00:20:00'; const target = { Input: 'SCORE', SelectedName: 'CLOCK.Text' }; const result = c.sendRuntimeVmixSequence(() => { commandReads++; return [{ Function: 'SetCountdown', ...target, Value: () => { valueReads++; return liveValue; } }]; }); await Promise.resolve(); assert.equal(commandReads, 0, 'command factories must wait for their actual queue turn'); assert.equal(valueReads, 0, 'countdown values must not be sampled before dispatch'); assert.equal(calls.length, 0); liveValue = '00:19:55'; release(); assert.equal((await result).ok, true); assert.equal(commandReads, 1); assert.equal(valueReads, 1); assert.deepEqual(calls[0].body.commands, [{ Function: 'SetCountdown', ...target, Value: liveValue }]); assert.equal(c.state.vmixCommandQueueDepth, 0); }, async native_first_start() { const c = runtime(nativeNames); let reads = 0; const commands = c.vmixGameCountdownControlCommands('SCORE', 'CLOCK.Text', () => { reads++; return 1200000; }, 'start'); assert.deepEqual(plain(commands), [ { Function: 'SetCountdown', Input: 'SCORE', SelectedName: 'CLOCK.Text', Value: '00:20:00' }, { Function: 'StartCountdown', Input: 'SCORE', SelectedName: 'CLOCK.Text' }, ]); assert.equal(reads, 1); }, async native_pause_resume() { const c = runtime(nativeNames); let reads = 0; const provider = () => { reads++; return 999000; }; for (const action of ['pause', 'stop']) { const commands = c.vmixGameCountdownControlCommands('SCORE', 'CLOCK.Text', provider, action); assert.deepEqual(plain(commands), [{ Function: 'SuspendCountdown', Input: 'SCORE', SelectedName: 'CLOCK.Text' }]); } assert.deepEqual(plain(c.vmixGameCountdownControlCommands('SCORE', 'CLOCK.Text', provider, 'resume')), [{ Function: 'StartCountdown', Input: 'SCORE', SelectedName: 'CLOCK.Text' }]); assert.equal(reads, 0, 'native pause/resume must not even read or reseed browser time'); }, async scoreboard_before_overlay() { const game = { running: false, currentMs: 1200000 }, component = { action_id: 'game' }; const timerStep = { id: 'timer', enabled: true, type: 'hockey_vmix_timers_start', sync_vmix_game: true, game_vmix_mode: 'countdown', game_timer_action_id: 'game', game_vmix_input: 'SCORE', game_vmix_selected_name: 'CLOCK.Text' }; const sequence = { id: 'scoreboard', name: 'Scoreboard', enabled: true, is_scoreboard_sequence: true, steps: [{ id: 'overlay', enabled: true, type: 'vmix_command' }] }; const order = []; const c = runtime([...nativeNames, 'vmixGameCountdownTargetKey', 'markVmixGameCountdownSeeded', 'configuredHockeyVmixGameCountdownTargets', 'syncConfiguredScoreboardCountdownsToRuntime', 'runShortcutSequence'], { shortcutSequenceIsOnAir: () => false, clone: plain, window: { dispatchEvent() {} }, CustomEvent: class { constructor(type, args) { this.type = type; this.detail = args.detail; } }, componentByActionId: () => component, isTimerComponent: () => true, ensureTimerState: () => game, runShortcutSequenceStep: async () => { order.push('overlay'); return { ok: true }; }, sendRuntimeVmixTimerSequence: async commands => { order.push(plain(commands)); return { ok: true }; }, reconcileShortcutSequenceOnAir() {}, activateConfiguredPenaltyCountdowns() {}, rebalanceVmixPenaltyTargets: async () => {}, hockeySyncScoreboardConditionalSteps: async () => {}, hockeyTitleMappingNotice: () => '', toast() {}, }); c.state.config.shortcut_sequences = [{ enabled: true, steps: [timerStep] }]; assert.equal(await c.runShortcutSequence(sequence), true); assert.deepEqual(order[0].map(row => row.Function), ['SetCountdown', 'StopCountdown']); assert.equal(order[0][0].Value, '00:20:00'); assert.equal(order[1], 'overlay', 'stopped clock seed must reach timer transport before OverlayIn'); order.length = 0; game.running = true; assert.equal(await c.runShortcutSequence(sequence), true); assert.deepEqual(order, ['overlay'], 'showing scoreboard cannot rewrite a running native timer'); }, }; module.exports = { definition, runtime, scenarios }; if (require.main === module) { (async () => { const scenario = process.argv[2]; assert.ok(scenarios[scenario], `Unknown scenario: ${scenario}`); await scenarios[scenario](); console.log(`PASS: ${scenario}`); })().catch(error => { console.error(error); process.exitCode = 1; }); }