test: add gemini and xai parser unit tests
This commit is contained in:
parent
07a544c50d
commit
988618e165
4 changed files with 153 additions and 0 deletions
|
|
@ -17,6 +17,8 @@ import { getProviders } from './providers/index.js';
|
|||
import { probeTeamsProvider } from './providers/anthropic-teams.js';
|
||||
import { getApiAteamStatus } from './providers/anthropic-api.js';
|
||||
import { probeShelleyProxy } from './providers/shelley-proxy.js';
|
||||
import { probeGeminiProvider } from './providers/gemini.js';
|
||||
import { probeXaiProvider } from './providers/xai.js';
|
||||
import { generateReport, getSeverity } from './report.js';
|
||||
import { logRun } from './logger.js';
|
||||
|
||||
|
|
@ -40,6 +42,10 @@ async function probeProvider(p) {
|
|||
result = await probeShelleyProxy(p.name, p.baseUrl);
|
||||
} else if (p.type === 'api-direct') {
|
||||
result = getApiAteamStatus();
|
||||
} else if (p.type === 'gemini-api') {
|
||||
result = await probeGeminiProvider(p.name, p.baseUrl, p.apiKey);
|
||||
} else if (p.type === 'xai-api') {
|
||||
result = await probeXaiProvider(p.name, p.baseUrl, p.apiKey);
|
||||
} else {
|
||||
result = { type: 'unknown', status: 'skipped', severity: 'unknown' };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,15 @@ function classifyProvider(name, config) {
|
|||
if (name === 'shelley-proxy') return 'shelley-proxy';
|
||||
if (name === 'api-ateam') return 'api-direct';
|
||||
if (config.api === 'anthropic-messages' && name.startsWith('team-')) return 'teams-direct';
|
||||
// Gemini — classify by baseUrl or name
|
||||
const baseUrl = (config.baseUrl || '').toLowerCase();
|
||||
if (baseUrl.includes('generativelanguage.googleapis.com') || name.toLowerCase().includes('gemini')) {
|
||||
return 'gemini-api';
|
||||
}
|
||||
// x.ai — classify by baseUrl or name
|
||||
if (baseUrl.includes('x.ai') || name.toLowerCase().includes('xai') || name.toLowerCase().includes('grok')) {
|
||||
return 'xai-api';
|
||||
}
|
||||
return null; // skip (zai, etc.)
|
||||
}
|
||||
|
||||
|
|
|
|||
43
report.js
43
report.js
|
|
@ -19,6 +19,21 @@ export function getSeverity(provider) {
|
|||
if (tokenPct > 0.85) return 'warning';
|
||||
return 'ok';
|
||||
}
|
||||
if (provider.type === 'gemini-api') {
|
||||
if (provider.status === 'exhausted') return 'critical';
|
||||
if (provider.status === 'ok') return 'ok';
|
||||
return 'unknown';
|
||||
}
|
||||
if (provider.type === 'xai-api') {
|
||||
if (provider.status === 'rate_limited') return 'critical';
|
||||
if (provider.status === 'ok') {
|
||||
const reqPct = provider.requests_remaining / provider.requests_limit;
|
||||
const tokPct = provider.tokens_remaining / provider.tokens_limit;
|
||||
if ((reqPct < 0.1) || (tokPct < 0.1)) return 'warning';
|
||||
return 'ok';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +119,34 @@ export function generateReport(result) {
|
|||
}
|
||||
} else if (p.type === 'api-direct') {
|
||||
detail = p.message || 'No billing data available';
|
||||
} else if (p.type === 'gemini-api') {
|
||||
if (p.status === 'invalid_key') {
|
||||
detail = 'Invalid API key (401)';
|
||||
} else if (p.status === 'no_key') {
|
||||
detail = 'No API key configured';
|
||||
} else if (p.status === 'exhausted') {
|
||||
const retryIn = p.retry_delay_seconds ? formatDuration(p.retry_delay_seconds) : '?';
|
||||
const violated = p.quota_violations?.length || 0;
|
||||
detail = `EXHAUSTED — retry in ${retryIn} | ${violated} quota(s) violated`;
|
||||
} else if (p.status === 'ok') {
|
||||
detail = 'Reachable — no quota depth visible';
|
||||
} else if (p.status === 'error') {
|
||||
detail = `Error: ${p.message || 'unknown'}`;
|
||||
}
|
||||
} else if (p.type === 'xai-api') {
|
||||
if (p.status === 'no_key') {
|
||||
detail = 'No API key configured';
|
||||
} else if (p.status === 'invalid_key') {
|
||||
detail = 'Invalid API key (401)';
|
||||
} else if (p.status === 'rate_limited') {
|
||||
detail = 'Rate limited (429)';
|
||||
} else if (p.status === 'ok') {
|
||||
const reqPct = p.requests_limit ? `${p.requests_remaining}/${p.requests_limit} req` : '';
|
||||
const tokPct = p.tokens_limit ? ` | ${p.tokens_remaining?.toLocaleString()}/${p.tokens_limit?.toLocaleString()} tok` : '';
|
||||
detail = reqPct + tokPct || 'OK';
|
||||
} else if (p.status === 'error') {
|
||||
detail = `Error: ${p.message || 'unknown'}`;
|
||||
}
|
||||
} else {
|
||||
detail = p.message || '';
|
||||
}
|
||||
|
|
|
|||
95
test.js
95
test.js
|
|
@ -8,6 +8,8 @@ import { readFileSync, existsSync } from 'fs';
|
|||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import { parseGeminiBody } from './providers/gemini.js';
|
||||
import { parseXaiHeaders } from './providers/xai.js';
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
|
@ -320,6 +322,99 @@ assert('log file exists after default run', existsSync(logFileAfter));
|
|||
const logLines = readFileSync(logFileAfter, 'utf-8').trim().split('\n');
|
||||
assert('log file has at least 2 entries', logLines.length >= 2);
|
||||
|
||||
// ── 11. Gemini provider parser ────────────────────────────────────────────────
|
||||
console.log('\n── 11. Gemini provider parser ──────────────────────────────────');
|
||||
|
||||
const gemini429Body = {
|
||||
error: {
|
||||
code: 429,
|
||||
status: 'RESOURCE_EXHAUSTED',
|
||||
details: [
|
||||
{
|
||||
'@type': 'type.googleapis.com/google.rpc.QuotaFailure',
|
||||
violations: [
|
||||
{
|
||||
quotaMetric: 'generativelanguage.googleapis.com/generate_content_free_tier_requests',
|
||||
quotaId: 'GenerateRequestsPerMinutePerProjectPerModel-FreeTier',
|
||||
quotaDimensions: { location: 'global', model: 'gemini-2.0-flash' },
|
||||
},
|
||||
{
|
||||
quotaMetric: 'generativelanguage.googleapis.com/generate_content_free_tier_requests',
|
||||
quotaId: 'GenerateRequestsPerDayPerProjectPerModel-FreeTier',
|
||||
quotaDimensions: { location: 'global', model: 'gemini-2.0-flash' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{ '@type': 'type.googleapis.com/google.rpc.RetryInfo', retryDelay: '43s' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
assert('providers/gemini.js exists', existsSync(join(root, 'providers/gemini.js')));
|
||||
|
||||
const g429 = parseGeminiBody(gemini429Body, 429);
|
||||
assert('gemini 429: status = exhausted', g429.status === 'exhausted');
|
||||
assert('gemini 429: type = gemini-api', g429.type === 'gemini-api');
|
||||
assert('gemini 429: quota_violations is array', Array.isArray(g429.quota_violations));
|
||||
assert('gemini 429: quota_violations has 2 entries', g429.quota_violations.length === 2);
|
||||
assert('gemini 429: retry_delay_seconds = 43', g429.retry_delay_seconds === 43);
|
||||
assert('gemini 429: severity = critical', g429.severity === 'critical');
|
||||
|
||||
const g200 = parseGeminiBody(null, 200);
|
||||
assert('gemini 200: status = ok', g200.status === 'ok');
|
||||
assert('gemini 200: severity = ok', g200.severity === 'ok');
|
||||
assert('gemini 200: quota_violations is empty array', Array.isArray(g200.quota_violations) && g200.quota_violations.length === 0);
|
||||
|
||||
const g401 = parseGeminiBody(null, 401);
|
||||
assert('gemini 401: status = invalid_key', g401.status === 'invalid_key');
|
||||
assert('gemini 401: severity = unknown', g401.severity === 'unknown');
|
||||
|
||||
// ── 12. x.ai provider parser ─────────────────────────────────────────────────
|
||||
console.log('\n── 12. x.ai provider parser ────────────────────────────────────');
|
||||
|
||||
class MockHeaders {
|
||||
constructor(map) { this._map = map; }
|
||||
get(name) { return this._map[name.toLowerCase()] ?? null; }
|
||||
}
|
||||
|
||||
const xai200Headers = new MockHeaders({
|
||||
'x-ratelimit-limit-requests': '1000',
|
||||
'x-ratelimit-remaining-requests': '998',
|
||||
'x-ratelimit-limit-tokens': '500000',
|
||||
'x-ratelimit-remaining-tokens': '499981',
|
||||
});
|
||||
const xai200HeadersNearFull = new MockHeaders({
|
||||
'x-ratelimit-limit-requests': '1000',
|
||||
'x-ratelimit-remaining-requests': '5',
|
||||
'x-ratelimit-limit-tokens': '500000',
|
||||
'x-ratelimit-remaining-tokens': '499981',
|
||||
});
|
||||
const xai429Headers = new MockHeaders({
|
||||
'x-ratelimit-remaining-requests': '0',
|
||||
});
|
||||
|
||||
assert('providers/xai.js exists', existsSync(join(root, 'providers/xai.js')));
|
||||
|
||||
const xNoKey = parseXaiHeaders(null, 200, null);
|
||||
assert('xai no key: status = no_key', xNoKey.status === 'no_key');
|
||||
assert('xai no key: severity = unknown', xNoKey.severity === 'unknown');
|
||||
|
||||
const x200 = parseXaiHeaders(xai200Headers, 200, 'xai-key');
|
||||
assert('xai 200: status = ok', x200.status === 'ok');
|
||||
assert('xai 200: requests_remaining = 998', x200.requests_remaining === 998);
|
||||
assert('xai 200: tokens_remaining = 499981', x200.tokens_remaining === 499981);
|
||||
assert('xai 200: severity = ok', x200.severity === 'ok');
|
||||
|
||||
const x200Near = parseXaiHeaders(xai200HeadersNearFull, 200, 'xai-key');
|
||||
assert('xai 200 near-full: severity = warning', x200Near.severity === 'warning');
|
||||
|
||||
const x429 = parseXaiHeaders(xai429Headers, 429, 'xai-key');
|
||||
assert('xai 429: status = rate_limited', x429.status === 'rate_limited');
|
||||
assert('xai 429: severity = critical', x429.severity === 'critical');
|
||||
|
||||
const x401 = parseXaiHeaders(null, 401, 'xai-key');
|
||||
assert('xai 401: status = invalid_key', x401.status === 'invalid_key');
|
||||
|
||||
// ── Results ──────────────────────────────────────────────────────────────────
|
||||
console.log('\n' + '═'.repeat(50));
|
||||
console.log(`Tests: ${passed + failed} | Passed: ${passed} | Failed: ${failed}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue