@mndrk/memx 0.3.2 → 0.3.4

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.
Files changed (39) hide show
  1. package/README.md +98 -77
  2. package/coverage/clover.xml +1160 -0
  3. package/coverage/coverage-final.json +3 -0
  4. package/coverage/lcov-report/base.css +224 -0
  5. package/coverage/lcov-report/block-navigation.js +87 -0
  6. package/coverage/lcov-report/favicon.png +0 -0
  7. package/coverage/lcov-report/index.html +131 -0
  8. package/coverage/lcov-report/index.js.html +7255 -0
  9. package/coverage/lcov-report/mcp.js.html +1009 -0
  10. package/coverage/lcov-report/prettify.css +1 -0
  11. package/coverage/lcov-report/prettify.js +2 -0
  12. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  13. package/coverage/lcov-report/sorter.js +210 -0
  14. package/coverage/lcov.info +2017 -0
  15. package/index.js +651 -243
  16. package/package.json +24 -2
  17. package/test/additional.test.js +373 -0
  18. package/test/branches.test.js +247 -0
  19. package/test/commands.test.js +663 -0
  20. package/test/context.test.js +185 -0
  21. package/test/coverage.test.js +366 -0
  22. package/test/dispatch.test.js +220 -0
  23. package/test/edge-coverage.test.js +250 -0
  24. package/test/edge.test.js +434 -0
  25. package/test/final-coverage.test.js +316 -0
  26. package/test/final-edges.test.js +199 -0
  27. package/test/init-local.test.js +316 -0
  28. package/test/init.test.js +122 -0
  29. package/test/interactive.test.js +229 -0
  30. package/test/main-dispatch.test.js +164 -0
  31. package/test/main-full.test.js +590 -0
  32. package/test/main.test.js +197 -0
  33. package/test/mcp-server.test.js +320 -0
  34. package/test/mcp.test.js +288 -0
  35. package/test/more.test.js +312 -0
  36. package/test/new.test.js +175 -0
  37. package/test/skill.test.js +247 -0
  38. package/test/tasks-interactive.test.js +243 -0
  39. package/test/utils.test.js +367 -0
