mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
acb5973186
## New Features ### LLM Providers - GroqProvider: Free tier (14.4k req/day), streaming, rate limiting - GitHubModelsProvider: Codespaces auto-detection, GITHUB_TOKEN auth ### Provider Health Monitoring - ProviderHealthMonitor: Circuit breaker pattern (closed → half-open → open) - QuotaManager: Per-provider quota tracking with daily/minute limits - HybridRouterHealthIntegration: Health-aware routing with fallback ### CLI Commands - `aqe providers status`: Health dashboard - `aqe providers list`: List configured providers - `aqe providers test`: Test provider connectivity - `aqe providers switch`: Switch default provider - `aqe providers quota`: Show quota usage ## Tests - 144 new tests for Phase 3-4 components ## Documentation - Updated README with Provider Health Monitoring section - Updated CHANGELOG with v2.6.5 features - Updated implementation plan to COMPLETE status Closes #169 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
265 lines
8.3 KiB
TypeScript
265 lines
8.3 KiB
TypeScript
/**
|
|
* Tests for Provider CLI Commands
|
|
*
|
|
* Tests the provider status command and health dashboard functionality.
|
|
*/
|
|
|
|
import { ProviderStatusCommand, ProviderStatusReport } from '../../src/cli/commands/providers/status';
|
|
|
|
describe('ProviderStatusCommand', () => {
|
|
beforeAll(() => {
|
|
// Use fake timers to avoid Jest timeout issues with setInterval
|
|
jest.useFakeTimers();
|
|
});
|
|
|
|
afterAll(() => {
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
// Clean up any previous state
|
|
ProviderStatusCommand.cleanup();
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
ProviderStatusCommand.cleanup();
|
|
jest.clearAllTimers();
|
|
});
|
|
|
|
describe('execute', () => {
|
|
it('should generate a status report', async () => {
|
|
const report = await ProviderStatusCommand.execute({ json: true });
|
|
|
|
expect(report).toBeDefined();
|
|
expect(report.timestamp).toBeDefined();
|
|
expect(report.overallHealth).toBeDefined();
|
|
expect(report.providers).toBeDefined();
|
|
expect(report.summary).toBeDefined();
|
|
});
|
|
|
|
it('should include summary statistics', async () => {
|
|
const report = await ProviderStatusCommand.execute({ json: true });
|
|
|
|
expect(report.summary).toHaveProperty('total');
|
|
expect(report.summary).toHaveProperty('healthy');
|
|
expect(report.summary).toHaveProperty('degraded');
|
|
expect(report.summary).toHaveProperty('unhealthy');
|
|
expect(report.summary).toHaveProperty('circuitOpen');
|
|
});
|
|
|
|
it('should register default providers', async () => {
|
|
const report = await ProviderStatusCommand.execute({ json: true });
|
|
|
|
// Should have at least some default providers registered
|
|
expect(report.summary.total).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should include provider details', async () => {
|
|
const report = await ProviderStatusCommand.execute({ json: true });
|
|
|
|
for (const provider of report.providers) {
|
|
expect(provider.id).toBeDefined();
|
|
expect(provider.name).toBeDefined();
|
|
expect(typeof provider.healthy).toBe('boolean');
|
|
expect(provider.circuitState).toMatch(/^(closed|open|half-open)$/);
|
|
expect(typeof provider.latency).toBe('number');
|
|
expect(typeof provider.errorRate).toBe('number');
|
|
expect(typeof provider.availability).toBe('number');
|
|
expect(provider.lastCheck).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it('should calculate overall health correctly', async () => {
|
|
const report = await ProviderStatusCommand.execute({ json: true });
|
|
|
|
expect(report.overallHealth).toMatch(/^(healthy|degraded|unhealthy|unknown)$/);
|
|
|
|
// Verify overall health matches summary
|
|
if (report.summary.total === 0) {
|
|
expect(report.overallHealth).toBe('unknown');
|
|
} else if (report.summary.healthy === report.summary.total) {
|
|
expect(report.overallHealth).toBe('healthy');
|
|
} else if (report.summary.healthy > 0) {
|
|
expect(report.overallHealth).toBe('degraded');
|
|
} else {
|
|
expect(report.overallHealth).toBe('unhealthy');
|
|
}
|
|
});
|
|
|
|
it('should work with detailed option', async () => {
|
|
const report = await ProviderStatusCommand.execute({ detailed: true, json: true });
|
|
|
|
expect(report).toBeDefined();
|
|
expect(report.providers).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('provider health checks', () => {
|
|
// Save original env
|
|
const originalEnv = process.env;
|
|
|
|
beforeEach(() => {
|
|
process.env = { ...originalEnv };
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
it('should detect Groq based on GROQ_API_KEY', async () => {
|
|
process.env.GROQ_API_KEY = 'test-key';
|
|
|
|
const report = await ProviderStatusCommand.execute({ json: true });
|
|
const groq = report.providers.find(p => p.id === 'groq');
|
|
|
|
expect(groq).toBeDefined();
|
|
expect(groq?.healthy).toBe(true);
|
|
});
|
|
|
|
it('should detect missing GROQ_API_KEY', async () => {
|
|
delete process.env.GROQ_API_KEY;
|
|
|
|
const report = await ProviderStatusCommand.execute({ json: true });
|
|
const groq = report.providers.find(p => p.id === 'groq');
|
|
|
|
expect(groq).toBeDefined();
|
|
expect(groq?.healthy).toBe(false);
|
|
expect(groq?.lastError).toContain('GROQ_API_KEY');
|
|
});
|
|
|
|
it('should detect Claude based on ANTHROPIC_API_KEY', async () => {
|
|
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
|
|
const report = await ProviderStatusCommand.execute({ json: true });
|
|
const claude = report.providers.find(p => p.id === 'claude');
|
|
|
|
expect(claude).toBeDefined();
|
|
expect(claude?.healthy).toBe(true);
|
|
});
|
|
|
|
it('should detect GitHub Models in Codespaces', async () => {
|
|
process.env.CODESPACES = 'true';
|
|
|
|
const report = await ProviderStatusCommand.execute({ json: true });
|
|
const github = report.providers.find(p => p.id === 'github-models');
|
|
|
|
expect(github).toBeDefined();
|
|
expect(github?.healthy).toBe(true);
|
|
});
|
|
|
|
it('should detect GitHub Models with GITHUB_TOKEN', async () => {
|
|
delete process.env.CODESPACES;
|
|
process.env.GITHUB_TOKEN = 'test-token';
|
|
|
|
const report = await ProviderStatusCommand.execute({ json: true });
|
|
const github = report.providers.find(p => p.id === 'github-models');
|
|
|
|
expect(github).toBeDefined();
|
|
expect(github?.healthy).toBe(true);
|
|
});
|
|
|
|
it('should detect OpenRouter based on OPENROUTER_API_KEY', async () => {
|
|
process.env.OPENROUTER_API_KEY = 'test-key';
|
|
|
|
const report = await ProviderStatusCommand.execute({ json: true });
|
|
const openrouter = report.providers.find(p => p.id === 'openrouter');
|
|
|
|
expect(openrouter).toBeDefined();
|
|
expect(openrouter?.healthy).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('output formatting', () => {
|
|
it('should output JSON when json option is true', async () => {
|
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
|
|
const report = await ProviderStatusCommand.execute({ json: true });
|
|
|
|
expect(consoleSpy).toHaveBeenCalled();
|
|
|
|
// Find the JSON output (first call with stringified object)
|
|
const jsonCall = consoleSpy.mock.calls.find(call => {
|
|
const arg = call[0];
|
|
if (typeof arg === 'string' && arg.startsWith('{')) {
|
|
try {
|
|
JSON.parse(arg);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
|
|
expect(jsonCall).toBeDefined();
|
|
if (jsonCall) {
|
|
const parsed = JSON.parse(jsonCall[0]);
|
|
expect(parsed.timestamp).toBeDefined();
|
|
expect(parsed.overallHealth).toBeDefined();
|
|
}
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('cleanup', () => {
|
|
it('should cleanup resources', () => {
|
|
// Should not throw
|
|
expect(() => ProviderStatusCommand.cleanup()).not.toThrow();
|
|
});
|
|
|
|
it('should stop watch mode on cleanup', () => {
|
|
ProviderStatusCommand.cleanup();
|
|
// Calling cleanup again should be safe
|
|
expect(() => ProviderStatusCommand.cleanup()).not.toThrow();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Provider Status Report Structure', () => {
|
|
let report: ProviderStatusReport;
|
|
|
|
beforeAll(async () => {
|
|
jest.useFakeTimers();
|
|
report = await ProviderStatusCommand.execute({ json: true });
|
|
});
|
|
|
|
afterAll(() => {
|
|
ProviderStatusCommand.cleanup();
|
|
jest.clearAllTimers();
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it('should have valid timestamp format', () => {
|
|
expect(new Date(report.timestamp).toISOString()).toBe(report.timestamp);
|
|
});
|
|
|
|
it('should have valid overall health status', () => {
|
|
expect(['healthy', 'degraded', 'unhealthy', 'unknown']).toContain(report.overallHealth);
|
|
});
|
|
|
|
it('should have providers array', () => {
|
|
expect(Array.isArray(report.providers)).toBe(true);
|
|
});
|
|
|
|
it('should have summary object with required fields', () => {
|
|
expect(report.summary).toMatchObject({
|
|
total: expect.any(Number),
|
|
healthy: expect.any(Number),
|
|
degraded: expect.any(Number),
|
|
unhealthy: expect.any(Number),
|
|
circuitOpen: expect.any(Number)
|
|
});
|
|
});
|
|
|
|
it('should have consistent summary counts', () => {
|
|
const { total, healthy, degraded, unhealthy } = report.summary;
|
|
// All providers should be accounted for (some may be in multiple states)
|
|
expect(total).toBeGreaterThanOrEqual(0);
|
|
expect(healthy).toBeGreaterThanOrEqual(0);
|
|
expect(degraded).toBeGreaterThanOrEqual(0);
|
|
expect(unhealthy).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|