eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1// Tests for the funding waterfall (docs/funding.md). node test/waterfall.test.mjs
2// The pinned vectors in test/waterfall-vectors.json are the spec: a failure
3// means the implementation drifted from docs/funding.md, and the fix is the
4// code (or a deliberate new algorithm version), never the vector.
5import assert from "node:assert/strict";
6import { readFileSync } from "node:fs";
7import { waterfall, applyQuarter, splitSurplus, monthsBetween, SPLIT } from "../tools/waterfall.mjs";
8
9let failures = 0;
10async function test(name, fn) {
11 try {
12 await fn();
13 console.log("PASS " + name);
14 } catch (e) {
15 console.log("FAIL " + name + " — " + (e && e.message));
16 failures++;
17 }
18}
19
20const { vectors } = JSON.parse(readFileSync(new URL("./waterfall-vectors.json", import.meta.url), "utf8"));
21
22for (const v of vectors) {
23 await test("vector: " + v.name, () => {
24 const result = waterfall(v.ledger, v.quarter);
25 for (const [key, want] of Object.entries(v.expected)) {
26 assert.deepEqual(result[key], want, key);
27 }
28 });
29}
30
31// Every distributed cent must be accounted for — nothing leaks, nothing is
32// minted. This holds for every vector, not just the hand-picked ones.
33await test("conservation: pools sum to D; investor pool = payments + retained + residue", () => {
34 for (const v of vectors) {
35 const r = waterfall(v.ledger, v.quarter);
36 if (!r.distributed) {
37 assert.equal(r.payments.length, 0, v.name);
38 continue;
39 }
40 assert.equal(r.investorPool + r.teamPool + r.commonsPool, r.distributable, v.name);
41 const paid = r.payments.reduce((s, p) => s + p.amount, 0);
42 assert.equal(paid + r.retained + r.residue, r.investorPool, v.name);
43 }
44});
45
46await test("determinism: identical inputs produce identical outputs", () => {
47 for (const v of vectors) {
48 assert.deepEqual(waterfall(v.ledger, v.quarter), waterfall(v.ledger, v.quarter), v.name);
49 }
50});
51
52await test("waterfall never mutates the ledger it is given", () => {
53 const v = vectors[0];
54 const before = JSON.stringify(v.ledger);
55 waterfall(v.ledger, v.quarter);
56 assert.equal(JSON.stringify(v.ledger), before);
57});
58
59await test("applyQuarter folds the result back into a publishable ledger", () => {
60 const v = vectors[0]; // the doc's worked example
61 const { result, ledger } = applyQuarter(v.ledger, v.quarter);
62 assert.deepEqual(
63 ledger.notes.map((n) => ({ id: n.id, remainingCap: n.remainingCap })),
64 result.notes,
65 );
66 const q = ledger.quarters.at(-1);
67 assert.equal(q.id, v.quarter.id);
68 assert.equal(q.floorMet, true);
69 assert.deepEqual(q.payments, result.payments);
70 // The fold keeps everything an auditor needs to re-run the quarter.
71 assert.equal(q.revenue, v.quarter.revenue);
72 assert.equal(q.investorPool, result.investorPool);
73});
74
75await test("splitSurplus: shares match SPLIT and rounding favours the commons", () => {
76 assert.deepEqual(SPLIT, { investors: 60, team: 25, commons: 15 });
77 for (const D of [1, 2, 3, 99, 100, 10001, 800000]) {
78 const { investors, team, commons } = splitSurplus(D);
79 assert.equal(investors + team + commons, D, "D=" + D);
80 assert.ok(commons >= Math.floor((D * SPLIT.commons) / 100), "D=" + D);
81 }
82});
83
84await test("monthsBetween counts whole calendar months", () => {
85 assert.equal(monthsBetween("2026-09-30", "2028-09-29"), 23);
86 assert.equal(monthsBetween("2026-09-30", "2028-09-30"), 24);
87 assert.equal(monthsBetween("2026-09-30", "2028-12-31"), 27);
88 assert.equal(monthsBetween("2026-01-31", "2026-02-28"), 0);
89});
90
91// The implementation lives in web/status/waterfall.js (served by the public
92// Investing page, which imports and runs it); tools/waterfall.mjs is the
93// documented Node-side path. The shim must expose the full surface — this
94// file imports through it, and here we pin that nothing is lost.
95await test("tools/waterfall.mjs re-exports the served module's whole surface", async () => {
96 const served = await import("../web/status/waterfall.js");
97 const shim = await import("../tools/waterfall.mjs");
98 assert.deepEqual(Object.keys(shim).sort(), Object.keys(served).sort());
99});
100
101// The committed public ledger (docs/funding.md § The ledger): the Investing
102// page renders whatever this holds, so its shape is a contract.
103await test("web/status/funding.json holds the ledger shape the page reads", () => {
104 const d = JSON.parse(readFileSync(new URL("../web/status/funding.json", import.meta.url), "utf8"));
105 assert.equal(typeof d.as_of, "string");
106 assert.equal(typeof d.adopted, "boolean");
107 for (const k of ["series", "notes", "quarters"]) assert.ok(Array.isArray(d[k]), k);
108 // Until adoption, the ledger must stay empty — no notes may exist before
109 // the model passes review and a series deliberately opens.
110 if (!d.adopted) for (const k of ["series", "notes", "quarters"]) assert.equal(d[k].length, 0, k);
111});
112
113// The Investing page must run the served module and read the committed
114// ledger — never carry a copy of either. The nav is the SHARED builder
115// (status-nav.js, #1222): pages declare their identity via data-title and
116// the builder marks the current pill at runtime — no static nav markup.
117await test("funding.html imports ./waterfall.js and fetches ./funding.json", () => {
118 const html = readFileSync(new URL("../web/status/funding.html", import.meta.url), "utf8");
119 assert.ok(html.includes('from "./waterfall.js"'));
120 assert.ok(html.includes('fetch("./funding.json"'));
121 assert.ok(html.includes('data-title="Investing"'));
122 assert.ok(html.includes('src="./status-nav.js"'));
123});
124
125// Every status page mounts the shared nav, and the nav carries the
126// Investing tab — the one place the link lives now (#1222).
127await test("all status pages carry the Investing nav link", () => {
128 const nav = readFileSync(new URL("../web/status/status-nav.js", import.meta.url), "utf8");
129 assert.ok(nav.includes('"funding.html"') || nav.includes('"./funding.html"'));
130 assert.ok(nav.includes('"Investing"'));
131 for (const page of ["index", "books", "economics", "carbon", "contributors",
132 "security", "prompts", "ssh", "hardening", "milestones"]) {
133 const html = readFileSync(new URL("../web/status/" + page + ".html", import.meta.url), "utf8");
134 assert.ok(html.includes('src="./status-nav.js"'), page);
135 }
136});
137
138console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`);
139process.exit(failures === 0 ? 0 : 1);
140