@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.
- 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
|
@@ -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
|
+
|