import * as NodeTimersPromises from "node:timers/promises"; import { describe, expect, it } from "vite-plus/test"; import type { PluginDefinition, PluginRuntimeFactory, PluginRuntimeSnapshot } from "./contract.ts"; const contributionLabels = (snapshot: PluginRuntimeSnapshot, slot: string) => snapshot.contributions[slot]?.map((item) => item.label) ?? []; const withOperationTimeout = async <Result>(operation: Promise<Result>): Promise<Result> => { const controller = new AbortController(); const timeout = NodeTimersPromises.setTimeout(100, undefined, { ref: false, signal: controller.signal, }).then(() => { throw new Error("operation timed out"); }); try { return await Promise.race([operation, timeout]); } finally { controller.abort(); }}; const failureCause = (error: unknown): unknown => typeof error === "object" && error !== null && "cause" in error ? (error as { readonly cause: unknown }).cause : error; const failureMessage = (error: unknown): string => { const cause = failureCause(error); return cause instanceof Error ? cause.message : String(cause);}; const provider = (version = "1.0.0"): PluginDefinition => ({ id: "acme.database", version, provides: { "acme.database@1": { name: `database-${version}` } }, activate(context) { context.register("status", { id: "database", label: `database ${version}` }); },}); const consumer = (): PluginDefinition => ({ id: "acme.issues", version: "1.0.0", requires: ["acme.database@1"], activate(context) { const database = context.resolve<{ readonly name: string }>("acme.database@1"); context.register("commands", { id: "create-issue", label: database.name }); },}); export function defineRuntimeContract(name: string, createRuntime: PluginRuntimeFactory) { describe(name, () => { it("activates providers before consumers regardless of manifest order", async () => { const runtime = createRuntime(); const snapshot = await runtime.reconcile([consumer(), provider()]); expect(snapshot.active).toEqual(["acme.database", "acme.issues"]); expect(contributionLabels(snapshot, "commands")).toEqual(["database-1.0.0"]); await runtime.dispose(); }); it("blocks missing dependencies without blocking independent plugins", async () => { const runtime = createRuntime(); const independent: PluginDefinition = { id: "acme.clock", version: "1.0.0", activate(context) { context.register("status", { id: "clock", label: "clock" }); }, }; const snapshot = await runtime.reconcile([consumer(), independent]); expect(snapshot.active).toEqual(["acme.clock"]); expect(snapshot.blocked["acme.issues"]).toContain("acme.database@1"); expect(contributionLabels(snapshot, "status")).toEqual(["clock"]); await runtime.dispose(); }); it("deactivates dependents before providers", async () => { const lifecycle: Array<string> = []; const runtime = createRuntime({ onLifecycle: ({ phase, pluginId }) => lifecycle.push(`${phase}:${pluginId}`), }); await runtime.reconcile([provider(), consumer()]); lifecycle.length = 0; await runtime.reconcile([]); expect(lifecycle).toEqual(["deactivate:acme.issues", "deactivate:acme.database"]); await runtime.dispose(); }); it("runs plugin finalizers in reverse registration order", async () => { const disposed: Array<string> = []; const runtime = createRuntime(); await runtime.reconcile([ { id: "acme.finalizers", version: "1.0.0", activate(context) { context.onDispose(() => { disposed.push("first"); }); context.onDispose(() => { disposed.push("second"); }); }, }, ]); await runtime.reconcile([]); expect(disposed).toEqual(["second", "first"]); await runtime.dispose(); }); it("keeps unchanged plugin scopes alive across reconciliation", async () => { let activations = 0; let disposals = 0; const stable: PluginDefinition = { id: "acme.stable", version: "1.0.0", activate(context) { activations += 1; context.onDispose(() => { disposals += 1; }); }, }; const runtime = createRuntime(); await runtime.reconcile([stable]); await runtime.reconcile([stable]); expect(activations).toBe(1); expect(disposals).toBe(0); await runtime.dispose(); expect(disposals).toBe(1); }); it("treats reordered requirements and providers as the same definition", async () => { let activations = 0; const firstService = {}; const secondService = {}; const activate: PluginDefinition["activate"] = () => { activations += 1; }; const dependencies: ReadonlyArray<PluginDefinition> = [ { id: "acme.first-dependency", version: "1.0.0", provides: { "acme.first@1": true }, activate() {}, }, { id: "acme.second-dependency", version: "1.0.0", provides: { "acme.second@1": true }, activate() {}, }, ]; const runtime = createRuntime(); await runtime.reconcile([ ...dependencies, { id: "acme.stable-order", version: "1.0.0", requires: ["acme.first@1", "acme.second@1"], provides: { "acme.output-one@1": firstService, "acme.output-two@1": secondService }, activate, }, ]); await runtime.reconcile([ ...dependencies, { id: "acme.stable-order", version: "1.0.0", requires: ["acme.second@1", "acme.first@1"], provides: { "acme.output-two@1": secondService, "acme.output-one@1": firstService }, activate, }, ]); expect(activations).toBe(1); await runtime.dispose(); }); it("restarts a plugin when its activation implementation changes", async () => { const runtime = createRuntime(); const first: PluginDefinition = { id: "acme.implementation", version: "1.0.0", activate(context) { context.register("commands", { id: "implementation", label: "first" }); }, }; const second: PluginDefinition = { id: "acme.implementation", version: "1.0.0", activate(context) { context.register("commands", { id: "implementation", label: "second" }); }, }; await runtime.reconcile([first]); const snapshot = await runtime.reconcile([second]); expect(contributionLabels(snapshot, "commands")).toEqual(["second"]); await runtime.dispose(); }); it("restarts a changed provider and its dependents without touching independent plugins", async () => { let independentActivations = 0; const lifecycle: Array<string> = []; const independent: PluginDefinition = { id: "acme.independent", version: "1.0.0", activate() { independentActivations += 1; }, }; const runtime = createRuntime({ onLifecycle: ({ phase, pluginId }) => lifecycle.push(`${phase}:${pluginId}`), }); await runtime.reconcile([consumer(), independent, provider()]); lifecycle.length = 0; const snapshot = await runtime.reconcile([consumer(), independent, provider("2.0.0")]); expect(independentActivations).toBe(1); expect(lifecycle).not.toContain("activate:acme.independent"); expect(lifecycle).not.toContain("deactivate:acme.independent"); expect(lifecycle.filter((event) => event.startsWith("activate:"))).toEqual([ "activate:acme.database", "activate:acme.issues", ]); expect(lifecycle.filter((event) => event.startsWith("deactivate:"))).toEqual([ "deactivate:acme.issues", "deactivate:acme.database", ]); expect(contributionLabels(snapshot, "commands")).toEqual(["database-2.0.0"]); await runtime.dispose(); }); it("returns deeply frozen snapshots", async () => { const runtime = createRuntime(); const snapshot = await runtime.reconcile([provider()]); const status = snapshot.contributions.status; expect(Object.isFrozen(snapshot)).toBe(true); expect(Object.isFrozen(snapshot.active)).toBe(true); expect(Object.isFrozen(snapshot.blocked)).toBe(true); expect(Object.isFrozen(snapshot.contributions)).toBe(true); expect(Object.isFrozen(status)).toBe(true); expect(Object.isFrozen(status?.[0])).toBe(true); await runtime.dispose(); }); it("treats every plugin id and contribution slot as data", async () => { const runtime = createRuntime(); const unusualProvider: PluginDefinition = { id: "toString", version: "1.0.0", provides: { "acme.unusual@1": "available" }, activate(context) { context.register("__proto__", { id: "provider", label: "provider" }); }, }; const unusualConsumer: PluginDefinition = { id: "constructor", version: "1.0.0", requires: ["acme.unusual@1"], activate(context) { context.resolve("acme.unusual@1"); context.register("toString", { id: "consumer", label: "consumer" }); }, }; const unusualBlocked: PluginDefinition = { id: "__proto__", version: "1.0.0", requires: ["acme.missing@1"], activate() { throw new Error("blocked plugin activated"); }, }; const snapshot = await runtime.reconcile([unusualConsumer, unusualProvider, unusualBlocked]); expect(snapshot.active).toEqual(["toString", "constructor"]); expect(Object.hasOwn(snapshot.blocked, "__proto__")).toBe(true); expect(snapshot.blocked.__proto__).toContain("acme.missing@1"); expect(Object.hasOwn(snapshot.contributions, "__proto__")).toBe(true); expect(Object.hasOwn(snapshot.contributions, "toString")).toBe(true); expect(contributionLabels(snapshot, "__proto__")).toEqual(["provider"]); expect(contributionLabels(snapshot, "toString")).toEqual(["consumer"]); await runtime.dispose(); }); it("restarts dependents when a provided value changes", async () => { const lifecycle: Array<string> = []; const runtime = createRuntime({ onLifecycle: ({ phase, pluginId }) => lifecycle.push(`${phase}:${pluginId}`), }); const serviceProvider = (name: string): PluginDefinition => ({ id: "acme.service", version: "1.0.0", provides: { "acme.service@1": { name } }, activate() {}, }); const serviceConsumer: PluginDefinition = { id: "acme.service-consumer", version: "1.0.0", requires: ["acme.service@1"], activate(context) { const service = context.resolve<{ readonly name: string }>("acme.service@1"); context.register("commands", { id: "service", label: service.name }); }, }; await runtime.reconcile([serviceProvider("first"), serviceConsumer]); lifecycle.length = 0; const snapshot = await runtime.reconcile([serviceProvider("second"), serviceConsumer]); expect(contributionLabels(snapshot, "commands")).toEqual(["second"]); expect(lifecycle.filter((event) => event.startsWith("activate:"))).toEqual([ "activate:acme.service", "activate:acme.service-consumer", ]); await runtime.dispose(); }); it("does not report a rolled-back candidate as activated", async () => { const lifecycle: Array<string> = []; const runtime = createRuntime({ onLifecycle: ({ phase, pluginId }) => lifecycle.push(`${phase}:${pluginId}`), }); const staged: PluginDefinition = { id: "acme.staged", version: "1.0.0", activate() {}, }; const broken: PluginDefinition = { id: "acme.broken", version: "1.0.0", activate() { throw new Error("activation failed"); }, }; let stagingFailure: unknown; try { await runtime.reconcile([staged, broken]); } catch (error) { stagingFailure = error; } expect(failureMessage(stagingFailure)).toContain("activation failed"); expect(lifecycle).toEqual([]); await runtime.dispose(); }); it("preserves activation failures when rollback cleanup also fails", async () => { const activationError = new Error("activation failed"); const cleanupError = new Error("rollback cleanup failed"); const cleanupEvents: Array<{ readonly phase: string; readonly error: unknown }> = []; const runtime = createRuntime({ onCleanupError: (event) => { cleanupEvents.push(event); throw new Error("cleanup observer failed"); }, }); const broken: PluginDefinition = { id: "acme.rollback-error", version: "1.0.0", activate(context) { context.onDispose(() => { throw cleanupError; }); throw activationError; }, }; let activationFailure: unknown; try { await runtime.reconcile([broken]); } catch (error) { activationFailure = error; } expect(failureCause(activationFailure)).toBe(activationError); expect(cleanupEvents).toHaveLength(1); expect(cleanupEvents[0]?.phase).toBe("rollback"); expect(failureCause(cleanupEvents[0]?.error)).toBe(cleanupError); await runtime.dispose(); }); it("returns the committed snapshot when retiring an old plugin reports cleanup errors", async () => { const cleanupError = new Error("retirement cleanup failed"); const cleanupEvents: Array<{ readonly phase: string; readonly error: unknown }> = []; const runtime = createRuntime({ onCleanupError: (event) => { cleanupEvents.push(event); throw new Error("cleanup observer failed"); }, }); await runtime.reconcile([ { id: "acme.retirement-error", version: "1.0.0", activate(context) { context.register("commands", { id: "retirement", label: "old" }); context.onDispose(() => { throw cleanupError; }); }, }, ]); const snapshot = await runtime.reconcile([ { id: "acme.retirement-error", version: "2.0.0", activate(context) { context.register("commands", { id: "retirement", label: "new" }); }, }, ]); expect(contributionLabels(snapshot, "commands")).toEqual(["new"]); expect(runtime.snapshot()).toBe(snapshot); expect(cleanupEvents).toHaveLength(1); expect(cleanupEvents[0]?.phase).toBe("retire"); expect(failureCause(cleanupEvents[0]?.error)).toBe(cleanupError); await runtime.dispose(); }); it("does not let lifecycle observers interrupt a committed transition", async () => { let oldDisposed = false; const observerErrors: Array<string> = []; const runtime = createRuntime({ onLifecycle: ({ phase, pluginId }) => { throw new Error(`${phase}:${pluginId}`); }, onLifecycleError: ({ phase, pluginId }) => { observerErrors.push(`${phase}:${pluginId}`); }, }); await runtime.reconcile([ { id: "acme.lifecycle-observer", version: "1.0.0", activate(context) { context.onDispose(() => { oldDisposed = true; }); }, }, ]); const snapshot = await runtime.reconcile([ { id: "acme.lifecycle-observer", version: "2.0.0", activate(context) { context.register("commands", { id: "observer", label: "committed" }); }, }, ]); expect(oldDisposed).toBe(true); expect(contributionLabels(snapshot, "commands")).toEqual(["committed"]); expect(observerErrors).toEqual([ "activate:acme.lifecycle-observer", "activate:acme.lifecycle-observer", "deactivate:acme.lifecycle-observer", ]); await runtime.dispose(); }); it("rejects runtime operations reentered from plugin activation", async () => { const runtime = createRuntime(); const reentrant: PluginDefinition = { id: "acme.reentrant", version: "1.0.0", async activate() { await runtime.reconcile([]); }, }; await expect(withOperationTimeout(runtime.reconcile([reentrant]))).rejects.toThrow( "reentrant", ); }); it("rejects runtime operations reentered from plugin finalizers", async () => {Lines 1–500 of 747. Load more
import * as NodeTimersPromises from "node:timers/promises"; import { describe, expect, it } from "vite-plus/test"; import type { PluginDefinition, PluginRuntimeFactory, PluginRuntimeSnapshot } from "./contract.ts"; const contributionLabels = (snapshot: PluginRuntimeSnapshot, slot: string) => snapshot.contributions[slot]?.map((item) => item.label) ?? []; const withOperationTimeout = async <Result>(operation: Promise<Result>): Promise<Result> => { const controller = new AbortController(); const timeout = NodeTimersPromises.setTimeout(100, undefined, { ref: false, signal: controller.signal, }).then(() => { throw new Error("operation timed out"); }); try { return await Promise.race([operation, timeout]); } finally { controller.abort(); } }; const failureCause = (error: unknown): unknown => typeof error === "object" && error !== null && "cause" in error ? (error as { readonly cause: unknown }).cause : error; const failureMessage = (error: unknown): string => { const cause = failureCause(error); return cause instanceof Error ? cause.message : String(cause); }; const provider = (version = "1.0.0"): PluginDefinition => ({ id: "acme.database", version, provides: { "acme.database@1": { name: `database-${version}` } }, activate(context) { context.register("status", { id: "database", label: `database ${version}` }); }, }); const consumer = (): PluginDefinition => ({ id: "acme.issues", version: "1.0.0", requires: ["acme.database@1"], activate(context) { const database = context.resolve<{ readonly name: string }>("acme.database@1"); context.register("commands", { id: "create-issue", label: database.name }); }, }); export function defineRuntimeContract(name: string, createRuntime: PluginRuntimeFactory) { describe(name, () => { it("activates providers before consumers regardless of manifest order", async () => { const runtime = createRuntime(); const snapshot = await runtime.reconcile([consumer(), provider()]); expect(snapshot.active).toEqual(["acme.database", "acme.issues"]); expect(contributionLabels(snapshot, "commands")).toEqual(["database-1.0.0"]); await runtime.dispose(); }); it("blocks missing dependencies without blocking independent plugins", async () => { const runtime = createRuntime(); const independent: PluginDefinition = { id: "acme.clock", version: "1.0.0", activate(context) { context.register("status", { id: "clock", label: "clock" }); }, }; const snapshot = await runtime.reconcile([consumer(), independent]); expect(snapshot.active).toEqual(["acme.clock"]); expect(snapshot.blocked["acme.issues"]).toContain("acme.database@1"); expect(contributionLabels(snapshot, "status")).toEqual(["clock"]); await runtime.dispose(); }); it("deactivates dependents before providers", async () => { const lifecycle: Array<string> = []; const runtime = createRuntime({ onLifecycle: ({ phase, pluginId }) => lifecycle.push(`${phase}:${pluginId}`), }); await runtime.reconcile([provider(), consumer()]); lifecycle.length = 0; await runtime.reconcile([]); expect(lifecycle).toEqual(["deactivate:acme.issues", "deactivate:acme.database"]); await runtime.dispose(); }); it("runs plugin finalizers in reverse registration order", async () => { const disposed: Array<string> = []; const runtime = createRuntime(); await runtime.reconcile([ { id: "acme.finalizers", version: "1.0.0", activate(context) { context.onDispose(() => { disposed.push("first"); }); context.onDispose(() => { disposed.push("second"); }); }, }, ]); await runtime.reconcile([]); expect(disposed).toEqual(["second", "first"]); await runtime.dispose(); }); it("keeps unchanged plugin scopes alive across reconciliation", async () => { let activations = 0; let disposals = 0; const stable: PluginDefinition = { id: "acme.stable", version: "1.0.0", activate(context) { activations += 1; context.onDispose(() => { disposals += 1; }); }, }; const runtime = createRuntime(); await runtime.reconcile([stable]); await runtime.reconcile([stable]); expect(activations).toBe(1); expect(disposals).toBe(0); await runtime.dispose(); expect(disposals).toBe(1); }); it("treats reordered requirements and providers as the same definition", async () => { let activations = 0; const firstService = {}; const secondService = {}; const activate: PluginDefinition["activate"] = () => { activations += 1; }; const dependencies: ReadonlyArray<PluginDefinition> = [ { id: "acme.first-dependency", version: "1.0.0", provides: { "acme.first@1": true }, activate() {}, }, { id: "acme.second-dependency", version: "1.0.0", provides: { "acme.second@1": true }, activate() {}, }, ]; const runtime = createRuntime(); await runtime.reconcile([ ...dependencies, { id: "acme.stable-order", version: "1.0.0", requires: ["acme.first@1", "acme.second@1"], provides: { "acme.output-one@1": firstService, "acme.output-two@1": secondService }, activate, }, ]); await runtime.reconcile([ ...dependencies, { id: "acme.stable-order", version: "1.0.0", requires: ["acme.second@1", "acme.first@1"], provides: { "acme.output-two@1": secondService, "acme.output-one@1": firstService }, activate, }, ]); expect(activations).toBe(1); await runtime.dispose(); }); it("restarts a plugin when its activation implementation changes", async () => { const runtime = createRuntime(); const first: PluginDefinition = { id: "acme.implementation", version: "1.0.0", activate(context) { context.register("commands", { id: "implementation", label: "first" }); }, }; const second: PluginDefinition = { id: "acme.implementation", version: "1.0.0", activate(context) { context.register("commands", { id: "implementation", label: "second" }); }, }; await runtime.reconcile([first]); const snapshot = await runtime.reconcile([second]); expect(contributionLabels(snapshot, "commands")).toEqual(["second"]); await runtime.dispose(); }); it("restarts a changed provider and its dependents without touching independent plugins", async () => { let independentActivations = 0; const lifecycle: Array<string> = []; const independent: PluginDefinition = { id: "acme.independent", version: "1.0.0", activate() { independentActivations += 1; }, }; const runtime = createRuntime({ onLifecycle: ({ phase, pluginId }) => lifecycle.push(`${phase}:${pluginId}`), }); await runtime.reconcile([consumer(), independent, provider()]); lifecycle.length = 0; const snapshot = await runtime.reconcile([consumer(), independent, provider("2.0.0")]); expect(independentActivations).toBe(1); expect(lifecycle).not.toContain("activate:acme.independent"); expect(lifecycle).not.toContain("deactivate:acme.independent"); expect(lifecycle.filter((event) => event.startsWith("activate:"))).toEqual([ "activate:acme.database", "activate:acme.issues", ]); expect(lifecycle.filter((event) => event.startsWith("deactivate:"))).toEqual([ "deactivate:acme.issues", "deactivate:acme.database", ]); expect(contributionLabels(snapshot, "commands")).toEqual(["database-2.0.0"]); await runtime.dispose(); }); it("returns deeply frozen snapshots", async () => { const runtime = createRuntime(); const snapshot = await runtime.reconcile([provider()]); const status = snapshot.contributions.status; expect(Object.isFrozen(snapshot)).toBe(true); expect(Object.isFrozen(snapshot.active)).toBe(true); expect(Object.isFrozen(snapshot.blocked)).toBe(true); expect(Object.isFrozen(snapshot.contributions)).toBe(true); expect(Object.isFrozen(status)).toBe(true); expect(Object.isFrozen(status?.[0])).toBe(true); await runtime.dispose(); }); it("treats every plugin id and contribution slot as data", async () => { const runtime = createRuntime(); const unusualProvider: PluginDefinition = { id: "toString", version: "1.0.0", provides: { "acme.unusual@1": "available" }, activate(context) { context.register("__proto__", { id: "provider", label: "provider" }); }, }; const unusualConsumer: PluginDefinition = { id: "constructor", version: "1.0.0", requires: ["acme.unusual@1"], activate(context) { context.resolve("acme.unusual@1"); context.register("toString", { id: "consumer", label: "consumer" }); }, }; const unusualBlocked: PluginDefinition = { id: "__proto__", version: "1.0.0", requires: ["acme.missing@1"], activate() { throw new Error("blocked plugin activated"); }, }; const snapshot = await runtime.reconcile([unusualConsumer, unusualProvider, unusualBlocked]); expect(snapshot.active).toEqual(["toString", "constructor"]); expect(Object.hasOwn(snapshot.blocked, "__proto__")).toBe(true); expect(snapshot.blocked.__proto__).toContain("acme.missing@1"); expect(Object.hasOwn(snapshot.contributions, "__proto__")).toBe(true); expect(Object.hasOwn(snapshot.contributions, "toString")).toBe(true); expect(contributionLabels(snapshot, "__proto__")).toEqual(["provider"]); expect(contributionLabels(snapshot, "toString")).toEqual(["consumer"]); await runtime.dispose(); }); it("restarts dependents when a provided value changes", async () => { const lifecycle: Array<string> = []; const runtime = createRuntime({ onLifecycle: ({ phase, pluginId }) => lifecycle.push(`${phase}:${pluginId}`), }); const serviceProvider = (name: string): PluginDefinition => ({ id: "acme.service", version: "1.0.0", provides: { "acme.service@1": { name } }, activate() {}, }); const serviceConsumer: PluginDefinition = { id: "acme.service-consumer", version: "1.0.0", requires: ["acme.service@1"], activate(context) { const service = context.resolve<{ readonly name: string }>("acme.service@1"); context.register("commands", { id: "service", label: service.name }); }, }; await runtime.reconcile([serviceProvider("first"), serviceConsumer]); lifecycle.length = 0; const snapshot = await runtime.reconcile([serviceProvider("second"), serviceConsumer]); expect(contributionLabels(snapshot, "commands")).toEqual(["second"]); expect(lifecycle.filter((event) => event.startsWith("activate:"))).toEqual([ "activate:acme.service", "activate:acme.service-consumer", ]); await runtime.dispose(); }); it("does not report a rolled-back candidate as activated", async () => { const lifecycle: Array<string> = []; const runtime = createRuntime({ onLifecycle: ({ phase, pluginId }) => lifecycle.push(`${phase}:${pluginId}`), }); const staged: PluginDefinition = { id: "acme.staged", version: "1.0.0", activate() {}, }; const broken: PluginDefinition = { id: "acme.broken", version: "1.0.0", activate() { throw new Error("activation failed"); }, }; let stagingFailure: unknown; try { await runtime.reconcile([staged, broken]); } catch (error) { stagingFailure = error; } expect(failureMessage(stagingFailure)).toContain("activation failed"); expect(lifecycle).toEqual([]); await runtime.dispose(); }); it("preserves activation failures when rollback cleanup also fails", async () => { const activationError = new Error("activation failed"); const cleanupError = new Error("rollback cleanup failed"); const cleanupEvents: Array<{ readonly phase: string; readonly error: unknown }> = []; const runtime = createRuntime({ onCleanupError: (event) => { cleanupEvents.push(event); throw new Error("cleanup observer failed"); }, }); const broken: PluginDefinition = { id: "acme.rollback-error", version: "1.0.0", activate(context) { context.onDispose(() => { throw cleanupError; }); throw activationError; }, }; let activationFailure: unknown; try { await runtime.reconcile([broken]); } catch (error) { activationFailure = error; } expect(failureCause(activationFailure)).toBe(activationError); expect(cleanupEvents).toHaveLength(1); expect(cleanupEvents[0]?.phase).toBe("rollback"); expect(failureCause(cleanupEvents[0]?.error)).toBe(cleanupError); await runtime.dispose(); }); it("returns the committed snapshot when retiring an old plugin reports cleanup errors", async () => { const cleanupError = new Error("retirement cleanup failed"); const cleanupEvents: Array<{ readonly phase: string; readonly error: unknown }> = []; const runtime = createRuntime({ onCleanupError: (event) => { cleanupEvents.push(event); throw new Error("cleanup observer failed"); }, }); await runtime.reconcile([ { id: "acme.retirement-error", version: "1.0.0", activate(context) { context.register("commands", { id: "retirement", label: "old" }); context.onDispose(() => { throw cleanupError; }); }, }, ]); const snapshot = await runtime.reconcile([ { id: "acme.retirement-error", version: "2.0.0", activate(context) { context.register("commands", { id: "retirement", label: "new" }); }, }, ]); expect(contributionLabels(snapshot, "commands")).toEqual(["new"]); expect(runtime.snapshot()).toBe(snapshot); expect(cleanupEvents).toHaveLength(1); expect(cleanupEvents[0]?.phase).toBe("retire"); expect(failureCause(cleanupEvents[0]?.error)).toBe(cleanupError); await runtime.dispose(); }); it("does not let lifecycle observers interrupt a committed transition", async () => { let oldDisposed = false; const observerErrors: Array<string> = []; const runtime = createRuntime({ onLifecycle: ({ phase, pluginId }) => { throw new Error(`${phase}:${pluginId}`); }, onLifecycleError: ({ phase, pluginId }) => { observerErrors.push(`${phase}:${pluginId}`); }, }); await runtime.reconcile([ { id: "acme.lifecycle-observer", version: "1.0.0", activate(context) { context.onDispose(() => { oldDisposed = true; }); }, }, ]); const snapshot = await runtime.reconcile([ { id: "acme.lifecycle-observer", version: "2.0.0", activate(context) { context.register("commands", { id: "observer", label: "committed" }); }, }, ]); expect(oldDisposed).toBe(true); expect(contributionLabels(snapshot, "commands")).toEqual(["committed"]); expect(observerErrors).toEqual([ "activate:acme.lifecycle-observer", "activate:acme.lifecycle-observer", "deactivate:acme.lifecycle-observer", ]); await runtime.dispose(); }); it("rejects runtime operations reentered from plugin activation", async () => { const runtime = createRuntime(); const reentrant: PluginDefinition = { id: "acme.reentrant", version: "1.0.0", async activate() { await runtime.reconcile([]); }, }; await expect(withOperationTimeout(runtime.reconcile([reentrant]))).rejects.toThrow( "reentrant", ); }); it("rejects runtime operations reentered from plugin finalizers", async () => {