Files
vyndr/tests/unit/reportTemplate.test.js
T
builtbykev 71d3b7b786 E10 Report issue template + E12 /report archive, to spec
PHASE 0 — the spec, read not recalled. E10: "Hybrid: dark billboard header
that survives every client, light paper body Gmail can't wreck. 600px,
stacked, no webfont dependence." Content law: "One email per slate day.
Top read, what changed, the record. Nothing else." E12: "EVERY ISSUE SHOWS
ITS OWN DAY RECORD -- THE ARCHIVE IS A LEDGER TOO."

COMPOSED, NOT FORKED. The audit had E10 as PARTIAL, not absent:
newsletterService already builds the daily report's CONTENT and lints its
voice. What was missing is the designed hybrid SHELL, so reportTemplate.js
is a template over that builder rather than a second report -- the same
call made for the movement strip, and for the same reason.

PHASE 1 — the hybrid shell is an ENGINEERING constraint, not a look, and
the tests say so: Gmail strips style blocks, Outlook ignores flexbox, and
a dark body renders as a black rectangle in several clients. Hence tables,
inline styles, 600px fixed, system fonts, no image required to read, and
the green SHIFTS from #00D4A0 to #00A57D on paper because the dark-mode
green is unreadable there.

FACT-CONTRACTED: a section whose data is absent is OMITTED and NAMED in
`omitted`, never filled. There is no code path producing a placeholder
figure. The honesty block carries the real numbers -- graded count,
cleared-ceiling count, the realized rate against baseline, and that we do
not issue A grades.

E1'S LAW TRAVELS EVEN THOUGH ITS RENDERING CANNOT. An SVG strip is not
reliable in email, so movementText carries the RULE: green only when the
move favours the read, and a flat market says FLAT · [N]D rather than
showing nothing.

NO DESIGNER SAMPLE DATA. Nabers 1,120.5, No 128, DAY RECORD 9-4 are a spec
for what a live issue renders; pasting them in would be fabrication
carrying a designer's authority and would look entirely correct. Tested.

PHASE 2 — /report is now the real archive, REPLACING the S41 redirect to
/blog. That redirect existed because the surface did not; E12 built it, so
the placeholder is correctly gone and the S41 test is updated rather than
worked around. Every row carries its own day record, and an unknown record
says UNSETTLED -- never a dash that reads as zero. Empty archive is an
honest state.

Backend: public read-only /api/report over Redis issues, plus the Next
proxy. Both surfaces registered under the reachability guard.

A test bug I made twice now: my check for forbidden sample values matched
the template's own doc block, which NAMES those values as things never to
paste. Documentation worth keeping, so both suites strip comments before
matching -- a guard that reads its own warning is not reading the code.

WAVE-2 STATUS: E1, F9-F11, E10, E12 done. Still gated -- F5 article media
and E16/F8 on the card-system reconciliation; the in-season hub IA on the
social chat's formula; E9/E15 on model; E2/E6 on licensing.

Read-only throughout; serving fingerprint unchanged including
newsletterService; accrual clock unchanged at 0 eligible dates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
2026-08-07 22:17:59 -04:00

