'use strict'; // A device row can vanish while its socket is still open — an operator deletes it, or a re-pair // replaces it. The next heartbeat carrying telemetry then failed the foreign key on // device_telemetry, and that throw was fatal in a way nobody would guess: // // the safe-socket wrapper treats a throwing handler as a broken one and disconnects the socket // SERVER-side -> socket.io does NOT auto-retry an 'io server disconnect' -> the player // sits there doing nothing until a human reloads the page. // // That is not theoretical. Deleting a device row mid-session took a real screen dark, and it // needed someone at the other end to press reload. A heartbeat for a device that no longer // exists is an ordinary race, not a fault worth ending a connection over. const { test, before, after } = require('node:test'); const assert = require('node:assert/strict'); const { spawn } = require('node:child_process'); const path = require('node:path'); const os = require('node:os'); const fs = require('node:fs'); const crypto = require('node:crypto'); const Database = require('better-sqlite3'); const ioClient = require('../node_modules/socket.io-client'); const { freePort } = require('./helpers/free-port'); let PORT, BASE, proc, db; const DATA_DIR = path.join(os.tmpdir(), 'st-hbdel-' + crypto.randomBytes(4).toString('hex')); const LOG = path.join(os.tmpdir(), 'st-hbdel-' + crypto.randomBytes(4).toString('hex') + '.log'); const TELEMETRY = { battery_level: 80, battery_charging: true, storage_free_mb: 100, storage_total_mb: 200, ram_free_mb: 50, ram_total_mb: 100, cpu_usage: 5, wifi_ssid: 'x', wifi_rssi: -50, uptime_seconds: 60 }; before(async () => { PORT = await freePort(); BASE = `http://127.0.0.1:${PORT}`; const logFd = fs.openSync(LOG, 'w'); proc = spawn('node', ['server.js'], { cwd: path.join(__dirname, '..'), env: { ...process.env, DATA_DIR, SELF_HOSTED: 'true', PORT: String(PORT), NODE_ENV: 'test' }, stdio: ['ignore', logFd, logFd], }); let up = false; for (let i = 0; i < 80; i++) { try { const r = await fetch(BASE + '/api/status'); if (r.ok) { up = true; break; } } catch { /* */ } await new Promise(r => setTimeout(r, 250)); } if (!up) throw new Error('server did not boot:\n' + fs.readFileSync(LOG, 'utf8').slice(-2000)); db = new Database(path.join(DATA_DIR, 'db', 'remote_display.db')); }); after(() => { try { db && db.close(); } catch { /* */ } try { proc.kill('SIGKILL'); } catch { /* */ } }); function makeDevice() { const id = crypto.randomUUID(); const token = crypto.randomBytes(32).toString('hex'); db.prepare(`INSERT INTO devices (id, name, status, device_token, created_at) VALUES (?, 'HB', 'online', ?, strftime('%s','now'))`).run(id, token); return { id, token }; } function connect(dev) { return new Promise((resolve, reject) => { const s = ioClient(BASE + '/device', { transports: ['websocket'], reconnection: false }); s.on('connect', () => s.emit('device:register', { device_id: dev.id, device_token: dev.token })); s.on('device:registered', () => resolve(s)); s.on('device:auth-error', (e) => reject(new Error(e && e.error))); setTimeout(() => reject(new Error('register timeout')), 10000); }); } const wait = (ms) => new Promise(r => setTimeout(r, ms)); test('a heartbeat for a device deleted mid-session does NOT drop the socket', async () => { const dev = makeDevice(); const s = await connect(dev); s.emit('device:heartbeat', { device_id: dev.id, telemetry: TELEMETRY }); await wait(400); assert.equal(s.connected, true, 'sanity: healthy heartbeat keeps the socket'); // The row disappears underneath the live socket. db.prepare('DELETE FROM devices WHERE id = ?').run(dev.id); s.emit('device:heartbeat', { device_id: dev.id, telemetry: TELEMETRY }); await wait(700); assert.equal(s.connected, true, 'the socket survives — before this, the FK threw, the server disconnected it, and socket.io ' + 'would not retry, so the screen stayed dark until someone reloaded it'); s.close(); }); test('and the server stays up rather than logging a foreign-key failure', async () => { const log = fs.readFileSync(LOG, 'utf8'); assert.doesNotMatch(log, /FOREIGN KEY constraint failed/, 'no constraint error was raised at all'); assert.doesNotMatch(log, /handler threw for/, 'and no handler was reported as broken'); }); test('telemetry for a LIVE device is still recorded', async () => { const dev = makeDevice(); const s = await connect(dev); s.emit('device:heartbeat', { device_id: dev.id, telemetry: TELEMETRY }); await wait(600); const n = db.prepare('SELECT COUNT(*) c FROM device_telemetry WHERE device_id = ?').get(dev.id).c; assert.ok(n >= 1, 'the guard must not have turned telemetry off for everyone'); s.close(); }); test('a heartbeat with no telemetry is unaffected', async () => { const dev = makeDevice(); const s = await connect(dev); s.emit('device:heartbeat', { device_id: dev.id }); await wait(400); assert.equal(s.connected, true); s.close(); });