import { it } from "@effect/vitest";import { describe, expect } from "vite-plus/test";import * as Cause from "effect/Cause";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Fiber from "effect/Fiber";import * as Scope from "effect/Scope"; import type { PluginRuntime, PluginRuntimeReconcileError } from "../src/runtime.ts"; import type { PluginDefinition, PluginRuntimeOptions, PluginRuntimeSnapshot,} from "../src/contract.ts"; type TestPluginRuntimeFactory = ( options?: PluginRuntimeOptions,) => Effect.Effect<PluginRuntime["Service"], never, Scope.Scope>; const makeEffectCallback = <A, E, R = never>() => { let unsafeResume: (effect: Effect.Effect<A, E, R>) => void = () => { throw new Error("effect callback has not started"); }; return { await: Effect.callback<A, E, R>((resume) => { unsafeResume = resume; }), resume: (effect: Effect.Effect<A, E, R>) => unsafeResume(effect), } as const;}; const failureOf = <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.exit(effect).pipe( Effect.map((exit) => (Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined)), ); const settlePromise = ( exit: Exit.Exit<unknown, unknown>, resolve: () => void, reject: (error: unknown) => void,) => { if (Exit.isFailure(exit)) reject(Cause.squash(exit.cause)); else resolve();}; const contributionLabels = (snapshot: PluginRuntimeSnapshot, slot: string) => snapshot.contributions[slot]?.map((item) => item.label) ?? []; 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: TestPluginRuntimeFactory) { describe(name, () => { it.effect("activates providers before consumers regardless of manifest order", () => Effect.scoped( Effect.gen(function* () { const runtime = yield* createRuntime(); const snapshot = yield* runtime.reconcile([consumer(), provider()]); expect(snapshot.active).toEqual(["acme.database", "acme.issues"]); expect(contributionLabels(snapshot, "commands")).toEqual(["database-1.0.0"]); yield* runtime.dispose; }), ), ); it.effect("blocks missing dependencies without blocking independent plugins", () => Effect.scoped( Effect.gen(function* () { const runtime = yield* createRuntime(); const independent: PluginDefinition = { id: "acme.clock", version: "1.0.0", activate(context) { context.register("status", { id: "clock", label: "clock" }); }, }; const snapshot = yield* 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"]); yield* runtime.dispose; }), ), ); it.effect("deactivates dependents before providers", () => Effect.scoped( Effect.gen(function* () { const lifecycle: Array<string> = []; const runtime = yield* createRuntime({ onLifecycle: ({ phase, pluginId }) => lifecycle.push(`${phase}:${pluginId}`), }); yield* runtime.reconcile([provider(), consumer()]); lifecycle.length = 0; yield* runtime.reconcile([]); expect(lifecycle).toEqual(["deactivate:acme.issues", "deactivate:acme.database"]); yield* runtime.dispose; }), ), ); it.effect("runs plugin finalizers in reverse registration order", () => Effect.scoped( Effect.gen(function* () { const disposed: Array<string> = []; const runtime = yield* createRuntime(); yield* runtime.reconcile([ { id: "acme.finalizers", version: "1.0.0", activate(context) { context.onDispose(() => { disposed.push("first"); }); context.onDispose(() => { disposed.push("second"); }); }, }, ]); yield* runtime.reconcile([]); expect(disposed).toEqual(["second", "first"]); yield* runtime.dispose; }), ), ); it.effect("keeps unchanged plugin scopes alive across reconciliation", () => Effect.scoped( Effect.gen(function* () { 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 = yield* createRuntime(); yield* runtime.reconcile([stable]); yield* runtime.reconcile([stable]); expect(activations).toBe(1); expect(disposals).toBe(0); yield* runtime.dispose; expect(disposals).toBe(1); }), ), ); it.effect("treats reordered requirements and providers as the same definition", () => Effect.scoped( Effect.gen(function* () { 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 = yield* createRuntime(); yield* 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, }, ]); yield* 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); yield* runtime.dispose; }), ), ); it.effect("restarts a plugin when its activation implementation changes", () => Effect.scoped( Effect.gen(function* () { const runtime = yield* 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" }); }, }; yield* runtime.reconcile([first]); const snapshot = yield* runtime.reconcile([second]); expect(contributionLabels(snapshot, "commands")).toEqual(["second"]); yield* runtime.dispose; }), ), ); it.effect( "restarts a changed provider and its dependents without touching independent plugins", () => Effect.scoped( Effect.gen(function* () { let independentActivations = 0; const lifecycle: Array<string> = []; const independent: PluginDefinition = { id: "acme.independent", version: "1.0.0", activate() { independentActivations += 1; }, }; const runtime = yield* createRuntime({ onLifecycle: ({ phase, pluginId }) => lifecycle.push(`${phase}:${pluginId}`), }); yield* runtime.reconcile([consumer(), independent, provider()]); lifecycle.length = 0; const snapshot = yield* 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"]); yield* runtime.dispose; }), ), ); it.effect("returns deeply frozen snapshots", () => Effect.scoped( Effect.gen(function* () { const runtime = yield* createRuntime(); const snapshot = yield* 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); yield* runtime.dispose; }), ), ); it.effect("detaches registered contributions from later plugin mutation", () => Effect.scoped( Effect.gen(function* () { const contribution = { id: "mutable", label: "original" }; const plugin: PluginDefinition = { id: "acme.mutable-contribution", version: "1.0.0", activate(context) { context.register("commands", contribution); }, }; const unrelated: PluginDefinition = { id: "acme.unrelated-contribution", version: "1.0.0", activate() {}, }; const runtime = yield* createRuntime(); yield* runtime.reconcile([plugin]); contribution.label = "changed"; const snapshot = yield* runtime.reconcile([plugin, unrelated]); expect(contributionLabels(snapshot, "commands")).toEqual(["original"]); yield* runtime.dispose; }), ), ); it.effect("treats every plugin id and contribution slot as data", () => Effect.scoped( Effect.gen(function* () { const runtime = yield* 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 = yield* 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"]); yield* runtime.dispose; }), ), ); it.effect("restarts dependents when a provided value changes", () => Effect.scoped( Effect.gen(function* () { const lifecycle: Array<string> = []; const runtime = yield* 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 }); }, }; yield* runtime.reconcile([serviceProvider("first"), serviceConsumer]); lifecycle.length = 0; const snapshot = yield* 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", ]); yield* runtime.dispose; }), ), ); it.effect("does not report a rolled-back candidate as activated", () => Effect.scoped( Effect.gen(function* () { const lifecycle: Array<string> = []; const runtime = yield* 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"); }, }; const stagingFailure = yield* failureOf(runtime.reconcile([staged, broken])); expect(failureMessage(stagingFailure)).toContain("activation failed"); expect(lifecycle).toEqual([]); yield* runtime.dispose; }), ), ); it.effect("preserves activation failures when rollback cleanup also fails", () => Effect.scoped( Effect.gen(function* () { const activationError = new Error("activation failed"); const cleanupError = new Error("rollback cleanup failed"); const cleanupEvents: Array<{ readonly phase: string; readonly error: unknown }> = []; const runtime = yield* 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; }, }; const activationFailure = yield* failureOf(runtime.reconcile([broken])); expect(failureCause(activationFailure)).toBe(activationError); expect(cleanupEvents).toHaveLength(1); expect(cleanupEvents[0]?.phase).toBe("rollback"); expect(failureCause(cleanupEvents[0]?.error)).toBe(cleanupError); yield* runtime.dispose; }),Lines 1–500 of 1057. Load more
import { it } from "@effect/vitest"; import { describe, expect } from "vite-plus/test"; import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as Scope from "effect/Scope"; import type { PluginRuntime, PluginRuntimeReconcileError } from "../src/runtime.ts"; import type { PluginDefinition, PluginRuntimeOptions, PluginRuntimeSnapshot, } from "../src/contract.ts"; type TestPluginRuntimeFactory = ( options?: PluginRuntimeOptions, ) => Effect.Effect<PluginRuntime["Service"], never, Scope.Scope>; const makeEffectCallback = <A, E, R = never>() => { let unsafeResume: (effect: Effect.Effect<A, E, R>) => void = () => { throw new Error("effect callback has not started"); }; return { await: Effect.callback<A, E, R>((resume) => { unsafeResume = resume; }), resume: (effect: Effect.Effect<A, E, R>) => unsafeResume(effect), } as const; }; const failureOf = <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.exit(effect).pipe( Effect.map((exit) => (Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined)), ); const settlePromise = ( exit: Exit.Exit<unknown, unknown>, resolve: () => void, reject: (error: unknown) => void, ) => { if (Exit.isFailure(exit)) reject(Cause.squash(exit.cause)); else resolve(); }; const contributionLabels = (snapshot: PluginRuntimeSnapshot, slot: string) => snapshot.contributions[slot]?.map((item) => item.label) ?? []; 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: TestPluginRuntimeFactory) { describe(name, () => { it.effect("activates providers before consumers regardless of manifest order", () => Effect.scoped( Effect.gen(function* () { const runtime = yield* createRuntime(); const snapshot = yield* runtime.reconcile([consumer(), provider()]); expect(snapshot.active).toEqual(["acme.database", "acme.issues"]); expect(contributionLabels(snapshot, "commands")).toEqual(["database-1.0.0"]); yield* runtime.dispose; }), ), ); it.effect("blocks missing dependencies without blocking independent plugins", () => Effect.scoped( Effect.gen(function* () { const runtime = yield* createRuntime(); const independent: PluginDefinition = { id: "acme.clock", version: "1.0.0", activate(context) { context.register("status", { id: "clock", label: "clock" }); }, }; const snapshot = yield* 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"]); yield* runtime.dispose; }), ), ); it.effect("deactivates dependents before providers", () => Effect.scoped( Effect.gen(function* () { const lifecycle: Array<string> = []; const runtime = yield* createRuntime({ onLifecycle: ({ phase, pluginId }) => lifecycle.push(`${phase}:${pluginId}`), }); yield* runtime.reconcile([provider(), consumer()]); lifecycle.length = 0; yield* runtime.reconcile([]); expect(lifecycle).toEqual(["deactivate:acme.issues", "deactivate:acme.database"]); yield* runtime.dispose; }), ), ); it.effect("runs plugin finalizers in reverse registration order", () => Effect.scoped( Effect.gen(function* () { const disposed: Array<string> = []; const runtime = yield* createRuntime(); yield* runtime.reconcile([ { id: "acme.finalizers", version: "1.0.0", activate(context) { context.onDispose(() => { disposed.push("first"); }); context.onDispose(() => { disposed.push("second"); }); }, }, ]); yield* runtime.reconcile([]); expect(disposed).toEqual(["second", "first"]); yield* runtime.dispose; }), ), ); it.effect("keeps unchanged plugin scopes alive across reconciliation", () => Effect.scoped( Effect.gen(function* () { 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 = yield* createRuntime(); yield* runtime.reconcile([stable]); yield* runtime.reconcile([stable]); expect(activations).toBe(1); expect(disposals).toBe(0); yield* runtime.dispose; expect(disposals).toBe(1); }), ), ); it.effect("treats reordered requirements and providers as the same definition", () => Effect.scoped( Effect.gen(function* () { 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 = yield* createRuntime(); yield* 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, }, ]); yield* 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); yield* runtime.dispose; }), ), ); it.effect("restarts a plugin when its activation implementation changes", () => Effect.scoped( Effect.gen(function* () { const runtime = yield* 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" }); }, }; yield* runtime.reconcile([first]); const snapshot = yield* runtime.reconcile([second]); expect(contributionLabels(snapshot, "commands")).toEqual(["second"]); yield* runtime.dispose; }), ), ); it.effect( "restarts a changed provider and its dependents without touching independent plugins", () => Effect.scoped( Effect.gen(function* () { let independentActivations = 0; const lifecycle: Array<string> = []; const independent: PluginDefinition = { id: "acme.independent", version: "1.0.0", activate() { independentActivations += 1; }, }; const runtime = yield* createRuntime({ onLifecycle: ({ phase, pluginId }) => lifecycle.push(`${phase}:${pluginId}`), }); yield* runtime.reconcile([consumer(), independent, provider()]); lifecycle.length = 0; const snapshot = yield* 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"]); yield* runtime.dispose; }), ), ); it.effect("returns deeply frozen snapshots", () => Effect.scoped( Effect.gen(function* () { const runtime = yield* createRuntime(); const snapshot = yield* 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); yield* runtime.dispose; }), ), ); it.effect("detaches registered contributions from later plugin mutation", () => Effect.scoped( Effect.gen(function* () { const contribution = { id: "mutable", label: "original" }; const plugin: PluginDefinition = { id: "acme.mutable-contribution", version: "1.0.0", activate(context) { context.register("commands", contribution); }, }; const unrelated: PluginDefinition = { id: "acme.unrelated-contribution", version: "1.0.0", activate() {}, }; const runtime = yield* createRuntime(); yield* runtime.reconcile([plugin]); contribution.label = "changed"; const snapshot = yield* runtime.reconcile([plugin, unrelated]); expect(contributionLabels(snapshot, "commands")).toEqual(["original"]); yield* runtime.dispose; }), ), ); it.effect("treats every plugin id and contribution slot as data", () => Effect.scoped( Effect.gen(function* () { const runtime = yield* 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 = yield* 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"]); yield* runtime.dispose; }), ), ); it.effect("restarts dependents when a provided value changes", () => Effect.scoped( Effect.gen(function* () { const lifecycle: Array<string> = []; const runtime = yield* 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 }); }, }; yield* runtime.reconcile([serviceProvider("first"), serviceConsumer]); lifecycle.length = 0; const snapshot = yield* 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", ]); yield* runtime.dispose; }), ), ); it.effect("does not report a rolled-back candidate as activated", () => Effect.scoped( Effect.gen(function* () { const lifecycle: Array<string> = []; const runtime = yield* 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"); }, }; const stagingFailure = yield* failureOf(runtime.reconcile([staged, broken])); expect(failureMessage(stagingFailure)).toContain("activation failed"); expect(lifecycle).toEqual([]); yield* runtime.dispose; }), ), ); it.effect("preserves activation failures when rollback cleanup also fails", () => Effect.scoped( Effect.gen(function* () { const activationError = new Error("activation failed"); const cleanupError = new Error("rollback cleanup failed"); const cleanupEvents: Array<{ readonly phase: string; readonly error: unknown }> = []; const runtime = yield* 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; }, }; const activationFailure = yield* failureOf(runtime.reconcile([broken])); expect(failureCause(activationFailure)).toBe(activationError); expect(cleanupEvents).toHaveLength(1); expect(cleanupEvents[0]?.phase).toBe("rollback"); expect(failureCause(cleanupEvents[0]?.error)).toBe(cleanupError); yield* runtime.dispose; }),