founders-break/ops/regression.test.cjs
gator 3572291933 Fix reminder postal configuration and require deliberate RSVP confirmation
Link previews no longer submit answers. Failed queue attempts retain reusable drafts; recipient queues prevent duplicate retries.

Tested: 18 isolated regressions pass; 13 fail on baseline.
AI-Agent: Codex (OpenAI)
AI-Session: 01a14186-e2ee-77b1-9526-a32c58cd6a00
2026-10-22 14:05:19 +00:00

160 lines
10 KiB
JavaScript

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);
});