Files
Profa 2253d05618 feat: flatten project structure — archive v2, promote v3 to root
Remove the nested v3/ subdirectory by moving all active source code,
tests, configs, and assets directly to the project root. Archive v2/
(dead code, 847MB) to v2-archive.tar.gz.

Key changes:
- Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/
- Move v3/tsconfig.json, v3/vitest.config.ts to root
- Merge v3/package.json into root package.json (ESM, scripts, deps, exports)
- Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/
- Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes
- Fix all init installer path resolution (3-level-up → 2-level-up)
- Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe"
- Update settings.json ADR/DDD directories to docs/implementation/
- Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks)
- Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces
- Update 15 guidance shards: v3/src/ → src/ path references
- Fix sync interfaces: remove v3/ and v2/ path references
- Fix test path calculations (process.cwd() + '..' no longer needed)
- Fix flaky perf test threshold (500ms → 1000ms for CI environments)
- Fix complexity-analyzer test to match ADR-051 keyword-based eligibility

Build passes, 17,901 tests pass, CLI and MCP bundles verified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:35:41 +00:00

414 lines
11 KiB
TypeScript

/**
* Agentic QE v3 - Rate Limiter Tests
* Tests for token bucket rate limiting
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
RateLimiter,
SlidingWindowRateLimiter,
createRateLimiter,
createStrictRateLimiter,
createSlidingWindowLimiter,
} from '../../../../src/mcp/security/rate-limiter';
describe('RateLimiter', () => {
let limiter: RateLimiter;
beforeEach(() => {
limiter = createRateLimiter({
tokensPerSecond: 10,
maxBurst: 20,
perClient: true,
});
});
afterEach(() => {
limiter.dispose();
});
describe('basic rate limiting', () => {
it('should allow requests within limit', () => {
const result = limiter.check('client1');
expect(result.allowed).toBe(true);
expect(result.remaining).toBeLessThanOrEqual(20);
});
it('should track remaining tokens', () => {
// Use some tokens
limiter.check('client1');
limiter.check('client1');
limiter.check('client1');
const result = limiter.check('client1');
expect(result.remaining).toBe(16); // 20 - 4 = 16
});
it('should deny requests when tokens exhausted', () => {
// Exhaust all tokens
for (let i = 0; i < 20; i++) {
limiter.check('client1');
}
const result = limiter.check('client1');
expect(result.allowed).toBe(false);
expect(result.remaining).toBe(0);
expect(result.retryAfter).toBeDefined();
});
it('should provide retry-after time', () => {
// Exhaust tokens
for (let i = 0; i < 20; i++) {
limiter.check('client1');
}
const result = limiter.check('client1');
expect(result.retryAfter).toBeGreaterThan(0);
expect(result.retryAfter).toBeLessThanOrEqual(1000); // Max 1 second wait for 10 tokens/sec
});
});
describe('per-client limiting', () => {
it('should maintain separate buckets per client', () => {
// Exhaust client1's tokens
for (let i = 0; i < 20; i++) {
limiter.check('client1');
}
// client2 should still have tokens
const result = limiter.check('client2');
expect(result.allowed).toBe(true);
expect(result.remaining).toBe(19);
});
it('should use global bucket when no client specified', () => {
// Create limiter without per-client limiting
const globalLimiter = createRateLimiter({
tokensPerSecond: 10,
maxBurst: 20,
perClient: false,
});
const result1 = globalLimiter.check();
const result2 = globalLimiter.check();
expect(result1.remaining - result2.remaining).toBe(1);
globalLimiter.dispose();
});
});
describe('token refill', () => {
it('should refill tokens over time', async () => {
vi.useFakeTimers();
// Use some tokens
for (let i = 0; i < 10; i++) {
limiter.check('client1');
}
const beforeRefill = limiter.check('client1');
expect(beforeRefill.remaining).toBe(9);
// Wait for tokens to refill (100ms = 1 token at 10/sec)
vi.advanceTimersByTime(1000);
const afterRefill = limiter.check('client1');
expect(afterRefill.remaining).toBeGreaterThan(9);
vi.useRealTimers();
});
it('should not exceed max burst', async () => {
vi.useFakeTimers();
// Wait a long time - should still cap at maxBurst
vi.advanceTimersByTime(10000);
const result = limiter.check('client1');
expect(result.remaining).toBe(19); // maxBurst - 1
expect(result.remaining).toBeLessThanOrEqual(20);
vi.useRealTimers();
});
});
describe('consume multiple tokens', () => {
it('should consume multiple tokens at once', () => {
const result = limiter.consume('client1', undefined, 5);
expect(result.allowed).toBe(true);
expect(result.remaining).toBe(15);
});
it('should deny if not enough tokens', () => {
const result = limiter.consume('client1', undefined, 25);
expect(result.allowed).toBe(false);
});
});
describe('async wait', () => {
it('should wait for tokens to become available', async () => {
vi.useFakeTimers();
// Exhaust tokens
for (let i = 0; i < 20; i++) {
limiter.check('client1');
}
const waitPromise = limiter.wait('client1');
// Advance time to allow token refill
vi.advanceTimersByTime(200);
const result = await waitPromise;
expect(result.allowed).toBe(true);
vi.useRealTimers();
});
});
describe('endpoint-specific limits', () => {
it('should apply endpoint-specific limits', () => {
limiter.addEndpointLimit({
pattern: /\/api\/admin/,
tokensPerSecond: 5,
maxBurst: 10,
});
const adminResult = limiter.check('client1', '/api/admin/users');
// Token bucket may have slight timing variations, use range check
expect(adminResult.remaining).toBeGreaterThanOrEqual(8); // ~10 maxBurst - 1 (±1 for timing)
expect(adminResult.remaining).toBeLessThanOrEqual(10);
// Normal endpoint has separate bucket from admin endpoint
const normalResult = limiter.check('client1', '/api/users');
expect(normalResult.remaining).toBeGreaterThanOrEqual(18); // ~20 maxBurst - 1 (±1 for timing)
expect(normalResult.remaining).toBeLessThanOrEqual(20);
});
it('should clear endpoint limits', () => {
limiter.addEndpointLimit({
pattern: '/test',
tokensPerSecond: 1,
maxBurst: 2,
});
limiter.clearEndpointLimits();
const result = limiter.check('client1', '/test');
expect(result.remaining).toBe(19); // Uses default maxBurst
});
});
describe('reset', () => {
it('should reset client bucket', () => {
// Use tokens
for (let i = 0; i < 10; i++) {
limiter.check('client1');
}
limiter.resetClient('client1');
const result = limiter.check('client1');
expect(result.remaining).toBe(19); // Full bucket
});
it('should reset all buckets', () => {
limiter.check('client1');
limiter.check('client2');
limiter.check('client3');
limiter.reset();
const stats = limiter.getStats();
expect(stats.activeBuckets).toBe(1); // Only global bucket
expect(stats.totalRequests).toBe(0);
});
});
describe('statistics', () => {
it('should track request statistics', () => {
limiter.check('client1');
limiter.check('client2');
limiter.check('client1');
const stats = limiter.getStats();
expect(stats.totalRequests).toBe(3);
expect(stats.allowedRequests).toBe(3);
expect(stats.deniedRequests).toBe(0);
});
it('should track denied requests', () => {
// Exhaust tokens
for (let i = 0; i < 20; i++) {
limiter.check('client1');
}
// Try more requests
limiter.check('client1');
limiter.check('client1');
const stats = limiter.getStats();
expect(stats.deniedRequests).toBe(2);
});
it('should track active buckets', () => {
limiter.check('client1');
limiter.check('client2');
limiter.check('client3');
const stats = limiter.getStats();
expect(stats.activeBuckets).toBe(4); // 3 clients + 1 global
});
it('should get client-specific stats', () => {
limiter.check('client1');
limiter.check('client1');
limiter.check('client1');
const clientStats = limiter.getClientStats('client1');
expect(clientStats).not.toBeNull();
expect(clientStats?.totalRequests).toBe(3);
});
it('should return null for unknown client', () => {
const stats = limiter.getClientStats('unknown');
expect(stats).toBeNull();
});
});
describe('rate limit headers', () => {
it('should provide rate limit headers', () => {
const result = limiter.check('client1');
expect(result.headers['X-RateLimit-Limit']).toBe(20);
expect(result.headers['X-RateLimit-Remaining']).toBe(19);
expect(result.headers['X-RateLimit-Reset']).toBeDefined();
});
it('should include Retry-After when denied', () => {
// Exhaust tokens
for (let i = 0; i < 20; i++) {
limiter.check('client1');
}
const result = limiter.check('client1');
expect(result.headers['Retry-After']).toBeDefined();
expect(result.headers['Retry-After']).toBeGreaterThan(0);
});
});
});
describe('SlidingWindowRateLimiter', () => {
let limiter: SlidingWindowRateLimiter;
beforeEach(() => {
limiter = createSlidingWindowLimiter(1000, 10); // 10 requests per second
});
afterEach(() => {
limiter.dispose();
});
describe('basic functionality', () => {
it('should allow requests within limit', () => {
const result = limiter.check('client1');
expect(result.allowed).toBe(true);
expect(result.remaining).toBe(9);
});
it('should deny requests exceeding limit', () => {
// Make 10 requests
for (let i = 0; i < 10; i++) {
limiter.check('client1');
}
const result = limiter.check('client1');
expect(result.allowed).toBe(false);
expect(result.remaining).toBe(0);
});
it('should track per-client limits', () => {
// Use client1's quota
for (let i = 0; i < 10; i++) {
limiter.check('client1');
}
// client2 should still have quota
const result = limiter.check('client2');
expect(result.allowed).toBe(true);
});
});
describe('sliding window behavior', () => {
it('should allow requests after window slides', async () => {
vi.useFakeTimers();
// Use all quota
for (let i = 0; i < 10; i++) {
limiter.check('client1');
}
// Wait for window to slide
vi.advanceTimersByTime(1100);
const result = limiter.check('client1');
expect(result.allowed).toBe(true);
vi.useRealTimers();
});
});
describe('reset', () => {
it('should reset specific client', () => {
for (let i = 0; i < 5; i++) {
limiter.check('client1');
}
limiter.reset('client1');
const result = limiter.check('client1');
expect(result.remaining).toBe(9);
});
it('should reset all clients', () => {
limiter.check('client1');
limiter.check('client2');
limiter.reset();
const result1 = limiter.check('client1');
const result2 = limiter.check('client2');
expect(result1.remaining).toBe(9);
expect(result2.remaining).toBe(9);
});
});
});
describe('createStrictRateLimiter', () => {
it('should create limiter with strict defaults', () => {
const limiter = createStrictRateLimiter();
// Should have lower limits (10 req/s, 20 burst)
const stats = limiter.getStats();
expect(stats.activeBuckets).toBe(1);
// Exhaust 20 tokens
for (let i = 0; i < 20; i++) {
limiter.check('client1');
}
const result = limiter.check('client1');
expect(result.allowed).toBe(false);
limiter.dispose();
});
});