File size: 4,970 Bytes
4c76b0d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | // 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<typeof import("../../channels/plugins/index.js")>(
"../../channels/plugins/index.js",
);
return {
...actual,
normalizeChannelId: (raw?: string | null) => raw ?? null,
};
});
vi.doMock("../../channels/plugins/registry.js", async () => {
const actual = await vi.importActual<typeof import("../../channels/plugins/registry.js")>(
"../../channels/plugins/registry.js",
);
return {
...actual,
listChannelPlugins,
};
});
vi.doMock("../../plugins/runtime.js", async () => {
const actual = await vi.importActual<typeof import("../../plugins/runtime.js")>(
"../../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");
});
});
|