import test from "node:test"; import assert from "node:assert/strict"; import { blankPlan } from "../shared/fields.js"; import { computeLedger } from "../shared/ledger.js"; import { buildPlanCurve, curveSegments, curveWarnings, } from "../shared/curve.js"; import { renderReportHtml } from "../shared/report.js"; import { parseDuration } from "../shared/time.js"; function plan() { const p = blankPlan(); Object.assign(p.fields, { 0.1: "Burundi", 0.4: "500", 1.4: "8:45", 4.1: "light", 4.2: "12", 4.3: "1:38", 3.2: "-0:30", 1.7: "-0:05", }); return p; } test("light development does not silently collapse to 1:03; explicit override wins", () => { const p = plan(); let l = computeLedger(p); assert.equal(l.rawC, 63); assert.equal(l.C, 75); assert.ok(l.warnings.some((w) => w.includes("below 1:15"))); p.fields["4.4"] = "1:20"; assert.equal(computeLedger(p).C, 80); }); test("machine profiles cannot modify plan timing or temperature defaults", () => { const p = plan(), l = computeLedger(p); const machine = { pace: { value: 1.8 }, n: 99, medians: { firstCrackTempC: 140, turningPointS: 10 }, }; assert.deepEqual(computeLedger(p, machine), l); assert.deepEqual(buildPlanCurve(p, l, machine), buildPlanCurve(p, l)); assert.equal(buildPlanCurve(p, l)[3].tempC, 200); }); test("default development average rise declines and cubic segments do not overshoot", () => { const p = plan(), l = computeLedger(p), points = buildPlanCurve(p, l); assert.deepEqual(curveWarnings(points), []); for (const s of curveSegments(points).segments) { for (let t = 0; t <= 1; t += 0.01) { const v = (1 - t) ** 3 * s.a.tempC + 3 * (1 - t) ** 2 * t * s.c1.tempC + 3 * (1 - t) * t * t * s.c2.tempC + t ** 3 * s.b.tempC; assert.ok( v >= Math.min(s.a.tempC, s.b.tempC) - 1e-8 && v <= Math.max(s.a.tempC, s.b.tempC) + 1e-8, ); } } }); test("explicit flick-producing targets are preserved and flagged", () => { const p = plan(); p.temps.drop.tempC = "230"; const points = buildPlanCurve(p, computeLedger(p)); assert.equal(points.at(-1).tempC, 230); assert.ok(curveWarnings(points).length); }); test("negative timings and implausible density are surfaced", () => { const p = plan(); p.fields["1.6"] = "-10:00"; p.fields["5.2"] = "76.14"; const l = computeLedger(p); assert.equal(l.A, null); assert.ok(l.warnings.some((w) => w.includes("761.4"))); assert.equal(parseDuration("1:99"), null); assert.equal(parseDuration("9".repeat(400)), null); }); test("standalone report has Fahrenheit, grams, graph annotations and escapes untrusted content", () => { const p = plan(); p.fields["0.1"] = ""; p.afterRoast.cupNotes = ""; const html = renderReportHtml(p); assert.ok(html.startsWith("")); assert.ok(html.includes("440 g")); assert.ok(html.includes("500 g")); assert.ok(html.includes("392°F")); assert.ok(html.includes("Development · 1:15")); assert.ok(html.includes("8:45")); assert.ok(html.includes("Maillard")); assert.ok(html.includes("@media print")); assert.ok(!html.includes("