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);
}