// Runtime import side-effect contract tests cover cold import behavior for plugin runtime code. import fs from "node:fs"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { assertNoImportTimeSideEffects } from "../../plugin-sdk/test-helpers/import-side-effects.js"; const listChannelPlugins = vi.hoisted(() => vi.fn(() => [ { id: "telegram", messaging: { defaultMarkdownTableMode: "bullets", }, }, ]), ); const getActivePluginChannelRegistryVersion = vi.hoisted(() => vi.fn(() => 1)); const CHANNEL_REGISTRY_SEAM = "listChannelPlugins()"; const CHANNEL_REGISTRY_WHY = "it boots active channel metadata on hot runtime/config import paths and turns cheap module evaluation into plugin registry work."; const CHANNEL_REGISTRY_FIX = "keep the seam behind a lazy getter/runtime boundary so import stays cold and the first real lookup loads once."; const HOT_RUNTIME_IMPORT_CASES = [ ["src/config/markdown-tables.ts", () => import("../../config/markdown-tables.js")], [ "src/plugin-sdk/approval-handler-adapter-runtime.ts", () => import("../../plugin-sdk/approval-handler-adapter-runtime.js"), ], [ "src/plugin-sdk/approval-gateway-runtime.ts", () => import("../../plugin-sdk/approval-gateway-runtime.js"), ], [ "src/plugin-sdk/approval-reference-runtime.ts", () => import("../../plugin-sdk/approval-reference-runtime.js"), ], ["src/plugins/runtime/runtime-system.ts", () => import("../runtime/runtime-system.js")], ["src/web-search/runtime.ts", () => import("../../web-search/runtime.js")], ["src/web-fetch/runtime.ts", () => import("../../web-fetch/runtime.js")], ] as const; function mockChannelRegistry() { vi.doMock("../../channels/plugins/index.js", async () => { const actual = await vi.importActual( "../../channels/plugins/index.js", ); return { ...actual, normalizeChannelId: (raw?: string | null) => raw ?? null, }; }); vi.doMock("../../channels/plugins/registry.js", async () => { const actual = await vi.importActual( "../../channels/plugins/registry.js", ); return { ...actual, listChannelPlugins, }; }); vi.doMock("../../plugins/runtime.js", async () => { const actual = await vi.importActual( "../../plugins/runtime.js", ); return { ...actual, getActivePluginChannelRegistryVersion, }; }); } function expectNoChannelRegistryDuringImport(moduleId: string) { assertNoImportTimeSideEffects({ moduleId, forbiddenSeam: CHANNEL_REGISTRY_SEAM, calls: listChannelPlugins.mock.calls, why: CHANNEL_REGISTRY_WHY, fixHint: CHANNEL_REGISTRY_FIX, }); expect(getActivePluginChannelRegistryVersion).not.toHaveBeenCalled(); } afterEach(() => { vi.resetModules(); vi.restoreAllMocks(); vi.doUnmock("../../channels/plugins/index.js"); vi.doUnmock("../../channels/plugins/registry.js"); vi.doUnmock("../../plugins/runtime.js"); }); describe("runtime import side-effect contracts", () => { beforeEach(() => { listChannelPlugins.mockClear(); getActivePluginChannelRegistryVersion.mockClear().mockReturnValue(1); }); it("keeps markdown table defaults lazy and memoized after import", async () => { mockChannelRegistry(); const markdownTables = await import("../../config/markdown-tables.js"); expectNoChannelRegistryDuringImport("src/config/markdown-tables.ts"); expect( markdownTables.resolveMarkdownTableMode({ channel: "telegram", supportsBlockTables: true, }), ).toBe("bullets"); expect(getActivePluginChannelRegistryVersion).toHaveBeenCalled(); expect(listChannelPlugins).toHaveBeenCalledTimes(1); expect( markdownTables.resolveMarkdownTableMode({ channel: "telegram", supportsBlockTables: true, }), ).toBe("bullets"); expect(getActivePluginChannelRegistryVersion).toHaveBeenCalled(); expect(listChannelPlugins).toHaveBeenCalledTimes(1); }); it.each(HOT_RUNTIME_IMPORT_CASES)("keeps %s cold", async (moduleId, importModule) => { mockChannelRegistry(); await importModule(); expectNoChannelRegistryDuringImport(moduleId); }); it("keeps runtime-channel off direct channel registry imports", () => { const source = fs.readFileSync("src/plugins/runtime/runtime-channel.ts", "utf8"); expect(source).not.toContain("../../channels/plugins/registry"); expect(source).not.toContain("../channels/plugins/registry"); }); it("keeps runtime index off direct channel registry imports", () => { const source = fs.readFileSync("src/plugins/runtime/index.ts", "utf8"); expect(source).not.toContain("../../channels/plugins/registry"); expect(source).not.toContain("../channels/plugins/registry"); }); });