@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,367 @@
1
+ /**
2
+ * Tests for utility 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('test answer')),
20
+ close: jest.fn(),
21
+ on: jest.fn()
22
+ }))
23
+ }));
24
+
25
+ const {
26
+ parseFrontmatter,
27
+ serializeFrontmatter,
28
+ loadConfig,
29
+ saveConfig,
30
+ loadIndex,
31
+ saveIndex,
32
+ readMemFile,
33
+ writeMemFile,
34
+ git,
35
+ getCurrentBranch,
36
+ findMemDir,
37
+ ensureTaskBranch,
38
+ CONFIG_DIR,
39
+ CONFIG_FILE,
40
+ CENTRAL_MEM,
41
+ INDEX_FILE,
42
+ c
43
+ } = require('../index.js');
44
+
45
+ const { spawnSync } = require('child_process');
46
+
47
+ describe('parseFrontmatter', () => {
48
+ test('returns empty object for null content', () => {
49
+ const result = parseFrontmatter(null);
50
+ expect(result).toEqual({ frontmatter: {}, body: '' });
51
+ });
52
+
53
+ test('returns empty object for undefined content', () => {
54
+ const result = parseFrontmatter(undefined);
55
+ expect(result).toEqual({ frontmatter: {}, body: '' });
56
+ });
57
+
58
+ test('returns empty object for empty string', () => {
59
+ const result = parseFrontmatter('');
60
+ expect(result).toEqual({ frontmatter: {}, body: '' });
61
+ });
62
+
63
+ test('parses frontmatter with single key', () => {
64
+ const content = '---\nstatus: active\n---\n\nBody content';
65
+ const result = parseFrontmatter(content);
66
+ expect(result.frontmatter).toEqual({ status: 'active' });
67
+ expect(result.body).toBe('Body content');
68
+ });
69
+
70
+ test('parses frontmatter with multiple keys', () => {
71
+ const content = '---\nstatus: active\nnext: do something\ntask: test-task\n---\n\n# Body\n\nSome content';
72
+ const result = parseFrontmatter(content);
73
+ expect(result.frontmatter).toEqual({
74
+ status: 'active',
75
+ next: 'do something',
76
+ task: 'test-task'
77
+ });
78
+ expect(result.body).toBe('# Body\n\nSome content');
79
+ });
80
+
81
+ test('handles values with colons', () => {
82
+ const content = '---\nwake: 8:30am daily\n---\n\nBody';
83
+ const result = parseFrontmatter(content);
84
+ expect(result.frontmatter).toEqual({ wake: '8:30am daily' });
85
+ });
86
+
87
+ test('returns full content as body when no frontmatter', () => {
88
+ const content = 'Just some body content without frontmatter';
89
+ const result = parseFrontmatter(content);
90
+ expect(result.frontmatter).toEqual({});
91
+ expect(result.body).toBe('Just some body content without frontmatter');
92
+ });
93
+
94
+ test('handles markdown with only frontmatter', () => {
95
+ const content = '---\nkey: value\n---\n';
96
+ const result = parseFrontmatter(content);
97
+ expect(result.frontmatter).toEqual({ key: 'value' });
98
+ expect(result.body).toBe('');
99
+ });
100
+
101
+ test('ignores lines without colons in frontmatter', () => {
102
+ const content = '---\nvalid: yes\ninvalid line\nanother: good\n---\n\nBody';
103
+ const result = parseFrontmatter(content);
104
+ expect(result.frontmatter).toEqual({ valid: 'yes', another: 'good' });
105
+ });
106
+ });
107
+
108
+ describe('serializeFrontmatter', () => {
109
+ test('serializes frontmatter with body', () => {
110
+ const frontmatter = { status: 'active', task: 'test' };
111
+ const body = '# Goal\n\nDo something';
112
+ const result = serializeFrontmatter(frontmatter, body);
113
+ expect(result).toBe('---\nstatus: active\ntask: test\n---\n\n# Goal\n\nDo something');
114
+ });
115
+
116
+ test('handles empty frontmatter', () => {
117
+ const result = serializeFrontmatter({}, 'Body content');
118
+ expect(result).toBe('---\n\n---\n\nBody content');
119
+ });
120
+
121
+ test('handles empty body', () => {
122
+ const result = serializeFrontmatter({ key: 'value' }, '');
123
+ expect(result).toBe('---\nkey: value\n---\n\n');
124
+ });
125
+
126
+ test('roundtrip with parseFrontmatter', () => {
127
+ const original = { status: 'active', task: 'test-task' };
128
+ const body = '# Test\n\nSome content';
129
+ const serialized = serializeFrontmatter(original, body);
130
+ const parsed = parseFrontmatter(serialized);
131
+ expect(parsed.frontmatter).toEqual(original);
132
+ expect(parsed.body).toBe(body);
133
+ });
134
+ });
135
+
136
+ describe('loadConfig / saveConfig', () => {
137
+ const testDir = path.join(os.tmpdir(), 'memx-test-config-' + Date.now());
138
+ const testConfigFile = path.join(testDir, 'config.json');
139
+
140
+ beforeEach(() => {
141
+ if (fs.existsSync(testDir)) {
142
+ fs.rmSync(testDir, { recursive: true });
143
+ }
144
+ });
145
+
146
+ afterEach(() => {
147
+ if (fs.existsSync(testDir)) {
148
+ fs.rmSync(testDir, { recursive: true });
149
+ }
150
+ });
151
+
152
+ test('loadConfig returns default when no file exists', () => {
153
+ // This would need CONFIG_FILE to point to a non-existent file
154
+ // Since we can't easily mock constants, we test the function behavior
155
+ const result = loadConfig();
156
+ expect(result).toHaveProperty('repos');
157
+ });
158
+
159
+ test('saveConfig creates directory if needed', () => {
160
+ // We can't easily test this without mocking the constants
161
+ // But we verify the function exists and is callable
162
+ expect(typeof saveConfig).toBe('function');
163
+ });
164
+ });
165
+
166
+ describe('loadIndex / saveIndex', () => {
167
+ test('loadIndex returns empty object when no file exists', () => {
168
+ // loadIndex should return {} when file doesn't exist
169
+ const result = loadIndex();
170
+ expect(typeof result).toBe('object');
171
+ });
172
+
173
+ test('saveIndex is a function', () => {
174
+ expect(typeof saveIndex).toBe('function');
175
+ });
176
+ });
177
+
178
+ describe('readMemFile / writeMemFile', () => {
179
+ const testDir = path.join(os.tmpdir(), 'memx-test-files-' + Date.now());
180
+
181
+ beforeEach(() => {
182
+ fs.mkdirSync(testDir, { recursive: true });
183
+ });
184
+
185
+ afterEach(() => {
186
+ if (fs.existsSync(testDir)) {
187
+ fs.rmSync(testDir, { recursive: true });
188
+ }
189
+ });
190
+
191
+ test('readMemFile returns null for non-existent file', () => {
192
+ const result = readMemFile(testDir, 'nonexistent.md');
193
+ expect(result).toBeNull();
194
+ });
195
+
196
+ test('writeMemFile writes file content', () => {
197
+ const content = '# Test\n\nContent';
198
+ writeMemFile(testDir, 'test.md', content);
199
+ const result = readMemFile(testDir, 'test.md');
200
+ expect(result).toBe(content);
201
+ });
202
+
203
+ test('readMemFile/writeMemFile roundtrip', () => {
204
+ const content = '---\nstatus: active\n---\n\n# Goal\n\nTest goal';
205
+ writeMemFile(testDir, 'goal.md', content);
206
+ const result = readMemFile(testDir, 'goal.md');
207
+ expect(result).toBe(content);
208
+ });
209
+ });
210
+
211
+ describe('git function', () => {
212
+ beforeEach(() => {
213
+ spawnSync.mockClear();
214
+ });
215
+
216
+ test('executes git command and returns stdout', () => {
217
+ spawnSync.mockReturnValue({
218
+ status: 0,
219
+ stdout: 'main\n',
220
+ stderr: ''
221
+ });
222
+
223
+ const result = git('/some/path', 'branch', '--show-current');
224
+ expect(result).toBe('main');
225
+ expect(spawnSync).toHaveBeenCalledWith(
226
+ 'git',
227
+ ['branch', '--show-current'],
228
+ expect.objectContaining({ cwd: '/some/path' })
229
+ );
230
+ });
231
+
232
+ test('throws error on non-zero exit', () => {
233
+ spawnSync.mockReturnValue({
234
+ status: 1,
235
+ stdout: '',
236
+ stderr: 'fatal: not a git repository'
237
+ });
238
+
239
+ expect(() => git('/some/path', 'status')).toThrow('fatal: not a git repository');
240
+ });
241
+
242
+ test('handles empty stdout', () => {
243
+ spawnSync.mockReturnValue({
244
+ status: 0,
245
+ stdout: '',
246
+ stderr: ''
247
+ });
248
+
249
+ const result = git('/some/path', 'status', '--porcelain');
250
+ expect(result).toBe('');
251
+ });
252
+ });
253
+
254
+ describe('getCurrentBranch', () => {
255
+ beforeEach(() => {
256
+ spawnSync.mockClear();
257
+ });
258
+
259
+ test('returns current branch name', () => {
260
+ spawnSync.mockReturnValue({
261
+ status: 0,
262
+ stdout: 'task/my-feature\n',
263
+ stderr: ''
264
+ });
265
+
266
+ const result = getCurrentBranch('/some/path');
267
+ expect(result).toBe('task/my-feature');
268
+ });
269
+ });
270
+
271
+ describe('findMemDir', () => {
272
+ const testDir = path.join(os.tmpdir(), 'memx-test-find-' + Date.now());
273
+ const subDir = path.join(testDir, 'subdir', 'nested');
274
+ const memDir = path.join(testDir, '.mem');
275
+
276
+ beforeEach(() => {
277
+ fs.mkdirSync(subDir, { recursive: true });
278
+ spawnSync.mockClear();
279
+ });
280
+
281
+ afterEach(() => {
282
+ if (fs.existsSync(testDir)) {
283
+ fs.rmSync(testDir, { recursive: true });
284
+ }
285
+ });
286
+
287
+ test('returns null when no .mem found', () => {
288
+ const result = findMemDir(subDir);
289
+ // Might return central mem if it exists, or null
290
+ expect(result === null || result.memDir).toBeTruthy();
291
+ });
292
+
293
+ test('finds local .mem directory', () => {
294
+ fs.mkdirSync(memDir, { recursive: true });
295
+ fs.mkdirSync(path.join(memDir, '.git'), { recursive: true });
296
+
297
+ const result = findMemDir(testDir);
298
+ expect(result).not.toBeNull();
299
+ expect(result.memDir).toBe(memDir);
300
+ expect(result.isLocal).toBe(true);
301
+ });
302
+
303
+ test('finds local .mem from subdirectory', () => {
304
+ fs.mkdirSync(memDir, { recursive: true });
305
+ fs.mkdirSync(path.join(memDir, '.git'), { recursive: true });
306
+
307
+ const result = findMemDir(subDir);
308
+ expect(result).not.toBeNull();
309
+ expect(result.memDir).toBe(memDir);
310
+ expect(result.isLocal).toBe(true);
311
+ });
312
+ });
313
+
314
+ describe('ensureTaskBranch', () => {
315
+ beforeEach(() => {
316
+ spawnSync.mockClear();
317
+ });
318
+
319
+ test('does nothing when taskBranch is null', () => {
320
+ ensureTaskBranch('/some/path', null);
321
+ // git should not be called for getting current branch
322
+ expect(spawnSync).not.toHaveBeenCalled();
323
+ });
324
+
325
+ test('switches branch when different from current', () => {
326
+ spawnSync
327
+ .mockReturnValueOnce({ status: 0, stdout: 'main\n', stderr: '' })
328
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
329
+
330
+ ensureTaskBranch('/some/path', 'task/feature');
331
+
332
+ expect(spawnSync).toHaveBeenCalledTimes(2);
333
+ expect(spawnSync).toHaveBeenLastCalledWith(
334
+ 'git',
335
+ ['checkout', 'task/feature'],
336
+ expect.any(Object)
337
+ );
338
+ });
339
+
340
+ test('does not switch when already on correct branch', () => {
341
+ spawnSync.mockReturnValue({ status: 0, stdout: 'task/feature\n', stderr: '' });
342
+
343
+ ensureTaskBranch('/some/path', 'task/feature');
344
+
345
+ expect(spawnSync).toHaveBeenCalledTimes(1);
346
+ });
347
+ });
348
+
349
+ describe('constants', () => {
350
+ test('CONFIG_DIR is defined', () => {
351
+ expect(CONFIG_DIR).toBeDefined();
352
+ expect(typeof CONFIG_DIR).toBe('string');
353
+ });
354
+
355
+ test('CENTRAL_MEM is defined', () => {
356
+ expect(CENTRAL_MEM).toBeDefined();
357
+ expect(CENTRAL_MEM).toContain('.mem');
358
+ });
359
+
360
+ test('colors object is defined', () => {
361
+ expect(c).toBeDefined();
362
+ expect(c.reset).toBeDefined();
363
+ expect(c.bold).toBeDefined();
364
+ expect(c.green).toBeDefined();
365
+ expect(c.red).toBeDefined();
366
+ });
367
+ });