157 lines
5.0 KiB
JavaScript
157 lines
5.0 KiB
JavaScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
vi.mock('../../opencode/auth.js', () => ({
|
|
readAuthFile: () => ({ deepseek: { key: 'test-token' } }),
|
|
}));
|
|
|
|
import { fetchQuota } from './deepseek.js';
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
const mockResponse = (body, init = {}) => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => body,
|
|
...init,
|
|
});
|
|
|
|
// Documented payload shape from https://api.deepseek.com/user/balance
|
|
const DOCUMENTED_PAYLOAD = {
|
|
is_available: true,
|
|
balance_infos: [
|
|
{
|
|
currency: 'USD',
|
|
total_balance: '7.54',
|
|
granted_balance: '0.00',
|
|
topped_up_balance: '7.54'
|
|
}
|
|
]
|
|
};
|
|
|
|
describe('DeepSeek quota provider', () => {
|
|
it('builds credits_balance window from documented USD payload (string balance)', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(DOCUMENTED_PAYLOAD)));
|
|
|
|
const result = await fetchQuota();
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.providerId).toBe('deepseek');
|
|
|
|
const window = result.usage.windows.credits_balance;
|
|
expect(window).toBeDefined();
|
|
expect(window.valueLabel).toBe('$7.54');
|
|
expect(window.usedPercent).toBeNull();
|
|
expect(window.windowSeconds).toBeNull();
|
|
expect(window.resetAt).toBeNull();
|
|
});
|
|
|
|
it('falls back to CNY entry when no USD entry is present', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
|
is_available: true,
|
|
balance_infos: [
|
|
{ currency: 'CNY', total_balance: '100.00', granted_balance: '0.00', topped_up_balance: '100.00' }
|
|
]
|
|
})));
|
|
|
|
const result = await fetchQuota();
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.usage.windows.credits_balance.valueLabel).toBe('¥100.00');
|
|
});
|
|
|
|
it('prefers the USD entry when both USD and CNY are present', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
|
is_available: true,
|
|
balance_infos: [
|
|
{ currency: 'CNY', total_balance: '100.00', granted_balance: '0.00', topped_up_balance: '100.00' },
|
|
{ currency: 'USD', total_balance: '3.55', granted_balance: '0.00', topped_up_balance: '3.55' }
|
|
]
|
|
})));
|
|
|
|
const result = await fetchQuota();
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.usage.windows.credits_balance.valueLabel).toBe('$3.55');
|
|
});
|
|
|
|
it('tolerates a numeric total_balance', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
|
is_available: true,
|
|
balance_infos: [{ currency: 'USD', total_balance: 12.5, granted_balance: 0, topped_up_balance: 12.5 }]
|
|
})));
|
|
|
|
const result = await fetchQuota();
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.usage.windows.credits_balance.valueLabel).toBe('$12.50');
|
|
});
|
|
|
|
it('maps 401 to session-expired error', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 401, json: async () => ({}) }));
|
|
|
|
const result = await fetchQuota();
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toBe('Session expired — please re-authenticate with DeepSeek');
|
|
});
|
|
|
|
it('maps 403 to session-expired error', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 403, json: async () => ({}) }));
|
|
|
|
const result = await fetchQuota();
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toBe('Session expired — please re-authenticate with DeepSeek');
|
|
});
|
|
|
|
it('reports invalid-response on JSON parse failure', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => { throw new SyntaxError('Unexpected token'); },
|
|
}));
|
|
|
|
const result = await fetchQuota();
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toBe('Invalid response from provider');
|
|
});
|
|
|
|
it('reports a normalized timeout error', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new DOMException('The operation timed out.', 'TimeoutError')));
|
|
|
|
const result = await fetchQuota();
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.error).toBe('Request timed out');
|
|
});
|
|
|
|
it('returns no-quota-data on a 200 payload with no usable balance', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
|
is_available: true,
|
|
balance_infos: [{ currency: 'USD', total_balance: '', granted_balance: '0.00', topped_up_balance: '0.00' }]
|
|
})));
|
|
|
|
const result = await fetchQuota();
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.configured).toBe(true);
|
|
expect(result.error).toBe('No quota data in response');
|
|
expect(result.usage).toBeNull();
|
|
});
|
|
|
|
it('keeps a literal zero balance as a valid valueLabel', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
|
is_available: true,
|
|
balance_infos: [{ currency: 'USD', total_balance: '0.00', granted_balance: '0.00', topped_up_balance: '0.00' }]
|
|
})));
|
|
|
|
const result = await fetchQuota();
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.usage.windows.credits_balance.valueLabel).toBe('$0.00');
|
|
});
|
|
});
|