import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { type ChannelId, type ChannelPlugin } from "../channels/plugins/types.js"; import { createSubsystemLogger, type SubsystemLogger, runtimeForLogger, } from "../logging/subsystem.js"; import { createEmptyPluginRegistry, type PluginRegistry } from "../plugins/registry.js"; import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js"; import type { PluginRuntime } from "../plugins/runtime/types.js"; import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; import { createChannelManager } from "./server-channels.js"; const hoisted = vi.hoisted(() => { const computeBackoff = vi.fn(() => 10); const sleepWithAbort = vi.fn((ms: number, abortSignal?: AbortSignal) => { return new Promise((resolve, reject) => { const timer = setTimeout(() => resolve(), ms); abortSignal?.addEventListener( "abort", () => { clearTimeout(timer); reject(new Error("aborted")); }, { once: true }, ); }); }); return { computeBackoff, sleepWithAbort }; }); vi.mock("../infra/backoff.js", () => ({ computeBackoff: hoisted.computeBackoff, sleepWithAbort: hoisted.sleepWithAbort, })); type TestAccount = { enabled?: boolean; configured?: boolean; }; function createTestPlugin(params?: { account?: TestAccount; startAccount?: NonNullable["gateway"]>["startAccount"]; includeDescribeAccount?: boolean; resolveAccount?: ChannelPlugin["config"]["resolveAccount"]; isConfigured?: ChannelPlugin["config"]["isConfigured"]; }): ChannelPlugin { const account = params?.account ?? { enabled: true, configured: true }; const includeDescribeAccount = params?.includeDescribeAccount !== false; const config: ChannelPlugin["config"] = { listAccountIds: () => [DEFAULT_ACCOUNT_ID], resolveAccount: params?.resolveAccount ?? (() => account), isEnabled: (resolved) => resolved.enabled !== false, ...(params?.isConfigured ? { isConfigured: params.isConfigured } : {}), }; if (includeDescribeAccount) { config.describeAccount = (resolved) => ({ accountId: DEFAULT_ACCOUNT_ID, enabled: resolved.enabled !== false, configured: resolved.configured !== false, }); } const gateway: NonNullable["gateway"]> = {}; if (params?.startAccount) { gateway.startAccount = params.startAccount; } return { id: "discord", meta: { id: "discord", label: "Discord", selectionLabel: "Discord", docsPath: "/channels/discord", blurb: "test stub", }, capabilities: { chatTypes: ["direct"] }, config, gateway, }; } function createDeferred(): { promise: Promise; resolve: () => void } { let resolvePromise = () => {}; const promise = new Promise((resolve) => { resolvePromise = resolve; }); return { promise, resolve: resolvePromise }; } function installTestRegistry(plugin: ChannelPlugin) { const registry = createEmptyPluginRegistry(); registry.channels.push({ pluginId: plugin.id, source: "test", plugin, }); setActivePluginRegistry(registry); } function createManager(options?: { channelRuntime?: PluginRuntime["channel"]; resolveChannelRuntime?: () => PluginRuntime["channel"]; loadConfig?: () => Record; }) { const log = createSubsystemLogger("gateway/server-channels-test"); const channelLogs = { discord: log } as Record; const runtime = runtimeForLogger(log); const channelRuntimeEnvs = { discord: runtime } as Record; return createChannelManager({ loadConfig: () => options?.loadConfig?.() ?? {}, channelLogs, channelRuntimeEnvs, ...(options?.channelRuntime ? { channelRuntime: options.channelRuntime } : {}), ...(options?.resolveChannelRuntime ? { resolveChannelRuntime: options.resolveChannelRuntime } : {}), }); } describe("server-channels auto restart", () => { let previousRegistry: PluginRegistry | null = null; beforeEach(() => { previousRegistry = getActivePluginRegistry(); vi.useFakeTimers(); hoisted.computeBackoff.mockClear(); hoisted.sleepWithAbort.mockClear(); }); afterEach(() => { vi.useRealTimers(); setActivePluginRegistry(previousRegistry ?? createEmptyPluginRegistry()); }); it("caps crash-loop restarts after max attempts", async () => { const startAccount = vi.fn(async () => {}); installTestRegistry( createTestPlugin({ startAccount, }), ); const manager = createManager(); await manager.startChannels(); await vi.advanceTimersByTimeAsync(200); expect(startAccount).toHaveBeenCalledTimes(11); const snapshot = manager.getRuntimeSnapshot(); const account = snapshot.channelAccounts.discord?.[DEFAULT_ACCOUNT_ID]; expect(account?.running).toBe(false); expect(account?.reconnectAttempts).toBe(11); await vi.advanceTimersByTimeAsync(200); expect(startAccount).toHaveBeenCalledTimes(11); }); it("does not auto-restart after manual stop during backoff", async () => { const startAccount = vi.fn(async () => {}); installTestRegistry( createTestPlugin({ startAccount, }), ); const manager = createManager(); await manager.startChannels(); vi.runAllTicks(); await manager.stopChannel("discord", DEFAULT_ACCOUNT_ID); await vi.advanceTimersByTimeAsync(200); expect(startAccount).toHaveBeenCalledTimes(1); }); it("marks enabled/configured when account descriptors omit them", () => { installTestRegistry( createTestPlugin({ includeDescribeAccount: false, }), ); const manager = createManager(); const snapshot = manager.getRuntimeSnapshot(); const account = snapshot.channelAccounts.discord?.[DEFAULT_ACCOUNT_ID]; expect(account?.enabled).toBe(true); expect(account?.configured).toBe(true); }); it("passes channelRuntime through channel gateway context when provided", async () => { const channelRuntime = { marker: "channel-runtime" } as unknown as PluginRuntime["channel"]; const startAccount = vi.fn(async (ctx) => { expect(ctx.channelRuntime).toBe(channelRuntime); }); installTestRegistry(createTestPlugin({ startAccount })); const manager = createManager({ channelRuntime }); await manager.startChannels(); expect(startAccount).toHaveBeenCalledTimes(1); }); it("deduplicates concurrent start requests for the same account", async () => { const startupGate = createDeferred(); const isConfigured = vi.fn(async () => { await startupGate.promise; return true; }); const startAccount = vi.fn(async () => {}); installTestRegistry(createTestPlugin({ startAccount, isConfigured })); const manager = createManager(); const firstStart = manager.startChannel("discord", DEFAULT_ACCOUNT_ID); const secondStart = manager.startChannel("discord", DEFAULT_ACCOUNT_ID); await Promise.resolve(); expect(isConfigured).toHaveBeenCalledTimes(1); expect(startAccount).not.toHaveBeenCalled(); startupGate.resolve(); await Promise.all([firstStart, secondStart]); expect(startAccount).toHaveBeenCalledTimes(1); }); it("cancels a pending startup when the account is stopped mid-boot", async () => { const startupGate = createDeferred(); const isConfigured = vi.fn(async () => { await startupGate.promise; return true; }); const startAccount = vi.fn(async () => {}); installTestRegistry(createTestPlugin({ startAccount, isConfigured })); const manager = createManager(); const startTask = manager.startChannel("discord", DEFAULT_ACCOUNT_ID); await Promise.resolve(); const stopTask = manager.stopChannel("discord", DEFAULT_ACCOUNT_ID); startupGate.resolve(); await Promise.all([startTask, stopTask]); expect(startAccount).not.toHaveBeenCalled(); }); it("does not resolve channelRuntime until a channel starts", async () => { const channelRuntime = { marker: "lazy-channel-runtime", } as unknown as PluginRuntime["channel"]; const resolveChannelRuntime = vi.fn(() => channelRuntime); const startAccount = vi.fn(async (ctx) => { expect(ctx.channelRuntime).toBe(channelRuntime); }); installTestRegistry(createTestPlugin({ startAccount })); const manager = createManager({ resolveChannelRuntime }); expect(resolveChannelRuntime).not.toHaveBeenCalled(); void manager.getRuntimeSnapshot(); expect(resolveChannelRuntime).not.toHaveBeenCalled(); await manager.startChannels(); expect(resolveChannelRuntime).toHaveBeenCalledTimes(1); expect(startAccount).toHaveBeenCalledTimes(1); }); it("reuses plugin account resolution for health monitor overrides", () => { installTestRegistry( createTestPlugin({ resolveAccount: (cfg, accountId) => { const accounts = ( cfg as { channels?: { discord?: { accounts?: Record< string, TestAccount & { healthMonitor?: { enabled?: boolean } } >; }; }; } ).channels?.discord?.accounts; if (!accounts) { return { enabled: true, configured: true }; } const direct = accounts[accountId ?? DEFAULT_ACCOUNT_ID]; if (direct) { return direct; } const normalized = (accountId ?? DEFAULT_ACCOUNT_ID).toLowerCase().replaceAll(" ", "-"); const matchKey = Object.keys(accounts).find( (key) => key.toLowerCase().replaceAll(" ", "-") === normalized, ); return matchKey ? (accounts[matchKey] ?? { enabled: true, configured: true }) : {}; }, }), ); const manager = createManager({ loadConfig: () => ({ channels: { discord: { accounts: { "Router D": { enabled: true, configured: true, healthMonitor: { enabled: false }, }, }, }, }, }), }); expect(manager.isHealthMonitorEnabled("discord", "router-d")).toBe(false); }); it("falls back to channel-level health monitor overrides when account resolution omits them", () => { installTestRegistry( createTestPlugin({ resolveAccount: () => ({ enabled: true, configured: true, }), }), ); const manager = createManager({ loadConfig: () => ({ channels: { discord: { healthMonitor: { enabled: false }, }, }, }), }); expect(manager.isHealthMonitorEnabled("discord", DEFAULT_ACCOUNT_ID)).toBe(false); }); it("uses raw account config overrides when resolvers omit health monitor fields", () => { installTestRegistry( createTestPlugin({ resolveAccount: () => ({ enabled: true, configured: true, }), }), ); const manager = createManager({ loadConfig: () => ({ channels: { discord: { accounts: { [DEFAULT_ACCOUNT_ID]: { healthMonitor: { enabled: false }, }, }, }, }, }), }); expect(manager.isHealthMonitorEnabled("discord", DEFAULT_ACCOUNT_ID)).toBe(false); }); it("fails closed when account resolution throws during health monitor gating", () => { installTestRegistry( createTestPlugin({ resolveAccount: () => { throw new Error("unresolved SecretRef"); }, }), ); const manager = createManager(); expect(manager.isHealthMonitorEnabled("discord", DEFAULT_ACCOUNT_ID)).toBe(false); }); it("does not treat an empty account id as the default account when matching raw overrides", () => { installTestRegistry( createTestPlugin({ resolveAccount: () => ({ enabled: true, configured: true, }), }), ); const manager = createManager({ loadConfig: () => ({ channels: { discord: { accounts: { default: { healthMonitor: { enabled: false }, }, }, }, }, }), }); expect(manager.isHealthMonitorEnabled("discord", "")).toBe(true); }); });