import { describe, it, expect } from 'vitest'; import { runUpdate, currentMonthFile, todayISO } from './cost-stop-hook.mjs'; import { PRICING } from './cost-pricing.mjs'; describe('runUpdate (pure)', () => { const ep = (date, cost) => ({ timestamps: { started_at: `${date}T12:00:00.000Z` }, task_cost: cost, }); it('adds today entry to empty existing state', () => { const r = runUpdate({ episodes: [ep('2026-05-28', { classifier_input_tokens: 1_000_000 })], dateISO: '2026-05-28', existing: {}, pricing: PRICING, }); expect(r['2026-05-28']).toBeDefined(); expect(r['2026-05-28'].classifier_usd).toBeCloseTo(3, 6); expect(r['2026-05-28'].episode_count).toBe(1); }); it('overwrites today entry but preserves other-day entries', () => { const r = runUpdate({ episodes: [ep('2026-05-28', { classifier_input_tokens: 1_000_000 })], dateISO: '2026-05-28', existing: { '2026-05-27': { total_usd: 9.99, episode_count: 5 } }, pricing: PRICING, }); expect(r['2026-05-27']).toEqual({ total_usd: 9.99, episode_count: 5 }); expect(r['2026-05-28'].classifier_usd).toBeCloseTo(3, 6); }); it('writes zero-episode-count day when no episodes match', () => { const r = runUpdate({ episodes: [], dateISO: '2026-05-28', existing: {}, pricing: PRICING, }); expect(r['2026-05-28'].episode_count).toBe(0); expect(r['2026-05-28'].total_usd).toBe(0); }); it('does not mutate the existing input object', () => { const existing = { '2026-05-27': { total_usd: 1 } }; runUpdate({ episodes: [], dateISO: '2026-05-28', existing, pricing: PRICING }); expect(Object.keys(existing)).toEqual(['2026-05-27']); expect(existing['2026-05-27']).toEqual({ total_usd: 1 }); }); }); describe('todayISO', () => { it('returns YYYY-MM-DD string for a Date instance', () => { expect(todayISO(new Date('2026-05-28T15:30:00.000Z'))).toBe('2026-05-28'); }); it('uses UTC date components', () => { // Date that's late evening UTC but next day in local — ISO must reflect UTC. expect(todayISO(new Date('2026-05-28T23:59:59.000Z'))).toBe('2026-05-28'); }); }); describe('currentMonthFile', () => { it('returns docs/observer/episodes-YYYY-MM.jsonl', () => { const p = currentMonthFile(new Date('2026-05-28T12:00:00.000Z'), '/repo'); expect(p.replace(/\\/g, '/')).toBe('/repo/docs/observer/episodes-2026-05.jsonl'); }); it('pads month with leading zero', () => { const p = currentMonthFile(new Date('2026-01-05T12:00:00.000Z'), '/repo'); expect(p.replace(/\\/g, '/')).toBe('/repo/docs/observer/episodes-2026-01.jsonl'); }); it('uses stateDir param (config-seam); дефолт docs/observer', () => { const now = new Date('2026-06-15T12:00:00.000Z'); expect(currentMonthFile(now, '/repo', '.claude/brain-state').replace(/\\/g, '/')) .toBe('/repo/.claude/brain-state/episodes-2026-06.jsonl'); expect(currentMonthFile(now, '/repo').replace(/\\/g, '/')) .toBe('/repo/docs/observer/episodes-2026-06.jsonl'); }); });