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/watch.test.js (4122 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 * as store from '../src/store.js';
8 import * as watch from '../src/watch.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-watch-'));
15   process.env.IDEAMINE_HOME = dir;
16   process.env.IDEAMINE_CLAUDE_BIN = FAKE;
17   process.env.CLAUDE_CONFIG_DIR = dir;
18   process.env.FAKE_CLAUDE_LOG = log = path.join(dir, 'call.json');
19 });
20 
21 const lock = () => path.join(store.home(), '.watch');
22 
23 test('the watcher starts a pass only when it is on, there is work, and no pass runs', () => {
24   let started = 0;
25   const kick = () => watch.kick({ startPass: () => started++ });
26   assert.equal(kick(), false); // off
27   watch.turnOn();
28   assert.equal(kick(), false); // no ideas
29   store.addIdeas(['one']);
30   assert.equal(kick(), true);
31   fs.mkdirSync(lock()); // a pass runs
32   assert.equal(kick(), false);
33   fs.rmSync(lock(), { recursive: true });
34   watch.turnOff();
35   assert.equal(kick(), false);
36   assert.equal(started, 1);
37 });
38 
39 test('a pass that cannot start is recorded, and the caller goes on', async () => {
40   // The dashboard server calls kick() and must live on. Once, a failed start stopped that server.
41   watch.turnOn();
42   store.addIdeas(['one']);
43   const node = process.execPath;
44   process.execPath = path.join(store.home(), 'no-such-node');
45   try {
46     assert.equal(watch.kick(), true);
47     await new Promise((resolve) => setTimeout(resolve, 300)); // the spawn error comes later
48   } finally {
49     process.execPath = node;
50   }
51   assert.match(watch.readState().error, /^cannot start a pass: .*ENOENT/);
52   assert.equal(watch.kick({ startPass: () => assert.fail('must wait after the error') }), false);
53 });
54 
55 test('a pass triages the inbox and the ideas from before pairing, with Haiku, and logs it', async () => {
56   store.addIdeas(['old idea', 'new idea']);
57   store.applyTriage([{ id: 1, verdict: 'do', impact: 3, size: 's', model: 'sonnet', title: 'Old', why: 'w', brief: 'b' }]);
58   watch.turnOn();
59   assert.deepEqual(watch.pendingWork(store.load()), [1, 2]);
60   await watch.pass();
61   const db = store.load();
62   assert.deepEqual(watch.pendingWork(db), []);
63   assert.deepEqual(db.ideas.map((i) => i.triage.by), ['haiku (headless)', 'haiku (headless)']);
64   assert.match(JSON.parse(fs.readFileSync(log, 'utf8')).args.join(' '), /--model haiku/);
65   assert.match(fs.readFileSync(path.join(store.home(), 'watch.log'), 'utf8'), /triaged #1 #2 \(haiku, 321 in \/ 45 out tokens\)/);
66   assert.ok(watch.readState().lastPass);
67   assert.ok(!fs.existsSync(lock()));
68 });
69 
70 test('a failed pass is logged, and the watcher waits before it tries again', async () => {
71   store.addIdeas(['one']);
72   watch.turnOn();
73   process.env.IDEAMINE_CLAUDE_BIN = path.join(os.tmpdir(), 'definitely-not-claude-xyz');
74   await watch.pass({ retries: 0 });
75   assert.match(watch.readState().error, /Claude Code CLI not found/);
76   assert.match(fs.readFileSync(path.join(store.home(), 'watch.log'), 'utf8'), /error: Claude Code CLI not found/);
77   assert.equal(watch.kick({ startPass: () => assert.fail('it must wait after an error') }), false);
78   assert.equal(watch.kick({ startPass: () => {}, now: Date.now() + 11 * 60 * 1000 }), true);
79 });
80 
81 test('only one pass runs at a time', async () => {
82   store.addIdeas(['one']);
83   watch.turnOn();
84   fs.mkdirSync(lock());
85   assert.equal(await watch.pass(), 'busy');
86   assert.ok(!fs.existsSync(log)); // no call to claude
87   assert.equal(store.load().ideas[0].status, 'inbox');
88 });
89 
90 test('the status tells if the watcher is on, what waits, and what it did last', async () => {
91   assert.match(watch.status(), /^ideamine watch: off/);
92   store.addIdeas(['one']);
93   watch.turnOn();
94   assert.match(watch.status(), /^ideamine watch: on since .*\n1 idea waits for the watcher/);
95   await watch.pass();
96   assert.match(watch.status(), /\nlast passes:\n {2}\d{4}-\d\d-\d\d \d\d:\d\d {2}triaged #1 /);
97 });