nook: add /agents, /changelog and /faq
Three organic surfaces, each answering an objection at the moment it bites. /agents lists all twelve agents with each hook's exact event surface (mirrors AgentSpec.all + hook/setup.go + the plugins) and what a card can do — allow/deny or observe. /changelog renders the appcast HTML the Sparkle updater already publishes, so release notes have one source of truth. /faq answers privacy (scoped honestly: no prompt or code content leaves the device), the notch question, battery, permissions, the macOS 14 floor and offline use — no refund question, per decision: the trial is the answer. Nav gains the three links. Tests: appcast parser suite (5 tests) covers CDATA, escaped HTML, Version-prefix titles and garbage input. Issues: FRE-12, FRE-13, FRE-14
This commit is contained in:
64
src/routes/api/the-nook/appcast.test.ts
Normal file
64
src/routes/api/the-nook/appcast.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { parseAppcastItems } from "./_appcast";
|
||||
|
||||
// The /changelog page renders exactly what the Sparkle updater receives, so
|
||||
// the parser's contract is the appcast's contract: newest-first items, a
|
||||
// shortVersionString title with the raw <title> as fallback, CDATA and
|
||||
// plain-encoded descriptions both accepted, garbage in → empty list out.
|
||||
|
||||
const item = (over: Record<string, string>) => `
|
||||
<item>
|
||||
<title>Version ${over.title ?? "1.0.0"}</title>
|
||||
${over.short ? `<sparkle:shortVersionString>${over.short}</sparkle:shortVersionString>` : ""}
|
||||
${over.build ? `<sparkle:version>${over.build}</sparkle:version>` : ""}
|
||||
${over.date ? `<pubDate>${over.date}</pubDate>` : ""}
|
||||
${over.desc ?? `<description><![CDATA[<ul><li>Fixes</li></ul>]]></description>`}
|
||||
<enclosure url="https://example.com/Nook-1.0.0.dmg" type="application/x-apple-diskimage"/>
|
||||
</item>`;
|
||||
|
||||
describe("parseAppcastItems", () => {
|
||||
it("parses newest-first items with version, build, date and CDATA notes", () => {
|
||||
const xml = `<rss><channel>
|
||||
${item({ title: "1.2.0", short: "1.2.0", build: "42", date: "Sat, 03 Oct 2026 12:00:00 +0000" })}
|
||||
${item({ title: "1.1.0", short: "1.1.0", build: "41", date: "Fri, 02 Oct 2026 12:00:00 +0000" })}
|
||||
</channel></rss>`;
|
||||
|
||||
const releases = parseAppcastItems(xml);
|
||||
expect(releases.length).toBe(2);
|
||||
expect(releases[0].version).toBe("1.2.0");
|
||||
expect(releases[0].build).toBe("42");
|
||||
expect(releases[0].date).toContain("03 Oct 2026");
|
||||
expect(releases[0].notesHtml).toBe("<ul><li>Fixes</li></ul>");
|
||||
expect(releases[1].version).toBe("1.1.0");
|
||||
});
|
||||
|
||||
it("falls back to the plain title when shortVersionString is absent", () => {
|
||||
const xml = `<rss><channel>${item({ title: "1.0.0" })}</channel></rss>`;
|
||||
const releases = parseAppcastItems(xml);
|
||||
expect(releases[0].version).toBe("1.0.0");
|
||||
expect(releases[0].build).toBeNull();
|
||||
});
|
||||
|
||||
it("accepts non-CDATA escaped-HTML descriptions", () => {
|
||||
const xml = `<rss><channel>
|
||||
${item({ desc: "<description><p>Hello</p></description>" })}
|
||||
</channel></rss>`;
|
||||
expect(parseAppcastItems(xml)[0].notesHtml).toBe("<p>Hello</p>");
|
||||
});
|
||||
|
||||
it("returns [] for empty, non-XML, or item-less input", () => {
|
||||
expect(parseAppcastItems("")).toEqual([]);
|
||||
expect(parseAppcastItems("<html>not an appcast</html>")).toEqual([]);
|
||||
expect(parseAppcastItems("<rss><channel></channel></rss>")).toEqual([]);
|
||||
});
|
||||
|
||||
it("skips items with no version signal at all", () => {
|
||||
const xml = `<rss><channel>
|
||||
<item><description><![CDATA[<p>orphan</p>]]></description></item>
|
||||
${item({ title: "1.0.0" })}
|
||||
</channel></rss>`;
|
||||
const releases = parseAppcastItems(xml);
|
||||
expect(releases.length).toBe(1);
|
||||
expect(releases[0].version).toBe("1.0.0");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user