// The desktop app's pure spine. // // The Electron half of `electron/` cannot be driven here — there is no browser // process under `node --test`, and installing Electron to run the test suite is // exactly the cost `desktop/` being its own npm project exists to avoid. So the // parts with a decision in them are written as pure modules and proved here, // and what is left in the Electron-facing files is wiring: // // electron/deeplink.ts what a `astrolabe://` link and a double-clicked file are // ALLOWED to name. Hostile input; the refusals are the // product. // electron/prefs.ts the port a vault gets, which decides whether the // reader's theme, tabs and folds survive a launch. // electron/cookie.ts the session lifetime, read off the wire instead of // copied out of server/auth.ts. // electron/ipc.ts the bridge's vocabulary (also counted by // `npm run check-desktop`). // electron/presenceState.ts which vault's activity Discord shows when // several are open, and the messages that carry it. // // Root `npm run typecheck` follows these imports in, which is the second half // of why they are pure: the desktop's spine is typechecked by the release gate // every contributor already runs, even though `electron/` is not in the root // tsconfig's `include`. import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import path from "node:path"; import { describe, it } from "node:test"; import { containsFile, knownVault, noteRef, parseDeepLink, relativeNote, routeForNote, vaultForFile, } from "../electron/deeplink.ts"; import { TO_MAIN, TO_RENDERER, COMMANDS } from "../electron/ipc.ts"; import { parseSessionCookie } from "../electron/cookie.ts"; import { regValue, windowsSystemVoice } from "../electron/systemVoice.ts"; import { childEnv, parseEnvFile } from "../electron/server.ts"; import { EMPTY_PREFS, MAX_RECENTS, PORT_MAX, MIN_WINDOW, PORT_MIN, fitToWorkArea, forgetVault, onSomeDisplay, parsePrefs, portCandidates, recentVaults, rememberBounds, rememberVault, rememberedPort, rememberZoom, saneZoom, seedFor, stepZoom, type Prefs, } from "../electron/prefs.ts"; import { isPresenceToParent, PresenceBook, presenceStatusMessage } from "../electron/presenceState.ts"; import { isPresenceFromParent } from "../shared/presence/messages.ts"; // ── Deep links: what a stranger's link may name ──────────────────────────── describe("noteRef", () => { it("accepts an ordinary vault-relative note", () => { assert.equal(noteRef("Ideas/Cache locality.md"), "Ideas/Cache locality.md"); assert.equal(noteRef("./Ideas/Note.md"), "Ideas/Note.md"); assert.equal(noteRef("notes\\Idea.md"), "notes/Idea.md"); }); it("refuses every shape of escape", () => { // A `astrolabe://` URL can be opened by any page in any browser with no // prompt. Each of these is a file outside the vault. for (const evil of [ "../secrets.md", "../../etc/passwd.md", "a/../../b.md", "a/b/../../../c.md", "/etc/passwd.md", "\\\\server\\share\\note.md", "C:/Users/me/note.md", "notes\\..\\..\\out.md", "note.md\u0000.png", ]) { assert.equal(noteRef(evil), null, `should refuse ${JSON.stringify(evil)}`); } }); it("refuses anything that is not a note", () => { // Not a purity rule — the app can open an attachment. It means nothing OFF // this machine gets to choose the file the app reads and indexes. assert.equal(noteRef("photo.png"), null); assert.equal(noteRef("archive.zip"), null); assert.equal(noteRef(".ssh/id_rsa"), null); assert.equal(noteRef(""), null); assert.equal(noteRef("."), null); }); it("keeps the three note extensions the rest of the product agrees on", () => { assert.equal(noteRef("a.md"), "a.md"); assert.equal(noteRef("a.tex"), "a.tex"); assert.equal(noteRef("a.latex"), "a.latex"); }); }); describe("parseDeepLink", () => { it("reads the two spellings a link in the wild actually has", () => { assert.deepEqual(parseDeepLink("astrolabe://note?path=Ideas/A.md"), { vault: null, note: "Ideas/A.md", }); assert.deepEqual(parseDeepLink("astrolabe:///note?path=Ideas/A.md"), { vault: null, note: "Ideas/A.md", }); }); it("carries a vault and a note together", () => { const link = parseDeepLink("astrolabe://open?vault=/home/me/vault¬e=A.md"); assert.equal(link?.note, "A.md"); assert.equal(link?.vault, path.normalize("/home/me/vault")); }); it("refuses anything that is not an astrolabe link", () => { assert.equal(parseDeepLink("https://example.com/"), null); assert.equal(parseDeepLink("file:///etc/passwd"), null); assert.equal(parseDeepLink("not a url"), null); assert.equal(parseDeepLink("astrolabe://delete?path=A.md"), null); assert.equal(parseDeepLink("astrolabe://open"), null); // names nothing }); it("drops a bad note but keeps a good vault", () => { const link = parseDeepLink("astrolabe://open?vault=/home/me/vault¬e=../../x.md"); assert.equal(link?.note, null); assert.equal(link?.vault, path.normalize("/home/me/vault")); }); it("refuses a relative vault", () => { // An absolute path is the only kind that means one directory. assert.equal(parseDeepLink("astrolabe://open?vault=../../..")?.vault ?? null, null); }); }); describe("knownVault", () => { const known = [path.resolve("/home/me/notes"), path.resolve("/home/me/work")]; it("honors a vault the reader has already opened", () => { assert.equal(knownVault(path.resolve("/home/me/notes"), known), known[0]); assert.equal(knownVault(path.resolve("/home/me/notes") + path.sep, known), known[0]); }); it("refuses a directory nothing has opened", () => { // THE rule. Without it, `astrolabe://open?vault=/` is a link that makes the // app index and serve the reader's entire disk. assert.equal(knownVault("/", known), null); assert.equal(knownVault(path.resolve("/home/me"), known), null); assert.equal(knownVault(path.resolve("/home/me/notes/sub"), known), null); }); }); describe("file associations", () => { const vault = path.resolve("/home/me/notes"); it("does not mistake a sibling for a child", () => { assert.equal(containsFile(vault, path.resolve("/home/me/notes-backup/A.md")), false); assert.equal(containsFile(vault, path.resolve("/home/me/notes/A.md")), true); assert.equal(containsFile(vault, vault), false); }); it("resolves a file to its vault-relative note", () => { assert.equal(relativeNote(vault, path.resolve("/home/me/notes/Ideas/A.md")), "Ideas/A.md"); assert.equal(relativeNote(vault, path.resolve("/home/me/other/A.md")), null); }); it("picks the DEEPEST vault holding the file", () => { // A reader who opened both a vault and a sub-folder of it as vaults meant // the sub-folder when they double-clicked a file inside it. const vaults = [path.resolve("/home/me"), path.resolve("/home/me/notes")]; assert.equal(vaultForFile(path.resolve("/home/me/notes/A.md"), vaults), vaults[1]); assert.equal(vaultForFile(path.resolve("/home/me/A.md"), vaults), vaults[0]); assert.equal(vaultForFile(path.resolve("/tmp/A.md"), vaults), null); }); }); describe("routeForNote", () => { it("spells a note exactly as the app's own address bar does", () => { // Not a stylistic agreement. A deep link that lands on `/Welcome.md` when // the router spells it `/Welcome` puts the reader on a URL the router // rewrites out from under them — and, in the tree-miss case, on a // different note. // // The expected values are `client/router.ts::notePathToUrl`'s, written out // rather than imported: that module reaches the store, the tree, the toast // host and a stylesheet, none of which loads under `node --test`. The two // implementations share the half that decides the answer // (`shared/noteFormat.ts::stripNoteExt`); this pins the other half. assert.equal(routeForNote("Welcome.md"), "/Welcome"); assert.equal(routeForNote("Ideas/Cache locality.md"), "/Ideas/Cache%20locality"); assert.equal(routeForNote("Paper.tex"), "/Paper"); assert.equal(routeForNote("Notes/A & B.md"), "/Notes/A%20%26%20B"); assert.equal(routeForNote("مقالة.md"), "/%D9%85%D9%82%D8%A7%D9%84%D8%A9"); }); }); // ── Ports: the reader's theme, tabs and folds, in one integer ────────────── function prefsWith(...vaults: { path: string; port: number }[]): Prefs { return { ...EMPTY_PREFS, vaults: vaults.map((v) => ({ path: v.path, port: v.port, bounds: null, lastOpened: 1 })), }; } describe("portCandidates", () => { it("offers the remembered port FIRST", () => { // The whole point: `localStorage` is keyed by origin and the origin is the // port, so the port a vault had is the port its stored layout lives on. const prefs = prefsWith({ path: "/v/a", port: 6842 }); assert.equal(portCandidates("/v/a", prefs)[0], 6842); assert.equal(rememberedPort("/v/a", prefs), 6842); }); it("offers no remembered port for a vault it has never seen", () => { assert.equal(rememberedPort("/v/new", EMPTY_PREFS), 0); }); it("covers the whole desktop band exactly once", () => { const list = portCandidates("/v/a", EMPTY_PREFS); assert.equal(list.length, PORT_MAX - PORT_MIN + 1); assert.equal(new Set(list).size, list.length); for (const port of list) assert.ok(port >= PORT_MIN && port <= PORT_MAX); }); it("skips ports other vaults already own, and never its own twice", () => { const prefs = prefsWith( { path: "/v/a", port: 6842 }, { path: "/v/b", port: 6843 }, { path: "/v/c", port: 6844 }, ); const list = portCandidates("/v/a", prefs); assert.equal(list[0], 6842); assert.equal(list.filter((p) => p === 6842).length, 1); assert.ok(!list.includes(6843)); assert.ok(!list.includes(6844)); }); it("seeds from the vault path, so the same vault lands on the same port", () => { // This is what makes a reinstall — or the same vault on a second machine — // keep its stored layout without the preferences file having survived. const first = portCandidates("/home/me/notes", EMPTY_PREFS)[0]; const again = portCandidates("/home/me/notes", EMPTY_PREFS)[0]; assert.equal(first, again); assert.notEqual(first, portCandidates("/home/me/work", EMPTY_PREFS)[0]); assert.equal(seedFor("/home/me/notes"), seedFor("/home/me/notes")); }); it("never proposes 6801 — the web deployment's own port", () => { // Running `npm start` in a terminal beside the desktop app is the normal // case, not a conflict to arbitrate. for (const vault of ["/a", "/b", "/some/deep/vault", "/خزانة"]) { assert.ok(!portCandidates(vault, EMPTY_PREFS).includes(6801)); } }); }); describe("parsePrefs", () => { it("survives anything at all", () => { for (const junk of [null, 42, "text", [], { vaults: "no" }, { vaults: [1, 2, null] }]) { const prefs = parsePrefs(junk); assert.deepEqual(prefs.vaults, []); assert.equal(prefs.spellcheck, true); } }); it("drops a bad field rather than the whole document", () => { const prefs = parsePrefs({ vaults: [ { path: "/v/a", port: 6842, lastOpened: 5 }, { port: 6843 }, // no path — meaningless { path: "/v/c", port: 99999 }, // outside the desktop band { path: "/v/a", port: 6844 }, // duplicate path ], }); assert.deepEqual(prefs.vaults.map((v) => v.path), ["/v/a", "/v/c"]); assert.equal(prefs.vaults.find((v) => v.path === "/v/c")?.port, 0); }); it("refuses window bounds that cannot be a window", () => { const sane = { x: 100, y: 80, width: 1280, height: 860, maximized: false }; assert.deepEqual(parsePrefs({ vaults: [{ path: "/v", port: 6820, bounds: sane }] }).vaults[0].bounds, sane); for (const bad of [ { x: 0, y: 0, width: 10, height: 10 }, { x: 0, y: 0, width: 1e9, height: 800 }, { x: NaN, y: 0, width: 800, height: 600 }, { x: 0, y: 0, width: 800 }, "nope", ]) { assert.equal(parsePrefs({ vaults: [{ path: "/v", port: 6820, bounds: bad }] }).vaults[0].bounds, null); } }); it("keeps a NEGATIVE origin — a display to the left of the primary one", () => { const left = { x: -1400, y: 40, width: 1280, height: 860, maximized: false }; assert.deepEqual(parsePrefs({ vaults: [{ path: "/v", port: 6820, bounds: left }] }).vaults[0].bounds, left); }); // The gap the Windows report fell into: the window's own minimum was 400 // tall, the preferences file's was 480 on both axes, and every Snap quadrant // on a 150% 1080p laptop lands between them. it("keeps every rectangle the window itself is allowed to be", () => { const smallest = { x: 0, y: 0, width: MIN_WINDOW.width, height: MIN_WINDOW.height, maximized: false }; assert.deepEqual(parsePrefs({ vaults: [{ path: "/v", port: 6820, bounds: smallest }] }).vaults[0].bounds, smallest); // A Snap quadrant on a 1366×768 laptop at 150%: 455×256 DIP is below the // floor on both axes and is still refused. const tooSmall = { x: 0, y: 0, width: 455, height: 256, maximized: false }; assert.equal(parsePrefs({ vaults: [{ path: "/v", port: 6820, bounds: tooSmall }] }).vaults[0].bounds, null); }); }); describe("fitToWorkArea", () => { const laptop = { x: 0, y: 0, width: 911, height: 512 }; it("pulls a window that hangs off the desk back onto it", () => { // Saved on a 1920×1080 desktop, re-opened on a scaled 1366 laptop: before // this, the caption buttons, the trailing edge and the bottom corner were // all outside the work area. const fitted = fitToWorkArea({ x: 1200, y: 700, width: 1280, height: 860 }, laptop); assert.deepEqual(fitted, { x: 0, y: 0, width: 911, height: 512 }); }); it("leaves a rectangle that already fits exactly alone", () => { const rect = { x: 40, y: 30, width: 700, height: 400 }; assert.deepEqual(fitToWorkArea(rect, laptop), rect); }); it("respects a work area that does not start at the origin", () => { const desk = { x: 0, y: 32, width: 1440, height: 868 }; assert.deepEqual(fitToWorkArea({ x: 0, y: 0, width: 1280, height: 860 }, desk), { x: 0, y: 32, width: 1280, height: 860 }); }); it("never returns a window smaller than the app's own floor while the desk holds it", () => { const fitted = fitToWorkArea({ x: 0, y: 0, width: 100, height: 100 }, laptop); assert.equal(fitted.width, MIN_WINDOW.width); assert.equal(fitted.height, MIN_WINDOW.height); }); }); describe("the recent list", () => { it("moves an opened vault to the head and keeps its geometry", () => { const bounds = { x: 10, y: 10, width: 900, height: 700, maximized: false }; let prefs = rememberVault(EMPTY_PREFS, "/v/a", 6842, 1); prefs = rememberBounds(prefs, "/v/a", bounds); prefs = rememberVault(prefs, "/v/b", 6843, 2); prefs = rememberVault(prefs, "/v/a", 6842, 3); assert.equal(prefs.vaults[0].path, "/v/a"); assert.deepEqual(prefs.vaults[0].bounds, bounds); assert.equal(recentVaults(prefs)[0].path, "/v/a"); }); it("caps the list", () => { let prefs = EMPTY_PREFS; for (let i = 0; i < MAX_RECENTS + 5; i++) prefs = rememberVault(prefs, `/v/${i}`, 6820 + i, i); assert.equal(prefs.vaults.length, MAX_RECENTS); }); // `maximized` is not a rectangle, so a rectangle we distrust says nothing // about it. Dropping the pair together is how a maximised window came back // at a size it had left two resizes ago. it("keeps the maximised flag even when the normal rectangle is refused", () => { const good = { x: 10, y: 10, width: 900, height: 700, maximized: false }; let prefs = rememberBounds(rememberVault(EMPTY_PREFS, "/v/a", 6842, 1), "/v/a", good); prefs = rememberBounds(prefs, "/v/a", { x: 0, y: 0, width: 200, height: 100, maximized: true }); assert.equal(prefs.vaults[0].bounds?.maximized, true); assert.equal(prefs.vaults[0].bounds?.width, 900, "the last rectangle we trusted is the one kept"); }); it("does not invent a vault from a stale window's geometry", () => { const before = rememberVault(EMPTY_PREFS, "/v/a", 6842, 1); const after = rememberBounds(before, "/v/gone", { x: 0, y: 0, width: 900, height: 700, maximized: false }); assert.deepEqual(after.vaults.map((v) => v.path), ["/v/a"]); }); it("forgets a vault, port and geometry together", () => { const prefs = forgetVault(rememberVault(EMPTY_PREFS, "/v/a", 6842, 1), "/v/a"); assert.deepEqual(prefs.vaults, []); }); }); describe("onSomeDisplay", () => { const primary = { bounds: { x: 0, y: 0, width: 1920, height: 1080 } }; const secondary = { bounds: { x: -1920, y: 0, width: 1920, height: 1080 } }; it("restores a window that is still visible", () => { assert.equal(onSomeDisplay({ x: 100, y: 100, width: 1280, height: 860, maximized: false }, [primary]), true); assert.equal(onSomeDisplay({ x: -1800, y: 60, width: 1280, height: 860, maximized: false }, [primary, secondary]), true); }); it("refuses a window on a monitor that has been unplugged", () => { // The failure this exists for: the app "does not start", while in fact it // is running perfectly, off the side of the desk. assert.equal(onSomeDisplay({ x: -1800, y: 60, width: 1280, height: 860, maximized: false }, [primary]), false); assert.equal(onSomeDisplay({ x: 4000, y: 0, width: 1280, height: 860, maximized: false }, [primary]), false); }); it("accepts a window hanging off an edge, if there is enough to grab", () => { assert.equal(onSomeDisplay({ x: 1830, y: 40, width: 1280, height: 860, maximized: false }, [primary]), true); assert.equal(onSomeDisplay({ x: 1900, y: 40, width: 1280, height: 860, maximized: false }, [primary]), false); }); }); // ── The session, as the server states it ─────────────────────────────────── describe("parseSessionCookie", () => { it("reads the name, the value and the lifetime the SERVER chose", () => { const cookie = parseSessionCookie( "astrolabe_session=v2.1.1770000000000.abc; Max-Age=604800; Path=/; HttpOnly; SameSite=Lax", ); assert.equal(cookie?.name, "astrolabe_session"); assert.equal(cookie?.value, "v2.1.1770000000000.abc"); // 7 days, and nothing in electron/ says "7" anywhere. assert.equal(cookie?.maxAge, 604800); }); it("does not care what the cookie is called", () => { assert.equal(parseSessionCookie("whatever=x; Max-Age=60")?.name, "whatever"); }); it("reports no lifetime rather than a wrong one", () => { assert.equal(parseSessionCookie("a=b; Path=/")?.maxAge, 0); assert.equal(parseSessionCookie("a=b; Max-Age=nonsense")?.maxAge, 0); assert.equal(parseSessionCookie("a=b; Max-Age=-1")?.maxAge, 0); }); it("refuses a header that is not a cookie", () => { assert.equal(parseSessionCookie(""), null); assert.equal(parseSessionCookie("=novalue"), null); assert.equal(parseSessionCookie("noequals"), null); }); }); // ── The bridge's vocabulary ──────────────────────────────────────────────── describe("the IPC register", () => { it("names every channel exactly once, in one namespace", () => { const all = [...Object.values(TO_MAIN), ...Object.values(TO_RENDERER)]; assert.equal(new Set(all).size, all.length, "a channel name is used twice"); for (const channel of all) assert.match(channel, /^astrolabe:[a-z-]+$/); }); it("has no command the menu could send that is not spelled here", () => { assert.equal(new Set(COMMANDS).size, COMMANDS.length); for (const command of COMMANDS) assert.match(command, /^[a-z-]+$/); }); }); describe("the data-dir override", () => { it("parses only an absolute path, and survives a round trip", () => { const prefs = parsePrefs({ vaults: [ { path: "/v/a", port: 6821, lastOpened: 5, data: "/srv/astrolabe-data" }, { path: "/v/b", port: 6822, lastOpened: 4, data: "relative/nope" }, ], }); assert.equal(prefs.vaults[0].data, "/srv/astrolabe-data"); assert.equal(prefs.vaults[1].data, undefined); }); it("SURVIVES rememberVault, which rebuilds the row on every open", () => { // Without the carry, the override existed until the first launch: the // rewrite dropped it, the vault silently reverted to an empty per-app // home, and the desktop "lost" the reader's settings. let prefs = parsePrefs({ vaults: [{ path: "/v/a", port: 6821, lastOpened: 5, data: "/srv/astrolabe-data" }], }); prefs = rememberVault(prefs, "/v/a", 6823, 99); assert.equal(prefs.vaults[0].data, "/srv/astrolabe-data"); assert.equal(prefs.vaults[0].port, 6823); }); }); describe("the voice Windows chose", () => { it("reads a REG_SZ out of reg query, whatever the value column is called", () => { const token = [ "", "HKEY_CURRENT_USER\\Software\\Microsoft\\Speech_OneCore\\Voices", " DefaultTokenId REG_SZ HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_frFR_PaulM", "", ].join("\r\n"); assert.equal(regValue(token), "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_frFR_PaulM"); const name = "\r\nHKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_frFR_PaulM\r\n (par défaut) REG_SZ Microsoft Paul - French (France)\r\n"; assert.equal(regValue(name), "Microsoft Paul - French (France)"); assert.equal(regValue("ERROR: The system was unable to find the specified registry key or value."), null); }); it("is asked only on Windows", async () => { assert.equal(await windowsSystemVoice("linux"), null); assert.equal(await windowsSystemVoice("darwin"), null); }); }); describe("the app zoom", () => { it("survives a relaunch: remembered, written, read back, and carried through the next open", () => { // The reader's report: 120%, close, launch — 100% again. rememberZoom // wrote it; the next launch's rememberVault rebuilt the row without it. let prefs = rememberVault(EMPTY_PREFS, "/v/a", 6842, 1); prefs = rememberZoom(prefs, "/v/a", stepZoom(1, 1)); assert.equal(prefs.vaults[0].zoom, 1.2); // desktop.json on disk, and the launch that reads it back. let next = parsePrefs(JSON.parse(JSON.stringify(prefs))); assert.equal(next.vaults[0].zoom, 1.2); next = rememberVault(next, "/v/a", 6842, 2); next = rememberBounds(next, "/v/a", { x: 10, y: 10, width: 1000, height: 700, maximized: false }); assert.equal(next.vaults[0].zoom, 1.2, "the open and the geometry keep the zoom"); // A vault never zoomed has no field at all, and opens at 1. assert.equal(rememberVault(EMPTY_PREFS, "/v/b", 6843, 1).vaults[0].zoom, undefined); }); it("is a sane factor whatever the file says, and a reset is exactly 1", () => { assert.equal(parsePrefs({ vaults: [{ path: "/v/a", port: 6821, lastOpened: 1, zoom: 40 }] }).vaults[0].zoom, 3); assert.equal(parsePrefs({ vaults: [{ path: "/v/a", port: 6821, lastOpened: 1, zoom: "big" }] }).vaults[0].zoom, 1); assert.equal(saneZoom(0.1), 0.5); assert.equal(stepZoom(1.44, 0), 1); assert.ok(Math.abs(stepZoom(1.2, -1) - 1) < 1e-9); }); it("is not invented for a vault that was never opened", () => { const prefs = rememberZoom(EMPTY_PREFS, "/v/ghost", 1.2); assert.equal(prefs.vaults.length, 0); }); }); describe("an env-linked vault is the deployment, in a window", () => { it("parses the .env dialect node --env-file reads, totally", () => { const env = parseEnvFile( [ "# a comment", "PORT=8080", "SITE_NAME='Night Garden'", 'ADMIN_PASSWORD_HASH="$argon2id$v=19$m=65536,t=3"', " PUBLIC_LAYOUT = blog ", "not a line", "lower=case is not a key", "", ].join("\n"), ); assert.equal(env.SITE_NAME, "Night Garden"); assert.equal(env.ADMIN_PASSWORD_HASH, "$argon2id$v=19$m=65536,t=3"); assert.equal(env.PUBLIC_LAYOUT, "blog"); assert.equal(env.lower, undefined); }); it("the deployment's identity wins; the desktop keeps only placement", () => { const cred = { password: "minted", hash: "minted-hash", secret: "minted-secret" }; const env = childEnv("/v", "/data", 6821, cred, { PORT: "8080", HOST: "0.0.0.0", ADMIN_PASSWORD_HASH: "owner-hash", SESSION_SECRET: "owner-secret", PUBLIC_LAYOUT: "blog", SITE_LANG: "ar", }); // Identity is the deployment's: the owner's password works, the public // site is public, the site speaks its own language. assert.equal(env.ADMIN_PASSWORD_HASH, "owner-hash"); assert.equal(env.SESSION_SECRET, "owner-secret"); assert.equal(env.PUBLIC_LAYOUT, "blog"); assert.equal(env.PUBLIC, undefined); // Placement is the desktop's: a deployment's PORT following it into a // window would fight the deployment for its own socket. assert.equal(env.PORT, "6821"); assert.equal(env.HOST, "127.0.0.1"); }); it("without a linked deployment, the minted credential still rules", () => { const cred = { password: "minted", hash: "minted-hash", secret: "minted-secret" }; const env = childEnv("/v", "/data", 6821, cred, null); assert.equal(env.ADMIN_PASSWORD_HASH, "minted-hash"); assert.equal(env.PUBLIC, "false"); }); it("the desktop is its own operator: Read aloud's external speaker is allowed, unless a deployment's .env says off", () => { const cred = { password: "minted", hash: "minted-hash", secret: "minted-secret" }; assert.equal(childEnv("/v", "/data", 6821, cred, null).SPEAK_EXTERNAL, "on"); assert.equal(childEnv("/v", "/data", 6821, cred, { SITE_LANG: "ar" }).SPEAK_EXTERNAL, "on"); assert.equal(childEnv("/v", "/data", 6821, cred, { SPEAK_EXTERNAL: "off" }).SPEAK_EXTERNAL, "off"); }); }); // ── The spellchecker's languages ─────────────────────────────────────────── // // electron/spellcheck.ts imports Electron's types, which the root typecheck // cannot resolve (desktop/ has its own node_modules; `npm run check-desktop` // typechecks it there), so the whitelist is asserted from the SOURCE: every // language `shared/script.ts::spellcheckLang` can stamp on a line must be // enabled, or the attribute is inert on the desktop. describe("spellcheck languages", () => { it("enables every language a line can be stamped with", () => { const src = readFileSync(new URL("../electron/spellcheck.ts", import.meta.url), "utf8"); const m = /export const LINE_LANGUAGES = \[([^\]]+)\]/.exec(src); assert.ok(m, "LINE_LANGUAGES is declared"); const listed = [...(m?.[1] ?? "").matchAll(/"([a-z]+)"/g)].map((x) => x[1]); // he, fa, ar from the script test; fr from shared/french.ts's line test; // ja from the furigana work (no dictionary anywhere — dropped at runtime). assert.deepEqual(listed, ["he", "fa", "ar", "fr", "ja"]); }); }); // ── Discord presence: one card for every open vault ─────────────────────── describe("PresenceBook", () => { const A = { type: 0 as const, details: "Writing" }; const B = { type: 0 as const, details: "Drawing" }; const msg = (clientId: string | null, activity: typeof A | null) => ({ type: "astrolabe:presence" as const, clientId, activity }); it("is null with nothing to show", () => { const book = new PresenceBook(); assert.equal(book.pick(), null); book.set("/v/one", msg(null, null), 1); assert.equal(book.pick(), null); }); it("the most recent non-null activity wins across vaults", () => { const book = new PresenceBook(); book.set("/v/one", msg("111111111111111111", A), 1); book.set("/v/two", msg("222222222222222222", B), 2); assert.deepEqual(book.pick(), { clientId: "222222222222222222", activity: B }); book.set("/v/one", msg("111111111111111111", A), 3); assert.deepEqual(book.pick(), { clientId: "111111111111111111", activity: A }); }); it("a vault that turns null steps aside instead of clearing another's card", () => { const book = new PresenceBook(); book.set("/v/one", msg("111111111111111111", A), 1); book.set("/v/two", msg(null, null), 2); assert.deepEqual(book.pick(), { clientId: "111111111111111111", activity: A }); book.set("/v/one", msg("111111111111111111", null), 3); assert.equal(book.pick(), null); }); it("a dropped vault is forgotten", () => { const book = new PresenceBook(); book.set("/v/one", msg("111111111111111111", A), 1); book.set("/v/two", msg("222222222222222222", B), 2); book.drop("/v/two"); assert.deepEqual(book.pick(), { clientId: "111111111111111111", activity: A }); }); }); describe("presence messages", () => { it("the child's message is recognized, and nothing else on the channel is", () => { assert.equal(isPresenceToParent({ type: "astrolabe:presence", clientId: "1", activity: null }), true); assert.equal(isPresenceToParent({ type: "astrolabe:presence", clientId: null, activity: { type: 0 } }), true); assert.equal(isPresenceToParent({ type: "astrolabe:listening", port: 6801 }), false); assert.equal(isPresenceToParent({ type: "astrolabe:presence", clientId: 1, activity: null }), false); assert.equal(isPresenceToParent({ type: "astrolabe:presence", clientId: "1", activity: [] }), false); assert.equal(isPresenceToParent(null), false); }); it("the parent's status message round-trips through the child's guard", () => { for (const c of ["off", "searching", "connected", "invalid-id"] as const) { assert.deepEqual(presenceStatusMessage(c), { type: "astrolabe:presence-status", connection: c }); assert.equal(isPresenceFromParent(presenceStatusMessage(c)), true); } assert.equal(isPresenceFromParent({ type: "astrolabe:presence-status", connection: "lost" }), false); assert.equal(isPresenceFromParent({ type: "astrolabe:presence", connection: "off" }), false); assert.equal(isPresenceFromParent("x"), false); }); });