@mndrk/memx 0.3.3 → 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.
- package/README.md +98 -77
- package/coverage/clover.xml +1160 -0
- package/coverage/coverage-final.json +3 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/index.js.html +7255 -0
- package/coverage/lcov-report/mcp.js.html +1009 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +2017 -0
- package/index.js +651 -243
- package/package.json +24 -2
- package/test/additional.test.js +373 -0
- package/test/branches.test.js +247 -0
- package/test/commands.test.js +663 -0
- package/test/context.test.js +185 -0
- package/test/coverage.test.js +366 -0
- package/test/dispatch.test.js +220 -0
- package/test/edge-coverage.test.js +250 -0
- package/test/edge.test.js +434 -0
- package/test/final-coverage.test.js +316 -0
- package/test/final-edges.test.js +199 -0
- package/test/init-local.test.js +316 -0
- package/test/init.test.js +122 -0
- package/test/interactive.test.js +229 -0
- package/test/main-dispatch.test.js +164 -0
- package/test/main-full.test.js +590 -0
- package/test/main.test.js +197 -0
- package/test/mcp-server.test.js +320 -0
- package/test/mcp.test.js +288 -0
- package/test/more.test.js +312 -0
- package/test/new.test.js +175 -0
- package/test/skill.test.js +247 -0
- package/test/tasks-interactive.test.js +243 -0
- 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.
|
|
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": "
|
|
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
|
+
});
|