Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
71 lines
2.7 KiB
TypeScript
71 lines
2.7 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import { createEventBus } from "../../../src/core/event-bus.ts";
|
|
import { createExtensionRuntime, loadExtensionFromFactory } from "../../../src/core/extensions/loader.ts";
|
|
import type { ExtensionAPI, ExtensionFactory, ResourceLoader } from "../../../src/index.ts";
|
|
import { createHarness, type Harness } from "../harness.ts";
|
|
|
|
describe("issue #7193 extension event-bus lifecycle", () => {
|
|
const harnesses: Harness[] = [];
|
|
|
|
afterEach(() => {
|
|
while (harnesses.length > 0) {
|
|
harnesses.pop()?.cleanup();
|
|
}
|
|
});
|
|
|
|
it("removes extension-owned event-bus listeners on reload and dispose", async () => {
|
|
const eventBus = createEventBus();
|
|
let extensionCalls = 0;
|
|
let hostCalls = 0;
|
|
let firstApi: ExtensionAPI | undefined;
|
|
const factory: ExtensionFactory = (pi) => {
|
|
firstApi ??= pi;
|
|
pi.events.on("reload:test", () => extensionCalls++);
|
|
};
|
|
eventBus.on("reload:test", () => hostCalls++);
|
|
|
|
const loadExtensions = async () => {
|
|
const runtime = createExtensionRuntime();
|
|
const extension = await loadExtensionFromFactory(factory, process.cwd(), eventBus, runtime);
|
|
return { extensions: [extension], errors: [], runtime };
|
|
};
|
|
let extensionsResult = await loadExtensions();
|
|
const resourceLoader: ResourceLoader = {
|
|
getExtensions: () => extensionsResult,
|
|
getSkills: () => ({ skills: [], diagnostics: [] }),
|
|
getPrompts: () => ({ prompts: [], diagnostics: [] }),
|
|
getThemes: () => ({ themes: [], diagnostics: [] }),
|
|
getAgentsFiles: () => ({ agentsFiles: [] }),
|
|
getSystemPrompt: () => undefined,
|
|
getSystemPromptSource: () => undefined,
|
|
getAppendSystemPrompt: () => [],
|
|
getAppendSystemPromptSources: () => [],
|
|
extendResources: () => {},
|
|
reload: async () => {
|
|
extensionsResult = await loadExtensions();
|
|
},
|
|
};
|
|
const harness = await createHarness({ resourceLoader });
|
|
harnesses.push(harness);
|
|
await harness.session.bindExtensions({ shutdownHandler: () => {} });
|
|
|
|
const emit = async () => {
|
|
const extensionBefore = extensionCalls;
|
|
const hostBefore = hostCalls;
|
|
eventBus.emit("reload:test", undefined);
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
return { extension: extensionCalls - extensionBefore, host: hostCalls - hostBefore };
|
|
};
|
|
|
|
expect(await emit()).toEqual({ extension: 1, host: 1 });
|
|
await harness.session.reload();
|
|
expect(() => firstApi?.getCommands()).toThrow("stale after session replacement or reload");
|
|
expect(await emit()).toEqual({ extension: 1, host: 1 });
|
|
await harness.session.reload();
|
|
expect(await emit()).toEqual({ extension: 1, host: 1 });
|
|
|
|
harness.session.dispose();
|
|
expect(await emit()).toEqual({ extension: 0, host: 1 });
|
|
});
|
|
});
|