On / test_dashboard.js
Arena.ai Agent
Add bounded per-broadcaster format recovery
ce697a5
Raw History Blame Contribute Delete
7.55 kB
#!/usr/bin/env node
'use strict';
// Execute the real inline dashboard script with a small DOM/fetch harness. No live API.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = process.argv[2] || __dirname;
const html = fs.readFileSync(path.join(root, 'index.html'), 'utf8');
const script = html.match(/<script>([\s\S]*?)<\/script>/)[1];
const ids = [...html.matchAll(/id="([^"]+)"/g)].map(m => m[1]);
const OLD = {id: 'OLD_CHANNEL', title: 'Old channel'};
const NEW = {id: 'NEW_CHANNEL', title: 'New channel'};
const clone = o => JSON.parse(JSON.stringify(o));
let passed = 0;
function harness() {
const elements = new Map(ids.map(id => [id, {
hidden: false, disabled: false, value: '', textContent: '', innerHTML: '', focus() {}
}]));
const state = {active: clone(OLD), authorized: clone(NEW), automatic: false, events: [],
requests: [], prompts: [], alerts: [], accept: true, blockCheck: null, failCheck: false};
const storage = new Map();
const sandbox = {
console, TextEncoder, Date, JSON, String, Number, Math, Promise,
btoa: s => Buffer.from(s, 'binary').toString('base64'),
sessionStorage: {getItem: k => storage.get(k) || null, setItem: (k,v) => storage.set(k,v), removeItem: k => storage.delete(k)},
document: {hidden: false, activeElement: null,
getElementById(id) {assert(elements.has(id), 'missing DOM element: ' + id);return elements.get(id)},
addEventListener() {}},
setInterval(fn) {state.timer = fn;return 1},
alert(text) {state.alerts.push(text)},
confirm(text) {state.prompts.push(text);return state.accept},
async fetch(url, opts) {
const body = opts.body ? JSON.parse(opts.body) : null;
state.requests.push({url, method: opts.method, body, headers: opts.headers});
let data, status = 200;
if (url === '/api/youtube') data = {channel: clone(state.active), automatic: state.automatic,
configured: true, events: clone(state.events), settings: {privacy:'public', title:'title', description:'description'}};
else if (url === '/api/state') data = {tasks:[], logs:[], monitor_running:true, check_interval:20};
else if (url === '/api/youtube/check') {
if (state.blockCheck) await state.blockCheck;
if (state.failCheck) {status=400;data={detail:'OAuth check failed'}}
else data={channel:Object.assign(clone(state.authorized),
state.active.id!==state.authorized.id ? {previous:clone(state.active)} : {})};
} else if (url === '/api/youtube/start' || url === '/api/youtube/channel') {
assert.equal(body.channel_id, state.authorized.id, 'must send the freshly checked channel id');
state.active=clone(state.authorized);state.automatic=url.endsWith('/start');
data={ok:true, channel:clone(state.active)};
} else if (url === '/api/youtube/stop') {state.automatic=false;data={ok:true}}
else throw Error('Unexpected route: '+url);
return {ok:status<400,status,async json(){return clone(data)}};
}
};
const context=vm.createContext(sandbox);
vm.runInContext(script, context);
vm.runInContext("authHeader='Basic dGVzdDp0ZXN0'", context);
return {state, elements, context,
async click(id){await vm.runInContext("$('"+id+"').onclick()",context)},
async refresh(){await vm.runInContext('refresh()',context)},
read(expr){return vm.runInContext(expr,context)}};
}
function pass(name){passed++;console.log('PASS '+name)}
(async()=>{
let h=harness();
h.state.events=[{username:'muhajer_iraqi',title:'Live event',phase:'live',youtube_status:'live',
source:'hd 720p',send_mode:'copy',pending_send_mode:'matchgop',output_seconds:60}];
await h.refresh();
assert.match(h.elements.get('events').innerHTML,/وضع الإرسال الفعلي: copy/);
assert.match(h.elements.get('events').innerHTML,/الترميز الثابت ينتظر خانة: matchgop/);
pass('Per-event sender mode is shown instead of hiding an account-specific fallback behind the global preference');
await h.click('ytcheck');
assert.equal(h.read('checkedChannel.id'),NEW.id);
assert.equal(h.state.active.id,OLD.id);
await h.refresh();await h.state.timer();
assert.equal(h.read('checkedChannel.id'),NEW.id);
assert.equal(h.read('lastY.channel.id'),OLD.id);
assert.equal(h.elements.get('ytadopt').hidden,false);
assert.match(h.elements.get('ytchannelnotice').textContent,/New channel/);
pass('OAuth check → refresh → timer retains NEW, not the journal OLD (production regression)');
await h.click('ytstart');
const posts=h.state.requests.filter(r=>r.method==='POST');
assert.equal(posts.at(-2).url,'/api/youtube/check');
assert.equal(posts.at(-1).url,'/api/youtube/start');
assert.equal(posts.at(-1).body.channel_id,NEW.id);
assert.match(h.state.prompts.at(-1),/Old channel/);
assert.match(h.state.prompts.at(-1),/New channel/);
assert.equal(h.state.automatic,true);
assert.equal(h.elements.get('ytadopt').hidden,true);
pass('Start rechecks OAuth and explicitly confirms both channel names before NEW is enabled');
h=harness();await h.click('ytcheck');await h.click('ytadopt');
assert.equal(h.state.active.id,NEW.id);
assert.equal(h.state.automatic,false);
assert(!h.state.requests.some(r=>r.url==='/api/youtube/start'));
assert.equal(h.elements.get('ytadopt').hidden,true);
pass('Adopt-only confirms the new channel without starting a broadcast');
h=harness();await h.click('ytcheck');h.state.accept=false;await h.click('ytstart');
assert.equal(h.state.active.id,OLD.id);
assert(!h.state.requests.some(r=>r.url==='/api/youtube/start'));
pass('Cancelled start never sends a start request or adopts the channel');
h=harness();await h.click('ytcheck');h.state.accept=false;await h.click('ytadopt');
assert(!h.state.requests.some(r=>r.url==='/api/youtube/channel'));
assert.equal(h.state.active.id,OLD.id);
pass('Cancelled adoption leaves the previous channel unchanged');
h=harness();await h.click('ytcheck');
h.state.authorized={id:'THIRD_CHANNEL',title:'Rotated channel'};
await h.click('ytstart');
assert.equal(h.state.active.id,'THIRD_CHANNEL');
assert.match(h.state.prompts.at(-1),/Rotated channel/);
pass('Secrets rotated after check are freshly discovered, not silently confirmed with a stale id');
h=harness();h.state.failCheck=true;await h.click('ytstart');
assert(!h.state.requests.some(r=>r.url==='/api/youtube/start'));
assert.match(h.elements.get('error').textContent,/OAuth check failed/);
assert.equal(h.elements.get('ytcheck').disabled,false);
pass('Failed OAuth check cannot start and restores the controls');
h=harness();let release;
h.state.blockCheck=new Promise(resolve=>{release=resolve});
const first=h.click('ytstart');await Promise.resolve();await h.click('ytstart');release();await first;
assert.equal(h.state.requests.filter(r=>r.url==='/api/youtube/start').length,1);
assert.equal(h.state.requests.filter(r=>r.url==='/api/youtube/check').length,1);
pass('Double-click while OAuth is pending sends only one check and one start');
h=harness();await h.click('ytcheck');await h.click('logout');
assert.equal(h.read('checkedChannel'),null);
assert.equal(h.read('authHeader'),'');
assert.equal(h.elements.get('ytadopt').hidden,true);
pass('Logout clears the checked channel and credentials');
console.log('\n'+passed+' dashboard regressions passed');
})().catch(err=>{console.error(err);process.exitCode=1});