/** * sweep.selftest.js — the audit tool audits itself. [Lane B, SPRINT11] * * Same shape as sail.selftest.js / rigging.selftest.js: [name, fn] pairs, so one * set of asserts runs under both Lane A's selftest.html (via js/tests/b.test.js) * and plain node. * * WHY THIS FILE EXISTS, in one sentence: site_audit shipped SPRINT9 and SPRINT10 * with no test of its own, and in SPRINT11 it turned out to have been flying the * corner block with the funnel switched OFF. * * The venturi lives in the SITE json; the sweep built its wind from the STORM def * alone and never called setVenturi, which main.js:424 does at every site load. * So the tool whose entire job is "catch a site that lies about its difficulty" * was itself lying about the difficulty of the only site that has a venturi — * under-reporting every corner load on the yard whose whole personality is the * gap. It reported PASS with $20 lines. That is the SPRINT6 p1=7.4 kN failure * inverted: there the tool called a fine site unriggable, here it called a mean * site cheap. A tool built to catch drift must not be the thing that drifts. * * The assert below is written to FAIL if the setVenturi calls are removed from * sweep.js: with the funnel dropped, the funnelled and unfunnelled sweeps * collapse to the same peak loads and the strict inequality goes red. */ import { AUDIT, auditSweep } from './sweep.js'; import { HARDWARE } from '../../web/world/js/contracts.js'; const TESTS = []; const test = (name, fn) => TESTS.push([name, fn]); const assert = (cond, msg) => { if (!cond) throw new Error(msg); }; /** * A deliberately synthetic yard, not site_02: four anchors around a bed, sized to * sit inside AUDIT.BAND so exactly one quad sweeps. Synthetic because this test * pins the SWEEP's plumbing, not the corner block's balance — site_02's numbers * are C's to tune and would make this assert fail every time they moved. */ const ANCHORS = [ { id: 'a1', type: 'post', pos: { x: -3, y: 3.9, z: -3 } }, { id: 'a2', type: 'post', pos: { x: 3, y: 3.9, z: -3 } }, { id: 'a3', type: 'post', pos: { x: 3, y: 3.9, z: 3 } }, { id: 'a4', type: 'post', pos: { x: -3, y: 3.9, z: 3 } }, ].map((a) => ({ ...a, sway: () => a.pos })); const BED = { x: 0, z: 0, w: 4, d: 4 }; /** A storm blowing dead along +Z, and a funnel whose axis matches it. */ const STORM = { id: 'sweep_selftest_storm', duration: 8, dir: Math.PI / 2, base: 14, gusts: { every: 3, peak: 1.6, downdraftOfTotal: 0.2 }, }; /** Centred on the bed, wide enough to swallow it, aligned with the storm. */ const FUNNEL = [{ x: 0, z: 0, axis: Math.PI / 2, gain: 2.0, radius: 12, sharp: 1 }]; const peaksOf = (venturi) => { const { rows } = auditSweep({ anchors: ANCHORS, bed: BED, stormDef: STORM, venturi }); assert(rows.length > 0, 'sweep selftest yard produced no candidate quad — fix the fixture, not the test'); return rows[0].tiers.map((c) => c.peak); }; test('sweep honours the SITE venturi, not just the storm', () => { const bare = peaksOf([]); const funnelled = peaksOf(FUNNEL); assert(bare.length === funnelled.length, 'the two sweeps disagree about corner count'); // Every corner must pull HARDER through the funnel. Strictly — an equal read // is the exact bug: it means setVenturi never reached the wind. for (let i = 0; i < bare.length; i++) { assert(funnelled[i] > bare[i], `corner ${i}: funnelled peak ${(funnelled[i] / 1000).toFixed(2)} kN is not above bare ` + `${(bare[i] / 1000).toFixed(2)} kN — the site's venturi is not reaching the sweep's wind ` + `(sweep.js must call wind.setVenturi, the way main.js does at every site load)`); } }); test('no venturi is a no-op — a site without a funnel is untouched', () => { // The other half of the contract: backyard_01 ships "venturi": [], and this // fix must not have moved a single number on it. Default arg == explicit []. const omitted = peaksOf(undefined); const empty = peaksOf([]); for (let i = 0; i < empty.length; i++) { assert(Math.abs(omitted[i] - empty[i]) < 1e-9, `corner ${i}: omitting venturi read ${omitted[i]} but [] read ${empty[i]} — a funnel-less site must be byte-identical`); } }); test('pricing reads the ANCHOR, not just the steel — ratingHint reaches tierFor', () => { // SPRINT12: sail.js fails a corner on rating × ratingHint, so the sweep must // price hardware against the same product or it audits a nicer yard than // ships — a fascia (0.35) or carport beam (0.22) corner would be sold a $5 // carabiner that the sim now snaps. Same synthetic yard, one anchor's hint // dropped to 0.01: its corner's demand (peak / hint) leaves the shop's 6.5 kN // ceiling entirely, so its tier must go to NONE while every OTHER corner and // every PEAK stays byte-identical (the hint is a pricing fact, not physics — // the sweep flies unbreakable audit hardware). Written to fail if the hint // is dropped from sweep.js's tierFor call: the two runs collapse into one. const sweep = (anchors) => auditSweep({ anchors, bed: BED, stormDef: STORM, venturi: [] }).rows[0]; const honest = sweep(ANCHORS); const lied = sweep(ANCHORS.map((a) => (a.id === 'a1' ? { ...a, ratingHint: 0.01, sway: a.sway } : a))); for (let i = 0; i < honest.tiers.length; i++) { assert(Math.abs(honest.tiers[i].peak - lied.tiers[i].peak) < 1e-9, `corner ${honest.tiers[i].id}: ratingHint moved a PEAK (${honest.tiers[i].peak} -> ${lied.tiers[i].peak}) — ` + 'the hint is a failure threshold, it must never touch the flown loads'); } const a1 = lied.tiers.find((c) => c.id === 'a1'); const a1honest = honest.tiers.find((c) => c.id === 'a1'); assert(a1honest.tier, 'fixture drift: a1 must be holdable at hint 1 or this test asserts nothing'); assert(!a1.tier, `a1 at hint 0.01 (demand ${(a1.peak / 0.01 / 1000).toFixed(1)} kN vs the 6.5 kN ceiling) still got ` + `hardware ($${a1.tier?.cost}) — sweep.js is pricing bare hw.rating, not rating × ratingHint`); const others = lied.tiers.filter((c) => c.id !== 'a1'); assert(others.every((c) => c.tier && c.tier.cost === honest.tiers.find((h) => h.id === c.id).tier.cost), 'a hint on a1 repriced a DIFFERENT corner — hints must be per-anchor'); assert(lied.unholdable.some((c) => c.id === 'a1'), 'an over-ceiling corner must land in unholdable, or the verdict lies'); }); test('SPRINT18 gate 3: the boundary is DWELL against the fuse, not a percentage of peak', () => { // WHAT THIS REPLACES. SPRINT13 priced every corner twice — `tier` (peak <= // rating) and `cleanTier` (peak <= rating × 0.85) — because C had measured the // bench under-reading the real UI by 5–15% with "cause unfound", so 15% of // taste stood in for it. Gate 2 found the cause (the shipped game never called // `rig.setFabric`, so it flew the membrane while every card said shade cloth); // with the fabric held equal the bench and the full game chain agree to three // decimals. There is no residual left, so the pad is not covering anything. // // But the real reason a percentage had to go is that NO percentage can work. // Measured on the wildnight pin, one flight, two corners on identical $15 // shackles (THREADS [B] SPRINT18 gate 3): // // p1 peak 3.528 kN / 3.20 = 10.3% OVER its rating -> HELD (dwell 0.150 s) // p2 peak 3.493 kN / 3.20 = 9.2% OVER its rating -> BROKE (dwell 0.400 s) // // p1 goes HIGHER than p2 and survives. Ordering by peak is INVERTED against // the outcome, so a rule of the form "peak within X% of rating" is wrong at // every X: 15% condemns p1's perfectly good steel, 9% blesses p2's break. // sail.js has never used peak — it fills an `overload` timer while a corner is // over its rating, bleeds it at OVERLOAD_RECOVER, and lets go past // OVERLOAD_SECS. `priceCandidate` replays exactly that, so the audit and the // sim now answer "will it hold" with the same arithmetic. // // What this fixture can prove: a corner with 8% headroom is now CLEAN (its load // never reaches the rating at all), where the old rule called it marginal — // that IS the re-ruling. Take the dwell replay out of sweep.js and `cleanTier` // falls back to a pad, `cleanHw` stops equalling `hw`, and this goes red. const sweep = (anchors) => auditSweep({ anchors, bed: BED, stormDef: STORM, venturi: [] }); const honest = sweep(ANCHORS); const a1h = honest.rows[0].tiers.find((c) => c.id === 'a1'); assert(a1h.tier, 'fixture drift: a1 must be holdable at hint 1'); assert(honest.verdict.ok && honest.verdict.code === 'pass', 'fixture drift: the honest yard must be a clean pass or this test asserts nothing'); assert(honest.rows[0].tiers.every((c) => c.dwell === 0), `the honest yard burned fuse on ${honest.rows[0].tiers.filter((c) => c.dwell > 0).map((c) => c.id)} — ` + 'a yard nothing is over-loaded on must replay a dwell of exactly 0'); assert(honest.rows[0].cleanHw === honest.rows[0].hw, `cleanHw $${honest.rows[0].cleanHw} != hw $${honest.rows[0].hw} on a yard where no corner ever reaches ` + 'its rating. That gap is the retired 15% pad — the audit is charging for steel the night cannot need'); const RATED = HARDWARE.at(-1); const hint = a1h.peak / (RATED.rating * 0.92); // → best steel holds a1 with 8% headroom const lied = sweep(ANCHORS.map((a) => (a.id === 'a1' ? { ...a, ratingHint: hint, sway: a.sway } : a))); const row = lied.rows[0]; const a1 = row.tiers.find((c) => c.id === 'a1'); assert(a1.tier === RATED, `a1 at the crafted hint must price to the rated shackle, got ${a1.tier?.name}`); assert(a1.headroom > 0 && a1.headroom < AUDIT.MARGIN, `fixture: a1's headroom ${a1.headroom} must sit inside (0, ${AUDIT.MARGIN}) — the band the OLD rule condemned`); assert(a1.dwell === 0, `a1 burned ${a1.dwell}s of fuse at 8% headroom. Headroom means the load never reached the rating, so the ` + 'fuse must never start — if this fires, the replay is reading the wrong quantity'); assert(a1.cleanTier === RATED && row.clean, `8% headroom must now read CLEAN (it did not, got clean=${row.clean}): the steel is simply stronger than ` + 'the night, and calling it a knife edge was the pad talking'); assert(lied.verdict.ok && lied.verdict.code === 'pass', `the verdict must bless a line no corner is ever over, got ${lied.verdict.code}/${lied.verdict.ok}`); // and the other side of the boundary: drop the ceiling UNDER the peak and the // tier is refused outright — this fixture's gusts are seconds long, so any // excursion outlasts the 0.4 s fuse and there is no timing to survive on. const tooWeak = sweep(ANCHORS.map((a) => (a.id === 'a1' ? { ...a, ratingHint: a1h.peak / (RATED.rating * 1.05), sway: a.sway } : a))); const weak = tooWeak.rows[0].tiers.find((c) => c.id === 'a1'); assert(!weak.tier && tooWeak.rows[0].unholdable.some((c) => c.id === 'a1'), `a1 priced to ${weak.tier?.name} with its ceiling 5% UNDER the peak — on a gust that lasts seconds the ` + 'fuse burns through and the corner is simply unholdable'); return 'clean = the fuse never starts (8% headroom passes); over the ceiling on a long gust = unholdable'; }); export const SWEEP_TESTS = TESTS;