Recently Written · git

ideamine

An idea inbox for Claude Code: /idea saves ideas at zero tokens; Claude triages them and routes each to the cheapest model that can build it.

git clone https://github.com/equwal/ideamine

Log | Files | Refs


tests/usage.test.js (5724 bytes)

1 import assert from 'node:assert/strict';
2 import fs from 'node:fs';
3 import os from 'node:os';
4 import path from 'node:path';
5 import { afterEach, beforeEach, test } from 'node:test';
6 import fc from 'fast-check';
7 import * as usage from '../src/usage.js';
8 
9 let dir; // the folder with the transcripts of the test
10 
11 beforeEach(() => {
12   process.env.IDEAMINE_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'ideamine-usage-'));
13   dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ideamine-transcripts-'));
14 });
15 
16 afterEach(() => {
17   fs.rmSync(dir, { recursive: true, force: true });
18 });
19 
20 /** One answer of the model, as Claude Code writes it into a transcript. */
21 function answer({ id = 'msg_1', model = 'claude-opus-5', cwd = 'C:\\work', at = '2026-09-22T10:00:00.000Z', input = 0, output = 0, read = 0, w5m = 0, w1h = 0 } = {}) {
22   return JSON.stringify({
23     type: 'assistant',
24     cwd,
25     sessionId: 's1',
26     timestamp: at,
27     message: {
28       id,
29       model,
30       usage: {
31         input_tokens: input,
32         output_tokens: output,
33         cache_read_input_tokens: read,
34         cache_creation_input_tokens: w5m + w1h,
35         cache_creation: { ephemeral_5m_input_tokens: w5m, ephemeral_1h_input_tokens: w1h },
36       },
37     },
38   });
39 }
40 
41 const write = (name, lines) => fs.writeFileSync(path.join(dir, name), `${lines.join('\n')}\n`);
42 
43 test('the price of a row follows the price list', () => {
44   // 1M input, 1M output, 1M cache read, 1M cache write with the 5-minute lifetime, on Opus 5.
45   const row = { model: 'claude-opus-5', input: 1e6, output: 1e6, read: 1e6, write5m: 1e6, write1h: 0 };
46   assert.equal(usage.cost(row), 5 + 25 + 0.5 + 5 * 1.25);
47   // The 1-hour lifetime costs 2 times the input price.
48   assert.equal(usage.cost({ model: 'claude-opus-5', write1h: 1e6 }), 10);
49   // A date at the end of the name names the same model.
50   assert.equal(usage.cost({ model: 'claude-haiku-4-5-20251001', output: 1e6 }), 5);
51   // An unknown model costs nothing, and the page can say so.
52   assert.equal(usage.cost({ model: 'gpt-9', output: 1e6 }), 0);
53   assert.equal(usage.priced('gpt-9'), false);
54   assert.equal(usage.priced('claude-opus-5'), true);
55 });
56 
57 test('the price of a sum is the sum of the prices, for every model', () => {
58   const models = fc.constantFrom(...Object.keys(usage.PRICES));
59   const count = fc.integer({ min: 0, max: 5_000_000 });
60   fc.assert(
61     fc.property(models, count, count, count, count, count, (model, input, output, read, write5m, write1h) => {
62       const one = { model, input, output, read, write5m, write1h };
63       const parts = [
64         { model, input, output: 0, read: 0, write5m: 0, write1h: 0 },
65         { model, input: 0, output, read: 0, write5m: 0, write1h: 0 },
66         { model, input: 0, output: 0, read, write5m: 0, write1h: 0 },
67         { model, input: 0, output: 0, read: 0, write5m, write1h },
68       ];
69       const sum = parts.reduce((n, p) => n + usage.cost(p), 0);
70       assert.ok(Math.abs(usage.cost(one) - sum) < 1e-9);
71     }),
72   );
73 });
74 
75 test('a scan adds the tokens up for each day, project, and model', () => {
76   write('a.jsonl', [
77     answer({ id: 'm1', output: 100, read: 1000 }),
78     answer({ id: 'm1', output: 100, read: 1000 }), // the same answer twice: it counts once
79     answer({ id: 'm2', model: 'claude-sonnet-5', output: 50 }),
80     answer({ id: 'm3', at: '2026-09-21T10:00:00.000Z', output: 7 }),
81     answer({ id: 'm4', cwd: 'C:\\other', output: 3 }),
82     '{"type":"user","message":{"content":"no tokens here"}}',
83     'half a line that a crash cut',
84   ]);
85   const { rows, files, read } = usage.scan({ dir });
86   assert.deepEqual([files, read], [1, 1]);
87   const find = (day, project, model) => rows.find((r) => r.day === day && r.project === project && r.model === model);
88   assert.deepEqual(
89     [find('2026-09-22', 'C:\\work', 'claude-opus-5').output, find('2026-09-22', 'C:\\work', 'claude-opus-5').messages],
90     [100, 1],
91   );
92   assert.equal(find('2026-09-22', 'C:\\work', 'claude-sonnet-5').output, 50);
93   assert.equal(find('2026-09-21', 'C:\\work', 'claude-opus-5').output, 7);
94   assert.equal(find('2026-09-22', 'C:\\other', 'claude-opus-5').output, 3);
95   assert.equal(rows.length, 4);
96 });
97 
98 test('the next scan reads only the new lines', () => {
99   write('a.jsonl', [answer({ id: 'm1', output: 10 })]);
100   usage.scan({ dir });
101   assert.equal(usage.scan({ dir }).read, 0); // nothing new: no file is read again
102   fs.appendFileSync(path.join(dir, 'a.jsonl'), `${answer({ id: 'm2', output: 5 })}\n`);
103   const second = usage.scan({ dir });
104   assert.equal(second.read, 1);
105   const row = second.rows.find((r) => r.model === 'claude-opus-5');
106   assert.deepEqual([row.output, row.messages], [15, 2]); // the tokens of both scans
107 });
108 
109 test('byProject and since group the rows the way the page shows them', () => {
110   const today = new Date().toISOString().slice(0, 10);
111   const old = new Date(Date.now() - 40 * 86400000).toISOString().slice(0, 10);
112   const rows = [
113     { day: today, project: 'p1', model: 'claude-opus-5', output: 1e6, messages: 2, input: 0, read: 0, write5m: 0, write1h: 0 },
114     { day: today, project: 'p1', model: 'claude-sonnet-5', output: 1e6, messages: 1, input: 0, read: 0, write5m: 0, write1h: 0 },
115     { day: old, project: 'p2', model: 'claude-opus-5', output: 2e6, messages: 5, input: 0, read: 0, write5m: 0, write1h: 0 },
116   ];
117   assert.equal(usage.since(rows, 30).length, 2);
118   assert.equal(usage.since(rows, 0).length, 3);
119   const [first, second] = usage.byProject(rows);
120   assert.deepEqual([first.project, first.cost], ['p2', 50]); // the most expensive project first
121   assert.deepEqual([second.project, second.cost, second.messages], ['p1', 35, 3]);
122   assert.deepEqual(Object.keys(second.models).sort(), ['claude-opus-5', 'claude-sonnet-5']);
123 });