134 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* E10 issue template + E12 archive.
*
* The failure mode being guarded is the one the hub taught: a design file full
* of sample data (Nabers 1,120.5, Nº 128, DAY RECORD 9-4) is a SPEC for what a
* live issue renders. Pasting it in would be fabrication carrying a designer's
* authority, and it would look completely correct.
*/
const fs = require('fs');
const path = require('path');
const t = require('../../src/services/report/reportTemplate');
const ROOT = path.join(__dirname, '..', '..');
/**
* Strip comments first. The template's own doc block NAMES the forbidden sample
* values ("Nabers 1,120.5 ... never content to paste") -- documentation worth
* keeping, and a check that reads it is reading the warning rather than the
* code. Same bug I made on the offseason hub.
*/
const stripComments = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
const ARCHIVE = stripComments(fs.readFileSync(path.join(ROOT, 'web/src/components/vyndr/ReportArchive.tsx'), 'utf8'));
const TPL = stripComments(fs.readFileSync(path.join(ROOT, 'src/services/report/reportTemplate.js'), 'utf8'));
const ARCHIVE_RAW = fs.readFileSync(path.join(ROOT, 'web/src/components/vyndr/ReportArchive.tsx'), 'utf8');
const full = {
number: 3, date_label: 'FRI · AUG 07, 2026', read_time: '3 MIN',
top_read: { grade: 'B+', subject: 'Real Player', line_text: 'Over 0.5 Hits',
movement: { from: 0.5, to: 0.5, days: 2 }, model: '0.61', best_book: 'DK', note: 'A real note.' },
changed: [{ time: '11:42 AM', tag: 'injury', text: 'Something real happened.' }],
record: { line: '94', note: 'settled' },
honesty: { graded: 2140, cleared_ceiling: 70, ceiling_letter: 'B+', ceiling_realized: 66, base_rate: 60, unissuable: 'A+, A, A-' },
};
describe('no designer sample data ships', () => {
it('the template source contains none of the spec\'s sample values', () => {
for (const sample of ['Nabers', '1,120.5', '1,188', 'Skenes', 'Wemby', '12,408']) {
expect(TPL).not.toContain(sample);
}
});
it('the archive contains no sample issues', () => {
for (const sample of ['Nabers', 'SKENES', '94', 'Nº 128']) {
expect(ARCHIVE).not.toContain(sample);
}
});
});
describe('a section with no data is OMITTED, never filled', () => {
it('names every omitted section rather than hiding the gap', () => {
const out = t.renderIssue({ number: 1, date_label: 'X' });
expect(out.omitted).toEqual(expect.arrayContaining(['top_read', 'changed', 'record', 'honesty']));
expect(out.html).not.toMatch(/TOP READ OF THE DAY/);
});
it('an empty issue still renders a valid, readable shell', () => {
const out = t.renderIssue({});
expect(out.html).toMatch(/VYND/);
expect(out.html).toMatch(/One email per slate day/);
});
it('renders every section when the data is there', () => {
const out = t.renderIssue(full);
expect(out.omitted).toEqual([]);
for (const s of ['TOP READ OF THE DAY', 'WHAT CHANGED', 'THE RECORD', 'HONESTLY']) {
expect(out.html).toContain(s);
}
});
});
describe('the E1 movement law travels into email', () => {
it('a flat market says FLAT with its day count, not nothing', () => {
// A strip cannot render in email, but its RULE still applies.
expect(t.movementText({ from: 1.5, to: 1.5, days: 4 })).toMatchObject({ text: 'FLAT · 4D' });
});
it('green is reserved for a move that FAVOURS the read', () => {
const toward = t.movementText({ from: 1.5, to: 1.2, dir: 'toward' });
const against = t.movementText({ from: 1.5, to: 1.8, dir: 'against' });
expect(toward.colour).toBe(t.T.greenOnPaper);
expect(against.colour).not.toBe(t.T.greenOnPaper);
});
it('an unreadable movement returns null rather than a guess', () => {
expect(t.movementText({ from: null, to: 1.2 })).toBeNull();
expect(t.movementText(null)).toBeNull();
});
});
describe('the hybrid shell is an engineering constraint, not a look', () => {
it('is 600px and table-based — Gmail strips style blocks, Outlook ignores flex', () => {
const out = t.renderIssue(full);
expect(out.width).toBe(600);
expect(out.html).toMatch(/<table role="presentation"/);
expect(out.html).not.toMatch(/<style/);
expect(out.html).not.toMatch(/display:\s*flex/);
});
it('the green SHIFTS on paper — the dark-mode green is unreadable there', () => {
expect(t.T.greenOnPaper).not.toBe(t.T.greenOnDark);
});
it('depends on no webfont and no image to be readable', () => {
const out = t.renderIssue(full);
expect(out.html).not.toMatch(/@font-face|fonts\.googleapis/);
expect(out.html).not.toMatch(/<img/);
});
it('keeps the LITERAL unsubscribe token for Listmonk to substitute', () => {
expect(t.renderIssue(full).html).toContain('{{ UnsubscribeURL }}');
});
it('ships a plain-text alternative', () => {
expect(t.renderIssue(full).text).toMatch(/TOP READ/);
});
});
describe('E12 — the archive is a ledger too', () => {
it('every row renders its own day record', () => {
expect(ARCHIVE).toMatch(/DAY RECORD/);
});
it('an unknown record says UNSETTLED, not a dash that reads as zero', () => {
expect(ARCHIVE).toMatch(/UNSETTLED/);
expect(ARCHIVE_RAW).toMatch(/never a dash that reads as zero/);
});
it('an empty archive is an honest state, not a broken page', () => {
expect(ARCHIVE).toMatch(/No issues published yet/);
});
});