memory-topics.test.mjs
664 lines 24.9 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Session 3 — Topic-Based Memory Partitioning tests.
3 *
4 * Covers: extractTopicFromEvent, slugify, FileMemoryProvider topic partitioning,
5 * MemoryManager topic methods, generateMemoryIndex with topics, CLI --topic,
6 * and MCP metadata builder.
7 */
8 import { describe, it, before, after, beforeEach } from 'node:test';
9 import assert from 'node:assert';
10 import fs from 'fs';
11 import path from 'path';
12 import os from 'os';
13
14 import { execSync } from 'child_process';
15 import { fileURLToPath } from 'url';
16
17 import {
18 createMemoryEvent,
19 extractTopicFromEvent,
20 slugify,
21 } from '../lib/memory-event.mjs';
22 import { FileMemoryProvider } from '../lib/memory-provider-file.mjs';
23 import {
24 MemoryManager,
25 createMemoryManager,
26 generateMemoryIndex,
27 } from '../lib/memory.mjs';
28
29 const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
31 let tmpDir;
32
33 before(() => {
34 tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'knowtation-topics-test-'));
35 });
36
37 after(() => {
38 fs.rmSync(tmpDir, { recursive: true, force: true });
39 });
40
41 // ---------------------------------------------------------------------------
42 // slugify
43 // ---------------------------------------------------------------------------
44
45 describe('slugify', () => {
46 it('lowercases and replaces non-alphanumeric with hyphens', () => {
47 assert.strictEqual(slugify('Hello World!'), 'hello-world');
48 });
49
50 it('strips leading and trailing hyphens', () => {
51 assert.strictEqual(slugify('--foo--bar--'), 'foo-bar');
52 });
53
54 it('collapses runs of hyphens', () => {
55 assert.strictEqual(slugify('a b c'), 'a-b-c');
56 });
57
58 it('truncates to 64 chars', () => {
59 const long = 'a'.repeat(100);
60 assert.strictEqual(slugify(long).length, 64);
61 });
62
63 it('handles empty string', () => {
64 assert.strictEqual(slugify(''), '');
65 });
66
67 it('handles numbers', () => {
68 assert.strictEqual(slugify('Phase 12'), 'phase-12');
69 });
70 });
71
72 // ---------------------------------------------------------------------------
73 // extractTopicFromEvent
74 // ---------------------------------------------------------------------------
75
76 describe('extractTopicFromEvent', () => {
77 it('uses explicit data.topic when provided', () => {
78 const event = createMemoryEvent('search', { topic: 'Blockchain Architecture', query: 'test' });
79 assert.strictEqual(extractTopicFromEvent(event), 'blockchain-architecture');
80 });
81
82 it('derives topic from data.path directory component', () => {
83 const event = createMemoryEvent('write', { path: 'projects/notes/file.md' });
84 assert.strictEqual(extractTopicFromEvent(event), 'projects');
85 });
86
87 it('derives topic from data.paths[0] directory component', () => {
88 const event = createMemoryEvent('search', { query: 'test', paths: ['inbox/capture.md', 'other.md'] });
89 assert.strictEqual(extractTopicFromEvent(event), 'inbox');
90 });
91
92 it('uses filename stem when path has no directory', () => {
93 const event = createMemoryEvent('write', { path: 'my-note.md' });
94 assert.strictEqual(extractTopicFromEvent(event), 'my-note');
95 });
96
97 it('extracts keywords from data.query', () => {
98 const event = createMemoryEvent('search', { query: 'blockchain consensus mechanism' });
99 assert.strictEqual(extractTopicFromEvent(event), 'blockchain-consensus-mechanism');
100 });
101
102 it('filters stop words from query', () => {
103 const event = createMemoryEvent('search', { query: 'the best way to do it' });
104 assert.strictEqual(extractTopicFromEvent(event), 'best-way');
105 });
106
107 it('limits query keywords to 3', () => {
108 const event = createMemoryEvent('search', { query: 'alpha beta gamma delta epsilon' });
109 assert.strictEqual(extractTopicFromEvent(event), 'alpha-beta-gamma');
110 });
111
112 it('uses data.source as fallback', () => {
113 const event = createMemoryEvent('capture', { source: 'mem0', text: 'hello' });
114 assert.strictEqual(extractTopicFromEvent(event), 'mem0');
115 });
116
117 it('uses data.source_type as fallback', () => {
118 const event = createMemoryEvent('import', { source_type: 'chatgpt', count: 5 });
119 assert.strictEqual(extractTopicFromEvent(event), 'chatgpt');
120 });
121
122 it('uses data.key for user events', () => {
123 const event = createMemoryEvent('user', { key: 'my_preference', theme: 'dark' });
124 assert.strictEqual(extractTopicFromEvent(event), 'my-preference');
125 });
126
127 it('uses data.format for export events', () => {
128 const event = createMemoryEvent('export', { format: 'md' });
129 assert.strictEqual(extractTopicFromEvent(event), 'export-md');
130 });
131
132 it('falls back to event type', () => {
133 const event = createMemoryEvent('index', { count: 100 });
134 assert.strictEqual(extractTopicFromEvent(event), 'index');
135 });
136
137 it('returns "unknown" for null event', () => {
138 assert.strictEqual(extractTopicFromEvent(null), 'unknown');
139 });
140
141 it('returns event type for null data', () => {
142 assert.strictEqual(extractTopicFromEvent({ type: 'search', data: null }), 'search');
143 });
144
145 it('handles backslash paths (Windows-style)', () => {
146 const event = createMemoryEvent('write', { path: 'vault\\notes\\test.md' });
147 assert.strictEqual(extractTopicFromEvent(event), 'vault');
148 });
149 });
150
151 // ---------------------------------------------------------------------------
152 // FileMemoryProvider — topic partitioning
153 // ---------------------------------------------------------------------------
154
155 describe('FileMemoryProvider with topicPartition', () => {
156 let providerDir;
157 let provider;
158
159 beforeEach(() => {
160 providerDir = path.join(tmpDir, 'fmp-topic-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6));
161 provider = new FileMemoryProvider(providerDir, { topicPartition: true });
162 });
163
164 it('topicPartitionEnabled returns true', () => {
165 assert.strictEqual(provider.topicPartitionEnabled, true);
166 });
167
168 it('storeEvent writes to both events.jsonl and topics/{slug}.jsonl', () => {
169 const event = createMemoryEvent('write', { path: 'projects/note.md' });
170 const result = provider.storeEvent(event);
171 assert.strictEqual(result.topic, 'projects');
172
173 const mainLog = path.join(providerDir, 'events.jsonl');
174 assert(fs.existsSync(mainLog));
175
176 const topicFile = path.join(providerDir, 'topics', 'projects.jsonl');
177 assert(fs.existsSync(topicFile));
178
179 const topicLines = fs.readFileSync(topicFile, 'utf8').split('\n').filter(Boolean);
180 assert.strictEqual(topicLines.length, 1);
181 const parsed = JSON.parse(topicLines[0]);
182 assert.strictEqual(parsed.id, event.id);
183 });
184
185 it('storeEvent returns topic in result', () => {
186 const event = createMemoryEvent('search', { query: 'blockchain consensus' });
187 const result = provider.storeEvent(event);
188 assert.strictEqual(result.topic, 'blockchain-consensus');
189 });
190
191 it('multiple events to same topic append to same file', () => {
192 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/a.md' }));
193 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/b.md' }));
194 provider.storeEvent(createMemoryEvent('write', { path: 'projects/c.md' }));
195
196 const inboxLines = fs.readFileSync(path.join(providerDir, 'topics', 'inbox.jsonl'), 'utf8')
197 .split('\n').filter(Boolean);
198 assert.strictEqual(inboxLines.length, 2);
199
200 const projectLines = fs.readFileSync(path.join(providerDir, 'topics', 'projects.jsonl'), 'utf8')
201 .split('\n').filter(Boolean);
202 assert.strictEqual(projectLines.length, 1);
203 });
204
205 it('listEvents with topic filter reads from topic file', () => {
206 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/a.md' }));
207 provider.storeEvent(createMemoryEvent('write', { path: 'projects/b.md' }));
208 provider.storeEvent(createMemoryEvent('search', { query: 'test', paths: ['inbox/c.md'] }));
209
210 const inboxEvents = provider.listEvents({ topic: 'inbox' });
211 assert.strictEqual(inboxEvents.length, 2);
212 for (const e of inboxEvents) {
213 assert(e.data.path?.startsWith('inbox') || e.data.paths?.[0]?.startsWith('inbox'));
214 }
215 });
216
217 it('listEvents with topic + type applies both filters', () => {
218 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/a.md' }));
219 provider.storeEvent(createMemoryEvent('search', { query: 'test', paths: ['inbox/c.md'] }));
220
221 const events = provider.listEvents({ topic: 'inbox', type: 'write' });
222 assert.strictEqual(events.length, 1);
223 assert.strictEqual(events[0].type, 'write');
224 });
225
226 it('listEvents with nonexistent topic returns empty', () => {
227 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/a.md' }));
228 const events = provider.listEvents({ topic: 'nonexistent' });
229 assert.deepStrictEqual(events, []);
230 });
231
232 it('listTopics returns all topic slugs', () => {
233 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/a.md' }));
234 provider.storeEvent(createMemoryEvent('write', { path: 'projects/b.md' }));
235 provider.storeEvent(createMemoryEvent('search', { query: 'blockchain test' }));
236
237 const topics = provider.listTopics();
238 assert(Array.isArray(topics));
239 assert(topics.includes('inbox'));
240 assert(topics.includes('projects'));
241 assert(topics.includes('blockchain-test'));
242 assert.deepStrictEqual(topics, [...topics].sort());
243 });
244
245 it('getTopicStats returns correct stats', () => {
246 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/a.md' }));
247 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/b.md' }));
248
249 const stats = provider.getTopicStats('inbox');
250 assert.strictEqual(stats.topic, 'inbox');
251 assert.strictEqual(stats.total, 2);
252 assert.strictEqual(typeof stats.oldest, 'string');
253 assert.strictEqual(typeof stats.newest, 'string');
254 });
255
256 it('getTopicStats returns zero for unknown topic', () => {
257 const stats = provider.getTopicStats('unknown');
258 assert.strictEqual(stats.total, 0);
259 assert.strictEqual(stats.oldest, null);
260 });
261
262 it('getStats includes topics array', () => {
263 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/a.md' }));
264 provider.storeEvent(createMemoryEvent('write', { path: 'projects/b.md' }));
265
266 const stats = provider.getStats();
267 assert(Array.isArray(stats.topics));
268 assert(stats.topics.includes('inbox'));
269 assert(stats.topics.includes('projects'));
270 });
271
272 it('clearEvents rebuilds topic partitions', () => {
273 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/a.md' }));
274 provider.storeEvent(createMemoryEvent('search', { query: 'blockchain' }));
275
276 assert(provider.listTopics().includes('inbox'));
277 assert(provider.listTopics().includes('blockchain'));
278
279 provider.clearEvents({ type: 'write' });
280
281 const topics = provider.listTopics();
282 assert(!topics.includes('inbox'));
283 assert(topics.includes('blockchain'));
284 });
285
286 it('clearEvents with no filters empties all topic files', () => {
287 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/a.md' }));
288 provider.storeEvent(createMemoryEvent('search', { query: 'test' }));
289
290 provider.clearEvents();
291
292 const topics = provider.listTopics();
293 assert.strictEqual(topics.length, 0);
294 });
295
296 it('pruneExpired rebuilds topic partitions', () => {
297 const old = createMemoryEvent('write', { path: 'inbox/a.md' });
298 old.ts = new Date(Date.now() - 100 * 86_400_000).toISOString();
299 provider.storeEvent(old);
300 provider.storeEvent(createMemoryEvent('search', { query: 'recent blockchain' }));
301
302 assert(provider.listTopics().includes('inbox'));
303
304 provider.pruneExpired(30);
305
306 const topics = provider.listTopics();
307 assert(!topics.includes('inbox'));
308 assert(topics.includes('recent-blockchain'));
309 });
310 });
311
312 // ---------------------------------------------------------------------------
313 // FileMemoryProvider — topic filter without partitioning (fallback scan)
314 // ---------------------------------------------------------------------------
315
316 describe('FileMemoryProvider topic filter without partitioning', () => {
317 let providerDir;
318 let provider;
319
320 beforeEach(() => {
321 providerDir = path.join(tmpDir, 'fmp-notopic-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6));
322 provider = new FileMemoryProvider(providerDir);
323 });
324
325 it('topicPartitionEnabled returns false', () => {
326 assert.strictEqual(provider.topicPartitionEnabled, false);
327 });
328
329 it('storeEvent does NOT return topic field', () => {
330 const event = createMemoryEvent('write', { path: 'inbox/a.md' });
331 const result = provider.storeEvent(event);
332 assert.strictEqual(result.topic, undefined);
333 });
334
335 it('storeEvent does NOT create topics/ directory', () => {
336 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/a.md' }));
337 assert(!fs.existsSync(path.join(providerDir, 'topics')));
338 });
339
340 it('listEvents with topic filter still works (scans all events)', () => {
341 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/a.md' }));
342 provider.storeEvent(createMemoryEvent('write', { path: 'projects/b.md' }));
343
344 const events = provider.listEvents({ topic: 'inbox' });
345 assert.strictEqual(events.length, 1);
346 assert.strictEqual(events[0].data.path, 'inbox/a.md');
347 });
348
349 it('listTopics returns empty when no partitioning', () => {
350 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/a.md' }));
351 assert.deepStrictEqual(provider.listTopics(), []);
352 });
353
354 it('getStats does NOT include topics array', () => {
355 provider.storeEvent(createMemoryEvent('write', { path: 'inbox/a.md' }));
356 const stats = provider.getStats();
357 assert.strictEqual(stats.topics, undefined);
358 });
359 });
360
361 // ---------------------------------------------------------------------------
362 // MemoryManager topic methods
363 // ---------------------------------------------------------------------------
364
365 describe('MemoryManager topic methods', () => {
366 it('list with topic filter delegates to provider', () => {
367 const dir = path.join(tmpDir, 'mm-topic-' + Date.now());
368 const provider = new FileMemoryProvider(dir, { topicPartition: true });
369 const mm = new MemoryManager(provider);
370
371 mm.store('write', { path: 'inbox/a.md' });
372 mm.store('write', { path: 'projects/b.md' });
373
374 const events = mm.list({ topic: 'inbox' });
375 assert.strictEqual(events.length, 1);
376 assert.strictEqual(events[0].data.path, 'inbox/a.md');
377 });
378
379 it('listTopics returns topic slugs', () => {
380 const dir = path.join(tmpDir, 'mm-topics-list-' + Date.now());
381 const provider = new FileMemoryProvider(dir, { topicPartition: true });
382 const mm = new MemoryManager(provider);
383
384 mm.store('write', { path: 'inbox/a.md' });
385 mm.store('search', { query: 'blockchain consensus' });
386
387 const topics = mm.listTopics();
388 assert(topics.includes('inbox'));
389 assert(topics.includes('blockchain-consensus'));
390 });
391
392 it('topicStats returns stats for a topic', () => {
393 const dir = path.join(tmpDir, 'mm-topic-stats-' + Date.now());
394 const provider = new FileMemoryProvider(dir, { topicPartition: true });
395 const mm = new MemoryManager(provider);
396
397 mm.store('write', { path: 'inbox/a.md' });
398 mm.store('write', { path: 'inbox/b.md' });
399
400 const stats = mm.topicStats('inbox');
401 assert.strictEqual(stats.total, 2);
402 });
403
404 it('listTopics returns empty for non-partitioned provider', () => {
405 const dir = path.join(tmpDir, 'mm-notopic-' + Date.now());
406 const provider = new FileMemoryProvider(dir);
407 const mm = new MemoryManager(provider);
408 mm.store('write', { path: 'inbox/a.md' });
409 assert.deepStrictEqual(mm.listTopics(), []);
410 });
411 });
412
413 // ---------------------------------------------------------------------------
414 // createMemoryManager with topic_partition config
415 // ---------------------------------------------------------------------------
416
417 describe('createMemoryManager with topic_partition', () => {
418 it('creates provider with topicPartition when config says topic_partition: true', () => {
419 const dataDir = path.join(tmpDir, 'cmm-tp-' + Date.now());
420 fs.mkdirSync(dataDir, { recursive: true });
421 const config = {
422 data_dir: dataDir,
423 memory: { enabled: true, provider: 'file', topic_partition: true },
424 };
425 const mm = createMemoryManager(config);
426 mm.store('write', { path: 'inbox/a.md' });
427
428 const topicFile = path.join(dataDir, 'memory', 'default', 'topics', 'inbox.jsonl');
429 assert(fs.existsSync(topicFile));
430 });
431
432 it('does NOT create topic files when topic_partition is not set', () => {
433 const dataDir = path.join(tmpDir, 'cmm-notp-' + Date.now());
434 fs.mkdirSync(dataDir, { recursive: true });
435 const config = {
436 data_dir: dataDir,
437 memory: { enabled: true, provider: 'file' },
438 };
439 const mm = createMemoryManager(config);
440 mm.store('write', { path: 'inbox/a.md' });
441
442 const topicsDir = path.join(dataDir, 'memory', 'default', 'topics');
443 assert(!fs.existsSync(topicsDir));
444 });
445 });
446
447 // ---------------------------------------------------------------------------
448 // generateMemoryIndex with topics
449 // ---------------------------------------------------------------------------
450
451 describe('generateMemoryIndex with topics', () => {
452 it('includes Topics section when topics exist', () => {
453 const dir = path.join(tmpDir, 'idx-topics-' + Date.now());
454 const provider = new FileMemoryProvider(dir, { topicPartition: true });
455 const mm = new MemoryManager(provider);
456
457 mm.store('write', { path: 'inbox/a.md' });
458 mm.store('search', { query: 'blockchain consensus' });
459
460 const idx = generateMemoryIndex(mm);
461 assert(idx.markdown.includes('## Topics'));
462 assert(idx.markdown.includes('inbox:'));
463 assert(idx.markdown.includes('blockchain-consensus:'));
464 assert(Array.isArray(idx.topics));
465 assert(idx.topics.includes('inbox'));
466 assert(idx.topics.includes('blockchain-consensus'));
467 });
468
469 it('omits Topics section when no topics', () => {
470 const dir = path.join(tmpDir, 'idx-no-topics-' + Date.now());
471 const provider = new FileMemoryProvider(dir);
472 const mm = new MemoryManager(provider);
473
474 mm.store('search', { query: 'test' });
475
476 const idx = generateMemoryIndex(mm);
477 assert(!idx.markdown.includes('## Topics'));
478 assert.deepStrictEqual(idx.topics, []);
479 });
480
481 it('Topics section shows event counts per topic', () => {
482 const dir = path.join(tmpDir, 'idx-topic-counts-' + Date.now());
483 const provider = new FileMemoryProvider(dir, { topicPartition: true });
484 const mm = new MemoryManager(provider);
485
486 mm.store('write', { path: 'inbox/a.md' });
487 mm.store('write', { path: 'inbox/b.md' });
488 mm.store('write', { path: 'inbox/c.md' });
489
490 const idx = generateMemoryIndex(mm);
491 assert(idx.markdown.includes('inbox: 3 events'));
492 });
493 });
494
495 // ---------------------------------------------------------------------------
496 // buildMemoryTopicResource
497 // ---------------------------------------------------------------------------
498
499 describe('buildMemoryTopicResource', () => {
500 it('returns events for a topic', async () => {
501 const { buildMemoryTopicResource } = await import('../mcp/resources/metadata.mjs');
502 const dataDir = path.join(tmpDir, 'mcp-topic-' + Date.now());
503 fs.mkdirSync(dataDir, { recursive: true });
504 const config = { data_dir: dataDir, memory: { enabled: true, provider: 'file', topic_partition: true } };
505 const mm = createMemoryManager(config);
506 mm.store('write', { path: 'inbox/a.md' });
507 mm.store('write', { path: 'projects/b.md' });
508
509 const result = buildMemoryTopicResource(config, 'inbox');
510 assert.strictEqual(result.enabled, true);
511 assert.strictEqual(result.topic, 'inbox');
512 assert.strictEqual(result.count, 1);
513 assert(Array.isArray(result.events));
514 assert(Array.isArray(result.all_topics));
515 });
516
517 it('returns empty when memory disabled', async () => {
518 const { buildMemoryTopicResource } = await import('../mcp/resources/metadata.mjs');
519 const result = buildMemoryTopicResource({ memory: { enabled: false } }, 'inbox');
520 assert.strictEqual(result.enabled, false);
521 assert.strictEqual(result.count, 0);
522 });
523
524 it('returns empty for nonexistent topic', async () => {
525 const { buildMemoryTopicResource } = await import('../mcp/resources/metadata.mjs');
526 const dataDir = path.join(tmpDir, 'mcp-topic-miss-' + Date.now());
527 fs.mkdirSync(dataDir, { recursive: true });
528 const config = { data_dir: dataDir, memory: { enabled: true, provider: 'file', topic_partition: true } };
529 const result = buildMemoryTopicResource(config, 'nonexistent');
530 assert.strictEqual(result.count, 0);
531 });
532 });
533
534 // ---------------------------------------------------------------------------
535 // CLI --topic integration (via execSync)
536 // ---------------------------------------------------------------------------
537
538 describe('CLI memory list --topic', () => {
539 const cliPath = path.join(__dirname, '..', 'cli', 'index.mjs');
540 let cliTmpDir;
541 let vaultDir;
542 let dataDir;
543
544 before(() => {
545 cliTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'knowtation-cli-topic-'));
546 vaultDir = path.join(cliTmpDir, 'vault');
547 dataDir = path.join(cliTmpDir, 'data');
548 fs.mkdirSync(vaultDir, { recursive: true });
549 fs.mkdirSync(dataDir, { recursive: true });
550 fs.mkdirSync(path.join(cliTmpDir, 'config'), { recursive: true });
551 fs.writeFileSync(
552 path.join(cliTmpDir, 'config', 'local.yaml'),
553 `vault_path: ${vaultDir}\ndata_dir: ${dataDir}\nmemory:\n enabled: true\n provider: file\n topic_partition: true\n`,
554 'utf8'
555 );
556 fs.writeFileSync(path.join(vaultDir, 'test.md'), '---\ntitle: test\n---\nHello', 'utf8');
557 });
558
559 after(() => {
560 fs.rmSync(cliTmpDir, { recursive: true, force: true });
561 });
562
563 function run(cmdArgs) {
564 const env = {
565 ...process.env,
566 KNOWTATION_VAULT_PATH: vaultDir,
567 KNOWTATION_DATA_DIR: dataDir,
568 KNOWTATION_MEMORY_ENABLED: 'true',
569 KNOWTATION_MEMORY_PROVIDER: 'file',
570 };
571 try {
572 const out = execSync(`node ${cliPath} ${cmdArgs}`, {
573 cwd: path.join(__dirname, '..'),
574 env,
575 timeout: 10000,
576 encoding: 'utf8',
577 stdio: ['pipe', 'pipe', 'pipe'],
578 });
579 return { stdout: out.trim(), exitCode: 0 };
580 } catch (e) {
581 return { stdout: (e.stdout || '').trim(), stderr: (e.stderr || '').trim(), exitCode: e.status };
582 }
583 }
584
585 it('memory list --topic filters events by topic', () => {
586 run('memory store inbox_item \'{"path":"inbox/a.md"}\'');
587 run('memory store project_item \'{"path":"projects/b.md"}\'');
588
589 const allR = run('memory list --json');
590 assert.strictEqual(allR.exitCode, 0);
591 const allData = JSON.parse(allR.stdout);
592 assert(allData.count >= 2);
593
594 const topicR = run('memory list --topic inbox --json');
595 assert.strictEqual(topicR.exitCode, 0);
596 });
597
598 it('memory list --help mentions --topic', () => {
599 const r = run('memory --help');
600 assert.strictEqual(r.exitCode, 0);
601 assert(r.stdout.includes('--topic'));
602 });
603 });
604
605 // ---------------------------------------------------------------------------
606 // Edge cases and backward compatibility
607 // ---------------------------------------------------------------------------
608
609 describe('topic partitioning backward compatibility', () => {
610 it('existing events without topic partitioning are not lost', () => {
611 const dir = path.join(tmpDir, 'compat-topic-' + Date.now());
612 const oldProvider = new FileMemoryProvider(dir);
613 oldProvider.storeEvent(createMemoryEvent('search', { query: 'old event' }));
614 oldProvider.storeEvent(createMemoryEvent('write', { path: 'inbox/note.md' }));
615
616 assert(!fs.existsSync(path.join(dir, 'topics')));
617
618 const newProvider = new FileMemoryProvider(dir, { topicPartition: true });
619 const events = newProvider.listEvents();
620 assert.strictEqual(events.length, 2);
621
622 newProvider.storeEvent(createMemoryEvent('write', { path: 'inbox/new.md' }));
623 assert(fs.existsSync(path.join(dir, 'topics', 'inbox.jsonl')));
624
625 const inboxFromTopic = newProvider.listEvents({ topic: 'inbox' });
626 assert.strictEqual(inboxFromTopic.length, 1);
627 assert.strictEqual(inboxFromTopic[0].data.path, 'inbox/new.md');
628
629 const allEvents = newProvider.listEvents();
630 assert.strictEqual(allEvents.length, 3);
631 });
632
633 it('FileMemoryProvider constructor defaults topicPartition to false', () => {
634 const dir = path.join(tmpDir, 'compat-default-' + Date.now());
635 const provider = new FileMemoryProvider(dir);
636 assert.strictEqual(provider.topicPartitionEnabled, false);
637 });
638
639 it('all existing provider methods still work with topic partitioning enabled', () => {
640 const dir = path.join(tmpDir, 'compat-methods-' + Date.now());
641 const provider = new FileMemoryProvider(dir, { topicPartition: true });
642
643 const event = createMemoryEvent('search', { query: 'test' });
644 const result = provider.storeEvent(event);
645 assert.strictEqual(result.id, event.id);
646 assert.strictEqual(result.ts, event.ts);
647
648 const latest = provider.getLatest('search');
649 assert.strictEqual(latest.data.query, 'test');
650
651 const list = provider.listEvents();
652 assert.strictEqual(list.length, 1);
653
654 const stats = provider.getStats();
655 assert.strictEqual(stats.total, 1);
656
657 assert.strictEqual(provider.supportsSearch(), false);
658 assert.deepStrictEqual(provider.searchEvents('anything'), []);
659
660 const clearResult = provider.clearEvents();
661 assert.strictEqual(clearResult.cleared, 1);
662 assert.strictEqual(provider.listEvents().length, 0);
663 });
664 });
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago