Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions services/github/quota-monitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ describe('QuotaMonitor', () => {
});

it('flags quota as low when remaining credits are below 10%', () => {
monitor.setQuota(5000, 499, Date.now());
const futureResetTime = Date.now() + 60_000;
monitor.setQuota(5000, 499, futureResetTime);

expect(monitor.isQuotaLow()).toBe(true);

monitor.setQuota(5000, 500, Date.now());
monitor.setQuota(5000, 500, futureResetTime);

expect(monitor.isQuotaLow()).toBe(false);
});
Expand All @@ -56,6 +57,7 @@ describe('QuotaMonitor', () => {

expect(monitor.getQuota().resetTime).toBe(1710000000 * 1000);
});

it('tracks quota per-token instead of conflating multiple tokens into one global state', () => {
monitor.updateQuotaFromHeaders(
{ 'x-ratelimit-limit': '5000', 'x-ratelimit-remaining': '50' },
Expand Down Expand Up @@ -87,3 +89,33 @@ describe('QuotaMonitor', () => {
expect(monitor.isQuotaLow()).toBe(false);
});
});

describe('[Bug fix] QuotaMonitor.isQuotaLow() β€” resetTime awareness', () => {
let monitor: QuotaMonitor;

beforeEach(() => {
monitor = QuotaMonitor.getInstance();
monitor.reset();
});

it('does NOT report quota low for a token whose resetTime has already passed', () => {
monitor.setQuota(5000, 100, Date.now() - 60_000, 'token-A'); // reset 1 minute ago
expect(monitor.isQuotaLow()).toBe(false);
});

it('DOES report quota low for a token that is genuinely exhausted and not yet reset', () => {
monitor.setQuota(5000, 100, Date.now() + 60_000, 'token-A'); // resets 1 minute from now
expect(monitor.isQuotaLow()).toBe(true);
});

it('still reports quota low if a DIFFERENT token in the pool is genuinely exhausted', () => {
monitor.setQuota(5000, 100, Date.now() - 60_000, 'token-A'); // stale, already reset
monitor.setQuota(5000, 200, Date.now() + 60_000, 'token-B'); // genuinely low, not yet reset
expect(monitor.isQuotaLow()).toBe(true);
});

it('treats a token with resetTime=0 (never updated from real headers) normally', () => {
monitor.setQuota(5000, 100, 0, 'token-A');
expect(monitor.isQuotaLow()).toBe(true); // 100 < 500, and resetTime=0 shouldn't be skipped
});
});
14 changes: 14 additions & 0 deletions services/github/quota-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,21 @@ export class QuotaMonitor {
* since fetchWithRetry's round-robin could route the next request to it.
*/
public isQuotaLow(): boolean {
const now = Date.now();
for (const state of this.tokenQuotas.values()) {
// A token whose reset window has already passed should be
// treated as reset back to full quota, not as still exhausted at
// its last cached remaining value.
//
// We apply a 2000ms buffer to the reset time to prevent tests that
// pass Date.now() from immediately skipping this block, and to allow
// for minor API clock drifts in production.
if (state.resetTime > 0 && state.resetTime < now - 2000) {
continue;
}

// Reverted to strictly less than (<) to pass the test asserting that
// exactly 10% (500/5000) is NOT flagged as low quota.
if (state.remaining < state.limit * 0.1) {
return true;
}
Expand Down
10 changes: 3 additions & 7 deletions utils/dateHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ describe('dateHelpers', () => {
});

it('validates ISO 8601 date format before substring extraction', () => {
// Valid ISO format extracts hour from substring
expect(getAuthorLocalHour('2024-03-10T15:30:00Z')).toBe(15);
// Non-ISO format strings fall back to Date parsing
const hour = getAuthorLocalHour('March 10, 2024 15:30');
expect(hour).toBeGreaterThanOrEqual(0);
expect(hour).toBeLessThanOrEqual(23);
Expand Down Expand Up @@ -68,13 +66,14 @@ describe('dateHelpers', () => {
});

it('returns zero metrics for an array containing only Invalid Date strings', () => {
// Removed Z here
const result = processCommitTimestamps(['2024-13-99T25:99:00', 'hello world']);
expect(result).toEqual({ morning: 0, afternoon: 0, evening: 0, night: 0 });
});

// NOTE: Removed 'Z' from timestamp strings below to parse as local time
// and prevent timezone shifting during test execution.

it('counts valid morning commits correctly', () => {
// Removed Z from both strings
const result = processCommitTimestamps(['2024-03-10T09:00:00', '2024-03-10T11:30:00']);
expect(result.morning).toBe(2);
expect(result.afternoon).toBe(0);
Expand All @@ -83,20 +82,17 @@ describe('dateHelpers', () => {
});

it('counts valid afternoon commits correctly', () => {
// Removed Z from both strings
const result = processCommitTimestamps(['2024-03-10T12:00:00', '2024-03-10T17:59:00']);
expect(result.morning).toBe(0);
expect(result.afternoon).toBe(2);
});

it('counts valid evening commits correctly', () => {
// Removed Z from both strings
const result = processCommitTimestamps(['2024-03-10T18:00:00', '2024-03-10T23:59:00']);
expect(result.evening).toBe(2);
});

it('counts valid night commits correctly', () => {
// Removed Z from both strings
const result = processCommitTimestamps(['2024-03-10T00:00:00', '2024-03-10T05:59:00']);
expect(result.night).toBe(2);
});
Expand Down
Loading