@@ -0,0 +1,663 @@
1
+ /**
2
+ * Tests for command functions in index.js
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ // Mock child_process before requiring index.js
10
+ jest.mock('child_process', () => ({
11
+ execSync: jest.fn(),
12
+ spawnSync: jest.fn(() => ({ status: 0, stdout: '', stderr: '' })),
13
+ spawn: jest.fn()
14
+ }));
15
+
16
+ // Mock readline
17
+ jest.mock('readline', () => ({
18
+ createInterface: jest.fn(() => ({
19
+ question: jest.fn((q, cb) => cb('')),
20
+ close: jest.fn(),
21
+ on: jest.fn()
22
+ }))
23
+ }));
24
+
25
+ const {
26
+ cmdStatus,
27
+ cmdGoal,
28
+ cmdNext,
29
+ cmdCheckpoint,
30
+ cmdLearn,
31
+ cmdSwitch,
32
+ cmdSync,
33
+ cmdHistory,
34
+ cmdStuck,
35
+ cmdQuery,
36
+ cmdPlaybook,
37
+ cmdLearnings,
38
+ cmdPromote,
39
+ cmdConstraint,
40
+ cmdProgress,
41
+ cmdCriteria,
42
+ cmdBranch,
43
+ cmdCommit,
44
+ cmdSet,
45
+ cmdGet,
46
+ cmdAppend,
47
+ cmdLog,
48
+ readMemFile,
49
+ writeMemFile,
50
+ parseFrontmatter,
51
+ git,
52
+ } = require('../index.js');
53
+
54
+ const { spawnSync, execSync } = require('child_process');
55
+
56
+ // Test directory setup
57
+ const testDir = path.join(os.tmpdir(), 'memx-test-commands-' + Date.now());
58
+ let memDir;
59
+
60
+ beforeEach(() => {
61
+ // Create fresh test directory
62
+ memDir = path.join(testDir, '.mem');
63
+ fs.mkdirSync(memDir, { recursive: true });
64
+ fs.mkdirSync(path.join(memDir, '.git'), { recursive: true });
65
+
66
+ // Reset mocks
67
+ spawnSync.mockClear();
68
+ execSync.mockClear();
69
+ jest.spyOn(console, 'log').mockImplementation();
70
+ });
71
+
72
+ afterEach(() => {
73
+ if (fs.existsSync(testDir)) {
74
+ fs.rmSync(testDir, { recursive: true });
75
+ }
76
+ jest.restoreAllMocks();
77
+ });
78
+
79
+ describe('cmdStatus', () => {
80
+ test('prints warning when no memDir', () => {
81
+ const consoleSpy = jest.spyOn(console, 'log');
82
+ cmdStatus(null);
83
+ expect(consoleSpy).toHaveBeenCalled();
84
+ const output = consoleSpy.mock.calls[0][0];
85
+ expect(output).toContain('No .mem repo found');
86
+ });
87
+
88
+ test('displays status when memDir exists', () => {
89
+ spawnSync.mockReturnValue({ status: 0, stdout: 'task/test\n', stderr: '' });
90
+
91
+ writeMemFile(memDir, 'goal.md', `---
92
+ task: test
93
+ ---
94
+
95
+ # Goal
96
+
97
+ Test goal
98
+ `);
99
+ writeMemFile(memDir, 'state.md', `---
100
+ status: active
101
+ ---
102
+
103
+ ## Next Step
104
+
105
+ Do something
106
+ `);
107
+
108
+ const consoleSpy = jest.spyOn(console, 'log');
109
+ cmdStatus(memDir);
110
+
111
+ expect(consoleSpy).toHaveBeenCalled();
112
+ });
113
+ });
114
+
115
+ describe('cmdGoal', () => {
116
+ test('prints warning when no memDir', () => {
117
+ const consoleSpy = jest.spyOn(console, 'log');
118
+ cmdGoal([], null);
119
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
120
+ });
121
+
122
+ test('displays current goal when no args', () => {
123
+ writeMemFile(memDir, 'goal.md', '# Goal\n\nTest goal content');
124
+ const consoleSpy = jest.spyOn(console, 'log');
125
+ cmdGoal([], memDir);
126
+ expect(consoleSpy).toHaveBeenCalled();
127
+ expect(consoleSpy.mock.calls[0][0]).toContain('Test goal content');
128
+ });
129
+
130
+ test('sets new goal when args provided', () => {
131
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
132
+ writeMemFile(memDir, 'goal.md', '---\ntask: test\n---\n\n# Old Goal');
133
+
134
+ cmdGoal(['New', 'goal', 'text'], memDir);
135
+
136
+ const content = readMemFile(memDir, 'goal.md');
137
+ expect(content).toContain('New goal text');
138
+ });
139
+ });
140
+
141
+ describe('cmdNext', () => {
142
+ test('prints warning when no memDir', () => {
143
+ const consoleSpy = jest.spyOn(console, 'log');
144
+ cmdNext([], null);
145
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
146
+ });
147
+
148
+ test('displays current next step when no args', () => {
149
+ writeMemFile(memDir, 'state.md', `---
150
+ status: active
151
+ ---
152
+
153
+ ## Next Step
154
+
155
+ Implement feature X
156
+ `);
157
+ const consoleSpy = jest.spyOn(console, 'log');
158
+ cmdNext([], memDir);
159
+ expect(consoleSpy.mock.calls[0][0]).toContain('Implement feature X');
160
+ });
161
+
162
+ test('sets new next step when args provided', () => {
163
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
164
+ writeMemFile(memDir, 'state.md', `---
165
+ status: active
166
+ ---
167
+
168
+ ## Next Step
169
+
170
+ Old step
171
+ `);
172
+
173
+ cmdNext(['New', 'step', 'here'], memDir);
174
+
175
+ const content = readMemFile(memDir, 'state.md');
176
+ expect(content).toContain('New step here');
177
+ });
178
+ });
179
+
180
+ describe('cmdCheckpoint', () => {
181
+ test('prints warning when no memDir', () => {
182
+ const consoleSpy = jest.spyOn(console, 'log');
183
+ cmdCheckpoint([], null);
184
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
185
+ });
186
+
187
+ test('requires message argument', () => {
188
+ const consoleSpy = jest.spyOn(console, 'log');
189
+ cmdCheckpoint([], memDir);
190
+ expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
191
+ });
192
+
193
+ test('adds checkpoint to state.md', () => {
194
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
195
+ writeMemFile(memDir, 'state.md', `---
196
+ status: active
197
+ ---
198
+
199
+ ## Checkpoints
200
+
201
+ - [ ] Started
202
+ `);
203
+
204
+ cmdCheckpoint(['Completed', 'first', 'task'], memDir);
205
+
206
+ const content = readMemFile(memDir, 'state.md');
207
+ expect(content).toContain('Completed first task');
208
+ expect(content).toContain('[x]');
209
+ });
210
+ });
211
+
212
+ describe('cmdLearn', () => {
213
+ test('prints warning when no memDir', () => {
214
+ const consoleSpy = jest.spyOn(console, 'log');
215
+ cmdLearn([], null);
216
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
217
+ });
218
+
219
+ test('requires insight argument', () => {
220
+ const consoleSpy = jest.spyOn(console, 'log');
221
+ cmdLearn([], memDir);
222
+ expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
223
+ });
224
+
225
+ test('adds learning to memory.md', () => {
226
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
227
+ writeMemFile(memDir, 'memory.md', '# Learnings\n\n');
228
+
229
+ cmdLearn(['Always', 'test', 'first'], memDir);
230
+
231
+ const content = readMemFile(memDir, 'memory.md');
232
+ expect(content).toContain('Always test first');
233
+ });
234
+
235
+ test('adds global learning to playbook.md with -g flag', () => {
236
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
237
+ writeMemFile(memDir, 'playbook.md', '# Playbook\n\n');
238
+
239
+ cmdLearn(['-g', 'Global', 'insight'], memDir);
240
+
241
+ const content = readMemFile(memDir, 'playbook.md');
242
+ expect(content).toContain('Global insight');
243
+ });
244
+ });
245
+
246
+ describe('cmdStuck', () => {
247
+ test('prints warning when no memDir', () => {
248
+ const consoleSpy = jest.spyOn(console, 'log');
249
+ cmdStuck([], null);
250
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
251
+ });
252
+
253
+ test('shows no blockers when status is clear', () => {
254
+ writeMemFile(memDir, 'state.md', '---\nstatus: active\n---\n\n');
255
+ const consoleSpy = jest.spyOn(console, 'log');
256
+ cmdStuck([], memDir);
257
+ expect(consoleSpy.mock.calls[0][0]).toContain('No blockers');
258
+ });
259
+
260
+ test('sets blocker status', () => {
261
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
262
+ writeMemFile(memDir, 'state.md', '---\nstatus: active\n---\n\n');
263
+
264
+ cmdStuck(['Waiting', 'for', 'API'], memDir);
265
+
266
+ const content = readMemFile(memDir, 'state.md');
267
+ const { frontmatter } = parseFrontmatter(content);
268
+ expect(frontmatter.blocker).toBe('Waiting for API');
269
+ expect(frontmatter.status).toBe('blocked');
270
+ });
271
+
272
+ test('clears blocker with "clear" argument', () => {
273
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
274
+ writeMemFile(memDir, 'state.md', '---\nstatus: blocked\nblocker: Some issue\n---\n\n');
275
+
276
+ cmdStuck(['clear'], memDir);
277
+
278
+ const content = readMemFile(memDir, 'state.md');
279
+ const { frontmatter } = parseFrontmatter(content);
280
+ expect(frontmatter.blocker).toBeUndefined();
281
+ expect(frontmatter.status).toBe('active');
282
+ });
283
+ });
284
+
285
+ describe('cmdPlaybook', () => {
286
+ test('prints warning when no memDir', () => {
287
+ const consoleSpy = jest.spyOn(console, 'log');
288
+ cmdPlaybook(null);
289
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
290
+ });
291
+
292
+ test('displays playbook content', () => {
293
+ writeMemFile(memDir, 'playbook.md', '# Playbook\n\n- Learning 1\n- Learning 2');
294
+ const consoleSpy = jest.spyOn(console, 'log');
295
+ cmdPlaybook(memDir);
296
+ expect(consoleSpy.mock.calls[0][0]).toContain('Playbook');
297
+ });
298
+
299
+ test('shows message when no playbook', () => {
300
+ const consoleSpy = jest.spyOn(console, 'log');
301
+ cmdPlaybook(memDir);
302
+ expect(consoleSpy.mock.calls[0][0]).toContain('No playbook');
303
+ });
304
+ });
305
+
306
+ describe('cmdLearnings', () => {
307
+ test('prints warning when no memDir', () => {
308
+ const consoleSpy = jest.spyOn(console, 'log');
309
+ cmdLearnings([], null);
310
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
311
+ });
312
+
313
+ test('shows no learnings message when empty', () => {
314
+ writeMemFile(memDir, 'memory.md', '# Learnings\n\n');
315
+ const consoleSpy = jest.spyOn(console, 'log');
316
+ cmdLearnings([], memDir);
317
+ expect(consoleSpy.mock.calls[0][0]).toContain('No learnings');
318
+ });
319
+
320
+ test('lists task learnings with numbers', () => {
321
+ writeMemFile(memDir, 'memory.md', '# Learnings\n\n- 2024-01-01: First\n- 2024-01-02: Second');
322
+ const consoleSpy = jest.spyOn(console, 'log');
323
+ cmdLearnings([], memDir);
324
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
325
+ expect(output).toContain('Task Learnings');
326
+ });
327
+
328
+ test('lists global learnings with -g flag', () => {
329
+ writeMemFile(memDir, 'playbook.md', '# Playbook\n\n- Global learning');
330
+ const consoleSpy = jest.spyOn(console, 'log');
331
+ cmdLearnings(['-g'], memDir);
332
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
333
+ expect(output).toContain('Playbook');
334
+ });
335
+ });
336
+
337
+ describe('cmdPromote', () => {
338
+ test('prints warning when no memDir', () => {
339
+ const consoleSpy = jest.spyOn(console, 'log');
340
+ cmdPromote([], null);
341
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
342
+ });
343
+
344
+ test('requires number argument', () => {
345
+ const consoleSpy = jest.spyOn(console, 'log');
346
+ cmdPromote([], memDir);
347
+ expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
348
+ });
349
+
350
+ test('promotes learning to playbook', () => {
351
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
352
+ writeMemFile(memDir, 'memory.md', '# Learnings\n\n- 2024-01-01: Important insight');
353
+ writeMemFile(memDir, 'playbook.md', '# Playbook\n\n');
354
+
355
+ cmdPromote(['1'], memDir);
356
+
357
+ const playbook = readMemFile(memDir, 'playbook.md');
358
+ expect(playbook).toContain('Important insight');
359
+ });
360
+
361
+ test('rejects invalid number', () => {
362
+ writeMemFile(memDir, 'memory.md', '# Learnings\n\n- One learning');
363
+ const consoleSpy = jest.spyOn(console, 'log');
364
+ cmdPromote(['5'], memDir);
365
+ expect(consoleSpy.mock.calls[0][0]).toContain('Invalid number');
366
+ });
367
+ });
368
+
369
+ describe('cmdSwitch', () => {
370
+ test('prints warning when no memDir', () => {
371
+ const consoleSpy = jest.spyOn(console, 'log');
372
+ cmdSwitch([], null);
373
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
374
+ });
375
+
376
+ test('requires name argument', () => {
377
+ const consoleSpy = jest.spyOn(console, 'log');
378
+ cmdSwitch([], memDir);
379
+ expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
380
+ });
381
+
382
+ test('switches to task branch', () => {
383
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
384
+ cmdSwitch(['feature'], memDir);
385
+ expect(spawnSync).toHaveBeenCalledWith(
386
+ 'git',
387
+ ['checkout', 'task/feature'],
388
+ expect.any(Object)
389
+ );
390
+ });
391
+ });
392
+
393
+ describe('cmdHistory', () => {
394
+ test('prints warning when no memDir', () => {
395
+ const consoleSpy = jest.spyOn(console, 'log');
396
+ cmdHistory(null);
397
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
398
+ });
399
+
400
+ test('shows git log', () => {
401
+ spawnSync.mockReturnValue({ status: 0, stdout: 'abc123 init: test\ndef456 checkpoint: done', stderr: '' });
402
+ const consoleSpy = jest.spyOn(console, 'log');
403
+ cmdHistory(memDir);
404
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
405
+ expect(output).toContain('History');
406
+ });
407
+ });
408
+
409
+ describe('cmdSync', () => {
410
+ test('prints warning when no memDir', () => {
411
+ const consoleSpy = jest.spyOn(console, 'log');
412
+ cmdSync(null);
413
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
414
+ });
415
+
416
+ test('warns when no remote configured', () => {
417
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
418
+ const consoleSpy = jest.spyOn(console, 'log');
419
+ cmdSync(memDir);
420
+ expect(consoleSpy.mock.calls[0][0]).toContain('No remote configured');
421
+ });
422
+ });
423
+
424
+ describe('cmdQuery', () => {
425
+ test('prints warning when no memDir', () => {
426
+ const consoleSpy = jest.spyOn(console, 'log');
427
+ cmdQuery([], null);
428
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
429
+ });
430
+
431
+ test('requires search argument', () => {
432
+ const consoleSpy = jest.spyOn(console, 'log');
433
+ cmdQuery([], memDir);
434
+ expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
435
+ });
436
+ });
437
+
438
+ describe('cmdProgress', () => {
439
+ test('prints warning when no memDir', () => {
440
+ const consoleSpy = jest.spyOn(console, 'log');
441
+ cmdProgress([], null);
442
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
443
+ });
444
+
445
+ test('shows progress bar', () => {
446
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
447
+ writeMemFile(memDir, 'goal.md', `# Goal
448
+
449
+ Test
450
+
451
+ ## Definition of Done
452
+
453
+ - [x] First
454
+ - [ ] Second
455
+ - [ ] Third
456
+
457
+ ## Progress: 0%
458
+ `);
459
+ const consoleSpy = jest.spyOn(console, 'log');
460
+ cmdProgress([], memDir);
461
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
462
+ expect(output).toContain('Progress');
463
+ expect(output).toContain('%');
464
+ });
465
+ });
466
+
467
+ describe('cmdCriteria', () => {
468
+ test('prints warning when no memDir', () => {
469
+ const consoleSpy = jest.spyOn(console, 'log');
470
+ cmdCriteria([], null);
471
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
472
+ });
473
+
474
+ test('adds new criterion', () => {
475
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
476
+ writeMemFile(memDir, 'goal.md', `# Goal
477
+
478
+ Test
479
+
480
+ ## Definition of Done
481
+
482
+ - [ ] Existing
483
+ `);
484
+ cmdCriteria(['add', 'New', 'criterion'], memDir);
485
+ const content = readMemFile(memDir, 'goal.md');
486
+ expect(content).toContain('New criterion');
487
+ });
488
+ });
489
+
490
+ describe('cmdConstraint', () => {
491
+ test('prints warning when no memDir', () => {
492
+ const consoleSpy = jest.spyOn(console, 'log');
493
+ cmdConstraint([], null);
494
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
495
+ });
496
+
497
+ test('adds constraint', () => {
498
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
499
+ writeMemFile(memDir, 'goal.md', `# Goal
500
+
501
+ ## Constraints
502
+
503
+ `);
504
+ cmdConstraint(['add', 'Must', 'be', 'fast'], memDir);
505
+ const content = readMemFile(memDir, 'goal.md');
506
+ expect(content).toContain('Must be fast');
507
+ });
508
+ });
509
+
510
+ describe('Primitive commands', () => {
511
+ describe('cmdBranch', () => {
512
+ test('lists branches when no args', () => {
513
+ spawnSync.mockReturnValue({ status: 0, stdout: ' main\n* task/feature', stderr: '' });
514
+ const consoleSpy = jest.spyOn(console, 'log');
515
+ cmdBranch([], memDir);
516
+ expect(spawnSync).toHaveBeenCalled();
517
+ });
518
+
519
+ test('creates new branch', () => {
520
+ spawnSync
521
+ .mockReturnValueOnce({ status: 0, stdout: ' main', stderr: '' })
522
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
523
+
524
+ cmdBranch(['new-feature'], memDir);
525
+ expect(spawnSync).toHaveBeenCalledWith(
526
+ 'git',
527
+ ['checkout', '-b', 'task/new-feature'],
528
+ expect.any(Object)
529
+ );
530
+ });
531
+ });
532
+
533
+ describe('cmdCommit', () => {
534
+ test('commits changes with message', () => {
535
+ spawnSync
536
+ .mockReturnValueOnce({ status: 0, stdout: 'M file.md', stderr: '' })
537
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' })
538
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
539
+
540
+ cmdCommit(['Test', 'commit'], memDir);
541
+ expect(spawnSync).toHaveBeenCalledWith(
542
+ 'git',
543
+ ['commit', '-m', 'Test commit'],
544
+ expect.any(Object)
545
+ );
546
+ });
547
+
548
+ test('shows nothing to commit when no changes', () => {
549
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
550
+ const consoleSpy = jest.spyOn(console, 'log');
551
+ cmdCommit([], memDir);
552
+ expect(consoleSpy.mock.calls[0][0]).toContain('No changes');
553
+ });
554
+ });
555
+
556
+ describe('cmdSet', () => {
557
+ test('prints warning when no memDir', () => {
558
+ const consoleSpy = jest.spyOn(console, 'log');
559
+ cmdSet([], null);
560
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
561
+ });
562
+
563
+ test('requires key and value', () => {
564
+ const consoleSpy = jest.spyOn(console, 'log');
565
+ cmdSet(['key'], memDir);
566
+ expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
567
+ });
568
+
569
+ test('sets value in frontmatter', () => {
570
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
571
+ writeMemFile(memDir, 'state.md', '---\nstatus: active\n---\n\nBody');
572
+
573
+ cmdSet(['mykey', 'myvalue'], memDir);
574
+
575
+ const content = readMemFile(memDir, 'state.md');
576
+ expect(content).toContain('mykey: myvalue');
577
+ });
578
+ });
579
+
580
+ describe('cmdGet', () => {
581
+ test('prints warning when no memDir', () => {
582
+ const consoleSpy = jest.spyOn(console, 'log');
583
+ cmdGet([], null);
584
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
585
+ });
586
+
587
+ test('requires key argument', () => {
588
+ const consoleSpy = jest.spyOn(console, 'log');
589
+ cmdGet([], memDir);
590
+ expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
591
+ });
592
+
593
+ test('gets value from frontmatter', () => {
594
+ writeMemFile(memDir, 'state.md', '---\nstatus: active\nmykey: myvalue\n---\n\n');
595
+ const consoleSpy = jest.spyOn(console, 'log');
596
+ cmdGet(['mykey'], memDir);
597
+ expect(consoleSpy.mock.calls[0][0]).toBe('myvalue');
598
+ });
599
+
600
+ test('shows not set for missing key', () => {
601
+ writeMemFile(memDir, 'state.md', '---\nstatus: active\n---\n\n');
602
+ const consoleSpy = jest.spyOn(console, 'log');
603
+ cmdGet(['missing'], memDir);
604
+ expect(consoleSpy.mock.calls[0][0]).toContain('Not set');
605
+ });
606
+ });
607
+
608
+ describe('cmdAppend', () => {
609
+ test('prints warning when no memDir', () => {
610
+ const consoleSpy = jest.spyOn(console, 'log');
611
+ cmdAppend([], null);
612
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
613
+ });
614
+
615
+ test('requires list and item arguments', () => {
616
+ const consoleSpy = jest.spyOn(console, 'log');
617
+ cmdAppend(['list'], memDir);
618
+ expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
619
+ });
620
+
621
+ test('appends to learnings list', () => {
622
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
623
+ writeMemFile(memDir, 'memory.md', '# Learnings\n\n');
624
+
625
+ cmdAppend(['learnings', 'New', 'item'], memDir);
626
+
627
+ const content = readMemFile(memDir, 'memory.md');
628
+ expect(content).toContain('New item');
629
+ });
630
+
631
+ test('appends to playbook list', () => {
632
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
633
+ writeMemFile(memDir, 'playbook.md', '# Playbook\n\n');
634
+
635
+ cmdAppend(['playbook', 'Global', 'insight'], memDir);
636
+
637
+ const content = readMemFile(memDir, 'playbook.md');
638
+ expect(content).toContain('Global insight');
639
+ });
640
+
641
+ test('rejects unknown list name', () => {
642
+ const consoleSpy = jest.spyOn(console, 'log');
643
+ cmdAppend(['unknown', 'item'], memDir);
644
+ expect(consoleSpy.mock.calls[0][0]).toContain('Unknown list');
645
+ });
646
+ });
647
+
648
+ describe('cmdLog', () => {
649
+ test('prints warning when no memDir', () => {
650
+ const consoleSpy = jest.spyOn(console, 'log');
651
+ cmdLog(null);
652
+ expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
653
+ });
654
+
655
+ test('shows git log', () => {
656
+ spawnSync.mockReturnValue({ status: 0, stdout: 'abc123 commit msg', stderr: '' });
657
+ const consoleSpy = jest.spyOn(console, 'log');
658
+ cmdLog(memDir);
659
+ expect(consoleSpy.mock.calls[0][0]).toContain('abc123');
660
+ });
661
+ });
662
+ });
663
+