test(ai-review): 補使用量統計與 resolve 安全與邊界測試

This commit is contained in:
Jeffery
2026-06-23 12:58:43 +08:00
parent f23d015e62
commit a27555b35a
3 changed files with 247 additions and 0 deletions
+196
View File
@@ -0,0 +1,196 @@
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import {
extractUsage,
recordUsage,
getRunUsage,
resetRunUsage,
recordRateLimit,
getRateLimit,
resetRateLimit,
resolveRemainingPercent,
fetchAccountQuota,
formatUsageStats,
formatUsageStatsLine,
} from './usage.js';
describe('extractUsage', () => {
it('parses OpenAI-compatible usage', () => {
const u = extractUsage({ usage: { prompt_tokens: 100, completion_tokens: 20, total_tokens: 120 } });
assert.deepEqual(u, { promptTokens: 100, completionTokens: 20, totalTokens: 120 });
});
it('parses OpenAI Responses input/output tokens and derives total', () => {
const u = extractUsage({ usage: { input_tokens: 50, output_tokens: 10 } });
assert.deepEqual(u, { promptTokens: 50, completionTokens: 10, totalTokens: 60 });
});
it('parses Gemini usageMetadata', () => {
const u = extractUsage({ usageMetadata: { promptTokenCount: 30, candidatesTokenCount: 5, totalTokenCount: 35 } });
assert.deepEqual(u, { promptTokens: 30, completionTokens: 5, totalTokens: 35 });
});
it('parses Ollama native eval counts', () => {
const u = extractUsage({ prompt_eval_count: 12, eval_count: 8 });
assert.deepEqual(u, { promptTokens: 12, completionTokens: 8, totalTokens: 20 });
});
it('parses OpenCode tokens from info.tokens', () => {
const u = extractUsage({ info: { tokens: { input: 7, output: 3 } } });
assert.deepEqual(u, { promptTokens: 7, completionTokens: 3, totalTokens: 10 });
});
it('returns null when no usage info is present', () => {
assert.equal(extractUsage({ choices: [{ message: { content: 'hi' } }] }), null);
assert.equal(extractUsage(null), null);
});
});
describe('recordUsage / getRunUsage', () => {
beforeEach(() => resetRunUsage());
it('accumulates across calls and counts every call', () => {
recordUsage({ usage: { prompt_tokens: 10, completion_tokens: 2, total_tokens: 12 } });
recordUsage({ usage: { prompt_tokens: 5, completion_tokens: 1, total_tokens: 6 } });
recordUsage({ parts: [] }); // no usage → still counts as a call
assert.deepEqual(getRunUsage(), { calls: 3, promptTokens: 15, completionTokens: 3, totalTokens: 18 });
});
it('returns a copy, not the internal object', () => {
const a = getRunUsage();
a.calls = 999;
assert.equal(getRunUsage().calls, 0);
});
});
describe('fetchAccountQuota', () => {
it('reads OpenRouter credits via injected get', async () => {
const get = async (url, opts) => {
assert.match(url, /openrouter\.ai\/api\/v1\/auth\/key$/);
assert.equal(opts.headers.Authorization, 'Bearer sk-or-xxx');
return { data: { data: { usage: 12.4, limit: 100, limit_remaining: 87.6 } } };
};
const q = await fetchAccountQuota('openai', { apiKeys: ['sk-or-xxx'], baseURL: 'https://openrouter.ai/api/v1' }, { get });
assert.deepEqual(q, { available: true, used: 12.4, limit: 100, remaining: 87.6, currency: 'USD', source: 'openrouter' });
});
it('derives remaining when OpenRouter omits limit_remaining', async () => {
const get = async () => ({ data: { data: { usage: 10, limit: 50 } } });
const q = await fetchAccountQuota('openai', { apiKeys: ['k'], baseURL: 'https://openrouter.ai/api/v1' }, { get });
assert.equal(q.remaining, 40);
});
it('reports unavailable for plain OpenAI (no openrouter)', async () => {
const q = await fetchAccountQuota('openai', { apiKeys: ['k'], baseURL: 'https://api.openai.com/v1' }, { get: async () => { throw new Error('should not call'); } });
assert.equal(q.available, false);
assert.match(q.reason, /API key 無法取得/);
});
it('reports 不適用 for local platforms', async () => {
assert.equal((await fetchAccountQuota('ollama', {})).available, false);
assert.equal((await fetchAccountQuota('opencode', {})).available, false);
});
it('degrades gracefully when the quota call throws', async () => {
const get = async () => { throw new Error('network down'); };
const q = await fetchAccountQuota('openai', { apiKeys: ['k'], baseURL: 'https://openrouter.ai/api/v1' }, { get });
assert.deepEqual(q, { available: false, reason: 'network down' });
});
it('reports unsupported provider', async () => {
const q = await fetchAccountQuota('mystery', {});
assert.equal(q.available, false);
assert.match(q.reason, /未支援/);
});
});
describe('recordRateLimit / getRateLimit', () => {
beforeEach(() => resetRateLimit());
it('captures OpenAI-style token rate-limit headers (case-insensitive)', () => {
recordRateLimit({ 'X-RateLimit-Remaining-Tokens': '190000', 'X-RateLimit-Limit-Tokens': '200000' });
assert.deepEqual(getRateLimit(), { hasData: true, remaining: 190000, limit: 200000, kind: 'tokens' });
});
it('captures Anthropic-style token rate-limit headers', () => {
recordRateLimit({ 'anthropic-ratelimit-tokens-remaining': '8000', 'anthropic-ratelimit-tokens-limit': '10000' });
assert.deepEqual(getRateLimit(), { hasData: true, remaining: 8000, limit: 10000, kind: 'tokens' });
});
it('falls back to request-dimension headers when token headers are absent', () => {
recordRateLimit({ 'x-ratelimit-remaining-requests': '45', 'x-ratelimit-limit-requests': '60' });
assert.deepEqual(getRateLimit(), { hasData: true, remaining: 45, limit: 60, kind: 'requests' });
});
it('ignores responses without rate-limit headers', () => {
recordRateLimit({ 'content-type': 'application/json' });
assert.equal(getRateLimit().hasData, false);
});
});
describe('resolveRemainingPercent', () => {
it('prefers account quota when a finite limit exists', () => {
const pct = resolveRemainingPercent({ available: true, used: 12.4, limit: 100, remaining: 87.6, currency: 'USD' }, { hasData: true, remaining: 1, limit: 10, kind: 'tokens' });
assert.equal(pct.percent, 87.6);
assert.equal(pct.basis, '帳號額度');
});
it('derives remaining from used when quota.remaining is absent', () => {
const pct = resolveRemainingPercent({ available: true, used: 25, limit: 100, currency: 'USD' }, null);
assert.equal(pct.percent, 75);
});
it('falls back to rate-limit window percent when quota has no limit', () => {
const pct = resolveRemainingPercent({ available: false, reason: 'x' }, { hasData: true, remaining: 150000, limit: 200000, kind: 'tokens' });
assert.equal(pct.percent, 75);
assert.match(pct.basis, /速率配額(當前視窗,token/);
});
it('returns null percent with a reason when nothing is available', () => {
const pct = resolveRemainingPercent({ available: false, reason: '本地服務,無帳號額度概念' }, { hasData: false });
assert.equal(pct.percent, null);
assert.equal(pct.reason, '本地服務,無帳號額度概念');
});
it('explains an unlimited account cannot yield a percent', () => {
const pct = resolveRemainingPercent({ available: true, used: 5, limit: null }, { hasData: false });
assert.equal(pct.percent, null);
assert.match(pct.reason, /無上限/);
});
});
describe('formatUsageStats', () => {
const usage = { calls: 7, promptTokens: 18432, completionTokens: 2107, totalTokens: 20539 };
it('renders token table and remaining percent from account quota', () => {
const out = formatUsageStats('openai', 'gpt-4o-mini', usage, { available: true, used: 12.4, limit: 100, remaining: 87.6, currency: 'USD' }, null);
assert.match(out, /## 🤖 AI 助理使用量/);
assert.match(out, /18,432 \| 2,107 \| 20,539/);
assert.match(out, /共 7 次呼叫/);
assert.match(out, /剩餘可用 \*\*87.6%\*\*(帳號額度:USD 87.6 \/ USD 100/);
});
it('renders remaining percent from rate-limit window when quota is unavailable', () => {
const out = formatUsageStats('claude', 'sonnet', usage, { available: false, reason: 'r' }, { hasData: true, remaining: 150000, limit: 200000, kind: 'tokens' });
assert.match(out, /剩餘可用 \*\*75%\*\*(速率配額(當前視窗,token):150,000 \/ 200,000/);
});
it('explains when no percentage can be computed', () => {
const out = formatUsageStats('ollama', 'llama3', usage, { available: false, reason: '本地服務,無帳號額度概念' }, { hasData: false });
assert.match(out, /剩餘可用:無法計算百分比(本地服務,無帳號額度概念)/);
});
});
describe('formatUsageStatsLine', () => {
const usage = { calls: 3, promptTokens: 100, completionTokens: 20, totalTokens: 120 };
it('summarises tokens and remaining percent on one line', () => {
const line = formatUsageStatsLine('openai', 'gpt-4o-mini', usage, { available: true, used: 1, limit: 10, remaining: 9, currency: 'USD' }, null);
assert.equal(line, '本次 openai/gpt-4o-mini: 提示100 + 回應20 = 120 token3 次呼叫);剩餘可用: 90%(帳號額度 USD 9/USD 10');
});
it('notes when remaining percent cannot be computed', () => {
const line = formatUsageStatsLine('ollama', 'llama3', usage, { available: false, reason: '本地服務,無帳號額度概念' }, { hasData: false });
assert.match(line, /;剩餘可用: 無法計算(本地服務,無帳號額度概念)/);
});
});