tests/libraryIndex.test.ts
Rama main · réplica de lectura del repositorio original.
// THE SHELF AS THE INDEX BUILDS IT — `libraryRefs()`, `libraryLessons()` and
// the cover allowlist, over a real fixture vault.
//
// tests/library.test.ts covers the RULES (what a legal row is, how a unit's
// name is read). This file covers the join to the index, which is where the
// publish-safety bugs live: what a visitor may fetch, what a folder note may
// put in an `href`, and how long resolving a forty-lesson book takes on the
// route an anonymous visitor hits for the door.
import assert from "node:assert/strict";
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { after, before, describe, it } from "node:test";
import {
indexFile,
initIndexer,
isAllowedAttachment,
libraryLessons,
libraryRefs,
posts,
type FilterLang,
} from "../server/indexer.ts";
import { getSettings, moveLibraryFolders, patchSettings } from "../server/settings.ts";
import { initSite } from "../server/site.ts";
import { initVault } from "../server/vault.ts";
import { makeDir, makeVault, note, removeVault } from "./helpers/vault.ts";
const PNG = "\x89PNG\r\n\x1a\n0123";
const OFF: FilterLang = null;
const BOOKS = "1 - Source Material/Books";
const LECTURES = "1 - Source Material/Lectures";
/** A book with `lessons` published notes spread over four chapters — the shape
* the reading companion writes, and the shape the perf case needs. */
function book(name: string, lessons: number): Record<string, string> {
const out: Record<string, string> = {};
for (let n = 1; n <= lessons; n++) {
const chapter = `Chapter ${1 + (n % 4)}`;
out[`${BOOKS}/${name}/${chapter}/Idea ${n}.md`] = note(
{ publish: "true", date: "2025-03-03" },
`A paragraph of English prose long enough to be classified and counted, number ${n}.\n`,
);
}
return out;
}
const files: Record<string, string> = {
// DECLARED IN THE VAULT, NOTHING PUBLISHED. Its cover must not be servable
// and its `source:` must not survive the walk.
[`${BOOKS}/Silent/Silent.md`]: note(
{ library: "book", cover: "covers/silent.jpg", source: "javascript:alert(1)" },
"A book whose notes are all drafts.\n",
),
[`${BOOKS}/Silent/Chapter 1/Draft.md`]: note({}, "Not published yet, not by anybody.\n"),
// DECLARED IN THE VAULT WITH A PUBLISHED LESSON.
[`${BOOKS}/Loud/Loud.md`]: note(
{ library: "book", cover: "covers/loud.jpg", source: "https://example.org/loud" },
"A book with a lesson a visitor may read.\n",
),
[`${BOOKS}/Loud/Chapter 1/First.md`]: note(
{ publish: "true", date: "2025-01-01" },
"The first published lesson, a full English paragraph so the language filter can classify it.\n",
),
// TWO ARABIC TITLES under the same parent. Neither makes an address, and
// under the old rule they became `/library/path` and `/library/path-2`,
// renumbered by whichever book existed that morning.
[`${BOOKS}/التفكير العلمي/الفصل 1/بداية.md`]: note(
{ publish: "true", date: "2025-02-02" },
"فقرة عربية كاملة تكفي لتصنيف اللغة وعدّ كلماتها في هذا الدرس الأول.\n",
),
[`${BOOKS}/نقد الفكر الديني/الفصل 1/بداية.md`]: note(
{ publish: "true", date: "2025-02-03" },
"فقرة عربية أخرى كاملة تكفي لتصنيف اللغة وعدّ كلماتها في هذا الدرس.\n",
),
// Claimed by the root, then taken back off the shelf by its own folder note.
[`${BOOKS}/Hushed/Hushed.md`]: note({ hidden: "true" }, "A book the owner is not showing yet.\n"),
[`${BOOKS}/Hushed/Chapter 1/One.md`]: note(
{ publish: "true", date: "2025-05-05" },
"A published note inside a folder whose own note says it is hidden.\n",
),
// Under the root with nothing published: never a path.
[`${BOOKS}/Unwritten/Chapter 1/Notes.md`]: note({}, "Nothing here is published yet, by anyone.\n"),
// Directly inside the root: a post, not a path, and not a reason to make the
// root itself a book.
[`${BOOKS}/A reading list.md`]: note(
{ publish: "true", date: "2025-04-04" },
"A published note sitting directly inside the folder that holds the books.\n",
),
// A second parent whose every child IS named by a row — the shape the
// invariance case needs.
[`${LECTURES}/PLSC 114/L1/Opening.md`]: note(
{ publish: "true", date: "2025-06-06" },
"The opening lecture of the course, in a full English paragraph.\n",
),
[`${LECTURES}/6.036/L1/Opening.md`]: note(
{ publish: "true", date: "2025-06-07" },
"The opening lecture of the other course, in a full English paragraph.\n",
),
"covers/silent.jpg": PNG,
"covers/loud.jpg": PNG,
...book("Mechanics", 40),
...book("Optics", 40),
...book("Thermodynamics", 40),
...book("Relativity", 40),
...book("Statistics", 40),
};
const data = makeDir();
const root = makeVault(files);
const ROWS = ["Mechanics", "Optics", "Thermodynamics", "Relativity", "Statistics"].map((name, n) => ({
id: `row${n}`,
slug: name.toLowerCase(),
folder: `${BOOKS}/${name}`,
kind: "book" as const,
title: name,
}));
before(async () => {
initSite({ ASTROLABE_DATA: data });
initVault(root);
await initIndexer();
patchSettings({ library: { enabled: true, paths: ROWS } });
});
after(() => {
removeVault(root);
removeVault(data);
});
describe("the cover allowlist asks whether anyone can reach the path", () => {
it("serves the cover of a path with a published lesson", () => {
assert.equal(isAllowedAttachment("covers/loud.jpg"), true);
});
it("refuses the cover of a declared path with nothing published", () => {
// Before this rule, `library: book` plus `cover:` in a folder note handed
// an anonymous caller a picture out of a folder every note of which is a
// draft — and no page on the site linked to it, because the path with no
// lesson is never sent.
assert.equal(isAllowedAttachment("covers/silent.jpg"), false);
});
});
describe("a folder note's source is judged like a row's", () => {
it("drops a source that is not http(s)", () => {
const silent = libraryRefs().find((ref) => ref.folder.endsWith("/Silent"));
assert.ok(silent, "the declared path is on the shelf");
assert.equal(silent.source, undefined);
});
it("keeps an https source", () => {
const loud = libraryRefs().find((ref) => ref.folder.endsWith("/Loud"));
assert.equal(loud?.source, "https://example.org/loud");
});
});
describe("resolving a shelf costs one vault walk, not one per lesson", () => {
it("resolves forty lessons for less than the whole feed costs", () => {
// `postMeta()` defaults its collection rows to `collectionRowsNow()`,
// which walks the whole vault. Called per lesson — which is what this
// function used to do — a five-path shelf of forty-lesson books was two
// hundred full walks, on every anonymous /api/me, which is the route that
// asks whether the door should be drawn.
//
// The bound is RELATIVE, against `posts()`: one walk over the published
// set plus a postMeta for each of two hundred notes. Forty lessons costing
// more than that whole feed can only mean the walk is back inside the
// loop, and a ratio calibrates itself to whatever machine is running it
// where a millisecond count does not. Both are warmed first — postMeta
// caches its excerpt and word count on the record, and measuring that
// cache filling would be measuring the wrong thing.
const ROUNDS = 25;
const spent = (run: () => void): number => {
const started = performance.now();
for (let n = 0; n < ROUNDS; n++) run();
return performance.now() - started;
};
const feedOnce = (): void => void posts(true, OFF);
const bookOnce = (): void => void libraryLessons(`${BOOKS}/Mechanics`, true, OFF);
feedOnce();
bookOnce();
// Many rounds, not one: both calls are sub-millisecond once the caches are
// warm, and a single pair of readings on a machine running the rest of the
// suite beside it measures the scheduler. Twenty-five of each averages the
// jitter out and leaves the forty-fold signal untouched.
const feed = Math.max(spent(feedOnce), 1);
const bookCost = spent(bookOnce);
assert.equal(libraryLessons(`${BOOKS}/Mechanics`, true, OFF).length, 40);
assert.ok(
bookCost < feed * 3,
`forty lessons took ${bookCost.toFixed(1)} ms against a ${feed.toFixed(1)} ms feed over the whole vault, ${ROUNDS} rounds each`,
);
});
it("finds every lesson of every path on the shelf", () => {
let total = 0;
for (const row of ROWS) total += libraryLessons(row.folder, true, OFF).length;
assert.equal(total, 200);
});
});
describe("a folder's rename carries its library row", () => {
it("repoints the row and its subtree, and drops it on a delete", () => {
patchSettings({
library: {
enabled: true,
paths: [
{ id: "a", slug: "mechanics", folder: `${BOOKS}/Mechanics`, kind: "book", title: "Mechanics" },
{ id: "b", slug: "nested", folder: `${BOOKS}/Mechanics/Chapter 1`, kind: "book", title: "Chapter one" },
{ id: "c", slug: "optics", folder: `${BOOKS}/Optics`, kind: "book", title: "Optics" },
],
},
});
moveLibraryFolders(`${BOOKS}/Mechanics`, `${BOOKS}/Classical Mechanics`);
const moved = getSettings().library?.paths ?? [];
assert.deepEqual(
moved.map((p) => p.folder),
[`${BOOKS}/Classical Mechanics`, `${BOOKS}/Classical Mechanics/Chapter 1`, `${BOOKS}/Optics`],
);
// The address survives the rename: that is the whole point — reader
// progress is keyed by slug, and a book does not move house.
assert.deepEqual(moved.map((p) => p.slug), ["mechanics", "nested", "optics"]);
moveLibraryFolders(`${BOOKS}/Classical Mechanics`, null);
assert.deepEqual(
(getSettings().library?.paths ?? []).map((p) => p.folder),
[`${BOOKS}/Optics`],
);
});
it("does not touch the file when no row is under the folder", () => {
const before = JSON.stringify(getSettings().library);
moveLibraryFolders(`${BOOKS}/Nothing Here`, `${BOOKS}/Still Nothing`);
assert.equal(JSON.stringify(getSettings().library), before);
});
});
// ── Shelf roots ────────────────────────────────────────────────────────────
const ROOT_BOOKS = { id: "r1", folder: BOOKS, kind: "book" as const };
/** The ref for one folder leaf, or undefined. */
const refFor = (leaf: string) => libraryRefs().find((ref) => ref.folder.endsWith(`/${leaf}`));
describe("a shelf root claims every published folder inside it", () => {
it("adds the children with published notes, and only those", () => {
patchSettings({ library: { enabled: true, paths: [], roots: [ROOT_BOOKS] } });
const folders = libraryRefs().map((ref) => ref.folder);
assert.ok(folders.includes(`${BOOKS}/Mechanics`), "a child with published notes is a path");
assert.ok(folders.includes(`${BOOKS}/Loud`), "a folder note under the root is still a path");
assert.ok(!folders.includes(`${BOOKS}/Unwritten`), "a child with nothing published is not a path");
assert.ok(!folders.includes(BOOKS), "a note directly in the root does not make the root a path");
});
it("gives a child the root's kind, and lets its folder note overrule", () => {
patchSettings({ library: { enabled: true, paths: [], roots: [{ ...ROOT_BOOKS, kind: "course" }] } });
assert.equal(refFor("Mechanics")?.kind, "course");
// `Loud/Loud.md` says `library: book`, which is why a root is not the last
// word about one folder.
assert.equal(refFor("Loud")?.kind, "book");
});
it("lets a folder note take a claimed child back off the shelf", () => {
patchSettings({ library: { enabled: true, paths: [], roots: [ROOT_BOOKS] } });
assert.equal(refFor("Hushed")?.hidden, true);
});
it("orders a root's children by title, behind the rows", () => {
patchSettings({
library: {
enabled: true,
roots: [ROOT_BOOKS],
paths: [{ id: "keep", slug: "optics", folder: `${BOOKS}/Optics`, kind: "book", title: "Optics" }],
},
});
const refs = libraryRefs();
assert.equal(refs[0].folder, `${BOOKS}/Optics`, "rows come first, in their own order");
// The root's children by title; then `Silent`, which no root claims (it
// has nothing published) and which reaches the shelf through its own
// folder note, in the vault order that branch has always used.
assert.deepEqual(refs.slice(1).map((ref) => ref.title), [
"Hushed",
"Loud",
"Mechanics",
"Relativity",
"Statistics",
"Thermodynamics",
"Silent",
]);
});
});
describe("a derived address is an address or it is nothing", () => {
it("does not publish a folder whose title makes no address", () => {
patchSettings({ library: { enabled: true, paths: [], roots: [ROOT_BOOKS] } });
const slugs = libraryRefs().map((ref) => ref.slug);
assert.ok(
!slugs.some((slug) => slug === "path" || /^path-\d+$/.test(slug)),
`no placeholder address survives: ${slugs.join(", ")}`,
);
assert.equal(refFor("التفكير العلمي"), undefined);
assert.equal(refFor("نقد الفكر الديني"), undefined);
});
it("never displaces a row: the derived path stands down", () => {
patchSettings({
library: {
enabled: true,
roots: [ROOT_BOOKS],
// A row PINNING the address the derived `Loud` would otherwise take.
paths: [{ id: "pin", slug: "loud", folder: `${BOOKS}/Optics`, kind: "book", title: "Optics" }],
},
});
const refs = libraryRefs();
assert.equal(refs.filter((ref) => ref.slug === "loud").length, 1);
assert.equal(refs.find((ref) => ref.slug === "loud")?.folder, `${BOOKS}/Optics`);
assert.equal(refs.find((ref) => ref.folder === `${BOOKS}/Loud`), undefined);
});
it("publishes an Arabic-titled folder once its note carries a slug", async () => {
// The vault-portable answer to "Needs an address": no settings row needed.
const notePath = `${BOOKS}/التفكير العلمي/التفكير العلمي.md`;
const abs = join(root, notePath);
mkdirSync(dirname(abs), { recursive: true });
writeFileSync(abs, note({ slug: "scientific-thinking" }, "ملاحظة المجلد نفسه، بلا عنوان جديد.\n"));
await indexFile(notePath);
patchSettings({ library: { enabled: true, paths: [], roots: [ROOT_BOOKS] } });
assert.equal(refFor("التفكير العلمي")?.slug, "scientific-thinking");
// And the OTHER one still waits: one address does not free the other.
assert.equal(refFor("نقد الفكر الديني"), undefined);
});
});
describe("adding a root over the rows that already name its children", () => {
it("changes nothing a visitor sees", () => {
const rows = [
{ id: "a", slug: "plsc-114", folder: `${LECTURES}/PLSC 114`, kind: "course" as const, title: "PLSC 114" },
{ id: "b", slug: "6-036", folder: `${LECTURES}/6.036`, kind: "course" as const, title: "6.036" },
];
patchSettings({ library: { enabled: true, roots: [], paths: rows } });
const before = JSON.stringify(libraryRefs());
patchSettings({ library: { enabled: true, roots: [{ id: "r2", folder: LECTURES, kind: "course" }], paths: rows } });
assert.equal(JSON.stringify(libraryRefs()), before, "a root over folders the rows already name adds nothing");
// And folding a row that said nothing the folder does not leaves the path
// where it was, with the address its title suggests.
patchSettings({ library: { enabled: true, roots: [{ id: "r2", folder: LECTURES, kind: "course" }], paths: [rows[0]] } });
const folded = libraryRefs().find((ref) => ref.folder === `${LECTURES}/6.036`);
assert.equal(folded?.slug, "6-036");
assert.equal(folded?.kind, "course");
});
});
describe("what the PATCH handler refuses a root", () => {
const refusal = (patch: Parameters<typeof patchSettings>[0]): string => {
try {
patchSettings(patch);
return "";
} catch (err) {
return (err as Error).message;
}
};
const roots = (list: unknown): Parameters<typeof patchSettings>[0] => ({
library: { enabled: true, paths: [], roots: list as never },
});
it("refuses more than eight", () => {
const many = Array.from({ length: 9 }, (_, n) => ({ id: `r${n}`, folder: `${BOOKS}/R${n}`, kind: "book" }));
assert.match(refusal(roots(many)), /too many roots/);
});
it("refuses a root inside another root", () => {
assert.match(
refusal(roots([ROOT_BOOKS, { id: "r2", folder: `${BOOKS}/Mechanics`, kind: "book" }])),
/sits inside another root or inside a path/,
);
});
it("refuses the vault itself", () => {
assert.match(refusal(roots([{ id: "r1", folder: "", kind: "book" }])), /names the vault itself/);
});
it("refuses a kind that is not a kind", () => {
assert.match(refusal(roots([{ id: "r1", folder: BOOKS, kind: "film" }])), /kind that is not book, course or series/);
});
it("refuses a root inside a row's folder", () => {
assert.match(
refusal({
library: {
enabled: true,
paths: [{ id: "a", slug: "mechanics", folder: `${BOOKS}/Mechanics`, kind: "book", title: "Mechanics" }],
roots: [{ id: "r1", folder: `${BOOKS}/Mechanics/Chapter 1`, kind: "book" }],
},
}),
/sits inside another root or inside a path/,
);
});
});