import * as NodeServices from "@effect/platform-node/NodeServices"; import { ServerSettingsError } from "@t3tools/contracts"; import { it } from "@effect/vitest"; import { expect } from "vite-plus/test"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as Schedule from "effect/Schedule"; import * as Schema from "effect/Schema"; import { PluginManifest } from "@t3tools/plugin-runtime/manifest"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; import * as ServerConfig from "../config.ts"; import * as ServerSettings from "../serverSettings.ts"; import * as PluginCommandCatalog from "./PluginCommandCatalog.ts"; import * as PluginPackageManager from "./PluginPackageManager.ts"; const packageId = "com.acme.runtime-status"; const commandId = "acme.runtime-status"; const manifest = { manifestVersion: 1, id: packageId, version: "1.0.0", apiVersion: 1, entrypoints: { server: "./index.mjs" }, capabilities: ["t3.commands@1"], contributes: { commands: [commandId] }, } as const; const encodeManifest = Schema.encodeSync(Schema.fromJsonString(PluginManifest)); const encodeJsonString = Schema.encodeSync(Schema.fromJsonString(Schema.String)); const decodePersistedEnabledPlugins = Schema.decodeUnknownSync( Schema.fromJsonString( Schema.Struct({ enabledPluginIds: Schema.optional(Schema.Array(Schema.String)) }), ), ); const pluginSource = (disposalFile: string, message = "External plugin runtime is active.") => ` import { appendFile } from "node:fs/promises"; export default function activate(api) { api.registerCommand( { id: "${commandId}", label: "External runtime status", description: "Report status from an external local plugin package.", surfaces: ["web", "desktop", "mobile"] }, () => ({ message: ${encodeJsonString(message)}, tone: "success" }) ); api.onDispose(() => appendFile(${encodeJsonString(disposalFile)}, "disposed\\n")); } `; const pluginSourceWithHelper = ` import { message } from "./message.mjs"; export default function activate(api) { api.registerCommand( { id: "${commandId}", label: "External runtime status", surfaces: ["web", "desktop", "mobile"] }, () => ({ message, tone: "success" }) ); } `; const pluginSourceWithRetirementGate = (startedSymbol: string, releaseSymbol: string) => ` export default function activate(api) { api.registerCommand( { id: "${commandId}", label: "External runtime status", surfaces: ["web", "desktop", "mobile"] }, () => ({ message: "retirement gate", tone: "success" }) ); api.onDispose(() => new Promise((resolve) => { const markStarted = Reflect.get(globalThis, Symbol.for(${encodeJsonString(startedSymbol)})); if (typeof markStarted === "function") markStarted(); Reflect.set(globalThis, Symbol.for(${encodeJsonString(releaseSymbol)}), resolve); })); } `; const pluginSourceWithCleanupFailure = ` export default function activate(api) { api.registerCommand( { id: "${commandId}", label: "External runtime status", surfaces: ["web", "desktop", "mobile"] }, () => ({ message: "cleanup failure", tone: "success" }) ); api.onDispose(() => { throw new Error("cleanup exploded"); }); } `; interface EnvironmentLayerOptions { readonly persistenceFailures?: { remaining: number }; } const makeEnvironmentLayer = (baseDir: string, options?: EnvironmentLayerOptions) => { const configLayer = Layer.fresh(ServerConfig.layerTest(process.cwd(), baseDir)); const liveSettingsLayer = ServerSettings.layer.pipe( Layer.provide(ServerSecretStore.layer), Layer.provideMerge(configLayer), ); const persistenceFailures = options?.persistenceFailures; const settingsLayer = persistenceFailures === undefined ? liveSettingsLayer : Layer.effect( ServerSettings.ServerSettingsService, Effect.gen(function* () { const live = yield* ServerSettings.ServerSettingsService; return ServerSettings.ServerSettingsService.of({ ...live, setEnabledPluginIds: (ids) => Effect.suspend(() => { if (persistenceFailures.remaining > 0) { persistenceFailures.remaining -= 1; return Effect.fail( new ServerSettingsError({ cause: new Error("injected persistence failure"), operation: "write-file", settingsPath: `${baseDir}/userdata/settings.json`, }), ); } return live.setEnabledPluginIds(ids); }), }); }), ).pipe(Layer.provide(liveSettingsLayer)); return PluginPackageManager.layer.pipe( Layer.provideMerge(PluginCommandCatalog.layer), Layer.provideMerge(settingsLayer), Layer.provideMerge(configLayer), ); }; const useEnvironment = ( baseDir: string, effect: Effect.Effect< A, E, PluginPackageManager.PluginPackageManager | PluginCommandCatalog.PluginCommandCatalog >, options?: EnvironmentLayerOptions, ) => Effect.scoped(effect.pipe(Effect.provide(makeEnvironmentLayer(baseDir, options)))); it.layer(NodeServices.layer)("plugin package lifecycle", (it) => { it.effect("loads the committed external runtime-status example without rebuilding", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-plugin-package-example-test-", }); const exampleId = "com.t3code.runtime-status-example"; const exampleCommandId = "example.runtime-status"; yield* fileSystem.makeDirectory(`${baseDir}/userdata/plugins`, { recursive: true }); yield* fileSystem.copy( path.resolve(import.meta.dirname, "../../../../examples/plugins/runtime-status"), `${baseDir}/userdata/plugins/${exampleId}`, ); yield* useEnvironment( baseDir, Effect.gen(function* () { const manager = yield* PluginPackageManager.PluginPackageManager; const catalog = yield* PluginCommandCatalog.PluginCommandCatalog; yield* manager.enable(exampleId); const listed = yield* catalog.list; expect( yield* catalog.invoke({ generation: listed.generation, id: exampleCommandId }), ).toEqual({ message: "external plugin runtime is active.", tone: "success" }); }), ); }), ); it.effect("discovers, enables, restarts, and cleanly disables an external package", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-plugin-package-test-", }); const packageDirectory = `${baseDir}/userdata/plugins/${packageId}`; yield* fileSystem.makeDirectory(packageDirectory, { recursive: true }); yield* fileSystem.writeFileString( `${packageDirectory}/t3-plugin.json`, encodeManifest(manifest), ); yield* fileSystem.writeFileString( `${packageDirectory}/index.mjs`, pluginSource(`${packageDirectory}/disposed.log`), ); yield* useEnvironment( baseDir, Effect.gen(function* () { const manager = yield* PluginPackageManager.PluginPackageManager; const catalog = yield* PluginCommandCatalog.PluginCommandCatalog; expect((yield* Effect.exit(manager.reload(packageId)))._tag).toBe("Failure"); expect(yield* manager.status).toMatchObject({ packages: [{ id: packageId, enabled: false, state: "disabled" }], }); expect(yield* manager.enable(packageId)).toMatchObject({ packages: [{ id: packageId, enabled: true, state: "active" }], }); const listed = yield* catalog.list; expect(listed.commands.map((command) => command.id)).toContain(commandId); expect(yield* catalog.invoke({ generation: listed.generation, id: commandId })).toEqual({ message: "External plugin runtime is active.", tone: "success", }); }), ); expect( decodePersistedEnabledPlugins( yield* fileSystem.readFileString(`${baseDir}/userdata/settings.json`), ).enabledPluginIds, ).toEqual([packageId]); yield* useEnvironment( baseDir, Effect.gen(function* () { const manager = yield* PluginPackageManager.PluginPackageManager; const catalog = yield* PluginCommandCatalog.PluginCommandCatalog; expect(yield* manager.status).toMatchObject({ packages: [{ id: packageId, enabled: true, state: "active" }], }); expect((yield* catalog.list).commands.map((command) => command.id)).toContain(commandId); expect(yield* manager.disable(packageId)).toMatchObject({ packages: [{ id: packageId, enabled: false, state: "disabled" }], }); expect((yield* catalog.list).commands.map((command) => command.id)).not.toContain( commandId, ); }), ); const persisted = yield* fileSystem.readFileString(`${baseDir}/userdata/settings.json`); expect(decodePersistedEnabledPlugins(persisted).enabledPluginIds ?? []).toEqual([]); expect(yield* fileSystem.readFileString(`${packageDirectory}/disposed.log`)).toBe( "disposed\ndisposed\n", ); }), ); it.effect("keeps the previous generation when import or activation fails during reload", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-plugin-package-rollback-test-", }); const packageDirectory = `${baseDir}/userdata/plugins/${packageId}`; yield* fileSystem.makeDirectory(packageDirectory, { recursive: true }); yield* fileSystem.writeFileString( `${packageDirectory}/t3-plugin.json`, encodeManifest(manifest), ); yield* fileSystem.writeFileString(`${packageDirectory}/index.mjs`, pluginSourceWithHelper); yield* fileSystem.writeFileString( `${packageDirectory}/message.mjs`, 'export const message = "generation one";\n', ); yield* useEnvironment( baseDir, Effect.gen(function* () { const manager = yield* PluginPackageManager.PluginPackageManager; const catalog = yield* PluginCommandCatalog.PluginCommandCatalog; yield* manager.enable(packageId); const committed = yield* catalog.list; const manifestV2 = { ...manifest, version: "2.0.0" as const }; yield* fileSystem.writeFileString( `${packageDirectory}/t3-plugin.json`, encodeManifest(manifestV2), ); yield* fileSystem.writeFileString(`${packageDirectory}/index.mjs`, "export default ("); expect((yield* Effect.exit(manager.reload(packageId)))._tag).toBe("Failure"); expect(yield* catalog.list).toBe(committed); expect( yield* catalog.invoke({ generation: committed.generation, id: commandId }), ).toEqual({ message: "generation one", tone: "success" }); yield* fileSystem.writeFileString( `${packageDirectory}/index.mjs`, "export default function activate() { throw new Error('activation failed') }", ); expect((yield* Effect.exit(manager.reload(packageId)))._tag).toBe("Failure"); expect(yield* catalog.list).toBe(committed); expect(yield* manager.status).toMatchObject({ packages: [ { id: packageId, version: "1.0.0", enabled: true, state: "error", error: "activation failed", }, ], }); yield* manager.enable(packageId); expect(yield* manager.status).toMatchObject({ packages: [{ id: packageId, state: "error", error: "activation failed" }], }); yield* fileSystem.writeFileString( `${packageDirectory}/index.mjs`, pluginSourceWithHelper, ); yield* fileSystem.writeFileString( `${packageDirectory}/message.mjs`, 'export const message = "generation two";\n', ); yield* manager.reload(packageId); expect(yield* manager.status).toMatchObject({ packages: [{ id: packageId, version: "2.0.0", enabled: true, state: "active" }], }); const reloaded = yield* catalog.list; expect(reloaded.generation).toBeGreaterThan(committed.generation); expect(yield* catalog.invoke({ generation: reloaded.generation, id: commandId })).toEqual( { message: "generation two", tone: "success", }, ); }), ); }), ); it.effect("rejects symbolic links before importing a trusted local package", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-plugin-package-symlink-test-", }); const sourceDirectory = `${baseDir}/linked-package-source`; const packageDirectory = `${baseDir}/userdata/plugins/${packageId}`; yield* fileSystem.makeDirectory(sourceDirectory, { recursive: true }); yield* fileSystem.makeDirectory(`${baseDir}/userdata/plugins`, { recursive: true }); yield* fileSystem.writeFileString( `${sourceDirectory}/t3-plugin.json`, encodeManifest(manifest), ); yield* fileSystem.writeFileString( `${sourceDirectory}/index.mjs`, pluginSource(`${sourceDirectory}/disposed.log`), ); yield* fileSystem.symlink(sourceDirectory, packageDirectory); yield* useEnvironment( baseDir, Effect.gen(function* () { const manager = yield* PluginPackageManager.PluginPackageManager; const catalog = yield* PluginCommandCatalog.PluginCommandCatalog; expect((yield* Effect.exit(manager.enable(packageId)))._tag).toBe("Failure"); expect((yield* catalog.list).commands.map((command) => command.id)).not.toContain( commandId, ); expect(yield* manager.status).toMatchObject({ packages: [{ id: packageId, enabled: false, state: "error" }], }); }), ); }), ); it.effect("keeps runtime and persisted enablement aligned when settings writes fail", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-plugin-package-persistence-test-", }); const packageDirectory = `${baseDir}/userdata/plugins/${packageId}`; yield* fileSystem.makeDirectory(packageDirectory, { recursive: true }); yield* fileSystem.writeFileString( `${packageDirectory}/t3-plugin.json`, encodeManifest(manifest), ); yield* fileSystem.writeFileString( `${packageDirectory}/index.mjs`, pluginSource(`${packageDirectory}/disposed.log`), ); yield* useEnvironment( baseDir, Effect.gen(function* () { const manager = yield* PluginPackageManager.PluginPackageManager; const catalog = yield* PluginCommandCatalog.PluginCommandCatalog; expect((yield* Effect.exit(manager.enable(packageId)))._tag).toBe("Failure"); expect((yield* catalog.list).commands.map((command) => command.id)).not.toContain( commandId, ); expect(yield* manager.status).toMatchObject({ packages: [{ id: packageId, enabled: false }], }); }), { persistenceFailures: { remaining: 1 } }, ); yield* useEnvironment( baseDir, Effect.gen(function* () { const manager = yield* PluginPackageManager.PluginPackageManager; yield* manager.enable(packageId); }), ); yield* useEnvironment( baseDir, Effect.gen(function* () { const manager = yield* PluginPackageManager.PluginPackageManager; const catalog = yield* PluginCommandCatalog.PluginCommandCatalog; expect((yield* Effect.exit(manager.disable(packageId)))._tag).toBe("Failure"); expect((yield* catalog.list).commands.map((command) => command.id)).toContain(commandId); expect(yield* manager.status).toMatchObject({ packages: [{ id: packageId, enabled: true, state: "active" }], }); }), { persistenceFailures: { remaining: 1 } }, ); }), ); it.effect("reports an invalid local manifest without blocking the package service", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-plugin-package-invalid-test-", }); const invalidDirectory = `${baseDir}/userdata/plugins/broken-package`; yield* fileSystem.makeDirectory(invalidDirectory, { recursive: true }); yield* fileSystem.writeFileString(`${invalidDirectory}/t3-plugin.json`, "{}"); yield* useEnvironment( baseDir, Effect.gen(function* () { const manager = yield* PluginPackageManager.PluginPackageManager; expect(yield* manager.status).toMatchObject({ errors: [{ directory: "broken-package" }], packages: [], }); }), ); }), ); it.effect("reports cleanup failures after disabling the committed package", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-plugin-package-cleanup-test-", }); const packageDirectory = `${baseDir}/userdata/plugins/${packageId}`; yield* fileSystem.makeDirectory(packageDirectory, { recursive: true }); yield* fileSystem.writeFileString( `${packageDirectory}/t3-plugin.json`, encodeManifest(manifest), ); yield* fileSystem.writeFileString( `${packageDirectory}/index.mjs`, pluginSourceWithCleanupFailure, ); yield* useEnvironment( baseDir, Effect.gen(function* () { const manager = yield* PluginPackageManager.PluginPackageManager; const catalog = yield* PluginCommandCatalog.PluginCommandCatalog; yield* manager.enable(packageId); expect(yield* manager.disable(packageId)).toMatchObject({ packages: [ { id: packageId, enabled: false, state: "error", error: "cleanup exploded", }, ], }); expect((yield* catalog.list).commands.map((command) => command.id)).not.toContain( commandId, ); }), ); }), ); it.effect("finishes disable bookkeeping when interrupted after the runtime commits", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const baseDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-plugin-package-interruption-test-", }); const packageDirectory = `${baseDir}/userdata/plugins/${packageId}`; const startedSymbol = `t3.test.plugin.retirement.started.${baseDir}`; const releaseSymbol = `t3.test.plugin.retirement.${baseDir}`; let markRetirementStarted!: () => void; const retirementStarted = new Promise((resolve) => { markRetirementStarted = resolve; }); Reflect.set(globalThis, Symbol.for(startedSymbol), markRetirementStarted); yield* fileSystem.makeDirectory(packageDirectory, { recursive: true }); yield* fileSystem.writeFileString( `${packageDirectory}/t3-plugin.json`, encodeManifest(manifest), ); yield* fileSystem.writeFileString( `${packageDirectory}/index.mjs`, pluginSourceWithRetirementGate(startedSymbol, releaseSymbol), ); yield* useEnvironment( baseDir, Effect.gen(function* () { const manager = yield* PluginPackageManager.PluginPackageManager; const catalog = yield* PluginCommandCatalog.PluginCommandCatalog; yield* manager.enable(packageId); const disabling = yield* Effect.forkChild(manager.disable(packageId)); yield* Effect.promise(() => retirementStarted); const interrupting = yield* Effect.forkChild(Fiber.interrupt(disabling)); yield* Effect.yieldNow; const release = Reflect.get(globalThis, Symbol.for(releaseSymbol)); expect(release).toBeTypeOf("function"); if (typeof release === "function") release(); yield* Fiber.join(interrupting); expect( yield* fileSystem.exists(`${baseDir}/userdata/plugin-cache/${packageId}/0`).pipe( Effect.repeat({ schedule: Schedule.spaced("1 millis"), until: (exists) => !exists, }), Effect.timeout("2 seconds"), ), ).toBe(false); Reflect.deleteProperty(globalThis, Symbol.for(startedSymbol)); Reflect.deleteProperty(globalThis, Symbol.for(releaseSymbol)); expect(yield* manager.status).toMatchObject({ packages: [{ id: packageId, enabled: false, state: "disabled" }], }); expect((yield* catalog.list).commands.map((command) => command.id)).not.toContain( commandId, ); }), ); const persisted = yield* fileSystem.readFileString(`${baseDir}/userdata/settings.json`); expect(decodePersistedEnabledPlugins(persisted).enabledPluginIds ?? []).toEqual([]); }), ); });