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/triage.test.js (6150 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 { beforeEach, test } from 'node:test';
6 import { fileURLToPath } from 'node:url';
7 import { headlessTriage } from '../src/claude.js';
8 import * as store from '../src/store.js';
9 
10 const FAKE = fileURLToPath(new URL('./fixtures/fake-claude.js', import.meta.url));
11 let log;
12 
13 beforeEach(() => {
14   const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ideamine-triage-'));
15   process.env.IDEAMINE_HOME = dir;
16   process.env.IDEAMINE_CLAUDE_BIN = FAKE;
17   process.env.CLAUDE_CONFIG_DIR = dir; // no .claude.json: the projects of this machine stay out of the tests
18   process.env.FAKE_CLAUDE_LOG = log = path.join(dir, 'call.json');
19 });
20 
21 test('the triage pairs each idea with the project that it is about', async () => {
22   const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ideamine-pair-'));
23   const alpha = path.join(root, 'alpha');
24   const beta = path.join(root, 'beta');
25   const chat = path.join(root, 'scratch-workspaces', 'chat1'); // where the user saved the ideas
26   for (const d of [alpha, beta, chat]) fs.mkdirSync(d, { recursive: true });
27   fs.writeFileSync(path.join(alpha, 'README.md'), '# Alpha: a scroller extension\n');
28   fs.writeFileSync(path.join(root, '.claude.json'), JSON.stringify({ projects: { [alpha]: {}, [beta]: {} } }));
29   process.env.CLAUDE_CONFIG_DIR = root;
30 
31   store.addIdeas(['make alpha scroll faster', 'beta needs a logo', 'a brand new app'], { project: chat });
32   await headlessTriage({ model: 'haiku' });
33   const db = store.load();
34   assert.equal(store.findIdea(db, 1).project, alpha);
35   assert.equal(store.findIdea(db, 2).project, beta);
36   assert.equal(store.findIdea(db, 3).project, chat); // no project fits: the idea stays where it was saved
37   assert.match(store.findIdea(db, 1).notes[0].text, /^paired with .*alpha \(was .*chat1\)$/);
38   assert.ok(db.ideas.every((i) => i.triage.paired));
39   const { input } = JSON.parse(fs.readFileSync(log, 'utf8'));
40   assert.match(input, /- project: /);
41   assert.ok(input.includes(`Projects on this machine:\nalpha: ${alpha} — Alpha: a scroller extension\nbeta: ${beta}\n\n`));
42 });
43 
44 test('regression: the triage answers with the name of the folder, and the idea is still paired', async () => {
45   // Haiku answered "simple-autoscroll-free", not the path, and ideamine 0.4.0 before this fix did not pair.
46   const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ideamine-name-'));
47   const extension = path.join(root, 'simple-autoscroll-free');
48   fs.mkdirSync(extension);
49   fs.writeFileSync(path.join(root, '.claude.json'), JSON.stringify({ projects: { [extension]: {} } }));
50   process.env.CLAUDE_CONFIG_DIR = root;
51   process.env.FAKE_CLAUDE_ANSWER = 'name';
52   store.addIdeas(['add a speed slider to simple-autoscroll-free']);
53   await headlessTriage({ model: 'haiku' });
54   delete process.env.FAKE_CLAUDE_ANSWER;
55   assert.equal(store.findIdea(store.load(), 1).project, extension);
56 });
57 
58 test('headless triage runs one minimal claude -p call and saves every verdict', async () => {
59   store.addIdeas(['cache responses', 'rename helpers', 'nft thing'], { project: '/p' });
60   // Pretend we are inside a desktop-app session at max effort.
61   process.env.CLAUDECODE = '1';
62   process.env.CLAUDE_EFFORT = 'max';
63   process.env.CLAUDE_CODE_MESSAGING_SOCKET = '\\\\.\\pipe\\x';
64   const out = await headlessTriage();
65   delete process.env.CLAUDECODE;
66   delete process.env.CLAUDE_EFFORT;
67   delete process.env.CLAUDE_CODE_MESSAGING_SOCKET;
68 
69   assert.equal(out.results.length, 3);
70   assert.deepEqual(out.tokens, { input: 321, output: 45 });
71   const db = store.load();
72   assert.deepEqual(db.ideas.map((i) => store.lane(i)), ['do', 'do', 'skip']);
73   assert.equal(store.findIdea(db, 2).triage.model, 'haiku');
74   assert.equal(store.findIdea(db, 1).triage.by, 'sonnet (headless)');
75 
76   const call = JSON.parse(fs.readFileSync(log, 'utf8'));
77   const flag = (name) => call.args[call.args.indexOf(name) + 1];
78   assert.equal(call.args[0], '-p');
79   assert.equal(flag('--model'), 'sonnet');
80   assert.equal(flag('--tools'), '');
81   assert.equal(flag('--setting-sources'), '');
82   assert.equal(flag('--effort'), 'low');
83   assert.ok(call.args.includes('--strict-mcp-config'));
84   assert.ok(call.args.includes('--no-session-persistence'));
85   assert.ok(JSON.parse(flag('--json-schema')).properties.verdicts);
86   // The child is an independent session: no nesting marker, no inherited effort or host socket.
87   assert.deepEqual(call.env, {
88     CLAUDECODE: null,
89     CLAUDE_EFFORT: null,
90     CLAUDE_CODE_MESSAGING_SOCKET: null,
91     ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL ?? null,
92     MAX_THINKING_TOKENS: process.env.MAX_THINKING_TOKENS ?? null, // low effort already limits Sonnet
93   });
94   assert.match(call.input, /Ideas to triage \(3\)/);
95 });
96 
97 test('haiku gets no effort flag, and an empty inbox makes no call', async () => {
98   assert.equal((await headlessTriage()).message, 'Nothing to triage: the inbox is empty.');
99   assert.ok(!fs.existsSync(log));
100   store.addIdeas(['one']);
101   await headlessTriage({ model: 'claude-haiku-4-5' });
102   const call = JSON.parse(fs.readFileSync(log, 'utf8'));
103   assert.equal(call.args[call.args.indexOf('--model') + 1], 'haiku');
104   assert.ok(!call.args.includes('--effort'));
105   // Haiku has no effort setting. Its thinking was 70% of the output tokens and did not change the verdicts.
106   assert.equal(call.env.MAX_THINKING_TOKENS, '0');
107 });
108 
109 test('headless triage of given ids judges only those ideas', async () => {
110   store.addIdeas(['one', 'two', 'three']);
111   const out = await headlessTriage({ ids: [3] });
112   assert.deepEqual(out.results.map((r) => r.id), [3]);
113   assert.deepEqual(store.load().ideas.map((i) => i.status), ['inbox', 'inbox', 'triaged']);
114   assert.match(JSON.parse(fs.readFileSync(log, 'utf8')).input, /Ideas to triage \(1\):\n#3: three\n/);
115 });
116 
117 test('a missing CLI gives a clear error', async () => {
118   store.addIdeas(['one']);
119   process.env.IDEAMINE_CLAUDE_BIN = path.join(os.tmpdir(), 'definitely-not-claude-xyz');
120   await assert.rejects(headlessTriage(), /Claude Code CLI not found/);
121   assert.equal(store.load().ideas[0].status, 'inbox');
122 });