// The sign-in modal must interrupt for a message a PERSON wrote, and never for a // system notice. Regression for the 2026-09-18 report: a member viewing daily ads got a // fresh popup after every single view, because every payout notice was stored as a // 'broadcast' and loadDashboard() re-pops on each refresh. // // node qa/messages-notice.mjs (JSON mode, no DB needed) import { createRequire } from 'node:module'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; const require = createRequire(import.meta.url); const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'iap-msg-')); const messages = require('../messages.js'); messages.init({ dataDir: dir }); const ok = [], bad = []; const t = (n, c, extra) => { (c ? ok : bad).push(n + (c || !extra ? '' : ' -> ' + extra)); }; const ME = 'earner@example.com'; // a real sponsor writing to their team await messages.deliver(36, 'sponsor@example.com', [ME], 'Welcome to my line', '

hi

'); // the system, reporting money that moved await messages.deliver(1, 'house@instantadpay.com', [ME], 'You just got paid 40.8923 POL on InstantAdPay', '

paid

', 'notice'); await messages.deliver(1, 'house@instantadpay.com', [ME], 'You missed 43.06 POL on InstantAdPay', '

missed

', 'notice'); const box = await messages.inbox(ME); t('every message reaches the inbox', box.length === 3, String(box.length)); t('the inbox badge counts notices too', (await messages.unreadCount(ME)) === 3, String(await messages.unreadCount(ME))); // the whole point const un = await messages.newestUnread(ME); t('the modal picks the human message, not the payout notice', un && un.subject === 'Welcome to my line', un && un.subject); // acknowledge it; nothing should be left to interrupt with await messages.markRead(ME, un.id); const after = await messages.newestUnread(ME); t('once acknowledged, nothing else pops', after === null, after && after.subject); t('but the notices are still unread in the inbox', (await messages.unreadCount(ME)) === 2, String(await messages.unreadCount(ME))); // a flood of notices must never produce a single interruption for (let i = 0; i < 25; i++) { await messages.deliver(1, 'house@instantadpay.com', [ME], 'You just got paid ' + i + ' POL on InstantAdPay', '

x

', 'notice'); } t('25 more payout notices still pop nothing', (await messages.newestUnread(ME)) === null); t('and they all show up in the inbox', (await messages.inbox(ME)).length === 28, String((await messages.inbox(ME)).length)); // an untagged deliver is still treated as a human broadcast (back-compat) await messages.deliver(36, 'sponsor@example.com', [ME], 'Team call tonight', '

call

'); const un2 = await messages.newestUnread(ME); t('an untagged message still interrupts, as before', un2 && un2.subject === 'Team call tonight', un2 && un2.subject); // chat never pops the modal and never lands in the inbox list await messages.sendChat(36, 'sponsor@example.com', ME, 'you around?'); t('chat stays out of the inbox list', (await messages.inbox(ME)).length === 29, String((await messages.inbox(ME)).length)); t('chat is counted separately', (await messages.chatUnread(ME)) === 1, String(await messages.chatUnread(ME))); fs.rmSync(dir, { recursive: true, force: true }); console.log('PASS ' + ok.length); for (const b of bad) console.log('FAIL ' + b); process.exit(bad.length ? 1 : 0);