feat(FN-3564): isolate plugin loader test state to prevent cross-test conta

Refactored plugin-loader tests and implementation to isolate plugin test contamination, improving test independence in `@fusion/core`.

Fusion-Task-Id: FN-3564
This commit is contained in:
Fusion
2026-05-06 02:08:42 -07:00
committed by gsxdsm
parent 79ee217d9f
commit f499f10818
16 changed files with 786 additions and 58 deletions

View File

@@ -34,6 +34,7 @@ describe("PluginRunner", () => {
getPluginTools: ReturnType<typeof vi.fn>;
getPluginRoutes: ReturnType<typeof vi.fn>;
getPluginUiSlots: ReturnType<typeof vi.fn>;
getPluginUiContributions: ReturnType<typeof vi.fn>;
getPluginRuntimes: ReturnType<typeof vi.fn>;
getPluginSkills: ReturnType<typeof vi.fn>;
getPluginWorkflowSteps: ReturnType<typeof vi.fn>;
@@ -94,6 +95,7 @@ describe("PluginRunner", () => {
getPluginTools: vi.fn().mockReturnValue([]),
getPluginRoutes: vi.fn().mockReturnValue([]),
getPluginUiSlots: vi.fn().mockReturnValue([]),
getPluginUiContributions: vi.fn().mockReturnValue([]),
getPluginRuntimes: vi.fn().mockReturnValue([]),
getPluginSkills: vi.fn().mockReturnValue([]),
getPluginWorkflowSteps: vi.fn().mockReturnValue([]),
@@ -534,6 +536,50 @@ describe("PluginRunner", () => {
});
});
describe("getPluginUiContributions()", () => {
it("returns structured contributions from pluginLoader", () => {
const contributions = [
{
pluginId: "plugin-a",
contribution: {
surface: "settings-config-section",
contributionId: "cfg-a",
sectionId: "openai",
title: "OpenAI settings",
pluginSettingKeys: ["openai.apiKey"],
},
},
];
mockPluginLoader.getPluginUiContributions.mockReturnValue(contributions);
const result = pluginRunner.getPluginUiContributions();
expect(result).toEqual(contributions);
expect(mockPluginLoader.getPluginUiContributions).toHaveBeenCalledTimes(1);
});
it("uses cached contributions until invalidated", () => {
const contributions = [
{
pluginId: "plugin-a",
contribution: {
surface: "onboarding-provider-recommendation",
contributionId: "rec-a",
providerId: "openai",
title: "OpenAI",
reason: "default",
},
},
];
mockPluginLoader.getPluginUiContributions.mockReturnValue(contributions);
pluginRunner.getPluginUiContributions();
pluginRunner.getPluginUiContributions();
expect(mockPluginLoader.getPluginUiContributions).toHaveBeenCalledTimes(1);
});
});
describe("getPluginRuntimes()", () => {
it("should return empty array when no plugins have runtimes", async () => {
mockPluginLoader.getPluginRuntimes.mockReturnValue([]);

View File

@@ -15,6 +15,7 @@ import type {
PluginToolDefinition,
PluginRouteDefinition,
PluginUiSlotDefinition,
PluginUiContributionDefinition,
PluginRuntimeRegistration,
PluginContext,
PluginSkillContribution,
@@ -68,6 +69,11 @@ interface CachedUiSlots {
version: number;
}
interface CachedUiContributions {
contributions: Array<{ pluginId: string; contribution: PluginUiContributionDefinition }>;
version: number;
}
/**
* Cached runtimes - rebuilt when plugin state changes
*/
@@ -112,6 +118,7 @@ export class PluginRunner {
private cachedTools: CachedTools | null = null;
private cachedRoutes: CachedRoutes | null = null;
private cachedUiSlots: CachedUiSlots | null = null;
private cachedUiContributions: CachedUiContributions | null = null;
private cachedRuntimes: CachedRuntimes | null = null;
private cachedSkills: CachedSkills | null = null;
private cachedWorkflowSteps: CachedWorkflowSteps | null = null;
@@ -121,6 +128,7 @@ export class PluginRunner {
private toolsCacheVersion = 0;
private routesCacheVersion = 0;
private uiSlotsCacheVersion = 0;
private uiContributionsCacheVersion = 0;
private runtimesCacheVersion = 0;
private skillsCacheVersion = 0;
private workflowStepsCacheVersion = 0;
@@ -196,6 +204,7 @@ export class PluginRunner {
this.invalidateToolsCache();
this.invalidateRoutesCache();
this.invalidateUiSlotsCache();
this.invalidateUiContributionsCache();
this.invalidateRuntimesCache();
this.invalidateSkillsCache();
this.invalidateWorkflowStepsCache();
@@ -284,6 +293,16 @@ export class PluginRunner {
return this.cachedUiSlots.slots;
}
getPluginUiContributions(): Array<{ pluginId: string; contribution: PluginUiContributionDefinition }> {
if (!this.cachedUiContributions || this.cachedUiContributions.version !== this.uiContributionsCacheVersion) {
this.cachedUiContributions = {
contributions: this.options.pluginLoader.getPluginUiContributions(),
version: this.uiContributionsCacheVersion,
};
}
return this.cachedUiContributions.contributions;
}
/**
* Get all runtime registrations from loaded plugins.
* Runtimes are cached and only rebuilt when plugin state changes.
@@ -404,6 +423,7 @@ export class PluginRunner {
this.invalidateToolsCache();
this.invalidateRoutesCache();
this.invalidateUiSlotsCache();
this.invalidateUiContributionsCache();
this.invalidateRuntimesCache();
this.invalidateSkillsCache();
this.invalidateWorkflowStepsCache();
@@ -423,6 +443,7 @@ export class PluginRunner {
this.invalidateToolsCache();
this.invalidateRoutesCache();
this.invalidateUiSlotsCache();
this.invalidateUiContributionsCache();
this.invalidateRuntimesCache();
this.invalidateSkillsCache();
this.invalidateWorkflowStepsCache();
@@ -447,6 +468,7 @@ export class PluginRunner {
this.invalidateToolsCache();
this.invalidateRoutesCache();
this.invalidateUiSlotsCache();
this.invalidateUiContributionsCache();
this.invalidateRuntimesCache();
this.invalidateSkillsCache();
this.invalidateWorkflowStepsCache();
@@ -471,6 +493,7 @@ export class PluginRunner {
this.invalidateToolsCache();
this.invalidateRoutesCache();
this.invalidateUiSlotsCache();
this.invalidateUiContributionsCache();
this.invalidateRuntimesCache();
this.invalidateSkillsCache();
this.invalidateWorkflowStepsCache();
@@ -494,6 +517,7 @@ export class PluginRunner {
this.invalidateToolsCache();
this.invalidateRoutesCache();
this.invalidateUiSlotsCache();
this.invalidateUiContributionsCache();
this.invalidateRuntimesCache();
this.invalidateSkillsCache();
this.invalidateWorkflowStepsCache();
@@ -509,6 +533,7 @@ export class PluginRunner {
this.invalidateToolsCache();
this.invalidateRoutesCache();
this.invalidateUiSlotsCache();
this.invalidateUiContributionsCache();
this.invalidateRuntimesCache();
this.invalidateSkillsCache();
this.invalidateWorkflowStepsCache();
@@ -524,6 +549,7 @@ export class PluginRunner {
this.invalidateToolsCache();
this.invalidateRoutesCache();
this.invalidateUiSlotsCache();
this.invalidateUiContributionsCache();
this.invalidateRuntimesCache();
this.invalidateSkillsCache();
this.invalidateWorkflowStepsCache();
@@ -539,6 +565,7 @@ export class PluginRunner {
this.invalidateToolsCache();
this.invalidateRoutesCache();
this.invalidateUiSlotsCache();
this.invalidateUiContributionsCache();
this.invalidateRuntimesCache();
this.invalidateSkillsCache();
this.invalidateWorkflowStepsCache();
@@ -554,6 +581,7 @@ export class PluginRunner {
this.invalidateToolsCache();
this.invalidateRoutesCache();
this.invalidateUiSlotsCache();
this.invalidateUiContributionsCache();
this.invalidateRuntimesCache();
this.invalidateSkillsCache();
this.invalidateWorkflowStepsCache();
@@ -757,6 +785,11 @@ export class PluginRunner {
this.log.log(`UI slots cache invalidated (version: ${this.uiSlotsCacheVersion})`);
}
private invalidateUiContributionsCache(): void {
this.uiContributionsCacheVersion++;
this.log.log(`UI contributions cache invalidated (version: ${this.uiContributionsCacheVersion})`);
}
/**
* Invalidate the runtimes cache, forcing rebuild on next access.
*/