1057 lines · 35.7 KB · TypeScript
RawDownload
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.