@polderlabs/bizar 4.0.0 → 4.2.1
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/README.md +11 -14
- package/bizar-dash/CHANGELOG.md +1 -1
- package/bizar-dash/src/server/api.mjs +2 -2
- package/bizar-dash/src/server/artifacts-store.mjs +4 -4
- package/bizar-dash/src/server/memory-git.mjs +142 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
- package/bizar-dash/src/server/memory-store.mjs +129 -10
- package/bizar-dash/src/server/mod-security.mjs +2 -2
- package/bizar-dash/src/server/routes/memory.mjs +174 -15
- package/bizar-dash/src/server/server.mjs +1 -1
- package/bizar-dash/src/server/state.mjs +2 -2
- package/bizar-dash/src/web/views/Config.tsx +461 -1
- package/bizar-dash/tests/memory-cli-readlistdelete.test.mjs +405 -0
- package/bizar-dash/tests/memory-cli-setup.test.mjs +382 -0
- package/bizar-dash/tests/memory-cli.test.mjs +542 -0
- package/bizar-dash/tests/memory-config.test.mjs +422 -0
- package/bizar-dash/tests/memory-conflicts.test.mjs +229 -0
- package/bizar-dash/tests/memory-git.test.mjs +109 -1
- package/bizar-dash/tests/memory-lightrag.test.mjs +153 -0
- package/bizar-dash/tests/memory-namespace.test.mjs +404 -0
- package/bizar-dash/tests/memory-path-safety.test.mjs +427 -0
- package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
- package/bizar-dash/tests/memory-roundtrip.test.mjs +219 -0
- package/cli/banner.mjs +1 -1
- package/cli/bin.mjs +4 -4
- package/cli/bootstrap.mjs +1 -1
- package/cli/copy.mjs +22 -16
- package/cli/doctor.mjs +4 -4
- package/cli/doctor.test.mjs +2 -2
- package/cli/init.mjs +2 -2
- package/cli/install.mjs +21 -16
- package/cli/memory.mjs +952 -37
- package/cli/utils.mjs +6 -3
- package/config/AGENTS.md +7 -7
- package/config/agents/_shared/AGENT_BASELINE.md +59 -61
- package/config/opencode.json +13 -38
- package/config/skills/memory-protocol/SKILL.md +105 -0
- package/config/skills/obsidian/SKILL.md +58 -1
- package/install.sh +11 -1
- package/package.json +2 -2
- package/plugins/bizar/index.ts +7 -0
- package/plugins/bizar/src/commands.ts +42 -1
- package/plugins/bizar/src/tools/open-kb.ts +191 -0
- package/plugins/bizar/tests/commands.test.ts +36 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/memory-config.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* Tests for writeLightRAGConfig, patch-mode POST /memory/config,
|
|
5
|
+
* redact behaviour, and LightRAG lifecycle endpoints.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
13
|
+
import http from 'node:http';
|
|
14
|
+
|
|
15
|
+
const httpGet = (port, path) =>
|
|
16
|
+
new Promise((resolve) => {
|
|
17
|
+
const req = http.request({ hostname: 'localhost', port, path, method: 'GET' }, (res) => {
|
|
18
|
+
let body = '';
|
|
19
|
+
res.on('data', (c) => (body += c));
|
|
20
|
+
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(body || '{}') }));
|
|
21
|
+
});
|
|
22
|
+
req.end();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const httpPost = (port, path, body) =>
|
|
26
|
+
new Promise((resolve) => {
|
|
27
|
+
const data = JSON.stringify(body);
|
|
28
|
+
const req = http.request(
|
|
29
|
+
{ hostname: 'localhost', port, path, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
|
|
30
|
+
(res) => {
|
|
31
|
+
let respBody = '';
|
|
32
|
+
res.on('data', (c) => (respBody += c));
|
|
33
|
+
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(respBody || '{}') }));
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
req.write(data);
|
|
37
|
+
req.end();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
writeLightRAGConfig,
|
|
42
|
+
resolveLightRAGConfig,
|
|
43
|
+
} = await import('../src/server/memory-lightrag.mjs');
|
|
44
|
+
|
|
45
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function makeTmp() {
|
|
48
|
+
const root = join(tmpdir(), `bizar-lightrag-cfg-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
49
|
+
mkdirSync(root, { recursive: true });
|
|
50
|
+
mkdirSync(join(root, '.bizar'), { recursive: true });
|
|
51
|
+
return root;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function buildApp(projectRoot) {
|
|
55
|
+
const { createMemoryRouter } = await import('../src/server/routes/memory.mjs');
|
|
56
|
+
const express = (await import('express')).default;
|
|
57
|
+
const app = express();
|
|
58
|
+
app.use(express.json());
|
|
59
|
+
app.use('/api', createMemoryRouter({ projectRoot }));
|
|
60
|
+
return app;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function withApp(projectRoot, fn) {
|
|
64
|
+
const app = await buildApp(projectRoot);
|
|
65
|
+
const server = app.listen(0);
|
|
66
|
+
const port = server.address().port;
|
|
67
|
+
try {
|
|
68
|
+
return await fn(port);
|
|
69
|
+
} finally {
|
|
70
|
+
server.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── writeLightRAGConfig — unit tests ─────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe('writeLightRAGConfig', () => {
|
|
77
|
+
let root;
|
|
78
|
+
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
root = makeTmp();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
try { rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('deep-merges into existing lightrag block', () => {
|
|
88
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify({
|
|
89
|
+
version: 1,
|
|
90
|
+
lightrag: { enabled: false, port: 8000, llmModel: 'gpt-4o' },
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const result = writeLightRAGConfig(root, { enabled: true, port: 9000 });
|
|
94
|
+
assert.equal(result.ok, true);
|
|
95
|
+
|
|
96
|
+
const mem = JSON.parse(readFileSync(join(root, '.bizar', 'memory.json'), 'utf8'));
|
|
97
|
+
assert.equal(mem.lightrag.enabled, true);
|
|
98
|
+
assert.equal(mem.lightrag.port, 9000);
|
|
99
|
+
assert.equal(mem.lightrag.llmModel, 'gpt-4o'); // preserved
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('validates llmBinding is in known set', () => {
|
|
103
|
+
const result = writeLightRAGConfig(root, { llmBinding: 'not-a-binding' });
|
|
104
|
+
assert.equal(result.ok, false);
|
|
105
|
+
assert.ok(result.error.includes('llmBinding'));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('validates embeddingBinding is in known set', () => {
|
|
109
|
+
const result = writeLightRAGConfig(root, { embeddingBinding: 'bad-embedding' });
|
|
110
|
+
assert.equal(result.ok, false);
|
|
111
|
+
assert.ok(result.error.includes('embeddingBinding'));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('rejects port out of range (0)', () => {
|
|
115
|
+
const result = writeLightRAGConfig(root, { port: 0 });
|
|
116
|
+
assert.equal(result.ok, false);
|
|
117
|
+
assert.ok(result.error.includes('port'));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('rejects port out of range (66666)', () => {
|
|
121
|
+
const result = writeLightRAGConfig(root, { port: 66666 });
|
|
122
|
+
assert.equal(result.ok, false);
|
|
123
|
+
assert.ok(result.error.includes('port'));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('accepts port 1 and 65535', () => {
|
|
127
|
+
let r = writeLightRAGConfig(root, { port: 1 });
|
|
128
|
+
assert.equal(r.ok, true);
|
|
129
|
+
r = writeLightRAGConfig(root, { port: 65535 });
|
|
130
|
+
assert.equal(r.ok, true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('canonicalises <empty> to empty string for apiKey', () => {
|
|
134
|
+
const r = writeLightRAGConfig(root, { apiKey: '<empty>' });
|
|
135
|
+
assert.equal(r.ok, true);
|
|
136
|
+
const mem = JSON.parse(readFileSync(join(root, '.bizar', 'memory.json'), 'utf8'));
|
|
137
|
+
assert.equal(mem.lightrag.apiKey, '');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('atomic — old or new file present after concurrent writes', () => {
|
|
141
|
+
writeLightRAGConfig(root, { port: 10000 });
|
|
142
|
+
for (let i = 0; i < 20; i++) {
|
|
143
|
+
writeLightRAGConfig(root, { port: i + 1 });
|
|
144
|
+
}
|
|
145
|
+
const content = readFileSync(join(root, '.bizar', 'memory.json'), 'utf8');
|
|
146
|
+
const parsed = JSON.parse(content);
|
|
147
|
+
assert.equal(parsed.lightrag.port, 20);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('creates .bizar/memory.json if missing', () => {
|
|
151
|
+
const fresh = join(tmpdir(), `bizar-fresh-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
152
|
+
mkdirSync(fresh, { recursive: true });
|
|
153
|
+
try {
|
|
154
|
+
const r = writeLightRAGConfig(fresh, { port: 5000 });
|
|
155
|
+
assert.equal(r.ok, true);
|
|
156
|
+
assert.ok(existsSync(join(fresh, '.bizar', 'memory.json')));
|
|
157
|
+
const mem = JSON.parse(readFileSync(join(fresh, '.bizar', 'memory.json'), 'utf8'));
|
|
158
|
+
assert.equal(mem.lightrag.port, 5000);
|
|
159
|
+
} finally {
|
|
160
|
+
rmSync(fresh, { recursive: true, force: true });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('preserves non-lightrag fields when merging', () => {
|
|
165
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify({
|
|
166
|
+
version: 1,
|
|
167
|
+
projectId: 'test-proj',
|
|
168
|
+
memoryRepo: { mode: 'local-only' },
|
|
169
|
+
lightrag: { port: 7000 },
|
|
170
|
+
}));
|
|
171
|
+
const r = writeLightRAGConfig(root, { llmModel: 'claude-3' });
|
|
172
|
+
assert.equal(r.ok, true);
|
|
173
|
+
const mem = JSON.parse(readFileSync(join(root, '.bizar', 'memory.json'), 'utf8'));
|
|
174
|
+
assert.equal(mem.projectId, 'test-proj');
|
|
175
|
+
assert.equal(mem.lightrag.llmModel, 'claude-3');
|
|
176
|
+
assert.equal(mem.lightrag.port, 7000);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// ── resolveLightRAGConfig — apiKey defaults ───────────────────────────────────
|
|
181
|
+
|
|
182
|
+
describe('resolveLightRAGConfig apiKey defaults', () => {
|
|
183
|
+
let root;
|
|
184
|
+
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
root = makeTmp();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
afterEach(() => {
|
|
190
|
+
try { rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('defaults apiKey to "env" when not set', () => {
|
|
194
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify({
|
|
195
|
+
lightrag: { port: 9621 },
|
|
196
|
+
}));
|
|
197
|
+
const cfg = resolveLightRAGConfig(root);
|
|
198
|
+
assert.equal(cfg.apiKey, 'env');
|
|
199
|
+
assert.equal(cfg.apiKeySource, 'env');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('reads existing apiKeySource', () => {
|
|
203
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify({
|
|
204
|
+
lightrag: { port: 9621, apiKey: 'sk-123', apiKeySource: 'file' },
|
|
205
|
+
}));
|
|
206
|
+
const cfg = resolveLightRAGConfig(root);
|
|
207
|
+
assert.equal(cfg.apiKey, 'sk-123');
|
|
208
|
+
assert.equal(cfg.apiKeySource, 'file');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ── HTTP route integration tests ─────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
describe('POST /api/memory/config patch mode', () => {
|
|
215
|
+
let root;
|
|
216
|
+
|
|
217
|
+
beforeEach(() => {
|
|
218
|
+
root = makeTmp();
|
|
219
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify({
|
|
220
|
+
version: 1,
|
|
221
|
+
lightrag: { port: 9621, llmModel: 'original-model' },
|
|
222
|
+
}));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
afterEach(() => {
|
|
226
|
+
try { rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('returns merged config with redacted apiKey', async () => {
|
|
230
|
+
writeLightRAGConfig(root, { apiKey: 'sk-secret123', apiKeySource: 'file' });
|
|
231
|
+
|
|
232
|
+
await withApp(root, async (port) => {
|
|
233
|
+
const res = await httpPost(port, '/api/memory/config', {
|
|
234
|
+
patch: true,
|
|
235
|
+
lightrag: { port: 5000 },
|
|
236
|
+
});
|
|
237
|
+
assert.equal(res.status, 200);
|
|
238
|
+
assert.equal(res.body.ok, true);
|
|
239
|
+
assert.equal(res.body.config.lightrag.port, 5000);
|
|
240
|
+
assert.equal(res.body.config.lightrag.llmModel, 'original-model');
|
|
241
|
+
// apiKey must be redacted
|
|
242
|
+
assert.equal(res.body.config.lightrag.apiKey, '***');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('rejects patch without lightrag field', async () => {
|
|
247
|
+
await withApp(root, async (port) => {
|
|
248
|
+
const res = await httpPost(port, '/api/memory/config', { patch: true, foo: 'bar' });
|
|
249
|
+
assert.equal(res.status, 400);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('rejects unknown top-level fields in patch', async () => {
|
|
254
|
+
await withApp(root, async (port) => {
|
|
255
|
+
const res = await httpPost(port, '/api/memory/config', { patch: true, lightrag: { port: 1 }, mode: 'managed' });
|
|
256
|
+
assert.equal(res.status, 400);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('validates port range through patch', async () => {
|
|
261
|
+
await withApp(root, async (port) => {
|
|
262
|
+
const res = await httpPost(port, '/api/memory/config', { patch: true, lightrag: { port: 99999 } });
|
|
263
|
+
assert.equal(res.status, 400);
|
|
264
|
+
assert.ok(res.body.message.includes('port'));
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('validates llmBinding through patch', async () => {
|
|
269
|
+
await withApp(root, async (port) => {
|
|
270
|
+
const res = await httpPost(port, '/api/memory/config', { patch: true, lightrag: { llmBinding: 'invalid' } });
|
|
271
|
+
assert.equal(res.status, 400);
|
|
272
|
+
assert.ok(res.body.message.includes('llmBinding'));
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('GET /api/memory/config — redaction', () => {
|
|
278
|
+
let root;
|
|
279
|
+
|
|
280
|
+
beforeEach(() => {
|
|
281
|
+
root = makeTmp();
|
|
282
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify({
|
|
283
|
+
version: 1,
|
|
284
|
+
lightrag: { port: 9621, apiKey: 'sk-abc123', apiKeySource: 'file' },
|
|
285
|
+
}));
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
afterEach(() => {
|
|
289
|
+
try { rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('GET /memory/config redacts apiKey to ***', async () => {
|
|
293
|
+
await withApp(root, async (port) => {
|
|
294
|
+
const res = await httpGet(port, '/api/memory/config');
|
|
295
|
+
assert.equal(res.status, 200);
|
|
296
|
+
assert.equal(res.body.config.lightrag.apiKey, '***');
|
|
297
|
+
assert.equal(res.body.config.lightrag.apiKeySource, 'file');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('GET /api/memory/lightrag/status', () => {
|
|
303
|
+
let root;
|
|
304
|
+
|
|
305
|
+
beforeEach(() => {
|
|
306
|
+
root = makeTmp();
|
|
307
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify({
|
|
308
|
+
version: 1,
|
|
309
|
+
lightrag: { port: 9621, llmBinding: 'ollama', embeddingBinding: 'ollama', llmModel: 'test-model', embeddingModel: 'test-embed' },
|
|
310
|
+
}));
|
|
311
|
+
mkdirSync(join(root, '.bizar', 'lightrag'), { recursive: true });
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
afterEach(() => {
|
|
315
|
+
try { rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('returns running:false when server not running', async () => {
|
|
319
|
+
await withApp(root, async (port) => {
|
|
320
|
+
const res = await httpGet(port, '/api/memory/lightrag/status');
|
|
321
|
+
assert.equal(res.status, 200);
|
|
322
|
+
assert.equal(res.body.running, false);
|
|
323
|
+
assert.equal(res.body.pid, null);
|
|
324
|
+
assert.equal(res.body.port, 9621);
|
|
325
|
+
assert.equal(res.body.llmBinding, 'ollama');
|
|
326
|
+
assert.equal(res.body.embeddingBinding, 'ollama');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('POST /api/memory/lightrag/start', () => {
|
|
332
|
+
let root;
|
|
333
|
+
|
|
334
|
+
beforeEach(() => {
|
|
335
|
+
root = makeTmp();
|
|
336
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify({
|
|
337
|
+
version: 1,
|
|
338
|
+
lightrag: { port: 9621 },
|
|
339
|
+
}));
|
|
340
|
+
mkdirSync(join(root, '.bizar', 'lightrag'), { recursive: true });
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
afterEach(() => {
|
|
344
|
+
try { rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('returns ok:false with error when lightrag-server not installed (or ok:true if it is)', async () => {
|
|
348
|
+
await withApp(root, async (port) => {
|
|
349
|
+
const res = await httpPost(port, '/api/memory/lightrag/start', {});
|
|
350
|
+
assert.equal(res.status, 200);
|
|
351
|
+
assert.equal(typeof res.body.ok, 'boolean');
|
|
352
|
+
if (!res.body.ok) {
|
|
353
|
+
assert.ok(res.body.error != null);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('POST /api/memory/lightrag/stop', () => {
|
|
360
|
+
let root;
|
|
361
|
+
|
|
362
|
+
beforeEach(() => {
|
|
363
|
+
root = makeTmp();
|
|
364
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify({
|
|
365
|
+
version: 1,
|
|
366
|
+
lightrag: { port: 9621 },
|
|
367
|
+
}));
|
|
368
|
+
mkdirSync(join(root, '.bizar', 'lightrag'), { recursive: true });
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
afterEach(() => {
|
|
372
|
+
try { rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('returns ok:true when nothing is running', async () => {
|
|
376
|
+
await withApp(root, async (port) => {
|
|
377
|
+
const res = await httpPost(port, '/api/memory/lightrag/stop', {});
|
|
378
|
+
assert.equal(res.status, 200);
|
|
379
|
+
assert.equal(res.body.ok, true);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe('GET /api/memory/lightrag/log', () => {
|
|
385
|
+
let root;
|
|
386
|
+
|
|
387
|
+
beforeEach(() => {
|
|
388
|
+
root = makeTmp();
|
|
389
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify({
|
|
390
|
+
version: 1,
|
|
391
|
+
lightrag: { port: 9621 },
|
|
392
|
+
}));
|
|
393
|
+
mkdirSync(join(root, '.bizar', 'lightrag'), { recursive: true });
|
|
394
|
+
// Write a log file with 60 lines
|
|
395
|
+
const lines = Array.from({ length: 60 }, (_, i) => `log line ${i + 1}`).join('\n');
|
|
396
|
+
writeFileSync(join(root, '.bizar', 'lightrag', 'lightrag.log'), lines);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
afterEach(() => {
|
|
400
|
+
try { rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('returns last 200 lines', async () => {
|
|
404
|
+
await withApp(root, async (port) => {
|
|
405
|
+
const res = await httpGet(port, '/api/memory/lightrag/log');
|
|
406
|
+
assert.equal(res.status, 200);
|
|
407
|
+
assert.ok(Array.isArray(res.body.lines));
|
|
408
|
+
assert.ok(res.body.lines.length <= 200);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('returns empty lines when no log file', async () => {
|
|
413
|
+
rmSync(join(root, '.bizar', 'lightrag', 'lightrag.log'), { force: true });
|
|
414
|
+
|
|
415
|
+
await withApp(root, async (port) => {
|
|
416
|
+
const res = await httpGet(port, '/api/memory/lightrag/log');
|
|
417
|
+
assert.equal(res.status, 200);
|
|
418
|
+
assert.ok(Array.isArray(res.body.lines));
|
|
419
|
+
assert.equal(res.body.lines.length, 0);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/memory-conflicts.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* Tests for the `bizar memory conflicts` CLI command — cmdConflicts at
|
|
5
|
+
* cli/memory.mjs:1219-1254.
|
|
6
|
+
*
|
|
7
|
+
* Two conflict detection mechanisms:
|
|
8
|
+
* 1. Frontmatter status=conflict
|
|
9
|
+
* 2. Git conflict markers in body: <<<<<<< HEAD / ======= / >>>>>>> <branch>
|
|
10
|
+
*
|
|
11
|
+
* Gap closed: memory-system-map §8.1 CRITICAL gap #2 "Conflict detection &
|
|
12
|
+
* resolution — cmdConflicts has no test".
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { join, dirname } from 'node:path';
|
|
19
|
+
import { mkdirSync, rmSync, writeFileSync, appendFileSync } from 'node:fs';
|
|
20
|
+
import { execFileSync } from 'node:child_process';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const CLI_BIN = join(dirname(__filename), '..', '..', 'cli', 'bin.mjs');
|
|
25
|
+
const TEST_MEMORY_STORE = await import('../src/server/memory-store.mjs').then((m) => m);
|
|
26
|
+
|
|
27
|
+
const uid = () => `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Spawn `bizar memory conflicts` in the given cwd.
|
|
31
|
+
* Returns { code, stdout, stderr }.
|
|
32
|
+
*/
|
|
33
|
+
function runBizarMemoryConflicts(cwd) {
|
|
34
|
+
try {
|
|
35
|
+
const stdout = execFileSync('node', [CLI_BIN, 'memory', 'conflicts'], {
|
|
36
|
+
cwd,
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
stdio: 'pipe',
|
|
39
|
+
timeout: 15_000,
|
|
40
|
+
});
|
|
41
|
+
return { code: 0, stdout, stderr: '' };
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return {
|
|
44
|
+
code: err.status ?? 1,
|
|
45
|
+
stdout: err.stdout ? err.stdout.toString() : '',
|
|
46
|
+
stderr: err.stderr ? err.stderr.toString() : (err.message ?? ''),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('bizar memory conflicts CLI', () => {
|
|
52
|
+
let projectRoot;
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
projectRoot = join(tmpdir(), `mem-conflicts-${uid()}`);
|
|
56
|
+
mkdirSync(projectRoot, { recursive: true });
|
|
57
|
+
mkdirSync(join(projectRoot, '.bizar'), { recursive: true });
|
|
58
|
+
|
|
59
|
+
// Write a minimal memory.json config
|
|
60
|
+
const config = {
|
|
61
|
+
version: 1,
|
|
62
|
+
backend: 'bizar-local',
|
|
63
|
+
projectId: 'conflict-test',
|
|
64
|
+
memoryRepo: {
|
|
65
|
+
mode: 'local-only',
|
|
66
|
+
path: join(projectRoot, '.obsidian'),
|
|
67
|
+
remote: null,
|
|
68
|
+
branch: 'main',
|
|
69
|
+
namespace: null,
|
|
70
|
+
},
|
|
71
|
+
namespaces: {
|
|
72
|
+
project: 'projects/conflict-test',
|
|
73
|
+
global: 'global/bizar',
|
|
74
|
+
user: 'users/tester',
|
|
75
|
+
},
|
|
76
|
+
lightrag: { enabled: false, host: '127.0.0.1', port: 9621, workingDir: join(projectRoot, '.bizar', 'lightrag') },
|
|
77
|
+
git: { autoPullOnSessionStart: false, autoCommitOnMemoryWrite: false, autoPushOnSessionEnd: false, commitAuthor: 'Bizar Memory <bizar-memory@local>', commitMessageTemplate: 'memory(conflict-test): {summary}' },
|
|
78
|
+
};
|
|
79
|
+
writeFileSync(join(projectRoot, '.bizar', 'memory.json'), JSON.stringify(config));
|
|
80
|
+
// Init the vault
|
|
81
|
+
mkdirSync(join(projectRoot, '.obsidian'), { recursive: true });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
try { rmSync(projectRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Write a note directly to disk (bypassing the API) so we can inject
|
|
90
|
+
* arbitrary frontmatter and body content including conflict markers.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} relPath - relative path inside .obsidian/
|
|
93
|
+
* @param {object} frontmatter - key-value pairs
|
|
94
|
+
* @param {string} body - body content (actual newlines, not \n escapes)
|
|
95
|
+
* @param {function|null} [postWrite] - optional(filePath) callback to modify the file after writing
|
|
96
|
+
*/
|
|
97
|
+
function writeNoteFile(relPath, frontmatter, body, postWrite = null) {
|
|
98
|
+
const filePath = join(projectRoot, '.obsidian', relPath);
|
|
99
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
100
|
+
// Write the note using the store (which handles YAML formatting correctly)
|
|
101
|
+
TEST_MEMORY_STORE.writeNote(projectRoot, relPath, { frontmatter, body });
|
|
102
|
+
if (postWrite) postWrite(filePath);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
it('reports frontmatter status=conflict as a conflict', () => {
|
|
106
|
+
writeNoteFile('conflict-fm.md',
|
|
107
|
+
{
|
|
108
|
+
memory_id: 'conflict_fm',
|
|
109
|
+
type: 'session_summary',
|
|
110
|
+
project_id: 'conflict-test',
|
|
111
|
+
status: 'conflict', // ← the conflict marker
|
|
112
|
+
confidence: 'verified',
|
|
113
|
+
created: new Date().toISOString(),
|
|
114
|
+
updated: new Date().toISOString(),
|
|
115
|
+
tags: [],
|
|
116
|
+
},
|
|
117
|
+
'This note has a frontmatter conflict status.',
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const r = runBizarMemoryConflicts(projectRoot);
|
|
121
|
+
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}; stderr=${r.stderr}`);
|
|
122
|
+
const output = `${r.stdout}\n${r.stderr}`;
|
|
123
|
+
assert.ok(
|
|
124
|
+
output.includes('conflict-fm.md'),
|
|
125
|
+
`output should mention conflict-fm.md; got: ${output}`,
|
|
126
|
+
);
|
|
127
|
+
assert.ok(
|
|
128
|
+
/frontmatter.*conflict|conflict.*frontmatter/i.test(output),
|
|
129
|
+
`output should mention frontmatter conflict; got: ${output}`,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('reports git conflict markers in body as a conflict', () => {
|
|
134
|
+
writeNoteFile('conflict-git.md',
|
|
135
|
+
{
|
|
136
|
+
memory_id: 'conflict_git',
|
|
137
|
+
type: 'session_summary',
|
|
138
|
+
project_id: 'conflict-test',
|
|
139
|
+
status: 'active',
|
|
140
|
+
confidence: 'verified',
|
|
141
|
+
created: new Date().toISOString(),
|
|
142
|
+
updated: new Date().toISOString(),
|
|
143
|
+
tags: [],
|
|
144
|
+
},
|
|
145
|
+
'This note has git conflict markers.',
|
|
146
|
+
(filePath) => {
|
|
147
|
+
// Append git conflict markers to the file after writing
|
|
148
|
+
appendFileSync(filePath, '\n<<<<<<< HEAD\nLocal change here.\n=======\nRemote change here.\n>>>>>>> remote-branch\n');
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const r = runBizarMemoryConflicts(projectRoot);
|
|
153
|
+
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}; stderr=${r.stderr}`);
|
|
154
|
+
const output = `${r.stdout}\n${r.stderr}`;
|
|
155
|
+
assert.ok(
|
|
156
|
+
output.includes('conflict-git.md'),
|
|
157
|
+
`output should mention conflict-git.md; got: ${output}`,
|
|
158
|
+
);
|
|
159
|
+
assert.ok(
|
|
160
|
+
/git conflict|conflict marker/i.test(output),
|
|
161
|
+
`output should mention git conflict; got: ${output}`,
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('reports both conflict types simultaneously', () => {
|
|
166
|
+
writeNoteFile('both-conflicts.md',
|
|
167
|
+
{
|
|
168
|
+
memory_id: 'both_conflicts',
|
|
169
|
+
type: 'session_summary',
|
|
170
|
+
project_id: 'conflict-test',
|
|
171
|
+
status: 'conflict',
|
|
172
|
+
confidence: 'verified',
|
|
173
|
+
created: new Date().toISOString(),
|
|
174
|
+
updated: new Date().toISOString(),
|
|
175
|
+
tags: [],
|
|
176
|
+
},
|
|
177
|
+
'Body with conflict markers.',
|
|
178
|
+
(filePath) => {
|
|
179
|
+
appendFileSync(filePath, '\n<<<<<<< HEAD\nLocal.\n=======\nRemote.\n>>>>>>> branch\n');
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const r = runBizarMemoryConflicts(projectRoot);
|
|
184
|
+
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}; stderr=${r.stderr}`);
|
|
185
|
+
const output = `${r.stdout}\n${r.stderr}`;
|
|
186
|
+
assert.ok(
|
|
187
|
+
output.includes('both-conflicts.md'),
|
|
188
|
+
`output should mention both-conflicts.md; got: ${output}`,
|
|
189
|
+
);
|
|
190
|
+
assert.ok(
|
|
191
|
+
/frontmatter.*conflict|conflict.*frontmatter/i.test(output),
|
|
192
|
+
`output should report frontmatter conflict; got: ${output}`,
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('outputs "no conflicts found" when vault is clean', () => {
|
|
197
|
+
writeNoteFile('clean-note.md',
|
|
198
|
+
{
|
|
199
|
+
memory_id: 'clean_note',
|
|
200
|
+
type: 'session_summary',
|
|
201
|
+
project_id: 'conflict-test',
|
|
202
|
+
status: 'active',
|
|
203
|
+
confidence: 'verified',
|
|
204
|
+
created: new Date().toISOString(),
|
|
205
|
+
updated: new Date().toISOString(),
|
|
206
|
+
tags: [],
|
|
207
|
+
},
|
|
208
|
+
'This is a perfectly normal, non-conflicted note.',
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const r = runBizarMemoryConflicts(projectRoot);
|
|
212
|
+
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}; stderr=${r.stderr}`);
|
|
213
|
+
const output = `${r.stdout}\n${r.stderr}`;
|
|
214
|
+
assert.ok(
|
|
215
|
+
/no conflicts? found/i.test(output),
|
|
216
|
+
`output should say "no conflicts found"; got: ${output}`,
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('handles empty vault gracefully', () => {
|
|
221
|
+
const r = runBizarMemoryConflicts(projectRoot);
|
|
222
|
+
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}; stderr=${r.stderr}`);
|
|
223
|
+
const output = `${r.stdout}\n${r.stderr}`;
|
|
224
|
+
assert.ok(
|
|
225
|
+
/no conflicts? found/i.test(output),
|
|
226
|
+
`empty vault should say "no conflicts found"; got: ${output}`,
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
});
|