Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357import assert from "node:assert/strict";import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";import { existsSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path";import test from "node:test";import { LivePiece, pieceDirectory } from "../src/live.mjs";import { GENRES, genreFor, genreLabels } from "../src/genres.mjs";import { randomChannel, randomSlug } from "../src/names.mjs";import { qrBlock } from "../src/qr.mjs";import { runtimeFor, runtimeForExtension, runtimeIds, runtimeMenu } from "../src/runtimes.mjs";
async function workspace(context) { const root = await mkdtemp(join(tmpdir(), "easel-live-")); context.after(() => rm(root, { recursive: true, force: true })); return root;}
const ok = () => new Response("Reloaded!", { status: 200 });
test("names are pronounceable and channels stay short enough to scan", () => { for (let attempt = 0; attempt < 200; attempt += 1) { assert.match(randomSlug(), /^[a-z]{4,6}$/); assert.match(randomChannel(), /^[A-Za-z0-9_-]{8}$/); } // The address is short so the code can be. Measure the real scan URL rather // than a copy of it, so lengthening it fails here instead of quietly growing // the symbol a version — which costs modules on the rock, where someone is // holding a phone up to it. A phone gets the URL with `https://` in front, // so that is what is measured: 32 bytes is a version-2 code at level L. for (let attempt = 0; attempt < 50; attempt += 1) { const url = new LivePiece({ cwd: tmpdir() }).scanUrl; const scanned = `https://${url}`; assert.ok( Buffer.byteLength(scanned) <= 32, `${scanned} is too long for a version-2 code`, ); const block = qrBlock(url); assert.equal(block.width, 29); assert.equal(block.height, 15); }});
// The scanned address is the short one; the host expands it. The rule it// expands to still matters — parse.mjs hands everything after `prompt~` to the// prompt as ONE parameter and the prompt splits its own arguments on spaces, so// the channel must arrive as its own word — but that lives in the `~<channel>`// redirect in lith/Caddyfile now, not here.test("the scan URL is the short route, not the command line", () => { const live = new LivePiece({ cwd: tmpdir(), channel: "Ab0-_9Zz" }); assert.equal(live.scanUrl, "prompt.ac/~Ab0-_9Zz"); const [, channel] = live.scanUrl.match(/\/~(.+)$/); assert.equal(channel, "Ab0-_9Zz", "the channel travels whole, needing no encoding");});
// Read the rendered block back into a grid of dark modules. A code that is// transposed, mirrored, or off by a row still "looks like" a QR code in a// terminal, so compare against the encoder rather than trusting the picture.function decodeBlock(block) { const grid = []; for (const line of block.lines) { const cells = line.split("\u2580").slice(0, -1); const top = []; const bottom = []; for (const cell of cells) { const [, fg, bg] = cell.match(/38;5;(\d+)m\x1b\[48;5;(\d+)m$/); top.push(fg === "0"); bottom.push(bg === "0"); } grid.push(top, bottom); } return grid;}
test("the rendered code matches the encoder module for module", async () => { const { qrcode, ErrorCorrectLevel } = await import("../src/vendor/qr.mjs"); const url = "prompt.ac/~Ab0-_9Zz"; const quiet = 2; const modules = qrcode(url, { errorCorrectLevel: ErrorCorrectLevel.L }).modules; const grid = decodeBlock(qrBlock(url, { quiet }));
for (let row = 0; row < modules.length; row += 1) { for (let column = 0; column < modules.length; column += 1) { assert.equal( grid[row + quiet][column + quiet], Boolean(modules[row][column]), `module ${column},${row} does not match the encoder`, ); } }
// The quiet zone has to be light or a camera never finds the code. for (let column = 0; column < grid[0].length; column += 1) { assert.equal(grid[0][column], false); assert.equal(grid[1][column], false); }
// A finder pattern is a 7x7 target: dark ring, light ring, dark 3x3 core. // Three corners carry one and the fourth never does, which is how a scanner // works out the code's orientation. const isFinder = (left, top) => { for (let y = 0; y < 7; y += 1) { for (let x = 0; x < 7; x += 1) { const ring = x === 0 || y === 0 || x === 6 || y === 6; const core = x >= 2 && x <= 4 && y >= 2 && y <= 4; if (grid[top + quiet + y][left + quiet + x] !== (ring || core)) return false; } } return true; }; const last = modules.length - 7; assert.equal(isFinder(0, 0), true, "top-left finder"); assert.equal(isFinder(last, 0), true, "top-right finder"); assert.equal(isFinder(0, last), true, "bottom-left finder"); assert.equal(isFinder(last, last), false, "no finder in the bottom-right");});
// The name lives in the blank's header comment only. The system's corner label// already shows it on screen, so a blank that also wrote it drew it twice — and// the agent's first edit tended to keep the centered placeholder around.test("every runtime carries a blank that names itself in a comment, not on screen", () => { assert.deepEqual(runtimeIds(), ["mjs", "lisp", "lua"]); for (const id of runtimeIds()) { const runtime = runtimeFor(id); const blank = runtime.blank("movika"); assert.match(blank, /movika/); assert.doesNotMatch(blank, /(?:write|text)\s*\(?\s*"movika"/, `${id} blank writes its name on screen`); assert.equal(runtimeForExtension(runtime.extension), runtime); } assert.equal(runtimeFor(".LISP").id, "lisp"); assert.equal(runtimeFor("mjs").routable, true); assert.equal(runtimeFor("lua").routable, false); assert.throws(() => runtimeFor("rust"), /unknown runtime/);
// The names a person reaches for resolve to the same three runtimes. assert.equal(runtimeFor("processing").id, "lua"); assert.equal(runtimeFor("L5").id, "lua"); assert.equal(runtimeFor("kidlisp").id, "lisp"); assert.equal(runtimeFor("js").id, "mjs"); assert.match(runtimeMenu(), /lua \(processing\)/);});
test("startup offers only an AC blank or a nopaint.art brush", () => { assert.deepEqual(genreLabels(), ["AC piece (blank)", "nopaint.art brush"]); assert.equal(GENRES.length, 2); assert.equal(genreFor("ac").id, "piece"); assert.equal(genreFor("brush").id, "nopaint"); assert.throws(() => genreFor("game"), /unknown genre/);});
test("the nopaint start uses the runtime's existing brush contract", async (context) => { const root = await workspace(context); const live = new LivePiece({ cwd: root, slug: "movika", genre: "nopaint", runtime: "lisp" }); assert.equal(live.runtime.id, "mjs", "brushes run as JavaScript even when another runtime was requested"); live.create(); const source = await readFile(live.file, "utf8"); assert.match(source, /function brush\(\{ ink, pen \}\)/); assert.match(source, /const system = "nopaint"/); assert.match(source, /export \{ brush, meta, system \}/); assert.doesNotMatch(source, /function paint\(/, "the brush does not duplicate the canvas lifecycle"); assert.equal(live.retarget(join(root, "other.lisp")), false); assert.throws(() => live.rename("other", "lisp"), /uses javascript/); assert.equal(live.cleanup(), true);});
// A live push carries no extension, so the client reads the source to decide// it is Lua: it must open with a `--` comment and declare setup or draw. The// Processing blank has to satisfy that on its own or it is compiled as// JavaScript and the phone never sees the piece.test("the Processing blank is recognisable as Lua from its source alone", () => { const source = runtimeFor("processing").blank("movika"); assert.ok(source.trim().startsWith("--"), "must open with a Lua comment"); assert.match(source, /(?:^|\n)\s*function\s+(setup|draw)\s*\(/); // Processing's vocabulary, not Aesthetic Computer's. assert.doesNotMatch(source, /function\s+paint\s*\(/);});
test("a session mints a blank piece, pushes it, and cleans up after itself", async (context) => { const root = await workspace(context); const calls = []; const live = new LivePiece({ cwd: root, slug: "movika", channel: "Ab0-_9Zz", fetch: async (url, options) => { calls.push({ url, ...JSON.parse(options.body) }); return ok(); }, });
assert.equal(live.file, join(root, "movika.mjs")); assert.equal(live.scanUrl, "prompt.ac/~Ab0-_9Zz"); assert.equal(live.publishedUrl("jeffrey"), "https://aesthetic.computer/@jeffrey/movika"); assert.equal(live.publishedUrl(""), "");
live.create(); assert.equal(existsSync(live.file), true); assert.equal(live.pristine, true); assert.match(await readFile(live.file, "utf8"), /movika/);
assert.equal(await live.push(), true); assert.equal(calls.length, 1); assert.equal(calls[0].url, "https://aesthetic.computer/run"); assert.equal(calls[0].piece, "movika"); assert.equal(calls[0].codeChannel, "Ab0-_9Zz"); assert.match(calls[0].source, /movika/);
// An untouched blank leaves nothing behind. assert.equal(live.cleanup(), true); assert.equal(existsSync(live.file), false);});
// The session server retains the last message a channel saw and replays it to// late joiners, so the harness announces a piece once per save and no more. A// watch that sees no edits must therefore be silent — and must not leave a// timer running behind it.test("a piece announces itself once per save, not on a beat", async (context) => { const root = await workspace(context); const calls = []; const live = new LivePiece({ cwd: root, slug: "movika", fetch: async (url, options) => { calls.push(JSON.parse(options.body)); return ok(); }, }); live.create(); live.watch(); await new Promise((resolve) => setTimeout(resolve, 130));
assert.equal(calls.length, 0, "an idle watch does not re-announce");
await writeFile(live.file, "// touched\n"); await new Promise((resolve) => setTimeout(resolve, 400)); assert.equal(calls.length, 1, "a save announces exactly once"); assert.match(calls[0].source, /touched/); assert.equal(live.pushes, 1, "and that push is counted as an edit");
live.unwatch(); await new Promise((resolve) => setTimeout(resolve, 100)); assert.equal(calls.length, 1, "stopping the watch leaves no timer behind"); live.cleanup();});
test("an edited piece is never deleted on the way out", async (context) => { const root = await workspace(context); const live = new LivePiece({ cwd: root, slug: "movika", fetch: async () => ok() }); live.create(); await writeFile(live.file, "export function paint() {}\n"); assert.equal(live.cleanup(), false); assert.equal(existsSync(live.file), true);});
test("renaming and retargeting follow the piece without changing the channel", async (context) => { const root = await workspace(context); const live = new LivePiece({ cwd: root, slug: "movika", channel: "Ab0-_9Zz", fetch: async () => ok() }); live.create(); const scan = live.scanUrl;
live.rename("smiley", "lisp"); assert.equal(live.file, join(root, "smiley.lisp")); assert.equal(live.runtime.label, "kidlisp"); assert.equal(existsSync(join(root, "movika.mjs")), false, "the untouched blank moved rather than piling up"); assert.match(await readFile(live.file, "utf8"), /\(wipe "#[0-9a-f]{6}"\)/); assert.equal(live.scanUrl, scan, "the QR stays valid across a rename");
assert.equal(live.retarget(join(root, "other.lisp")), true); assert.equal(live.slug, "other"); assert.equal(live.retarget(join(root, "notes.txt")), false, "only piece files are tracked"); assert.equal(live.slug, "other"); assert.throws(() => live.rename("bad name"), /letters, digits/);});
test("pieces land in the disks folder when the workspace is the AC repository", async (context) => { const root = await workspace(context); assert.equal(pieceDirectory(root), root); const disks = join(root, "system", "public", "aesthetic.computer", "disks"); await rm(disks, { recursive: true, force: true }); const { mkdir } = await import("node:fs/promises"); await mkdir(disks, { recursive: true }); assert.equal(pieceDirectory(root), disks); const live = new LivePiece({ cwd: root, slug: "movika", fetch: async () => ok() }); assert.equal(live.file, join(disks, "movika.mjs"));});
test("a failed push is reported rather than swallowed", async (context) => { const root = await workspace(context); const live = new LivePiece({ cwd: root, slug: "movika", fetch: async () => new Response("nope", { status: 500 }), }); live.create(); await assert.rejects(() => live.push(), /HTTP 500/);});
// A signed-in session owns its channel by name, and that name is the piece's// own public address — so the thing on the rock, the thing the phone loads and// the thing `/run` checks the token against are all one string.test("a signed-in session owns its channel and scans its published address", () => { const live = new LivePiece({ cwd: tmpdir(), slug: "murafi", handle: "jeffrey", channel: "Ab0-_9Zz", }); assert.equal(live.channel, "jeffrey/murafi"); assert.equal(live.scanUrl, "prompt.ac/@jeffrey/murafi"); assert.equal( live.publishedUrl("jeffrey"), "https://aesthetic.computer/@jeffrey/murafi", "the scanned address and the published address are the same piece", );});
// Signing out has to give the opaque channel back rather than leave a dangling// `@/slug`, which `/run` would read as an owned channel with no owner.test("signing out falls back to the opaque channel", () => { const live = new LivePiece({ cwd: tmpdir(), slug: "murafi", handle: "jeffrey", channel: "Ab0-_9Zz" }); live.handle = ""; assert.equal(live.channel, "Ab0-_9Zz"); assert.equal(live.scanUrl, "prompt.ac/~Ab0-_9Zz");});
test("a push carries the session's token, and omits the header without one", async () => { const calls = []; const fetchStub = async (url, options) => { calls.push(options); return { ok: true }; }; const root = await mkdtemp(join(tmpdir(), "ac-token-")); await writeFile(join(root, "murafi.mjs"), "// piece\n");
const signedIn = new LivePiece({ cwd: root, slug: "murafi", handle: "jeffrey", fetch: fetchStub, token: async () => "tok_123", }); await signedIn.push(); assert.equal(calls[0].headers.Authorization, "Bearer tok_123"); assert.equal(JSON.parse(calls[0].body).codeChannel, "jeffrey/murafi");
const signedOut = new LivePiece({ cwd: root, slug: "murafi", fetch: fetchStub }); await signedOut.push(); assert.equal(calls[1].headers.Authorization, undefined, "no token, no header");});
test('concurrent pushes of identical bytes send once; failure remains retryable', async context => { const cwd=await workspace(context);let calls=0,fail=true; const live=new LivePiece({cwd,slug:'dedup',fetch:async()=>{calls++;return new Response('',{status:fail?500:200});}});live.create(); await assert.rejects(live.push()); fail=false; await Promise.all([live.push(),live.push(),live.push()]);assert.equal(calls,2); await live.push();assert.equal(calls,2); await writeFile(live.file,'export function paint({wipe}) { wipe("red"); }');await live.push();assert.equal(calls,3); live.handle='another';await live.push();assert.equal(calls,4);});