Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
6.3 kB · 174 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175/** * The two things about a camo record that are easy to get wrong and silent * when they are. * * The first is that `source` is not in the lexicon. A validator that dropped * undeclared fields would make renaming a camo delete the settings that let * the editor re-open it, and nothing would report an error — the record would * simply come back plainer than it went in. * * The second is that reading `source` has to be defensive. Generator ids and * parameter meanings change when the generator code changes, so a record * written by another version of this editor is untrusted input even though we * wrote it. Anything unreadable has to come back as null rather than as a * camo drawn wrongly. * * The modules under test are TypeScript that imports generated code, so this * bundles them the way the site does rather than reaching for a TS runner. * Run with `npm test`. */import { test } from "node:test";import assert from "node:assert/strict";import { mkdtemp, rm } from "node:fs/promises";import { tmpdir } from "node:os";import { join } from "node:path";import { execFile } from "node:child_process";import { promisify } from "node:util";
const run = promisify(execFile);const web = join(new URL(".", import.meta.url).pathname, "..");
// camo-record.fixture.ts is the entry, bundled with the site's own bundler so// the generated lexicons and `@atproto/lex` resolve the way they do in a// build. The output goes to a temp directory; nothing is written to the tree.const dir = await mkdtemp(join(tmpdir(), "camo-record-"));const bundle = join(dir, "fixture.mjs");await run( join(web, "node_modules/.bin/rolldown"), [ join(web, "scripts/camo-record.fixture.ts"), "--format", "esm", "--platform", "node", "--file", bundle, ], { cwd: web },);
const { readSource, makeSource, SOURCE_VERSION, camo, jsonToLex, lexToJson } = await import(bundle);
const PALETTE = { name: "Woodland", colors: [ [74, 83, 58], [104, 112, 78], ],};/** What the editor holds: fractions, straight off the sliders. */const PARAMS = { seed: 7, scale: 0.51, contrast: 0.4, detail: 0.3, angle: 0 };/** What those become on the record: thousandths, and whole numbers. */const STORED_PARAMS = { seed: 7, scale: 510, contrast: 400, detail: 300, angle: 0,};
/** * A record as it comes off the wire, carrying settings the schema never * declared. Cloned, because the cases below mutate what they are given. */const wire = () => structuredClone({ $type: "blue.lance.camo", name: "Highland", image: { $type: "blob", ref: { $link: "bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy", }, mimeType: "image/png", size: 1234, }, createdAt: "2026-08-06T12:00:00.000Z", source: { v: SOURCE_VERSION, generator: "dpm", params: STORED_PARAMS, palette: PALETTE, }, });
test("a rename keeps the settings the schema does not declare", () => { const parsed = camo.parse(jsonToLex(wire())); // What a rename does: put back the record that was read, with one field // replaced. Nothing is rebuilt from the fields this code knows about. const renamed = { ...parsed, name: "Lowland" }; const json = lexToJson(renamed);
assert.equal(json.name, "Lowland"); assert.deepEqual( json.source, wire().source, "source must survive the round trip", ); assert.equal(json.$type, "blue.lance.camo");});
test("the declared fields are still validated", () => { const missingName = jsonToLex({ ...wire(), name: undefined }); assert.equal(camo.safeValidate(missingName).success, false);
const tooLong = jsonToLex({ ...wire(), name: "a".repeat(641) }); assert.equal(camo.safeValidate(tooLong).success, false);});
test("settings this build cannot act on read as none", () => { const cases = { "no source at all": (r) => delete r.source, "a version this build does not know": (r) => (r.source.v = SOURCE_VERSION + 1), "a generator this build does not have": (r) => (r.source.generator = "hologram"), "a parameter that is not a number": (r) => (r.source.params.seed = "seven"), "a parameter missing": (r) => delete r.source.params.angle, // The bug this whole shape exists for: the data model has no float, so a // fraction cannot be written and must not be read back as if it could. "a fractional slider": (r) => (r.source.params.scale = 0.51), "a slider past its range": (r) => (r.source.params.scale = 1001), "a fractional colour": (r) => (r.source.palette.colors[0][0] = 74.5), "a colour outside 0-255": (r) => (r.source.palette.colors[0][1] = 300), "a colour that is not a triple": (r) => (r.source.palette.colors[0] = [1, 2]), "an empty palette": (r) => (r.source.palette.colors = []), }; for (const [why, mutate] of Object.entries(cases)) { const record = wire(); mutate(record); assert.equal(readSource(record), null, why); }});
test("settings this build wrote are whole numbers, and read back", () => { const source = makeSource("dpm", PARAMS, PALETTE);
// The check that would have caught the production failure: everything the // record carries has to be an integer, because atproto's data model has no // float and refuses the entire write when it meets one. const numbers = []; (function walk(v) { if (typeof v === "number") numbers.push(v); else if (Array.isArray(v)) v.forEach(walk); else if (v && typeof v === "object") Object.values(v).forEach(walk); })(source); assert.ok(numbers.length > 0); for (const n of numbers) { assert.ok(Number.isInteger(n), `${n} is not a whole number`); } assert.deepEqual(source.params, STORED_PARAMS);
// And the sliders come back where the player left them. const back = readSource({ source }); assert.equal(back.params.scale, 0.51); assert.equal(back.params.contrast, 0.4); assert.equal(back.generator, "dpm"); // A palette flag is carried; anything else on the palette is not invented. const flagged = makeSource("pride", PARAMS, { ...PALETTE, flag: true }); assert.equal(readSource({ source: flagged })?.palette.flag, true);});
test.after(() => rm(dir, { recursive: true, force: true }));