// Tests for the funding waterfall (docs/funding.md). node test/waterfall.test.mjs // The pinned vectors in test/waterfall-vectors.json are the spec: a failure // means the implementation drifted from docs/funding.md, and the fix is the // code (or a deliberate new algorithm version), never the vector. import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { waterfall, applyQuarter, splitSurplus, monthsBetween, SPLIT } from "../tools/waterfall.mjs"; let failures = 0; async function test(name, fn) { try { await fn(); console.log("PASS " + name); } catch (e) { console.log("FAIL " + name + " — " + (e && e.message)); failures++; } } const { vectors } = JSON.parse(readFileSync(new URL("./waterfall-vectors.json", import.meta.url), "utf8")); for (const v of vectors) { await test("vector: " + v.name, () => { const result = waterfall(v.ledger, v.quarter); for (const [key, want] of Object.entries(v.expected)) { assert.deepEqual(result[key], want, key); } }); } // Every distributed cent must be accounted for — nothing leaks, nothing is // minted. This holds for every vector, not just the hand-picked ones. await test("conservation: pools sum to D; investor pool = payments + retained + residue", () => { for (const v of vectors) { const r = waterfall(v.ledger, v.quarter); if (!r.distributed) { assert.equal(r.payments.length, 0, v.name); continue; } assert.equal(r.investorPool + r.teamPool + r.commonsPool, r.distributable, v.name); const paid = r.payments.reduce((s, p) => s + p.amount, 0); assert.equal(paid + r.retained + r.residue, r.investorPool, v.name); } }); await test("determinism: identical inputs produce identical outputs", () => { for (const v of vectors) { assert.deepEqual(waterfall(v.ledger, v.quarter), waterfall(v.ledger, v.quarter), v.name); } }); await test("waterfall never mutates the ledger it is given", () => { const v = vectors[0]; const before = JSON.stringify(v.ledger); waterfall(v.ledger, v.quarter); assert.equal(JSON.stringify(v.ledger), before); }); await test("applyQuarter folds the result back into a publishable ledger", () => { const v = vectors[0]; // the doc's worked example const { result, ledger } = applyQuarter(v.ledger, v.quarter); assert.deepEqual( ledger.notes.map((n) => ({ id: n.id, remainingCap: n.remainingCap })), result.notes, ); const q = ledger.quarters.at(-1); assert.equal(q.id, v.quarter.id); assert.equal(q.floorMet, true); assert.deepEqual(q.payments, result.payments); // The fold keeps everything an auditor needs to re-run the quarter. assert.equal(q.revenue, v.quarter.revenue); assert.equal(q.investorPool, result.investorPool); }); await test("splitSurplus: shares match SPLIT and rounding favours the commons", () => { assert.deepEqual(SPLIT, { investors: 60, team: 25, commons: 15 }); for (const D of [1, 2, 3, 99, 100, 10001, 800000]) { const { investors, team, commons } = splitSurplus(D); assert.equal(investors + team + commons, D, "D=" + D); assert.ok(commons >= Math.floor((D * SPLIT.commons) / 100), "D=" + D); } }); await test("monthsBetween counts whole calendar months", () => { assert.equal(monthsBetween("2026-09-30", "2028-09-29"), 23); assert.equal(monthsBetween("2026-09-30", "2028-09-30"), 24); assert.equal(monthsBetween("2026-09-30", "2028-12-31"), 27); assert.equal(monthsBetween("2026-01-31", "2026-02-28"), 0); }); // The implementation lives in web/status/waterfall.js (served by the public // Investing page, which imports and runs it); tools/waterfall.mjs is the // documented Node-side path. The shim must expose the full surface — this // file imports through it, and here we pin that nothing is lost. await test("tools/waterfall.mjs re-exports the served module's whole surface", async () => { const served = await import("../web/status/waterfall.js"); const shim = await import("../tools/waterfall.mjs"); assert.deepEqual(Object.keys(shim).sort(), Object.keys(served).sort()); }); // The committed public ledger (docs/funding.md § The ledger): the Investing // page renders whatever this holds, so its shape is a contract. await test("web/status/funding.json holds the ledger shape the page reads", () => { const d = JSON.parse(readFileSync(new URL("../web/status/funding.json", import.meta.url), "utf8")); assert.equal(typeof d.as_of, "string"); assert.equal(typeof d.adopted, "boolean"); for (const k of ["series", "notes", "quarters"]) assert.ok(Array.isArray(d[k]), k); // Until adoption, the ledger must stay empty — no notes may exist before // the model passes review and a series deliberately opens. if (!d.adopted) for (const k of ["series", "notes", "quarters"]) assert.equal(d[k].length, 0, k); }); // The Investing page must run the served module and read the committed // ledger — never carry a copy of either. The nav is the SHARED builder // (status-nav.js, #1222): pages declare their identity via data-title and // the builder marks the current pill at runtime — no static nav markup. await test("funding.html imports ./waterfall.js and fetches ./funding.json", () => { const html = readFileSync(new URL("../web/status/funding.html", import.meta.url), "utf8"); assert.ok(html.includes('from "./waterfall.js"')); assert.ok(html.includes('fetch("./funding.json"')); assert.ok(html.includes('data-title="Investing"')); assert.ok(html.includes('src="./status-nav.js"')); }); // Every status page mounts the shared nav, and the nav carries the // Investing tab — the one place the link lives now (#1222). await test("all status pages carry the Investing nav link", () => { const nav = readFileSync(new URL("../web/status/status-nav.js", import.meta.url), "utf8"); assert.ok(nav.includes('"funding.html"') || nav.includes('"./funding.html"')); assert.ok(nav.includes('"Investing"')); for (const page of ["index", "books", "economics", "carbon", "contributors", "security", "prompts", "ssh", "hardening", "milestones"]) { const html = readFileSync(new URL("../web/status/" + page + ".html", import.meta.url), "utf8"); assert.ok(html.includes('src="./status-nav.js"'), page); } }); console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`); process.exit(failures === 0 ? 0 : 1);