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/publish.test.js (6929 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 * as config from '../src/config.js';
7 import * as embed from '../src/embed.js';
8 import * as publish from '../src/publish.js';
9 import * as store from '../src/store.js';
10 import { startFakeServer } from './fixtures/fake-server.js';
11 
12 let server;
13 
14 beforeEach(async () => {
15   process.env.IDEAMINE_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'ideamine-publish-'));
16   server = await startFakeServer();
17   process.env.IDEAMINE_EMBED_URL = `${server.url}/v1`;
18   for (const name of ['IDEAMINE_PUBLISH_URL', 'IDEAMINE_EMBED_MODEL', 'IDEAMINE_GROUP_THRESHOLD', 'IDEAMINE_SEARCH_THRESHOLD']) delete process.env[name];
19 });
20 
21 afterEach(() => server.close());
22 
23 const T = (h) => `2026-09-21T${String(h).padStart(2, '0')}:00:00.000Z`;
24 
25 test('timeline phases follow the inbox, the queue, and the work', () => {
26   const base = { id: 1, created: T(1), status: 'inbox' };
27   assert.deepEqual(publish.phases(base), [{ phase: 'inbox', from: T(1), to: null }]);
28   const queued = { ...base, status: 'triaged', triage: { at: T(2) } };
29   assert.deepEqual(publish.phases(queued), [
30     { phase: 'inbox', from: T(1), to: T(2) },
31     { phase: 'queued', from: T(2), to: null },
32   ]);
33   const done = { ...queued, status: 'done', started: T(3), closed: T(5) };
34   assert.deepEqual(publish.phases(done), [
35     { phase: 'inbox', from: T(1), to: T(2) },
36     { phase: 'queued', from: T(2), to: T(3) },
37     { phase: 'doing', from: T(3), to: T(5) },
38   ]);
39   // Done before start times were recorded: the work bar is a guess, and says so.
40   assert.deepEqual(publish.phases({ ...queued, status: 'done', closed: T(5) }), [
41     { phase: 'inbox', from: T(1), to: T(2) },
42     { phase: 'doing', from: T(2), to: T(5), estimated: true },
43   ]);
44   assert.deepEqual(publish.phases({ ...base, status: 'dropped', closed: T(4) }), [{ phase: 'inbox', from: T(1), to: T(4) }]);
45 });
46 
47 test('the snapshot has a ticket for each idea, groups, related ideas, and the vectors', async () => {
48   process.env.IDEAMINE_GROUP_THRESHOLD = '0.5';
49   store.addIdeas(['subtitles for audiobooks', 'subtitles for audiobooks on android', 'a tor exit relay'], { project: '/work/app' });
50   store.applyTriage([{ id: 3, verdict: 'do', impact: 4, size: 's', model: 'haiku', title: 'Tor relay', why: 'w', brief: 'b' }]);
51   store.updateIdea(3, { status: 'doing' });
52   const { data, note } = await publish.build(store.load());
53   assert.equal(note, '');
54   assert.equal(data.version, 1);
55   assert.equal(data.embed.available, true);
56   assert.equal(data.embed.query_prefix, 'search_query: ');
57   assert.equal(data.counts.doing, 1);
58   const [one, two, three] = data.ideas;
59   assert.deepEqual([one.key, one.lane, one.project, three.lane, three.model], ['IDEA-1', 'inbox', 'app', 'doing', 'haiku']);
60   assert.deepEqual(data.ideas.map((i) => i.rank), [2, 1, 0]); // doing first, then the inbox, newest first
61   assert.deepEqual(data.groups, [{ id: 0, label: data.groups[0].label, ids: [1, 2] }]);
62   assert.deepEqual([one.group, two.group, three.group], [0, 0, null]);
63   assert.equal(one.related[0].id, 2);
64   assert.ok(three.phases.some((p) => p.phase === 'doing' && p.from === three.started));
65   const vectors = await embed.vectorsFor(store.load().ideas);
66   assert.deepEqual([...embed.decodeVec(one.vec)], [...vectors.get(1)]);
67 });
68 
69 test('without the embedding server, the snapshot has no groups or vectors, and says why', async () => {
70   store.addIdeas(['one idea']);
71   server.options.down = true;
72   const { data, note } = await publish.build(store.load());
73   assert.match(note, /without search by meaning or groups: .*503/);
74   assert.equal(data.embed.available, false);
75   assert.deepEqual([data.groups, data.ideas[0].vec, data.ideas[0].related], [[], null, []]);
76 });
77 
78 test('publish uploads data.json and index.html with PUT, and kick() starts again only after a change', async () => {
79   store.addIdeas(['one idea']);
80   let started = 0;
81   const kick = () => publish.kick({ start: () => started++ });
82   assert.equal(kick(), false); // no dashboard server set
83   config.set('publish_url', `${server.url}/dash`);
84   assert.equal(kick(), true); // never published
85   const out = await publish.publish();
86   assert.deepEqual([out.where, out.ideas], [`${server.url}/dash`, 1]);
87   assert.deepEqual([...server.files.keys()], ['/dash/data.json', '/dash/index.html']);
88   assert.equal(JSON.parse(server.files.get('/dash/data.json')).ideas[0].title, 'one idea');
89   assert.match(server.files.get('/dash/index.html'), /<html/i);
90   assert.equal(kick(), false); // nothing changed
91   await new Promise((resolve) => setTimeout(resolve, 20)); // a later modification time
92   store.addIdeas(['two']);
93   assert.equal(kick(), true);
94   assert.equal(started, 2);
95 });
96 
97 test('a failed background publish is recorded, and kick() waits before the next try', async () => {
98   store.addIdeas(['one idea']);
99   const closed = await startFakeServer();
100   await closed.close();
101   config.set('publish_url', `${closed.url}/`);
102   assert.equal(await publish.backgroundPublish(), 'error');
103   assert.match(publish.readState().error, /cannot reach/);
104   assert.equal(publish.kick({ start: () => assert.fail('must wait') }), false);
105   assert.match(publish.status(), /failed/);
106 });
107 
108 test('a background publish that cannot start is recorded, and the caller goes on', async () => {
109   store.addIdeas(['one idea']);
110   config.set('publish_url', `${server.url}/`);
111   const node = process.execPath;
112   process.execPath = path.join(store.home(), 'no-such-node');
113   try {
114     assert.equal(publish.kick(), true);
115     await new Promise((resolve) => setTimeout(resolve, 300)); // the spawn error comes later
116   } finally {
117     process.execPath = node;
118   }
119   assert.match(publish.readState().error, /^cannot start a publish: .*ENOENT/);
120   assert.equal(publish.kick({ start: () => assert.fail('must wait after the error') }), false);
121 });
122 
123 test('publish --dir writes both files to a folder', async () => {
124   store.addIdeas(['one idea']);
125   const dir = path.join(store.home(), 'out');
126   const out = await publish.publish({ dir });
127   assert.equal(out.where, dir);
128   assert.deepEqual(fs.readdirSync(dir).sort(), ['data.json', 'index.html']);
129 });
130 
131 test('config: the environment wins over the file, and an empty value restores the default', () => {
132   assert.equal(config.get('search_threshold'), 0.5);
133   assert.equal(config.set('search_threshold', '0.42'), 0.42);
134   process.env.IDEAMINE_SEARCH_THRESHOLD = '0.9';
135   assert.equal(config.get('search_threshold'), 0.9);
136   delete process.env.IDEAMINE_SEARCH_THRESHOLD;
137   assert.equal(config.set('search_threshold', ''), 0.5);
138   assert.throws(() => config.set('search_threshold', '2'), /from 0 to 1/);
139   assert.throws(() => config.get('nope'), /unknown setting/);
140   assert.match(config.describe(), /^embed_url\s+http:\/\/127\.0\.0\.1:\d+\/v1\s+IDEAMINE_EMBED_URL/);
141 });