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', ]); }); });