create-walle 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-walle.js +134 -0
- package/package.json +18 -0
- package/template/.env.example +40 -0
- package/template/CLAUDE.md +12 -0
- package/template/LICENSE +21 -0
- package/template/README.md +167 -0
- package/template/bin/setup.js +100 -0
- package/template/claude-code-skill.md +60 -0
- package/template/claude-task-manager/api-prompts.js +1841 -0
- package/template/claude-task-manager/api-reviews.js +275 -0
- package/template/claude-task-manager/approval-agent.js +454 -0
- package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
- package/template/claude-task-manager/db.js +1721 -0
- package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
- package/template/claude-task-manager/git-utils.js +214 -0
- package/template/claude-task-manager/package-lock.json +1607 -0
- package/template/claude-task-manager/package.json +31 -0
- package/template/claude-task-manager/prompt-harvest.js +1148 -0
- package/template/claude-task-manager/public/css/prompts.css +880 -0
- package/template/claude-task-manager/public/css/reviews.css +430 -0
- package/template/claude-task-manager/public/css/walle.css +732 -0
- package/template/claude-task-manager/public/favicon.ico +0 -0
- package/template/claude-task-manager/public/icon.svg +37 -0
- package/template/claude-task-manager/public/index.html +8346 -0
- package/template/claude-task-manager/public/js/prompts.js +3159 -0
- package/template/claude-task-manager/public/js/reviews.js +1292 -0
- package/template/claude-task-manager/public/js/walle.js +3081 -0
- package/template/claude-task-manager/public/manifest.json +13 -0
- package/template/claude-task-manager/public/prompts.html +4353 -0
- package/template/claude-task-manager/public/setup.html +216 -0
- package/template/claude-task-manager/queue-engine.js +404 -0
- package/template/claude-task-manager/server-state.js +5 -0
- package/template/claude-task-manager/server.js +2254 -0
- package/template/claude-task-manager/session-utils.js +124 -0
- package/template/claude-task-manager/start.sh +17 -0
- package/template/claude-task-manager/tests/test-ai-search.js +61 -0
- package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
- package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
- package/template/claude-task-manager/tests/test-features-v2.js +127 -0
- package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
- package/template/claude-task-manager/tests/test-insights.js +124 -0
- package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
- package/template/claude-task-manager/tests/test-permissions.js +122 -0
- package/template/claude-task-manager/tests/test-pin.js +51 -0
- package/template/claude-task-manager/tests/test-prompts.js +164 -0
- package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
- package/template/claude-task-manager/tests/test-review.js +104 -0
- package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
- package/template/claude-task-manager/tests/test-send-final.js +30 -0
- package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
- package/template/claude-task-manager/tests/test-send-integration.js +107 -0
- package/template/claude-task-manager/tests/test-send-visual.js +34 -0
- package/template/claude-task-manager/tests/test-session-create.js +147 -0
- package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
- package/template/claude-task-manager/tests/test-url-hash.js +68 -0
- package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
- package/template/claude-task-manager/tests/test-ux-review.js +130 -0
- package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
- package/template/claude-task-manager/tests/test-zoom.js +92 -0
- package/template/claude-task-manager/tests/test-zoom2.js +67 -0
- package/template/docs/site/api/README.md +187 -0
- package/template/docs/site/guides/claude-code.md +58 -0
- package/template/docs/site/guides/configuration.md +96 -0
- package/template/docs/site/guides/quickstart.md +158 -0
- package/template/docs/site/index.md +14 -0
- package/template/docs/site/skills/README.md +135 -0
- package/template/wall-e/.dockerignore +11 -0
- package/template/wall-e/Dockerfile +25 -0
- package/template/wall-e/adapters/adapter-base.js +37 -0
- package/template/wall-e/adapters/ctm.js +193 -0
- package/template/wall-e/adapters/slack.js +56 -0
- package/template/wall-e/agent.js +319 -0
- package/template/wall-e/api-walle.js +1073 -0
- package/template/wall-e/brain.js +1235 -0
- package/template/wall-e/channels/agent-api.js +172 -0
- package/template/wall-e/channels/channel-base.js +14 -0
- package/template/wall-e/channels/imessage-channel.js +113 -0
- package/template/wall-e/channels/slack-channel.js +118 -0
- package/template/wall-e/chat.js +778 -0
- package/template/wall-e/decision/confidence.js +93 -0
- package/template/wall-e/deploy.sh +35 -0
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
- package/template/wall-e/extraction/contradiction.js +168 -0
- package/template/wall-e/extraction/knowledge-extractor.js +190 -0
- package/template/wall-e/fly.toml +24 -0
- package/template/wall-e/loops/ingest.js +34 -0
- package/template/wall-e/loops/reflect.js +63 -0
- package/template/wall-e/loops/tasks.js +487 -0
- package/template/wall-e/loops/think.js +125 -0
- package/template/wall-e/package-lock.json +533 -0
- package/template/wall-e/package.json +18 -0
- package/template/wall-e/scripts/ingest-slack-search.js +85 -0
- package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
- package/template/wall-e/scripts/slack-backfill.js +295 -0
- package/template/wall-e/scripts/slack-channel-history.js +454 -0
- package/template/wall-e/server.js +93 -0
- package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
- package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
- package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
- package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
- package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
- package/template/wall-e/skills/claude-code-reader.js +144 -0
- package/template/wall-e/skills/mcp-client.js +407 -0
- package/template/wall-e/skills/skill-executor.js +163 -0
- package/template/wall-e/skills/skill-loader.js +410 -0
- package/template/wall-e/skills/skill-planner.js +88 -0
- package/template/wall-e/skills/slack-ingest.js +329 -0
- package/template/wall-e/skills/slack-pull-live.js +270 -0
- package/template/wall-e/skills/tool-executor.js +188 -0
- package/template/wall-e/tests/adapter-base.test.js +20 -0
- package/template/wall-e/tests/adapter-ctm.test.js +122 -0
- package/template/wall-e/tests/adapter-slack.test.js +98 -0
- package/template/wall-e/tests/agent-api.test.js +256 -0
- package/template/wall-e/tests/api-walle.test.js +222 -0
- package/template/wall-e/tests/brain.test.js +602 -0
- package/template/wall-e/tests/channels.test.js +104 -0
- package/template/wall-e/tests/chat.test.js +103 -0
- package/template/wall-e/tests/confidence.test.js +134 -0
- package/template/wall-e/tests/contradiction.test.js +217 -0
- package/template/wall-e/tests/ingest.test.js +113 -0
- package/template/wall-e/tests/mcp-client.test.js +71 -0
- package/template/wall-e/tests/reflect.test.js +103 -0
- package/template/wall-e/tests/server.test.js +111 -0
- package/template/wall-e/tests/skills.test.js +198 -0
- package/template/wall-e/tests/slack-ingest.test.js +103 -0
- package/template/wall-e/tests/think.test.js +435 -0
- package/template/wall-e/tools/local-tools.js +697 -0
- package/template/wall-e/tools/slack-mcp.js +290 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
const { describe, it, before, after } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const brain = require('../brain.js');
|
|
8
|
+
|
|
9
|
+
let testDir;
|
|
10
|
+
let testDbPath;
|
|
11
|
+
|
|
12
|
+
function setupTestDb() {
|
|
13
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wall-e-test-'));
|
|
14
|
+
testDbPath = path.join(testDir, 'test-brain.db');
|
|
15
|
+
brain.initDb(testDbPath);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function teardownTestDb() {
|
|
19
|
+
try { brain.closeDb(); } catch (_) {}
|
|
20
|
+
if (testDir && fs.existsSync(testDir)) {
|
|
21
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Task 2: Schema and Init ────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
describe('Brain DB Schema and Init', () => {
|
|
28
|
+
before(() => setupTestDb());
|
|
29
|
+
after(() => teardownTestDb());
|
|
30
|
+
|
|
31
|
+
it('creates database with WAL mode', () => {
|
|
32
|
+
const db = brain.getDb();
|
|
33
|
+
const row = db.pragma('journal_mode', { simple: true });
|
|
34
|
+
assert.equal(row, 'wal');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('has foreign keys enabled', () => {
|
|
38
|
+
const db = brain.getDb();
|
|
39
|
+
const row = db.pragma('foreign_keys', { simple: true });
|
|
40
|
+
assert.equal(row, 1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('has all 19 tables', () => {
|
|
44
|
+
const db = brain.getDb();
|
|
45
|
+
const tables = db.prepare(
|
|
46
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
|
47
|
+
).all().map(r => r.name);
|
|
48
|
+
|
|
49
|
+
const expected = [
|
|
50
|
+
'agent_actions', 'agent_exchanges', 'briefing_items', 'chat_messages', 'code_identity', 'current_state',
|
|
51
|
+
'daily_summaries', 'domain_confidence', 'knowledge', 'loop_checkpoints', 'memories',
|
|
52
|
+
'owner', 'patterns', 'pending_questions', 'people', 'personas',
|
|
53
|
+
'skill_executions', 'skills', 'tasks'
|
|
54
|
+
].sort();
|
|
55
|
+
|
|
56
|
+
assert.deepEqual(tables, expected);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('has at least 18 indexes', () => {
|
|
60
|
+
const db = brain.getDb();
|
|
61
|
+
const indexes = db.prepare(
|
|
62
|
+
"SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'"
|
|
63
|
+
).all();
|
|
64
|
+
assert.ok(indexes.length >= 18, `Expected >= 18 indexes, got ${indexes.length}`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('has idx_memories_source index', () => {
|
|
68
|
+
const db = brain.getDb();
|
|
69
|
+
const idx = db.prepare(
|
|
70
|
+
"SELECT name FROM sqlite_master WHERE type='index' AND name='idx_memories_source'"
|
|
71
|
+
).get();
|
|
72
|
+
assert.ok(idx, 'idx_memories_source should exist');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('has idx_knowledge_active_subject index', () => {
|
|
76
|
+
const db = brain.getDb();
|
|
77
|
+
const idx = db.prepare(
|
|
78
|
+
"SELECT name FROM sqlite_master WHERE type='index' AND name='idx_knowledge_active_subject'"
|
|
79
|
+
).get();
|
|
80
|
+
assert.ok(idx, 'idx_knowledge_active_subject should exist');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('has idx_actions_status index', () => {
|
|
84
|
+
const db = brain.getDb();
|
|
85
|
+
const idx = db.prepare(
|
|
86
|
+
"SELECT name FROM sqlite_master WHERE type='index' AND name='idx_actions_status'"
|
|
87
|
+
).get();
|
|
88
|
+
assert.ok(idx, 'idx_actions_status should exist');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ── Task 3: CRUD Helpers ────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
describe('Owner config', () => {
|
|
95
|
+
before(() => setupTestDb());
|
|
96
|
+
after(() => teardownTestDb());
|
|
97
|
+
|
|
98
|
+
it('setOwner then getOwner returns value', () => {
|
|
99
|
+
brain.setOwner('name', 'Test Owner');
|
|
100
|
+
assert.equal(brain.getOwner('name'), 'Test Owner');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('getOwnerName returns name', () => {
|
|
104
|
+
brain.setOwner('name', 'Test Owner');
|
|
105
|
+
assert.equal(brain.getOwnerName(), 'Test Owner');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('getOwner for nonexistent key returns null', () => {
|
|
109
|
+
assert.equal(brain.getOwner('nonexistent'), null);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('Memory CRUD', () => {
|
|
114
|
+
before(() => setupTestDb());
|
|
115
|
+
after(() => teardownTestDb());
|
|
116
|
+
|
|
117
|
+
it('insertMemory returns id and getMemory retrieves it', () => {
|
|
118
|
+
const result = brain.insertMemory({
|
|
119
|
+
source: 'slack',
|
|
120
|
+
source_id: 'msg-001',
|
|
121
|
+
memory_type: 'message_received',
|
|
122
|
+
content: 'Can you review?',
|
|
123
|
+
timestamp: '2026-03-22T10:00:00Z'
|
|
124
|
+
});
|
|
125
|
+
assert.ok(result.id, 'should return an id');
|
|
126
|
+
const mem = brain.getMemory(result.id);
|
|
127
|
+
assert.equal(mem.source, 'slack');
|
|
128
|
+
assert.equal(mem.source_id, 'msg-001');
|
|
129
|
+
assert.equal(mem.content, 'Can you review?');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('inserting same source+source_id twice returns null (dedup)', () => {
|
|
133
|
+
brain.insertMemory({
|
|
134
|
+
source: 'slack',
|
|
135
|
+
source_id: 'msg-dup',
|
|
136
|
+
memory_type: 'message_received',
|
|
137
|
+
content: 'first',
|
|
138
|
+
timestamp: '2026-03-22T10:00:00Z'
|
|
139
|
+
});
|
|
140
|
+
const dup = brain.insertMemory({
|
|
141
|
+
source: 'slack',
|
|
142
|
+
source_id: 'msg-dup',
|
|
143
|
+
memory_type: 'message_received',
|
|
144
|
+
content: 'second',
|
|
145
|
+
timestamp: '2026-03-22T10:01:00Z'
|
|
146
|
+
});
|
|
147
|
+
assert.equal(dup, null);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('listMemories filters by source', () => {
|
|
151
|
+
brain.insertMemory({
|
|
152
|
+
source: 'email',
|
|
153
|
+
source_id: 'em-001',
|
|
154
|
+
memory_type: 'message_received',
|
|
155
|
+
content: 'Email content',
|
|
156
|
+
timestamp: '2026-03-22T10:00:00Z'
|
|
157
|
+
});
|
|
158
|
+
const results = brain.listMemories({ source: 'email' });
|
|
159
|
+
assert.ok(results.length >= 1);
|
|
160
|
+
assert.ok(results.every(r => r.source === 'email'));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('listMemories filters by since', () => {
|
|
164
|
+
brain.insertMemory({
|
|
165
|
+
source: 'slack',
|
|
166
|
+
source_id: 'msg-old',
|
|
167
|
+
memory_type: 'message_received',
|
|
168
|
+
content: 'Old message',
|
|
169
|
+
timestamp: '2026-03-21T08:00:00Z'
|
|
170
|
+
});
|
|
171
|
+
const results = brain.listMemories({ since: '2026-03-22T09:00:00Z' });
|
|
172
|
+
assert.ok(results.length >= 1);
|
|
173
|
+
assert.ok(results.every(r => r.timestamp >= '2026-03-22T09:00:00Z'));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('insertMemory with missing source throws', () => {
|
|
177
|
+
assert.throws(() => {
|
|
178
|
+
brain.insertMemory({
|
|
179
|
+
memory_type: 'message_received',
|
|
180
|
+
content: 'No source',
|
|
181
|
+
timestamp: '2026-03-22T10:00:00Z'
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('Knowledge CRUD', () => {
|
|
188
|
+
before(() => setupTestDb());
|
|
189
|
+
after(() => teardownTestDb());
|
|
190
|
+
|
|
191
|
+
it('insertKnowledge returns id', () => {
|
|
192
|
+
const result = brain.insertKnowledge({
|
|
193
|
+
category: 'technical',
|
|
194
|
+
subject: 'TestUser',
|
|
195
|
+
predicate: 'prefers',
|
|
196
|
+
object: 'SQLite'
|
|
197
|
+
});
|
|
198
|
+
assert.ok(result.id);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('findKnowledge returns matching results', () => {
|
|
202
|
+
brain.insertKnowledge({
|
|
203
|
+
category: 'technical',
|
|
204
|
+
subject: 'TestUser',
|
|
205
|
+
predicate: 'uses',
|
|
206
|
+
object: 'Node.js'
|
|
207
|
+
});
|
|
208
|
+
const results = brain.findKnowledge({ subject: 'TestUser', status: 'active' });
|
|
209
|
+
assert.ok(results.length >= 1);
|
|
210
|
+
assert.ok(results.every(r => r.subject === 'TestUser'));
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('insertKnowledge with missing required fields throws', () => {
|
|
214
|
+
assert.throws(() => {
|
|
215
|
+
brain.insertKnowledge({
|
|
216
|
+
category: 'technical',
|
|
217
|
+
subject: 'TestUser'
|
|
218
|
+
// missing predicate and object
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('Checkpoint CRUD', () => {
|
|
225
|
+
before(() => setupTestDb());
|
|
226
|
+
after(() => teardownTestDb());
|
|
227
|
+
|
|
228
|
+
it('upsertCheckpoint then getCheckpoint returns it', () => {
|
|
229
|
+
brain.upsertCheckpoint('ingest', { last_memory_at: '2026-03-22T10:00:00Z' });
|
|
230
|
+
const cp = brain.getCheckpoint('ingest');
|
|
231
|
+
assert.ok(cp);
|
|
232
|
+
assert.equal(cp.loop_name, 'ingest');
|
|
233
|
+
assert.equal(cp.last_memory_at, '2026-03-22T10:00:00Z');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('upserting again updates existing row', () => {
|
|
237
|
+
brain.upsertCheckpoint('ingest', { last_memory_at: '2026-03-22T11:00:00Z' });
|
|
238
|
+
const cp = brain.getCheckpoint('ingest');
|
|
239
|
+
assert.equal(cp.last_memory_at, '2026-03-22T11:00:00Z');
|
|
240
|
+
|
|
241
|
+
// Should still be just one row
|
|
242
|
+
const db = brain.getDb();
|
|
243
|
+
const count = db.prepare("SELECT COUNT(*) as cnt FROM loop_checkpoints WHERE loop_name='ingest'").get();
|
|
244
|
+
assert.equal(count.cnt, 1);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ── Task 4: Daily Backup ────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
describe('backup', () => {
|
|
251
|
+
before(() => setupTestDb());
|
|
252
|
+
after(() => teardownTestDb());
|
|
253
|
+
|
|
254
|
+
it('createBackup returns backupPath and backupName', () => {
|
|
255
|
+
const result = brain.createBackup('test');
|
|
256
|
+
assert.ok(result.backupPath, 'should have backupPath');
|
|
257
|
+
assert.ok(result.backupName, 'should have backupName');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('backup file exists at backupPath', () => {
|
|
261
|
+
const result = brain.createBackup('test');
|
|
262
|
+
assert.ok(fs.existsSync(result.backupPath), 'backup file should exist');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('backupName starts with wall-e-brain-', () => {
|
|
266
|
+
const result = brain.createBackup('test');
|
|
267
|
+
assert.ok(result.backupName.startsWith('wall-e-brain-'), `Expected backupName to start with 'wall-e-brain-', got '${result.backupName}'`);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('backupName includes -test', () => {
|
|
271
|
+
const result = brain.createBackup('test');
|
|
272
|
+
assert.ok(result.backupName.includes('-test'), `Expected backupName to include '-test', got '${result.backupName}'`);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ── Phase 2 Task 1: People CRUD ──────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
describe('People CRUD', () => {
|
|
279
|
+
before(() => setupTestDb());
|
|
280
|
+
after(() => teardownTestDb());
|
|
281
|
+
|
|
282
|
+
it('insertPerson returns id and getPerson retrieves it', () => {
|
|
283
|
+
const result = brain.insertPerson({ name: 'Alice', relationship: 'colleague' });
|
|
284
|
+
assert.ok(result.id);
|
|
285
|
+
const person = brain.getPerson(result.id);
|
|
286
|
+
assert.equal(person.name, 'Alice');
|
|
287
|
+
assert.equal(person.relationship, 'colleague');
|
|
288
|
+
assert.equal(person.trust_level, 0.5);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('insertPerson with all fields', () => {
|
|
292
|
+
const result = brain.insertPerson({
|
|
293
|
+
name: 'Bob',
|
|
294
|
+
relationship: 'friend',
|
|
295
|
+
aliases: 'Bobby,Robert',
|
|
296
|
+
identities: 'bob@example.com',
|
|
297
|
+
trust_level: 0.9,
|
|
298
|
+
notes: 'Met at conference'
|
|
299
|
+
});
|
|
300
|
+
const person = brain.getPerson(result.id);
|
|
301
|
+
assert.equal(person.aliases, 'Bobby,Robert');
|
|
302
|
+
assert.equal(person.trust_level, 0.9);
|
|
303
|
+
assert.equal(person.notes, 'Met at conference');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('insertPerson with missing name throws', () => {
|
|
307
|
+
assert.throws(() => brain.insertPerson({ relationship: 'colleague' }), /name/);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('getPerson returns null for nonexistent id', () => {
|
|
311
|
+
assert.equal(brain.getPerson('nonexistent-id'), null);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('listPeople returns all people', () => {
|
|
315
|
+
const all = brain.listPeople();
|
|
316
|
+
assert.ok(all.length >= 2);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('listPeople filters by relationship', () => {
|
|
320
|
+
const friends = brain.listPeople({ relationship: 'friend' });
|
|
321
|
+
assert.ok(friends.length >= 1);
|
|
322
|
+
assert.ok(friends.every(p => p.relationship === 'friend'));
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('listPeople respects limit', () => {
|
|
326
|
+
const limited = brain.listPeople({ limit: 1 });
|
|
327
|
+
assert.equal(limited.length, 1);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('updatePerson updates allowed fields', () => {
|
|
331
|
+
const { id } = brain.insertPerson({ name: 'Charlie' });
|
|
332
|
+
brain.updatePerson(id, { trust_level: 0.8, trust_reason: 'Reliable', notes: 'Updated' });
|
|
333
|
+
const person = brain.getPerson(id);
|
|
334
|
+
assert.equal(person.trust_level, 0.8);
|
|
335
|
+
assert.equal(person.trust_reason, 'Reliable');
|
|
336
|
+
assert.equal(person.notes, 'Updated');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('updatePerson rejects invalid fields', () => {
|
|
340
|
+
const { id } = brain.insertPerson({ name: 'Dave' });
|
|
341
|
+
assert.throws(() => brain.updatePerson(id, { name: 'Evil' }), /Invalid update fields/);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('updatePerson throws for nonexistent person', () => {
|
|
345
|
+
assert.throws(() => brain.updatePerson('nonexistent', { notes: 'x' }), /Person not found/);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('updatePerson throws with empty updates', () => {
|
|
349
|
+
const { id } = brain.insertPerson({ name: 'Eve' });
|
|
350
|
+
assert.throws(() => brain.updatePerson(id, {}), /No updates provided/);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// ── Phase 2 Task 1: Pending Questions ────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
describe('Pending Questions', () => {
|
|
357
|
+
before(() => setupTestDb());
|
|
358
|
+
after(() => teardownTestDb());
|
|
359
|
+
|
|
360
|
+
it('insertQuestion returns id', () => {
|
|
361
|
+
const result = brain.insertQuestion({
|
|
362
|
+
question_type: 'clarification',
|
|
363
|
+
question: 'What timezone do you prefer?',
|
|
364
|
+
context: 'scheduling',
|
|
365
|
+
priority: 'high'
|
|
366
|
+
});
|
|
367
|
+
assert.ok(result.id);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('insertQuestion with missing question_type throws', () => {
|
|
371
|
+
assert.throws(() => brain.insertQuestion({ question: 'Hello?' }), /question_type/);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('insertQuestion with missing question throws', () => {
|
|
375
|
+
assert.throws(() => brain.insertQuestion({ question_type: 'clarification' }), /question/);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('listQuestions returns questions ordered by created_at DESC', () => {
|
|
379
|
+
brain.insertQuestion({ question_type: 'preference', question: 'First?' });
|
|
380
|
+
brain.insertQuestion({ question_type: 'preference', question: 'Second?' });
|
|
381
|
+
const list = brain.listQuestions({ question_type: 'preference' });
|
|
382
|
+
assert.ok(list.length >= 2);
|
|
383
|
+
// Most recent first
|
|
384
|
+
assert.ok(list[0].created_at >= list[list.length - 1].created_at);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('listQuestions filters by status', () => {
|
|
388
|
+
const pending = brain.listQuestions({ status: 'pending' });
|
|
389
|
+
assert.ok(pending.length >= 1);
|
|
390
|
+
assert.ok(pending.every(q => q.status === 'pending'));
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('listQuestions respects limit', () => {
|
|
394
|
+
const limited = brain.listQuestions({ limit: 1 });
|
|
395
|
+
assert.equal(limited.length, 1);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('answerQuestion updates status and answer', () => {
|
|
399
|
+
const { id } = brain.insertQuestion({ question_type: 'clarification', question: 'Timezone?' });
|
|
400
|
+
brain.answerQuestion(id, { answer: 'UTC-5', resolution_type: 'answered', resolution_evidence: 'User said so' });
|
|
401
|
+
const questions = brain.listQuestions({ status: 'answered' });
|
|
402
|
+
const q = questions.find(q => q.id === id);
|
|
403
|
+
assert.ok(q);
|
|
404
|
+
assert.equal(q.answer, 'UTC-5');
|
|
405
|
+
assert.equal(q.resolution_type, 'answered');
|
|
406
|
+
assert.ok(q.answered_at);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('answerQuestion with inferred resolution_type sets status to inferred', () => {
|
|
410
|
+
const { id } = brain.insertQuestion({ question_type: 'preference', question: 'Theme?' });
|
|
411
|
+
brain.answerQuestion(id, { answer: 'dark', resolution_type: 'inferred' });
|
|
412
|
+
const questions = brain.listQuestions({ status: 'inferred' });
|
|
413
|
+
const q = questions.find(q => q.id === id);
|
|
414
|
+
assert.ok(q);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('answerQuestion with invalid resolution_type throws', () => {
|
|
418
|
+
const { id } = brain.insertQuestion({ question_type: 'clarification', question: 'Color?' });
|
|
419
|
+
assert.throws(() => brain.answerQuestion(id, { answer: 'blue', resolution_type: 'invalid' }), /Invalid resolution_type/);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('answerQuestion with missing answer throws', () => {
|
|
423
|
+
const { id } = brain.insertQuestion({ question_type: 'clarification', question: 'Size?' });
|
|
424
|
+
assert.throws(() => brain.answerQuestion(id, { resolution_type: 'answered' }), /Answer is required/);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('answerQuestion for nonexistent question throws', () => {
|
|
428
|
+
assert.throws(() => brain.answerQuestion('nonexistent', { answer: 'x', resolution_type: 'answered' }), /Question not found/);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// ── Phase 2 Task 1: Knowledge updates ────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
describe('Knowledge updates', () => {
|
|
435
|
+
before(() => setupTestDb());
|
|
436
|
+
after(() => teardownTestDb());
|
|
437
|
+
|
|
438
|
+
it('supersedeKnowledge marks old as superseded', () => {
|
|
439
|
+
const old = brain.insertKnowledge({ category: 'preference', subject: 'TestUser', predicate: 'prefers_editor', object: 'VSCode' });
|
|
440
|
+
const newer = brain.insertKnowledge({ category: 'preference', subject: 'TestUser', predicate: 'prefers_editor', object: 'Cursor' });
|
|
441
|
+
brain.supersedeKnowledge(old.id, newer.id);
|
|
442
|
+
const oldK = brain.getKnowledge(old.id);
|
|
443
|
+
assert.equal(oldK.status, 'superseded');
|
|
444
|
+
assert.equal(oldK.superseded_by, newer.id);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('supersedeKnowledge throws for nonexistent id', () => {
|
|
448
|
+
const { id } = brain.insertKnowledge({ category: 'technical', subject: 'x', predicate: 'y', object: 'z' });
|
|
449
|
+
assert.throws(() => brain.supersedeKnowledge('nonexistent', id), /Knowledge not found/);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('updateKnowledgeStatus sets valid status', () => {
|
|
453
|
+
const { id } = brain.insertKnowledge({ category: 'technical', subject: 'a', predicate: 'b', object: 'c' });
|
|
454
|
+
brain.updateKnowledgeStatus(id, 'disputed');
|
|
455
|
+
assert.equal(brain.getKnowledge(id).status, 'disputed');
|
|
456
|
+
brain.updateKnowledgeStatus(id, 'retracted');
|
|
457
|
+
assert.equal(brain.getKnowledge(id).status, 'retracted');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('updateKnowledgeStatus throws for invalid status', () => {
|
|
461
|
+
const { id } = brain.insertKnowledge({ category: 'technical', subject: 'd', predicate: 'e', object: 'f' });
|
|
462
|
+
assert.throws(() => brain.updateKnowledgeStatus(id, 'bogus'), /Invalid knowledge status/);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('updateKnowledgeStatus throws for nonexistent id', () => {
|
|
466
|
+
assert.throws(() => brain.updateKnowledgeStatus('nonexistent', 'active'), /Knowledge not found/);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// ── Phase 2 Task 1: Brain Stats ──────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
describe('Brain Stats', () => {
|
|
473
|
+
before(() => setupTestDb());
|
|
474
|
+
after(() => teardownTestDb());
|
|
475
|
+
|
|
476
|
+
it('getBrainStats returns all counts', () => {
|
|
477
|
+
// Seed some data
|
|
478
|
+
brain.insertMemory({ source: 'test', memory_type: 'note', content: 'hello', timestamp: '2026-01-01T00:00:00Z' });
|
|
479
|
+
brain.insertKnowledge({ category: 'technical', subject: 'x', predicate: 'y', object: 'z' });
|
|
480
|
+
brain.insertPerson({ name: 'Stat Person' });
|
|
481
|
+
brain.insertQuestion({ question_type: 'test', question: 'Q?' });
|
|
482
|
+
|
|
483
|
+
const stats = brain.getBrainStats();
|
|
484
|
+
assert.equal(typeof stats.memory_count, 'number');
|
|
485
|
+
assert.ok(stats.memory_count >= 1);
|
|
486
|
+
assert.equal(typeof stats.knowledge_count, 'number');
|
|
487
|
+
assert.ok(stats.knowledge_count >= 1);
|
|
488
|
+
assert.equal(typeof stats.people_count, 'number');
|
|
489
|
+
assert.ok(stats.people_count >= 1);
|
|
490
|
+
assert.equal(typeof stats.pattern_count, 'number');
|
|
491
|
+
assert.equal(typeof stats.pending_question_count, 'number');
|
|
492
|
+
assert.ok(stats.pending_question_count >= 1);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// ── Phase 2 Task 1: Daily Summaries ──────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
describe('Daily Summaries', () => {
|
|
499
|
+
before(() => setupTestDb());
|
|
500
|
+
after(() => teardownTestDb());
|
|
501
|
+
|
|
502
|
+
it('insertDailySummary and getDailySummary', () => {
|
|
503
|
+
const result = brain.insertDailySummary('2026-03-22', 'Good day', { memories: 10 });
|
|
504
|
+
assert.ok(result.id);
|
|
505
|
+
const summary = brain.getDailySummary('2026-03-22');
|
|
506
|
+
assert.equal(summary.date, '2026-03-22');
|
|
507
|
+
assert.equal(summary.summary, 'Good day');
|
|
508
|
+
assert.equal(JSON.parse(summary.stats).memories, 10);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('getDailySummary returns null for nonexistent date', () => {
|
|
512
|
+
assert.equal(brain.getDailySummary('1999-01-01'), null);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('insertDailySummary with missing date throws', () => {
|
|
516
|
+
assert.throws(() => brain.insertDailySummary(null, 'summary'), /date/);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('insertDailySummary with missing summary throws', () => {
|
|
520
|
+
assert.throws(() => brain.insertDailySummary('2026-03-23', null), /summary/);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('insertDailySummary duplicate date throws', () => {
|
|
524
|
+
brain.insertDailySummary('2026-03-24', 'First');
|
|
525
|
+
assert.throws(() => brain.insertDailySummary('2026-03-24', 'Second'));
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('listDailySummaries returns ordered by date DESC', () => {
|
|
529
|
+
brain.insertDailySummary('2026-03-25', 'Day 25');
|
|
530
|
+
brain.insertDailySummary('2026-03-26', 'Day 26');
|
|
531
|
+
const list = brain.listDailySummaries();
|
|
532
|
+
assert.ok(list.length >= 2);
|
|
533
|
+
assert.ok(list[0].date >= list[1].date);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('listDailySummaries respects limit', () => {
|
|
537
|
+
const limited = brain.listDailySummaries(1);
|
|
538
|
+
assert.equal(limited.length, 1);
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
describe('Briefing Items', () => {
|
|
543
|
+
before(() => { setupTestDb(); });
|
|
544
|
+
after(() => { teardownTestDb(); });
|
|
545
|
+
|
|
546
|
+
it('insertBriefingItem creates and returns id', () => {
|
|
547
|
+
const result = brain.insertBriefingItem({
|
|
548
|
+
task_id: 'task-1', skill: 'morning-briefing', title: 'Test item',
|
|
549
|
+
category: 'hiring', owner: 'TestUser', urgency: 'today',
|
|
550
|
+
context: JSON.stringify({ search_queries: ['test query'] }),
|
|
551
|
+
});
|
|
552
|
+
assert.ok(result.id);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('getBriefingItem retrieves item', () => {
|
|
556
|
+
const inserted = brain.insertBriefingItem({
|
|
557
|
+
skill: 'morning-briefing', title: 'Another item', urgency: 'async',
|
|
558
|
+
});
|
|
559
|
+
const item = brain.getBriefingItem(inserted.id);
|
|
560
|
+
assert.equal(item.title, 'Another item');
|
|
561
|
+
assert.equal(item.urgency, 'async');
|
|
562
|
+
assert.equal(item.status, 'open');
|
|
563
|
+
assert.equal(item.times_seen, 1);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('findBriefingItemByTitle matches open items', () => {
|
|
567
|
+
const found = brain.findBriefingItemByTitle('morning-briefing', 'Test item');
|
|
568
|
+
assert.ok(found);
|
|
569
|
+
assert.equal(found.title, 'Test item');
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('findBriefingItemByTitle does not match done items', () => {
|
|
573
|
+
const item = brain.insertBriefingItem({ skill: 'test', title: 'Done item' });
|
|
574
|
+
brain.updateBriefingItem(item.id, { status: 'done', resolved_at: new Date().toISOString() });
|
|
575
|
+
const found = brain.findBriefingItemByTitle('test', 'Done item');
|
|
576
|
+
assert.equal(found, null);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('updateBriefingItem updates fields', () => {
|
|
580
|
+
const item = brain.insertBriefingItem({ skill: 'test', title: 'Update me' });
|
|
581
|
+
brain.updateBriefingItem(item.id, { status: 'snoozed', snooze_until: '2026-04-01T07:00:00Z', notes: 'will check later' });
|
|
582
|
+
const updated = brain.getBriefingItem(item.id);
|
|
583
|
+
assert.equal(updated.status, 'snoozed');
|
|
584
|
+
assert.equal(updated.notes, 'will check later');
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('listBriefingItems filters by skill and status', () => {
|
|
588
|
+
const all = brain.listBriefingItems({ skill: 'morning-briefing' });
|
|
589
|
+
assert.ok(all.length >= 2);
|
|
590
|
+
const open = brain.listBriefingItems({ skill: 'morning-briefing', status: 'open' });
|
|
591
|
+
assert.ok(open.every(function(i) { return i.status === 'open'; }));
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('listOpenBriefingItems excludes done and dismissed', () => {
|
|
595
|
+
const open = brain.listOpenBriefingItems('morning-briefing');
|
|
596
|
+
assert.ok(open.every(function(i) { return i.status !== 'done' && i.status !== 'dismissed'; }));
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('insertBriefingItem requires title', () => {
|
|
600
|
+
assert.throws(() => brain.insertBriefingItem({ skill: 'test' }), /title/);
|
|
601
|
+
});
|
|
602
|
+
});
|