/** * Unit tests for `src/services/sync-drain-summary.ts` — the per-brand * drain translator that the cycle-8 cron route delegates to. * * The interesting case here is the discriminated union narrowing: * `runScheduledTTSync` returns either a regular `TTDrainResult` * (counters, `skipped: number`) or `{ skipped: true, reason }`. Both * branches carry a `skipped` property, so narrowing on the literal * `result.skipped === true` is the only correct disambiguation. * * Regression coverage: if narrowing switched to `"skipped" in result`, * the happy-branch test (lines 30-44) would fail because * `{ skipped: 1, ... }` would misroute into the if-branch and * `result.reason` would be undefined. */ import { describe, it, expect, vi } from "vitest"; vi.mock("server-only", () => ({})); import { drainOneBrand, summarizeDrains, type DrainRunner, } from "@/services/sync-drain-summary"; describe("drainOneBrand", () => { it("forwards a regular TTDrainResult as a flat DrainSummary", async () => { const run: DrainRunner = vi.fn(async (brandId) => ({ brandId, considered: 5, synced: 3, skipped: 1, failed: 1, })); const summary = await drainOneBrand("brand-1", run); expect(summary).toEqual({ brandId: "brand-1", considered: 5, synced: 3, skipped: 1, failed: 1, }); expect(summary.skippedReason).toBeUndefined(); expect(summary.error).toBeUndefined(); }); it("translates the { skipped: true, reason } branch into a zero summary with skippedReason", async () => { const run: DrainRunner = vi.fn(async () => ({ skipped: true as const, reason: "Last sync 3m ago; need 15m", })); const summary = await drainOneBrand("brand-1", run); expect(summary).toEqual({ brandId: "brand-1", considered: 0, synced: 0, skipped: 0, failed: 0, skippedReason: "Last sync 3m ago; need 15m", }); }); it("catches a thrown Error and surfaces it as `error` (does not reject)", async () => { const run: DrainRunner = vi.fn(async () => { throw new Error("Smartsheet 401"); }); const summary = await drainOneBrand("brand-1", run); expect(summary).toEqual({ brandId: "brand-1", considered: 0, synced: 0, skipped: 0, failed: 0, error: "Smartsheet 401", }); }); it("coerces non-Error thrown values to a string", async () => { const run: DrainRunner = vi.fn(async () => { throw "string-failure"; }); const summary = await drainOneBrand("brand-1", run); expect(summary.error).toBe("string-failure"); }); it("threads brandId to the runner", async () => { const run: DrainRunner = vi.fn(async () => ({ brandId: "ignored", considered: 0, synced: 0, skipped: 0, failed: 0, })); await drainOneBrand("the-real-brand", run); expect(run).toHaveBeenCalledWith("the-real-brand"); }); }); describe("summarizeDrains", () => { it("sums every counter across all per-brand summaries", () => { const totals = summarizeDrains([ { brandId: "a", considered: 5, synced: 3, skipped: 1, failed: 1, }, { brandId: "b", considered: 4, synced: 2, skipped: 1, failed: 1, skippedReason: "Not due", }, { brandId: "c", considered: 0, synced: 0, skipped: 0, failed: 0, error: "401", }, ]); expect(totals).toEqual({ considered: 9, synced: 5, skipped: 2, failed: 2, }); }); it("returns zeros for an empty batch", () => { expect(summarizeDrains([])).toEqual({ considered: 0, synced: 0, skipped: 0, failed: 0, }); }); });