Files
timmy-talking-turd/tests/pattern-summary.acceptance.mjs
T
timmy d670360560
Quality gates / quality (pull_request) Successful in 2m59s
feat: explainable longitudinal pattern summaries with cited records (#34)
The home insight card now reports the confirmed-log count and form
groupings for a window, every line names the exact confirmed records it
was built from, and following a citation opens those records. A window
below the minimum states that it is too sparse instead of describing a
trend, and a window with both firm and loose logs says it is mixed rather
than picking a winner. No line explains a cause, rules on food, or offers
a health verdict.

- src/summary.js: window filtering, insufficient-data guard, per-form
  lines, mixed-pattern line, boundary copy, record citations
- app.js: explainable home card plus a pattern-source sheet listing the
  cited records
- tests/summary.test.js, tests/pattern-summary.acceptance.mjs: unit and
  real-browser coverage, gated in CI and the release builder
- scripts/record_release_demo.mjs: fictional multi-day sample logs,
  citation follow-through, and the sparse-window refusal on camera
2026-10-14 08:13:02 +00:00

89 lines
4.4 KiB
JavaScript

import { chromium } from 'playwright';
import assert from 'node:assert/strict';
import { mkdir } from 'node:fs/promises';
// Issue #34: every longitudinal summary line must point at the confirmed records behind it, and a
// sparse window must say so instead of describing a trend. The records are fictional sample entries
// written into the app's own local ledger contract; no real health data is involved.
const DAY = 86400000;
const sample = (id, daysAgo, bristolType) => ({
id,
occurredAt: new Date(Date.now() - daysAgo * DAY).toISOString(),
bristolType,
color: 'brown',
urgency: 1,
discomfort: 1,
note: '',
photoDataUrl: '',
symptoms: {},
});
await mkdir('artifacts', { recursive: true });
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, serviceWorkers: 'block' });
const page = await context.newPage();
const errors = [];
page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); });
page.on('pageerror', error => errors.push(error.message));
const seed = async entries => page.evaluate(list => {
const base = document.querySelector('meta[name="timmy-base-path"]')?.content || '/';
localStorage.setItem(`timmy:${base}:ledger-v1`, JSON.stringify(list));
}, entries);
await page.goto('http://127.0.0.1:4173', { waitUntil: 'networkidle' });
await page.evaluate(() => localStorage.clear());
// 1. A sparse window refuses to describe a pattern.
await seed([sample('sparse-a', 1, 4), sample('sparse-b', 4, 6)]);
await page.reload({ waitUntil: 'networkidle' });
const sparse = page.locator('[data-summary-insufficient]');
assert.equal(await sparse.count(), 1, 'a window below the minimum must show honest insufficient-data copy');
assert.match(await sparse.innerText(), /not enough confirmed logs/i);
assert.match(await sparse.innerText(), /2 of 3/);
assert.equal(await page.locator('[data-statement]').count(), 0, 'no pattern line may be claimed from two logs');
await page.screenshot({ path: 'artifacts/pattern-summary-insufficient-mobile.png' });
// 2. A populated window renders explainable lines, each citing its own records.
const seeded = [
sample('rec-firm', 2, 1),
sample('rec-typical-1', 5, 3),
sample('rec-typical-2', 9, 4),
sample('rec-typical-3', 12, 4),
sample('rec-old', 40, 6),
];
await seed(seeded);
await page.reload({ waitUntil: 'networkidle' });
const card = page.locator('[data-pattern-summary]');
assert.equal(await card.count(), 1, 'the home insight card is the explainable summary');
assert.match(await page.locator('[data-summary-window]').innerText(), /last 28 days/i);
assert.match(await page.locator('[data-statement="frequency"]').innerText(), /4 confirmed logs/);
assert.equal(await page.locator('[data-statement="firm"]').getAttribute('data-records'), '1');
assert.equal(await page.locator('[data-statement="typical"]').getAttribute('data-records'), '3');
// The out-of-window log is neither counted nor offered as a citation.
assert.doesNotMatch(await card.innerText(), /5 confirmed logs/);
await page.screenshot({ path: 'artifacts/pattern-summary-home-mobile.png' });
// 3. Following a citation shows exactly the contributing confirmed records.
await page.locator('[data-statement-link="typical"]').click();
const records = page.locator('[data-summary-records]');
await records.waitFor();
assert.equal(await page.locator('[data-summary-record]').count(), 3, 'the citation lists exactly its own records');
const listed = await page.locator('[data-summary-record]').evaluateAll(nodes => nodes.map(node => node.getAttribute('data-entry-id')));
assert.deepEqual(listed.sort(), ['rec-typical-1', 'rec-typical-2', 'rec-typical-3']);
assert.match(await records.innerText(), /Type 3/);
assert.match(await records.innerText(), /These are the confirmed logs this line was built from/);
await page.screenshot({ path: 'artifacts/pattern-summary-records-mobile.png' });
await page.locator('#pattern-records-close').click();
assert.equal(await page.locator('[data-summary-records]').count(), 0, 'the citation sheet closes cleanly');
// 4. The summary never explains a cause or clears a food.
const summaryText = await card.innerText();
assert.doesNotMatch(summaryText, /because|due to|caused|safe to eat|avoid eating|should eat|diagnos/i);
assert.deepEqual(errors, []);
await context.close();
await browser.close();
console.log('PASS longitudinal pattern summaries cite their confirmed records and refuse to describe a sparse window');