@polderlabs/bizar 5.4.0 → 5.5.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/bizar-dash/dist/assets/{EnvVarsSection-DqvB8ZcJ.js → EnvVarsSection-L413CQEI.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-DqvB8ZcJ.js.map → EnvVarsSection-L413CQEI.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js → MobileChat-0kGQb1S6.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-DRXP2ERQ.js.map → MobileChat-0kGQb1S6.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-Cq1oFIdO.js +1 -0
- package/bizar-dash/dist/assets/{MobileSettings-CVW9VhEo.js.map → MobileSettings-Cq1oFIdO.js.map} +1 -1
- package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js → Toast-C6f1zFJ2.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-CcZVVN4i.js.map → Toast-C6f1zFJ2.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-OMEGx5KA.js → icons-BsWXWkEF.js} +121 -126
- package/bizar-dash/dist/assets/icons-BsWXWkEF.js.map +1 -0
- package/bizar-dash/dist/assets/main-BklYy02e.css +1 -0
- package/bizar-dash/dist/assets/main-CBTgYSeG.js +18 -0
- package/bizar-dash/dist/assets/main-CBTgYSeG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-CsaN4CxH.js +1 -0
- package/bizar-dash/dist/assets/{mobile-XN2P7YfM.js.map → mobile-CsaN4CxH.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-layout-D6V-7WnA.css → mobile-layout-CJnZNLy3.css} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-iFBdjEE4.js.map +1 -0
- package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js → useSlashCommands-DGJcC9iQ.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-iyoW95UB.js.map → useSlashCommands-DGJcC9iQ.js.map} +1 -1
- package/bizar-dash/dist/index.html +8 -8
- package/bizar-dash/dist/mobile.html +4 -4
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/backup-store.mjs +6 -1
- package/bizar-dash/src/server/bg-spawner.mjs +574 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +242 -0
- package/bizar-dash/src/server/memory-store.mjs +43 -3
- package/bizar-dash/src/server/routes/background.mjs +143 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +10 -0
- package/bizar-dash/src/server/routes/memory.mjs +8 -0
- package/bizar-dash/src/server/server.mjs +22 -0
- package/bizar-dash/src/web/App.tsx +3 -0
- package/bizar-dash/src/web/MobileApp.tsx +1 -10
- package/bizar-dash/src/web/components/InstallConfirmDialog.tsx +1 -1
- package/bizar-dash/src/web/components/SettingsNav.tsx +1 -10
- package/bizar-dash/src/web/lib/types.ts +39 -1
- package/bizar-dash/src/web/styles/main.css +43 -0
- package/bizar-dash/src/web/styles/mobile-layout.css +7 -5
- package/bizar-dash/src/web/styles/settings.css +46 -0
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +399 -30
- package/bizar-dash/src/web/views/Settings.tsx +12 -12
- package/bizar-dash/src/web/views/SpawnAgentModal.tsx +221 -0
- package/bizar-dash/tests/BackgroundAgents.test.tsx +160 -0
- package/bizar-dash/tests/background-pause-resume.test.mjs +46 -0
- package/bizar-dash/tests/background-spawn-from-ui.test.mjs +36 -0
- package/bizar-dash/tests/background-steer.test.mjs +42 -0
- package/bizar-dash/tests/lightrag-startup-hook.test.mjs +206 -0
- package/bizar-dash/tests/memory-auto-reindex.test.mjs +196 -0
- package/bizar-dash/tests/memory-cli.test.mjs +1 -1
- package/bizar-dash/tests/memory-default-vault.test.mjs +90 -3
- package/bizar-dash/tests/memory-lightrag-extended.test.mjs +131 -0
- package/bizar-dash/tests/mobile-layout.test.tsx +36 -0
- package/bizar-dash/tests/settings-layout.test.tsx +3 -3
- package/bizar-dash/tests/settings-nav.test.tsx +7 -5
- package/cli/bin.mjs +16 -0
- package/cli/commands/deploy/docker.mjs +2 -1
- package/cli/commands/lightrag.mjs +171 -0
- package/cli/commands/service.mjs +7 -0
- package/cli/commands/util.mjs +8 -1
- package/cli/init.mjs +1 -1
- package/cli/post-install-smoke.mjs +230 -0
- package/cli/provision.mjs +177 -0
- package/cli/provision.test.mjs +180 -0
- package/cli/service-controller.mjs +378 -28
- package/cli/service-controller.test.mjs +100 -0
- package/cli/service-env.mjs +139 -0
- package/cli/service.mjs +23 -0
- package/config/agents/_shared/AGENT_BASELINE.md +6 -6
- package/install.sh +87 -6
- package/package.json +1 -1
- package/plugins/bizar/index.ts +147 -0
- package/plugins/bizar/src/background-state.ts +84 -5
- package/plugins/bizar/src/background.ts +388 -10
- package/plugins/bizar/src/hooks/memory-inject.ts +247 -0
- package/plugins/bizar/src/hooks/memory-write-on-end.ts +188 -0
- package/plugins/bizar/src/opencode-runner.ts +87 -1
- package/plugins/bizar/src/tools/bg-pause.ts +77 -0
- package/plugins/bizar/src/tools/bg-report-progress.ts +101 -0
- package/plugins/bizar/src/tools/bg-resume.ts +72 -0
- package/plugins/bizar/src/tools/bg-send-message.ts +105 -0
- package/plugins/bizar/src/tools/bg-spawn.ts +13 -6
- package/plugins/bizar/src/tools/bg-status.ts +10 -0
- package/plugins/bizar/src/tools/memory-list.ts +135 -0
- package/plugins/bizar/src/tools/memory-read.ts +142 -0
- package/plugins/bizar/src/tools/memory-search.ts +228 -0
- package/plugins/bizar/src/tools/memory-write.ts +183 -0
- package/plugins/bizar/tests/memory-write-on-end.test.ts +92 -0
- package/plugins/bizar/tests/tools/bg-pause.test.ts +64 -0
- package/plugins/bizar/tests/tools/bg-report-progress.test.ts +82 -0
- package/plugins/bizar/tests/tools/bg-resume.test.ts +43 -0
- package/plugins/bizar/tests/tools/bg-send-message.test.ts +46 -0
- package/bizar-dash/dist/assets/MobileSettings-CVW9VhEo.js +0 -1
- package/bizar-dash/dist/assets/icons-OMEGx5KA.js.map +0 -1
- package/bizar-dash/dist/assets/main-CMAGZouj.js +0 -16
- package/bizar-dash/dist/assets/main-CMAGZouj.js.map +0 -1
- package/bizar-dash/dist/assets/main-O2tZVdHm.css +0 -1
- package/bizar-dash/dist/assets/mobile-XN2P7YfM.js +0 -1
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js +0 -2
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js.map +0 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/lightrag-startup-hook.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.x — Tests for the lightragStartupHook() introduced in issue #6.
|
|
5
|
+
*
|
|
6
|
+
* Strategy: drive lightragStartupHook() against a temp project root that
|
|
7
|
+
* has a `.bizar/memory.json` (or no config at all). The hook is async
|
|
8
|
+
* and must:
|
|
9
|
+
* - resolve the project's lightrag config
|
|
10
|
+
* - honour `lightrag.enabled: false` → return ok=true, reason='disabled'
|
|
11
|
+
* - honour BIZAR_LIGHTRAG_AUTOSTART=0 → return ok=true, reason='env-disabled'
|
|
12
|
+
* - return ok=false, reason='no-project' when no projectRoot is passed
|
|
13
|
+
* - gracefully handle missing binary / errors → return ok=false, error=...
|
|
14
|
+
* - return ok=true, reason='already-running' when the server is up
|
|
15
|
+
*
|
|
16
|
+
* Tests do NOT require `lightrag-server` to be installed. When it isn't,
|
|
17
|
+
* the "auto-start" paths return ok=false with a clear error, which is
|
|
18
|
+
* exactly the contract the dashboard relies on.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
22
|
+
import assert from 'node:assert/strict';
|
|
23
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
24
|
+
import { tmpdir } from 'node:os';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
lightragStartupHook,
|
|
29
|
+
findLightragBinary,
|
|
30
|
+
resolveLightRAGConfig,
|
|
31
|
+
} = await import('../src/server/memory-lightrag.mjs');
|
|
32
|
+
|
|
33
|
+
let tmpRoot;
|
|
34
|
+
const ORIG_AUTOSTART = process.env.BIZAR_LIGHTRAG_AUTOSTART;
|
|
35
|
+
const ORIG_VAULT = process.env.BIZAR_MEMORY_VAULT;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
tmpRoot = mkdtempSync(join(tmpdir(), 'bizar-lrh-'));
|
|
39
|
+
mkdirSync(join(tmpRoot, '.bizar'), { recursive: true });
|
|
40
|
+
delete process.env.BIZAR_LIGHTRAG_AUTOSTART;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
if (tmpRoot && existsSync(tmpRoot)) rmSync(tmpRoot, { recursive: true, force: true });
|
|
45
|
+
if (ORIG_AUTOSTART === undefined) delete process.env.BIZAR_LIGHTRAG_AUTOSTART;
|
|
46
|
+
else process.env.BIZAR_LIGHTRAG_AUTOSTART = ORIG_AUTOSTART;
|
|
47
|
+
if (ORIG_VAULT === undefined) delete process.env.BIZAR_MEMORY_VAULT;
|
|
48
|
+
else process.env.BIZAR_MEMORY_VAULT = ORIG_VAULT;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function writeMemoryJson(root, patch = {}) {
|
|
52
|
+
const cfg = { ...patch };
|
|
53
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify(cfg));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('lightragStartupHook — no project', () => {
|
|
57
|
+
test('returns ok=false, reason=no-project when projectRoot is missing', async () => {
|
|
58
|
+
const r = await lightragStartupHook(null);
|
|
59
|
+
assert.equal(r.ok, false);
|
|
60
|
+
assert.equal(r.started, false);
|
|
61
|
+
assert.equal(r.reason, 'no-project');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('returns ok=false, reason=no-project when projectRoot is empty string', async () => {
|
|
65
|
+
const r = await lightragStartupHook('');
|
|
66
|
+
assert.equal(r.ok, false);
|
|
67
|
+
assert.equal(r.reason, 'no-project');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('lightragStartupHook — disabled paths', () => {
|
|
72
|
+
test('returns ok=true, reason=disabled when lightrag.enabled is false', async () => {
|
|
73
|
+
writeMemoryJson(tmpRoot, { lightrag: { enabled: false, port: 9621 } });
|
|
74
|
+
const r = await lightragStartupHook(tmpRoot);
|
|
75
|
+
assert.equal(r.ok, true, 'hook succeeds (skips) when explicitly disabled');
|
|
76
|
+
assert.equal(r.started, false);
|
|
77
|
+
assert.equal(r.reason, 'disabled');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('BIZAR_LIGHTRAG_AUTOSTART=0 short-circuits the hook', async () => {
|
|
81
|
+
writeMemoryJson(tmpRoot, {}); // lightrag.enabled defaults to true
|
|
82
|
+
process.env.BIZAR_LIGHTRAG_AUTOSTART = '0';
|
|
83
|
+
const r = await lightragStartupHook(tmpRoot);
|
|
84
|
+
assert.equal(r.ok, true);
|
|
85
|
+
assert.equal(r.started, false);
|
|
86
|
+
assert.equal(r.reason, 'env-disabled');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('BIZAR_LIGHTRAG_AUTOSTART=false short-circuits the hook', async () => {
|
|
90
|
+
writeMemoryJson(tmpRoot, {});
|
|
91
|
+
process.env.BIZAR_LIGHTRAG_AUTOSTART = 'false';
|
|
92
|
+
const r = await lightragStartupHook(tmpRoot);
|
|
93
|
+
assert.equal(r.ok, true);
|
|
94
|
+
assert.equal(r.reason, 'env-disabled');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('lightragStartupHook — start path', () => {
|
|
99
|
+
test('returns a structured result without throwing', async () => {
|
|
100
|
+
// Use a config that points at a bogus port. If lightrag-server is
|
|
101
|
+
// installed locally, the hook may start it; if not, it will fail
|
|
102
|
+
// with a clean error. Either way, the contract is: the hook
|
|
103
|
+
// returns a structured result and never throws. We don't run the
|
|
104
|
+
// full 30s startup probe here — that's covered by the live
|
|
105
|
+
// memory-lightrag.test.mjs.
|
|
106
|
+
writeMemoryJson(tmpRoot, {
|
|
107
|
+
lightrag: {
|
|
108
|
+
enabled: true,
|
|
109
|
+
host: '127.0.0.1',
|
|
110
|
+
// Use a high port that's very unlikely to be open. The
|
|
111
|
+
// startup probe is bounded by resolveLightRAGConfig's
|
|
112
|
+
// startupTimeoutMs which defaults to 30s — too long for a
|
|
113
|
+
// unit test. We instead pre-create a "running" PID file to
|
|
114
|
+
// short-circuit the probe to "already-running" and assert
|
|
115
|
+
// the hook returns ok=true, reason='already-running'.
|
|
116
|
+
port: 29999,
|
|
117
|
+
workingDir: join(tmpRoot, '.bizar', 'lightrag'),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
// Pre-create a fake PID file pointing at a live parent PID.
|
|
121
|
+
// The hook's readPidAlive() will return alive=true, so the hook
|
|
122
|
+
// short-circuits to "already-running" without spawning a process.
|
|
123
|
+
mkdirSync(join(tmpRoot, '.bizar', 'lightrag'), { recursive: true });
|
|
124
|
+
writeFileSync(join(tmpRoot, '.bizar', 'lightrag', 'lightrag.pid'), String(process.pid));
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const r = await lightragStartupHook(tmpRoot);
|
|
128
|
+
assert.equal(r.ok, true, `hook should not error when server is already running`);
|
|
129
|
+
assert.equal(r.started, false);
|
|
130
|
+
assert.equal(r.reason, 'already-running');
|
|
131
|
+
} finally {
|
|
132
|
+
rmSync(join(tmpRoot, '.bizar', 'lightrag'), { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('never throws even when the config is corrupt', async () => {
|
|
137
|
+
// Write a memory.json that is not valid JSON.
|
|
138
|
+
writeFileSync(join(tmpRoot, '.bizar', 'memory.json'), '{ not valid json');
|
|
139
|
+
// Pre-create a "running" PID file to short-circuit the start
|
|
140
|
+
// probe. Without this, the hook would attempt a real start.
|
|
141
|
+
mkdirSync(join(tmpRoot, '.bizar', 'lightrag'), { recursive: true });
|
|
142
|
+
writeFileSync(join(tmpRoot, '.bizar', 'lightrag', 'lightrag.pid'), String(process.pid));
|
|
143
|
+
try {
|
|
144
|
+
const r = await lightragStartupHook(tmpRoot);
|
|
145
|
+
// resolveLightRAGConfig falls back to defaults on parse error, so
|
|
146
|
+
// the hook will see the running PID and return already-running.
|
|
147
|
+
assert.ok(r);
|
|
148
|
+
assert.equal(typeof r.ok, 'boolean');
|
|
149
|
+
// We don't assert on r.reason — depends on whether the corrupt
|
|
150
|
+
// config parse happens before the PID check.
|
|
151
|
+
} finally {
|
|
152
|
+
rmSync(join(tmpRoot, '.bizar', 'lightrag'), { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('lightragStartupHook — env var override', () => {
|
|
158
|
+
test('BIZAR_LIGHTRAG_AUTOSTART=1 / empty does NOT block (default behaviour)', async () => {
|
|
159
|
+
// Without an explicit "disable" value, env should not block startup.
|
|
160
|
+
// We pre-create a PID file so the hook short-circuits to
|
|
161
|
+
// "already-running" without spawning a process.
|
|
162
|
+
writeMemoryJson(tmpRoot, {
|
|
163
|
+
lightrag: {
|
|
164
|
+
enabled: true,
|
|
165
|
+
port: 29998,
|
|
166
|
+
workingDir: join(tmpRoot, '.bizar', 'lightrag'),
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
mkdirSync(join(tmpRoot, '.bizar', 'lightrag'), { recursive: true });
|
|
170
|
+
writeFileSync(join(tmpRoot, '.bizar', 'lightrag', 'lightrag.pid'), String(process.pid));
|
|
171
|
+
// Don't set the env var at all (already deleted in beforeEach).
|
|
172
|
+
delete process.env.BIZAR_LIGHTRAG_AUTOSTART;
|
|
173
|
+
try {
|
|
174
|
+
const r = await lightragStartupHook(tmpRoot);
|
|
175
|
+
assert.notEqual(r.reason, 'env-disabled', 'no env-var override → env-disabled is not the reason');
|
|
176
|
+
} finally {
|
|
177
|
+
rmSync(join(tmpRoot, '.bizar', 'lightrag'), { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('lightragStartupHook — config resolution', () => {
|
|
183
|
+
test('resolveLightRAGConfig still works for the temp project', () => {
|
|
184
|
+
writeMemoryJson(tmpRoot, {
|
|
185
|
+
lightrag: {
|
|
186
|
+
enabled: true,
|
|
187
|
+
port: 12345,
|
|
188
|
+
host: '0.0.0.0',
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
const cfg = resolveLightRAGConfig(tmpRoot);
|
|
192
|
+
assert.equal(cfg.enabled, true);
|
|
193
|
+
assert.equal(cfg.port, 12345);
|
|
194
|
+
assert.equal(cfg.host, '0.0.0.0');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('findLightRAGBinary returns null or a path (does not throw)', () => {
|
|
198
|
+
const bin = findLightragBinary();
|
|
199
|
+
// Either we found it or we didn't. We only care that the call
|
|
200
|
+
// didn't throw — proves the startup hook's "binary missing" path
|
|
201
|
+
// is well-behaved.
|
|
202
|
+
assert.ok(bin === null || typeof bin === 'string');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
console.log(' lightrag-startup-hook tests loaded');
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/memory-auto-reindex.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.x — Tests for the auto-reindex-on-write feature.
|
|
5
|
+
*
|
|
6
|
+
* POST /memory/notes and PUT /memory/notes/* must return the note response
|
|
7
|
+
* promptly WITHOUT waiting for reindexSingleNote to complete (fire-and-forget).
|
|
8
|
+
*
|
|
9
|
+
* These tests verify:
|
|
10
|
+
* 1. POST /memory/notes returns 201 and the note is persisted immediately
|
|
11
|
+
* 2. PUT /memory/notes/* returns 200 and the note is updated immediately
|
|
12
|
+
* 3. Both handlers are non-blocking (reindex is async, fire-and-forget)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import http from 'node:http';
|
|
21
|
+
|
|
22
|
+
const httpPost = (port, path, body) =>
|
|
23
|
+
new Promise((resolve) => {
|
|
24
|
+
const data = JSON.stringify(body);
|
|
25
|
+
const req = http.request(
|
|
26
|
+
{ hostname: 'localhost', port, path, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
|
|
27
|
+
(res) => {
|
|
28
|
+
let respBody = '';
|
|
29
|
+
res.on('data', (c) => (respBody += c));
|
|
30
|
+
res.on('end', () => {
|
|
31
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(respBody || '{}') }); }
|
|
32
|
+
catch { resolve({ status: res.statusCode, body: respBody }); }
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
req.write(data);
|
|
37
|
+
req.end();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const httpPut = (port, path, body) =>
|
|
41
|
+
new Promise((resolve) => {
|
|
42
|
+
const data = JSON.stringify(body);
|
|
43
|
+
const req = http.request(
|
|
44
|
+
{ hostname: 'localhost', port, path, method: 'PUT', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
|
|
45
|
+
(res) => {
|
|
46
|
+
let respBody = '';
|
|
47
|
+
res.on('data', (c) => (respBody += c));
|
|
48
|
+
res.on('end', () => {
|
|
49
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(respBody || '{}') }); }
|
|
50
|
+
catch { resolve({ status: res.statusCode, body: respBody }); }
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
req.write(data);
|
|
55
|
+
req.end();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
async function buildApp(projectRoot) {
|
|
59
|
+
const { createMemoryRouter } = await import('../src/server/routes/memory.mjs');
|
|
60
|
+
const express = (await import('express')).default;
|
|
61
|
+
const app = express();
|
|
62
|
+
app.use(express.json());
|
|
63
|
+
app.use('/api', createMemoryRouter({ projectRoot }));
|
|
64
|
+
return app;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function withApp(projectRoot, fn) {
|
|
68
|
+
const app = await buildApp(projectRoot);
|
|
69
|
+
const server = app.listen(0);
|
|
70
|
+
const port = server.address().port;
|
|
71
|
+
try {
|
|
72
|
+
return await fn(port);
|
|
73
|
+
} finally {
|
|
74
|
+
server.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function makeTmp() {
|
|
79
|
+
const root = join(tmpdir(), `bizar-auto-reindex-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
80
|
+
mkdirSync(root, { recursive: true });
|
|
81
|
+
mkdirSync(join(root, '.bizar'), { recursive: true });
|
|
82
|
+
// Write a minimal memory.json so the router doesn't fail on init.
|
|
83
|
+
writeFileSync(join(root, '.bizar', 'memory.json'), JSON.stringify({
|
|
84
|
+
version: 1,
|
|
85
|
+
backend: 'bizar-local',
|
|
86
|
+
projectId: 'reindex-test',
|
|
87
|
+
memoryRepo: { mode: 'local-only', path: join(root, '.obsidian') },
|
|
88
|
+
namespaces: { project: 'projects/reindex-test', global: 'global/bizar', user: 'users/tester' },
|
|
89
|
+
lightrag: { enabled: false },
|
|
90
|
+
git: { autoPullOnSessionStart: false, autoCommitOnMemoryWrite: false, autoPushOnSessionEnd: false },
|
|
91
|
+
}));
|
|
92
|
+
return root;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let tmpRoot;
|
|
96
|
+
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
tmpRoot = makeTmp();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
afterEach(() => {
|
|
102
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('POST /memory/notes — auto-reindex', () => {
|
|
106
|
+
test('returns 201 and the written note immediately', async () => {
|
|
107
|
+
const result = await withApp(tmpRoot, async (port) => {
|
|
108
|
+
const start = Date.now();
|
|
109
|
+
const r = await httpPost(port, '/api/memory/notes', {
|
|
110
|
+
path: 'sessions/test-post.md',
|
|
111
|
+
frontmatter: {
|
|
112
|
+
memory_id: 'test-mid-001',
|
|
113
|
+
type: 'session_summary',
|
|
114
|
+
project_id: 'reindex-test',
|
|
115
|
+
status: 'active',
|
|
116
|
+
confidence: 'verified',
|
|
117
|
+
created: '2026-07-06T00:00:00Z',
|
|
118
|
+
updated: '2026-07-06T00:00:00Z',
|
|
119
|
+
tags: ['auto-generated'],
|
|
120
|
+
},
|
|
121
|
+
body: '# Test note\n\nContent here.',
|
|
122
|
+
});
|
|
123
|
+
const elapsed = Date.now() - start;
|
|
124
|
+
return { r, elapsed };
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
assert.equal(result.r.status, 201, `expected 201, got ${result.r.status}: ${JSON.stringify(result.r.body)}`);
|
|
128
|
+
assert.equal(result.r.body.relPath, 'sessions/test-post.md');
|
|
129
|
+
// Response must be fast — reindex is fire-and-forget (no lightrag-server in tests).
|
|
130
|
+
assert.ok(result.elapsed < 2000, `expected <2s response, got ${result.elapsed}ms`);
|
|
131
|
+
// Note must be persisted on disk.
|
|
132
|
+
const notePath = join(tmpRoot, '.obsidian', 'sessions', 'test-post.md');
|
|
133
|
+
assert.ok(existsSync(notePath), 'note should be written to disk immediately');
|
|
134
|
+
const content = readFileSync(notePath, 'utf8');
|
|
135
|
+
assert.ok(content.includes('Test note'));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('rejects notes not ending in .md', async () => {
|
|
139
|
+
const result = await withApp(tmpRoot, async (port) =>
|
|
140
|
+
httpPost(port, '/api/memory/notes', {
|
|
141
|
+
path: 'sessions/test.txt',
|
|
142
|
+
frontmatter: {
|
|
143
|
+
memory_id: 'test-mid-002',
|
|
144
|
+
type: 'task_summary',
|
|
145
|
+
project_id: 'reindex-test',
|
|
146
|
+
status: 'active',
|
|
147
|
+
confidence: 'verified',
|
|
148
|
+
created: '2026-07-06T00:00:00Z',
|
|
149
|
+
updated: '2026-07-06T00:00:00Z',
|
|
150
|
+
tags: [],
|
|
151
|
+
},
|
|
152
|
+
body: 'content',
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
assert.equal(result.status, 400);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('PUT /memory/notes/* — auto-reindex', () => {
|
|
160
|
+
test('returns 200 and the updated note immediately', async () => {
|
|
161
|
+
// Pre-create the note with valid frontmatter.
|
|
162
|
+
mkdirSync(join(tmpRoot, '.obsidian', 'sessions'), { recursive: true });
|
|
163
|
+
writeFileSync(
|
|
164
|
+
join(tmpRoot, '.obsidian', 'sessions', 'test-put.md'),
|
|
165
|
+
'---\nmemory_id: test-mid-put\ntype: session_summary\nproject_id: reindex-test\nstatus: active\nconfidence: verified\ncreated: "2026-07-06T00:00:00Z"\nupdated: "2026-07-06T00:00:00Z"\ntags: []\n---\n\nOriginal content.',
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const result = await withApp(tmpRoot, async (port) => {
|
|
169
|
+
const start = Date.now();
|
|
170
|
+
const r = await httpPut(port, '/api/memory/notes/sessions/test-put.md', {
|
|
171
|
+
frontmatter: {
|
|
172
|
+
memory_id: 'test-mid-put',
|
|
173
|
+
type: 'session_summary',
|
|
174
|
+
project_id: 'reindex-test',
|
|
175
|
+
status: 'active',
|
|
176
|
+
confidence: 'verified',
|
|
177
|
+
created: '2026-07-06T00:00:00Z',
|
|
178
|
+
updated: '2026-07-06T00:00:00Z',
|
|
179
|
+
tags: ['auto-generated'],
|
|
180
|
+
},
|
|
181
|
+
body: '# Updated note\n\nNew content.',
|
|
182
|
+
});
|
|
183
|
+
const elapsed = Date.now() - start;
|
|
184
|
+
return { r, elapsed };
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
assert.equal(result.r.status, 200, `expected 200, got ${result.r.status}: ${JSON.stringify(result.r.body)}`);
|
|
188
|
+
assert.equal(result.r.body.relPath, 'sessions/test-put.md');
|
|
189
|
+
assert.ok(result.elapsed < 2000, `expected <2s response, got ${result.elapsed}ms`);
|
|
190
|
+
// Note must be updated on disk.
|
|
191
|
+
const notePath = join(tmpRoot, '.obsidian', 'sessions', 'test-put.md');
|
|
192
|
+
const content = readFileSync(notePath, 'utf8');
|
|
193
|
+
assert.ok(content.includes('Updated note'));
|
|
194
|
+
assert.ok(!content.includes('Original content'));
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -390,7 +390,7 @@ describe('bizar memory setup CLI', () => {
|
|
|
390
390
|
`memoryRepo.remote should match the requested URL — got ${cfg.memoryRepo.remote}`);
|
|
391
391
|
assert.strictEqual(cfg.gitRemote, remote,
|
|
392
392
|
`top-level gitRemote should also be set so cmdPush works — got ${cfg.gitRemote}`);
|
|
393
|
-
// Vault path lives under ~/.local/share/bizar/memory
|
|
393
|
+
// Vault path lives under ~/.bizar_memory/<repo-name> (v5.x — was ~/.local/share/bizar/memory)
|
|
394
394
|
assert.ok(cfg.memoryRepo.path.endsWith(repo), `vault path should end with ${repo}`);
|
|
395
395
|
});
|
|
396
396
|
|
|
@@ -2,21 +2,34 @@
|
|
|
2
2
|
* tests/memory-default-vault.test.mjs
|
|
3
3
|
*
|
|
4
4
|
* Tests for the default vault location, currentVault(), and ensureVaultExists().
|
|
5
|
+
*
|
|
6
|
+
* v5.x — DEFAULT_MEMORY_VAULT relocated from `~/.local/share/bizar/memory` to
|
|
7
|
+
* `~/.bizar_memory` (issue #5). The legacy path is exported as
|
|
8
|
+
* `LEGACY_MEMORY_VAULT` for migration detection.
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
8
12
|
import assert from 'node:assert';
|
|
9
13
|
import { tmpdir, homedir } from 'node:os';
|
|
10
14
|
import { join } from 'node:path';
|
|
11
|
-
import { rmSync, existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { rmSync, existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
12
16
|
|
|
13
17
|
const TEST_STORE = await import('../src/server/memory-store.mjs').then((m) => m);
|
|
14
18
|
|
|
15
19
|
describe('DEFAULT_MEMORY_VAULT', () => {
|
|
16
|
-
it('is ~/.
|
|
17
|
-
const expected = join(homedir(), '.
|
|
20
|
+
it('is ~/.bizar_memory', () => {
|
|
21
|
+
const expected = join(homedir(), '.bizar_memory');
|
|
18
22
|
assert.strictEqual(TEST_STORE.DEFAULT_MEMORY_VAULT, expected);
|
|
19
23
|
});
|
|
24
|
+
|
|
25
|
+
it('LEGACY_MEMORY_VAULT keeps the previous path for migration detection', () => {
|
|
26
|
+
const expected = join(homedir(), '.local', 'share', 'bizar', 'memory');
|
|
27
|
+
assert.strictEqual(TEST_STORE.LEGACY_MEMORY_VAULT, expected);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('default vault differs from legacy vault (relocation confirmed)', () => {
|
|
31
|
+
assert.notStrictEqual(TEST_STORE.DEFAULT_MEMORY_VAULT, TEST_STORE.LEGACY_MEMORY_VAULT);
|
|
32
|
+
});
|
|
20
33
|
});
|
|
21
34
|
|
|
22
35
|
describe('DEFAULT_GIT_REMOTE', () => {
|
|
@@ -96,3 +109,77 @@ describe('ensureVaultExists', () => {
|
|
|
96
109
|
assert.match(content, /bizar@localhost/);
|
|
97
110
|
});
|
|
98
111
|
});
|
|
112
|
+
|
|
113
|
+
describe('ensureVaultExists — legacy path migration notice (v5.x)', () => {
|
|
114
|
+
let fakeHome;
|
|
115
|
+
let legacyPath;
|
|
116
|
+
let newPath;
|
|
117
|
+
let origHome;
|
|
118
|
+
let origVault;
|
|
119
|
+
let origWarn;
|
|
120
|
+
const captured = [];
|
|
121
|
+
const fakeConsole = { ...console, warn: (...args) => captured.push(args.join(' ')), log: () => {} };
|
|
122
|
+
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
origHome = process.env.HOME;
|
|
125
|
+
origVault = process.env.BIZAR_MEMORY_VAULT;
|
|
126
|
+
origWarn = console.warn;
|
|
127
|
+
console.warn = fakeConsole.warn;
|
|
128
|
+
|
|
129
|
+
// Build a synthetic home with both the legacy path (with content) and
|
|
130
|
+
// a new default path. We can't override `homedir()` in pure JS so we
|
|
131
|
+
// re-import the module via a fresh dynamic import after setting HOME.
|
|
132
|
+
fakeHome = join(tmpdir(), `bizar-memlegacy-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
133
|
+
process.env.HOME = fakeHome;
|
|
134
|
+
delete process.env.BIZAR_MEMORY_VAULT;
|
|
135
|
+
legacyPath = join(fakeHome, '.local', 'share', 'bizar', 'memory');
|
|
136
|
+
newPath = join(fakeHome, '.bizar_memory');
|
|
137
|
+
mkdirSync(legacyPath, { recursive: true });
|
|
138
|
+
writeFileSync(join(legacyPath, 'marker.md'), '# legacy note');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
afterEach(() => {
|
|
142
|
+
console.warn = origWarn;
|
|
143
|
+
if (origHome === undefined) delete process.env.HOME;
|
|
144
|
+
else process.env.HOME = origHome;
|
|
145
|
+
if (origVault === undefined) delete process.env.BIZAR_MEMORY_VAULT;
|
|
146
|
+
else process.env.BIZAR_MEMORY_VAULT = origVault;
|
|
147
|
+
if (fakeHome && existsSync(fakeHome)) rmSync(fakeHome, { recursive: true, force: true });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('logs a one-time migration notice when legacy vault exists and new default does not', async () => {
|
|
151
|
+
// Re-import the module after env change so HOME is picked up.
|
|
152
|
+
const fresh = await import(`../src/server/memory-store.mjs?v=${Date.now()}`);
|
|
153
|
+
captured.length = 0;
|
|
154
|
+
const result = fresh.ensureVaultExists();
|
|
155
|
+
assert.strictEqual(result, newPath, 'returns new default path');
|
|
156
|
+
assert.strictEqual(existsSync(newPath), true, 'new vault was created');
|
|
157
|
+
// The legacy path was NOT touched.
|
|
158
|
+
assert.strictEqual(existsSync(legacyPath), true, 'legacy path still exists');
|
|
159
|
+
assert.strictEqual(existsSync(join(legacyPath, 'marker.md')), true, 'legacy contents preserved');
|
|
160
|
+
// The notice mentions the legacy path + new path + the manual mv command.
|
|
161
|
+
const all = captured.join('\n');
|
|
162
|
+
assert.match(all, /legacy memory vault/);
|
|
163
|
+
assert.match(all, new RegExp(legacyPath.replace(/[/.]/g, '\\$&')));
|
|
164
|
+
assert.match(all, new RegExp(newPath.replace(/[/.]/g, '\\$&')));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('does not log the notice when BIZAR_MEMORY_VAULT is set (explicit override)', async () => {
|
|
168
|
+
process.env.BIZAR_MEMORY_VAULT = join(fakeHome, 'explicit-vault');
|
|
169
|
+
const fresh = await import(`../src/server/memory-store.mjs?v=${Date.now()}-${Math.random()}`);
|
|
170
|
+
captured.length = 0;
|
|
171
|
+
const result = fresh.ensureVaultExists();
|
|
172
|
+
assert.strictEqual(result, process.env.BIZAR_MEMORY_VAULT);
|
|
173
|
+
assert.strictEqual(captured.length, 0, 'no notice when BIZAR_MEMORY_VAULT is explicit');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('does not log the notice when new default already exists (idempotent)', async () => {
|
|
177
|
+
// Pre-create the new vault so the migration notice is skipped.
|
|
178
|
+
mkdirSync(newPath, { recursive: true });
|
|
179
|
+
const fresh = await import(`../src/server/memory-store.mjs?v=${Date.now()}-${Math.random()}-1`);
|
|
180
|
+
captured.length = 0;
|
|
181
|
+
fresh.ensureVaultExists();
|
|
182
|
+
// Even with legacy present, the new vault already exists → no notice.
|
|
183
|
+
assert.strictEqual(captured.length, 0, 'no notice when new default already exists');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -19,6 +19,8 @@ const {
|
|
|
19
19
|
stats,
|
|
20
20
|
recordQuery,
|
|
21
21
|
rebuildGraph,
|
|
22
|
+
detectAvailableLLM,
|
|
23
|
+
reindexSingleNote,
|
|
22
24
|
} = await import('../src/server/memory-lightrag.mjs');
|
|
23
25
|
|
|
24
26
|
let tmpRoot;
|
|
@@ -159,4 +161,133 @@ describe('rebuildGraph', () => {
|
|
|
159
161
|
assert.equal(typeof result.ok, 'boolean');
|
|
160
162
|
// workingDir should have been wiped and re-created (or attempted).
|
|
161
163
|
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('detectAvailableLLM', () => {
|
|
167
|
+
const ORIG_OPENAI = process.env.OPENAI_API_KEY;
|
|
168
|
+
const ORIG_ANTHROPIC = process.env.ANTHROPIC_API_KEY;
|
|
169
|
+
const ORIG_MINIMAX = process.env.MINIMAX_API_KEY;
|
|
170
|
+
|
|
171
|
+
afterEach(() => {
|
|
172
|
+
if (ORIG_OPENAI === undefined) delete process.env.OPENAI_API_KEY;
|
|
173
|
+
else process.env.OPENAI_API_KEY = ORIG_OPENAI;
|
|
174
|
+
if (ORIG_ANTHROPIC === undefined) delete process.env.ANTHROPIC_API_KEY;
|
|
175
|
+
else process.env.ANTHROPIC_API_KEY = ORIG_ANTHROPIC;
|
|
176
|
+
if (ORIG_MINIMAX === undefined) delete process.env.MINIMAX_API_KEY;
|
|
177
|
+
else process.env.MINIMAX_API_KEY = ORIG_MINIMAX;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('returns null when llmBinding is ollama, no API keys set, and ollama is unreachable', async () => {
|
|
181
|
+
delete process.env.OPENAI_API_KEY;
|
|
182
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
183
|
+
delete process.env.MINIMAX_API_KEY;
|
|
184
|
+
const cfg = resolveLightRAGConfig(tmpRoot);
|
|
185
|
+
cfg.llmBinding = 'ollama';
|
|
186
|
+
cfg.llmBindingHost = 'http://localhost:59999'; // nothing listening
|
|
187
|
+
const result = await detectAvailableLLM(cfg);
|
|
188
|
+
// When ollama is the binding but unreachable, and no API keys are set, returns null.
|
|
189
|
+
assert.equal(result, null);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('returns openai-available when llmBinding is ollama but OPENAI_API_KEY is set (ollama fallback)', async () => {
|
|
193
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
194
|
+
delete process.env.MINIMAX_API_KEY;
|
|
195
|
+
process.env.OPENAI_API_KEY = 'sk-test-key';
|
|
196
|
+
const cfg = resolveLightRAGConfig(tmpRoot);
|
|
197
|
+
cfg.llmBinding = 'ollama';
|
|
198
|
+
cfg.llmBindingHost = 'http://localhost:59999'; // unreachable
|
|
199
|
+
const result = await detectAvailableLLM(cfg);
|
|
200
|
+
assert.ok(result);
|
|
201
|
+
assert.equal(result.provider, 'openai');
|
|
202
|
+
assert.equal(result.available, true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('returns anthropic-available when llmBinding is anthropic and ANTHROPIC_API_KEY is set', async () => {
|
|
206
|
+
delete process.env.OPENAI_API_KEY;
|
|
207
|
+
delete process.env.MINIMAX_API_KEY;
|
|
208
|
+
process.env.ANTHROPIC_API_KEY = 'sk-ant-test';
|
|
209
|
+
const cfg = resolveLightRAGConfig(tmpRoot);
|
|
210
|
+
cfg.llmBinding = 'anthropic';
|
|
211
|
+
const result = await detectAvailableLLM(cfg);
|
|
212
|
+
assert.ok(result);
|
|
213
|
+
assert.equal(result.provider, 'anthropic');
|
|
214
|
+
assert.equal(result.available, true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('returns minimax-available when llmBinding is minimax and MINIMAX_API_KEY is set', async () => {
|
|
218
|
+
delete process.env.OPENAI_API_KEY;
|
|
219
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
220
|
+
process.env.MINIMAX_API_KEY = 'minimax-test-key';
|
|
221
|
+
const cfg = resolveLightRAGConfig(tmpRoot);
|
|
222
|
+
cfg.llmBinding = 'minimax';
|
|
223
|
+
const result = await detectAvailableLLM(cfg);
|
|
224
|
+
assert.ok(result);
|
|
225
|
+
assert.equal(result.provider, 'minimax');
|
|
226
|
+
assert.equal(result.available, true);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('returns null when llmBinding is openai but no API key is set', async () => {
|
|
230
|
+
delete process.env.OPENAI_API_KEY;
|
|
231
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
232
|
+
delete process.env.MINIMAX_API_KEY;
|
|
233
|
+
const cfg = resolveLightRAGConfig(tmpRoot);
|
|
234
|
+
cfg.llmBinding = 'openai';
|
|
235
|
+
const result = await detectAvailableLLM(cfg);
|
|
236
|
+
assert.equal(result, null);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('reindexSingleNote', () => {
|
|
241
|
+
test('returns error when memory not initialized', async () => {
|
|
242
|
+
mkdirSync(join(tmpRoot, '.bizar'), { recursive: true });
|
|
243
|
+
// No memory.json
|
|
244
|
+
const r = await reindexSingleNote(tmpRoot, 'notes/test.md');
|
|
245
|
+
assert.equal(r.ok, false);
|
|
246
|
+
assert.ok(r.error.includes('not initialized'));
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('returns error when note does not exist', async () => {
|
|
250
|
+
mkdirSync(join(tmpRoot, '.bizar'), { recursive: true });
|
|
251
|
+
writeFileSync(join(tmpRoot, '.bizar', 'memory.json'), JSON.stringify({
|
|
252
|
+
projectId: 'testproj',
|
|
253
|
+
memoryRepo: { mode: 'local-only' },
|
|
254
|
+
}));
|
|
255
|
+
const r = await reindexSingleNote(tmpRoot, 'notes/nonexistent.md');
|
|
256
|
+
assert.equal(r.ok, false);
|
|
257
|
+
assert.ok(r.error.includes('not found'));
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('returns error on path traversal attempt', async () => {
|
|
261
|
+
mkdirSync(join(tmpRoot, '.bizar', 'memory', 'projects', 'testproj'), { recursive: true });
|
|
262
|
+
writeFileSync(join(tmpRoot, '.bizar', 'memory.json'), JSON.stringify({
|
|
263
|
+
projectId: 'testproj',
|
|
264
|
+
memoryRepo: {
|
|
265
|
+
mode: 'managed',
|
|
266
|
+
path: join(tmpRoot, '.bizar', 'memory'),
|
|
267
|
+
},
|
|
268
|
+
}));
|
|
269
|
+
// Attempt path traversal
|
|
270
|
+
const r = await reindexSingleNote(tmpRoot, '../../../etc/passwd');
|
|
271
|
+
assert.equal(r.ok, false);
|
|
272
|
+
assert.ok(r.error.includes('traversal'));
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('returns structured result when note exists and lightrag disabled', async () => {
|
|
276
|
+
mkdirSync(join(tmpRoot, '.bizar', 'memory', 'projects', 'testproj'), { recursive: true });
|
|
277
|
+
writeFileSync(join(tmpRoot, '.bizar', 'memory.json'), JSON.stringify({
|
|
278
|
+
projectId: 'testproj',
|
|
279
|
+
memoryRepo: {
|
|
280
|
+
mode: 'managed',
|
|
281
|
+
path: join(tmpRoot, '.bizar', 'memory'),
|
|
282
|
+
},
|
|
283
|
+
lightrag: { enabled: false },
|
|
284
|
+
}));
|
|
285
|
+
writeFileSync(
|
|
286
|
+
join(tmpRoot, '.bizar', 'memory', 'projects', 'testproj', 'test.md'),
|
|
287
|
+
'---\ntitle: Test\n---\n\nBody.',
|
|
288
|
+
);
|
|
289
|
+
const r = await reindexSingleNote(tmpRoot, 'test.md');
|
|
290
|
+
assert.equal(r.ok, false);
|
|
291
|
+
assert.ok(r.error.includes('disabled'));
|
|
292
|
+
});
|
|
162
293
|
});
|