Files
triggerdotdev--trigger.dev/apps/webapp/test/billingLimitConvergeResolve.test.ts
2026-07-13 13:32:57 +08:00

103 lines
3.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { buildBillingLimitResolveDedupeKey } from "~/v3/services/billingLimit/billingLimitConstants";
import { classifyPendingBillingLimitResolveConvergeFailure } from "~/v3/services/billingLimit/billingLimitPendingResolveFailure.server";
import { runPendingBillingLimitResolves } from "~/v3/services/billingLimit/billingLimitPendingResolveCoordinator.server";
import type { PendingBillingLimitResolve } from "~/v3/services/billingLimit/billingLimitPendingResolve.types";
describe("billingLimitConvergeResolve", () => {
it("builds a stable dedupe key from org id and resolvedAt", () => {
expect(buildBillingLimitResolveDedupeKey("org_123", "2026-06-16T12:00:00.000Z")).toBe(
"billing-limit-resolve:org_123:2026-06-16T12:00:00.000Z"
);
});
it("classifies converge failures for ops triage", () => {
expect(classifyPendingBillingLimitResolveConvergeFailure("new_only")).toBe("cancel-failing");
expect(classifyPendingBillingLimitResolveConvergeFailure("queue")).toBe("converge-failing");
});
});
describe("runPendingBillingLimitResolves", () => {
const pending: PendingBillingLimitResolve = {
organizationId: "org_123",
resumeMode: "new_only",
resolvedAt: "2026-06-17T12:00:00.000Z",
};
it("keeps org pending and skips ack when converge throws (cancel-failing path)", async () => {
const completeCalls: string[] = [];
const stillPending = await runPendingBillingLimitResolves([pending], {
converge: async () => {
throw new Error("bulk cancel failed");
},
complete: async (organizationId) => {
completeCalls.push(organizationId);
return { completed: true };
},
});
expect(stillPending).toEqual(new Set(["org_123"]));
expect(completeCalls).toEqual([]);
});
it("keeps org pending when converge succeeds but ack throws (ack-only path)", async () => {
let convergeCalls = 0;
let completeCalls = 0;
const stillPending = await runPendingBillingLimitResolves(
[{ ...pending, resumeMode: "queue" }],
{
converge: async () => {
convergeCalls += 1;
},
complete: async () => {
completeCalls += 1;
throw new Error("resolve-complete unavailable");
},
}
);
expect(stillPending).toEqual(new Set(["org_123"]));
expect(convergeCalls).toBe(1);
expect(completeCalls).toBe(1);
});
it("keeps org pending when ack returns completed: false", async () => {
const stillPending = await runPendingBillingLimitResolves([pending], {
converge: async () => undefined,
complete: async () => ({ completed: false }),
});
expect(stillPending).toEqual(new Set(["org_123"]));
});
it("clears org from pending set when converge and ack both succeed", async () => {
const stillPending = await runPendingBillingLimitResolves([pending], {
converge: async () => undefined,
complete: async () => ({ completed: true }),
});
expect(stillPending).toEqual(new Set());
});
it("retries ack on a later tick after a transient ack failure", async () => {
let ackAttempts = 0;
const deps = {
converge: async () => undefined,
complete: async () => {
ackAttempts += 1;
if (ackAttempts === 1) {
throw new Error("resolve-complete unavailable");
}
return { completed: true };
},
};
expect(await runPendingBillingLimitResolves([pending], deps)).toEqual(new Set(["org_123"]));
expect(await runPendingBillingLimitResolves([pending], deps)).toEqual(new Set());
expect(ackAttempts).toBe(2);
});
});