Files
2026-07-13 12:26:02 +08:00

118 lines
4.1 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { fetchVercelGatewayReport, vercelGateway } from '../../src/providers/vercel-gateway.js'
import { parseAllSessions, clearSessionCache } from '../../src/parser.js'
describe('vercel-gateway provider', () => {
const originalFetch = globalThis.fetch
const originalKey = process.env.AI_GATEWAY_API_KEY
beforeEach(() => {
process.env.AI_GATEWAY_API_KEY = 'test-key'
})
afterEach(() => {
globalThis.fetch = originalFetch
if (originalKey === undefined) delete process.env.AI_GATEWAY_API_KEY
else process.env.AI_GATEWAY_API_KEY = originalKey
vi.restoreAllMocks()
})
it('discovers a session when API key is set', async () => {
const sessions = await vercelGateway.discoverSessions()
expect(sessions).toHaveLength(1)
expect(sessions[0]?.provider).toBe('vercel-gateway')
})
it('returns empty discovery without API key', async () => {
delete process.env.AI_GATEWAY_API_KEY
delete process.env.VERCEL_OIDC_TOKEN
const sessions = await vercelGateway.discoverSessions()
expect(sessions).toEqual([])
})
it('maps report rows to parsed calls', async () => {
globalThis.fetch = vi.fn(async () => ({
ok: true,
json: async () => ({
results: [{
day: '2026-06-01',
model: 'anthropic/claude-sonnet-4.6',
total_cost: 1.25,
input_tokens: 1000,
output_tokens: 200,
request_count: 3,
}],
}),
})) as typeof fetch
const range = {
start: new Date('2026-06-01T00:00:00.000Z'),
end: new Date('2026-06-02T23:59:59.999Z'),
}
const rows = await fetchVercelGatewayReport(range)
expect(rows).toHaveLength(1)
const source = { path: 'vercel-ai-gateway:report', project: 'Vercel AI Gateway', provider: 'vercel-gateway' }
const seen = new Set<string>()
const calls = []
for await (const call of vercelGateway.createSessionParser(source, seen, range).parse()) {
calls.push(call)
}
expect(calls).toHaveLength(1)
expect(calls[0]?.costUSD).toBe(1.25)
expect(calls[0]?.model).toBe('anthropic/claude-sonnet-4.6')
})
})
describe('vercel-gateway end-to-end (parseAllSessions network path)', () => {
const originalFetch = globalThis.fetch
const originalKey = process.env.AI_GATEWAY_API_KEY
const originalCacheDir = process.env.CODEBURN_CACHE_DIR
let cacheDir: string
beforeEach(async () => {
cacheDir = await mkdtemp(join(tmpdir(), 'cb-vercel-cache-'))
process.env.CODEBURN_CACHE_DIR = cacheDir
process.env.AI_GATEWAY_API_KEY = 'test-key'
clearSessionCache()
})
afterEach(async () => {
globalThis.fetch = originalFetch
if (originalKey === undefined) delete process.env.AI_GATEWAY_API_KEY
else process.env.AI_GATEWAY_API_KEY = originalKey
if (originalCacheDir === undefined) delete process.env.CODEBURN_CACHE_DIR
else process.env.CODEBURN_CACHE_DIR = originalCacheDir
clearSessionCache()
vi.restoreAllMocks()
await rm(cacheDir, { recursive: true, force: true })
})
// Regression: the synthetic source path `vercel-ai-gateway:report` has no file
// on disk, so it was dropped by the fingerprintFile gate in parseProviderSources
// and the provider always reported $0. Network providers must survive that gate
// and contribute their fetched cost through the real aggregation pipeline.
it('network source survives the fingerprint gate and contributes cost', async () => {
globalThis.fetch = vi.fn(async () => ({
ok: true,
json: async () => ({
results: [
{ day: '2026-06-01', model: 'openai/gpt-4o', total_cost: 12.34, input_tokens: 1000, output_tokens: 500, request_count: 3 },
],
}),
})) as typeof fetch
const range = {
start: new Date('2026-05-01T00:00:00.000Z'),
end: new Date('2026-06-09T23:59:59.999Z'),
}
const projects = await parseAllSessions(range, 'vercel-gateway')
const total = projects.reduce((sum, p) => sum + p.totalCostUSD, 0)
expect(total).toBeCloseTo(12.34, 2)
})
})