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"]; }): 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, }; 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 installTestRegistry(plugin: ChannelPlugin) { const registry = createEmptyPluginRegistry(); registry.channels.push({ pluginId: plugin.id, source: "test", plugin, }); setActivePluginRegistry(registry); } function createManager(options?: { channelRuntime?: 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 } : {}), }); } 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(10); 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("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); }); });