const { test } = require('node:test'); const assert = require('node:assert/strict'); const vm = require('node:vm'); const fs = require('node:fs'); const path = require('node:path'); const root = process.env.FOUNDERS_SOURCE_ROOT || path.resolve(__dirname, '..'); const source = file => fs.readFileSync(path.join(root, file), 'utf8'); const tick = () => new Promise(resolve => setImmediate(resolve)); function page(query, response = { ok: true }) { const nodes = new Map(), requests = []; class Element { constructor(id) { this.id = id; this.listeners = {}; this.children = []; this.value = ''; this.hidden = false; this.style = {}; this.dataset = {}; this.classList = { add() {}, toggle() {}, contains() { return false; } }; } setAttribute() {} removeAttribute() {} focus() {} prepend() {} appendChild(child) { this.children.push(child); if (child.id) nodes.set(child.id, child); return child; } insertBefore(child) { this.appendChild(child); } addEventListener(name, fn) { this.listeners[name] = fn; } querySelector(selector) { if (selector === '[name="event_date"]') return { value: '2026-10-22' }; return new Element(); } querySelectorAll() { return []; } closest() { return new Element(); } } for (const id of ['nav', 'rsvp', 'rsvp-form', 'rsvp-submit', 'form-status', 'rsvp-done', 'rsvp-live', 'f-name', 'f-email']) nodes.set(id, new Element(id)); nodes.get('rsvp-form').action = 'https://qa.invalid/forms/rsvp'; nodes.get('rsvp-done').hidden = true; const document = { body: new Element(), currentScript: null, getElementById: id => nodes.get(id) || null, querySelectorAll: () => [], querySelector: () => null, createElement: () => new Element() }; const location = { search: query, pathname: '/' }; const window = { matchMedia: () => ({ matches: true }), location, history: { replaceState() {} } }; const fetch = async (url, options) => { requests.push({ url, options }); return { ok: true, json: async () => response }; }; vm.runInNewContext(source('public/js/main.js'), { document, window, location, URLSearchParams, Intl, Date, IntersectionObserver: class { observe() {} disconnect() {} }, fetch, setTimeout }); return { nodes, requests, click: (choice, isTrusted = true) => nodes.get('confirm-rsvp-' + choice)?.listeners.click({ isTrusted }) }; } for (const choice of ['yes', 'no']) test(`opening a legacy ${choice} link never submits`, () => { const p = page(`?a=${choice}&e=qa%40example.invalid`); assert.equal(p.requests.length, 0); }); test('dated link needs a trusted click and submits only once', async () => { const p = page('?a=yes&e=qa%2Btest%40example.invalid&d=2026-10-22'); assert.equal(p.requests.length, 0); assert.ok(p.nodes.has('confirm-rsvp-yes')); p.click('yes', false); assert.equal(p.requests.length, 0); p.click('yes'); p.click('no'); await tick(); assert.equal(p.requests.length, 1); const body = new URLSearchParams(p.requests[0].options.body); assert.equal(body.get('email'), 'qa+test@example.invalid'); assert.equal(body.get('event_date'), '2026-10-22'); assert.equal(body.get('attending'), 'yes'); assert.equal(body.get('rsvp_confirmed'), 'yes'); assert.equal(p.nodes.get('rsvp-done').hidden, false); }); test('a deliberate no is preserved', async () => { const p = page('?a=no&e=qa%40example.invalid&d=2026-10-22'); p.click('no'); await tick(); assert.equal(new URLSearchParams(p.requests[0].options.body).get('attending'), 'no'); }); test('a stale dated invitation cannot silently answer for another week', () => { const p = page('?a=no&e=qa%40example.invalid&d=2026-10-15'); assert.equal(p.requests.length, 0); assert.equal(p.nodes.has('confirm-rsvp-no'), false); assert.match(p.nodes.get('form-status').textContent, /different week/); }); test('an invalid address keeps the ordinary form available', () => { const p = page('?a=yes&e=invalid'); assert.equal(p.requests.length, 0); assert.equal(p.nodes.get('rsvp-form').hidden, false); }); test('a rejected save leaves confirmation available for retry', async () => { const p = page('?a=yes&e=qa%40example.invalid', { ok: false }); p.click('yes'); await tick(); assert.equal(p.nodes.get('confirm-rsvp-yes').disabled, false); assert.equal(p.nodes.get('rsvp-done').hidden, true); assert.equal(p.nodes.get('rsvp-live').hidden, false); }); function functionFixture(file, { now = '2026-10-28T18:00:00Z', event = {}, initial = [], campaigns = [], recipients = [] } = {}) { const writes = [], calls = [], store = new Map(initial); let failSend = false; class FixedDate extends Date { constructor(...args) { super(...(args.length ? args : [now])); } static now() { return Date.parse(now); } } const fn = vm.runInNewContext(`(async function(ctx) { ${source('ops/' + file)}\n})`, { Date: FixedDate, Intl, URLSearchParams }); const env = { POSTAL_ADDRESS: 'Synthetic QA postal fixture, not a production address', LIST_ID: 'qa-list', FROM_ADDRESS_ID: 'qa-sender', NOTIFY: 'qa@example.invalid', FROM: 'qa@example.invalid', CORBEN_KEY: 'synthetic-key' }; const ctx = { event, env, log() {}, kv: { get: async key => store.get(key), set: async (key, value) => { writes.push({ key, value }); store.set(key, value); } }, fetch: async (_url, options) => { const request = JSON.parse(options.body); calls.push(request); if (request.tool === 'send_campaign' && failSend) return { ok: false, status: 500, text: 'synthetic queue failure' }; let data; if (request.tool === 'list_campaigns') data = campaigns; else if (request.tool === 'get_campaign_stats') data = recipients; else if (request.tool === 'create_campaign') { data = { id: 'qa-campaign', ...request.arguments, status: 'draft' }; campaigns.push(data); } else if (request.tool === 'send_campaign') { data = { ok: true, queued: 2 }; campaigns.find(c => c.id === request.arguments.id).status = 'sending'; } else if (request.tool === 'list_contacts') data = [{ id: 'qa-contact', email: 'qa@example.invalid', tags: ['founders-break'] }]; else data = { id: 'synthetic-id', ok: true }; return { ok: true, status: 200, json: data }; } }; return { env, calls, writes, store, run: () => fn(ctx), failSend: value => { failSend = value; } }; } const answerData = { source: 'reminder-email', name: 'Synthetic QA', email: 'qa@example.invalid', event_date: '2026-10-22', attending: 'yes' }; for (const attending of ['yes', 'no']) test(`backend rejects unconfirmed ${attending} without a write or send`, async () => { const f = functionFixture('founders-break-rsvp.js', { event: { data: { ...answerData, attending } } }); assert.equal((await f.run()).skipped, 'confirmation required'); assert.equal(f.writes.length, 0); assert.equal(f.calls.length, 0); }); test('repeated confirmed answer sends no duplicate notification', async () => { const f = functionFixture('founders-break-rsvp.js', { event: { data: { ...answerData, rsvp_confirmed: 'yes' } }, initial: [['founders-break:answer:2026-10-22:qa@example.invalid', 'yes']] }); assert.equal((await f.run()).changed, false); assert.equal(f.calls.length, 0); assert.equal(f.writes.length, 0); }); test('a confirmed new answer still reaches the existing RSVP pipeline', async () => { const f = functionFixture('founders-break-rsvp.js', { event: { data: { ...answerData, rsvp_confirmed: 'yes' } } }); assert.equal((await f.run()).attending, 'yes'); assert.equal(f.calls.filter(c => c.tool === 'send_email').length, 2); }); test('ordinary site registration still works', async () => { const f = functionFixture('founders-break-rsvp.js', { event: { data: { ...answerData, source: undefined } } }); assert.equal((await f.run()).ok, true); }); test('weekly queue request includes postal configuration and dated links', async () => { const f = functionFixture('founders-break-reminder.js'); await f.run(); const send = f.calls.find(c => c.tool === 'send_campaign'); assert.equal(send.arguments.delivery.postal_address, f.env.POSTAL_ADDRESS); assert.equal(send.arguments.delivery.timezone, 'America/New_York'); assert.match(f.calls.find(c => c.tool === 'create_campaign').arguments.html_body, /&d=2026-10-29/); assert.equal(f.store.get('founders-break:reminder:2026-10-29').state, 'queued'); }); test('failed send retains a prepared draft and a later run reuses it', async () => { const f = functionFixture('founders-break-reminder.js'); f.failSend(true); await assert.rejects(f.run(), /send_campaign failed/); assert.equal(f.store.get('founders-break:reminder:2026-10-29').state, 'prepared'); f.failSend(false); await f.run(); assert.equal(f.calls.filter(c => c.tool === 'create_campaign').length, 1); assert.equal(f.store.get('founders-break:reminder:2026-10-29').state, 'queued'); }); test('a legacy failure marker does not block reuse of an unsent draft', async () => { const f = functionFixture('founders-break-reminder.js', { initial: [['founders-break:reminder:2026-10-29', 'legacy-timestamp']], campaigns: [{ id: 'draft-id', name: "Founder's Break reminder 2026-10-29", status: 'draft' }] }); await f.run(); assert.equal(f.calls.filter(c => c.tool === 'create_campaign').length, 0); assert.equal(f.calls.find(c => c.tool === 'send_campaign').arguments.id, 'draft-id'); }); test('queued campaigns and draft campaigns with recipient queues are not sent again', async () => { for (const state of ['sending', 'sent', 'paused', 'scheduled', 'draft']) { const f = functionFixture('founders-break-reminder.js', { campaigns: [{ id: 'existing-id', name: "Founder's Break reminder 2026-10-29", status: state }], recipients: [{}] }); await f.run(); assert.equal(f.calls.filter(c => c.tool === 'send_campaign').length, 0); } }); test('missing postal address fails before campaign creation', async () => { const f = functionFixture('founders-break-reminder.js'); f.env.POSTAL_ADDRESS = ''; await assert.rejects(f.run(), /POSTAL_ADDRESS/); assert.equal(f.calls.length, 0); }); test('Thursday remains unsent without running a catch-up campaign', async () => { const f = functionFixture('founders-break-reminder.js', { now: '2026-10-22T18:00:00Z' }); assert.match((await f.run()).skipped, /not wednesday/); assert.equal(f.calls.length, 0); });