Files
brain/tools/cost-aggregator.test.mjs
T

120 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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',
]);
});
});