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

@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
import { mkdtempSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { PluginLoader } from "../plugin-loader.js";
import * as loggerModule from "../logger.js";
vi.mock("@mariozechner/pi-ai", () => ({
AssistantMessageEventStream: class AssistantMessageEventStream {
@@ -131,8 +132,7 @@ type MockStructuredLogger = {
error: ReturnType<typeof vi.fn>;
};
async function loadPluginLoaderWithMockedLogger() {
vi.resetModules();
function mockStructuredLoggerFactory() {
const loggerMap = new Map<string, MockStructuredLogger>();
const createLoggerMock = vi.fn((prefix: string): MockStructuredLogger => {
const existing = loggerMap.get(prefix);
@@ -147,12 +147,10 @@ async function loadPluginLoaderWithMockedLogger() {
return logger;
});
vi.doMock("../logger.js", () => ({
createLogger: createLoggerMock,
}));
const { PluginLoader: MockedPluginLoader } = await import("../plugin-loader.js");
return { MockedPluginLoader, createLoggerMock, loggerMap };
// Use a spy instead of resetModules/doMock so this suite cannot corrupt
// other modules' live exports (notably plugin-types normalization helpers).
vi.spyOn(loggerModule, "createLogger").mockImplementation(createLoggerMock);
return { createLoggerMock, loggerMap };
}
describe("PluginLoader", () => {
@@ -876,8 +874,12 @@ export default plugin;
// ── structured logging ──────────────────────────────────────────────
describe("structured logging", () => {
afterEach(() => {
vi.doUnmock("./logger.js");
it("keeps plugin-types normalization exports callable after logger mocking", async () => {
mockStructuredLoggerFactory();
const pluginTypes = await import("../plugin-types.js");
expect(typeof pluginTypes.normalizePluginUiContributionDefinition).toBe("function");
expect(typeof pluginTypes.normalizePluginUiContributionSurface).toBe("function");
});
it("logs when skipping a disabled plugin", async () => {
@@ -893,8 +895,8 @@ export default plugin;
});
await pluginStore.disablePlugin("disabled-log-test");
const { MockedPluginLoader, loggerMap } = await loadPluginLoaderWithMockedLogger();
const loader = new MockedPluginLoader({ pluginStore, taskStore: mockTaskStore });
const { loggerMap } = mockStructuredLoggerFactory();
const loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
await expect(loader.loadPlugin("disabled-log-test")).rejects.toThrow("disabled");
expect(loggerMap.get("plugin-loader")?.log).toHaveBeenCalledWith(
@@ -914,8 +916,8 @@ export default plugin;
path: pluginPath,
});
const { MockedPluginLoader, loggerMap } = await loadPluginLoaderWithMockedLogger();
const loader = new MockedPluginLoader({ pluginStore, taskStore: mockTaskStore });
const { loggerMap } = mockStructuredLoggerFactory();
const loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
await loader.loadPlugin("already-loaded-log");
await loader.loadPlugin("already-loaded-log");
@@ -937,8 +939,8 @@ export default plugin;
path: pluginPath,
});
const { MockedPluginLoader, loggerMap } = await loadPluginLoaderWithMockedLogger();
const loader = new MockedPluginLoader({ pluginStore, taskStore: mockTaskStore });
const { loggerMap } = mockStructuredLoggerFactory();
const loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
await loader.loadPlugin("reload-log-test");
await loader.reloadPlugin("reload-log-test");
@@ -961,8 +963,8 @@ export default plugin;
path: pluginPath,
});
const { MockedPluginLoader, loggerMap } = await loadPluginLoaderWithMockedLogger();
const loader = new MockedPluginLoader({ pluginStore, taskStore: mockTaskStore });
const { loggerMap } = mockStructuredLoggerFactory();
const loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
await loader.loadPlugin(pluginId);
await writePluginWithHooks(
@@ -1002,8 +1004,8 @@ export default plugin;
path: pluginPath,
});
const { MockedPluginLoader, loggerMap } = await loadPluginLoaderWithMockedLogger();
const loader = new MockedPluginLoader({ pluginStore, taskStore: mockTaskStore });
const { loggerMap } = mockStructuredLoggerFactory();
const loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
await loader.loadPlugin(pluginId);
await writePluginWithHooks(
@@ -1041,8 +1043,8 @@ export default plugin;
path: pluginPath,
});
const { MockedPluginLoader, loggerMap } = await loadPluginLoaderWithMockedLogger();
const loader = new MockedPluginLoader({ pluginStore, taskStore: mockTaskStore });
const { loggerMap } = mockStructuredLoggerFactory();
const loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
await loader.loadPlugin(pluginId);
await loader.stopPlugin(pluginId);
@@ -1074,8 +1076,8 @@ export default plugin;
path: badPath,
});
const { MockedPluginLoader, loggerMap } = await loadPluginLoaderWithMockedLogger();
const loader = new MockedPluginLoader({ pluginStore, taskStore: mockTaskStore });
const { loggerMap } = mockStructuredLoggerFactory();
const loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
await loader.loadAllPlugins();
@@ -1088,8 +1090,8 @@ export default plugin;
it("logs invokeHook failures", async () => {
await pluginStore.init();
const { MockedPluginLoader, loggerMap } = await loadPluginLoaderWithMockedLogger();
const loader = new MockedPluginLoader({ pluginStore, taskStore: mockTaskStore });
const { loggerMap } = mockStructuredLoggerFactory();
const loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
(loader as any).plugins.set("hook-error-log", {
manifest: makeManifest({ id: "hook-error-log" }),
@@ -1130,8 +1132,8 @@ export default plugin;
path: pluginPath,
});
const { MockedPluginLoader, loggerMap } = await loadPluginLoaderWithMockedLogger();
const loader = new MockedPluginLoader({ pluginStore, taskStore: mockTaskStore });
const { loggerMap } = mockStructuredLoggerFactory();
const loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
await loader.loadPlugin(pluginId);
@@ -1568,6 +1570,56 @@ export default plugin;
describe("getPluginUiContributions", () => {
it("returns normalized structured contributions and sorts deterministically", async () => {
await pluginStore.init();
const loader = new PluginLoader({
pluginStore,
taskStore: mockTaskStore,
});
(loader as any).plugins.set("plugin-b", {
manifest: makeManifest({ id: "plugin-b" }),
state: "started",
hooks: {},
uiContributions: [
{
surface: "onboarding-recommendation-card",
contributionId: "rec-b",
providerId: "openai",
title: "OpenAI",
reason: "default",
order: 10,
},
],
} as FusionPlugin);
(loader as any).plugins.set("plugin-a", {
manifest: makeManifest({ id: "plugin-a" }),
state: "started",
hooks: {},
uiContributions: [
{
surface: "settings-integration-card",
contributionId: "cfg-a",
sectionId: "openai",
title: "OpenAI settings",
pluginSettingKeys: ["openai.apiKey"],
order: 1,
},
],
} as FusionPlugin);
const contributions = loader.getPluginUiContributions();
expect(contributions).toHaveLength(2);
expect(contributions[0]?.pluginId).toBe("plugin-a");
expect(contributions[0]?.contribution.surface).toBe("settings-config-section");
expect(contributions[1]?.contribution.surface).toBe("onboarding-provider-recommendation");
});
});
describe("getPluginDashboardViews", () => {
it("returns empty array when no plugins loaded", async () => {
await pluginStore.init();

View File

@@ -14,8 +14,14 @@ import type {
PluginSetupManifest,
PluginSkillContribution,
PluginWorkflowStepContribution,
PluginUiContributionDefinition,
PluginUiContributionInputDefinition,
} from "../plugin-types.js";
import {
normalizePluginUiContributionDefinition,
normalizePluginUiContributionSurface,
validatePluginManifest,
} from "../plugin-types.js";
import { validatePluginManifest } from "../plugin-types.js";
describe("validatePluginManifest", () => {
// ── Valid Manifests ─────────────────────────────────────────────────
@@ -1065,6 +1071,95 @@ describe("PluginRuntimeRegistration", () => {
});
});
describe("plugin ui contribution normalization", () => {
it("normalizes settings-integration-card to settings-config-section", () => {
const normalized = normalizePluginUiContributionDefinition({
surface: "settings-integration-card",
contributionId: "settings-a",
sectionId: "provider-a",
title: "Provider settings",
pluginSettingKeys: ["provider.apiKey"],
});
expect(normalized.surface).toBe("settings-config-section");
});
it("normalizes onboarding-recommendation-card to onboarding-provider-recommendation", () => {
const normalized = normalizePluginUiContributionDefinition({
surface: "onboarding-recommendation-card",
contributionId: "rec-a",
providerId: "openai",
title: "OpenAI",
reason: "Best default",
});
expect(normalized.surface).toBe("onboarding-provider-recommendation");
});
it("passes through final structured surface names unchanged", () => {
expect(normalizePluginUiContributionSurface("settings-config-section")).toBe("settings-config-section");
expect(normalizePluginUiContributionSurface("onboarding-provider-recommendation")).toBe(
"onboarding-provider-recommendation",
);
});
it("accepts legacy surface names in input definitions for compatibility", () => {
const legacyInput: PluginUiContributionInputDefinition = {
surface: "settings-integration-card",
contributionId: "legacy-settings",
sectionId: "provider-a",
title: "Provider settings",
pluginSettingKeys: ["provider.apiKey"],
};
expect(legacyInput.surface).toBe("settings-integration-card");
});
it("supports all final structured contribution surfaces", () => {
const contributions: PluginUiContributionDefinition[] = [
{
surface: "settings-provider-card",
contributionId: "settings-provider",
providerId: "anthropic",
title: "Anthropic",
providerType: "api_key",
},
{
surface: "settings-config-section",
contributionId: "settings-config",
sectionId: "anthropic",
title: "Anthropic config",
pluginSettingKeys: ["anthropic.apiKey"],
},
{
surface: "onboarding-provider-card",
contributionId: "onboarding-provider",
providerId: "openai",
title: "OpenAI",
providerType: "oauth",
},
{
surface: "onboarding-setup-help",
contributionId: "setup-help",
title: "Need help?",
body: "Run auth login",
bodyFormat: "text",
},
{
surface: "onboarding-provider-recommendation",
contributionId: "provider-recommendation",
providerId: "openai",
title: "Recommended",
reason: "Fast setup",
},
{
surface: "post-onboarding-recommendation",
contributionId: "post-recommendation",
title: "Next step",
description: "Enable budgets",
},
];
expect(contributions).toHaveLength(6);
});
});
describe("plugin contribution types", () => {
it("accepts a minimal PluginSkillContribution shape", () => {
const skill: PluginSkillContribution = {

View File

@@ -143,6 +143,17 @@ export type {
PluginRouteMethod,
PluginUiSurface,
PluginUiSlotDefinition,
PluginUiContributionSurface,
PluginUiContributionWhen,
PluginUiActionDescriptor,
SettingsProviderCardContribution,
SettingsConfigSectionContribution,
OnboardingProviderCardContribution,
OnboardingSetupHelpContribution,
OnboardingProviderRecommendationContribution,
PostOnboardingRecommendationContribution,
PluginUiContributionDefinition,
PluginUiContributionInputDefinition,
PluginDashboardViewDefinition,
PluginRuntimeManifestMetadata,
PluginRuntimeFactory,
@@ -165,7 +176,7 @@ export type {
PluginState,
PluginInstallation,
} from "./plugin-types.js";
export { validatePluginManifest } from "./plugin-types.js";
export { validatePluginManifest, normalizePluginUiContributionSurface, normalizePluginUiContributionDefinition } from "./plugin-types.js";
export { PluginStore } from "./plugin-store.js";
export type { PluginStoreEvents, PluginRegistrationInput, PluginUpdateInput } from "./plugin-store.js";
export { PluginLoader } from "./plugin-loader.js";

View File

@@ -23,6 +23,7 @@ import type {
PluginToolDefinition,
PluginRouteDefinition,
PluginUiSlotDefinition,
PluginUiContributionDefinition,
PluginDashboardViewDefinition,
PluginOnSchemaInit,
PluginRuntimeRegistration,
@@ -34,13 +35,12 @@ import type {
PluginSetupManifest,
PluginSetupHooks,
} from "./plugin-types.js";
import { validatePluginManifest } from "./plugin-types.js";
import { normalizePluginUiContributionDefinition, validatePluginManifest } from "./plugin-types.js";
import { createLogger } from "./logger.js";
import { getCreateAiSessionFactory } from "./ai-engine-loader.js";
// Minimum Fusion version for plugin compatibility checks (can be expanded later)
const MINIMUM_FUSION_VERSION = "0.1.0";
const log = createLogger("plugin-loader");
let moduleImportVersion = 0;
export interface PluginLoaderOptions {
@@ -98,6 +98,7 @@ export class PluginLoader extends EventEmitter<{
/** Cache of dynamically imported modules */
private loadedModules: Map<string, unknown> = new Map();
private readonly log = createLogger("plugin-loader");
constructor(private options: PluginLoaderOptions) {
super();
@@ -112,7 +113,7 @@ export class PluginLoader extends EventEmitter<{
private async createContext(plugin: FusionPlugin): Promise<PluginContext> {
const createAiSession = await getCreateAiSessionFactory();
if (process.env.DEBUG?.includes("plugins")) {
log.log(
this.log.log(
createAiSession
? `[plugin:${plugin.manifest.id}] createAiSession available`
: `[plugin:${plugin.manifest.id}] createAiSession unavailable`,
@@ -128,7 +129,7 @@ export class PluginLoader extends EventEmitter<{
emitEvent: (event: string, data: unknown) => {
this.emit("plugin:error", { pluginId: plugin.manifest.id, error: new Error(`Custom event: ${event}`) });
// Custom events are logged but not surfaced as errors
log.log(`[plugin:${plugin.manifest.id}] Custom event: ${event}`, data);
this.log.log(`[plugin:${plugin.manifest.id}] Custom event: ${event}`, data);
},
};
}
@@ -172,7 +173,7 @@ export class PluginLoader extends EventEmitter<{
// Skip disabled plugins
if (!installation.enabled) {
log.log(`Skipping disabled plugin: ${pluginId}`);
this.log.log(`Skipping disabled plugin: ${pluginId}`);
throw Object.assign(new Error(`Plugin "${pluginId}" is disabled`), {
code: "PLUGIN_DISABLED",
});
@@ -180,7 +181,7 @@ export class PluginLoader extends EventEmitter<{
// Skip already loaded plugins
if (this.plugins.has(pluginId)) {
log.log(`Plugin already loaded: ${pluginId}`);
this.log.log(`Plugin already loaded: ${pluginId}`);
return this.plugins.get(pluginId)!;
}
@@ -207,7 +208,7 @@ export class PluginLoader extends EventEmitter<{
plugin.manifest.fusionVersion,
);
if (!compatible) {
log.warn(
this.log.warn(
`Plugin ${pluginId} requires Fusion ${plugin.manifest.fusionVersion}, minimum is ${MINIMUM_FUSION_VERSION}`,
);
}
@@ -319,7 +320,7 @@ export class PluginLoader extends EventEmitter<{
*/
private invalidateModuleCache(path: string): void {
this.loadedModules.delete(path);
log.log(`Module cache invalidated for: ${path}`);
this.log.log(`Module cache invalidated for: ${path}`);
}
/**
@@ -347,7 +348,7 @@ export class PluginLoader extends EventEmitter<{
const installation = await this.options.pluginStore.getPlugin(pluginId);
const pluginPath = this.resolvePluginPath(installation.path);
log.log(`Reloading plugin: ${pluginId}`);
this.log.log(`Reloading plugin: ${pluginId}`);
// Call onUnload with timeout
try {
@@ -357,7 +358,7 @@ export class PluginLoader extends EventEmitter<{
`onUnload timeout for ${pluginId}`,
);
} catch (err) {
log.warn(`onUnload for ${pluginId} timed out or failed:`, err);
this.log.warn(`onUnload for ${pluginId} timed out or failed:`, err);
// Continue with reload despite onUnload issues
}
@@ -397,13 +398,13 @@ export class PluginLoader extends EventEmitter<{
// State is already "started", no need to update store
// (avoiding started -> started transition which is disallowed)
log.log(`Plugin ${pluginId} reloaded successfully`);
this.log.log(`Plugin ${pluginId} reloaded successfully`);
this.emit("plugin:reloaded", { pluginId, plugin: newPlugin });
return newPlugin;
} catch (err) {
// Rollback: restore old plugin
log.error(`Reload failed for ${pluginId}, rolling back:`, err);
this.log.error(`Reload failed for ${pluginId}, rolling back:`, err);
try {
// Restore old plugin
@@ -420,10 +421,10 @@ export class PluginLoader extends EventEmitter<{
// Update store state back to started
await this.options.pluginStore.updatePluginState(pluginId, "started");
log.warn(`Rollback successful for ${pluginId}`);
this.log.warn(`Rollback successful for ${pluginId}`);
} catch (rollbackErr) {
// Rollback also failed - remove plugin and set error state
log.error(
this.log.error(
`Rollback failed for ${pluginId}, removing plugin:`,
rollbackErr,
);
@@ -556,7 +557,7 @@ export class PluginLoader extends EventEmitter<{
} catch (err) {
if ((err as { code?: string }).code !== "PLUGIN_DISABLED") {
errors++;
log.error(
this.log.error(
`Failed to load plugin ${installation.id}:`,
err,
);
@@ -612,7 +613,7 @@ export class PluginLoader extends EventEmitter<{
async stopPlugin(pluginId: string): Promise<void> {
const plugin = this.plugins.get(pluginId);
if (!plugin) {
log.log(`Plugin not loaded: ${pluginId}`);
this.log.log(`Plugin not loaded: ${pluginId}`);
return;
}
@@ -628,7 +629,7 @@ export class PluginLoader extends EventEmitter<{
`onUnload timeout for ${pluginId}`,
);
} catch (err) {
log.error(`Error in onUnload for ${pluginId}:`, err);
this.log.error(`Error in onUnload for ${pluginId}:`, err);
}
// Update state
@@ -673,7 +674,7 @@ export class PluginLoader extends EventEmitter<{
try {
await this.stopPlugin(plugin.id);
} catch (err) {
log.error(`Error stopping plugin ${plugin.id}:`, err);
this.log.error(`Error stopping plugin ${plugin.id}:`, err);
}
}
}
@@ -695,7 +696,7 @@ export class PluginLoader extends EventEmitter<{
try {
await this.safeCallHook(plugin, hookName, args);
} catch (err) {
log.error(
this.log.error(
`Error in ${hookName} hook for ${pluginId}:`,
err,
);
@@ -803,6 +804,31 @@ export class PluginLoader extends EventEmitter<{
}
/**
* Get all structured UI contributions from loaded plugins.
*/
getPluginUiContributions(): Array<{ pluginId: string; contribution: PluginUiContributionDefinition }> {
const contributions: Array<{ pluginId: string; contribution: PluginUiContributionDefinition }> = [];
for (const [pluginId, plugin] of this.plugins) {
if (plugin.uiContributions) {
for (const contribution of plugin.uiContributions) {
contributions.push({
pluginId,
contribution: normalizePluginUiContributionDefinition(contribution),
});
}
}
}
return contributions.sort((a, b) => {
const orderA = a.contribution.order ?? Number.MAX_SAFE_INTEGER;
const orderB = b.contribution.order ?? Number.MAX_SAFE_INTEGER;
if (orderA !== orderB) return orderA - orderB;
if (a.pluginId !== b.pluginId) return a.pluginId.localeCompare(b.pluginId);
return a.contribution.contributionId.localeCompare(b.contribution.contributionId);
});
}
/**
* Get all top-level dashboard view definitions from loaded plugins.
*/

View File

@@ -252,6 +252,128 @@ export interface PluginUiSlotDefinition {
* Top-level dashboard view definition for plugin-provided navigation destinations.
* This is separate from embedded uiSlots and is rendered via host-managed registry.
*/
export type PluginUiContributionSurface =
| "settings-provider-card"
| "settings-config-section"
| "onboarding-provider-card"
| "onboarding-setup-help"
| "onboarding-provider-recommendation"
| "post-onboarding-recommendation";
export interface PluginUiContributionWhen {
providerIds?: string[];
runtimeIds?: string[];
authState?: "required" | "authenticated" | "unauthenticated";
onboardingState?: "required" | "in-progress" | "complete";
}
export interface PluginUiActionDescriptor {
kind:
| "open-settings-section"
| "open-onboarding"
| "refresh-auth-status"
| "save-api-key"
| "toggle-cli-provider"
| "open-external-url";
target?: string;
label?: string;
}
interface PluginUiContributionBase {
surface: PluginUiContributionSurface;
contributionId: string;
title: string;
description?: string;
order?: number;
when?: PluginUiContributionWhen;
}
interface PluginUiProviderCardBase extends PluginUiContributionBase {
providerId: string;
providerType: "cli" | "oauth" | "api_key" | "custom";
settingsSectionId?: string;
statusSource?: { kind: string; providerId: string; route?: string };
actions?: PluginUiActionDescriptor[];
}
export interface SettingsProviderCardContribution extends PluginUiProviderCardBase {
surface: "settings-provider-card";
}
export interface SettingsConfigSectionContribution extends PluginUiContributionBase {
surface: "settings-config-section";
sectionId: string;
pluginSettingKeys: string[];
layout?: "section" | "card" | "disclosure";
}
export interface OnboardingProviderCardContribution extends PluginUiProviderCardBase {
surface: "onboarding-provider-card";
quickStart?: string;
recommended?: boolean;
}
export interface OnboardingSetupHelpContribution extends PluginUiContributionBase {
surface: "onboarding-setup-help";
providerId?: string;
body: string;
bodyFormat: "text" | "markdown";
actions?: PluginUiActionDescriptor[];
}
export interface OnboardingProviderRecommendationContribution extends PluginUiContributionBase {
surface: "onboarding-provider-recommendation";
providerId: string;
reason: string;
actions?: PluginUiActionDescriptor[];
priority?: number;
}
export interface PostOnboardingRecommendationContribution extends PluginUiContributionBase {
surface: "post-onboarding-recommendation";
description: string;
actions?: PluginUiActionDescriptor[];
priority?: number;
dismissible?: boolean;
}
export type PluginUiContributionDefinition =
| SettingsProviderCardContribution
| SettingsConfigSectionContribution
| OnboardingProviderCardContribution
| OnboardingSetupHelpContribution
| OnboardingProviderRecommendationContribution
| PostOnboardingRecommendationContribution;
type LegacyPluginUiContributionSurface =
| "settings-integration-card"
| "onboarding-recommendation-card";
export type PluginUiContributionInputDefinition = Omit<PluginUiContributionDefinition, "surface"> & {
surface: PluginUiContributionSurface | LegacyPluginUiContributionSurface;
};
export function normalizePluginUiContributionSurface(
surface: PluginUiContributionSurface | LegacyPluginUiContributionSurface,
): PluginUiContributionSurface {
if (surface === "settings-integration-card") {
return "settings-config-section";
}
if (surface === "onboarding-recommendation-card") {
return "onboarding-provider-recommendation";
}
return surface;
}
export function normalizePluginUiContributionDefinition(
contribution: PluginUiContributionInputDefinition,
): PluginUiContributionDefinition {
return {
...contribution,
surface: normalizePluginUiContributionSurface(contribution.surface),
} as PluginUiContributionDefinition;
}
export interface PluginDashboardViewDefinition {
/** Unique view identifier within a plugin namespace. */
viewId: string;
@@ -450,6 +572,7 @@ export interface FusionPlugin {
tools?: PluginToolDefinition[];
routes?: PluginRouteDefinition[];
uiSlots?: PluginUiSlotDefinition[];
uiContributions?: PluginUiContributionInputDefinition[];
/** Plugin-contributed top-level dashboard views. */
dashboardViews?: PluginDashboardViewDefinition[];
/** Agent runtime registration for providing custom runtime implementations */