feat(FN-3154): add plugin schema hooks, nav placement, and progress preserv
This merge introduces three major features: task reset now preserves existing progress with explicit user confirmation (FN-3185), the engine's soft-pause/unpause behavior is restored and documented (FN-3201), and the plugin system gains schema hook aggregation with new navigation placement and icons Fusion-Task-Id: FN-3154
This commit is contained in:
@@ -1344,6 +1344,26 @@ describe("PluginLoader", () => {
|
||||
expect(loader.getPluginDashboardViews()).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns aggregated views from a single plugin", async () => {
|
||||
await pluginStore.init();
|
||||
const loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
|
||||
(loader as any).plugins.set("views-a", {
|
||||
manifest: makeManifest({ id: "views-a" }),
|
||||
state: "started",
|
||||
hooks: {},
|
||||
dashboardViews: [
|
||||
{ viewId: "graph", label: "Graph", componentPath: "./graph.js", placement: "more" },
|
||||
{ viewId: "timeline", label: "Timeline", componentPath: "./timeline.js", placement: "overflow" },
|
||||
],
|
||||
} as FusionPlugin);
|
||||
|
||||
const views = loader.getPluginDashboardViews();
|
||||
expect(views.map((entry) => entry.pluginId + ":" + entry.view.viewId)).toEqual([
|
||||
"views-a:graph",
|
||||
"views-a:timeline",
|
||||
]);
|
||||
});
|
||||
|
||||
it("aggregates dashboard views from multiple plugins and keeps uiSlots separate", async () => {
|
||||
await pluginStore.init();
|
||||
const loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
|
||||
@@ -1371,6 +1391,48 @@ describe("PluginLoader", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPluginSchemaInitHooks", () => {
|
||||
it("returns empty array when no plugins define onSchemaInit", async () => {
|
||||
await pluginStore.init();
|
||||
const loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
|
||||
(loader as any).plugins.set("no-hook", {
|
||||
manifest: makeManifest({ id: "no-hook" }),
|
||||
state: "started",
|
||||
hooks: {},
|
||||
} as FusionPlugin);
|
||||
|
||||
expect(loader.getPluginSchemaInitHooks()).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns hooks only from plugins that define onSchemaInit", async () => {
|
||||
await pluginStore.init();
|
||||
const loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
|
||||
const hookA = async () => {};
|
||||
const hookB = () => {};
|
||||
|
||||
(loader as any).plugins.set("schema-a", {
|
||||
manifest: makeManifest({ id: "schema-a" }),
|
||||
state: "started",
|
||||
hooks: { onSchemaInit: hookA },
|
||||
} as FusionPlugin);
|
||||
(loader as any).plugins.set("schema-b", {
|
||||
manifest: makeManifest({ id: "schema-b" }),
|
||||
state: "started",
|
||||
hooks: { onLoad: async () => {} },
|
||||
} as FusionPlugin);
|
||||
(loader as any).plugins.set("schema-c", {
|
||||
manifest: makeManifest({ id: "schema-c" }),
|
||||
state: "started",
|
||||
hooks: { onSchemaInit: hookB },
|
||||
} as FusionPlugin);
|
||||
|
||||
const hooks = loader.getPluginSchemaInitHooks();
|
||||
expect(hooks.map((entry) => entry.pluginId)).toEqual(["schema-a", "schema-c"]);
|
||||
expect(hooks[0]?.hook).toBe(hookA);
|
||||
expect(hooks[1]?.hook).toBe(hookB);
|
||||
});
|
||||
});
|
||||
|
||||
// ── getPluginRuntimes ─────────────────────────────────────────────
|
||||
|
||||
describe("getPluginRuntimes", () => {
|
||||
|
||||
@@ -779,6 +779,24 @@ describe("PluginUiSlotDefinition", () => {
|
||||
|
||||
// ── FusionPlugin with uiSlots ──────────────────────────────────────────
|
||||
|
||||
describe("PluginDashboardViewDefinition", () => {
|
||||
it("accepts a valid PluginDashboardViewDefinition with optional fields", () => {
|
||||
const view = {
|
||||
viewId: "roadmap-planner",
|
||||
label: "Roadmap Planner",
|
||||
componentPath: "./views/RoadmapPlanner.js",
|
||||
icon: "Map",
|
||||
order: 10,
|
||||
placement: "overflow",
|
||||
description: "Plan milestones and slices",
|
||||
};
|
||||
|
||||
expect(view.viewId).toBe("roadmap-planner");
|
||||
expect(view.placement).toBe("overflow");
|
||||
expect(view.description).toContain("milestones");
|
||||
});
|
||||
});
|
||||
|
||||
describe("FusionPlugin with uiSlots", () => {
|
||||
it("accepts a FusionPlugin with uiSlots array", () => {
|
||||
const plugin = {
|
||||
@@ -818,6 +836,27 @@ describe("FusionPlugin with uiSlots", () => {
|
||||
|
||||
expect((plugin as any).uiSlots).toBeUndefined();
|
||||
});
|
||||
|
||||
it("accepts a FusionPlugin with dashboardViews and onSchemaInit hook", () => {
|
||||
const plugin: FusionPlugin = {
|
||||
manifest: { id: "test-plugin", name: "Test Plugin", version: "1.0.0" },
|
||||
state: "started",
|
||||
hooks: {
|
||||
onSchemaInit: async () => {},
|
||||
},
|
||||
dashboardViews: [
|
||||
{
|
||||
viewId: "dependencies",
|
||||
label: "Dependencies",
|
||||
componentPath: "./views/Dependencies.js",
|
||||
placement: "primary",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(plugin.hooks.onSchemaInit).toBeTypeOf("function");
|
||||
expect(plugin.dashboardViews?.[0]?.viewId).toBe("dependencies");
|
||||
});
|
||||
});
|
||||
|
||||
// ── FusionPlugin with runtime ──────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user