import { test } from 'node:test'; import assert from 'node:assert/strict'; import { Store } from '../src/store/index.js'; import { computeCost, addUsage } from '../src/model/pricing.js'; test('pricing: computeCost 按模型×token 折算(USD/1M)', () => { // opus: in15 / out75 → 各 1M token = 15 + 75 = 90 assert.equal(computeCost('claude-opus-4-8', { input: 1_000_000, output: 1_000_000, cacheRead: 0, cacheWrite: 0 }), 90); // sonnet: in3 → 2M input = 6 assert.equal(computeCost('claude-sonnet-4-6', { input: 2_000_000, output: 0, cacheRead: 0, cacheWrite: 0 }), 6); // haiku: cacheRead 0.1 → 10M = 1 assert.equal(computeCost('claude-haiku-4-5', { input: 0, output: 0, cacheRead: 10_000_000, cacheWrite: 0 }), 1); // 未知模型 → 0(不阻断) assert.equal(computeCost('mystery-model', { input: 9_999_999, output: 0, cacheRead: 0, cacheWrite: 0 }), 0); // 前缀匹配(容忍日期后缀) assert.equal(computeCost('claude-opus-4-8-20260101', { input: 1_000_000, output: 0, cacheRead: 0, cacheWrite: 0 }), 15); }); test('pricing: addUsage 逐项累加', () => { const a = { input: 1, output: 2, cacheRead: 3, cacheWrite: 4 }; assert.deepEqual(addUsage(a, a), { input: 2, output: 4, cacheRead: 6, cacheWrite: 8 }); }); test('store: setRunUsage 累加成本 + costSummary 聚合', () => { const s = new Store(':memory:'); const p = s.createProject({ name: 'b', repoPath: '/tmp/b-' + Math.random() }); const t = s.createTask({ projectId: p.id, title: 'x', complexity: 'easy' }); const r = s.startRun(t.id, 'executor'); // 一个 run 内多次 CC 调用(executor + 复审)→ 累加 const c1 = s.setRunUsage(r.id, { input: 1_000_000, output: 0, cacheRead: 0, cacheWrite: 0 }, 'claude-opus-4-8'); assert.equal(c1, 15); const c2 = s.setRunUsage(r.id, { input: 0, output: 1_000_000, cacheRead: 0, cacheWrite: 0 }, 'claude-opus-4-8'); assert.equal(c2, 90); const sum = s.costSummary('month'); assert.equal(sum.total, 90); assert.equal(sum.byProject.length, 1); assert.equal(sum.byProject[0].projectId, p.id); assert.equal(sum.byProject[0].costUsd, 90); assert.equal(sum.byProject[0].tokens, 2_000_000); s.close(); }); test('store: usageDetail/costSummary 按 daemon 本地时区分桶(UTC+8 跨日不再偏移一天)', () => { const origTZ = process.env.TZ; process.env.TZ = 'Asia/Shanghai'; // 固定 UTC+8,制造 UTC 日 != 本地日 try { const s = new Store(':memory:'); const p = s.createProject({ name: 'tz', repoPath: '/tmp/tz-' + Math.random() }); const t = s.createTask({ projectId: p.id, title: 'x', complexity: 'easy' }); // —— 按天分桶:近 2 天内 UTC 18:00 的 run → 本地为次日 02:00,本地日应比 UTC 日晚一天 —— const r1 = s.startRun(t.id, 'executor'); s.setRunUsage(r1.id, { input: 1_000_000, output: 0, cacheRead: 0, cacheWrite: 0 }, 'claude-opus-4-8'); const base = new Date(); base.setUTCDate(base.getUTCDate() - 2); base.setUTCHours(18, 0, 0, 0); const startedAt = base.toISOString(); s.db.prepare('UPDATE runs SET started_at = ? WHERE id = ?').run(startedAt, r1.id); const utcDay = startedAt.slice(0, 10); const ld = new Date(startedAt); const localDay = `${ld.getFullYear()}-${String(ld.getMonth() + 1).padStart(2, '0')}-${String(ld.getDate()).padStart(2, '0')}`; assert.notEqual(localDay, utcDay, 'UTC+8 下 18:00Z 应跨到本地次日,前提才成立'); const detail = s.usageDetail('day'); assert.ok(detail.series.some((b) => b.bucket === localDay), `应分桶到本地日 ${localDay}`); assert.equal(detail.series.some((b) => b.bucket === utcDay), false, `不应再用 UTC 日 ${utcDay}`); // —— 当天窗口:本地今天 02:00(UTC 仍为昨天 18:00)的 run 应计入「今天」 —— const r2 = s.startRun(t.id, 'executor'); s.setRunUsage(r2.id, { input: 0, output: 1_000_000, cacheRead: 0, cacheWrite: 0 }, 'claude-opus-4-8'); // $75 const today2am = new Date(); today2am.setHours(2, 0, 0, 0); // 本地今天 02:00 s.db.prepare('UPDATE runs SET started_at = ? WHERE id = ?').run(today2am.toISOString(), r2.id); assert.ok(s.costSummary('day').total >= 75, '本地 00:00–08:00 的 run 应计入今天(旧 UTC 口径会漏算)'); s.close(); } finally { if (origTZ === undefined) delete process.env.TZ; else process.env.TZ = origTZ; } }); test('store: enforceBudget 超额 → 暂停项目 + 广播 budget.exceeded', () => { const s = new Store(':memory:'); const p = s.createProject({ name: 'b', repoPath: '/tmp/b-' + Math.random() }); const t = s.createTask({ projectId: p.id, title: 'x', complexity: 'easy' }); const r = s.startRun(t.id, 'executor'); s.setRunUsage(r.id, { input: 0, output: 1_000_000, cacheRead: 0, cacheWrite: 0 }, 'claude-opus-4-8'); // $75 // 无预算 → 不干预 assert.equal(s.enforceBudget(p.id), false); assert.equal(s.getProject(p.id)!.status, 'active'); // 设 $50 上限 → 超额、暂停、发事件 s.patchProject(p.id, { budgetUsd: 50, budgetPeriod: 'month' }); assert.equal(s.enforceBudget(p.id), true); assert.equal(s.getProject(p.id)!.status, 'paused'); const ev = s.listEvents(p.id).find((e) => e.type === 'budget.exceeded'); assert.ok(ev, '应有 budget.exceeded 事件'); assert.equal((ev!.payload as { budget: number }).budget, 50); s.close(); });