Files
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

175 lines
5.1 KiB
TypeScript

import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'fs';
import os from 'os';
import path from 'path';
import { recordUsage, readUsageRecords, type UsageRecordInput } from '@/lib/server/usage-storage';
let tmpDir: string;
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'usage-test-'));
});
afterEach(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
const llmInput: UsageRecordInput = {
kind: 'llm',
source: 'scene-content',
providerId: 'openai',
modelId: 'claude-sonnet-4-6',
modelString: 'openai:claude-sonnet-4-6',
usage: {
inputTokens: 100,
outputTokens: 50,
cacheReadTokens: 0,
cacheCreationTokens: 0,
reasoningTokens: 0,
},
};
describe('recordUsage — LLM', () => {
it('appends a jsonl line with token counts and no cost fields', async () => {
await recordUsage(llmInput, { baseDir: tmpDir });
const records = await readUsageRecords({ baseDir: tmpDir });
expect(records).toHaveLength(1);
expect(records[0].kind).toBe('llm');
expect(records[0].modelId).toBe('claude-sonnet-4-6');
expect(records[0].inputTokens).toBe(100);
expect(records[0].outputTokens).toBe(50);
// No cost fields exist anymore.
expect(records[0]).not.toHaveProperty('totalCostUsd');
expect(records[0]).not.toHaveProperty('costNull');
expect(records[0].id).toBeTruthy();
expect(records[0].createdAt).toBeGreaterThan(0);
});
it('defaults kind to llm when omitted', async () => {
const { kind: _omit, ...noKind } = llmInput;
await recordUsage(noKind, { baseDir: tmpDir });
const records = await readUsageRecords({ baseDir: tmpDir });
expect(records[0].kind).toBe('llm');
});
it('skips writing when there are no billable tokens', async () => {
await recordUsage(
{
...llmInput,
usage: {
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheCreationTokens: 0,
reasoningTokens: 0,
},
},
{ baseDir: tmpDir },
);
expect(await readUsageRecords({ baseDir: tmpDir })).toHaveLength(0);
});
});
describe('recordUsage — multimodal (non-token)', () => {
it('records TTS characters', async () => {
await recordUsage(
{
kind: 'tts',
unit: 'character',
source: 'tts',
providerId: 'minimax-tts',
modelId: 'speech-2.8-hd',
modelString: 'minimax-tts:speech-2.8-hd',
quantity: 1234,
},
{ baseDir: tmpDir },
);
const [r] = await readUsageRecords({ baseDir: tmpDir });
expect(r.kind).toBe('tts');
expect(r.quantity).toBe(1234);
expect(r.unit).toBe('character');
expect(r.inputTokens).toBe(0);
});
it('records image count and video seconds', async () => {
await recordUsage(
{
kind: 'image',
unit: 'image',
source: 'image',
providerId: 'minimax-image',
modelId: 'image-01',
modelString: 'minimax-image:image-01',
quantity: 1,
},
{ baseDir: tmpDir },
);
await recordUsage(
{
kind: 'video',
unit: 'second',
source: 'video',
providerId: 'minimax-video',
modelId: 'MiniMax-Hailuo-2.3',
modelString: 'minimax-video:MiniMax-Hailuo-2.3',
quantity: 6,
},
{ baseDir: tmpDir },
);
const records = await readUsageRecords({ baseDir: tmpDir });
expect(records).toHaveLength(2);
expect(records.find((r) => r.kind === 'image')?.quantity).toBe(1);
expect(records.find((r) => r.kind === 'video')?.quantity).toBe(6);
});
it('skips non-token usage with quantity <= 0', async () => {
await recordUsage(
{
kind: 'image',
unit: 'image',
source: 'image',
providerId: 'minimax-image',
modelId: 'image-01',
modelString: 'minimax-image:image-01',
quantity: 0,
},
{ baseDir: tmpDir },
);
expect(await readUsageRecords({ baseDir: tmpDir })).toHaveLength(0);
});
it('never throws on a write failure (fire-and-forget)', async () => {
const filePath = path.join(tmpDir, 'not-a-dir');
await fs.writeFile(filePath, 'x');
await expect(recordUsage(llmInput, { baseDir: filePath })).resolves.toBeUndefined();
});
});
describe('readUsageRecords', () => {
it('returns empty array when no usage dir exists', async () => {
expect(await readUsageRecords({ baseDir: path.join(tmpDir, 'nope') })).toEqual([]);
});
it('treats a legacy row without kind as llm and ignores legacy cost fields', async () => {
const dir = tmpDir;
const legacy = {
id: 'x',
createdAt: 1,
source: 'scene-content',
providerId: 'openai',
modelId: 'm',
modelString: 'openai:m',
inputTokens: 5,
outputTokens: 3,
cacheReadTokens: 0,
cacheCreationTokens: 0,
reasoningTokens: 0,
totalCostUsd: 0.01, // legacy field, must be ignored
};
await fs.writeFile(path.join(dir, '2026-01.jsonl'), JSON.stringify(legacy) + '\n');
const [r] = await readUsageRecords({ baseDir: dir });
expect(r.kind).toBe('llm');
expect(r.inputTokens).toBe(5);
});
});