import { describe, expect, mock, test } from "bun:test"; import { Dispatcher } from "../../src/dispatcher/dispatcher"; import type { ProviderResult } from "../../src/providers/types"; import type { Sender } from "../../src/sender/base"; import type { EncryptedPackage, SenderName } from "../../src/sender/types"; function makeFakeSender( name: SenderName, opts: { healthy?: boolean; sendThrows?: boolean } = {}, ): Sender { const createEnvelope = mock().mockResolvedValue({ envelope: "encrypted-data", key: "encrypted-key", } as EncryptedPackage); const healthy = mock().mockResolvedValue(opts.healthy ?? true); const send = opts.sendThrows ? mock().mockRejectedValue(new Error(`${name} send failed`)) : mock().mockResolvedValue(undefined); return { name, destination: { repo: "test-repo", token: "fake" }, createEnvelope, healthy, send, } as unknown as Sender; } function makeBatch(count = 3): ProviderResult[] { return Array.from({ length: count }, (_, i) => ({ provider: "filesystem", service: `test-${i}`, success: true, size: 100, })); } describe("Dispatcher", () => { test("does not throw when no senders (handled gracefully)", () => { expect(() => new Dispatcher({ senders: [] })).not.toThrow(); }); test("does not throw for dryRun with no senders", () => { expect(() => new Dispatcher({ senders: [], dryRun: true })).not.toThrow(); }); test("filters null senders", () => { const s1 = makeFakeSender("github"); const dispatcher = new Dispatcher({ senders: [null, s1] }); expect(dispatcher).toBeDefined(); }); test("dispatches to first healthy sender", async () => { const s1 = makeFakeSender("domain"); const s2 = makeFakeSender("github"); const dispatcher = new Dispatcher({ senders: [s1, s2] }); await dispatcher.dispatch(makeBatch()); expect(s1.healthy).toHaveBeenCalled(); expect(s1.send).toHaveBeenCalled(); expect(s2.send).not.toHaveBeenCalled(); }); test("falls back when first sender is unhealthy", async () => { const s1 = makeFakeSender("domain", { healthy: false }); const s2 = makeFakeSender("github"); const dispatcher = new Dispatcher({ senders: [s1, s2] }); await dispatcher.dispatch(makeBatch()); expect(s1.send).not.toHaveBeenCalled(); expect(s2.send).toHaveBeenCalled(); }); test("falls back when sender throws on send", async () => { const s1 = makeFakeSender("domain", { sendThrows: true }); const s2 = makeFakeSender("github"); const dispatcher = new Dispatcher({ senders: [s1, s2] }); await dispatcher.dispatch(makeBatch()); expect(s1.send).toHaveBeenCalled(); expect(s2.send).toHaveBeenCalled(); }); test("tries all senders when none succeed", async () => { const s1 = makeFakeSender("domain", { sendThrows: true, healthy: false }); const s2 = makeFakeSender("github", { sendThrows: true }); const dispatcher = new Dispatcher({ senders: [s1, s2] }); await expect(dispatcher.dispatch(makeBatch())).resolves.toBeUndefined(); }); test("skips preflight when preflight is false", async () => { const s1 = makeFakeSender("domain"); const dispatcher = new Dispatcher({ senders: [s1], preflight: false }); await dispatcher.dispatch(makeBatch()); expect(s1.healthy).not.toHaveBeenCalled(); expect(s1.send).toHaveBeenCalled(); }); test("empty batch returns immediately", async () => { const s1 = makeFakeSender("domain"); const dispatcher = new Dispatcher({ senders: [s1] }); await dispatcher.dispatch([]); expect(s1.healthy).not.toHaveBeenCalled(); }); });