| 1 | // 🤖 Contract test for the public status dashboards: the fields each page in |
| 2 | // web/status/ declares it reads (its FIELDS manifest) must exist in the JSON |
| 3 | // deploy/status-publish.sh generates. The fixtures in test/status-fixtures/ |
| 4 | // are the script's own --dry-run output (fleet.json, books.json, |
| 5 | // books-no-expenses.json, books-hetzner-constant.json; regenerate with |
| 6 | // `./deploy/status-publish.sh --dry-run <dir>` after changing the generator — |
| 7 | // the no-expenses variant via STATUS_DRY_EXPENSES=omit, the hetzner-constant |
| 8 | // one via STATUS_DRY_EXPENSES=hetzner-constant) plus the hand-pinned degraded |
| 9 | // shape (books-unavailable.json, matching books_unavailable()). If this |
| 10 | // fails, page and generator have drifted — fix whichever side changed, then |
| 11 | // the fixtures. |
| 12 | // |
| 13 | // The security ledger rides the same pattern with its own generator: |
| 14 | // test/status-fixtures/security.json is deploy/security-scan.sh's --dry-run |
| 15 | // output (the ATTENTION shape, folded from a fixture govulncheck stream full |
| 16 | // of fake CVE/GHSA/GO ids — regenerate with |
| 17 | // `./deploy/security-scan.sh --dry-run test/status-fixtures`), and |
| 18 | // web/status/security.json is the committed real scan. Both must satisfy the |
| 19 | // redaction contract below: status + counts + sha + timestamp, NEVER an |
| 20 | // advisory id or package path (an unpatched-vuln inventory is an attacker |
| 21 | // roadmap — see the scan script's disclosure header). |
| 22 | // node test/status-contract.test.mjs |
| 23 | import assert from "node:assert/strict"; |
| 24 | import { readFileSync } from "node:fs"; |
| 25 | |
| 26 | let failures = 0; |
| 27 | async function test(name, fn) { |
| 28 | try { |
| 29 | await fn(); |
| 30 | console.log("PASS " + name); |
| 31 | } catch (e) { |
| 32 | console.log("FAIL " + name + " — " + (e && e.message)); |
| 33 | failures++; |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | const read = (p) => readFileSync(new URL("../" + p, import.meta.url), "utf8"); |
| 38 | const fleet = JSON.parse(read("test/status-fixtures/fleet.json")); |
| 39 | // One dry-run fixture per capacity band (STATUS_DRY_CAPACITY=… regenerates |
| 40 | // them) — the abstract derivation is pinned at every level. |
| 41 | const fleetGF = JSON.parse(read("test/status-fixtures/fleet-getting-full.json")); |
| 42 | const fleetAC = JSON.parse(read("test/status-fixtures/fleet-at-capacity.json")); |
| 43 | // STATUS_DRY_MESH_PEER=1 regenerates this one — a second, live mesh peer's |
| 44 | // self-published stats merged into hosts.boxes alongside the local box. |
| 45 | const fleetMH = JSON.parse(read("test/status-fixtures/fleet-multi-host.json")); |
| 46 | const books = JSON.parse(read("test/status-fixtures/books.json")); |
| 47 | const booksDown = JSON.parse(read("test/status-fixtures/books-unavailable.json")); |
| 48 | const booksNoExp = JSON.parse(read("test/status-fixtures/books-no-expenses.json")); |
| 49 | const booksHzConst = JSON.parse(read("test/status-fixtures/books-hetzner-constant.json")); |
| 50 | |
| 51 | // Pull the FIELDS manifest out of a page's inline script. |
| 52 | function fields(html) { |
| 53 | const m = html.match(/const FIELDS = \[([\s\S]*?)\];/); |
| 54 | assert.ok(m, "page declares a FIELDS manifest"); |
| 55 | return [...m[1].matchAll(/"([^"]+)"/g)].map((x) => x[1]); |
| 56 | } |
| 57 | |
| 58 | // Resolve a dotted path against an object; "seg[]" descends into a non-empty |
| 59 | // array — ANY element carrying the rest of the path satisfies it (the |
| 60 | // expenses providers are heterogeneous by design: month-to-date providers |
| 61 | // carry month_to_date_cents, flat-rate ones monthly_cents). |
| 62 | function resolve(obj, path) { |
| 63 | const segs = path.split("."); |
| 64 | function walk(cur, i) { |
| 65 | if (i === segs.length) return cur; |
| 66 | if (cur === null || typeof cur !== "object") return undefined; |
| 67 | const seg = segs[i]; |
| 68 | if (seg.endsWith("[]")) { |
| 69 | const arr = cur[seg.slice(0, -2)]; |
| 70 | if (!Array.isArray(arr) || arr.length === 0) return undefined; |
| 71 | for (const el of arr) { |
| 72 | const v = walk(el, i + 1); |
| 73 | if (v !== undefined) return v; |
| 74 | } |
| 75 | return undefined; |
| 76 | } |
| 77 | return walk(cur[seg], i + 1); |
| 78 | } |
| 79 | return walk(obj, 0); |
| 80 | } |
| 81 | |
| 82 | const indexHtml = read("web/status/index.html"); |
| 83 | const booksHtml = read("web/status/books.html"); |
| 84 | // The carbon page reads TWO shapes. `built` is COMMITTED data (accrued by |
| 85 | // tools/carbon-tally.mjs) — the real web/status/carbon.json is its own fixture, |
| 86 | // and carries only { updated_at, methodology, built:{…} } (run absent → page |
| 87 | // shows "pending"). The live `run` block is injected by deploy/status-publish.sh |
| 88 | // at upload; its shape is pinned by the dry-run fixture test/status-fixtures/ |
| 89 | // carbon.json (built + run — regenerate with `./deploy/status-publish.sh |
| 90 | // --dry-run <dir>` then copy <dir>/carbon.json here). |
| 91 | const carbon = JSON.parse(read("web/status/carbon.json")); |
| 92 | const carbonRun = JSON.parse(read("test/status-fixtures/carbon.json")); |
| 93 | const carbonHtml = read("web/status/carbon.html"); |
| 94 | // The contributors index is COMMITTED data too (tools/contributors.mjs). |
| 95 | const contributors = JSON.parse(read("web/status/contributors.json")); |
| 96 | const contributorsHtml = read("web/status/contributors.html"); |
| 97 | // The tier-economics snapshot is COMMITTED data (re-derived by hand from |
| 98 | // docs/tier-economics.md when prices or input rates change) — the real |
| 99 | // web/status/economics.json is its own fixture, like the carbon ledger. |
| 100 | const economics = JSON.parse(read("web/status/economics.json")); |
| 101 | const economicsHtml = read("web/status/economics.html"); |
| 102 | // The music ledger is COMMITTED data too (tools/music-ledger.mjs): |
| 103 | // an index plus one shard per month, so the page never fetches more than a |
| 104 | // month at a time. |
| 105 | const music = JSON.parse(read("web/status/music.json")); |
| 106 | const musicHtml = read("web/status/music.html"); |
| 107 | const musicShards = music.months.map((m) => [m, JSON.parse(read("web/status/music-" + m + ".json"))]); |
| 108 | // The security ledger: the dry-run fixture (attention shape — proves the |
| 109 | // dirty path redacts) plus, when present, the committed real scan. The real |
| 110 | // file may legitimately be absent (the page then shows "scan pending", never |
| 111 | // a fake clean), so it's checked only when it exists. |
| 112 | const securityFix = JSON.parse(read("test/status-fixtures/security.json")); |
| 113 | const securityHtml = read("web/status/security.html"); |
| 114 | let securityLive = null; |
| 115 | try { securityLive = JSON.parse(read("web/status/security.json")); } catch { /* absent = pending, honest */ } |
| 116 | const securityShapes = [["security fixture", securityFix]] |
| 117 | .concat(securityLive ? [["committed security.json", securityLive]] : []); |
| 118 | |
| 119 | await test("fleet page fields all exist in the generator's fleet.json", () => { |
| 120 | for (const f of fields(indexHtml)) { |
| 121 | assert.notEqual(resolve(fleet, f), undefined, "fleet.json is missing " + f); |
| 122 | } |
| 123 | }); |
| 124 | |
| 125 | await test("books page fields all exist in one of the two books shapes", () => { |
| 126 | for (const f of fields(booksHtml)) { |
| 127 | const hit = resolve(books, f) !== undefined || resolve(booksDown, f) !== undefined; |
| 128 | assert.ok(hit, "neither books.json shape has " + f); |
| 129 | } |
| 130 | }); |
| 131 | |
| 132 | await test("carbon page fields resolve — built in committed carbon.json, run in the live fixture", () => { |
| 133 | for (const f of fields(carbonHtml)) { |
| 134 | if (f.startsWith("run.")) { |
| 135 | // run.* is absent from the committed file by design — pinned by the fixture. |
| 136 | assert.notEqual(resolve(carbonRun, f), undefined, "run fixture is missing " + f); |
| 137 | } else { |
| 138 | assert.notEqual(resolve(carbon, f), undefined, "committed carbon.json is missing " + f); |
| 139 | } |
| 140 | } |
| 141 | }); |
| 142 | |
| 143 | await test("contributors page fields all exist in the committed contributors.json", () => { |
| 144 | for (const f of fields(contributorsHtml)) { |
| 145 | assert.notEqual(resolve(contributors, f), undefined, "contributors.json is missing " + f); |
| 146 | } |
| 147 | }); |
| 148 | |
| 149 | await test("economics page fields all exist in the committed economics.json", () => { |
| 150 | for (const f of fields(economicsHtml)) { |
| 151 | assert.notEqual(resolve(economics, f), undefined, "economics.json is missing " + f); |
| 152 | } |
| 153 | }); |
| 154 | |
| 155 | await test("music page fields all exist in the committed music.json index", () => { |
| 156 | for (const f of fields(musicHtml)) { |
| 157 | assert.notEqual(resolve(music, f), undefined, "music.json is missing " + f); |
| 158 | } |
| 159 | }); |
| 160 | |
| 161 | // The music ledger publishes deliberately-chosen content, so its own |
| 162 | // hygiene is structural: month shards that match the index and their own |
| 163 | // filenames, no external art (data URIs only, deduped in the shard's albums |
| 164 | // map — the contributor-avatars rule), skip-filtered rows only (≥60s apart |
| 165 | // per listener, checked across shard boundaries), and signal words from the |
| 166 | // fixed vocabulary — a bare catalog stream carries none, because Music |
| 167 | // can't tell a direct pick from autoplay. |
| 168 | await test("music shards are well-formed, sorted, art self-contained", () => { |
| 169 | assert.match(music.updated_at, /^\d{4}-\d{2}-\d{2}T/, "index updated_at is ISO"); |
| 170 | assert.deepEqual(music.months, [...music.months].sort().reverse(), "index months newest-first"); |
| 171 | const artKey = (t) => t.album_id != null ? String(t.album_id) : t.track + " — " + t.artist; |
| 172 | let total = 0; |
| 173 | const lastBy = new Map(); // per listener, across shards oldest→newest |
| 174 | for (const [m, s] of [...musicShards].reverse()) { |
| 175 | assert.equal(s.month, m, "shard month matches its filename"); |
| 176 | for (const uri of Object.values(s.albums)) { |
| 177 | assert.match(uri, /^data:image\//, m + ": album art must be a data URI"); |
| 178 | } |
| 179 | let prev = ""; |
| 180 | for (const t of s.tracks) { |
| 181 | assert.ok(t.ts >= prev, m + ": not sorted oldest-first at " + t.ts); |
| 182 | prev = t.ts; |
| 183 | assert.equal(t.ts.slice(0, 7), m, "row " + t.ts + " outside its shard month"); |
| 184 | assert.ok(t.track && t.artist && t.album && t.listener, "row is complete"); |
| 185 | assert.ok(["Apple Music", "local file", "library"].includes(t.source), "source word: " + t.source); |
| 186 | assert.ok(Array.isArray(t.signals) && t.signals.every((x) => ["queued", "radio", "shuffle"].includes(x)), |
| 187 | "signals ⊆ {queued,radio,shuffle}: " + JSON.stringify(t.signals)); |
| 188 | assert.ok(!(artKey(t) in s.albums) || typeof s.albums[artKey(t)] === "string", "art key resolves"); |
| 189 | const last = lastBy.get(t.listener); |
| 190 | if (last && last.track !== t.track) { |
| 191 | assert.ok(Date.parse(t.ts) - Date.parse(last.ts) >= 60_000, |
| 192 | "skip leaked: " + last.track + " held under 60s before " + t.track); |
| 193 | } |
| 194 | lastBy.set(t.listener, t); |
| 195 | } |
| 196 | total += s.tracks.length; |
| 197 | } |
| 198 | assert.equal(music.totals.tracks, total, "index totals.tracks is the shard sum"); |
| 199 | }); |
| 200 | |
| 201 | await test("security page fields all exist in the scanner's security.json", () => { |
| 202 | for (const [name, s] of securityShapes) { |
| 203 | for (const f of fields(securityHtml)) { |
| 204 | assert.notEqual(resolve(s, f), undefined, name + " is missing " + f); |
| 205 | } |
| 206 | } |
| 207 | }); |
| 208 | |
| 209 | // Contributor entries stay well-formed as the tool regenerates them: known |
| 210 | // kinds, positive commit counts, sorted by the metric, humans carry a profile |
| 211 | // link + self-contained data-URI avatar (the pages allow no external hosts), |
| 212 | // and no email address ever reaches the published JSON. |
| 213 | await test("contributors entries are well-formed, sorted, and email-free", () => { |
| 214 | let prev = Infinity; |
| 215 | for (const c of contributors.contributors) { |
| 216 | assert.ok(["human", "ai"].includes(c.kind), "kind: " + c.kind); |
| 217 | assert.ok(Number.isInteger(c.commits) && c.commits > 0, c.id + ": commits"); |
| 218 | assert.ok(c.commits <= prev, "not sorted by commits at " + c.id); |
| 219 | prev = c.commits; |
| 220 | assert.ok(c.name && c.id, "name + id present"); |
| 221 | if (c.kind === "human") { |
| 222 | assert.match(c.url, /^https:\/\/github\.com\//, c.id + ": profile url"); |
| 223 | assert.match(c.avatar, /^data:image\//, c.id + ": avatar must be a data URI"); |
| 224 | } |
| 225 | } |
| 226 | assert.ok(!/[\w.+-]+@[\w-]+\.[\w.]+/.test(JSON.stringify(contributors).replace(/data:image\/[^"]+/g, "")), |
| 227 | "an email address leaked into contributors.json"); |
| 228 | }); |
| 229 | |
| 230 | // The tier-economics snapshot must stay internally consistent as it's |
| 231 | // re-derived (docs/tier-economics.md: the formulas are the contract, the |
| 232 | // numbers a snapshot): net follows the Stripe fee line exactly (the two fee |
| 233 | // constants below mirror the input-rates table — update both together), |
| 234 | // profit ≈ net − cost within the doc's ~rounding, margin is profit over |
| 235 | // GROSS, and every tier's included allowance sits below its break-even — |
| 236 | // the "no tier can structurally lose money" claim the page makes in prose. |
| 237 | await test("economics snapshot arithmetic + structurally-profitable claim hold", () => { |
| 238 | assert.match(economics.as_of, /^\d{4}-\d{2}-\d{2}$/, "as_of is a date"); |
| 239 | assert.equal(economics.currency, "eur"); |
| 240 | const q = economics.free_spaces_per_account; |
| 241 | assert.ok(Number.isInteger(q) && q >= 1, "free-space quota is a positive integer"); |
| 242 | assert.ok(economics.price_book.length > 0 && economics.storage_allowances.length > 0 |
| 243 | && economics.input_rates.length > 0 && economics.estimates.length > 0, "all four tables non-empty"); |
| 244 | for (const p of economics.price_book) { |
| 245 | assert.ok(Number.isInteger(p.yearly_cents) && p.yearly_cents > 0, p.tier + ": yearly_cents is integer cents"); |
| 246 | if ("monthly_cents" in p) { |
| 247 | assert.ok(Number.isInteger(p.monthly_cents) && p.monthly_cents > 0, p.tier + ": monthly_cents is integer cents"); |
| 248 | // A monthly toggle must cost more per year than the annual price, or |
| 249 | // the annual default the page defends would be the worse deal. |
| 250 | assert.ok(12 * p.monthly_cents >= p.yearly_cents, p.tier + ": 12×monthly ≥ yearly"); |
| 251 | } |
| 252 | } |
| 253 | const allowance = new Map(economics.storage_allowances.map((s) => [s.tier, s.gb])); |
| 254 | for (const s of economics.storage_allowances) { |
| 255 | assert.ok(Number.isInteger(s.gb) && s.gb > 0, s.tier + ": allowance is a positive GB integer"); |
| 256 | } |
| 257 | for (const e of economics.estimates) { |
| 258 | const label = e.plan + " · " + e.billing; |
| 259 | const charges = e.billing === "monthly" ? 12 : 1; |
| 260 | // Stripe fee constants (1.5% + €0.25 per charge) from the input-rates table. |
| 261 | const net = e.gross_year_cents - 25 * charges - 0.015 * e.gross_year_cents; |
| 262 | assert.ok(Math.abs(e.net_year_cents - net) <= 1, |
| 263 | label + ": net must follow the fee model (" + e.net_year_cents + " vs " + net.toFixed(1) + ")"); |
| 264 | assert.ok(e.cost_year_cents > 0 && e.cost_year_cents < e.net_year_cents, label + ": cost sits inside net"); |
| 265 | const tol = Math.max(5, Math.round(e.gross_year_cents * 0.005)); // costs are ~ estimates |
| 266 | assert.ok(Math.abs(e.profit_year_cents - (e.net_year_cents - e.cost_year_cents)) <= tol, |
| 267 | label + ": profit ≈ net − cost (" + e.profit_year_cents + ")"); |
| 268 | assert.ok(Math.abs(e.margin_pct - (100 * e.profit_year_cents) / e.gross_year_cents) <= 1, |
| 269 | label + ": margin is profit over gross"); |
| 270 | const gb = allowance.get(e.plan); |
| 271 | assert.ok(Number.isInteger(gb), label + ": plan has a storage-allowance row"); |
| 272 | assert.ok(e.break_even_gb > gb, label + ": allowance must sit below break-even (structurally profitable)"); |
| 273 | } |
| 274 | }); |
| 275 | |
| 276 | // A manifest entry nobody reads is drift too: each leaf property must appear |
| 277 | // in the page's render code, not just the manifest. |
| 278 | await test("every declared field is actually read by its page", () => { |
| 279 | for (const [html, fs] of [[indexHtml, fields(indexHtml)], [booksHtml, fields(booksHtml)], [carbonHtml, fields(carbonHtml)], [contributorsHtml, fields(contributorsHtml)], [securityHtml, fields(securityHtml)], [economicsHtml, fields(economicsHtml)], [musicHtml, fields(musicHtml)]]) { |
| 280 | const body = html.slice(html.indexOf("];") + 2); |
| 281 | for (const f of fs) { |
| 282 | const leaf = f.split(".").pop().replace("[]", ""); |
| 283 | assert.ok(body.includes(leaf), "manifest field " + f + " is never used"); |
| 284 | } |
| 285 | } |
| 286 | }); |
| 287 | |
| 288 | // The degraded shape must be renderable: available flips the page, reason is |
| 289 | // the message. |
| 290 | await test("degraded books shape carries available:false + reason", () => { |
| 291 | assert.equal(booksDown.available, false); |
| 292 | assert.equal(typeof booksDown.reason, "string"); |
| 293 | }); |
| 294 | |
| 295 | // Privacy re-check on the committed fixtures (the dry-run asserts this too — |
| 296 | // belt and suspenders on what's in git): no fixture hostname, subscription, |
| 297 | // customer, transaction, or provider-account/workspace id may appear in |
| 298 | // published output. |
| 299 | await test("fixtures contain no per-payer ids or hostnames", () => { |
| 300 | for (const j of [fleet, fleetGF, fleetAC, books, booksDown, booksNoExp, booksHzConst, carbon, carbonRun, contributors, economics, ...securityShapes.map(([, s]) => s)]) { |
| 301 | const s = JSON.stringify(j); |
| 302 | assert.ok(!/sub_|cus_|txn_|wrkspc_|req_fixture|ce_fixture|dnsfix|hzfix|\.example|\.sock/.test(s), "leak in " + s.slice(0, 80)); |
| 303 | } |
| 304 | }); |
| 305 | |
| 306 | // ---- the security ledger: redacted by contract -------------------------------- |
| 307 | // The disclosure line (deploy/security-scan.sh header): the published block is |
| 308 | // a STATUS + COUNT only. The schema is an ALLOWLIST — any extra key fails, so |
| 309 | // an advisory id, package path, or version list can't ship by accident. |
| 310 | await test("security ledger carries only the redacted hygiene schema", () => { |
| 311 | for (const [name, s] of securityShapes) { |
| 312 | assert.deepEqual(Object.keys(s).sort(), ["dependencies", "generated_at", "go_modules", "scanned_sha"], name + ": keys"); |
| 313 | assert.match(s.generated_at, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/, name + ": generated_at is ISO UTC"); |
| 314 | // The build sha is already public via the signed release feed — the one |
| 315 | // version identifier the page may carry. |
| 316 | assert.match(s.scanned_sha, /^[0-9a-f]{7,40}$/, name + ": scanned_sha is a git sha"); |
| 317 | assert.ok(Number.isInteger(s.go_modules) && s.go_modules > 0, name + ": go_modules is a positive count"); |
| 318 | assert.deepEqual(Object.keys(s.dependencies).sort(), ["advisories_under_remediation", "status"], name + ": dependency keys"); |
| 319 | assert.ok(["clean", "attention", "pending"].includes(s.dependencies.status), |
| 320 | name + ": status is a vocabulary word, got " + s.dependencies.status); |
| 321 | const n = s.dependencies.advisories_under_remediation; |
| 322 | assert.ok(Number.isInteger(n) && n >= 0, name + ": count is an integer ≥ 0"); |
| 323 | // Status and count must agree — "clean" beside a nonzero count (or the |
| 324 | // reverse) would be a lie in one direction or the other. |
| 325 | assert.equal(n > 0, s.dependencies.status === "attention", name + ": status agrees with the count"); |
| 326 | } |
| 327 | }); |
| 328 | |
| 329 | // The redaction guard: no advisory id in ANY grammar (Go vulndb GO-…, CVE, |
| 330 | // GHSA) — an unpatched finding publishes as a count, never a name. The |
| 331 | // fixture was folded from a stream FULL of such ids, so this proves the |
| 332 | // generator strips them, not merely that the input was clean. |
| 333 | await test("security ledger never names an advisory", () => { |
| 334 | for (const [name, s] of securityShapes) { |
| 335 | const t = JSON.stringify(s); |
| 336 | assert.ok(!/CVE-\d{4}-\d+|GHSA-|GO-\d{4}-\d+/i.test(t), name + " carries an advisory id: " + t.slice(0, 120)); |
| 337 | } |
| 338 | }); |
| 339 | |
| 340 | // Pin the dirty path end-to-end: the dry-run's fixture stream carries two |
| 341 | // advisories with symbol-level findings (one of them twice — findings dedupe |
| 342 | // by advisory) plus a module-level-only informational one that must NOT |
| 343 | // count. Redacted result: attention + 2, and nothing else. |
| 344 | await test("security dry-run fixture folds to attention + count 2", () => { |
| 345 | assert.equal(securityFix.dependencies.status, "attention"); |
| 346 | assert.equal(securityFix.dependencies.advisories_under_remediation, 2); |
| 347 | assert.equal(securityFix.scanned_sha, "abc1234"); |
| 348 | assert.equal(securityFix.go_modules, 42); |
| 349 | }); |
| 350 | |
| 351 | // Expenses publish ITEMIZED by design — decision #19's other half (income |
| 352 | // aggregates, expenses itemize; the amounts are public on purpose). The |
| 353 | // schema is still an ALLOWLIST: provider word + EXACTLY ONE amount key |
| 354 | // (month_to_date_cents for metered providers, monthly_cents / yearly_cents |
| 355 | // for flat subscriptions, to_date_cents for a cumulative operator-attested |
| 356 | // lifetime figure — each as billed: a yearly fee is never divided into a |
| 357 | // fake monthly, a lifetime figure never smeared across months) + its own |
| 358 | // billing currency, nothing else — so an account id, invoice id, or |
| 359 | // resource-level line can't ship by accident. Currencies stay per-provider, |
| 360 | // never FX-converted, and only usd/eur exist today. |
| 361 | await test("expenses carry only the allowed itemized schema", () => { |
| 362 | const exp = books.expenses; |
| 363 | assert.ok(exp, "books fixture carries expenses"); |
| 364 | assert.deepEqual(Object.keys(exp).sort(), ["month", "providers"], "expenses keys"); |
| 365 | assert.match(exp.month, /^\d{4}-\d{2}$/, "expenses.month is YYYY-MM"); |
| 366 | // Each provider's key set carries exactly one of the four amount keys — |
| 367 | // the deepEqual below enforces the exactly-one rule per provider. |
| 368 | const allowed = { |
| 369 | "aws": ["currency", "month_to_date_cents", "provider"], |
| 370 | "hetzner": ["currency", "monthly_cents", "provider"], |
| 371 | "anthropic": ["currency", "month_to_date_cents", "provider"], |
| 372 | "claude.ai": ["currency", "provider", "to_date_cents"], |
| 373 | "dnsimple": ["currency", "month_to_date_cents", "provider"], |
| 374 | "apple": ["currency", "provider", "yearly_cents"], |
| 375 | }; |
| 376 | // The dry-run's fixed fixture exercises every provider, in publish order. |
| 377 | assert.deepEqual(exp.providers.map((p) => p.provider), |
| 378 | ["aws", "hetzner", "anthropic", "claude.ai", "dnsimple", "apple"], "fixture exercises all providers"); |
| 379 | for (const p of exp.providers) { |
| 380 | assert.ok(p.provider in allowed, "unknown provider " + p.provider); |
| 381 | assert.deepEqual(Object.keys(p).sort(), allowed[p.provider], p.provider + " keys"); |
| 382 | assert.ok(["usd", "eur"].includes(p.currency), p.provider + " currency ∈ {usd,eur}, got " + p.currency); |
| 383 | const cents = p.month_to_date_cents ?? p.monthly_cents ?? p.yearly_cents ?? p.to_date_cents; |
| 384 | assert.ok(Number.isInteger(cents) && cents >= 0, p.provider + " amount is integer cents ≥ 0"); |
| 385 | } |
| 386 | }); |
| 387 | |
| 388 | // No providers resolve → the whole expenses object is omitted (degrade, never |
| 389 | // zero, never guess). The degraded income shape needn't carry it either. |
| 390 | await test("expenses omit entirely when no provider resolves", () => { |
| 391 | assert.ok(!("expenses" in booksNoExp), "no-expenses fixture must omit the key"); |
| 392 | assert.equal(booksNoExp.available, true, "omission is independent of income availability"); |
| 393 | }); |
| 394 | |
| 395 | // Hetzner's figure is API-computed when the operator constant is unset: the |
| 396 | // default fixture pins the servers-API math (two dry-run fixture pages, each |
| 397 | // server priced by the ONE server_type.prices[] entry matching its own |
| 398 | // location — gross monthly EUR 16.4288 + 4.5100 = 20.9388, rounded once at |
| 399 | // the total to 2094 cents). And when STATUS_HETZNER_MONTHLY_CENTS is set the |
| 400 | // constant wins over a resolvable API path — operator truth beats computed. |
| 401 | // Either way the key shape is identical (monthly_cents, eur). |
| 402 | await test("hetzner: API-computed cents with rounding; constant overrides", () => { |
| 403 | const hz = books.expenses.providers.find((p) => p.provider === "hetzner"); |
| 404 | assert.deepEqual(hz, { provider: "hetzner", monthly_cents: 2094, currency: "eur" }, |
| 405 | "API path: location-matched gross sum, rounded"); |
| 406 | const hzc = booksHzConst.expenses.providers.find((p) => p.provider === "hetzner"); |
| 407 | assert.deepEqual(hzc, { provider: "hetzner", monthly_cents: 1400, currency: "eur" }, |
| 408 | "constant override: the operator figure publishes, same shape"); |
| 409 | }); |
| 410 | |
| 411 | // The carbon tally must stay internally consistent as entries accrue — the |
| 412 | // totals are the sum of the ledger, each entry's kg derives from its output |
| 413 | // tokens × the published factors, and each SOURCE's periods are chronological |
| 414 | // and non-overlapping (tools/carbon-tally.mjs appends strictly after that |
| 415 | // source's last period_end; an overlap means two branches double-counted the |
| 416 | // same window — drop the younger entry and re-run --write). Sources are |
| 417 | // anonymous name-pair labels, same rule as the fleet's box labels: the tally |
| 418 | // is public, so never a hostname or username. |
| 419 | await test("carbon tally arithmetic + per-source chronology hold", () => { |
| 420 | const b = carbon.built; |
| 421 | const f = b.factors_mg_co2e_per_output_token; |
| 422 | assert.ok(f.low < f.central && f.central < f.high, "factors ordered"); |
| 423 | let out = 0, kg = { low: 0, central: 0, high: 0 }; |
| 424 | const prevEnd = new Map(); |
| 425 | for (const e of b.entries) { |
| 426 | assert.match(e.source, /^[a-z]+-[a-z]+$/, "source must be a name-pair label, got " + e.source); |
| 427 | assert.ok(["measured", "extrapolated"].includes(e.basis), "basis word: " + e.basis); |
| 428 | assert.ok(Date.parse(e.period_start) < Date.parse(e.period_end), "period runs forward"); |
| 429 | assert.ok(Date.parse(e.period_start) >= (prevEnd.get(e.source) ?? -Infinity), |
| 430 | e.source + " periods overlap at " + e.period_start); |
| 431 | prevEnd.set(e.source, Date.parse(e.period_end)); |
| 432 | for (const band of ["low", "central", "high"]) { |
| 433 | const want = (e.output_tokens * f[band]) / 1e6; // mg → kg |
| 434 | assert.ok(Math.abs(e["kg_" + band] - want) <= 0.06, |
| 435 | "entry kg_" + band + " ≠ tokens × factor (" + e["kg_" + band] + " vs " + want.toFixed(2) + ")"); |
| 436 | kg[band] += e["kg_" + band]; |
| 437 | } |
| 438 | out += e.output_tokens; |
| 439 | } |
| 440 | assert.equal(b.totals.output_tokens, out, "built.totals.output_tokens is the ledger sum"); |
| 441 | for (const band of ["low", "central", "high"]) { |
| 442 | assert.ok(Math.abs(b.totals["kg_" + band] - kg[band]) <= 0.05, |
| 443 | "built.totals.kg_" + band + " is the ledger sum"); |
| 444 | } |
| 445 | assert.equal(Date.parse(carbon.updated_at), Math.max(...b.entries.map((e) => Date.parse(e.period_end))), |
| 446 | "updated_at is the newest built period_end"); |
| 447 | }); |
| 448 | |
| 449 | // The injected run block: an ALLOWLIST schema (any extra key fails, so a needle |
| 450 | // can't ship by accident), banded low≤central≤high, aggregates only — no |
| 451 | // hostname/IP/socket, and the per-space denominator is the public instance |
| 452 | // count, NEVER per-member. |
| 453 | await test("run block carries only the allowed operational schema, banded, no needles", () => { |
| 454 | const r = carbonRun.run; |
| 455 | assert.deepEqual(Object.keys(r).sort(), |
| 456 | ["cumulative_kg", "daily_kg", "generated_at", "grid_g_per_kwh", "per_space_daily_kg", |
| 457 | "pue", "services", "since", "spaces", "vcpus", "watts"], "run keys"); |
| 458 | assert.match(r.generated_at, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/, "generated_at is ISO UTC"); |
| 459 | assert.match(r.since, /^\d{4}-\d{2}-\d{2}$/, "since is a date"); |
| 460 | assert.ok(Number.isInteger(r.spaces) && r.spaces > 0, "spaces is a positive instance count"); |
| 461 | assert.ok(Number.isInteger(r.vcpus) && r.vcpus > 0, "vcpus is a positive count"); |
| 462 | assert.ok(typeof r.pue === "number" && r.pue >= 1, "pue is a real ≥ 1"); |
| 463 | for (const band of ["watts", "grid_g_per_kwh", "daily_kg", "cumulative_kg", "per_space_daily_kg"]) { |
| 464 | assert.deepEqual(Object.keys(r[band]).sort(), ["central", "high", "low"], band + " is banded"); |
| 465 | assert.ok(r[band].low <= r[band].central && r[band].central <= r[band].high, band + " ordered low≤central≤high"); |
| 466 | } |
| 467 | for (const s of r.services) { |
| 468 | assert.deepEqual(Object.keys(s).sort(), ["daily_kg_central", "name"], "service " + s.name + " keys"); |
| 469 | assert.ok(typeof s.name === "string" && s.name.length > 0, "service name"); |
| 470 | assert.ok(typeof s.daily_kg_central === "number" && s.daily_kg_central >= 0, s.name + " daily kg ≥ 0"); |
| 471 | } |
| 472 | const t = JSON.stringify(r); |
| 473 | assert.ok(!/\d+\.\d+\.\d+\.\d+|[a-z0-9-]+\.(sock|example|com|net|chat|at)\b/i.test(t), |
| 474 | "run block reads like a hostname/IP: " + t.slice(0, 80)); |
| 475 | }); |
| 476 | |
| 477 | // The run math folds server_type + region grid → watts → kg exactly, so the |
| 478 | // generator's arithmetic can't drift (mirrors deploy/status-publish.sh's |
| 479 | // carbon_run_object + its dry-run assertions). 6 vCPUs (cpx31=4 + cx22=2), the |
| 480 | // Germany fixture grid 350 g/kWh, W/vCPU central 3.5, PUE 1.15, 10-day span, 6 |
| 481 | // spaces. |
| 482 | await test("run fixture folds server_type + grid to the asserted watts→kg math", () => { |
| 483 | const r = carbonRun.run; |
| 484 | assert.equal(r.vcpus, 6, "vCPUs summed across the servers fixture"); |
| 485 | assert.equal(r.watts.central, 21, "6 vCPU × 3.5 W"); |
| 486 | assert.equal(r.grid_g_per_kwh.central, 350, "Germany fixture grid"); |
| 487 | assert.equal(r.pue, 1.15); |
| 488 | // 6 × 3.5 × 24 × 350 × 1.15 / 1e6 + 0.001 (AWS) + 0.005 (inference) = 0.20886. |
| 489 | assert.equal(r.daily_kg.central, 0.20886, "daily central kg"); |
| 490 | assert.equal(r.cumulative_kg.central, 2.0886, "cumulative = daily × 10 days"); |
| 491 | assert.equal(r.per_space_daily_kg.central, 0.03481, "per space = daily ÷ 6 spaces"); |
| 492 | // The service breakdown's central terms sum to the daily central (router term |
| 493 | // is the watts→kg piece; AWS + inference are the small explicit terms). |
| 494 | const sum = r.services.reduce((a, s) => a + s.daily_kg_central, 0); |
| 495 | assert.ok(Math.abs(sum - r.daily_kg.central) <= 1e-6, "services sum to the daily central"); |
| 496 | }); |
| 497 | |
| 498 | // Capacity + hosts publish ABSTRACT levels only (Paul's call: bucketed words, |
| 499 | // never needles). The schema is an ALLOWLIST — any extra key under hosts / |
| 500 | // capacity fails, so a precise number (remaining slots, GB, percentages, cap |
| 501 | // values) can't ship by accident, and the level/load words themselves must be |
| 502 | // digit-free vocabulary entries. |
| 503 | const fleetShapes = [ |
| 504 | ["fleet", fleet], ["fleet-getting-full", fleetGF], ["fleet-at-capacity", fleetAC], |
| 505 | ["fleet-multi-host", fleetMH], |
| 506 | ]; |
| 507 | await test("hosts + capacity carry only the allowed abstract schema", () => { |
| 508 | for (const [name, f] of fleetShapes) { |
| 509 | assert.deepEqual(Object.keys(f.capacity).sort(), ["level"], name + ": capacity keys"); |
| 510 | assert.ok(["plenty", "getting-full", "at-capacity"].includes(f.capacity.level), |
| 511 | name + ": capacity.level is a band word, got " + f.capacity.level); |
| 512 | assert.deepEqual(Object.keys(f.hosts).sort(), ["boxes", "count"], name + ": hosts keys"); |
| 513 | assert.equal(f.hosts.count, f.hosts.boxes.length, name + ": count matches boxes"); |
| 514 | for (const b of f.hosts.boxes) { |
| 515 | // region is optional (omitted when the operator sets none). When present |
| 516 | // it must be a coarse public area — continent + country — and must never |
| 517 | // read like an IP or hostname (no dots-and-digits / label.tld shapes). |
| 518 | // always_on is likewise optional: a REMOTE mesh peer's self-published |
| 519 | // snapshot (mesh_peer_boxes) never carries it — that's a local-box-only |
| 520 | // concept — so a peer's key set is the "required" set on its own, |
| 521 | // while the LOCAL box always adds always_on. |
| 522 | const required = ["asleep", "awake", "label", "load"]; |
| 523 | const keys = Object.keys(b).sort(); |
| 524 | const allowed = required.concat(["always_on", "region"]).sort(); |
| 525 | assert.ok(required.every((k) => keys.includes(k)), name + ": box missing a required key: " + keys); |
| 526 | assert.ok(keys.every((k) => allowed.includes(k)), name + ": box carries a disallowed key: " + keys); |
| 527 | if ("region" in b) { |
| 528 | assert.equal(typeof b.region, "string", name + ": region is a string"); |
| 529 | assert.ok(!/\d+\.\d+|[a-z0-9-]+\.[a-z]{2,}/i.test(b.region), |
| 530 | name + ": region reads like an IP/hostname: " + b.region); |
| 531 | } |
| 532 | // Labels are anonymous ordinals — never a hostname, IP, or URL. |
| 533 | assert.match(b.label, /^[a-z]+-[a-z]+$/, name + ": box label must be a name-action pair"); |
| 534 | assert.ok([null, "comfortable", "busy", "critical"].includes(b.load), |
| 535 | name + ": box load is a band word or null, got " + b.load); |
| 536 | } |
| 537 | // No digit may ride in the capacity subtree — the band word is the entire |
| 538 | // public surface. |
| 539 | assert.ok(!/\d/.test(JSON.stringify(f.capacity)), name + ": capacity carries a number"); |
| 540 | } |
| 541 | }); |
| 542 | |
| 543 | // The multi-host fixture specifically: a second, live mesh peer merges into |
| 544 | // hosts.boxes without displacing the local box, and count reflects both. |
| 545 | await test("multi-host fixture merges the local box with a live mesh peer", () => { |
| 546 | assert.equal(fleetMH.hosts.count, 2, "hosts.count reflects both boxes"); |
| 547 | assert.equal(fleetMH.hosts.boxes.length, 2); |
| 548 | const [local, peer] = fleetMH.hosts.boxes; |
| 549 | assert.ok("always_on" in local, "the local box still reports always_on"); |
| 550 | assert.ok(!("always_on" in peer), "a remote peer never reports always_on (a genuinely local-box-only concept)"); |
| 551 | assert.equal(peer.region, "UK · London", "a peer's own LCHAT_BOX_REGION rides through the merge verbatim"); |
| 552 | assert.notEqual(local.label, peer.label, "the two boxes must have distinct labels"); |
| 553 | }); |
| 554 | |
| 555 | // The local box's own row must prefer its self-published, ownership-filtered |
| 556 | // snapshot (mesh-self-stats.json in the dry-run fixture: awake=1) over its |
| 557 | // raw local `ss` sweep (which would show awake=2, see the single-box `fleet` |
| 558 | // fixture) — otherwise this box's own row still overlaps a peer's, even once |
| 559 | // the peer's row is correctly filtered. |
| 560 | await test("the local box's own row prefers its self-published stats over its raw ss sweep", () => { |
| 561 | const [local] = fleetMH.hosts.boxes; |
| 562 | assert.equal(local.awake, 1, "local.awake must come from the self-published snapshot, not the unfiltered ss sweep (2)"); |
| 563 | assert.notEqual(local.awake, fleet.hosts.boxes[0].awake, |
| 564 | "a regression would make the mesh-off and mesh-on local rows identical again"); |
| 565 | }); |
| 566 | |
| 567 | // A host genuinely running on a mesh peer has no local socket on this box, so |
| 568 | // the top-level instances.awake/asleep must sum every box's own verified |
| 569 | // split, not just this box's — otherwise a peer-hosted instance reads as |
| 570 | // "asleep" fleet-wide even while it's actually awake there. |
| 571 | await test("top-level awake/asleep are fleet-wide, not just the local box", () => { |
| 572 | const [local, peer] = fleetMH.hosts.boxes; |
| 573 | assert.equal(fleetMH.instances.awake, local.awake + peer.awake, |
| 574 | "instances.awake must sum every box's own awake count"); |
| 575 | assert.equal(fleetMH.instances.asleep, fleetMH.instances.hibernating - fleetMH.instances.awake, |
| 576 | "instances.asleep must be hibernating minus the fleet-wide awake total"); |
| 577 | assert.notEqual(fleetMH.instances.awake, local.awake, |
| 578 | "a regression would silently drop back to only the local box's awake count"); |
| 579 | }); |
| 580 | |
| 581 | // Each committed fixture set derives its band — pins the 70/90 thresholds and |
| 582 | // the disk-floor rule in the generator (see dry_run's capacity fixtures). |
| 583 | await test("capacity fixtures derive plenty / getting-full / at-capacity", () => { |
| 584 | assert.equal(fleet.capacity.level, "plenty"); |
| 585 | assert.equal(fleetGF.capacity.level, "getting-full"); |
| 586 | assert.equal(fleetAC.capacity.level, "at-capacity"); |
| 587 | assert.equal(fleet.hosts.boxes[0].load, "comfortable"); |
| 588 | // getting-full is driven by the registered ratio, not memory — capacity and |
| 589 | // box load are independent signals. |
| 590 | assert.equal(fleetGF.hosts.boxes[0].load, "comfortable"); |
| 591 | assert.equal(fleetAC.hosts.boxes[0].load, "critical"); |
| 592 | // The STATUS_BOX_REGION seam: the dry-run's fixed coarse region rides |
| 593 | // through to the box row verbatim. |
| 594 | assert.equal(fleet.hosts.boxes[0].region, "EU · Germany"); |
| 595 | }); |
| 596 | |
| 597 | // ---- the shared masthead + nav (status-nav.js): one copy, no drift ------------ |
| 598 | // The pages drifted while each carried its own copy of the masthead + nav |
| 599 | // (stale link lists, bare h1 overrides quietly moving the masthead between |
| 600 | // pages). Now every dashboard ships two empty mounts filled by the one |
| 601 | // builder — so no page may carry an inline copy, no page CSS may reach the |
| 602 | // shared masthead with a bare h1 selector, and the builder's GROUPS must |
| 603 | // cover exactly the dashboard pages that exist. |
| 604 | await test("status pages all mount the shared masthead + nav, no inline copies", () => { |
| 605 | const navJs = read("web/status/status-nav.js"); |
| 606 | const navPages = [...navJs.matchAll(/\["([a-z-]+\.html)"/g)].map((m) => m[1]); |
| 607 | const pages = ["index.html", "hardening.html", "security.html", "books.html", "economics.html", |
| 608 | "funding.html", "carbon.html", "contributors.html", "prompts.html", "ssh.html", "music.html", |
| 609 | "roadmap.html", "milestones.html"]; |
| 610 | assert.deepEqual([...navPages].sort(), [...pages].sort(), "GROUPS covers exactly the dashboard pages"); |
| 611 | for (const p of pages) { |
| 612 | const html = read("web/status/" + p); |
| 613 | assert.match(html, /<header data-title="[^"]+"><\/header>/, p + ": masthead mount missing"); |
| 614 | assert.ok(html.includes('<nav class="status-nav" aria-label="Status"></nav>'), p + ": empty nav mount missing"); |
| 615 | assert.ok(html.includes('src="./status-nav.js"'), p + ": shared include missing"); |
| 616 | assert.ok(!html.includes("group-label"), p + ": carries an inline nav copy"); |
| 617 | assert.ok(!html.includes('class="brand"'), p + ": carries an inline masthead copy"); |
| 618 | const style = (html.match(/<style>([\s\S]*?)<\/style>/) || [, ""])[1]; |
| 619 | assert.ok(!/(^|[^-\w#.])h1\s*\{/m.test(style), p + ": a bare h1 selector reaches the shared masthead"); |
| 620 | } |
| 621 | }); |
| 622 | |
| 623 | if (failures > 0) { |
| 624 | console.error(failures + " failure(s)"); |
| 625 | process.exit(1); |
| 626 | } |
| 627 | |