397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
120 lines
4.4 KiB
JavaScript
120 lines
4.4 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
||
import { episodeUsd, aggregateDay } from './cost-aggregator.mjs';
|
||
import { PRICING } from './cost-pricing.mjs';
|
||
|
||
describe('episodeUsd', () => {
|
||
it('returns all-zero object for empty/missing task_cost', () => {
|
||
const u = episodeUsd({}, PRICING);
|
||
expect(u.classifier_usd).toBe(0);
|
||
expect(u.self_assessment_usd).toBe(0);
|
||
expect(u.reviewer_subagent_usd).toBe(0);
|
||
expect(u.reviewer_direct_fallback_usd).toBe(0);
|
||
expect(u.self_retrospect_usd).toBe(0);
|
||
expect(u.total_usd).toBe(0);
|
||
});
|
||
|
||
it('includes judge_spend_usd in episodeUsd + total', () => {
|
||
const ep = { task_cost: { judge_spend_usd: 0.002 } };
|
||
const u = episodeUsd(ep, PRICING);
|
||
expect(u.judge_spend_usd).toBe(0.002);
|
||
expect(u.total_usd).toBeCloseTo(0.002, 9);
|
||
});
|
||
|
||
it('computes classifier_usd from sonnet pricing', () => {
|
||
const ep = { task_cost: { classifier_input_tokens: 1_000_000, classifier_output_tokens: 100_000 } };
|
||
const u = episodeUsd(ep, PRICING);
|
||
// 1M × $3 + 100k × $15 = $3 + $1.5 = $4.5
|
||
expect(u.classifier_usd).toBeCloseTo(4.5, 6);
|
||
});
|
||
|
||
it('computes self_assessment_usd from opus pricing', () => {
|
||
const ep = { task_cost: { self_assessment_input_tokens: 1_000_000, self_assessment_output_tokens: 10_000 } };
|
||
const u = episodeUsd(ep, PRICING);
|
||
// 1M × $15 + 10k × $75 = $15 + $0.75 = $15.75
|
||
expect(u.self_assessment_usd).toBeCloseTo(15.75, 6);
|
||
});
|
||
|
||
it('sums reviewer_subagent_usd and reviewer_direct_fallback_usd as-is', () => {
|
||
const ep = { task_cost: { reviewer_subagent_usd: 0.5, reviewer_direct_fallback_usd: 0.05 } };
|
||
const u = episodeUsd(ep, PRICING);
|
||
expect(u.reviewer_subagent_usd).toBe(0.5);
|
||
expect(u.reviewer_direct_fallback_usd).toBe(0.05);
|
||
});
|
||
|
||
it('reads self_retrospect_usd if present, defaults to 0', () => {
|
||
const ep1 = { task_cost: { self_retrospect_usd: 2.0 } };
|
||
expect(episodeUsd(ep1, PRICING).self_retrospect_usd).toBe(2.0);
|
||
const ep2 = { task_cost: {} };
|
||
expect(episodeUsd(ep2, PRICING).self_retrospect_usd).toBe(0);
|
||
});
|
||
|
||
it('total_usd is sum of all 5 components', () => {
|
||
const ep = { task_cost: {
|
||
classifier_input_tokens: 1_000_000, // $3
|
||
classifier_output_tokens: 0,
|
||
self_assessment_input_tokens: 0,
|
||
self_assessment_output_tokens: 100_000, // $7.5
|
||
reviewer_subagent_usd: 0.5,
|
||
reviewer_direct_fallback_usd: 0.05,
|
||
self_retrospect_usd: 1.0,
|
||
} };
|
||
const u = episodeUsd(ep, PRICING);
|
||
// 3 + 7.5 + 0.5 + 0.05 + 1.0 = 12.05
|
||
expect(u.total_usd).toBeCloseTo(12.05, 6);
|
||
});
|
||
});
|
||
|
||
describe('aggregateDay', () => {
|
||
const epOn = (date, cost) => ({
|
||
timestamps: { started_at: `${date}T12:00:00.000Z` },
|
||
task_cost: cost,
|
||
});
|
||
|
||
it('returns zero-day object when no episodes match date', () => {
|
||
const result = aggregateDay([epOn('2026-05-27', {})], '2026-05-28', PRICING);
|
||
expect(result.episode_count).toBe(0);
|
||
expect(result.total_usd).toBe(0);
|
||
});
|
||
|
||
it('counts only episodes whose started_at starts with the given date', () => {
|
||
const episodes = [
|
||
epOn('2026-05-28', { classifier_input_tokens: 1_000_000 }), // $3
|
||
epOn('2026-05-28', { self_assessment_output_tokens: 100_000 }), // $7.5
|
||
epOn('2026-05-27', { classifier_input_tokens: 1_000_000 }), // EXCLUDED
|
||
epOn('2026-05-29', { classifier_input_tokens: 1_000_000 }), // EXCLUDED
|
||
];
|
||
const r = aggregateDay(episodes, '2026-05-28', PRICING);
|
||
expect(r.episode_count).toBe(2);
|
||
expect(r.classifier_usd).toBeCloseTo(3, 6);
|
||
expect(r.self_assessment_usd).toBeCloseTo(7.5, 6);
|
||
expect(r.total_usd).toBeCloseTo(10.5, 6);
|
||
});
|
||
|
||
it('skips malformed episodes (no timestamps / non-string started_at)', () => {
|
||
const episodes = [
|
||
null,
|
||
{},
|
||
{ timestamps: null },
|
||
{ timestamps: { started_at: 42 } },
|
||
epOn('2026-05-28', { classifier_input_tokens: 1_000_000 }), // $3
|
||
];
|
||
const r = aggregateDay(episodes, '2026-05-28', PRICING);
|
||
expect(r.episode_count).toBe(1);
|
||
expect(r.classifier_usd).toBeCloseTo(3, 6);
|
||
});
|
||
|
||
it('returns object with exactly 8 keys (6 components + total + count)', () => {
|
||
const r = aggregateDay([], '2026-05-28', PRICING);
|
||
expect(Object.keys(r).sort()).toEqual([
|
||
'classifier_usd',
|
||
'episode_count',
|
||
'judge_spend_usd',
|
||
'reviewer_direct_fallback_usd',
|
||
'reviewer_subagent_usd',
|
||
'self_assessment_usd',
|
||
'self_retrospect_usd',
|
||
'total_usd',
|
||
]);
|
||
});
|
||
});
|