Download tests/study.test.mjs from skailiner/threshold: direct link, hf CLI and curl.
- Browser
- Download file 8.53 kB
-
https://huggingface.co/spaces/skailiner/threshold/resolve/main/tests/study.test.mjs
- Command line
-
hf download hf://spaces/skailiner/threshold/tests/study.test.mjs
-
curl -L -o study.test.mjs https://huggingface.co/spaces/skailiner/threshold/resolve/main/tests/study.test.mjs
8.53 kB
| import assert from "node:assert/strict"; | |
| import { defaults, climates, limits, simulateDay, summarize } from "../build/model.mjs"; | |
| import { | |
| StudyWorkspace, | |
| newStudy, | |
| validateStudy, | |
| saveStudy, | |
| parseStudy, | |
| evaluateStudy, | |
| studyBrief, | |
| comparisonSentence, | |
| peakInterval, | |
| signed, | |
| STUDY_LIMIT, | |
| } from "../build/study.mjs"; | |
| let count = 0; | |
| const test = (label, run) => { | |
| run(); | |
| count++; | |
| console.log("PASS", label); | |
| }; | |
| const near = (a, b, e = 1e-9) => assert.ok(Math.abs(a - b) < e, `${a} != ${b}`); | |
| const pair = (climate, A, B) => ({ | |
| ...newStudy(), | |
| climate, | |
| alternatives: { A: { name: "Starting point", design: A }, B: { name: "Test", design: B } }, | |
| }); | |
| test("Strict study round-trips all design boundaries and viewing fields", () => { | |
| let cases = 0; | |
| for (const climate of Object.keys(climates)) | |
| for (const active of ["A", "B"]) | |
| for (const lens of ["light", "heat", "air"]) | |
| for (const end of [0, 1]) { | |
| const design = Object.fromEntries( | |
| Object.entries(limits) | |
| .filter(([key]) => key !== "hour") | |
| .map(([key, bounds]) => [key, bounds[end]]), | |
| ); | |
| const s = { ...pair(climate, design, defaults), active, lens, hour: end ? 23 : 0 }; | |
| assert.deepEqual(parseStudy(saveStudy(s)), s); | |
| cases++; | |
| } | |
| assert.equal(cases, 36); | |
| }); | |
| test("Reject malformed alternatives, unknown/derived fields, coercions and unsafe names", () => { | |
| const base = newStudy(); | |
| for (const mutation of [ | |
| (s) => (s.summary = {}), | |
| (s) => (s.model = "future"), | |
| (s) => (s.active = "C"), | |
| (s) => (s.hour = 24), | |
| (s) => (s.climate = "constructor"), | |
| (s) => (s.lens = "daylight"), | |
| (s) => (s.alternatives.A.extra = 1), | |
| (s) => (s.alternatives.B.design.mass = "50"), | |
| (s) => delete s.alternatives.B.design.shade, | |
| (s) => (s.alternatives.B.design.solar = 8), | |
| (s) => (s.alternatives.A.name = ""), | |
| (s) => (s.alternatives.A.name = "x".repeat(61)), | |
| ...["\n", "\u0085", "\u2028", "\u2029", "\ud800"].map( | |
| (c) => (s) => (s.alternatives.A.name = "a" + c + "b"), | |
| ), | |
| ]) { | |
| const s = structuredClone(base); | |
| mutation(s); | |
| assert.throws(() => validateStudy(s)); | |
| } | |
| assert.throws(() => parseStudy("{")); | |
| assert.throws(() => parseStudy(" ".repeat(STUDY_LIMIT + 1)), /32 KiB/); | |
| assert.throws(() => parseStudy("界".repeat(STUDY_LIMIT / 2)), /32 KiB/); | |
| }); | |
| test("Comparison is B minus A, symmetric on swap, independent of names/view selection", () => { | |
| const s = newStudy(), | |
| r = evaluateStudy(s), | |
| swapped = evaluateStudy({ ...s, alternatives: { A: s.alternatives.B, B: s.alternatives.A } }); | |
| for (const key of Object.keys(r.delta)) near(r.delta[key], -swapped.delta[key]); | |
| const same = evaluateStudy(pair("humid", defaults, defaults)); | |
| for (const value of Object.values(same.delta)) near(value, 0); | |
| const rename = structuredClone(s); | |
| rename.active = "A"; | |
| rename.hour = 2; | |
| rename.lens = "air"; | |
| rename.alternatives.B.name = "Changed label"; | |
| assert.deepEqual(evaluateStudy(rename).delta, r.delta); | |
| const calls = []; | |
| evaluateStudy({ ...s, climate: "arid" }, (climate, design) => { | |
| calls.push(climate); | |
| const simulation = simulateDay(climates[climate], design); | |
| return { simulation, summary: summarize(simulation) }; | |
| }); | |
| assert.deepEqual(calls, ["arid", "arid"]); | |
| }); | |
| test("Shade, mass and ventilation trade-offs use actual model outputs, not blanket improvement claims", () => { | |
| let r = evaluateStudy(pair("temperate", { ...defaults, shade: 0 }, { ...defaults, shade: 2.4 })); | |
| assert.ok(r.delta.solarKWh < 0 && r.delta.peakC < 0 && r.delta.bandHours > 0); | |
| r = evaluateStudy(pair("arid", { ...defaults, mass: 0 }, { ...defaults, mass: 100 })); | |
| assert.ok(r.delta.peakC < 0 && r.delta.bandHours < 0); | |
| assert.match(comparisonSentence(r), /separate trade-offs/); | |
| assert.match(r.changes[0].mechanism, /need not/); | |
| const d = { ...defaults, orientation: 0, glazing: 12, shade: 0, mass: 100 }; | |
| r = evaluateStudy(pair("humid", { ...d, ventilation: 5 }, { ...d, ventilation: 100 })); | |
| assert.ok(r.delta.peakC < 0 && r.delta.meanACH < 0); | |
| assert.match(r.changes[0].mechanism, /need not/); | |
| }); | |
| test("Either unsettled day suppresses settled comparison claims; peak interval has step bounds", () => { | |
| const s = newStudy(); | |
| let i = 0; | |
| const r = evaluateStudy(s, (climate, design) => { | |
| const simulation = simulateDay(climates[climate], design, ++i === 1 ? { maximumDays: 1 } : {}); | |
| return { simulation, summary: summarize(simulation) }; | |
| }); | |
| assert.equal(r.settled, false); | |
| assert.match(comparisonSentence(r), /^Unsettled comparison/); | |
| assert.doesNotMatch(comparisonSentence(r), /B minus A/); | |
| assert.equal( | |
| peakInterval({ simulation: { minutes: 10 }, summary: { peak: { hour: 16 + 5 / 60 } } }), | |
| "4:00 PM–4:10 PM", | |
| ); | |
| assert.equal(signed(-0.0001), "0.00"); | |
| }); | |
| test("Raw invalid names block commands and exports; applying invalid drafts is atomic", () => { | |
| const w = new StudyWorkspace(), | |
| before = w.read().study; | |
| w.editName("A", ""); | |
| const frozen = JSON.stringify(w.read()); | |
| for (const action of [ | |
| () => w.applyNames(), | |
| () => w.configure({ shade: 2 }), | |
| () => w.select("A"), | |
| () => w.copy(), | |
| () => w.request(newStudy(), "Reset"), | |
| () => w.beginFile(), | |
| () => w.save(), | |
| () => w.brief(), | |
| ]) { | |
| assert.throws(action); | |
| assert.equal(JSON.stringify(w.read()), frozen); | |
| } | |
| w.discardNames(); | |
| assert.deepEqual(w.read().study, before); | |
| w.editName("A", " First idea "); | |
| w.applyNames(); | |
| assert.equal(w.read().study.alternatives.A.name, "First idea"); | |
| const applied = JSON.stringify(w.read()); | |
| assert.throws(() => w.configure({ shade: 1, climate: "bad" })); | |
| assert.equal(JSON.stringify(w.read()), applied); | |
| }); | |
| test("Copy/reset confirmations preserve names and values until explicit acceptance", () => { | |
| const w = new StudyWorkspace(); | |
| w.editName("A", "Keep this name"); | |
| w.applyNames(); | |
| const before = w.read().study; | |
| w.copy(); | |
| assert.deepEqual(w.read().study, before); | |
| assert.throws(() => w.configure({ shade: 2 })); | |
| assert.throws(() => w.editName("A", "No")); | |
| assert.throws(() => w.save()); | |
| w.dismiss(); | |
| assert.deepEqual(w.read().study, before); | |
| w.copy(); | |
| w.confirm(); | |
| assert.deepEqual(w.read().study.alternatives.A.design, before.alternatives.B.design); | |
| assert.equal(w.read().study.alternatives.A.name, "Keep this name"); | |
| w.request(newStudy(), "Reset"); | |
| w.dismiss(); | |
| assert.equal(w.read().study.alternatives.A.name, "Keep this name"); | |
| w.request(newStudy(), "Reset"); | |
| w.confirm(); | |
| assert.deepEqual(w.read().study, newStudy()); | |
| }); | |
| test("File locks, malformed/cancelled imports and stale reads cannot overwrite work", () => { | |
| const w = new StudyWorkspace(), | |
| original = w.read().study, | |
| ticket = w.beginFile(); | |
| for (const action of [ | |
| () => w.select("A"), | |
| () => w.configure({ hour: 2 }), | |
| () => w.editName("A", "New"), | |
| () => w.beginFile(), | |
| () => w.save(), | |
| ]) | |
| assert.throws(action); | |
| assert.throws(() => w.stageFile(ticket, "bad")); | |
| assert.deepEqual(w.read().study, original); | |
| w.cancelFile(ticket); | |
| w.configure({ hour: 3 }); | |
| const newer = w.read(), | |
| frozen = JSON.stringify(newer); | |
| assert.throws(() => w.stageFile(ticket, saveStudy(original)), /stale/); | |
| w.cancelFile(ticket); | |
| assert.equal(JSON.stringify(w.read()), frozen); | |
| const next = w.beginFile(); | |
| w.stageFile(next, saveStudy(original)); | |
| assert.equal(w.read().study.hour, 3); | |
| w.dismiss(); | |
| assert.equal(w.read().study.hour, 3); | |
| const last = w.beginFile(); | |
| w.stageFile(last, saveStudy(original)); | |
| w.confirm(); | |
| assert.deepEqual(w.read().study, original); | |
| }); | |
| test("Reports are deterministic, include both inputs/trade-offs and isolate supplied names", () => { | |
| const s = newStudy(); | |
| s.alternatives.A.name = '<script>alert("name")</script>'; | |
| const brief = studyBrief(s); | |
| assert.equal(brief, studyBrief(parseStudy(saveStudy(s)))); | |
| assert.ok(brief.includes("A: " + JSON.stringify(s.alternatives.A.name))); | |
| assert.match(brief, /Multiple settings changed together/); | |
| assert.match(brief, /10-minute step-average/); | |
| assert.match(brief, /not an editable backup/); | |
| assert.match(brief, /Nothing is autosaved/); | |
| assert.match(brief, /No HVAC/); | |
| assert.match(brief, /SHARED ASSUMPTIONS/); | |
| assert.match(brief, /glazing/); | |
| assert.match(studyBrief(pair("humid", defaults, defaults)), /identical settings/); | |
| }); | |
| console.log(`\n${count} consumer test groups passed.`); | |