'use strict'; // Session S7 (a1) — THE VYNDR REPORT assembly + send. Everything injected: // fixtures stand in for the snapshot cache, the streaks engine inputs, and // the ledger aggregate. No network, no Redis. const { buildDailyReport, sendDailyReport, subscribe, __internals, } = require('../../src/services/newsletterService'); // ---------------------------------------------------------------- fixtures const SNAPSHOT_MLB = { sport: 'mlb', updated_at: '2026-07-11T14:00:00Z', grades: [ { player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'over', grade: 'A+', confidence: 82, archetype: 'BOMBER' }, { player: 'Mookie Betts', stat_type: 'hits', line: 1.5, direction: 'over', grade: 'B', confidence: 64 }, { player: 'Luis Castillo', stat_type: 'pitcher_strikeouts', line: 6.5, direction: 'under', grade: 'A', confidence: 71, archetype: 'WHIFF' }, ], deltas: [], }; const ROSTER_MLB = [ { name: 'Seiya Suzuki', team: 'Chicago Cubs', games: [ { date: '2026-07-10', hits: 2, opponent: 'STL' }, { date: '2026-07-09', hits: 1, opponent: 'STL' }, { date: '2026-07-08', hits: 3, opponent: 'PIT' }, { date: '2026-07-07', hits: 1, opponent: 'PIT' }, { date: '2026-07-06', hits: 2, opponent: 'CIN' }, { date: '2026-07-05', hits: 0, opponent: 'CIN' }, ], }, ]; const AGG_BUILDING = { window_days: 30, min_sample: 20, settled: 4, hits: 3, misses: 1, pushes: 0, hit_pct: null, pending: 17, beat_close_pct: null }; const AGG_REAL = { window_days: 30, min_sample: 20, settled: 25, hits: 15, misses: 9, pushes: 1, hit_pct: 63, pending: 6, beat_close_pct: 58 }; function deps(overrides = {}) { return { cacheGet: async (key) => (key === 'snapshot:mlb:latest' ? SNAPSHOT_MLB : null), loadRosterLogs: async (sport) => (sport === 'mlb' ? ROSTER_MLB : []), computeStreaks: require('../../src/services/streaksService').computeStreaks, applyLens: require('../../src/services/streakLens').applyLens, getModelAggregate: async () => AGG_BUILDING, now: () => new Date('2026-07-11T15:00:00Z'), ...overrides, }; } // VOICE v1.1 lint — the banned list the templates must never emit. // "lock"/"tail"/"fade" as words (not substrings — "blocked"/"detail" are fine). const BANNED_WORDS = /\b(lock|locks|tail|tails|fade|fades|guarantee[ds]?|free money|can't lose)\b/i; const HYPE_EMOJI = /[\u{1F525}\u{1F4B0}\u{1F680}\u{1F4AF}\u{1F512}]/u; describe('buildDailyReport — VOICE lint', () => { let report; beforeAll(async () => { report = await buildDailyReport(['mlb'], deps()); }); test('no exclamation points anywhere — subject, html, text', () => { expect(report.subject).not.toContain('!'); expect(report.text).not.toContain('!'); // html may contain !important? we never use it — assert none at all expect(report.html).not.toContain('!'); }); test('no banned vocabulary, no hype emoji', () => { for (const part of [report.subject, report.text, report.html]) { expect(part).not.toMatch(BANNED_WORDS); expect(part).not.toMatch(HYPE_EMOJI); } }); test('subject carries the pipeline signal count and the ET date', () => { expect(report.subject).toBe('THE VYNDR REPORT — Sat, Jul 11 · 3 signals'); }); test('signals come only from the snapshot fixture, o/u for sides', () => { expect(report.text).toContain('Aaron Judge — total bases o1.5 · A+ · BOMBER'); expect(report.text).toContain('Luis Castillo — pitcher strikeouts u6.5 · A · WHIFF'); expect(report.counts.signals).toBe(3); }); test('A+ sorts ahead of B regardless of array order', () => { const judge = report.text.indexOf('Aaron Judge'); const betts = report.text.indexOf('Mookie Betts'); expect(judge).toBeGreaterThan(-1); expect(betts).toBeGreaterThan(judge); }); test('STREAK WATCH renders the real computed streak through the lens', () => { expect(report.text).toContain('STREAK WATCH'); expect(report.text).toContain('Seiya Suzuki'); // Lens: built-vs opponents from the game log — real, not composed. expect(report.text).toMatch(/built vs/); expect(report.counts.streaks).toBeGreaterThan(0); }); test('RG footer present in html AND text: 21+, 1-800-GAMBLER, unsubscribe', () => { for (const part of [report.text, report.html]) { expect(part).toContain('21+'); expect(part).toContain('1-800-GAMBLER'); expect(part).toContain('{{ UnsubscribeURL }}'); } }); }); describe('buildDailyReport — the record gate (n≥20 lives upstream)', () => { test('hit_pct null → RECORD BUILDING with the pending count, never a %', async () => { const report = await buildDailyReport(['mlb'], deps()); expect(report.text).toContain('RECORD BUILDING · 17 pending'); expect(report.text).not.toMatch(/\d+%.*misses included/); }); test('hit_pct present → the real record line, misses included', async () => { const report = await buildDailyReport(['mlb'], deps({ getModelAggregate: async () => AGG_REAL })); expect(report.text).toContain('Last 30 days: 15-9 (63%), misses included, n=24'); expect(report.text).toContain('beat the close 58%'); expect(report.text).not.toContain('RECORD BUILDING'); }); test('aggregate throws → building line, never a crash', async () => { const report = await buildDailyReport(['mlb'], deps({ getModelAggregate: async () => { throw new Error('supabase down'); } })); expect(report.text).toContain('RECORD BUILDING · 0 pending'); }); }); describe('buildDailyReport — empty pipeline is a valid state', () => { test('no snapshot, no roster → honest empty wire, zero counts', async () => { const report = await buildDailyReport(['mlb'], deps({ cacheGet: async () => null, loadRosterLogs: async () => [], })); expect(report.counts.signals).toBe(0); expect(report.counts.streaks).toBe(0); expect(report.text).toContain('No graded slate at send time'); expect(report.subject).toBe('THE VYNDR REPORT — Sat, Jul 11'); // no invented number // Compliance footer still present even on an empty report. expect(report.text).toContain('1-800-GAMBLER'); }); }); describe('sendDailyReport — env-gated, empty-refusing', () => { const ENV = { LISTMONK_URL: 'http://127.0.0.1:9000', LISTMONK_USER: 'vyndr-api', LISTMONK_TOKEN: 'tok123', LISTMONK_LIST_ID: '3', }; test('no env → graceful no-op, no fetch', async () => { const fetchImpl = jest.fn(); const res = await sendDailyReport({ env: {}, fetchImpl, deps: deps() }); expect(res).toEqual({ ok: false, reason: 'not configured' }); expect(fetchImpl).not.toHaveBeenCalled(); }); test('empty report → refuses to send even with env set', async () => { const fetchImpl = jest.fn(); const res = await sendDailyReport({ env: ENV, fetchImpl, deps: deps({ cacheGet: async () => null, loadRosterLogs: async () => [] }), }); expect(res.ok).toBe(false); expect(res.reason).toBe('empty report'); expect(fetchImpl).not.toHaveBeenCalled(); }); test('creates then starts the campaign with the token auth scheme', async () => { const calls = []; const fetchImpl = jest.fn(async (url, opts) => { calls.push({ url, opts }); if (url.endsWith('/api/campaigns')) { return { ok: true, status: 200, json: async () => ({ data: { id: 42 } }) }; } return { ok: true, status: 200, json: async () => ({}) }; }); const res = await sendDailyReport({ env: ENV, fetchImpl, deps: deps() }); expect(res.ok).toBe(true); expect(res.campaignId).toBe(42); const create = calls[0]; expect(create.url).toBe('http://127.0.0.1:9000/api/campaigns'); expect(create.opts.headers.Authorization).toBe('token vyndr-api:tok123'); const body = JSON.parse(create.opts.body); expect(body.lists).toEqual([3]); expect(body.subject).toContain('THE VYNDR REPORT'); expect(body.body).toContain('{{ UnsubscribeURL }}'); expect(body.altbody).toContain('1-800-GAMBLER'); const start = calls[1]; expect(start.url).toBe('http://127.0.0.1:9000/api/campaigns/42/status'); expect(JSON.parse(start.opts.body)).toEqual({ status: 'running' }); }); test('listmonk create failure → honest error, no start call', async () => { const fetchImpl = jest.fn(async () => ({ ok: false, status: 500, json: async () => ({}) })); const res = await sendDailyReport({ env: ENV, fetchImpl, deps: deps() }); expect(res.ok).toBe(false); expect(res.reason).toBe('listmonk create 500'); expect(fetchImpl).toHaveBeenCalledTimes(1); }); }); describe('subscribe — double opt-in forwarding', () => { const ENV = { LISTMONK_URL: 'http://127.0.0.1:9000/', LISTMONK_USER: 'vyndr-api', LISTMONK_TOKEN: 'tok123', LISTMONK_LIST_ID: '3', }; test('no env → not configured, no fetch', async () => { const fetchImpl = jest.fn(); const res = await subscribe('kev@example.com', { env: {}, fetchImpl }); expect(res).toEqual({ ok: false, reason: 'not configured' }); expect(fetchImpl).not.toHaveBeenCalled(); }); test('forwards with preconfirm_subscriptions:false (Listmonk sends the confirmation)', async () => { const fetchImpl = jest.fn(async () => ({ ok: true, status: 200, json: async () => ({}) })); const res = await subscribe('KEV@Example.com ', { env: ENV, fetchImpl }); expect(res).toEqual({ ok: true }); const [url, opts] = fetchImpl.mock.calls[0]; expect(url).toBe('http://127.0.0.1:9000/api/subscribers'); // trailing slash trimmed const body = JSON.parse(opts.body); expect(body.email).toBe('kev@example.com'); expect(body.preconfirm_subscriptions).toBe(false); expect(body.lists).toEqual([3]); expect(body.status).toBe('enabled'); }); test('409 already-subscribed is ok (idempotent, no enumeration)', async () => { const fetchImpl = jest.fn(async () => ({ ok: false, status: 409, json: async () => ({}) })); const res = await subscribe('kev@example.com', { env: ENV, fetchImpl }); expect(res).toEqual({ ok: true }); }); test('network failure → calm reason, never throws', async () => { const fetchImpl = jest.fn(async () => { throw new Error('ECONNREFUSED'); }); const res = await subscribe('kev@example.com', { env: ENV, fetchImpl }); expect(res.ok).toBe(false); expect(res.reason).toBe('ECONNREFUSED'); }); }); describe('__internals', () => { test('listmonkConfig requires all four env vars', () => { expect(__internals.listmonkConfig({})).toBeNull(); expect(__internals.listmonkConfig({ LISTMONK_URL: 'x', LISTMONK_USER: 'u', LISTMONK_TOKEN: 't' })).toBeNull(); expect(__internals.listmonkConfig({ LISTMONK_URL: 'x', LISTMONK_USER: 'u', LISTMONK_TOKEN: 't', LISTMONK_LIST_ID: 'nope' })).toBeNull(); expect(__internals.listmonkConfig({ LISTMONK_URL: 'http://x/', LISTMONK_USER: 'u', LISTMONK_TOKEN: 't', LISTMONK_LIST_ID: '7' })) .toEqual({ url: 'http://x', user: 'u', token: 't', listId: 7 }); }); test('recordLine gates on hit_pct, not on settled count', () => { expect(__internals.recordLine(null)).toBe('RECORD BUILDING · 0 pending'); expect(__internals.recordLine({ hit_pct: null, pending: 12 })).toBe('RECORD BUILDING · 12 pending'); expect(__internals.recordLine({ hit_pct: 61, hits: 14, misses: 9, window_days: 30, beat_close_pct: null })) .toBe('Last 30 days: 14-9 (61%), misses included, n=23'); }); test('statLabel humanizes stat keys', () => { expect(__internals.statLabel('total_bases')).toBe('total bases'); expect(__internals.statLabel('PITCHER_STRIKEOUTS')).toBe('pitcher strikeouts'); }); });