diff --git a/test/helpers/pty-transcript-cost.unit.test.ts b/test/helpers/pty-transcript-cost.unit.test.ts index c02e5db07..d05d3591e 100644 --- a/test/helpers/pty-transcript-cost.unit.test.ts +++ b/test/helpers/pty-transcript-cost.unit.test.ts @@ -45,6 +45,7 @@ describe('ptyTranscriptCostUsd', () => { const unpriced = fixture(project => fs.writeFileSync(path.join(project, `${SID}.jsonl`), assistant('msg_a', 'claude-opus-5-5', { output_tokens: 1 }) + '\n' + assistant('msg_b', 'some-unpriced-model', { output_tokens: 1 }) + '\n')); const empty = fixture(project => fs.writeFileSync(path.join(project, `${SID}.jsonl`), JSON.stringify({ type: 'user', sessionId: SID }) + '\n')); + const unreadable = fixture(project => fs.mkdirSync(path.join(project, `${SID}.jsonl`))); try { expect(ptyTranscriptCostUsd(unpriced, SID)).toBeUndefined(); expect(ptyTranscriptCostUsd(empty, SID)).toBeUndefined(); @@ -53,6 +54,7 @@ describe('ptyTranscriptCostUsd', () => { expect(ptyTranscriptCostUsd(null, SID)).toBeUndefined(); expect(ptyTranscriptCostUsd(path.join(empty, 'missing'), SID)).toBeUndefined(); expect(pricedTranscript(undefined)).toEqual({}); - } finally { for (const dir of [unpriced, empty]) fs.rmSync(dir, { recursive: true, force: true }); } + expect(ptyTranscriptCostUsd(unreadable, SID)).toBeUndefined(); + } finally { for (const dir of [unpriced, empty, unreadable]) fs.rmSync(dir, { recursive: true, force: true }); } }); }); diff --git a/test/helpers/pty/transcript-cost.ts b/test/helpers/pty/transcript-cost.ts index 064d520b8..c54a70bed 100644 --- a/test/helpers/pty/transcript-cost.ts +++ b/test/helpers/pty/transcript-cost.ts @@ -12,31 +12,30 @@ export function pricedTranscript(session: { hermeticConfigDir?: string | null; s * Price one PTY session from the transcripts Claude Code wrote under its owned config dir: * `projects//.jsonl` (inline sidechains included) and `projects///subagents/*.jsonl`, * rows deduplicated by message id. Undefined (cost-unknown, never $0) without a config dir, session id, transcript or - * usage, or when a message's model has no price row. + * usage, when a message's model has no price row, or when any read fails: it runs in ledger writers before close and + * cleanup, so it never throws. */ export function ptyTranscriptCostUsd(configDir: string | null | undefined, sessionId: string | undefined): number | undefined { if (!configDir || !sessionId) return undefined; - const projects = path.join(configDir, 'projects'); - let dirs: fs.Dirent[]; try { - dirs = fs.readdirSync(projects, { withFileTypes: true }); + const projects = path.join(configDir, 'projects'); + const files: string[] = []; + for (const dir of fs.readdirSync(projects, { withFileTypes: true })) { + if (!dir.isDirectory()) continue; + const main = path.join(projects, dir.name, `${sessionId}.jsonl`); + if (fs.existsSync(main)) files.push(main); + const subagents = path.join(projects, dir.name, sessionId, 'subagents'); + if (fs.existsSync(subagents)) files.push(...fs.readdirSync(subagents).filter(f => f.endsWith('.jsonl')).map(f => path.join(subagents, f))); + } + const rows = files.flatMap(file => fs.readFileSync(file, 'utf8').split('\n').flatMap(line => { + try { + return [JSON.parse(line)]; + } catch { + return []; + } + })); + return streamedUsageCostUsd(rows); } catch { return undefined; } - const files: string[] = []; - for (const dir of dirs) { - if (!dir.isDirectory()) continue; - const main = path.join(projects, dir.name, `${sessionId}.jsonl`); - if (fs.existsSync(main)) files.push(main); - const subagents = path.join(projects, dir.name, sessionId, 'subagents'); - if (fs.existsSync(subagents)) files.push(...fs.readdirSync(subagents).filter(f => f.endsWith('.jsonl')).map(f => path.join(subagents, f))); - } - const rows = files.flatMap(file => fs.readFileSync(file, 'utf8').split('\n').flatMap(line => { - try { - return [JSON.parse(line)]; - } catch { - return []; - } - })); - return streamedUsageCostUsd(rows); }