fix(analytics): cache native Codex usage scans

Cache parsed native Codex rollout usage entries per file fingerprint to avoid repeated full-history JSONL parsing on dashboard refreshes.

Add regression coverage for cache reuse, invalid cache fallback, cliproxy cache separation, and scoped cache fixtures.
This commit is contained in:
walker1211 authored and GitHub committed 2026-05-02 21:16:07 -04:00
1 parent 74898210f9
commit 11b12f146d
2 files changed
+309 -15

No files matched your search

@@ -2,8 +2,18 @@ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { runWithScopedCcsHome } from '../../../src/utils/config-manager';
import { scanCodexNativeUsageEntries } from '../../../src/web-server/usage/codex-native-usage-collector';
type TestUsageCache = {
includeCliproxySessions: boolean;
files: Record<string, { mtimeMs: number; entries: unknown[] }>;
};
function readTestUsageCache(cachePath: string): TestUsageCache {
return JSON.parse(fs.readFileSync(cachePath, 'utf8')) as TestUsageCache;
}
function writeCodexRollout(
baseDir: string,
options: {
@@ -12,7 +22,7 @@ function writeCodexRollout(
model?: string;
cwd?: string;
} = {}
): void {
): string {
const sessionId = options.sessionId ?? 'codex-session-1';
const rolloutDir = path.join(baseDir, 'sessions', '2026', '03', '02');
const rolloutPath = path.join(rolloutDir, `rollout-${sessionId}.jsonl`);
@@ -124,6 +134,38 @@ function writeCodexRollout(
fs.mkdirSync(rolloutDir, { recursive: true });
fs.writeFileSync(rolloutPath, `${lines.join('\n')}\n`, 'utf8');
return rolloutPath;
}
function appendCodexTokenCount(rolloutPath: string): void {
fs.appendFileSync(
rolloutPath,
`${JSON.stringify({
timestamp: '2026-03-02T10:15:00.000Z',
type: 'event_msg',
payload: {
type: 'token_count',
info: {
total_token_usage: {
input_tokens: 200,
cached_input_tokens: 40,
output_tokens: 20,
reasoning_output_tokens: 5,
total_tokens: 265,
},
last_token_usage: {
input_tokens: 50,
cached_input_tokens: 10,
output_tokens: 10,
reasoning_output_tokens: 2,
total_tokens: 72,
},
model_context_window: 200000,
},
},
})}\n`,
'utf8'
);
}
describe('codex native usage collector', () => {
@@ -137,13 +179,21 @@ describe('codex native usage collector', () => {
fs.rmSync(tempRoot, { recursive: true, force: true });
});
function getCacheDir(): string {
const cacheDir = path.join(tempRoot, 'ccs-cache');
fs.mkdirSync(cacheDir, { recursive: true });
return cacheDir;
}
it('parses token_count events into raw usage entries and suppresses duplicates', async () => {
writeCodexRollout(tempRoot);
const entries = await scanCodexNativeUsageEntries({
env: { CODEX_HOME: tempRoot },
homeDir: tempRoot,
});
const entries = await runWithScopedCcsHome(tempRoot, () =>
scanCodexNativeUsageEntries({
env: { CODEX_HOME: tempRoot },
homeDir: tempRoot,
})
);
expect(entries).toHaveLength(2);
expect(entries[0]).toMatchObject({
@@ -166,10 +216,12 @@ describe('codex native usage collector', () => {
it('skips cliproxy-backed codex sessions by default to avoid double counting', async () => {
writeCodexRollout(tempRoot, { modelProvider: 'cliproxy' });
const entries = await scanCodexNativeUsageEntries({
env: { CODEX_HOME: tempRoot },
homeDir: tempRoot,
});
const entries = await runWithScopedCcsHome(tempRoot, () =>
scanCodexNativeUsageEntries({
env: { CODEX_HOME: tempRoot },
homeDir: tempRoot,
})
);
expect(entries).toHaveLength(0);
});
@@ -177,11 +229,121 @@ describe('codex native usage collector', () => {
it('also skips ccs_runtime-backed codex bridge sessions by default', async () => {
writeCodexRollout(tempRoot, { modelProvider: 'ccs_runtime' });
const entries = await scanCodexNativeUsageEntries({
env: { CODEX_HOME: tempRoot },
homeDir: tempRoot,
});
const entries = await runWithScopedCcsHome(tempRoot, () =>
scanCodexNativeUsageEntries({
env: { CODEX_HOME: tempRoot },
homeDir: tempRoot,
})
);
expect(entries).toHaveLength(0);
});
it('reuses cached rollout entries when file size and mtime are unchanged', async () => {
const rolloutPath = writeCodexRollout(tempRoot);
const cacheDir = getCacheDir();
const stableMtime = new Date('2026-03-02T10:20:00.000Z');
fs.utimesSync(rolloutPath, stableMtime, stableMtime);
const firstEntries = await scanCodexNativeUsageEntries({
env: { CODEX_HOME: tempRoot },
homeDir: tempRoot,
cacheDir,
});
const cachePath = path.join(cacheDir, 'codex-native-usage-v1.json');
const originalStats = fs.statSync(rolloutPath);
const originalCache = readTestUsageCache(cachePath);
const originalCachedRollout = originalCache.files[rolloutPath];
expect(originalCachedRollout?.mtimeMs).toBe(originalStats.mtimeMs);
const originalContent = fs.readFileSync(rolloutPath, 'utf8');
fs.writeFileSync(rolloutPath, '#'.repeat(originalContent.length), 'utf8');
fs.utimesSync(rolloutPath, stableMtime, stableMtime);
const secondEntries = await scanCodexNativeUsageEntries({
env: { CODEX_HOME: tempRoot },
homeDir: tempRoot,
cacheDir,
});
expect(secondEntries).toEqual(firstEntries);
});
it('reparses a rollout file after its size changes', async () => {
const rolloutPath = writeCodexRollout(tempRoot);
const cacheDir = getCacheDir();
const firstEntries = await scanCodexNativeUsageEntries({
env: { CODEX_HOME: tempRoot },
homeDir: tempRoot,
cacheDir,
});
appendCodexTokenCount(rolloutPath);
const secondEntries = await scanCodexNativeUsageEntries({
env: { CODEX_HOME: tempRoot },
homeDir: tempRoot,
cacheDir,
});
expect(firstEntries).toHaveLength(2);
expect(secondEntries).toHaveLength(3);
expect(secondEntries[2]).toMatchObject({
inputTokens: 50,
cacheReadTokens: 10,
outputTokens: 12,
});
});
it('falls back to parsing rollout files when the cache file is corrupt', async () => {
writeCodexRollout(tempRoot);
const cacheDir = getCacheDir();
fs.writeFileSync(path.join(cacheDir, 'codex-native-usage-v1.json'), '{not-json', 'utf8');
const entries = await scanCodexNativeUsageEntries({
env: { CODEX_HOME: tempRoot },
homeDir: tempRoot,
cacheDir,
});
expect(entries).toHaveLength(2);
});
it('keeps default and include-cliproxy cache entries separate', async () => {
writeCodexRollout(tempRoot, { modelProvider: 'cliproxy' });
const cacheDir = getCacheDir();
const defaultEntries = await scanCodexNativeUsageEntries({
env: { CODEX_HOME: tempRoot },
homeDir: tempRoot,
cacheDir,
});
const includedEntries = await scanCodexNativeUsageEntries({
env: { CODEX_HOME: tempRoot },
homeDir: tempRoot,
includeCliproxySessions: true,
cacheDir,
});
expect(defaultEntries).toHaveLength(0);
expect(includedEntries).toHaveLength(2);
const defaultCachePath = path.join(cacheDir, 'codex-native-usage-v1.json');
const includeCachePath = path.join(cacheDir, 'codex-native-usage-with-cliproxy-v1.json');
expect(fs.existsSync(defaultCachePath)).toBe(true);
expect(fs.existsSync(includeCachePath)).toBe(true);
const defaultCache = readTestUsageCache(defaultCachePath);
const includeCache = readTestUsageCache(includeCachePath);
const defaultCachedRollout = defaultCache.files[Object.keys(defaultCache.files)[0] ?? ''];
const includeCachedRollout = includeCache.files[Object.keys(includeCache.files)[0] ?? ''];
expect(defaultCache.includeCliproxySessions).toBe(false);
expect(defaultCachedRollout?.entries).toHaveLength(0);
expect(includeCache.includeCliproxySessions).toBe(true);
expect(includeCachedRollout?.entries).toHaveLength(2);
});
});