'use strict';
const assert = require('node:assert/strict');
const vm = require('node:vm');
const {script, loader} = JSON.parse(require('node:fs').readFileSync(process.argv[2], 'utf8'));
const token = 'a'.repeat(64);
const json = value => new Response(JSON.stringify(value));
const session = (extra = {}) => json({username: '
', role: 'user', csrf: token, expires_in: 3600, ...extra});
const ticket = () => json({ticket: 't'.repeat(32)});
const serialSettings = (extra = {}) => ({running: true, baud: 230400, data_bits: '8', parity: 'none',
stop_bits: '1', flow: 'rts-cts', dtr: 'on-connect', rts_threshold: 96, ...extra});
const failure = status => new Response('SECRET ERROR BODY', {status, headers: {'Retry-After': '7'}});
const deferred = () => { let resolve; const promise = new Promise(r => { resolve = r; }); return {promise, resolve}; };
const tick = async () => { for (let i = 0; i < 6; ++i) await new Promise(r => setImmediate(r)); };
function browser({onlyLoader = false, withLoader = false, role = 'user'} = {}) {
const nodes = {}, events = {}, calls = [], redirects = [], timers = new Map(), sockets = [], terminals = [];
const queues = {'/api/session': [], '/api/status': [], '/api/ws-ticket': [], '/api/admin/ws-ticket': [], '/api/logout': [], '/api/settings/serial': [], '/api/settings/serial-operation': []};
const fits = [];
let serial = 0, now = Date.now();
class Clock extends Date { static now() { return now; } }
const on = (key, fn) => { if (!(events[key] ||= []).includes(fn)) events[key].push(fn); };
const emit = (key, event = {}) => { for (const fn of events[key] || []) fn(event); };
const timeout = (fn, ms, interval = false) => { timers.set(++serial, {fn, ms, interval}); return serial; };
class Socket {
static OPEN = 1;
constructor(url) { this.url = url; this.readyState = 0; this.bufferedAmount = 0; this.events = {}; this.sent = []; sockets.push(this); }
addEventListener(k, fn) { this.events[k] = fn; }
emit(k, event = {}) { if (k === 'open') this.readyState = 1; this.events[k]?.(event); this['on' + k]?.(event); }
close() { this.closed = true; this.readyState = 3; this.emit('close'); }
send(value) { this.sent.push(value); }
}
class Terminal {
constructor(options) { this.options = options; this.writes = []; terminals.push(this); }
loadAddon() {} open() {} focus() {} resize(cols, rows) { this.cols = cols; this.rows = rows; } onData(fn) { this.input = fn; }
write(bytes, callback) { this.writes.push([...bytes]); if (this.holdWrites) (this.pending ||= []).push(callback); else callback?.(); }
}
const window = {confirm: () => true, addEventListener: on, removeEventListener(k, fn) { events[k] = (events[k] || []).filter(f => f !== fn); },
setTimeout: timeout, clearTimeout: id => timers.delete(id),
setInterval: (fn, ms) => timeout(fn, ms, true), clearInterval: id => timers.delete(id),
requestAnimationFrame: fn => timeout(fn, -1), cancelAnimationFrame: id => timers.delete(id),
location: {origin: 'https://sak.local', replace: path => redirects.push(path)}};
const context = vm.createContext({window, document: {getElementById(id) {
return nodes[id] ||= {textContent: '', dataset: {}, classList: {toggle() {}},
setAttribute(k, v) { this[k] = v; },
getBoundingClientRect: () => ({width: 100, height: 100}),
addEventListener(k, fn) { this[k] = fn; }};
}}, Terminal, FitAddon: {FitAddon: class {
constructor() { this.measurements = []; this.calls = 0; fits.push(this); }
proposeDimensions() { ++this.calls; return this.measurements.length ? this.measurements.shift() : {cols: 80, rows: 24}; }
}},
TextEncoder, TextDecoder, Uint8Array, ArrayBuffer, AbortController, URL, Date: Clock, performance: {now: () => now}, WebSocket: Socket,
fetch: async (url, options) => {
// Apply the Origin regression guard to every mutation, including logout.
assert.ok(Object.hasOwn(queues, url));
if (options.method === 'POST') assert.equal(options.mode, 'cors');
assert.equal(options.headers?.Origin, undefined);
calls.push({url, ...options});
const next = queues[url].shift();
if (next !== undefined) return typeof next === 'function' ? next(options) : next;
if (url === '/api/session') return session({role});
if (url === '/api/status') return json({});
if (url === '/api/settings/serial') return json(serialSettings());
if (url === '/api/ws-ticket') return ticket();
if (url === '/api/admin/ws-ticket') return json({ticket: '0123456789abcdef'.repeat(4), expires_in: 30});
throw new Error('network unavailable');
}});
if (withLoader || onlyLoader) vm.runInContext(loader, context);
const start = () => vm.runInContext(script, context);
const fire = ms => {
const match = [...timers].find(([, t]) => t.ms === ms); assert.ok(match, `missing timer ${ms}`);
const [id, t] = match; if (!t.interval) timers.delete(id); t.fn();
};
return {nodes, calls, redirects, timers, sockets, terminals, queues, fits, events, emit, start, fire,
click: id => nodes[id].click(), elapse: ms => { now += ms; }, window};
}
async function connected() { const b = browser(); b.start(); await tick(); assert.equal(b.sockets.length, 1); return b; }
let passed = 0;
async function test(name, fn) { await fn(); ++passed; console.log('PASS JS:', name); }
(async () => {
await test('bootstrap, CSRF, bounded expiry safe text, serial protocol and disconnect pause', async () => {
const b = await connected();
assert.equal(b.calls[0].url, '/api/session');
const post = b.calls.find(c => c.url === '/api/ws-ticket');
assert.equal(post.method, 'POST'); assert.equal(post.body, ''); assert.equal(post.headers['X-CSRF-Token'], token);
for (const call of b.calls) for (const [k, v] of Object.entries({credentials: 'same-origin', mode: call.method === 'POST' ? 'cors' : 'same-origin', cache: 'no-store', redirect: 'error'})) assert.equal(call[k], v);
assert.match(b.nodes['session-info'].textContent, /^
.*one hour absolute/);
const ws = b.sockets[0], term = b.terminals[0]; ws.emit('open');
ws.emit('message', {data: JSON.stringify({type: 'hello', clientId: 8, writerId: 8, role: 'writer'})});
term.input('x'.repeat(2050)); assert.deepEqual(ws.sent.map(x => x.length), [1024, 1024, 2]);
ws.emit('message', {data: Uint8Array.of(0, 255, 13, 10).buffer}); assert.deepEqual(term.writes, [[0, 255, 13, 10]]);
b.click('release-control'); assert.equal(ws.sent.at(-1), 'release-writer');
ws.emit('message', {data: JSON.stringify({type: 'writer', writerId: 0, role: 'observer'})});
b.click('request-control'); assert.equal(ws.sent.at(-1), 'request-writer');
b.click('connection-toggle'); assert.ok(ws.closed); assert.equal(b.nodes['connection-toggle'].textContent, 'Connect');
const count = b.calls.length; ws.emit('close'); await tick(); assert.equal(b.calls.length, count);
b.click('connection-toggle'); await tick(); assert.equal(b.calls[count].url, '/api/session');
});
await test('401 at session/ticket/status stops everything and navigates only once', async () => {
for (const path of ['/api/session', '/api/ws-ticket', '/api/status']) {
const b = browser(); b.queues[path].push(failure(401)); b.start(); await tick();
assert.deepEqual(b.redirects, ['/login']); assert.ok(b.sockets.every(s => s.closed));
assert.equal(b.timers.size, 0); b.window.sakSessionExpired(); assert.equal(b.redirects.length, 1);
b.click('connection-toggle'); await tick(); assert.equal(b.redirects.length, 1);
}
});
await test('403 mutation is manual-only; capacity backoff is not credentials and revalidates', async () => {
for (const status of [403, 429, 503]) {
const b = browser(); b.queues['/api/ws-ticket'].push(failure(status)); b.start(); await tick();
assert.deepEqual(b.redirects, []); assert.equal(b.sockets.length, 0);
if (status === 403) {
assert.match(b.nodes['connection-detail'].textContent, /security check/);
assert.equal(b.nodes['connection-toggle'].textContent, 'Connect'); b.click('connection-toggle');
} else {
assert.match(b.nodes['connection-detail'].textContent, /capacity or backoff/); b.fire(7000);
}
await tick(); assert.equal(b.calls.filter(c => c.url === '/api/session').length, 2);
assert.equal(b.sockets.length, 1);
}
});
await test('logout success, lost success, uncertain network and explicit recovery', async () => {
for (const outcome of ['204', 'lost401', 'lost200', 'offline', '403', '503']) {
const b = await connected();
b.queues['/api/logout'].push(outcome === '204' ? new Response(null, {status: 204}) :
['403', '503'].includes(outcome) ? failure(Number(outcome)) : () => { throw new Error('SECRET NETWORK'); });
if (outcome === 'lost401') b.queues['/api/session'].push(session(), failure(401));
if (outcome === 'offline') b.queues['/api/session'].push(session(), () => { throw new Error('offline'); });
await b.click('sign-out'); await tick(); assert.ok(b.sockets[0].closed);
assert.equal(b.calls.filter(c => c.url === '/api/logout').length, 1);
const post = b.calls.find(c => c.url === '/api/logout'); assert.equal(post.body, ''); assert.equal(post.headers['X-CSRF-Token'], token);
if (['204', 'lost401'].includes(outcome)) { assert.deepEqual(b.redirects, ['/login']); assert.equal(b.timers.size, 0); }
else {
assert.deepEqual(b.redirects, []); assert.match(b.nodes['connection-status'].textContent, /not confirmed/);
assert.ok(!b.nodes['connection-detail'].textContent.includes('SECRET'));
assert.equal(b.nodes['sign-out'].disabled, false);
const count = b.calls.length; b.click('connection-toggle'); await tick(); assert.equal(b.calls[count].url, '/api/session');
assert.equal(b.sockets.length, 2);
}
}
});
await test('logout cancels pending status/ticket/session; late 401 and WS events cannot affect new work', async () => {
for (const path of ['/api/session', '/api/ws-ticket', '/api/status']) {
const d = deferred(), b = browser(); b.queues[path].push(d.promise); b.start(); await tick();
b.queues['/api/logout'].push(failure(403)); await b.click('sign-out'); await tick();
assert.ok(b.calls.find(c => c.url === path).signal.aborted);
const detail = b.nodes['connection-detail'].textContent;
d.resolve(failure(401)); await tick(); assert.deepEqual(b.redirects, []); assert.equal(b.nodes['connection-detail'].textContent, detail);
}
const b = await connected(), old = b.sockets[0]; b.click('connection-toggle'); b.click('connection-toggle'); await tick();
old.emit('open'); old.emit('message', {data: JSON.stringify({type: 'hello', clientId: 99, writerId: 99, role: 'writer'})}); old.emit('error'); old.emit('close');
assert.equal(b.nodes['client-id'].textContent, '—'); assert.equal(b.terminals[0].options.disableStdin, true);
});
await test('pagehide/restore revalidates, preserves pause; late logout cannot navigate restored page', async () => {
for (const paused of [false, true]) {
const b = await connected(); if (paused) b.click('connection-toggle');
b.emit('pagehide'); const count = b.calls.length; b.emit('pageshow', {persisted: true}); await tick();
assert.equal(b.calls[count].url, '/api/session'); assert.equal(b.sockets.length, paused ? 1 : 2);
if (paused) assert.equal(b.nodes['connection-toggle'].textContent, 'Connect');
}
const b = await connected(), d = deferred(); b.queues['/api/logout'].push(d.promise);
const pending = b.click('sign-out'); await tick(); b.emit('pagehide'); b.emit('pageshow', {persisted: true}); await tick();
d.resolve(new Response(null, {status: 204})); await pending; assert.deepEqual(b.redirects, []);
});
await test('bounded schema/body validation, timeout, expiry and retry session checks', async () => {
for (const response of [session({csrf: 'A'.repeat(64)}), session({expires_in: 3601}), session({expires_in: -1}),
session({expires_in: 1.5}), session({role: 'root'}), session({username: 'x'.repeat(17)}),
new Response(' '.repeat(513)), new Response(Uint8Array.of(255)), json(null)]) {
const b = browser(); b.queues['/api/session'].push(response); b.start(); await tick();
assert.equal(b.sockets.length, 0); assert.equal(b.calls.length, 1); assert.ok(b.calls[0].signal.aborted);
}
const b = browser(); b.queues['/api/session'].push(o => new Promise((_, reject) => o.signal.addEventListener('abort', () => reject(new Error('timeout')))));
b.start(); b.fire(15000); await tick(); b.fire(1000); await tick(); assert.equal(b.calls[1].url, '/api/session');
const c = browser(); c.queues['/api/session'].push(session({expires_in: 2})); c.start(); await tick();
const expiry = [...c.timers.values()].find(timer => timer.ms >= 0 && timer.ms <= 2000);
assert.ok(expiry); c.fire(expiry.ms); assert.deepEqual(c.redirects, ['/login']);
});
await test('late body completions and superseded restore session are ignored', async () => {
for (const path of ['/api/session', '/api/ws-ticket', '/api/status']) {
let stream;
const b = browser(); b.queues[path].push(new Response(new ReadableStream({start(c) { stream = c; }})));
b.start(); await tick(); b.emit('pagehide');
const text = path === '/api/session' ? {username: 'late', role: 'admin', csrf: token, expires_in: 3600} :
path === '/api/ws-ticket' ? {ticket: 't'.repeat(32)} : {wifi: {available: true, state: 'LATE'}};
stream.enqueue(new TextEncoder().encode(JSON.stringify(text))); stream.close(); await tick();
assert.ok(b.sockets.every(socket => socket.closed)); assert.deepEqual(b.redirects, []); assert.equal(b.timers.size, 0);
assert.ok(!b.nodes['wifi-summary'].textContent.includes('LATE'));
}
const b = await connected(); b.click('connection-toggle'); b.emit('pagehide');
const d = deferred(); b.queues['/api/session'].push(d.promise);
b.emit('pageshow', {persisted: true}); await tick(); b.click('connection-toggle'); await tick();
d.resolve(failure(401)); await tick(); assert.deepEqual(b.redirects, []); assert.equal(b.sockets.length, 2);
});
await test('inline asset failures: 401 login, offline usable fallback, pagehide and shared navigation guard', async () => {
for (const status of [401, 503]) {
const b = browser({onlyLoader: true}); b.queues['/api/session'].push(failure(status));
b.emit('error', {target: {tagName: 'SCRIPT'}}); await tick();
assert.deepEqual(b.redirects, status === 401 ? ['/login'] : []); assert.equal(b.timers.size, 0);
b.emit('error', {target: {tagName: 'SCRIPT'}}); assert.equal(b.calls.length, 1);
}
const b = browser({onlyLoader: true}), d = deferred(); b.queues['/api/session'].push(d.promise);
b.emit('error', {target: {tagName: 'LINK'}}); b.emit('pagehide'); d.resolve(failure(401)); await tick(); assert.deepEqual(b.redirects, []);
const c = browser({withLoader: true}); c.start(); await tick();
c.queues['/api/session'].push(failure(401)); c.emit('error', {target: {tagName: 'IMG'}}); await tick();
assert.deepEqual(c.redirects, ['/login']); assert.ok(c.sockets[0].closed); assert.equal(c.timers.size, 0);
});
async function adminBrowser() {
const b = browser({role: 'admin'}); b.queues['/api/session'].push(session({role: 'admin'}));
b.start(); await tick(); const ws = b.sockets[0]; ws.emit('open');
ws.emit('message', {data: JSON.stringify({type: 'hello', clientId: 8, writerId: 8, role: 'writer'})});
b.click('select-admin'); b.click('admin-toggle'); await tick();
assert.equal(b.sockets.length, 2); b.sockets[1].emit('open'); return b;
}
await test('ordinary user is serial-only; admin selection preserves socket/lease and isolates input/output', async () => {
const u = await connected(); assert.equal(u.nodes['terminal-selector'].hidden, true);
u.click('select-admin'); u.click('admin-toggle'); await tick(); assert.equal(u.terminals.length, 1); assert.equal(u.sockets.length, 1);
const b = await adminBrowser(), [serial, admin] = b.sockets, [st, at] = b.terminals;
assert.equal(admin.url, 'wss://sak.local/ws/admin?ticket=' + '0123456789abcdef'.repeat(4));
assert.equal(b.calls.find(c => c.url === '/api/admin/ws-ticket').headers['X-CSRF-Token'], token);
st.input('WRONG'); at.input('x'.repeat(1025)); assert.equal(serial.sent.length, 0);
assert.deepEqual(admin.sent.map(x => x.length), [512, 512, 1]);
serial.emit('message', {data: Uint8Array.of(0, 255).buffer});
admin.emit('message', {data: Uint8Array.of(27, 91).buffer});
assert.deepEqual(st.writes, [[0, 255]]); assert.deepEqual(at.writes, [[27, 91]]);
b.click('release-control'); assert.equal(serial.sent.at(-1), 'release-writer');
for (let i = 0; i < 20; ++i) { b.click('select-serial'); at.input('WRONG'); b.click('select-admin'); }
assert.equal(b.sockets.length, 2); assert.equal(b.terminals.length, 2); assert.ok(!serial.closed);
assert.equal(b.nodes['client-id'].textContent, '8'); assert.equal(b.nodes['writer-id'].textContent, '8');
assert.equal(b.nodes['release-control'].disabled, false); assert.equal(admin.sent.length, 3);
b.click('admin-toggle'); assert.ok(admin.closed); assert.equal(admin.onmessage, null); assert.ok(!serial.closed);
b.click('admin-toggle'); await tick(); const replacement = b.sockets[2]; replacement.emit('open'); replacement.emit('close');
assert.ok(!serial.closed); assert.equal(b.nodes['admin-toggle'].textContent, 'Open admin');
b.click('select-serial'); st.input('ok'); assert.deepEqual([...serial.sent.at(-1)], [111, 107]);
});
await test('admin rejects serial-format and malformed tickets before upgrade without disturbing serial', async () => {
for (const value of ['t'.repeat(32), 'a'.repeat(63), 'a'.repeat(65), 'g'.repeat(64), null]) {
const b = browser({role: 'admin'}); b.start(); await tick();
const serial = b.sockets[0]; serial.emit('open');
b.queues['/api/admin/ws-ticket'].push(json({ticket: value}));
b.click('select-admin'); b.click('admin-toggle'); await tick();
assert.equal(b.sockets.length, 1); assert.ok(!serial.closed);
assert.equal(b.nodes['admin-detail'].textContent, 'Admin connection failed. Open admin to retry.');
}
});
await test('bounded hidden output continues draining, separate scrollback and input overflow closes only admin', async () => {
const b = await adminBrowser(), [serial, admin] = b.sockets, [st, at] = b.terminals;
assert.equal(st.options.scrollback, 5000); assert.equal(at.options.scrollback, 5000);
st.holdWrites = true;
for (let i = 0; i < 65; ++i) serial.emit('message', {data: new Uint8Array(1024).buffer});
assert.equal(st.writes.length, 64); assert.match(b.nodes['output-detail'].textContent, /serial 1024 B, admin 0 B/);
st.pending.shift()(); serial.emit('message', {data: Uint8Array.of(42).buffer}); assert.equal(st.writes.at(-1)[0], 42);
at.input('x'.repeat(4097)); assert.ok(admin.closed); assert.ok(!serial.closed); assert.equal(admin.sent.length, 0);
});
await test('admin failures and cancellation are isolated; 401/logout/expiry/pagehide close both', async () => {
for (const status of [403, 503, 401]) {
const b = await adminBrowser(), serial = b.sockets[0]; b.click('admin-toggle');
b.queues['/api/admin/ws-ticket'].push(failure(status)); b.click('admin-toggle'); await tick();
assert.equal(!!serial.closed, status === 401);
assert.deepEqual(b.redirects, status === 401 ? ['/login'] : []);
assert.equal(b.sockets.length, 2);
}
for (const action of ['pagehide', 'expiry', 'logout']) {
const b = await adminBrowser();
if (action === 'pagehide') b.emit('pagehide');
if (action === 'expiry') b.window.sakSessionExpired();
if (action === 'logout') { b.queues['/api/logout'].push(new Response(null, {status: 204})); await b.click('sign-out'); }
await tick(); assert.ok(b.sockets.every(s => s.closed)); assert.equal(b.sockets[1].onmessage, null);
if (action === 'pagehide') {
b.queues['/api/session'].push(session({role: 'admin'})); b.emit('pageshow', {persisted: true}); await tick();
assert.equal(b.sockets.length, 3); assert.match(b.sockets[2].url, /\/ws\/serial/);
assert.equal(b.nodes['admin-toggle'].textContent, 'Open admin');
}
}
const b = await adminBrowser(); b.click('admin-toggle'); const d = deferred();
b.queues['/api/admin/ws-ticket'].push(d.promise); b.click('admin-toggle'); await tick();
b.click('admin-toggle'); d.resolve(failure(401)); await tick();
assert.deepEqual(b.redirects, []); assert.ok(!b.sockets[0].closed); assert.equal(b.sockets.length, 2);
});
await test('selected resize, listener cleanup, admin handshake deadline and stale callback fencing', async () => {
const b = await adminBrowser(), [serial, old] = b.sockets;
b.fire(-1); assert.equal(b.terminals[1].cols, 80); assert.equal(b.terminals[0].cols, undefined);
b.click('select-serial'); b.fire(-1); assert.equal(b.terminals[0].rows, 24);
const stale = old.onmessage;
b.click('select-admin'); b.click('admin-toggle'); b.click('admin-toggle'); await tick();
stale({data: Uint8Array.of(99).buffer}); assert.equal(b.terminals[1].writes.length, 0);
b.fire(15000); assert.ok(b.sockets[2].closed); assert.ok(!serial.closed);
assert.match(b.nodes['admin-detail'].textContent, /timed out/);
for (let i = 0; i < 3; ++i) {
b.emit('pagehide'); assert.equal(b.events.resize.length, 0);
assert.ok(b.sockets.every(s => s.onmessage === null));
b.queues['/api/session'].push(session({role: 'admin'}));
b.emit('pageshow', {persisted: true}); await tick(); assert.equal(b.events.resize.length, 1);
}
b.window.sakSessionExpired(); assert.equal(b.events.resize.length, 0);
});
await test('changed session identity replaces document before adoption, including live admin and paused restore', async () => {
const identities = [
{username: 'other-admin', role: 'admin', csrf: 'b'.repeat(64)},
{username: 'other-user', role: 'user', csrf: 'b'.repeat(64)},
{role: 'admin', csrf: 'b'.repeat(64)}, // Same account, different login session.
{username: 'other-admin', role: 'admin'}, // Principal fields are checked independently.
{role: 'user'}
];
for (const identity of identities) for (const route of ['restore', 'paused-restore', 'live', 'logout']) {
const b = await adminBrowser(), [serial, admin] = b.sockets;
serial.emit('message', {data: Uint8Array.of(65).buffer});
admin.emit('message', {data: Uint8Array.of(66).buffer});
const stale = admin.onmessage, info = b.nodes['session-info'].textContent;
const count = b.calls.length;
b.queues['/api/session'].push(session(identity));
if (route.includes('restore')) {
if (route === 'paused-restore') b.click('connection-toggle');
b.emit('pagehide');
assert.equal(b.nodes.terminal.hidden, true); assert.equal(b.nodes['admin-terminal'].hidden, true);
b.emit('pageshow', {persisted: true}); b.click('select-admin');
assert.equal(b.nodes['admin-terminal'].hidden, true);
} else if (route === 'live') {
b.click('connection-toggle'); assert.ok(!admin.closed); b.click('connection-toggle');
} else await b.click('sign-out');
await tick();
assert.deepEqual(b.redirects, ['/']); assert.ok(serial.closed && admin.closed);
assert.equal(b.nodes['session-info'].textContent, info); // B was never adopted into A's document.
assert.equal(b.nodes.terminal.hidden, true); assert.equal(b.nodes['admin-terminal'].hidden, true);
assert.deepEqual(b.calls.slice(count).map(c => c.url), ['/api/session']);
assert.equal(b.timers.size, 0); assert.equal(b.events.resize.length, 0);
stale({data: Uint8Array.of(67).buffer}); b.terminals[1].input('WRONG');
b.click('select-admin'); b.click('admin-toggle'); b.emit('pageshow', {persisted: true});
assert.deepEqual(b.terminals[1].writes, [[66]]); assert.equal(admin.sent.length, 0);
assert.equal(b.nodes['admin-terminal'].hidden, true); assert.equal(b.sockets.length, 2);
}
});
await test('same-session restore preserves both scrollbacks; pending validation never reveals them', async () => {
for (const paused of [false, true]) {
const b = await adminBrowser();
b.sockets[0].emit('message', {data: Uint8Array.of(65).buffer});
b.sockets[1].emit('message', {data: Uint8Array.of(66).buffer});
if (paused) b.click('connection-toggle');
b.emit('pagehide'); const d = deferred(); b.queues['/api/session'].push(d.promise);
b.emit('pageshow', {persisted: true}); await tick(); b.click('select-serial'); b.click('select-admin');
assert.equal(b.nodes.terminal.hidden, true); assert.equal(b.nodes['admin-terminal'].hidden, true);
d.resolve(session({role: 'admin', expires_in: 3500})); await tick();
assert.deepEqual(b.redirects, []); assert.equal(b.terminals.length, 2);
assert.deepEqual(b.terminals.map(t => t.writes), [[[65]], [[66]]]);
assert.equal(b.nodes['admin-terminal'].hidden, false); assert.equal(b.sockets.length, paused ? 2 : 3);
}
const b = await adminBrowser(), admin = b.sockets[1];
admin.emit('message', {data: Uint8Array.of(66).buffer});
b.click('connection-toggle'); b.click('connection-toggle'); await tick();
assert.ok(!admin.closed); assert.deepEqual(b.redirects, []); assert.deepEqual(b.terminals[1].writes, [[66]]);
});
await test('undefined first fit retries unchanged bounds, stops after three retries and fences stale work', async () => {
const b = await adminBrowser(), fit = b.fits[1];
fit.measurements.push(undefined); b.fire(-1);
assert.equal(b.terminals[1].cols, undefined); b.fire(-1);
assert.equal(b.terminals[1].cols, 80); assert.equal(fit.calls, 2);
b.emit('resize'); b.fire(-1); assert.equal(fit.calls, 2); // Successful measurement is cached.
b.click('select-serial'); b.click('select-admin');
fit.measurements.push(undefined, undefined, undefined, undefined);
for (let i = 0; i < 4; ++i) b.fire(-1);
assert.equal(fit.calls, 6); assert.ok(![...b.timers.values()].some(t => t.ms === -1));
b.emit('resize'); b.fire(-1); assert.equal(fit.calls, 7); // Failed bounds were never cached.
for (const action of ['pagehide', 'expiry', 'logout']) {
const c = await adminBrowser(); c.fits[1].measurements.push(undefined); c.fire(-1);
const stale = [...c.timers.values()].find(t => t.ms === -1).fn;
if (action === 'pagehide') c.emit('pagehide');
if (action === 'expiry') c.window.sakSessionExpired();
if (action === 'logout') { c.queues['/api/logout'].push(failure(403)); await c.click('sign-out'); }
await tick(); stale(); assert.equal(c.fits[1].calls, 1);
assert.ok(![...c.timers.values()].some(t => t.ms === -1));
if (action === 'pagehide') {
c.emit('pageshow', {persisted: true}); await tick(); stale();
assert.equal(c.fits[1].calls, 1); c.fire(-1); assert.equal(c.fits[1].calls, 2);
}
}
});
await test('Settings is admin-only, read-only, bounded and preserves both sockets, lease and hidden output', async () => {
const u = await connected(); u.click('select-settings'); u.click('refresh-settings'); await tick();
assert.ok(!u.calls.some(c => c.url === '/api/settings/serial'));
const b = await adminBrowser(), [serial, admin] = b.sockets;
assert.ok(!b.calls.some(c => c.url === '/api/settings/serial'));
for (let i = 0; i < 10; ++i) {
b.click('select-settings'); await tick();
assert.equal(b.nodes['serial-settings'].hidden, false);
assert.equal(b.nodes.terminal.hidden, true); assert.equal(b.nodes['admin-terminal'].hidden, true);
assert.equal(b.nodes['settings-values'].hidden, false);
assert.equal(b.nodes['setting-baud'].textContent, '230400');
assert.equal(b.nodes['setting-dtr'].textContent, 'on-connect');
assert.equal(b.nodes['setting-running'].textContent, 'Running');
b.terminals.forEach(t => { assert.equal(t.options.disableStdin, true); t.input('WRONG'); });
serial.emit('message', {data: Uint8Array.of(65).buffer}); admin.emit('message', {data: Uint8Array.of(66).buffer});
b.click('select-serial'); b.click('select-admin');
}
assert.equal(b.sockets.length, 2); assert.ok(!serial.closed && !admin.closed);
assert.equal(serial.sent.length, 0); assert.equal(admin.sent.length, 0);
assert.equal(b.nodes['client-id'].textContent, '8'); assert.equal(b.nodes['writer-id'].textContent, '8');
assert.equal(b.nodes['release-control'].disabled, false);
assert.deepEqual(b.terminals.map(t => t.writes.length), [10, 10]);
const reads = b.calls.filter(c => c.url === '/api/settings/serial');
assert.equal(reads.length, 10); assert.ok(reads.every(c => c.method === 'GET' && c.body === undefined));
b.queues['/api/settings/serial'].push(json(serialSettings({running: false, baud: 110, data_bits: '7', parity: 'odd', stop_bits: '2', flow: 'none', dtr: 'inactive', rts_threshold: 1})));
b.click('select-settings'); await tick(); assert.equal(b.nodes['setting-running'].textContent, 'Stopped');
b.click('refresh-settings'); await tick(); assert.equal(b.nodes['setting-running'].textContent, 'Running');
});
await test('Settings rejects malformed/oversized schemas, contains errors, and retries only explicitly', async () => {
for (const response of [json(null), json(serialSettings({secret: 'bad'})), json(serialSettings({baud: 1000001})),
json(serialSettings({running: 1})), json(serialSettings({parity: '
'})), json(serialSettings({rts_threshold: 0})),
new Response(' '.repeat(257)), new Response(Uint8Array.of(255)), failure(400), failure(403), failure(404), failure(429), failure(503)]) {
const b = await adminBrowser(); b.queues['/api/settings/serial'].push(response);
b.click('select-settings'); await tick();
assert.equal(b.nodes['settings-values'].hidden, true); assert.equal(b.nodes['setting-baud'].textContent, '');
assert.match(b.nodes['settings-detail'].textContent, /Refresh to retry/);
assert.ok(!b.nodes['settings-detail'].textContent.includes('SECRET'));
assert.equal(b.nodes['refresh-settings'].disabled, false); assert.ok(b.sockets.every(s => !s.closed));
assert.equal(b.calls.filter(c => c.url === '/api/settings/serial').length, 1);
b.click('refresh-settings'); await tick(); assert.equal(b.nodes['settings-values'].hidden, false);
}
const b = await adminBrowser();
b.queues['/api/settings/serial'].push(o => new Promise((_, reject) => o.signal.addEventListener('abort', () => reject(new Error('SECRET timeout')))));
b.click('select-settings'); await tick(); b.click('refresh-settings');
assert.equal(b.calls.filter(c => c.url === '/api/settings/serial').length, 1);
b.fire(15000); await tick(); assert.match(b.nodes['settings-detail'].textContent, /Refresh to retry/);
assert.ok(b.sockets.every(s => !s.closed));
});
await test('Settings cancellation fences late replies; session change, 401, logout and restore clear the view', async () => {
for (const action of ['switch', 'pagehide', 'expiry', 'logout']) {
const b = await adminBrowser(), d = deferred(); b.queues['/api/settings/serial'].push(d.promise);
b.click('select-settings'); await tick(); const call = b.calls.find(c => c.url === '/api/settings/serial');
if (action === 'switch') b.click('select-serial');
if (action === 'pagehide') b.emit('pagehide');
if (action === 'expiry') b.window.sakSessionExpired();
if (action === 'logout') { b.queues['/api/logout'].push(new Response(null, {status: 204})); await b.click('sign-out'); }
assert.ok(call.signal.aborted); d.resolve(failure(401)); await tick();
assert.equal(b.nodes['serial-settings'].hidden, true); assert.equal(b.nodes['setting-baud'].textContent, '');
if (action === 'switch') { assert.deepEqual(b.redirects, []); assert.ok(b.sockets.every(s => !s.closed)); }
if (action === 'pagehide') {
b.emit('pageshow', {persisted: true}); await tick();
assert.deepEqual(b.redirects, []); assert.equal(b.nodes['settings-values'].hidden, true);
b.click('refresh-settings'); await tick(); assert.equal(b.nodes['settings-values'].hidden, false);
}
}
for (const change of ['401', 'identity']) {
const b = await adminBrowser();
if (change === '401') b.queues['/api/settings/serial'].push(failure(401));
else b.queues['/api/session'].push(session({role: 'admin', csrf: 'b'.repeat(64)}));
b.click('select-settings'); await tick();
assert.deepEqual(b.redirects, [change === '401' ? '/login' : '/']);
assert.ok(b.sockets.every(s => s.closed)); assert.equal(b.nodes['serial-settings'].hidden, true);
assert.equal(b.nodes['setting-baud'].textContent, '');
}
});
await test('Settings session validation never strands concurrent serial reconnect; newer admission fences old settings checks', async () => {
const b = await adminBrowser(), d = deferred();
b.queues['/api/session'].push(d.promise); b.sockets[0].emit('close'); b.fire(1000); await tick();
b.click('select-settings'); await tick(); assert.equal(b.nodes['settings-values'].hidden, false);
d.resolve(session({role: 'admin'})); await tick();
assert.equal(b.sockets.length, 3); assert.ok(!b.sockets[1].closed);
const c = await adminBrowser(), old = deferred(); c.queues['/api/session'].push(old.promise);
c.click('select-settings'); await tick(); c.sockets[0].emit('close'); c.fire(1000); await tick();
old.resolve(session({role: 'admin'})); await tick();
assert.equal(c.sockets.length, 3); assert.ok(!c.sockets[1].closed);
assert.equal(c.nodes['refresh-settings'].disabled, false);
assert.match(c.nodes['settings-detail'].textContent, /Refresh to retry/);
c.click('refresh-settings'); await tick(); assert.equal(c.nodes['settings-values'].hidden, false);
});
await test('Serial typed actions: draft, confirmation, CSRF, explicit results and working/persisted semantics', async () => {
const b = browser({role: 'admin'}); b.start(); await tick(); b.sockets[0].emit('open');
b.click('select-settings'); await tick();
const path = '/api/settings/serial-operation';
assert.equal(b.nodes['edit-baud'].value, '230400');
const baseline = b.calls.length;
b.nodes['edit-baud'].value = '460800'; assert.equal(b.calls.length, baseline);
b.window.confirm = () => false; b.click('serial-reset'); await tick(); assert.equal(b.calls.length, baseline);
b.window.confirm = () => true;
for (const [i, action] of ['apply', 'save', 'load', 'defaults', 'reset', 'start', 'stop'].entries()) {
if (i) { b.click('refresh-settings'); await tick(); }
b.queues[path].push(json({id: i + 1, action, state: 'pending'}));
b.click('serial-' + action); await tick();
const post = b.calls.filter(c => c.url === path && c.method === 'POST').at(-1);
assert.equal(post.headers['X-CSRF-Token'], token); assert.equal(post.headers['Content-Type'], 'application/json');
assert.ok(Buffer.byteLength(post.body) <= 256);
const payload = JSON.parse(post.body); assert.equal(payload.action, action);
if (action === 'apply') {
assert.equal(payload.baud, 460800); assert.equal(payload.rts_threshold, 96); assert.equal(Object.keys(payload).length, 8);
} else assert.deepEqual(payload, {action});
assert.ok(b.nodes['serial-apply'].disabled && b.nodes['serial-result'].disabled);
assert.ok(!b.nodes['serial-edit'].hidden && !b.nodes['settings-values'].hidden);
assert.match(b.nodes['settings-detail'].textContent, /stale/);
const count = b.calls.length; b.click('serial-save'); await tick(); assert.equal(b.calls.length, count);
b.queues[path].push(json({id: i + 1, action, state: 'ok'})); b.fire(1000); await tick();
assert.ok(!b.nodes['serial-apply'].disabled && !b.nodes['serial-edit'].hidden);
assert.match(b.nodes['serial-operation-detail'].textContent, /completed.*RAM.*NVS/);
}
assert.equal(b.sockets.length, 1); assert.ok(!b.sockets[0].closed && !b.sockets[0].sent.length);
});
await test('Serial validation and capacity failures never auto-retry or transmit command strings', async () => {
const b = browser({role: 'admin'}); b.start(); await tick(); b.click('select-settings'); await tick();
const path = '/api/settings/serial-operation';
for (const [key, value] of [['baud', '0'], ['baud', '1000001'], ['baud', '1e3'], ['baud', '-1'], ['parity', 'mark'], ['rts_threshold', '128']]) {
b.click('refresh-settings'); await tick(); b.nodes['edit-' + key].value = value;
const count = b.calls.length; b.click('serial-apply'); await tick(); assert.equal(b.calls.length, count);
}
b.click('refresh-settings'); await tick();
for (const status of [400, 403, 429, 503]) {
b.queues[path].push(failure(status)); b.click('serial-save'); await tick();
assert.match(b.nodes['serial-operation-detail'].textContent, /Check Result/);
const count = b.calls.length; await tick(); b.click('serial-save'); await tick(); assert.equal(b.calls.length, count);
assert.ok(!b.nodes['serial-operation-detail'].textContent.includes('SECRET'));
b.queues[path].push(json({id: 0, action: 'none', state: 'idle'})); b.click('serial-result'); await tick();
b.click('refresh-settings'); await tick();
}
assert.equal(b.calls.filter(c => c.url === path && c.method === 'POST').length, 4);
b.queues[path].push(json({id: 1, action: 'save', state: 'pending'})); b.click('serial-save'); await tick();
b.queues[path].push(json({id: 1, action: 'save', state: 'ok'})); b.fire(1000); await tick();
b.click('refresh-settings'); await tick();
b.queues[path].push(() => { throw new Error('lost'); }); b.click('serial-reset'); await tick();
b.queues[path].push(json({id: 1, action: 'save', state: 'ok'})); b.click('serial-result'); await tick();
assert.match(b.nodes['serial-operation-detail'].textContent, /acknowledgement was lost.*earlier operation/);
assert.ok(b.sockets.every(s => !s.closed));
});
await test('Serial navigation fences delayed acknowledgement without losing sockets, lease or uncertain-work gate', async () => {
const b = browser({role: 'admin'}); b.start(); await tick(); const serial = b.sockets[0]; serial.emit('open');
serial.emit('message', {data: JSON.stringify({type: 'hello', clientId: 8, writerId: 8, role: 'writer'})});
b.click('select-admin'); b.click('admin-toggle'); await tick(); b.sockets[1].emit('open');
b.click('select-settings'); await tick(); const d = deferred(), path = '/api/settings/serial-operation';
b.queues[path].push(d.promise); b.click('serial-reset'); await tick();
const post = b.calls.find(c => c.url === path); b.click('select-serial'); assert.ok(post.signal.aborted);
d.resolve(json({id: 44, action: 'reset', state: 'pending'})); await tick();
b.click('select-settings'); await tick(); assert.ok(b.nodes['serial-reset'].disabled);
b.queues[path].push(json({id: 44, action: 'reset', state: 'ok'})); b.click('serial-result'); await tick();
assert.match(b.nodes['serial-operation-detail'].textContent, /completed/);
assert.equal(b.sockets.length, 2); assert.ok(b.sockets.every(s => !s.closed));
assert.equal(b.nodes['client-id'].textContent, '8'); assert.equal(b.nodes['writer-id'].textContent, '8');
assert.deepEqual(serial.sent, []);
});
await test('Serial result bounds, failure explanations, timeout and no result replay', async () => {
const b = browser({role: 'admin'}); b.start(); await tick(); b.click('select-settings'); await tick();
const path = '/api/settings/serial-operation';
for (const state of ['loaded_defaults', 'failed', 'rollback_failed', 'cancelled', 'pending']) {
b.queues[path].push(json({id: 42, action: 'reset', state})); b.click('serial-result'); await tick();
assert.ok(!b.nodes['serial-operation-detail'].textContent.includes('unknown. Check'));
}
for (const response of [new Response(' '.repeat(97)), json({id: 0, action: 'save', state: 'ok'}),
json({id: 42, action: 'save', state: 'ok', extra: 1}), json({id: 42, action: '
', state: 'ok'}),
json({id: -1, action: 'none', state: 'idle'}), new Response(Uint8Array.of(255))]) {
b.queues[path].push(response); b.click('serial-result'); await tick();
assert.match(b.nodes['serial-operation-detail'].textContent, /outcome unknown/);
}
b.queues[path].push(o => new Promise((_, reject) => o.signal.addEventListener('abort', () => reject(new Error('timeout')))));
b.click('serial-result'); await tick(); b.fire(15000); await tick();
assert.match(b.nodes['serial-operation-detail'].textContent, /No automatic retry/);
assert.equal(b.calls.filter(c => c.url === path && c.method === 'POST').length, 0);
});
await test('Serial mutation security: current-session identity, 401 and pagehide cancel work safely', async () => {
for (const mode of ['identity', '401', 'pagehide']) {
const b = browser({role: 'admin'}); b.start(); await tick(); b.click('select-settings'); await tick();
const path = '/api/settings/serial-operation', d = deferred();
if (mode === 'identity') b.queues['/api/session'].push(session({role: 'admin', username: 'replacement'}));
else b.queues[path].push(mode === '401' ? failure(401) : d.promise);
b.click('serial-save'); await tick();
if (mode === 'identity') { assert.deepEqual(b.redirects, ['/']); assert.equal(b.calls.filter(c => c.url === path).length, 0); }
if (mode === '401') assert.deepEqual(b.redirects, ['/login']);
if (mode === 'pagehide') {
b.emit('pagehide'); const text = b.nodes['serial-operation-detail'].textContent;
d.resolve(json({id: 42, action: 'save', state: 'pending'})); await tick();
assert.equal(b.nodes['serial-operation-detail'].textContent, text);
}
assert.ok(b.sockets.every(s => s.closed) && b.nodes['serial-settings'].hidden);
}
});
await test('Serial uncertain outcomes survive repeated result reads, refresh and navigation', async () => {
for (const lostAck of [true, false]) {
const b = await adminBrowser(), path = '/api/settings/serial-operation';
b.click('select-settings'); await tick();
b.queues[path].push(lostAck ? () => { throw new Error('lost'); } : json({id: 41, action: 'save', state: 'pending'}));
b.click('serial-save'); await tick();
const warning = lostAck ? /acknowledgement was lost/ : /Previous result was replaced.*unknown/;
for (let i = 0; i < 2; ++i) {
b.queues[path].push(json({id: 42, action: 'reset', state: 'ok'}));
if (!lostAck && i === 0) b.fire(1000); else b.click('serial-result');
await tick();
assert.match(b.nodes['serial-operation-detail'].textContent, warning);
}
b.queues[path].push(failure(503)); b.click('serial-result'); await tick();
assert.match(b.nodes['serial-operation-detail'].textContent, warning);
assert.match(b.nodes['serial-operation-detail'].textContent, /No automatic retry/);
b.click('refresh-settings'); await tick();
assert.match(b.nodes['serial-operation-detail'].textContent, warning);
b.click('select-serial'); b.click('select-settings'); await tick();
assert.match(b.nodes['serial-operation-detail'].textContent, warning);
b.queues[path].push(json({id: 42, action: 'reset', state: 'ok'}));
b.click('serial-result'); await tick();
assert.match(b.nodes['serial-operation-detail'].textContent, warning);
b.click('refresh-settings'); await tick();
b.queues[path].push(json({id: 43, action: 'save', state: 'pending'}));
b.click('serial-save'); await tick();
assert.doesNotMatch(b.nodes['serial-operation-detail'].textContent, warning);
assert.equal(b.calls.filter(c => c.url === path && c.method === 'POST').length, 2);
assert.equal(b.sockets.length, 2); assert.ok(b.sockets.every(s => !s.closed));
assert.ok(b.sockets.every(s => !s.sent.length));
}
});
await test('Automatic checks: pending then completion refreshes working config, retains outcome and isolates sockets', async () => {
const b = await adminBrowser(), path = '/api/settings/serial-operation';
b.click('select-settings'); await tick();
const before = b.calls.filter(c => c.url === '/api/settings/serial').length;
b.queues[path].push(json({id: 50, action: 'apply', state: 'pending'}));
b.nodes['edit-baud'].value = '460800'; b.click('serial-apply');
assert.match(b.nodes['serial-operation-detail'].textContent, /Applying/); await tick();
for (const state of ['pending', 'pending', 'ok']) {
assert.ok(b.nodes['edit-baud'].disabled && b.nodes['serial-stop'].disabled);
assert.ok(!b.nodes['settings-values'].hidden && !b.nodes['serial-edit'].hidden);
b.queues[path].push(json({id: 50, action: 'apply', state}));
if (state === 'ok') b.queues['/api/settings/serial'].push(json({...serialSettings(), baud: 460800}));
b.elapse(1000); b.fire(1000); await tick();
}
assert.equal(b.calls.filter(c => c.url === path && c.method === 'GET').length, 3);
assert.equal(b.calls.filter(c => c.url === path && c.method === 'POST').length, 1);
assert.equal(b.calls.filter(c => c.url === '/api/settings/serial').length, before + 1);
assert.equal(b.nodes['setting-baud'].textContent, '460800');
assert.match(b.nodes['serial-operation-detail'].textContent, /completed/);
assert.doesNotMatch(b.nodes['settings-detail'].textContent, /stale/);
assert.ok(!b.nodes['edit-baud'].disabled && !b.nodes['serial-apply'].disabled);
assert.ok(![...b.timers.values()].some(t => t.ms === 1000 || t.ms === 15000));
assert.equal(b.sockets.length, 2); assert.ok(b.sockets.every(s => !s.closed && !s.sent.length));
});
await test('Repeated current Settings selection preserves submission, polling and completion refresh', async () => {
const b = await adminBrowser(), path = '/api/settings/serial-operation';
b.click('select-settings'); await tick(); b.nodes['edit-baud'].value = '460800';
const repeatedSelection = async request => {
const count = b.calls.length, timers = [...b.timers];
const view = () => Object.fromEntries(Object.entries(b.nodes).map(([id, node]) =>
[id, [node.hidden, node.disabled, node.value, node.textContent, node['aria-pressed']]]));
const before = view();
for (let i = 0; i < 3; ++i) { b.click('select-settings'); await tick(); }
assert.equal(b.calls.length, count);
assert.deepEqual([...b.timers], timers);
assert.deepEqual(view(), before);
if (request) assert.ok(!request.signal.aborted);
assert.ok(!b.nodes['serial-settings'].hidden && !b.nodes['settings-values'].hidden && !b.nodes['serial-edit'].hidden);
};
const post = deferred(); b.queues[path].push(post.promise);
b.click('serial-apply'); await tick();
assert.match(b.nodes['serial-operation-detail'].textContent, /Applying/);
await repeatedSelection(b.calls.filter(c => c.url === path).at(-1));
post.resolve(json({id: 57, action: 'apply', state: 'pending'})); await tick();
await repeatedSelection();
const pending = deferred(); b.queues[path].push(pending.promise);
b.fire(1000); await tick();
await repeatedSelection(b.calls.filter(c => c.url === path).at(-1));
pending.resolve(json({id: 57, action: 'apply', state: 'pending'})); await tick();
await repeatedSelection();
const refresh = deferred(); b.queues['/api/settings/serial'].push(refresh.promise);
b.queues[path].push(json({id: 57, action: 'apply', state: 'ok'}));
b.fire(1000); await tick();
assert.match(b.nodes['serial-operation-detail'].textContent, /completed/);
assert.match(b.nodes['settings-detail'].textContent, /Reading.*stale/);
await repeatedSelection(b.calls.filter(c => c.url === '/api/settings/serial').at(-1));
refresh.resolve(json({...serialSettings(), baud: 460800})); await tick();
assert.equal(b.nodes['setting-baud'].textContent, '460800');
assert.equal(b.nodes['edit-baud'].value, '460800');
assert.ok(!b.nodes['serial-apply'].disabled);
assert.match(b.nodes['serial-operation-detail'].textContent, /completed/);
assert.doesNotMatch(b.nodes['settings-detail'].textContent, /stale/);
await repeatedSelection();
assert.equal(b.calls.filter(c => c.url === path && c.method === 'POST').length, 1);
assert.equal(b.calls.filter(c => c.url === path && c.method === 'GET').length, 2);
assert.equal(b.calls.filter(c => c.url === '/api/settings/serial').length, 2);
assert.ok(![...b.timers.values()].some(t => t.ms === 1000 || t.ms === 15000));
assert.equal(b.sockets.length, 2); assert.ok(b.sockets.every(s => !s.closed && !s.sent.length));
assert.equal(b.nodes['client-id'].textContent, '8'); assert.equal(b.nodes['writer-id'].textContent, '8');
});
await test('Ten automatic GET attempts exhaust budget; manual recovery completes without POST retry', async () => {
const b = await adminBrowser(), path = '/api/settings/serial-operation';
b.click('select-settings'); await tick();
b.queues[path].push(json({id: 51, action: 'save', state: 'pending'})); b.click('serial-save'); await tick();
for (let i = 0; i < 10; ++i) {
b.queues[path].push(json({id: 51, action: 'save', state: 'pending'}));
b.elapse(1000); b.fire(1000); await tick();
}
assert.equal(b.calls.filter(c => c.url === path && c.method === 'GET').length, 10);
assert.ok(![...b.timers.values()].some(t => t.ms === 1000 || t.ms === 15000));
assert.match(b.nodes['serial-operation-detail'].textContent, /Automatic checking stopped.*uncertain.*Check Result/);
assert.ok(b.nodes['serial-save'].disabled && !b.nodes['serial-result'].disabled);
assert.match(b.nodes['settings-detail'].textContent, /stale/);
b.queues[path].push(json({id: 51, action: 'save', state: 'ok'})); b.click('serial-result'); await tick();
assert.match(b.nodes['serial-operation-detail'].textContent, /completed/);
assert.ok(!b.nodes['serial-save'].disabled);
assert.equal(b.calls.filter(c => c.url === path && c.method === 'POST').length, 1);
});
await test('15s overall deadline aborts slow checks and fences late bodies and delayed timers', async () => {
for (const mode of ['fetch', 'body', 'delayed-timer', 'late-response']) {
const b = await adminBrowser(), path = '/api/settings/serial-operation';
b.click('select-settings'); await tick();
b.queues[path].push(json({id: 52, action: 'stop', state: 'pending'})); b.click('serial-stop'); await tick();
const d = deferred(); let stream;
if (mode !== 'delayed-timer') {
b.queues[path].push(mode === 'body' ? new Response(new ReadableStream({start(c) { stream = c; }})) : d.promise);
b.elapse(1000); b.fire(1000); await tick(); b.elapse(14000);
if (mode === 'late-response') { d.resolve(json({id: 52, action: 'stop', state: 'ok'})); await tick(); }
else b.fire(15000);
assert.ok(b.calls.filter(c => c.url === path).at(-1).signal.aborted);
} else { b.elapse(15000); b.fire(1000); }
await tick();
const text = b.nodes['serial-operation-detail'].textContent;
assert.match(text, /Automatic checking stopped/);
assert.ok(!b.nodes['serial-result'].disabled);
b.queues[path].push(json({id: 52, action: 'stop', state: 'ok'})); b.click('serial-result'); await tick();
const recovered = b.nodes['serial-operation-detail'].textContent;
if (stream) { stream.enqueue(new TextEncoder().encode(JSON.stringify({id: 52, action: 'stop', state: 'failed'}))); stream.close(); }
else d.resolve(json({id: 52, action: 'stop', state: 'failed'}));
await tick(); assert.equal(b.nodes['serial-operation-detail'].textContent, recovered);
assert.match(recovered, /completed/);
assert.equal(b.calls.filter(c => c.url === path && c.method === 'POST').length, 1);
}
});
await test('Known terminal outcomes always refresh; failed refresh preserves visible stale snapshot and outcome', async () => {
for (const state of ['ok', 'failed', 'rollback_failed', 'cancelled', 'loaded_defaults']) {
const b = await adminBrowser(), path = '/api/settings/serial-operation';
b.click('select-settings'); await tick();
b.queues[path].push(json({id: 53, action: 'reset', state: 'pending'})); b.click('serial-reset'); await tick();
const d = deferred(); b.queues['/api/settings/serial'].push(d.promise);
b.queues[path].push(json({id: 53, action: 'reset', state})); b.fire(1000); await tick();
const outcome = b.nodes['serial-operation-detail'].textContent;
assert.ok(b.nodes['edit-baud'].disabled && !b.nodes['serial-edit'].hidden && !b.nodes['settings-values'].hidden);
assert.match(b.nodes['settings-detail'].textContent, /stale/);
d.resolve(failure(503)); await tick();
assert.equal(b.nodes['serial-operation-detail'].textContent, outcome);
assert.equal(b.nodes['setting-baud'].textContent, '230400');
assert.ok(!b.nodes['serial-edit'].hidden && !b.nodes['settings-values'].hidden && !b.nodes['refresh-settings'].disabled);
assert.match(b.nodes['settings-detail'].textContent, /stale.*Refresh/);
b.click('refresh-settings'); await tick();
assert.equal(b.nodes['serial-operation-detail'].textContent, outcome);
assert.doesNotMatch(b.nodes['settings-detail'].textContent, /stale/);
}
});
await test('Automatic timers and in-flight checks cancel on navigation, pagehide, logout and identity change', async () => {
for (const mode of ['navigation', 'pagehide', 'logout', 'identity', 'expiry']) for (const inFlight of [false, true]) {
const b = await adminBrowser(), path = '/api/settings/serial-operation';
b.click('select-settings'); await tick();
b.queues[path].push(json({id: 54, action: 'start', state: 'pending'})); b.click('serial-start'); await tick();
const callbacks = [...b.timers.values()].filter(t => t.ms === 1000 || t.ms === 15000).map(t => t.fn);
const d = deferred();
if (inFlight) { b.queues[path].push(d.promise); b.fire(1000); await tick(); }
if (mode === 'navigation') b.click('select-serial');
if (mode === 'pagehide') b.emit('pagehide');
if (mode === 'expiry') b.window.sakSessionExpired();
if (mode === 'logout') { b.queues['/api/logout'].push(new Response(null, {status: 204})); b.click('sign-out'); }
if (mode === 'identity') {
b.queues['/api/session'].push(session({role: 'admin', username: 'replacement'}));
b.click('connection-toggle'); b.click('connection-toggle');
}
await tick();
const text = b.nodes['serial-operation-detail'].textContent, count = b.calls.filter(c => c.url === path).length;
if (inFlight) assert.ok(b.calls.filter(c => c.url === path).at(-1).signal.aborted);
for (const callback of callbacks) callback();
d.resolve(json({id: 54, action: 'start', state: 'ok'})); await tick();
assert.equal(b.nodes['serial-operation-detail'].textContent, text);
assert.equal(b.calls.filter(c => c.url === path).length, count);
assert.ok(![...b.timers.values()].some(t => t.ms === 1000 || t.ms === 15000));
if (mode === 'navigation') { b.click('select-settings'); await tick(); assert.equal(b.calls.filter(c => c.url === path).length, count); }
if (mode === 'pagehide') { b.emit('pageshow', {persisted: true}); await tick(); assert.equal(b.calls.filter(c => c.url === path).length, count); }
}
});
await test('Automatic read errors stop checking; cancelled completion refresh cannot overwrite a newer view', async () => {
const path = '/api/settings/serial-operation';
for (const response of [failure(503), new Response(' '.repeat(97)), () => { throw new Error('network'); }]) {
const b = await adminBrowser(); b.click('select-settings'); await tick();
b.queues[path].push(json({id: 55, action: 'save', state: 'pending'}), response);
b.click('serial-save'); await tick(); b.fire(1000); await tick();
assert.match(b.nodes['serial-operation-detail'].textContent, /Check Result.*No automatic retry/);
assert.match(b.nodes['settings-detail'].textContent, /stale/);
assert.ok(!b.nodes['serial-result'].disabled && b.nodes['serial-save'].disabled);
assert.ok(![...b.timers.values()].some(t => t.ms === 1000 || t.ms === 15000));
assert.equal(b.calls.filter(c => c.url === path && c.method === 'POST').length, 1);
}
const b = await adminBrowser(); b.click('select-settings'); await tick();
const d = deferred(); b.queues['/api/settings/serial'].push(d.promise);
b.queues[path].push(json({id: 56, action: 'load', state: 'pending'}), json({id: 56, action: 'load', state: 'ok'}));
b.click('serial-load'); await tick(); b.fire(1000); await tick();
const outcome = b.nodes['serial-operation-detail'].textContent;
const read = b.calls.filter(c => c.url === '/api/settings/serial').at(-1);
b.click('select-serial'); assert.ok(read.signal.aborted);
b.click('select-settings'); await tick();
d.resolve(json({...serialSettings(), baud: 110})); await tick();
assert.equal(b.nodes['setting-baud'].textContent, '230400');
assert.equal(b.nodes['serial-operation-detail'].textContent, outcome);
});
await test('Routine actions never confirm; Reset cancellation has no request or state change', async () => {
const b = await adminBrowser(), path = '/api/settings/serial-operation'; b.click('select-settings'); await tick();
const confirms = []; b.window.confirm = message => { confirms.push(message); return false; };
for (const [i, action] of ['apply', 'start', 'stop', 'load', 'defaults', 'save'].entries()) {
b.queues[path].push(json({id: i + 1, action, state: 'pending'}), json({id: i + 1, action, state: 'ok'}));
b.click('serial-' + action); await tick(); b.fire(1000); await tick();
}
assert.deepEqual(confirms, []);
const count = b.calls.length, text = b.nodes['serial-operation-detail'].textContent;
b.click('serial-reset'); await tick(); assert.equal(b.calls.length, count);
assert.equal(b.nodes['serial-operation-detail'].textContent, text);
assert.equal(confirms.length, 1); assert.match(confirms[0], /overwrites saved NVS configuration/);
assert.equal(b.calls.filter(c => c.url === path && c.method === 'POST').length, 6);
});
console.log(`PASS ${passed} browser behavior groups (production C-rendered JS)`);
})().catch(error => { console.error(error); process.exitCode = 1; });