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/store.test.js (7316 bytes)

1 import assert from 'node:assert/strict';
2 import { spawn } from 'node:child_process';
3 import fs from 'node:fs';
4 import os from 'node:os';
5 import path from 'node:path';
6 import { beforeEach, test } from 'node:test';
7 import { fileURLToPath } from 'node:url';
8 import * as store from '../src/store.js';
9 
10 const BIN = fileURLToPath(new URL('../bin/ideamine.js', import.meta.url));
11 
12 beforeEach(() => {
13   process.env.IDEAMINE_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'ideamine-test-'));
14 });
15 
16 test('add, triage, and list ideas by lane and priority', () => {
17   store.addIdeas(['small win', 'big but slow', 'meh', 'later'], { project: '/p' });
18   store.applyTriage([
19     { id: 1, verdict: 'do', impact: 3, size: 'xs', model: 'haiku', title: 'Small win', why: 'cheap', brief: 'b' },
20     { id: 2, verdict: 'do', impact: 5, size: 'xl', model: 'claude-opus-5', title: 'Big', why: 'w', brief: 'b' },
21     { id: 3, verdict: 'skip', impact: 1, size: 'm', model: 'sonnet', title: 'Meh', why: 'no', brief: 'ignored' },
22   ]);
23   const db = store.load();
24   assert.deepEqual(store.listIdeas(db).map((i) => i.id), [1, 2, 4]); // do by value/effort, then inbox
25   assert.deepEqual(store.listIdeas(db, { filter: 'skip' }).map((i) => i.id), [3]);
26   assert.equal(store.findIdea(db, 2).triage.model, 'opus'); // full model ids normalize to aliases
27   assert.equal(store.findIdea(db, 3).triage.brief, ''); // skip verdicts carry no brief
28   assert.equal(store.pickNext(db, { project: '/p' }).id, 1);
29   assert.deepEqual(store.counts(db), { inbox: 1, do: 2, maybe: 0, skip: 1, doing: 0, done: 0, dropped: 0, open: 3 });
30 });
31 
32 test('pickNext takes the best "do" idea, else the best "maybe", and never an idea from another lane', () => {
33   // Every combination of lanes, so that no lane can get into or out of the queue unseen.
34   const LANES = ['do', 'maybe', 'skip', 'inbox', 'doing', 'done', 'dropped'];
35   const VERDICTS = ['do', 'maybe', 'skip'];
36   const make = (lane, id) => ({
37     id,
38     title: lane,
39     status: VERDICTS.includes(lane) ? 'triaged' : lane,
40     triage: lane === 'inbox' ? null : { verdict: VERDICTS.includes(lane) ? lane : 'do', impact: 3, size: 'm', model: 'sonnet' },
41   });
42   for (let mask = 0; mask < 1 << LANES.length; mask++) {
43     const present = LANES.filter((_, bit) => mask & (1 << bit));
44     const db = { ideas: present.map((lane, i) => make(lane, i + 1)) };
45     const want = present.includes('do') ? 'do' : present.includes('maybe') ? 'maybe' : null;
46     assert.equal(store.pickNext(db)?.title ?? null, want, `lanes: ${present.join(', ')}`);
47   }
48 
49   // The verdict counts before the project: a "do" idea elsewhere beats a "maybe" idea here.
50   const db = {
51     ideas: [
52       { id: 1, status: 'triaged', project: '/here', triage: { verdict: 'maybe', impact: 5, size: 'xs', model: 'haiku' } },
53       { id: 2, status: 'triaged', project: '/there', triage: { verdict: 'do', impact: 1, size: 'xl', model: 'opus' } },
54     ],
55   };
56   assert.equal(store.pickNext(db, { project: '/here' }).id, 2);
57 });
58 
59 test('regression: after a triage of "0 do · 1 maybe · 1 skip", /ideas go still has an idea to build', () => {
60   store.addIdeas(['tes', 'redesign the AssistKey UI for a premium e-ink feel']);
61   store.applyTriage([
62     { id: 1, verdict: 'skip', impact: 1, size: 'xs', model: 'haiku', title: "Clarify vague 'tes' idea", why: 'w', brief: '' },
63     { id: 2, verdict: 'maybe', impact: 3, size: 'l', model: 'opus', title: 'Redesign AssistKey UI', why: 'w', brief: 'b' },
64   ]);
65   assert.equal(store.pickNext(store.load()).id, 2);
66 });
67 
68 test('removeIdeas deletes ideas for good, all or nothing, and never reuses an id', () => {
69   store.addIdeas(['one', 'two', 'three']);
70   assert.throws(() => store.removeIdeas([1, 99]), /no idea #99/);
71   assert.equal(store.load().ideas.length, 3); // an unknown id deletes nothing
72   assert.deepEqual(store.removeIdeas(['#2', 3, '3']).map((i) => i.id), [2, 3]);
73   assert.deepEqual(store.load().ideas.map((i) => i.id), [1]);
74   assert.equal(store.addIdeas(['four'])[0].idea.id, 4);
75 });
76 
77 test('bad verdicts are reported per item, not fatal', () => {
78   store.addIdeas(['one']);
79   const res = store.applyTriage([{ id: 1, verdict: 'yes' }, { id: 99, verdict: 'do' }]);
80   assert.match(res[0].error, /verdict/);
81   assert.equal(res[1].error, 'no such idea');
82   assert.equal(store.findIdea(store.load(), 1).status, 'inbox');
83 });
84 
85 test('status changes, notes, model override, and reopen', () => {
86   store.addIdeas(['thing']);
87   store.updateIdea(1, { status: 'start', note: 'on it' });
88   let idea = store.updateIdea('#1', { status: 'done', note: 'shipped' });
89   assert.equal(idea.status, 'done');
90   assert.ok(idea.closed);
91   assert.deepEqual(idea.notes.map((n) => n.text), ['on it', 'shipped']);
92   idea = store.updateIdea(1, { status: 'reopen', model: 'opus' });
93   assert.equal(idea.status, 'triaged');
94   assert.equal(idea.triage.model, 'opus');
95   assert.equal(idea.closed, undefined);
96   assert.throws(() => store.updateIdea(1, { status: 'nope' }), /status must be/);
97   assert.throws(() => store.updateIdea(42, { note: 'x' }), /no idea #42/);
98 });
99 
100 test('the start time is recorded when work starts, for the dashboard timeline', async () => {
101   store.addIdeas(['thing']);
102   assert.equal(store.findIdea(store.load(), 1).started, undefined);
103   const first = store.updateIdea(1, { status: 'doing' }).started;
104   assert.ok(first);
105   assert.equal(store.updateIdea(1, { status: 'doing', note: 'still on it' }).started, first); // no new start
106   assert.equal(store.updateIdea(1, { status: 'done' }).started, first);
107   store.updateIdea(1, { status: 'reopen' });
108   await new Promise((resolve) => setTimeout(resolve, 5));
109   assert.ok(store.updateIdea(1, { status: 'start' }).started > first); // the latest start
110 });
111 
112 test('a corrupt archive is never overwritten', () => {
113   fs.mkdirSync(store.home(), { recursive: true });
114   fs.writeFileSync(store.dbPath(), '{"ideas": [oops');
115   assert.throws(() => store.addIdeas(['x']), /not valid JSON/);
116   assert.equal(fs.readFileSync(store.dbPath(), 'utf8'), '{"ideas": [oops');
117 });
118 
119 test('a stale lock from a crashed writer is recovered', () => {
120   fs.mkdirSync(path.join(store.home(), '.lock'), { recursive: true });
121   const old = new Date(Date.now() - 60_000);
122   fs.utimesSync(path.join(store.home(), '.lock'), old, old);
123   assert.equal(store.addIdeas(['after crash'])[0].idea.id, 1);
124 });
125 
126 test('concurrent writers from many processes never lose an idea', async () => {
127   const procs = 8;
128   const each = 6;
129   const run = (n) =>
130     new Promise((resolve, reject) => {
131       // Each process appends its ideas through the CLI, all racing for the same archive.
132       const script = `for (let i = 0; i < ${each}; i++) require('node:child_process').execFileSync(process.execPath, [${JSON.stringify(BIN)}, 'add', 'proc ${n} idea ' + i]);`;
133       const child = spawn(process.execPath, ['-e', script], { env: process.env, stdio: ['ignore', 'ignore', 'pipe'] });
134       let err = '';
135       child.stderr.on('data', (d) => (err += d));
136       child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(err))));
137     });
138   await Promise.all(Array.from({ length: procs }, (_, n) => run(n)));
139   const ids = store.load().ideas.map((i) => i.id).sort((a, b) => a - b);
140   assert.equal(ids.length, procs * each);
141   assert.deepEqual(ids, Array.from({ length: procs * each }, (_, i) => i + 1));
142 });