feat(freebuff): enhance Freebucks handling and pricing logic across services
This commit is contained in:
@@ -294,6 +294,50 @@ describe("freebuff session pre-flight", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rate_limited with a resetAt locks the account until the daily Pacific reset (skip the day, not retry loops)", async () => {
|
||||
const resetAt = "2099-01-01T00:00:00.000Z";
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({
|
||||
status: "rate_limited",
|
||||
resetAt,
|
||||
retryAfterMs: 1234,
|
||||
freebucksShortfall: { price: 15, balance: 0 },
|
||||
message: "Freebucks exhausted",
|
||||
}),
|
||||
);
|
||||
await expect(requestSession("tok-1", "deepseek/deepseek-v4-flash", null)).rejects.toMatchObject({
|
||||
status: 429,
|
||||
resetsAtMs: Date.parse(resetAt),
|
||||
});
|
||||
});
|
||||
|
||||
it("spend_limited falls back to retryAfterMs when resetAt is absent", async () => {
|
||||
const retryAfterMs = 90 * 60 * 1000;
|
||||
fetchMock.mockResolvedValue(
|
||||
jsonResponse({ status: "spend_limited", retryAfterMs, message: "daily spend cap" }),
|
||||
);
|
||||
const before = Date.now();
|
||||
try {
|
||||
await requestSession("tok-1", "deepseek/deepseek-v4-flash", null);
|
||||
throw new Error("should have rejected");
|
||||
} catch (error) {
|
||||
expect(error.status).toBe(429);
|
||||
expect(error.resetsAtMs).toBeGreaterThanOrEqual(before + retryAfterMs - 1000);
|
||||
expect(error.resetsAtMs).toBeLessThanOrEqual(before + retryAfterMs + 1000);
|
||||
}
|
||||
});
|
||||
|
||||
it("rate_limited without any reset hint stays a plain error (transient cooldown path)", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ status: "rate_limited", message: "busy" }));
|
||||
try {
|
||||
await requestSession("tok-1", "deepseek/deepseek-v4-flash", null);
|
||||
throw new Error("should have rejected");
|
||||
} catch (error) {
|
||||
expect(error.status).toBeUndefined();
|
||||
expect(error.resetsAtMs).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("throws a friendly error on country_blocked", async () => {
|
||||
fetchMock.mockResolvedValue(jsonResponse({ status: "country_blocked" }));
|
||||
await expect(ensureSession("tok-1", "deepseek/deepseek-v4-flash", null)).rejects.toThrow(
|
||||
|
||||
@@ -119,6 +119,115 @@ describe("getUsageForProvider(freebuff)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("reports the Freebucks daily pool under each priced model for metered accounts (no rateLimitsByModel)", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
status: "none",
|
||||
accessTier: "limited",
|
||||
rateLimitsByModel: {},
|
||||
freebucks: {
|
||||
balance: 10,
|
||||
daily: { limit: 25, spent: 15, remaining: 10, resetAt: "2026-09-08T07:00:00.000Z" },
|
||||
wallet: { balance: 0, monthlyBonus: 0 },
|
||||
spend: { limitUsd: 4, resetAt: "2026-09-08T07:00:00.000Z" },
|
||||
monthly: { limitUsd: 10, spentUsd: 3.5, remainingUsd: 6.5, resetAt: "2026-10-01T07:00:00.000Z" },
|
||||
planId: null,
|
||||
prices: { "deepseek/deepseek-v4-flash": 15, "z-ai/glm-5.3-flash": 5 },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "freebuff",
|
||||
accessToken: "tok-1",
|
||||
});
|
||||
|
||||
expect(usage.message).toBeUndefined();
|
||||
expect(usage.quotas["deepseek/deepseek-v4-flash"]).toMatchObject({
|
||||
used: 15,
|
||||
total: 25,
|
||||
resetAt: "2026-09-08T07:00:00.000Z",
|
||||
recurring: true,
|
||||
unlimited: false,
|
||||
price: 15,
|
||||
displayName: "DeepSeek V4 Flash",
|
||||
});
|
||||
expect(usage.quotas["z-ai/glm-5.3-flash"]).toMatchObject({
|
||||
used: 15,
|
||||
total: 25,
|
||||
price: 5,
|
||||
displayName: "GLM 5.3 Flash",
|
||||
});
|
||||
// No session pools → only the freebucks-derived rows exist.
|
||||
expect(Object.keys(usage.quotas)).toEqual([
|
||||
"deepseek/deepseek-v4-flash",
|
||||
"z-ai/glm-5.3-flash",
|
||||
]);
|
||||
// Account summary rides the response for the card header (server data,
|
||||
// nothing hardcoded client-side).
|
||||
expect(usage.freebucks).toEqual({
|
||||
balance: 10,
|
||||
daily: {
|
||||
limit: 25,
|
||||
spent: 15,
|
||||
remaining: 10,
|
||||
resetAt: "2026-09-08T07:00:00.000Z",
|
||||
},
|
||||
wallet: { balance: 0 },
|
||||
monthly: { remainingUsd: 6.5, limitUsd: 10, resetAt: "2026-10-01T07:00:00.000Z" },
|
||||
});
|
||||
});
|
||||
|
||||
it("folds the server's announced priceChanges into the live price (promos expire without a client release)", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
status: "none",
|
||||
accessTier: "full",
|
||||
rateLimitsByModel: {},
|
||||
freebucks: {
|
||||
balance: 20,
|
||||
daily: { limit: 25, spent: 5, remaining: 20, resetAt: "2026-09-08T07:00:00.000Z" },
|
||||
wallet: { balance: 0, monthlyBonus: 0 },
|
||||
planId: null,
|
||||
prices: { "upstage/solar-pro4": 0 },
|
||||
priceNotices: { "upstage/solar-pro4": "0 Freebucks · Labor Day weekend (through Sep 7 PT)" },
|
||||
priceChanges: [
|
||||
{
|
||||
at: "2026-01-01T00:00:00.000Z", // already due
|
||||
modelId: "upstage/solar-pro4",
|
||||
price: 5,
|
||||
tagline: "Limited-time trial",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "freebuff",
|
||||
accessToken: "tok-1",
|
||||
});
|
||||
|
||||
// The due change is folded in: price 5 + new tagline, schedule emptied.
|
||||
expect(usage.quotas["upstage/solar-pro4"]).toMatchObject({
|
||||
price: 5,
|
||||
priceNote: "Limited-time trial",
|
||||
displayName: "Solar Pro 4",
|
||||
});
|
||||
});
|
||||
|
||||
it("attaches no price to legacy session-quota rows", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(jsonResponse(PRE_JOIN));
|
||||
|
||||
const usage = await getUsageForProvider({
|
||||
provider: "freebuff",
|
||||
accessToken: "tok-1",
|
||||
});
|
||||
|
||||
expect(usage.quotas["deepseek/deepseek-v4-flash"].price).toBeUndefined();
|
||||
expect(usage.freebucks).toBeUndefined();
|
||||
});
|
||||
|
||||
it("surfaces a re-login message on 401", async () => {
|
||||
proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "unauthorized" }, 401));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user