mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-25 22:16:19 +00:00
* 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
116 lines
3.1 KiB
TypeScript
116 lines
3.1 KiB
TypeScript
import { expect, rs, test } from "@rstest/core";
|
|
|
|
import { mountSurface } from "@/core/extensions/surfaces";
|
|
|
|
test("surface cleanup aborts outstanding work and rejects late backend calls", async () => {
|
|
const container = document.createElement("div");
|
|
const dispose = rs.fn();
|
|
const backend = rs.fn(async () => ({}));
|
|
let callLater: (
|
|
name: string,
|
|
payload: Record<string, unknown>,
|
|
) => Promise<unknown> = rs.fn();
|
|
let signal: AbortSignal | undefined;
|
|
const cleanup = mountSurface(
|
|
container,
|
|
{
|
|
id: "picker",
|
|
slot: "page",
|
|
title: "Picker",
|
|
mount(root, context) {
|
|
signal = context.signal;
|
|
callLater = context.callBackend;
|
|
root.textContent = "PLUGIN UI";
|
|
return { dispose };
|
|
},
|
|
},
|
|
{
|
|
namespace: "community.example",
|
|
locale: "en",
|
|
settings: {},
|
|
threadId: "a",
|
|
callBackend: backend,
|
|
},
|
|
rs.fn(),
|
|
);
|
|
expect(container.shadowRoot?.textContent).toBe("PLUGIN UI");
|
|
await callLater("search", {});
|
|
cleanup();
|
|
cleanup();
|
|
await expect(callLater("search", {})).rejects.toThrow();
|
|
expect(backend).toHaveBeenCalledTimes(1);
|
|
expect(signal?.aborted).toBe(true);
|
|
expect(dispose).toHaveBeenCalledTimes(1);
|
|
expect(container.shadowRoot?.textContent).toBe("");
|
|
});
|
|
|
|
test("a failed mount is isolated and cannot leave its partial UI behind", () => {
|
|
const error = rs.fn();
|
|
const container = document.createElement("div");
|
|
mountSurface(
|
|
container,
|
|
{
|
|
id: "bad",
|
|
slot: "page",
|
|
title: "Broken",
|
|
mount(root) {
|
|
root.textContent = "partial";
|
|
throw new Error("broken");
|
|
},
|
|
},
|
|
{
|
|
namespace: "community.example",
|
|
locale: "en",
|
|
settings: {},
|
|
callBackend: rs.fn(),
|
|
},
|
|
error,
|
|
);
|
|
expect(error).toHaveBeenCalledTimes(1);
|
|
expect(container.shadowRoot?.textContent).toBe("");
|
|
});
|
|
|
|
for (const failMount of [false, true]) {
|
|
test(`surface ${failMount ? "mount failure" : "cleanup"} aborts navigation and rejects stale callbacks`, async () => {
|
|
let openLater!: (id: string) => Promise<void>;
|
|
let pending!: Promise<void>;
|
|
let finish!: () => void;
|
|
const navigate = rs.fn();
|
|
const open = rs.fn(async (_id: string, signal: AbortSignal) => {
|
|
await new Promise<void>((resolve) => {
|
|
finish = resolve;
|
|
});
|
|
signal.throwIfAborted();
|
|
navigate();
|
|
});
|
|
const cleanup = mountSurface(
|
|
document.createElement("div"),
|
|
{
|
|
id: "library",
|
|
slot: "page",
|
|
title: "Library",
|
|
mount(_root, context) {
|
|
openLater = context.openConversation!;
|
|
pending = openLater("thread");
|
|
if (failMount) throw new Error("mount failed");
|
|
return { dispose: rs.fn() };
|
|
},
|
|
},
|
|
{
|
|
namespace: "bookmarks",
|
|
locale: "en",
|
|
settings: {},
|
|
callBackend: rs.fn(),
|
|
openConversation: open,
|
|
},
|
|
rs.fn(),
|
|
);
|
|
if (!failMount) cleanup();
|
|
finish();
|
|
await expect(pending).rejects.toThrow();
|
|
await expect(openLater("thread")).rejects.toThrow();
|
|
expect(open).toHaveBeenCalledTimes(1);
|
|
expect(navigate).not.toHaveBeenCalled();
|
|
});
|
|
}
|