Wenchao An e2f19d8335
feat(plugins): full-stack plugin APIs and bookmarks (#5647)
* feat(plugins): add full-stack contributions and bookmarks example

* ci(plugins): provision bookmark gateway for browser tests

* fix(plugins): authenticate module downloads through configured backend

* fix(plugins): isolate contributions and localize extension UI

* fix(plugins): preserve bookmark agent routing and contain async callbacks

* fix(plugins): pin durable batch workers to app extension snapshots
2026-09-22 11:18:57 +08:00

103 lines
3.0 KiB
TypeScript

import { afterEach, describe, expect, rs, test } from "@rstest/core";
import type {
FrontendExtension,
PluginSurface,
} from "@/core/extensions/contracts";
import {
pluginPages,
pluginPageTitle,
pluginPagePath,
} from "@/core/extensions/pages";
import {
loadFrontendExtensions,
type LoadedContribution,
} from "@/core/extensions/registry";
rs.mock("@/core/config", () => ({ getBackendBaseURL: () => "" }));
rs.mock("@/core/api/fetcher", () => ({
fetch: async () => new Response("export default {}"),
}));
afterEach(() => {
rs.restoreAllMocks();
});
const library: PluginSurface = {
id: "library",
title: "Library",
slot: "page",
navigation: { label: "My bookmarks", labelZh: "我的书签" },
mount: () => ({ dispose: () => undefined }),
};
const entry: LoadedContribution = {
namespace: "community.bookmarks",
module: "bookmarks.v1",
entry: `/api/plugins/modules/bookmarks.v1/${"a".repeat(64)}.mjs`,
title: "Bookmarks",
description: "",
settings: { enabled: true },
extension: { apiVersion: 1, module: "bookmarks.v1", surfaces: [library] },
};
describe("plugin-owned pages", () => {
test("only loaded enabled page surfaces become namespaced routes", () => {
const entries = [
entry,
{ ...entry, namespace: "disabled", settings: { enabled: false } },
{ ...entry, namespace: "unloaded", extension: undefined },
{
...entry,
namespace: "another.plugin",
extension: {
...entry.extension!,
surfaces: [library],
},
},
];
const pages = pluginPages(entries);
expect(pages.map(({ href }) => href)).toEqual([
"/workspace/extensions/community.bookmarks/library",
"/workspace/extensions/another.plugin/library",
]);
expect(pluginPageTitle(library, "zh-CN")).toBe("我的书签");
expect(pluginPageTitle(library, "en-US")).toBe("My bookmarks");
expect(pluginPagePath("a/b?c", "page#one")).toBe(
"/workspace/extensions/a%2Fb%3Fc/page%23one",
);
});
test("page URLs do not require a sidebar entry", () => {
const pages = pluginPages([
{
...entry,
extension: {
...entry.extension!,
surfaces: [{ ...library, navigation: undefined }],
},
},
]);
expect(pages).toHaveLength(1);
expect(pluginPageTitle(pages[0]!.surface, "zh-CN")).toBe("Library");
});
test("invalid navigation is contained to its module", async () => {
for (const surface of [
{ ...library, slot: "composer" },
{ ...library, navigation: { label: "" } },
{ ...library, navigation: { label: "ok", labelZh: 42 } },
{ ...library, id: "../chats" },
]) {
const importer = rs.fn(async () => ({
default: {
...entry.extension,
surfaces: [surface],
} as FrontendExtension,
}));
const result = await loadFrontendExtensions([entry], importer);
expect(importer).toHaveBeenCalledTimes(1);
expect(result[0]?.error).toBeTruthy();
expect(pluginPages(result)).toEqual([]);
}
});
});