103 lines
3.6 KiB
TypeScript
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);
|
|
});
|
|
});
|