Merge design/cloudflare-static-publish into main: Cloudflare static publish + CC credits
Integrates the fully-static Cloudflare publish line (live at benstull.art/human-experience-simulator) into main: the static build (Pages + R2, manifest-only media tree, baked API JSON), the "Run simulation" start button, the public CC-BY/BY-SA credits page with the "License & reuse" CC BY-SA 4.0 derivative declaration (Option A + D1), the repeatable Cloudflare deploy script, and assorted load/audio fixes (progressive boot, scaleAudioUrl via mediaBase). Clean auto-merge with main's per-clip affect/feelings + i18n work — only app.js and altitude-lock.spec.ts overlapped, both auto-resolved. Verified green on the merged tree: node 29, pytest 315 passed / 2 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
"use strict";
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const A = require("../static/alteration.js");
|
||||
|
||||
const c = (o) => ({ visual: "on", audio: "off", left: 0, right: 0, dark: 0, light: 0, ...o });
|
||||
|
||||
test("identity at zero knobs", () => {
|
||||
const p = A.plan(c({}));
|
||||
assert.equal(p.grade.tone, 0);
|
||||
assert.equal(p.overlay.level, 0);
|
||||
assert.equal(p.dream.strength, 0);
|
||||
assert.equal(p.is_identity, true);
|
||||
});
|
||||
|
||||
test("mood tone is signed (light - dark)/KNOB_MAX, clamped", () => {
|
||||
assert.equal(A.plan(c({ light: 4 })).grade.tone, 1); // full light
|
||||
assert.equal(A.plan(c({ dark: 4 })).grade.tone, -1); // full dark
|
||||
assert.equal(A.plan(c({ light: 2 })).grade.tone, 0.5);
|
||||
});
|
||||
|
||||
test("left drives overlay level + intensity; right drives dream + affect", () => {
|
||||
const p = A.plan(c({ left: 2, right: 4 }));
|
||||
assert.equal(p.overlay.level, 2);
|
||||
assert.equal(p.overlay.intensity, 0.5);
|
||||
assert.equal(p.dream.strength, 4);
|
||||
assert.equal(p.dream.intensity, 1);
|
||||
assert.equal(p.affect.strength, 4);
|
||||
assert.equal(p.affect.intensity, 1);
|
||||
assert.equal(p.is_identity, false);
|
||||
});
|
||||
|
||||
test("renderAudio: off → null, no coupling", () => {
|
||||
assert.deepEqual(A.renderAudio("off", "cosmos.mp3", "https://x/"),
|
||||
{ source: "off", url: null, altitude_coupled: false });
|
||||
});
|
||||
|
||||
test("renderAudio: soundtrack → mediaBase audio url, coupled", () => {
|
||||
assert.deepEqual(A.renderAudio("soundtrack", "cosmos.mp3", "https://x/"),
|
||||
{ source: "soundtrack", url: "https://x/audio/cosmos.mp3", altitude_coupled: true });
|
||||
// no scale audio → null url but still coupled
|
||||
assert.deepEqual(A.renderAudio("soundtrack", "", "https://x/"),
|
||||
{ source: "soundtrack", url: null, altitude_coupled: true });
|
||||
});
|
||||
|
||||
test("alteration() composes plan + render with video.shown from visual", () => {
|
||||
const r = A.alteration(c({ visual: "off", audio: "soundtrack", left: 1 }), "reef.mp3", "https://x/");
|
||||
assert.equal(r.render.video.shown, false);
|
||||
assert.equal(r.render.audio.url, "https://x/audio/reef.mp3");
|
||||
assert.equal(r.plan.overlay.level, 1);
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
"use strict";
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const C = require("../static/credits.js");
|
||||
|
||||
const CLIPS = [
|
||||
{ id: "reef_x", title: "Reef X", license: "CC-BY-SA 4.0 — credit Joe Mabel", source: "Wikimedia <reef>" },
|
||||
{ id: "cosmos", title: "Cosmic Cliffs", license: "CC-BY-class — credit NASA, ESA, CSA, STScI", source: "NASA SVS 31348" },
|
||||
{ id: "pd_only", title: "PD clip", license: "public-domain (US Gov, 17 U.S.C. §105)", source: "NASA" },
|
||||
];
|
||||
|
||||
test("creditEntries returns one entry per clip, preserving license/source", () => {
|
||||
const out = C.creditEntries(CLIPS);
|
||||
assert.equal(out.length, CLIPS.length, "no clip is silently dropped from the credits");
|
||||
const reef = out.find((e) => e.id === "reef_x");
|
||||
assert.equal(reef.license, "CC-BY-SA 4.0 — credit Joe Mabel");
|
||||
assert.equal(reef.source, "Wikimedia <reef>");
|
||||
assert.equal(reef.title, "Reef X");
|
||||
});
|
||||
|
||||
test("creditEntries is sorted by id for deterministic output", () => {
|
||||
const ids = C.creditEntries(CLIPS).map((e) => e.id);
|
||||
assert.deepEqual(ids, ["cosmos", "pd_only", "reef_x"]);
|
||||
});
|
||||
|
||||
test("creditEntries falls back title->id and tolerates missing fields", () => {
|
||||
const out = C.creditEntries([{ id: "bare" }]);
|
||||
assert.deepEqual(out, [{ id: "bare", title: "bare", license: "", source: "" }]);
|
||||
});
|
||||
|
||||
test("creditEntries handles null/empty input", () => {
|
||||
assert.deepEqual(C.creditEntries(null), []);
|
||||
assert.deepEqual(C.creditEntries([]), []);
|
||||
});
|
||||
|
||||
test("creditsListHtml renders every clip's attribution and escapes HTML", () => {
|
||||
const html = C.creditsListHtml(CLIPS);
|
||||
assert.match(html, /Cosmic Cliffs/);
|
||||
assert.match(html, /Joe Mabel/);
|
||||
// free-text source "Wikimedia <reef>" must be escaped, not injected as a tag
|
||||
assert.match(html, /Wikimedia <reef>/);
|
||||
assert.doesNotMatch(html, /<reef>/);
|
||||
// one block per clip
|
||||
const blocks = html.match(/class="credit"/g) || [];
|
||||
assert.equal(blocks.length, CLIPS.length);
|
||||
});
|
||||
|
||||
test("credits.html wires config.js + credits.js, the attributions container, and a back link", () => {
|
||||
const html = fs.readFileSync(path.join(__dirname, "../static/credits.html"), "utf8");
|
||||
assert.match(html, /<script src="config\.js">/); // static/live media-base + flag
|
||||
assert.match(html, /<script src="credits\.js">/);
|
||||
assert.match(html, /id="video-credits"/); // JS fills this
|
||||
assert.match(html, /href="index\.html"/); // back into the experience
|
||||
});
|
||||
|
||||
test("credits.html carries the CC BY-SA derivative declaration (Option D1)", () => {
|
||||
const html = fs.readFileSync(path.join(__dirname, "../static/credits.html"), "utf8");
|
||||
assert.match(html, /CC BY-SA 4\.0/); // the altered work's own license
|
||||
assert.match(html, /altered/i); // states the footage is modified
|
||||
// The section is NOT titled "About this work" — that name belongs to the separate
|
||||
// About page; here the declaration lives under "License & reuse".
|
||||
assert.doesNotMatch(html, /About this work/i);
|
||||
assert.match(html, /License & reuse/);
|
||||
});
|
||||
@@ -8,7 +8,7 @@ const I = require("../static/i18n.js");
|
||||
const html = fs.readFileSync(path.join(__dirname, "../static/index.html"), "utf8");
|
||||
|
||||
test("index.html includes the i18n script and a language select", () => {
|
||||
assert.match(html, /<script src="\/i18n\.js">/);
|
||||
assert.match(html, /<script src="i18n\.js">/); // relative: works at dev-root and under the deploy path prefix
|
||||
assert.match(html, /id="lang-select"/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user