mirror of
https://github.com/HeyPuter/puter.git
synced 2026-07-08 08:12:15 +00:00
vis: make metering service alarm only go off on actual abuse (#3208)
This commit is contained in:
@@ -321,6 +321,79 @@ describe('MeteringService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── overuse alarm ────────────────────────────────────────────────
|
||||
|
||||
describe('overuse alarm', () => {
|
||||
const wasOveruseAlarmed = (alarmSpy: ReturnType<typeof vi.spyOn>) =>
|
||||
alarmSpy.mock.calls.some(
|
||||
(call) =>
|
||||
typeof call[0] === 'string' &&
|
||||
call[0].includes('usage exceeded'),
|
||||
);
|
||||
|
||||
it('does not alarm when a single large request crosses the limit in one shot', async () => {
|
||||
const bigActor: Actor = { user: makeUser() };
|
||||
const sub = await target.getActorSubscription(bigActor);
|
||||
const alarmSpy = vi.spyOn(server.clients.alarm, 'create');
|
||||
|
||||
// Previous usage was 0 (under the allowance) — one big request that
|
||||
// blows past the limit is legitimate and shouldn't page.
|
||||
await target.incrementUsage(
|
||||
bigActor,
|
||||
'ai:chat',
|
||||
1,
|
||||
sub.monthUsageAllowance * 5,
|
||||
);
|
||||
|
||||
expect(wasOveruseAlarmed(alarmSpy)).toBe(false);
|
||||
alarmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('alarms on the next expense once already at or past the limit', async () => {
|
||||
const overActor: Actor = { user: makeUser() };
|
||||
const sub = await target.getActorSubscription(overActor);
|
||||
|
||||
// First expense takes them exactly to the limit — no alarm yet.
|
||||
await target.incrementUsage(
|
||||
overActor,
|
||||
'ai:chat',
|
||||
1,
|
||||
sub.monthUsageAllowance,
|
||||
);
|
||||
|
||||
// Spy only on the *second* expense, which lands while already at
|
||||
// the limit — this is the one that should flag.
|
||||
const alarmSpy = vi.spyOn(server.clients.alarm, 'create');
|
||||
await target.incrementUsage(overActor, 'ai:chat', 1, 1_000);
|
||||
|
||||
expect(alarmSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('usage exceeded'),
|
||||
expect.stringContaining('exceeded their usage allowance'),
|
||||
expect.objectContaining({ totalUsage: expect.any(Number) }),
|
||||
);
|
||||
alarmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does not alarm while purchased credits still cover the overage', async () => {
|
||||
const creditActor: Actor = { user: makeUser() };
|
||||
const sub = await target.getActorSubscription(creditActor);
|
||||
await target.updateAddonCredit(creditActor.user.uuid, 5_000_000);
|
||||
|
||||
await target.incrementUsage(
|
||||
creditActor,
|
||||
'ai:chat',
|
||||
1,
|
||||
sub.monthUsageAllowance,
|
||||
);
|
||||
|
||||
const alarmSpy = vi.spyOn(server.clients.alarm, 'create');
|
||||
await target.incrementUsage(creditActor, 'ai:chat', 1, 1_000);
|
||||
|
||||
expect(wasOveruseAlarmed(alarmSpy)).toBe(false);
|
||||
alarmSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// ── batchIncrementUsages ─────────────────────────────────────────
|
||||
|
||||
describe('batchIncrementUsages', () => {
|
||||
|
||||
@@ -893,20 +893,22 @@ export class MeteringService extends PuterService {
|
||||
incrementCost,
|
||||
} = ctx;
|
||||
|
||||
const allowedMultiple = Math.floor(
|
||||
actorUsages.total / actorSubscription.monthUsageAllowance,
|
||||
);
|
||||
const previousMultiple = Math.floor(
|
||||
(actorUsages.total - incrementCost) /
|
||||
actorSubscription.monthUsageAllowance,
|
||||
);
|
||||
const isOver2x = allowedMultiple >= 2;
|
||||
const crossedThreshold = previousMultiple < allowedMultiple;
|
||||
const allowance = actorSubscription.monthUsageAllowance;
|
||||
// No metered allowance to exceed (e.g. unlimited policies) — nothing to flag.
|
||||
if (!(allowance > 0)) return;
|
||||
|
||||
// Only alarm if the actor was ALREADY at or past their allowance before
|
||||
// this expense arrived. A single large request that crosses the limit in
|
||||
// one shot (previous usage still under the allowance) is legitimate and
|
||||
// shouldn't page — we only flag sustained overuse, i.e. the next expense
|
||||
// that lands while they're already over.
|
||||
const previousUsage = actorUsages.total - incrementCost;
|
||||
const wasAlreadyOverLimit = previousUsage >= allowance;
|
||||
const hasNoAddonCredit =
|
||||
(actorAddons.purchasedCredits || 0) <=
|
||||
(actorAddons.consumedPurchaseCredits || 0);
|
||||
|
||||
if (!(isOver2x && crossedThreshold && hasNoAddonCredit)) return;
|
||||
if (!(wasAlreadyOverLimit && hasNoAddonCredit)) return;
|
||||
|
||||
this.clients.alarm.create(
|
||||
`metering usage exceeded by user: ${actor.user?.username}`,
|
||||
|
||||
Reference in New Issue
Block a user