@polderlabs/bizar 4.2.0 → 4.2.2
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/src/server/memory-git.mjs +62 -0
- package/bizar-dash/src/server/memory-store.mjs +82 -10
- 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 +54 -0
- package/bizar-dash/tests/memory-conflicts.test.mjs +229 -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-roundtrip.test.mjs +219 -0
- package/cli/memory.mjs +273 -6
- package/package.json +2 -2
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/memory-namespace.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* Tests for namespace isolation across the three supported namespaces:
|
|
5
|
+
* projects/<id>/ — project-scoped notes
|
|
6
|
+
* global/bizar/ — cross-project system knowledge
|
|
7
|
+
* users/<userId>/ — personal user notes
|
|
8
|
+
*
|
|
9
|
+
* Verifies that notes written to one namespace:
|
|
10
|
+
* (a) exist and are readable
|
|
11
|
+
* (b) appear in listNotes/searchVault results
|
|
12
|
+
* (c) do not cross-contaminate (notes from one namespace don't appear under another's path)
|
|
13
|
+
*
|
|
14
|
+
* Gap closed: memory-system-map §8.1 CRITICAL gap #1 "Namespace creation &
|
|
15
|
+
* isolation — global/ and users/ namespaces are configured but never tested".
|
|
16
|
+
*
|
|
17
|
+
* FIX 5: listNotes/readNote/deleteNote now accept an `opts.root` override so
|
|
18
|
+
* that callers (CLI cmdList/cmdRead/cmdDelete, plus tests) can target
|
|
19
|
+
* specific namespaces in managed mode. resolveNamespaceRoot() in
|
|
20
|
+
* memory-store.mjs returns the absolute path for each namespace, and the
|
|
21
|
+
* new opts.root parameter lets the existing functions operate on a
|
|
22
|
+
* different root than the default vaultRoot.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
26
|
+
import assert from 'node:assert/strict';
|
|
27
|
+
import { tmpdir } from 'node:os';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
import { mkdirSync, rmSync } from 'node:fs';
|
|
30
|
+
|
|
31
|
+
const TEST_MEMORY_STORE = await import('../src/server/memory-store.mjs').then((m) => m);
|
|
32
|
+
|
|
33
|
+
const uid = () => `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
34
|
+
|
|
35
|
+
function validNote(namespace) {
|
|
36
|
+
return {
|
|
37
|
+
frontmatter: {
|
|
38
|
+
memory_id: `ns_${namespace}_${uid().slice(0, 8)}`,
|
|
39
|
+
type: 'session_summary',
|
|
40
|
+
project_id: 'ns-test-proj',
|
|
41
|
+
status: 'active',
|
|
42
|
+
confidence: 'verified',
|
|
43
|
+
created: new Date().toISOString(),
|
|
44
|
+
updated: new Date().toISOString(),
|
|
45
|
+
tags: [namespace],
|
|
46
|
+
title: `Namespace test — ${namespace}`,
|
|
47
|
+
},
|
|
48
|
+
body: `Content in namespace: ${namespace}\nUnique: ${uid()}\n`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('memory-namespace: managed mode — write/read across namespaces', () => {
|
|
53
|
+
let projectRoot;
|
|
54
|
+
let sharedRepo;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
projectRoot = join(tmpdir(), `ns-test-${uid()}`);
|
|
58
|
+
sharedRepo = join(tmpdir(), `ns-repo-${uid()}`);
|
|
59
|
+
mkdirSync(projectRoot, { recursive: true });
|
|
60
|
+
mkdirSync(sharedRepo, { recursive: true });
|
|
61
|
+
mkdirSync(join(projectRoot, '.bizar'), { recursive: true });
|
|
62
|
+
|
|
63
|
+
// Create all namespace directories in the shared repo
|
|
64
|
+
mkdirSync(join(sharedRepo, 'projects', 'ns-test-proj', 'decisions'), { recursive: true });
|
|
65
|
+
mkdirSync(join(sharedRepo, 'projects', 'ns-test-proj', 'api'), { recursive: true });
|
|
66
|
+
mkdirSync(join(sharedRepo, 'global', 'bizar'), { recursive: true });
|
|
67
|
+
mkdirSync(join(sharedRepo, 'users', 'tester'), { recursive: true });
|
|
68
|
+
|
|
69
|
+
TEST_MEMORY_STORE.saveConfig(projectRoot, {
|
|
70
|
+
version: 1,
|
|
71
|
+
backend: 'bizar-local',
|
|
72
|
+
projectId: 'ns-test-proj',
|
|
73
|
+
memoryRepo: {
|
|
74
|
+
mode: 'managed',
|
|
75
|
+
path: sharedRepo,
|
|
76
|
+
remote: null,
|
|
77
|
+
branch: 'main',
|
|
78
|
+
namespace: 'projects/ns-test-proj',
|
|
79
|
+
},
|
|
80
|
+
namespaces: {
|
|
81
|
+
project: 'projects/ns-test-proj',
|
|
82
|
+
global: 'global/bizar',
|
|
83
|
+
user: 'users/tester',
|
|
84
|
+
},
|
|
85
|
+
lightrag: { enabled: false, host: '127.0.0.1', port: 9621, workingDir: join(projectRoot, '.bizar', 'lightrag') },
|
|
86
|
+
git: { autoPullOnSessionStart: false, autoCommitOnMemoryWrite: false, autoPushOnSessionEnd: false, commitAuthor: 'Bizar Memory <bizar-memory@local>', commitMessageTemplate: 'memory(ns-test): {summary}' },
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
try { rmSync(projectRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
92
|
+
try { rmSync(sharedRepo, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('namespace filtering via resolveNamespaceRoot + opts.root (Fix 5)', () => {
|
|
96
|
+
it('resolveNamespaceRoot returns project root for "project" namespace', () => {
|
|
97
|
+
const vi = TEST_MEMORY_STORE.resolveVault(projectRoot);
|
|
98
|
+
const root = TEST_MEMORY_STORE.resolveNamespaceRoot(vi, 'project');
|
|
99
|
+
assert.strictEqual(root, vi.vaultRoot, 'project namespace root should equal vaultRoot');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('resolveNamespaceRoot returns <vaultRoot>/<namespaces.X> for global/user', () => {
|
|
103
|
+
const vi = TEST_MEMORY_STORE.resolveVault(projectRoot);
|
|
104
|
+
const globalRoot = TEST_MEMORY_STORE.resolveNamespaceRoot(vi, 'global');
|
|
105
|
+
const userRoot = TEST_MEMORY_STORE.resolveNamespaceRoot(vi, 'user');
|
|
106
|
+
assert.ok(globalRoot && globalRoot.includes('global/bizar'),
|
|
107
|
+
`global root should include 'global/bizar'; got: ${globalRoot}`);
|
|
108
|
+
assert.ok(userRoot && userRoot.includes('users/tester'),
|
|
109
|
+
`user root should include 'users/tester'; got: ${userRoot}`);
|
|
110
|
+
// In BOTH local-only and managed modes, global/user live UNDER
|
|
111
|
+
// vaultRoot — writeNote('global/bizar/...', …) writes there directly.
|
|
112
|
+
// This matches the actual on-disk layout callers depend on.
|
|
113
|
+
assert.ok(globalRoot.startsWith(vi.vaultRoot),
|
|
114
|
+
`global root should be INSIDE vaultRoot; got: ${globalRoot} (vaultRoot: ${vi.vaultRoot})`);
|
|
115
|
+
assert.ok(userRoot.startsWith(vi.vaultRoot),
|
|
116
|
+
`user root should be INSIDE vaultRoot; got: ${userRoot} (vaultRoot: ${vi.vaultRoot})`);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('listNotes with { root, namespace } returns only notes from that namespace (managed mode)', () => {
|
|
120
|
+
// Write notes to all three namespaces using their full relPaths
|
|
121
|
+
TEST_MEMORY_STORE.writeNote(projectRoot, 'decisions/proj-filter-fix5.md', validNote('project'));
|
|
122
|
+
TEST_MEMORY_STORE.writeNote(projectRoot, 'global/bizar/glob-filter-fix5.md', validNote('global'));
|
|
123
|
+
TEST_MEMORY_STORE.writeNote(projectRoot, 'users/tester/usr-filter-fix5.md', validNote('user'));
|
|
124
|
+
|
|
125
|
+
const vi = TEST_MEMORY_STORE.resolveVault(projectRoot);
|
|
126
|
+
|
|
127
|
+
const globalRoot = TEST_MEMORY_STORE.resolveNamespaceRoot(vi, 'global');
|
|
128
|
+
const userRoot = TEST_MEMORY_STORE.resolveNamespaceRoot(vi, 'user');
|
|
129
|
+
|
|
130
|
+
const globalNotes = TEST_MEMORY_STORE.listNotes(projectRoot, { root: globalRoot });
|
|
131
|
+
const userNotes = TEST_MEMORY_STORE.listNotes(projectRoot, { root: userRoot });
|
|
132
|
+
|
|
133
|
+
const globalRelPaths = globalNotes.map((n) => n.relPath);
|
|
134
|
+
const userRelPaths = userNotes.map((n) => n.relPath);
|
|
135
|
+
|
|
136
|
+
assert.ok(globalRelPaths.some((p) => p.includes('glob-filter-fix5')),
|
|
137
|
+
`global notes should include glob-filter-fix5; got: ${JSON.stringify(globalRelPaths)}`);
|
|
138
|
+
assert.ok(userRelPaths.some((p) => p.includes('usr-filter-fix5')),
|
|
139
|
+
`user notes should include usr-filter-fix5; got: ${JSON.stringify(userRelPaths)}`);
|
|
140
|
+
// No overlap
|
|
141
|
+
for (const n of globalNotes) {
|
|
142
|
+
assert.ok(!n.relPath.includes('proj-filter'),
|
|
143
|
+
`global set should NOT include project notes; got: ${n.relPath}`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('readNote / deleteNote with { root } works in non-project namespaces', () => {
|
|
148
|
+
// Write using the full relPath (which goes through vaultRoot — writeNote
|
|
149
|
+
// doesn't accept opts.root, but the file lands in the right namespace
|
|
150
|
+
// because vaultRoot + 'global/bizar/...' resolves correctly).
|
|
151
|
+
const writeRelPath = 'global/bizar/read-with-root-fix5.md';
|
|
152
|
+
TEST_MEMORY_STORE.writeNote(projectRoot, writeRelPath, validNote('global'));
|
|
153
|
+
|
|
154
|
+
const vi = TEST_MEMORY_STORE.resolveVault(projectRoot);
|
|
155
|
+
const globalRoot = TEST_MEMORY_STORE.resolveNamespaceRoot(vi, 'global');
|
|
156
|
+
|
|
157
|
+
// relPath for read/delete is relative to `root` (which already ends in
|
|
158
|
+
// 'global/bizar'), so just the basename.
|
|
159
|
+
const relPath = 'read-with-root-fix5.md';
|
|
160
|
+
|
|
161
|
+
const readBack = TEST_MEMORY_STORE.readNote(projectRoot, relPath, { root: globalRoot });
|
|
162
|
+
assert.ok(readBack, 'readNote with { root } should find the note');
|
|
163
|
+
assert.strictEqual(readBack.relPath, relPath);
|
|
164
|
+
|
|
165
|
+
const deleted = TEST_MEMORY_STORE.deleteNote(projectRoot, relPath, { root: globalRoot });
|
|
166
|
+
assert.strictEqual(deleted, true, 'deleteNote with { root } should return true');
|
|
167
|
+
|
|
168
|
+
const after = TEST_MEMORY_STORE.readNote(projectRoot, relPath, { root: globalRoot });
|
|
169
|
+
assert.strictEqual(after, null, 'note should be gone after delete');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('writeNote to each namespace', () => {
|
|
174
|
+
it('writes a note to the project namespace (subdir of vaultRoot)', () => {
|
|
175
|
+
const note = validNote('project');
|
|
176
|
+
const relPath = 'decisions/project-ns-test.md';
|
|
177
|
+
const written = TEST_MEMORY_STORE.writeNote(projectRoot, relPath, note);
|
|
178
|
+
assert.strictEqual(written.relPath, relPath, 'relPath should match');
|
|
179
|
+
assert.strictEqual(written.schemaValid, true, 'schemaValid should be true');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('writes a note to the global namespace (sibling to vaultRoot)', () => {
|
|
183
|
+
const note = validNote('global');
|
|
184
|
+
// In managed mode with vaultRoot=sharedRepo/projects/ns-test-proj, writing to
|
|
185
|
+
// global/bizar/... creates the file at sharedRepo/global/bizar/... (sibling to
|
|
186
|
+
// the project vault, not nested within it)
|
|
187
|
+
const relPath = 'global/bizar/global-ns-test.md';
|
|
188
|
+
const written = TEST_MEMORY_STORE.writeNote(projectRoot, relPath, note);
|
|
189
|
+
assert.strictEqual(written.relPath, relPath, 'relPath should match');
|
|
190
|
+
assert.strictEqual(written.schemaValid, true, 'schemaValid should be true');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('writes a note to the user namespace (sibling to vaultRoot)', () => {
|
|
194
|
+
const note = validNote('user');
|
|
195
|
+
const relPath = 'users/tester/user-ns-test.md';
|
|
196
|
+
const written = TEST_MEMORY_STORE.writeNote(projectRoot, relPath, note);
|
|
197
|
+
assert.strictEqual(written.relPath, relPath, 'relPath should match');
|
|
198
|
+
assert.strictEqual(written.schemaValid, true, 'schemaValid should be true');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('readNote across namespaces', () => {
|
|
203
|
+
it('reads a note from the project namespace', () => {
|
|
204
|
+
const relPath = 'decisions/read-project.md';
|
|
205
|
+
TEST_MEMORY_STORE.writeNote(projectRoot, relPath, validNote('project'));
|
|
206
|
+
const readBack = TEST_MEMORY_STORE.readNote(projectRoot, relPath);
|
|
207
|
+
assert.ok(readBack, 'should read back project namespace note');
|
|
208
|
+
assert.strictEqual(readBack.frontmatter.type, 'session_summary');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('reads a note from the global namespace', () => {
|
|
212
|
+
const relPath = 'global/bizar/read-global.md';
|
|
213
|
+
TEST_MEMORY_STORE.writeNote(projectRoot, relPath, validNote('global'));
|
|
214
|
+
const readBack = TEST_MEMORY_STORE.readNote(projectRoot, relPath);
|
|
215
|
+
assert.ok(readBack, 'should read back global namespace note');
|
|
216
|
+
assert.strictEqual(readBack.frontmatter.type, 'session_summary');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('reads a note from the user namespace', () => {
|
|
220
|
+
const relPath = 'users/tester/read-user.md';
|
|
221
|
+
TEST_MEMORY_STORE.writeNote(projectRoot, relPath, validNote('user'));
|
|
222
|
+
const readBack = TEST_MEMORY_STORE.readNote(projectRoot, relPath);
|
|
223
|
+
assert.ok(readBack, 'should read back user namespace note');
|
|
224
|
+
assert.strictEqual(readBack.frontmatter.type, 'session_summary');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('listNotes: all namespaces visible when vault is repo root', () => {
|
|
229
|
+
// This sub-suite uses local-only mode where vault IS the repo root,
|
|
230
|
+
// so namespace filters actually work as intended.
|
|
231
|
+
|
|
232
|
+
let localRoot;
|
|
233
|
+
|
|
234
|
+
beforeEach(() => {
|
|
235
|
+
localRoot = join(tmpdir(), `ns-local-${uid()}`);
|
|
236
|
+
mkdirSync(localRoot, { recursive: true });
|
|
237
|
+
mkdirSync(join(localRoot, '.bizar'), { recursive: true });
|
|
238
|
+
TEST_MEMORY_STORE.saveConfig(localRoot, {
|
|
239
|
+
version: 1,
|
|
240
|
+
backend: 'bizar-local',
|
|
241
|
+
projectId: 'ns-local-test',
|
|
242
|
+
memoryRepo: {
|
|
243
|
+
mode: 'local-only',
|
|
244
|
+
path: join(localRoot, '.obsidian'),
|
|
245
|
+
remote: null,
|
|
246
|
+
branch: 'main',
|
|
247
|
+
namespace: null,
|
|
248
|
+
},
|
|
249
|
+
namespaces: {
|
|
250
|
+
project: 'projects/ns-local-test',
|
|
251
|
+
global: 'global/bizar',
|
|
252
|
+
user: 'users/tester',
|
|
253
|
+
},
|
|
254
|
+
lightrag: { enabled: false, host: '127.0.0.1', port: 9621, workingDir: join(localRoot, '.bizar', 'lightrag') },
|
|
255
|
+
git: { autoPullOnSessionStart: false, autoCommitOnMemoryWrite: false, autoPushOnSessionEnd: false, commitAuthor: 'Bizar Memory <bizar-memory@local>', commitMessageTemplate: 'memory(ns-local): {summary}' },
|
|
256
|
+
});
|
|
257
|
+
TEST_MEMORY_STORE.initVault(localRoot);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
afterEach(() => {
|
|
261
|
+
try { rmSync(localRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('listNotes (no filter) returns notes from all three namespace directories', () => {
|
|
265
|
+
TEST_MEMORY_STORE.writeNote(localRoot, 'decisions/local-project.md', validNote('project'));
|
|
266
|
+
TEST_MEMORY_STORE.writeNote(localRoot, 'global/bizar/local-global.md', validNote('global'));
|
|
267
|
+
TEST_MEMORY_STORE.writeNote(localRoot, 'users/tester/local-user.md', validNote('user'));
|
|
268
|
+
|
|
269
|
+
const all = TEST_MEMORY_STORE.listNotes(localRoot);
|
|
270
|
+
assert.ok(all.length >= 3, `should have at least 3 notes; got ${all.length}`);
|
|
271
|
+
const relPaths = all.map((n) => n.relPath);
|
|
272
|
+
assert.ok(relPaths.some((p) => p.includes('local-project')), 'should include project note');
|
|
273
|
+
assert.ok(relPaths.some((p) => p.includes('local-global')), 'should include global note');
|
|
274
|
+
assert.ok(relPaths.some((p) => p.includes('local-user')), 'should include user note');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('namespace filter returns only notes within that namespace directory', () => {
|
|
278
|
+
TEST_MEMORY_STORE.writeNote(localRoot, 'decisions/filter-project.md', validNote('project'));
|
|
279
|
+
TEST_MEMORY_STORE.writeNote(localRoot, 'global/bizar/filter-global.md', validNote('global'));
|
|
280
|
+
TEST_MEMORY_STORE.writeNote(localRoot, 'users/tester/filter-user.md', validNote('user'));
|
|
281
|
+
|
|
282
|
+
const projectNotes = TEST_MEMORY_STORE.listNotes(localRoot, { namespace: 'projects' });
|
|
283
|
+
const globalNotes = TEST_MEMORY_STORE.listNotes(localRoot, { namespace: 'global/bizar' });
|
|
284
|
+
const userNotes = TEST_MEMORY_STORE.listNotes(localRoot, { namespace: 'users/tester' });
|
|
285
|
+
|
|
286
|
+
// Each namespace filter returns only notes from within that namespace directory.
|
|
287
|
+
// relPath is relative to searchRoot (the namespace directory), not to vaultRoot,
|
|
288
|
+
// so a note at .obsidian/global/bizar/filter-global.md has relPath 'filter-global.md'
|
|
289
|
+
// when listed with namespace='global/bizar'.
|
|
290
|
+
|
|
291
|
+
// Verify: notes written to global/bizar/ appear in globalNotes
|
|
292
|
+
const globalRelPaths = globalNotes.map((n) => n.relPath);
|
|
293
|
+
assert.ok(
|
|
294
|
+
globalRelPaths.some((p) => p.includes('filter-global')),
|
|
295
|
+
`globalNotes should include filter-global; got: ${JSON.stringify(globalRelPaths)}`,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// Verify: notes written to users/tester/ appear in userNotes
|
|
299
|
+
const userRelPaths = userNotes.map((n) => n.relPath);
|
|
300
|
+
assert.ok(
|
|
301
|
+
userRelPaths.some((p) => p.includes('filter-user')),
|
|
302
|
+
`userNotes should include filter-user; got: ${JSON.stringify(userRelPaths)}`,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Verify: project notes do NOT appear in globalNotes
|
|
306
|
+
for (const n of globalNotes) {
|
|
307
|
+
assert.ok(
|
|
308
|
+
!n.relPath.includes('filter-project'),
|
|
309
|
+
`globalNotes should NOT include project note; got relPath: ${n.relPath}`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Verify: global notes do NOT appear in userNotes
|
|
314
|
+
for (const n of userNotes) {
|
|
315
|
+
assert.ok(
|
|
316
|
+
!n.relPath.includes('filter-global'),
|
|
317
|
+
`userNotes should NOT include global note; got relPath: ${n.relPath}`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Verify: no overlap between any two namespace sets
|
|
322
|
+
const globalSet = new Set(globalNotes.map((n) => n.relPath));
|
|
323
|
+
const userSet = new Set(userNotes.map((n) => n.relPath));
|
|
324
|
+
assert.strictEqual(
|
|
325
|
+
[...globalSet].filter((p) => userSet.has(p)).length,
|
|
326
|
+
0,
|
|
327
|
+
'global and user sets should not overlap',
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('searchVault: cross-namespace search', () => {
|
|
333
|
+
beforeEach(() => {
|
|
334
|
+
TEST_MEMORY_STORE.writeNote(projectRoot, 'decisions/search-project.md', {
|
|
335
|
+
frontmatter: {
|
|
336
|
+
memory_id: `search_proj_${uid().slice(0, 8)}`,
|
|
337
|
+
type: 'session_summary', project_id: 'ns-test-proj', status: 'active',
|
|
338
|
+
confidence: 'verified', created: new Date().toISOString(),
|
|
339
|
+
updated: new Date().toISOString(), tags: [], title: 'Project note UNIQUE_TOKEN_PRJ',
|
|
340
|
+
},
|
|
341
|
+
body: 'Project namespace token UNIQUE_TOKEN_PRJ',
|
|
342
|
+
});
|
|
343
|
+
TEST_MEMORY_STORE.writeNote(projectRoot, 'global/bizar/search-global.md', {
|
|
344
|
+
frontmatter: {
|
|
345
|
+
memory_id: `search_global_${uid().slice(0, 8)}`,
|
|
346
|
+
type: 'session_summary', project_id: 'ns-test-proj', status: 'active',
|
|
347
|
+
confidence: 'verified', created: new Date().toISOString(),
|
|
348
|
+
updated: new Date().toISOString(), tags: [], title: 'Global note UNIQUE_TOKEN_GLB',
|
|
349
|
+
},
|
|
350
|
+
body: 'Global namespace token UNIQUE_TOKEN_GLB',
|
|
351
|
+
});
|
|
352
|
+
TEST_MEMORY_STORE.writeNote(projectRoot, 'users/tester/search-user.md', {
|
|
353
|
+
frontmatter: {
|
|
354
|
+
memory_id: `search_user_${uid().slice(0, 8)}`,
|
|
355
|
+
type: 'session_summary', project_id: 'ns-test-proj', status: 'active',
|
|
356
|
+
confidence: 'verified', created: new Date().toISOString(),
|
|
357
|
+
updated: new Date().toISOString(), tags: [], title: 'User note UNIQUE_TOKEN_USR',
|
|
358
|
+
},
|
|
359
|
+
body: 'User namespace token UNIQUE_TOKEN_USR',
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('searchVault finds notes in the project namespace by unique token', () => {
|
|
364
|
+
const results = TEST_MEMORY_STORE.searchVault(projectRoot, 'UNIQUE_TOKEN_PRJ');
|
|
365
|
+
assert.ok(results.length >= 1, 'should find project note');
|
|
366
|
+
assert.ok(results.some((r) => r.relPath.includes('search-project')), 'should find the project note');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('searchVault finds notes in the global namespace by unique token', () => {
|
|
370
|
+
const results = TEST_MEMORY_STORE.searchVault(projectRoot, 'UNIQUE_TOKEN_GLB');
|
|
371
|
+
assert.ok(results.length >= 1, 'should find global note');
|
|
372
|
+
assert.ok(results.some((r) => r.relPath.includes('search-global')), 'should find the global note');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('searchVault finds notes in the user namespace by unique token', () => {
|
|
376
|
+
const results = TEST_MEMORY_STORE.searchVault(projectRoot, 'UNIQUE_TOKEN_USR');
|
|
377
|
+
assert.ok(results.length >= 1, 'should find user note');
|
|
378
|
+
assert.ok(results.some((r) => r.relPath.includes('search-user')), 'should find the user note');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('searchVault cross-namespace search returns notes from ALL namespaces', () => {
|
|
382
|
+
const results = TEST_MEMORY_STORE.searchVault(projectRoot, 'namespace token');
|
|
383
|
+
const relPaths = results.map((r) => r.relPath);
|
|
384
|
+
assert.ok(relPaths.some((p) => p.includes('search-project')), 'should include project note');
|
|
385
|
+
assert.ok(relPaths.some((p) => p.includes('search-global')), 'should include global note');
|
|
386
|
+
assert.ok(relPaths.some((p) => p.includes('search-user')), 'should include user note');
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe('deleteNote: namespace isolation', () => {
|
|
391
|
+
it('deleting from one namespace does not affect notes in another', () => {
|
|
392
|
+
TEST_MEMORY_STORE.writeNote(projectRoot, 'decisions/to-delete-project.md', validNote('project'));
|
|
393
|
+
TEST_MEMORY_STORE.writeNote(projectRoot, 'global/bizar/to-delete-global.md', validNote('global'));
|
|
394
|
+
|
|
395
|
+
const deleted = TEST_MEMORY_STORE.deleteNote(projectRoot, 'decisions/to-delete-project.md');
|
|
396
|
+
assert.strictEqual(deleted, true, 'deleteNote should return true for project note');
|
|
397
|
+
|
|
398
|
+
assert.strictEqual(TEST_MEMORY_STORE.readNote(projectRoot, 'decisions/to-delete-project.md'), null, 'project note should be gone');
|
|
399
|
+
|
|
400
|
+
const globalStillThere = TEST_MEMORY_STORE.readNote(projectRoot, 'global/bizar/to-delete-global.md');
|
|
401
|
+
assert.ok(globalStillThere, 'global note should still exist after project note deletion');
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
});
|