From 8cc8707a294e796884bc40145970fcb4cc869c90 Mon Sep 17 00:00:00 2001 From: moose-lab Date: Thu, 18 Jun 2026 12:26:21 +0800 Subject: [PATCH] test: guard cost report agent coverage --- src/core/__tests__/cost-tracker.test.ts | 35 ++++++++++++++ src/core/cost-tracker.ts | 62 ++++++++++++++++++++++++- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/core/__tests__/cost-tracker.test.ts b/src/core/__tests__/cost-tracker.test.ts index 5ea6404..2530687 100644 --- a/src/core/__tests__/cost-tracker.test.ts +++ b/src/core/__tests__/cost-tracker.test.ts @@ -4,9 +4,12 @@ import { tmpdir } from "os"; import { test } from "node:test"; import assert from "node:assert/strict"; import * as costTracker from "../cost-tracker"; +import { LOCAL_CLI_AGENT_DEFINITIONS } from "../local-cli-agent-definitions"; import { aggregateCostReport, buildCostReport, + COST_REPORT_AGENT_COVERAGE, + COST_REPORT_PROVIDERS, computeCodexApiCostUSD, computeCodexSubscriptionCredits, scanCodexSession, @@ -69,6 +72,38 @@ test("computeCodexSubscriptionCredits uses the Codex token-based rate card", () assert.equal(computeCodexSubscriptionCredits("gpt-5.3-codex", TOKENS), 398.125); }); +test("cost report coverage explicitly accounts for every local CLI agent", () => { + const configuredAgentIds = new Set(Object.keys(COST_REPORT_AGENT_COVERAGE)); + const knownAgentIds = LOCAL_CLI_AGENT_DEFINITIONS.map((agent) => agent.id); + + assert.deepEqual([...configuredAgentIds].sort(), [...knownAgentIds].sort()); +}); + +test("supported cost report agents declare a cached or incremental reader", () => { + for (const [agentId, coverage] of Object.entries(COST_REPORT_AGENT_COVERAGE)) { + if (!coverage.supported) continue; + + assert.ok( + coverage.cacheStrategy.length > 0, + `${agentId} must document the cache strategy that prevents full-history rescans` + ); + assert.ok( + coverage.sourcePath.length > 0, + `${agentId} must document the historical usage source it reads` + ); + } +}); + +test("cost report provider list matches supported agent coverage", () => { + const supportedProviders = new Set( + Object.values(COST_REPORT_AGENT_COVERAGE) + .filter((coverage) => coverage.supported) + .map((coverage) => coverage.provider) + ); + + assert.deepEqual([...supportedProviders].sort(), [...COST_REPORT_PROVIDERS].sort()); +}); + test("buildCostReport reuses unchanged Codex session scans", async () => { await withCostDirs(async ({ claudeDir, codexDir }) => { const tools = costTracker as typeof costTracker & { diff --git a/src/core/cost-tracker.ts b/src/core/cost-tracker.ts index 6d54899..809e81e 100644 --- a/src/core/cost-tracker.ts +++ b/src/core/cost-tracker.ts @@ -7,9 +7,69 @@ import dayjs from "dayjs"; import { discoverProjects } from "./discovery"; import type { Session } from "./types"; -export type CostProvider = "claude_code" | "codex"; +export const COST_REPORT_PROVIDERS = ["claude_code", "codex"] as const; +export type CostProvider = (typeof COST_REPORT_PROVIDERS)[number]; export type BillingMode = "api" | "oauth_subscription"; +export type CostReportAgentCoverage = + | { + supported: true; + provider: CostProvider; + sourcePath: string; + cacheStrategy: string; + } + | { + supported: false; + unsupportedReason: string; + }; + +export const COST_REPORT_AGENT_COVERAGE = { + claude: { + supported: true, + provider: "claude_code", + sourcePath: "~/.claude/projects/**/*.jsonl", + cacheStrategy: "discoverProjects() IM-23 cache keyed by path, mtimeMs, and size with bounded concurrency", + }, + codex: { + supported: true, + provider: "codex", + sourcePath: "~/.codex/sessions/**/*.jsonl plus ~/.codex/archived_sessions/**/*.jsonl", + cacheStrategy: "scanCodexSessionCached() cache keyed by path, mtimeMs, and size with bounded concurrency", + }, + "cursor-agent": { + supported: false, + unsupportedReason: "No durable local cost/usage history source is parsed by DevLog yet", + }, + copilot: { + supported: false, + unsupportedReason: "No durable local cost/usage history source is parsed by DevLog yet", + }, + gemini: { + supported: false, + unsupportedReason: "No durable local cost/usage history source is parsed by DevLog yet", + }, + hermes: { + supported: false, + unsupportedReason: "Runner is pending and no durable local cost/usage history source is parsed by DevLog yet", + }, + kimi: { + supported: false, + unsupportedReason: "Runner is pending and no durable local cost/usage history source is parsed by DevLog yet", + }, + opencode: { + supported: false, + unsupportedReason: "No durable local cost/usage history source is parsed by DevLog yet", + }, + pi: { + supported: false, + unsupportedReason: "Runner is pending and no durable local cost/usage history source is parsed by DevLog yet", + }, + qwen: { + supported: false, + unsupportedReason: "No durable local cost/usage history source is parsed by DevLog yet", + }, +} as const satisfies Record; + export interface CostTokenTotals { inputTokens: number; cachedInputTokens: number;