@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
package/package.json CHANGED
@@ -1,13 +1,32 @@
1
1
  {
2
2
  "name": "@mndrk/memx",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Persistent memory for AI agents - git-backed, branch-per-task, CLI interface",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "mem": "./index.js"
8
8
  },
9
9
  "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1"
10
+ "test": "jest --coverage",
11
+ "test:watch": "jest --watch"
12
+ },
13
+ "jest": {
14
+ "testEnvironment": "node",
15
+ "coverageThreshold": {
16
+ "global": {
17
+ "branches": 78,
18
+ "functions": 80,
19
+ "lines": 83,
20
+ "statements": 83
21
+ }
22
+ },
23
+ "testPathIgnorePatterns": [
24
+ "/node_modules/"
25
+ ],
26
+ "coveragePathIgnorePatterns": [
27
+ "/node_modules/",
28
+ "/test/"
29
+ ]
11
30
  },
12
31
  "keywords": [
13
32
  "ai",
@@ -23,5 +42,8 @@
23
42
  "repository": {
24
43
  "type": "git",
25
44
  "url": "https://github.com/ramarlina/memx"
45
+ },
46
+ "devDependencies": {
47
+ "jest": "^30.2.0"
26
48
  }
27
49
  }
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Additional tests to increase coverage
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
+ cmdSwitch,
27
+ cmdSync,
28
+ cmdHistory,
29
+ cmdStuck,
30
+ cmdQuery,
31
+ cmdConstraint,
32
+ cmdProgress,
33
+ cmdCriteria,
34
+ cmdBranch,
35
+ cmdCommit,
36
+ writeMemFile,
37
+ readMemFile,
38
+ parseFrontmatter,
39
+ git,
40
+ getCurrentBranch,
41
+ } = require('../index.js');
42
+
43
+ const { spawnSync, execSync } = require('child_process');
44
+
45
+ const testDir = path.join(os.tmpdir(), 'memx-test-additional-' + Date.now());
46
+ let memDir;
47
+
48
+ beforeEach(() => {
49
+ memDir = path.join(testDir, '.mem');
50
+ fs.mkdirSync(memDir, { recursive: true });
51
+ fs.mkdirSync(path.join(memDir, '.git'), { recursive: true });
52
+ spawnSync.mockClear();
53
+ execSync.mockClear();
54
+ jest.spyOn(console, 'log').mockImplementation();
55
+ });
56
+
57
+ afterEach(() => {
58
+ if (fs.existsSync(testDir)) {
59
+ fs.rmSync(testDir, { recursive: true });
60
+ }
61
+ jest.restoreAllMocks();
62
+ });
63
+
64
+ describe('cmdSwitch additional tests', () => {
65
+ test('tries without task/ prefix if first attempt fails', () => {
66
+ spawnSync
67
+ .mockReturnValueOnce({ status: 1, stdout: '', stderr: 'error' })
68
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
69
+
70
+ cmdSwitch(['main'], memDir);
71
+
72
+ expect(spawnSync).toHaveBeenCalledTimes(2);
73
+ });
74
+
75
+ test('handles both attempts failing', () => {
76
+ spawnSync.mockReturnValue({ status: 1, stdout: '', stderr: 'error' });
77
+
78
+ const consoleSpy = jest.spyOn(console, 'log');
79
+ cmdSwitch(['nonexistent'], memDir);
80
+
81
+ expect(consoleSpy.mock.calls.length).toBeGreaterThan(0);
82
+ });
83
+
84
+ test('handles task/ prefix in input', () => {
85
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
86
+ cmdSwitch(['task/feature'], memDir);
87
+ expect(spawnSync).toHaveBeenCalledWith(
88
+ 'git',
89
+ ['checkout', 'task/feature'],
90
+ expect.any(Object)
91
+ );
92
+ });
93
+ });
94
+
95
+ describe('cmdSync additional tests', () => {
96
+ test('handles sync with remote', () => {
97
+ spawnSync
98
+ .mockReturnValueOnce({ status: 0, stdout: 'origin', stderr: '' }) // git remote
99
+ .mockReturnValueOnce({ status: 0, stdout: 'task/test', stderr: '' }) // getCurrentBranch
100
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' }) // pull
101
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' }); // push
102
+
103
+ const consoleSpy = jest.spyOn(console, 'log');
104
+ cmdSync(memDir);
105
+
106
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
107
+ expect(output).toContain('Syncing');
108
+ });
109
+
110
+ test('handles pull failure gracefully', () => {
111
+ spawnSync
112
+ .mockReturnValueOnce({ status: 0, stdout: 'origin', stderr: '' })
113
+ .mockReturnValueOnce({ status: 0, stdout: 'task/test', stderr: '' })
114
+ .mockReturnValueOnce({ status: 1, stdout: '', stderr: 'pull error' }) // pull fails
115
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' }); // push
116
+
117
+ const consoleSpy = jest.spyOn(console, 'log');
118
+ cmdSync(memDir);
119
+
120
+ expect(consoleSpy).toHaveBeenCalled();
121
+ });
122
+
123
+ test('handles push failure', () => {
124
+ spawnSync
125
+ .mockReturnValueOnce({ status: 0, stdout: 'origin', stderr: '' })
126
+ .mockReturnValueOnce({ status: 0, stdout: 'task/test', stderr: '' })
127
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' })
128
+ .mockReturnValueOnce({ status: 1, stdout: '', stderr: 'push error' });
129
+
130
+ const consoleSpy = jest.spyOn(console, 'log');
131
+ cmdSync(memDir);
132
+
133
+ expect(consoleSpy).toHaveBeenCalled();
134
+ });
135
+ });
136
+
137
+ describe('cmdStuck additional tests', () => {
138
+ test('shows existing blocker', () => {
139
+ writeMemFile(memDir, 'state.md', '---\nstatus: blocked\nblocker: Waiting for review\n---\n\n');
140
+ const consoleSpy = jest.spyOn(console, 'log');
141
+ cmdStuck([], memDir);
142
+ expect(consoleSpy.mock.calls[0][0]).toContain('Waiting for review');
143
+ });
144
+ });
145
+
146
+ describe('cmdQuery additional tests', () => {
147
+ test('shows results from grep', () => {
148
+ execSync.mockReturnValue('goal.md:3:Test result');
149
+ const consoleSpy = jest.spyOn(console, 'log');
150
+ cmdQuery(['test'], memDir);
151
+ expect(consoleSpy.mock.calls[0][0]).toContain('Test result');
152
+ });
153
+
154
+ test('handles no results', () => {
155
+ execSync.mockImplementation(() => { throw new Error('no matches'); });
156
+ const consoleSpy = jest.spyOn(console, 'log');
157
+ cmdQuery(['nonexistent'], memDir);
158
+ expect(consoleSpy.mock.calls[0][0]).toContain('No matches');
159
+ });
160
+ });
161
+
162
+ describe('cmdConstraint additional tests', () => {
163
+ test('lists existing constraints', () => {
164
+ writeMemFile(memDir, 'goal.md', `# Goal
165
+
166
+ ## Constraints
167
+
168
+ - No breaking changes
169
+ - Test coverage > 80%
170
+ `);
171
+ const consoleSpy = jest.spyOn(console, 'log');
172
+ cmdConstraint([], memDir);
173
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
174
+ expect(output).toContain('Constraints');
175
+ });
176
+
177
+ test('shows no constraints message', () => {
178
+ writeMemFile(memDir, 'goal.md', '# Goal\n\n## Constraints\n\n');
179
+ const consoleSpy = jest.spyOn(console, 'log');
180
+ cmdConstraint([], memDir);
181
+ expect(consoleSpy.mock.calls[0][0]).toContain('No constraints');
182
+ });
183
+
184
+ test('removes constraint by number', () => {
185
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
186
+ writeMemFile(memDir, 'goal.md', `# Goal
187
+
188
+ ## Constraints
189
+
190
+ - First constraint
191
+ - Second constraint
192
+ `);
193
+ const consoleSpy = jest.spyOn(console, 'log');
194
+ cmdConstraint(['remove', '1'], memDir);
195
+
196
+ const content = readMemFile(memDir, 'goal.md');
197
+ expect(content).not.toContain('First constraint');
198
+ });
199
+
200
+ test('shows usage for unknown subcommand', () => {
201
+ const consoleSpy = jest.spyOn(console, 'log');
202
+ cmdConstraint(['unknown'], memDir);
203
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
204
+ expect(output).toContain('mem constraint');
205
+ });
206
+ });
207
+
208
+ describe('cmdProgress additional tests', () => {
209
+ test('shows message when no criteria defined', () => {
210
+ writeMemFile(memDir, 'goal.md', `# Goal
211
+
212
+ Test
213
+
214
+ ## Definition of Done
215
+
216
+ `);
217
+ const consoleSpy = jest.spyOn(console, 'log');
218
+ cmdProgress([], memDir);
219
+ expect(consoleSpy.mock.calls[0][0]).toContain('No criteria');
220
+ });
221
+
222
+ test('updates progress percentage in file', () => {
223
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
224
+ writeMemFile(memDir, 'goal.md', `# Goal
225
+
226
+ Test
227
+
228
+ ## Definition of Done
229
+
230
+ - [x] First
231
+ - [x] Second
232
+ - [x] Third
233
+
234
+ ## Progress: 0%
235
+ `);
236
+ const consoleSpy = jest.spyOn(console, 'log');
237
+ cmdProgress([], memDir);
238
+
239
+ const content = readMemFile(memDir, 'goal.md');
240
+ expect(content).toContain('100%');
241
+ });
242
+
243
+ test('handles goal.md not found', () => {
244
+ const consoleSpy = jest.spyOn(console, 'log');
245
+ cmdProgress([], memDir);
246
+ expect(consoleSpy.mock.calls[0][0]).toContain('No goal.md');
247
+ });
248
+ });
249
+
250
+ describe('cmdCriteria additional tests', () => {
251
+ test('shows usage when no args', () => {
252
+ writeMemFile(memDir, 'goal.md', '# Goal\n\n## Definition of Done\n\n- [ ] Test');
253
+ const consoleSpy = jest.spyOn(console, 'log');
254
+ cmdCriteria([], memDir);
255
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
256
+ expect(output).toContain('mem criteria');
257
+ });
258
+
259
+ test('marks criterion complete with check subcommand', () => {
260
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
261
+ writeMemFile(memDir, 'goal.md', `# Goal
262
+
263
+ ## Definition of Done
264
+
265
+ - [ ] First
266
+ - [ ] Second
267
+
268
+ ## Progress: 0%
269
+ `);
270
+ cmdCriteria(['check', '1'], memDir);
271
+
272
+ const content = readMemFile(memDir, 'goal.md');
273
+ expect(content).toContain('- [x] First');
274
+ });
275
+
276
+ test('handles goal.md not found', () => {
277
+ const consoleSpy = jest.spyOn(console, 'log');
278
+ cmdCriteria(['add', 'Test'], memDir);
279
+ expect(consoleSpy.mock.calls[0][0]).toContain('No goal.md');
280
+ });
281
+ });
282
+
283
+ describe('cmdBranch additional tests', () => {
284
+ test('switches to existing branch', () => {
285
+ spawnSync
286
+ .mockReturnValueOnce({ status: 0, stdout: ' main\n task/existing', stderr: '' })
287
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
288
+
289
+ const consoleSpy = jest.spyOn(console, 'log');
290
+ cmdBranch(['existing'], memDir);
291
+
292
+ expect(spawnSync).toHaveBeenCalledWith(
293
+ 'git',
294
+ ['checkout', 'task/existing'],
295
+ expect.any(Object)
296
+ );
297
+ });
298
+
299
+ test('handles branch operation error', () => {
300
+ spawnSync
301
+ .mockReturnValueOnce({ status: 0, stdout: ' main', stderr: '' })
302
+ .mockReturnValueOnce({ status: 1, stdout: '', stderr: 'error' });
303
+
304
+ const consoleSpy = jest.spyOn(console, 'log');
305
+ cmdBranch(['new-branch'], memDir);
306
+
307
+ expect(consoleSpy).toHaveBeenCalled();
308
+ });
309
+
310
+ test('handles task/ prefix in branch name', () => {
311
+ spawnSync
312
+ .mockReturnValueOnce({ status: 0, stdout: ' main', stderr: '' })
313
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
314
+
315
+ cmdBranch(['task/my-feature'], memDir);
316
+
317
+ expect(spawnSync).toHaveBeenCalledWith(
318
+ 'git',
319
+ ['checkout', '-b', 'task/my-feature'],
320
+ expect.any(Object)
321
+ );
322
+ });
323
+ });
324
+
325
+ describe('cmdCommit additional tests', () => {
326
+ test('uses default message when none provided', () => {
327
+ spawnSync
328
+ .mockReturnValueOnce({ status: 0, stdout: 'M file.md', stderr: '' })
329
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' })
330
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
331
+
332
+ cmdCommit([], memDir);
333
+
334
+ expect(spawnSync).toHaveBeenCalledWith(
335
+ 'git',
336
+ ['commit', '-m', 'checkpoint'],
337
+ expect.any(Object)
338
+ );
339
+ });
340
+
341
+ test('handles commit error', () => {
342
+ spawnSync
343
+ .mockReturnValueOnce({ status: 0, stdout: 'M file.md', stderr: '' })
344
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' })
345
+ .mockReturnValueOnce({ status: 1, stdout: '', stderr: 'commit failed' });
346
+
347
+ const consoleSpy = jest.spyOn(console, 'log');
348
+ cmdCommit(['test'], memDir);
349
+
350
+ expect(consoleSpy).toHaveBeenCalled();
351
+ });
352
+ });
353
+
354
+ describe('git helper additional tests', () => {
355
+ test('handles git error with empty stderr', () => {
356
+ spawnSync.mockReturnValue({ status: 1, stdout: '', stderr: '' });
357
+ expect(() => git(memDir, 'status')).toThrow('Git command failed');
358
+ });
359
+
360
+ test('handles null stdout', () => {
361
+ spawnSync.mockReturnValue({ status: 0, stdout: null, stderr: '' });
362
+ const result = git(memDir, 'status');
363
+ expect(result).toBe('');
364
+ });
365
+ });
366
+
367
+ describe('getCurrentBranch', () => {
368
+ test('returns branch name', () => {
369
+ spawnSync.mockReturnValue({ status: 0, stdout: 'main\n', stderr: '' });
370
+ const result = getCurrentBranch(memDir);
371
+ expect(result).toBe('main');
372
+ });
373
+ });
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Tests for branch coverage
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
+ cmdInit,
27
+ cmdConstraint,
28
+ cmdProgress,
29
+ cmdCriteria,
30
+ cmdBranch,
31
+ cmdCommit,
32
+ cmdSwitch,
33
+ cmdSync,
34
+ writeMemFile,
35
+ readMemFile,
36
+ CENTRAL_MEM,
37
+ loadIndex,
38
+ saveIndex,
39
+ } = require('../index.js');
40
+
41
+ const { spawnSync, execSync } = require('child_process');
42
+
43
+ const testDir = path.join(os.tmpdir(), 'memx-test-branches-' + Date.now());
44
+ let memDir;
45
+
46
+ beforeEach(() => {
47
+ memDir = path.join(testDir, '.mem');
48
+ fs.mkdirSync(memDir, { recursive: true });
49
+ fs.mkdirSync(path.join(memDir, '.git'), { recursive: true });
50
+ spawnSync.mockClear();
51
+ execSync.mockClear();
52
+ jest.spyOn(console, 'log').mockImplementation();
53
+ });
54
+
55
+ afterEach(() => {
56
+ if (fs.existsSync(testDir)) {
57
+ fs.rmSync(testDir, { recursive: true });
58
+ }
59
+ jest.restoreAllMocks();
60
+ });
61
+
62
+ describe('cmdInit edge cases', () => {
63
+ test('handles branch creation error', async () => {
64
+ spawnSync.mockReturnValueOnce({ status: 1, stdout: '', stderr: 'branch exists' });
65
+
66
+ const consoleSpy = jest.spyOn(console, 'log');
67
+ await cmdInit(['existing-task'], memDir);
68
+
69
+ expect(consoleSpy.mock.calls[0][0]).toContain('Error');
70
+ });
71
+
72
+ test('creates task with goal in existing repo', async () => {
73
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
74
+
75
+ const consoleSpy = jest.spyOn(console, 'log');
76
+ await cmdInit(['my-task', 'Build', 'something'], memDir);
77
+
78
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
79
+ expect(output).toContain('Created task branch');
80
+ expect(output).toContain('Build something');
81
+ });
82
+
83
+ test('shows usage when no name provided with existing memDir', async () => {
84
+ const consoleSpy = jest.spyOn(console, 'log');
85
+ await cmdInit([], memDir);
86
+
87
+ expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
88
+ });
89
+ });
90
+
91
+ describe('cmdConstraint edge cases', () => {
92
+ test('shows constraint not found for invalid number', () => {
93
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
94
+ writeMemFile(memDir, 'goal.md', `# Goal\n\n## Constraints\n\n- First\n- Second`);
95
+
96
+ const consoleSpy = jest.spyOn(console, 'log');
97
+ cmdConstraint(['remove', '99'], memDir);
98
+
99
+ expect(consoleSpy.mock.calls[0][0]).toContain('not found');
100
+ });
101
+
102
+ test('adds constraint to new section', () => {
103
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
104
+ writeMemFile(memDir, 'goal.md', `# Goal\n\nTest goal`);
105
+
106
+ cmdConstraint(['add', 'New constraint'], memDir);
107
+
108
+ const content = readMemFile(memDir, 'goal.md');
109
+ expect(content).toContain('## Constraints');
110
+ expect(content).toContain('New constraint');
111
+ });
112
+ });
113
+
114
+ describe('cmdProgress edge cases', () => {
115
+ test('shows warning when no Definition of Done section', () => {
116
+ writeMemFile(memDir, 'goal.md', `# Goal\n\nJust a goal without criteria`);
117
+
118
+ const consoleSpy = jest.spyOn(console, 'log');
119
+ cmdProgress([], memDir);
120
+
121
+ expect(consoleSpy.mock.calls[0][0]).toContain('No "Definition of Done"');
122
+ });
123
+
124
+ test('shows progress with mixed criteria', () => {
125
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
126
+ writeMemFile(memDir, 'goal.md', `# Goal
127
+
128
+ Test goal
129
+
130
+ ## Definition of Done
131
+
132
+ - [x] First done
133
+ - [ ] Second not done
134
+ - [x] Third done
135
+
136
+ ## Progress: 0%`);
137
+
138
+ const consoleSpy = jest.spyOn(console, 'log');
139
+ cmdProgress([], memDir);
140
+
141
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
142
+ // Should show 66% (2 of 3)
143
+ expect(output).toMatch(/66%|67%/);
144
+ });
145
+ });
146
+
147
+ describe('cmdCriteria edge cases', () => {
148
+ test('checks criterion by number', () => {
149
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
150
+ writeMemFile(memDir, 'goal.md', `# Goal
151
+
152
+ ## Definition of Done
153
+
154
+ - [ ] First
155
+ - [ ] Second
156
+ - [ ] Third
157
+
158
+ ## Progress: 0%`);
159
+
160
+ cmdCriteria(['check', '2'], memDir);
161
+
162
+ const content = readMemFile(memDir, 'goal.md');
163
+ expect(content).toContain('- [ ] First');
164
+ expect(content).toContain('- [x] Second');
165
+ expect(content).toContain('- [ ] Third');
166
+ });
167
+
168
+ test('adds criterion with add command', () => {
169
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
170
+ writeMemFile(memDir, 'goal.md', `# Goal
171
+
172
+ ## Definition of Done
173
+
174
+ - [ ] First
175
+
176
+ ## Progress: 0%`);
177
+
178
+ cmdCriteria(['add', 'New', 'criterion'], memDir);
179
+
180
+ const content = readMemFile(memDir, 'goal.md');
181
+ expect(content).toContain('New criterion');
182
+ });
183
+ });
184
+
185
+ describe('cmdBranch edge cases', () => {
186
+ test('lists branches when no args', () => {
187
+ spawnSync.mockReturnValue({ status: 0, stdout: ' main\n* task/test', stderr: '' });
188
+
189
+ const consoleSpy = jest.spyOn(console, 'log');
190
+ cmdBranch([], memDir);
191
+
192
+ expect(consoleSpy).toHaveBeenCalled();
193
+ });
194
+
195
+ test('creates branch with task/ prefix already present', () => {
196
+ spawnSync
197
+ .mockReturnValueOnce({ status: 0, stdout: ' main', stderr: '' })
198
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
199
+
200
+ const consoleSpy = jest.spyOn(console, 'log');
201
+ cmdBranch(['task/already-prefixed'], memDir);
202
+
203
+ expect(spawnSync).toHaveBeenCalledWith(
204
+ 'git',
205
+ ['checkout', '-b', 'task/already-prefixed'],
206
+ expect.any(Object)
207
+ );
208
+ });
209
+ });
210
+
211
+ describe('cmdCommit edge cases', () => {
212
+ test('shows warning when nothing to commit', () => {
213
+ spawnSync.mockReturnValueOnce({ status: 0, stdout: '', stderr: '' }); // git status shows nothing
214
+
215
+ const consoleSpy = jest.spyOn(console, 'log');
216
+ cmdCommit(['test message'], memDir);
217
+
218
+ expect(consoleSpy.mock.calls[0][0]).toContain('No changes');
219
+ });
220
+
221
+ test('commits with custom message', () => {
222
+ spawnSync
223
+ .mockReturnValueOnce({ status: 0, stdout: 'M file.md', stderr: '' })
224
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' })
225
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
226
+
227
+ const consoleSpy = jest.spyOn(console, 'log');
228
+ cmdCommit(['Custom', 'commit', 'message'], memDir);
229
+
230
+ expect(spawnSync).toHaveBeenCalledWith(
231
+ 'git',
232
+ ['commit', '-m', 'Custom commit message'],
233
+ expect.any(Object)
234
+ );
235
+ });
236
+ });
237
+
238
+ describe('cmdSync edge cases', () => {
239
+ test('shows warning when no remote', () => {
240
+ spawnSync.mockReturnValueOnce({ status: 0, stdout: '', stderr: '' }); // no remote
241
+
242
+ const consoleSpy = jest.spyOn(console, 'log');
243
+ cmdSync(memDir);
244
+
245
+ expect(consoleSpy.mock.calls[0][0]).toContain('No remote');
246
+ });
247
+ });