import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { listChannelPlugins } from "../channels/plugins/index.js"; import type { ChannelPlugin } from "../channels/plugins/types.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createTestRegistry } from "../test-utils/channel-plugins.js"; import { buildGatewayReloadPlan, diffConfigPaths, resolveGatewayReloadSettings, } from "./config-reload.js"; describe("diffConfigPaths", () => { it("captures nested config changes", () => { const prev = { hooks: { gmail: { account: "a" } } }; const next = { hooks: { gmail: { account: "b" } } }; const paths = diffConfigPaths(prev, next); expect(paths).toContain("hooks.gmail.account"); }); it("captures array changes", () => { const prev = { messages: { groupChat: { mentionPatterns: ["a"] } } }; const next = { messages: { groupChat: { mentionPatterns: ["b"] } } }; const paths = diffConfigPaths(prev, next); expect(paths).toContain("messages.groupChat.mentionPatterns"); }); }); describe("buildGatewayReloadPlan", () => { const emptyRegistry = createTestRegistry([]); const telegramPlugin: ChannelPlugin = { id: "telegram", meta: { id: "telegram", label: "Telegram", selectionLabel: "Telegram", docsPath: "/channels/telegram", blurb: "test", }, capabilities: { chatTypes: ["direct"] }, config: { listAccountIds: () => [], resolveAccount: () => ({}), }, reload: { configPrefixes: ["channels.telegram"] }, }; const whatsappPlugin: ChannelPlugin = { id: "whatsapp", meta: { id: "whatsapp", label: "WhatsApp", selectionLabel: "WhatsApp", docsPath: "/channels/whatsapp", blurb: "test", }, capabilities: { chatTypes: ["direct"] }, config: { listAccountIds: () => [], resolveAccount: () => ({}), }, reload: { configPrefixes: ["web"], noopPrefixes: ["channels.whatsapp"] }, }; const registry = createTestRegistry([ { pluginId: "telegram", plugin: telegramPlugin, source: "test" }, { pluginId: "whatsapp", plugin: whatsappPlugin, source: "test" }, ]); beforeEach(() => { setActivePluginRegistry(registry); }); afterEach(() => { setActivePluginRegistry(emptyRegistry); }); it("marks gateway changes as restart required", () => { const plan = buildGatewayReloadPlan(["gateway.port"]); expect(plan.restartGateway).toBe(true); expect(plan.restartReasons).toContain("gateway.port"); }); it("restarts the Gmail watcher for hooks.gmail changes", () => { const plan = buildGatewayReloadPlan(["hooks.gmail.account"]); expect(plan.restartGateway).toBe(false); expect(plan.restartGmailWatcher).toBe(true); expect(plan.reloadHooks).toBe(true); }); it("restarts providers when provider config prefixes change", () => { const changedPaths = ["web.enabled", "channels.telegram.botToken"]; const plan = buildGatewayReloadPlan(changedPaths); expect(plan.restartGateway).toBe(false); const expected = new Set( listChannelPlugins() .filter((plugin) => (plugin.reload?.configPrefixes ?? []).some((prefix) => changedPaths.some((path) => path === prefix || path.startsWith(`${prefix}.`)), ), ) .map((plugin) => plugin.id), ); expect(expected.size).toBeGreaterThan(0); expect(plan.restartChannels).toEqual(expected); }); it("treats gateway.remote as no-op", () => { const plan = buildGatewayReloadPlan(["gateway.remote.url"]); expect(plan.restartGateway).toBe(false); expect(plan.noopPaths).toContain("gateway.remote.url"); }); it("defaults unknown paths to restart", () => { const plan = buildGatewayReloadPlan(["unknownField"]); expect(plan.restartGateway).toBe(true); }); }); describe("resolveGatewayReloadSettings", () => { it("uses defaults when unset", () => { const settings = resolveGatewayReloadSettings({}); expect(settings.mode).toBe("hybrid"); expect(settings.debounceMs).toBe(300); }); });