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