feat: changelog.md release notes, v tags, plugin release notes and videos

This commit is contained in:
LukeGus committed 2026-10-06 18:13:39 -05:00
1 parent b9dd3488a5
commit 9bc2be721b
45 files changed
+1462 -816

No files matched your search

+37
View File
@@ -680,3 +680,40 @@ describe("installing from a file", () => {
).rejects.toMatchObject({ code: "UPLOAD_EXPIRED" });
});
});
describe("release notes", () => {
it("reads an installed plugin's CHANGELOG.md", async () => {
createFixturePlugin({ id: "docker", root: bundled });
fs.writeFileSync(
path.join(bundled, "docker", "CHANGELOG.md"),
"# Changelog\n\n## 1.1.0 - 2026-10-06\n\n### Fixed\n- A bug\n\n## 1.0.0\n\n### Added\n- First\n",
);
await boot();
expect(await manage.getPluginChangelog("docker")).toEqual([
{
version: "1.1.0",
date: "2026-10-06",
changes: [{ type: "fixed", text: "A bug" }],
},
{ version: "1.0.0", changes: [{ type: "added", text: "First" }] },
]);
});
it("is empty without a changelog and 404s for an unknown plugin", async () => {
createFixturePlugin({ id: "docker", root: bundled });
await boot();
expect(await manage.getPluginChangelog("docker")).toEqual([]);
await expect(manage.getPluginChangelog("nope")).rejects.toMatchObject({
status: 404,
});
});
it("parses registry notes and drops empty ones", () => {
expect(manage.readReleaseNotes("Summary.\n\n### Added\n- Thing")).toEqual({
summary: "Summary.",
changes: [{ type: "added", text: "Thing" }],
});
expect(manage.readReleaseNotes("")).toBeUndefined();
expect(manage.readReleaseNotes(undefined)).toBeUndefined();
});
});
@@ -12,6 +12,7 @@ import {
validateManifest,
SUPPORTED_PLUGIN_API_VERSION,
isTermixCompatible,
youtubeVideoId,
} from "../../plugins/manifest.js";
function base(overrides: Record<string, unknown> = {}) {
@@ -852,3 +853,47 @@ describe("contributes.protocols", () => {
expect(errors.join("\n")).toMatch(/extra/);
});
});
describe("manifest video", () => {
it("accepts YouTube links", () => {
for (const video of [
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"https://youtu.be/dQw4w9WgXcQ",
"https://www.youtube.com/embed/dQw4w9WgXcQ",
"https://youtube.com/shorts/dQw4w9WgXcQ",
]) {
expect(validateManifest(base({ video }))).toEqual([]);
}
});
it("rejects anything that is not a YouTube video", () => {
for (const video of [
"https://evil.example/watch?v=dQw4w9WgXcQ",
"http://www.youtube.com/watch?v=dQw4w9WgXcQ",
"https://www.youtube.com.evil.example/watch?v=dQw4w9WgXcQ",
"https://user@www.youtube.com/watch?v=dQw4w9WgXcQ",
"https://www.youtube.com/watch?v=short",
"https://www.youtube.com/channel/UC123",
"javascript:alert(1)",
"",
42,
]) {
expect(validateManifest(base({ video }))).toEqual([
expect.stringContaining('Field "video" must be a YouTube link'),
]);
}
});
});
describe("youtubeVideoId", () => {
it("returns only the id", () => {
expect(
youtubeVideoId("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=42"),
).toBe("dQw4w9WgXcQ");
expect(youtubeVideoId("https://youtu.be/dQw4w9WgXcQ?si=abc")).toBe(
"dQw4w9WgXcQ",
);
expect(youtubeVideoId("https://youtu.be/../../x")).toBeNull();
expect(youtubeVideoId(undefined)).toBeNull();
});
});
@@ -121,6 +121,28 @@ describe("parseRegistryIndex", () => {
expect(index.plugins.map((p) => p.id)).toEqual(["ok"]);
});
it("keeps release notes and only a YouTube video id", () => {
const NOTES = "### Added\n- Thing";
const index = parseRegistryIndex({
plugins: [
{
id: "docker",
video: "https://youtu.be/dQw4w9WgXcQ",
versions: [version({ notes: NOTES })],
},
{
id: "other",
video: "https://evil.example/watch?v=dQw4w9WgXcQ",
versions: [version({ notes: "x".repeat(30_000) })],
},
],
});
expect(index.plugins[0].videoId).toBe("dQw4w9WgXcQ");
expect(index.plugins[0].versions[0].notes).toBe(NOTES);
expect(index.plugins[1].videoId).toBeUndefined();
expect(index.plugins[1].versions[0].notes).toHaveLength(20_000);
});
it("refuses something that is not an index", () => {
expect(() => parseRegistryIndex({ nope: true })).toThrow(/no plugins/);
});