// Feeds and read-later (docs/feeds.md): the list a note keeps // (shared/feeds.ts), the three formats and discovery (server/feedParse.ts), // the sanitiser (shared/feedHtml.ts), and the whole round against a LOCAL // server serving the fixtures under tests/fixtures/feeds — nothing here // touches the network beyond 127.0.0.1. Every fixture's own host is rewritten // to the local origin as it is served, so an item's link, and the page Keep // fetches for it, are local too. import assert from "node:assert/strict"; import { after, before, describe, it } from "node:test"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { createServer, type Server } from "node:http"; import type { AddressInfo } from "node:net"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { FEEDS_DEFAULT_MINUTES, feedsCadenceMinutes, groupItems, keptNote, parseFeedList, READING_FOLDER } from "../shared/feeds.ts"; import { htmlExcerpt, sanitizeFeedHtml } from "../shared/feedHtml.ts"; import { discoverFeeds, parseFeed, parseFeedDate } from "../server/feedParse.ts"; import { closeFeeds, feedsState, initFeeds, keepItem, markRead, openItem, runRound } from "../server/feeds.ts"; import { decks, deckCards, indexFile, initIndexer, isNotePublished } from "../server/indexer.ts"; import { patchSettings } from "../server/settings.ts"; import { initSite } from "../server/site.ts"; import { initVault } from "../server/vault.ts"; import { makeDir, makeVault, removeVault } from "./helpers/vault.ts"; const FIX = path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures", "feeds"); const fixture = (name: string): string => readFileSync(path.join(FIX, name), "utf8"); describe("the list (shared/feeds.ts)", () => { it("reads addresses, folders and tags from every feeds fence, and nothing outside them", () => { const md = [ "# Feeds", "", "https://outside.example/rss is prose, not a feed", "", "```feeds", "#reading", "https://a.example/feed.xml → Reading/Essays", "#essays #long-form", "// a comment line", "https://b.example/rss -> Clips/B", "", "https://c.example/atom", "```", "", "```js", "https://d.example/not-a-feed", "```", "", "```feeds", "https://a.example/feed.xml", "ftp://nope.example/", "https://e.example/x → ../escape", "```", ].join("\n"); const { feeds, problems } = parseFeedList(md); assert.deepEqual( feeds.map((f) => [f.url, f.folder, f.tags]), [ ["https://a.example/feed.xml", "Reading/Essays", ["reading", "essays", "long-form"]], ["https://b.example/rss", "Clips/B", ["reading"]], ["https://c.example/atom", READING_FOLDER, ["reading"]], ["https://e.example/x", READING_FOLDER, []], ], ); assert.deepEqual(problems.map((p) => p.reason), ["duplicate", "notAnAddress", "badFolder"]); assert.equal(problems[0].line, 20); }); it("an unclosed fence runs to the end; a heading is not a tag line", () => { const { feeds, problems } = parseFeedList("```feeds\nhttps://x.example/f\n# Not tags\n"); assert.equal(feeds.length, 1); assert.deepEqual(problems.map((p) => p.reason), ["notAnAddress"]); }); it("the cadence is sync's when sync runs on a timer, else the hour", () => { assert.equal(feedsCadenceMinutes({ enabled: true, remote: "git@x:y", intervalMinutes: 15 }), 15); assert.equal(feedsCadenceMinutes({ enabled: true, remote: null, intervalMinutes: 15 }), FEEDS_DEFAULT_MINUTES); assert.equal(feedsCadenceMinutes({ enabled: false, remote: "git@x:y", intervalMinutes: 15 }), FEEDS_DEFAULT_MINUTES); }); it("a kept note names its source, feed, dates and tags, and carries no publish key", () => { const text = keptNote({ title: "On: Things", url: "https://x.example/a", feed: 'The "Feed"', published: "2026-09-20", kept: "2026-09-23", tags: ["reading", "long-form", "2026", "true"], body: "# On: Things\n\nBody.", }); assert.equal( text, '---\nsource: "https://x.example/a"\nfeed: "The \\"Feed\\""\npublished: 2026-09-20\nkept: 2026-09-23\ntags: [reading, long-form, "2026", "true"]\n---\n\n# On: Things\n\nBody.\n', ); assert.doesNotMatch(text, /publish:/); }); it("groups unread items by feed in the list's order, newest first", () => { const feed = (url: string) => ({ url, title: url, site: null, folder: "R", tags: [], unread: 0, checked: null, error: null }); const item = (f: string, guid: string, published: number) => ({ feed: f, guid, title: guid, url: null, author: null, published, read: false, kept: null, excerpt: "" }); const out = groupItems([feed("b"), feed("a"), feed("empty")], [item("a", "a1", 1), item("b", "b1", 1), item("a", "a2", 5)]); assert.deepEqual(out.map((g) => [g.feed.url, g.items.map((i) => i.guid)]), [["b", ["b1"]], ["a", ["a2", "a1"]]]); }); }); describe("the sanitiser (shared/feedHtml.ts)", () => { it("keeps prose, drops script and handlers, resolves and fences links and images", () => { const out = sanitizeFeedHtml( '
Hi there
bad ok
Hi there
bad ok
',
);
});
it("escapes text and attribute values", () => {
assert.equal(sanitizeFeedHtml('a <b> & "c"
q', null), 'a <b> & "c"
q'); }); it("an excerpt is the first words, plain", () => { assert.equal(htmlExcerpt("One two
three
"), "One two three"); assert.match(htmlExcerpt(`${"word ".repeat(100)}
`, 40), /…$/); }); }); describe("the three formats (server/feedParse.ts)", () => { it("RSS 2.0: content:encoded over description, relative links resolved, a stable id for every item", () => { const feed = parseFeed(fixture("rss.xml"), "application/rss+xml", "https://marginal.example/rss.xml"); assert.ok(feed); assert.equal(feed.kind, "rss"); assert.equal(feed.title, "The Marginal Reader"); assert.equal(feed.site, "https://marginal.example/"); assert.equal(feed.items.length, 3); const [a, b, c] = feed.items; assert.equal(a.guid, "marginal-2026-09-20-commonplace"); assert.equal(a.url, "https://marginal.example/essays/commonplace"); assert.equal(a.author, "Ibn Muqla"); assert.equal(a.published, Date.parse("2026-09-20T08:30:00Z")); assert.match(a.content ?? "", /commonplace book is a reader's<\/strong>/); assert.equal(a.summary, "A short teaser about commonplace books."); assert.equal(b.guid, "https://marginal.example/essays/marginalia"); assert.equal(b.summary, null); assert.equal(c.guid, "https://marginal.example/notes/undated"); assert.equal(c.published, null); }); it("Atom: xhtml content printed back, html content unescaped, xml:base honoured", () => { const feed = parseFeed(fixture("atom.xml"), "application/atom+xml", "https://atlas.example/atom.xml"); assert.ok(feed); assert.equal(feed.kind, "atom"); assert.equal(feed.site, "https://atlas.example/"); const [a, b] = feed.items; assert.equal(a.title, "The Astrolabe’s Rete"); assert.equal(a.url, "https://atlas.example/posts/rete"); assert.equal(a.author, "Maryam al-Ijliya"); assert.match(a.content ?? "", /The rete<\/em> is the pierced plate/);
assert.match(a.content ?? "", / The sighting rule on the back. First paragraph. Second paragraph with <angle> brackets.
/);
assert.equal(b.content, "
]*loading="lazy"`));
const teaser = openItem(`${origin}/rss.xml`, `${origin}/essays/marginalia`);
assert.equal(teaser.summaryOnly, true);
});
it("Keep writes the article from the feed into the feed's folder, with its facts, and never publishes it", async () => {
const out = await keepItem(`${origin}/rss.xml`, "marginal-2026-09-20-commonplace");
assert.equal(out.path, "Reading/Marginal/On Keeping a Commonplace Book.md");
assert.equal(out.fetched, false);
const text = readFileSync(path.join(root, out.path), "utf8");
assert.match(text, new RegExp(`^---\\nsource: "${origin}/essays/commonplace"\\nfeed: "The Marginal Reader"\\npublished: 2026-09-2\\d\\nkept: \\d{4}-\\d\\d-\\d\\d\\ntags: \\[reading\\]\\n---\\n\\n# On Keeping a Commonplace Book\\n\\nA commonplace book is a \\*\\*reader's\\*\\* memory`));
assert.doesNotMatch(text, /alert|steal/);
assert.equal(isNotePublished(out.path), false);
// Kept is read, and a second Keep is the same note.
const again = await keepItem(`${origin}/rss.xml`, "marginal-2026-09-20-commonplace");
assert.deepEqual(again, { path: out.path, fetched: false, already: true });
const state = await feedsState();
assert.ok(!state.items.some((i) => i.guid === "marginal-2026-09-20-commonplace" && i.feed === `${origin}/rss.xml`));
});
it("Keep of a teaser fetches the page and keeps its article, not its navigation", async () => {
const out = await keepItem(`${origin}/rss.xml`, `${origin}/essays/marginalia`);
assert.equal(out.fetched, true);
assert.equal(out.path, "Reading/Marginal/Marginalia, Briefly.md");
const text = readFileSync(path.join(root, out.path), "utf8");
assert.match(text, /The margin is where a book talks back/);
assert.match(text, /> A book is a machine to think with\./);
assert.match(text, new RegExp(`\\[the commonplace book\\]\\(${origin}/essays/commonplace\\)`));
assert.doesNotMatch(text, /Home|©/);
assert.ok(hits.includes("/essays/marginalia"));
});
it("with fetching off, Keep keeps what the feed carried and asks nobody", async () => {
patchSettings({ feeds: { fetch: false } });
const before = hits.length;
const out = await keepItem(`${origin}/atom.xml`, "tag:atlas.example,2026:alidade");
assert.equal(out.fetched, false);
assert.equal(hits.length, before);
assert.match(readFileSync(path.join(root, out.path), "utf8"), /tags: \[reading, essays\]\n---\n\n# Alidade\n\nThe sighting rule on the back\.\n$/);
patchSettings({ feeds: { fetch: true } });
});
it("a highlight made in a kept note is a card in Orbits' implicit deck", async () => {
const rel = "Reading/Marginal/On Keeping a Commonplace Book.md";
const abs = path.join(root, rel);
writeFileSync(abs, readFileSync(abs, "utf8").replace("the heading is yours", "==the heading is yours=="));
await indexFile(rel);
const implicit = decks("2026-09-23").find((d) => d.implicit);
assert.ok(implicit && implicit.counts.total >= 1);
const cards = deckCards("*", null) ?? [];
assert.ok(cards.some((c) => c.path === rel), "the kept note's highlight is in the implicit deck");
});
it("read and unread round-trip; an unknown item is a 404", async () => {
markRead(`${origin}/feed.json`, "42", true);
assert.ok(!(await feedsState()).items.some((i) => i.guid === "42"));
markRead(`${origin}/feed.json`, "42", false);
assert.ok((await feedsState()).items.some((i) => i.guid === "42"));
assert.throws(() => markRead(`${origin}/feed.json`, "nope", true), /No such item/);
});
it("nothing of the feeds is in the vault but the kept notes", () => {
assert.ok(existsSync(path.join(data, "feeds.db")));
assert.ok(!existsSync(path.join(root, "feeds.db")));
});
});