| import { describe, expect, it, vi } from "vitest"; |
| import type { ChannelId } from "../../channels/plugins/index.js"; |
| import type { ChannelAccountSnapshot } from "../../channels/plugins/types.js"; |
| import type { ChannelManager, ChannelRuntimeSnapshot } from "../server-channels.js"; |
| import { createReadinessChecker } from "./readiness.js"; |
|
|
| function snapshotWith( |
| accounts: Record<string, Partial<ChannelAccountSnapshot>>, |
| ): ChannelRuntimeSnapshot { |
| const channels: ChannelRuntimeSnapshot["channels"] = {}; |
| const channelAccounts: ChannelRuntimeSnapshot["channelAccounts"] = {}; |
|
|
| for (const [channelId, accountSnapshot] of Object.entries(accounts)) { |
| const resolved = { accountId: "default", ...accountSnapshot } as ChannelAccountSnapshot; |
| channels[channelId as ChannelId] = resolved; |
| channelAccounts[channelId as ChannelId] = { default: resolved }; |
| } |
|
|
| return { channels, channelAccounts }; |
| } |
|
|
| function createManager(snapshot: ChannelRuntimeSnapshot): ChannelManager { |
| return { |
| getRuntimeSnapshot: vi.fn(() => snapshot), |
| startChannels: vi.fn(), |
| startChannel: vi.fn(), |
| stopChannel: vi.fn(), |
| markChannelLoggedOut: vi.fn(), |
| isManuallyStopped: vi.fn(() => false), |
| resetRestartAttempts: vi.fn(), |
| }; |
| } |
|
|
| function createHealthyDiscordManager(startedAt: number, lastEventAt: number): ChannelManager { |
| return createManager( |
| snapshotWith({ |
| discord: { |
| running: true, |
| connected: true, |
| enabled: true, |
| configured: true, |
| lastStartAt: startedAt, |
| lastEventAt, |
| }, |
| }), |
| ); |
| } |
|
|
| function withReadinessClock(run: () => void) { |
| vi.useFakeTimers(); |
| vi.setSystemTime(new Date("2026-03-06T12:00:00Z")); |
| try { |
| run(); |
| } finally { |
| vi.useRealTimers(); |
| } |
| } |
|
|
| function createReadinessHarness(params: { |
| startedAgoMs: number; |
| accounts: Record<string, Partial<ChannelAccountSnapshot>>; |
| cacheTtlMs?: number; |
| }) { |
| const startedAt = Date.now() - params.startedAgoMs; |
| const manager = createManager(snapshotWith(params.accounts)); |
| return { |
| manager, |
| readiness: createReadinessChecker({ |
| channelManager: manager, |
| startedAt, |
| cacheTtlMs: params.cacheTtlMs, |
| }), |
| }; |
| } |
|
|
| describe("createReadinessChecker", () => { |
| it("reports ready when all managed channels are healthy", () => { |
| withReadinessClock(() => { |
| const startedAt = Date.now() - 5 * 60_000; |
| const manager = createHealthyDiscordManager(startedAt, Date.now() - 1_000); |
|
|
| const readiness = createReadinessChecker({ channelManager: manager, startedAt }); |
| expect(readiness()).toEqual({ ready: true, failing: [], uptimeMs: 300_000 }); |
| }); |
| }); |
|
|
| it("ignores disabled and unconfigured channels", () => { |
| withReadinessClock(() => { |
| const { readiness } = createReadinessHarness({ |
| startedAgoMs: 5 * 60_000, |
| accounts: { |
| discord: { |
| running: false, |
| enabled: false, |
| configured: true, |
| lastStartAt: Date.now() - 5 * 60_000, |
| }, |
| telegram: { |
| running: false, |
| enabled: true, |
| configured: false, |
| lastStartAt: Date.now() - 5 * 60_000, |
| }, |
| }, |
| }); |
| expect(readiness()).toEqual({ ready: true, failing: [], uptimeMs: 300_000 }); |
| }); |
| }); |
|
|
| it("uses startup grace before marking disconnected channels not ready", () => { |
| withReadinessClock(() => { |
| const { readiness } = createReadinessHarness({ |
| startedAgoMs: 30_000, |
| accounts: { |
| discord: { |
| running: true, |
| connected: false, |
| enabled: true, |
| configured: true, |
| lastStartAt: Date.now() - 30_000, |
| }, |
| }, |
| }); |
| expect(readiness()).toEqual({ ready: true, failing: [], uptimeMs: 30_000 }); |
| }); |
| }); |
|
|
| it("reports disconnected managed channels after startup grace", () => { |
| withReadinessClock(() => { |
| const { readiness } = createReadinessHarness({ |
| startedAgoMs: 5 * 60_000, |
| accounts: { |
| discord: { |
| running: true, |
| connected: false, |
| enabled: true, |
| configured: true, |
| lastStartAt: Date.now() - 5 * 60_000, |
| }, |
| }, |
| }); |
| expect(readiness()).toEqual({ ready: false, failing: ["discord"], uptimeMs: 300_000 }); |
| }); |
| }); |
|
|
| it("keeps restart-pending channels ready during reconnect backoff", () => { |
| withReadinessClock(() => { |
| const startedAt = Date.now() - 5 * 60_000; |
| const { readiness } = createReadinessHarness({ |
| startedAgoMs: 5 * 60_000, |
| accounts: { |
| discord: { |
| running: false, |
| restartPending: true, |
| reconnectAttempts: 3, |
| enabled: true, |
| configured: true, |
| lastStartAt: startedAt - 30_000, |
| lastStopAt: Date.now() - 5_000, |
| }, |
| }, |
| }); |
| expect(readiness()).toEqual({ ready: true, failing: [], uptimeMs: 300_000 }); |
| }); |
| }); |
|
|
| it("treats stale-socket channels as ready to avoid pulling healthy idle pods", () => { |
| withReadinessClock(() => { |
| const startedAt = Date.now() - 31 * 60_000; |
| const { readiness } = createReadinessHarness({ |
| startedAgoMs: 31 * 60_000, |
| accounts: { |
| discord: { |
| running: true, |
| connected: true, |
| enabled: true, |
| configured: true, |
| lastStartAt: startedAt, |
| lastEventAt: Date.now() - 31 * 60_000, |
| }, |
| }, |
| }); |
| expect(readiness()).toEqual({ ready: true, failing: [], uptimeMs: 1_860_000 }); |
| }); |
| }); |
|
|
| it("keeps telegram long-polling channels ready without stale-socket classification", () => { |
| withReadinessClock(() => { |
| const startedAt = Date.now() - 31 * 60_000; |
| const { readiness } = createReadinessHarness({ |
| startedAgoMs: 31 * 60_000, |
| accounts: { |
| telegram: { |
| running: true, |
| connected: true, |
| enabled: true, |
| configured: true, |
| lastStartAt: startedAt, |
| lastEventAt: null, |
| }, |
| }, |
| }); |
| expect(readiness()).toEqual({ ready: true, failing: [], uptimeMs: 1_860_000 }); |
| }); |
| }); |
|
|
| it("caches readiness snapshots briefly to keep repeated probes cheap", () => { |
| withReadinessClock(() => { |
| const { manager, readiness } = createReadinessHarness({ |
| startedAgoMs: 5 * 60_000, |
| accounts: { |
| discord: { |
| running: true, |
| connected: true, |
| enabled: true, |
| configured: true, |
| lastStartAt: Date.now() - 5 * 60_000, |
| lastEventAt: Date.now() - 1_000, |
| }, |
| }, |
| cacheTtlMs: 1_000, |
| }); |
| expect(readiness()).toEqual({ ready: true, failing: [], uptimeMs: 300_000 }); |
| vi.advanceTimersByTime(500); |
| expect(readiness()).toEqual({ ready: true, failing: [], uptimeMs: 300_500 }); |
| expect(manager.getRuntimeSnapshot).toHaveBeenCalledTimes(1); |
|
|
| vi.advanceTimersByTime(600); |
| expect(readiness()).toEqual({ ready: true, failing: [], uptimeMs: 301_100 }); |
| expect(manager.getRuntimeSnapshot).toHaveBeenCalledTimes(2); |
| }); |
| }); |
| }); |
|
|