test(test 目錄): 將 12 個單元測試移至 app/test 並更新 npm test 指令

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-06-26 14:15:55 +08:00
co-authored by Claude Opus 4.8
parent 51fd223b48
commit 303104bb20
13 changed files with 16 additions and 16 deletions
+299
View File
@@ -0,0 +1,299 @@
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('respects an explicit total_tokens of 0 instead of summing', () => {
const u = extractUsage({ usage: { prompt_tokens: 10, completion_tokens: 2, total_tokens: 0 } });
assert.equal(u.totalTokens, 0);
});
it('returns null when no usage info is present', () => {
assert.equal(extractUsage({ choices: [{ message: { content: 'hi' } }] }), null);
assert.equal(extractUsage(null), null);
});
it('handles malformed usage payloads without throwing or NaN', () => {
assert.equal(extractUsage(undefined), null);
assert.equal(extractUsage('not-an-object'), null);
assert.equal(extractUsage({ usage: 'x' }), null); // usage 非物件
assert.equal(extractUsage({ usage: {} }), null); // 欄位缺失
// 非數字 token 欄位 → 一律以 0 計,最終無有效 usage → null(不會回傳 NaN
assert.equal(extractUsage({ usage: { prompt_tokens: 'abc', completion_tokens: null, total_tokens: 'x' } }), 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('does not treat spoofed openrouter hostnames as OpenRouter (no key leak)', async () => {
const get = async () => { throw new Error('should not be called for spoofed host'); };
for (const baseURL of ['https://openrouter.ai.evil.com/api/v1', 'https://evil.com/openrouter.ai']) {
const q = await fetchAccountQuota('openai', { apiKeys: ['sk-secret'], baseURL }, { get });
assert.equal(q.available, false);
assert.match(q.reason, /API key 無法取得/);
}
});
it('only accepts the exact openrouter.ai apex host (subdomains are not OpenRouter)', async () => {
const get = async () => { throw new Error('should not be called for non-apex host'); };
const q = await fetchAccountQuota('openai', { apiKeys: ['sk-secret'], baseURL: 'https://api.openrouter.ai/api/v1' }, { get });
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, /未支援/);
});
it('degrades gracefully when apiKeys is empty or undefined', async () => {
const get = async () => { throw new Error('should not be called'); };
// 空陣列 / 未提供 key 都不應丟錯,依平台回報無法取得或不適用
assert.equal((await fetchAccountQuota('openai', { apiKeys: [], baseURL: 'https://api.openai.com/v1' }, { get })).available, false);
assert.equal((await fetchAccountQuota('ollama', { apiKeys: [] }, { get })).available, false);
assert.equal((await fetchAccountQuota('claude', {}, { get })).available, false);
});
});
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, /無上限/);
});
it('returns null percent for non-finite or non-positive quota limits', () => {
for (const limit of [0, -5, Infinity, NaN, undefined]) {
const pct = resolveRemainingPercent({ available: true, used: 0, limit, remaining: limit, currency: 'USD' }, null);
assert.equal(pct.percent, null, `quota.limit=${limit} 應算不出百分比`);
}
});
it('returns null percent for non-finite or non-positive rate limits', () => {
for (const limit of [0, -1, Infinity, NaN]) {
const pct = resolveRemainingPercent({ available: false, reason: 'x' }, { hasData: true, remaining: limit, limit, kind: 'tokens' });
assert.equal(pct.percent, null, `rate.limit=${limit} 應算不出百分比`);
}
});
it('returns null percent when rate.remaining is null/undefined', () => {
for (const remaining of [null, undefined]) {
const pct = resolveRemainingPercent({ available: false, reason: 'x' }, { hasData: true, remaining, limit: 200000, kind: 'tokens' });
assert.equal(pct.percent, null, `rate.remaining=${remaining} 應算不出百分比`);
}
});
it('returns null percent when limit is finite but remaining is non-finite', () => {
const pct = resolveRemainingPercent({ available: true, used: 0, limit: 100, remaining: Infinity, currency: 'USD' }, null);
assert.equal(pct.percent, null);
});
it('does not divide by zero when quota.limit is 0', () => {
const pct = resolveRemainingPercent({ available: true, used: 5, limit: 0, currency: 'USD' }, { hasData: false });
assert.equal(pct.percent, null); // limit > 0 守衛擋掉除以零
assert.ok(typeof pct.reason === 'string' && pct.reason.length > 0);
});
it('reports 100% when remaining equals limit and 0% when remaining is 0', () => {
const full = resolveRemainingPercent({ available: true, used: 0, limit: 100, remaining: 100, currency: 'USD' }, null);
assert.equal(full.percent, 100);
const empty = resolveRemainingPercent({ available: true, used: 100, limit: 100, remaining: 0, currency: 'USD' }, null);
assert.equal(empty.percent, 0);
});
});
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, /;剩餘可用: 無法計算(本地服務,無帳號額度概念)/);
});
it('produces safe text (no NaN) when quota/rate carry invalid numbers', () => {
// quota.limit 為 NaN、rate.limit 為 NaN → 不應算出百分比、不得輸出 NaN
const line = formatUsageStatsLine('openai', 'm', usage,
{ available: true, used: 5, limit: NaN, currency: 'USD' },
{ hasData: true, remaining: NaN, limit: NaN, kind: 'tokens' });
assert.doesNotMatch(line, /NaN/);
assert.match(line, /剩餘可用: 無法計算/);
});
it('does not output Infinity/NaN/negative percent for invalid quota numbers', () => {
const ownUsage = { calls: 1, promptTokens: 1, completionTokens: 1, totalTokens: 2 };
for (const limit of [Infinity, 0, -5, NaN]) {
const line = formatUsageStatsLine('openai', 'm', ownUsage, { available: true, used: 0, limit, remaining: limit, currency: 'USD' }, null);
assert.doesNotMatch(line, /NaN|Infinity|-\d+%/);
assert.match(line, /剩餘可用: 無法計算/);
}
});
it('falls back to a valid rate percent when only the quota limit is invalid', () => {
const ownUsage = { calls: 1, promptTokens: 1, completionTokens: 1, totalTokens: 2 };
const line = formatUsageStatsLine('openai', 'm', ownUsage,
{ available: true, used: 0, limit: Infinity, currency: 'USD' }, // quota 無效
{ hasData: true, remaining: 150000, limit: 200000, kind: 'tokens' }); // rate 有效 → 75%
assert.match(line, /剩餘可用: 75%(速率配額/);
});
});