@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,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for cmdInit local repo creation and cmdBranch central mem init
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
// Mock child_process
|
|
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 { spawnSync, execSync } = require('child_process');
|
|
26
|
+
|
|
27
|
+
const testDir = path.join(os.tmpdir(), 'memx-test-init-local-' + Date.now());
|
|
28
|
+
const originalCwd = process.cwd;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
32
|
+
spawnSync.mockReset();
|
|
33
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
34
|
+
execSync.mockReset();
|
|
35
|
+
execSync.mockReturnValue('');
|
|
36
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
37
|
+
jest.spyOn(console, 'error').mockImplementation();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
process.cwd = originalCwd;
|
|
42
|
+
if (fs.existsSync(testDir)) {
|
|
43
|
+
fs.rmSync(testDir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
jest.restoreAllMocks();
|
|
46
|
+
jest.resetModules();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('cmdInit local repo creation', () => {
|
|
50
|
+
test('creates new local .mem repo', async () => {
|
|
51
|
+
// Mock cwd to return our test directory
|
|
52
|
+
const projectDir = path.join(testDir, 'project');
|
|
53
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
54
|
+
process.cwd = jest.fn(() => projectDir);
|
|
55
|
+
|
|
56
|
+
jest.resetModules();
|
|
57
|
+
const { cmdInit } = require('../index.js');
|
|
58
|
+
|
|
59
|
+
await cmdInit(['test-task', 'Build', 'something'], null);
|
|
60
|
+
|
|
61
|
+
const output = console.log.mock.calls.map(c => c[0]).join('\n');
|
|
62
|
+
expect(output).toContain('Created');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('warns when .mem already exists locally', async () => {
|
|
66
|
+
const projectDir = path.join(testDir, 'existing-project');
|
|
67
|
+
fs.mkdirSync(path.join(projectDir, '.mem'), { recursive: true });
|
|
68
|
+
process.cwd = jest.fn(() => projectDir);
|
|
69
|
+
|
|
70
|
+
jest.resetModules();
|
|
71
|
+
const { cmdInit } = require('../index.js');
|
|
72
|
+
|
|
73
|
+
await cmdInit(['new-task'], null);
|
|
74
|
+
|
|
75
|
+
const output = console.log.mock.calls.map(c => c[0]).join('\n');
|
|
76
|
+
expect(output).toContain('already exists');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('creates task with goal in new local repo', async () => {
|
|
80
|
+
const projectDir = path.join(testDir, 'new-project');
|
|
81
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
82
|
+
process.cwd = jest.fn(() => projectDir);
|
|
83
|
+
|
|
84
|
+
jest.resetModules();
|
|
85
|
+
const { cmdInit } = require('../index.js');
|
|
86
|
+
|
|
87
|
+
await cmdInit(['feature', 'Add', 'new', 'feature'], null);
|
|
88
|
+
|
|
89
|
+
const output = console.log.mock.calls.map(c => c[0]).join('\n');
|
|
90
|
+
expect(output).toContain('feature');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('handles git init failure', async () => {
|
|
94
|
+
const projectDir = path.join(testDir, 'fail-project');
|
|
95
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
96
|
+
process.cwd = jest.fn(() => projectDir);
|
|
97
|
+
|
|
98
|
+
// Make execSync throw for git init
|
|
99
|
+
execSync.mockImplementation((cmd) => {
|
|
100
|
+
if (cmd && cmd.includes && cmd.includes('git init')) {
|
|
101
|
+
throw new Error('git init failed');
|
|
102
|
+
}
|
|
103
|
+
return '';
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Also make spawnSync fail for git commands
|
|
107
|
+
spawnSync.mockImplementation((cmd) => {
|
|
108
|
+
if (cmd === 'git') {
|
|
109
|
+
return { status: 1, stdout: '', stderr: 'git error' };
|
|
110
|
+
}
|
|
111
|
+
return { status: 0, stdout: '', stderr: '' };
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
jest.resetModules();
|
|
115
|
+
const { cmdInit } = require('../index.js');
|
|
116
|
+
|
|
117
|
+
await cmdInit(['fail-task'], null);
|
|
118
|
+
|
|
119
|
+
// Should handle error or succeed - just verify it ran
|
|
120
|
+
expect(console.log).toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('cmdBranch central mem initialization', () => {
|
|
125
|
+
test('initializes central mem when it does not exist', () => {
|
|
126
|
+
// Use a unique path for CENTRAL_MEM that doesn't exist
|
|
127
|
+
const uniqueCentralMem = path.join(testDir, 'central-mem-init-' + Date.now());
|
|
128
|
+
|
|
129
|
+
// We need to mock the module to use our test path
|
|
130
|
+
jest.resetModules();
|
|
131
|
+
|
|
132
|
+
// This is tricky - CENTRAL_MEM is a constant. Let's test the behavior instead.
|
|
133
|
+
const { cmdBranch, CENTRAL_MEM } = require('../index.js');
|
|
134
|
+
|
|
135
|
+
// Create a memDir that exists
|
|
136
|
+
const memDir = path.join(testDir, '.mem-test');
|
|
137
|
+
fs.mkdirSync(path.join(memDir, '.git'), { recursive: true });
|
|
138
|
+
|
|
139
|
+
cmdBranch(['new-branch'], memDir);
|
|
140
|
+
|
|
141
|
+
expect(console.log).toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('cmdCommit edge cases', () => {
|
|
146
|
+
test('shows warning when central mem does not exist', () => {
|
|
147
|
+
jest.resetModules();
|
|
148
|
+
|
|
149
|
+
const { cmdCommit, CENTRAL_MEM } = require('../index.js');
|
|
150
|
+
|
|
151
|
+
// Remove central mem if it exists
|
|
152
|
+
if (fs.existsSync(CENTRAL_MEM)) {
|
|
153
|
+
// We can't remove the real central mem, so just test with null memDir
|
|
154
|
+
cmdCommit(['test'], null);
|
|
155
|
+
|
|
156
|
+
const output = console.log.mock.calls.map(c => c[0]).join('\n');
|
|
157
|
+
// It should either show warning or use central mem
|
|
158
|
+
expect(console.log).toHaveBeenCalled();
|
|
159
|
+
} else {
|
|
160
|
+
cmdCommit(['test'], null);
|
|
161
|
+
expect(console.log).toHaveBeenCalled();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('interactiveInit edge cases', () => {
|
|
167
|
+
test('handles git init error in interactiveInit', async () => {
|
|
168
|
+
// This is tested indirectly through the module
|
|
169
|
+
jest.resetModules();
|
|
170
|
+
|
|
171
|
+
// Mock prompt responses
|
|
172
|
+
let promptIndex = 0;
|
|
173
|
+
const mockResponses = ['Test goal', 'Criterion 1', ''];
|
|
174
|
+
|
|
175
|
+
jest.doMock('readline', () => ({
|
|
176
|
+
createInterface: jest.fn(() => ({
|
|
177
|
+
question: jest.fn((q, cb) => {
|
|
178
|
+
cb(mockResponses[promptIndex++] || '');
|
|
179
|
+
}),
|
|
180
|
+
close: jest.fn(),
|
|
181
|
+
on: jest.fn()
|
|
182
|
+
}))
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
execSync.mockImplementation((cmd) => {
|
|
186
|
+
if (cmd === 'git init') {
|
|
187
|
+
throw new Error('git init failed');
|
|
188
|
+
}
|
|
189
|
+
return '';
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const { interactiveInit } = require('../index.js');
|
|
193
|
+
|
|
194
|
+
const result = await interactiveInit();
|
|
195
|
+
|
|
196
|
+
// Should handle error gracefully
|
|
197
|
+
expect(console.log).toHaveBeenCalled();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('startMCPServer', () => {
|
|
202
|
+
test('starts MCP server process', () => {
|
|
203
|
+
jest.resetModules();
|
|
204
|
+
|
|
205
|
+
// Create mock before requiring module
|
|
206
|
+
const mockChild = {
|
|
207
|
+
on: jest.fn((event, handler) => {
|
|
208
|
+
if (event === 'exit') {
|
|
209
|
+
setTimeout(() => handler(0), 10);
|
|
210
|
+
}
|
|
211
|
+
return mockChild;
|
|
212
|
+
})
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
jest.doMock('child_process', () => ({
|
|
216
|
+
execSync: jest.fn(),
|
|
217
|
+
spawnSync: jest.fn(() => ({ status: 0, stdout: '', stderr: '' })),
|
|
218
|
+
spawn: jest.fn(() => mockChild)
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
const { startMCPServer, CENTRAL_MEM } = require('../index.js');
|
|
222
|
+
|
|
223
|
+
fs.mkdirSync(path.join(CENTRAL_MEM, '.git'), { recursive: true });
|
|
224
|
+
|
|
225
|
+
startMCPServer(['mcp']);
|
|
226
|
+
|
|
227
|
+
const { spawn } = require('child_process');
|
|
228
|
+
expect(spawn).toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('handles MCP server spawn error', () => {
|
|
232
|
+
jest.resetModules();
|
|
233
|
+
|
|
234
|
+
const originalExit = process.exit;
|
|
235
|
+
process.exit = jest.fn();
|
|
236
|
+
|
|
237
|
+
const mockChild = {
|
|
238
|
+
on: jest.fn((event, handler) => {
|
|
239
|
+
if (event === 'error') {
|
|
240
|
+
setTimeout(() => handler(new Error('spawn failed')), 10);
|
|
241
|
+
}
|
|
242
|
+
return mockChild;
|
|
243
|
+
})
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
jest.doMock('child_process', () => ({
|
|
247
|
+
execSync: jest.fn(),
|
|
248
|
+
spawnSync: jest.fn(() => ({ status: 0, stdout: '', stderr: '' })),
|
|
249
|
+
spawn: jest.fn(() => mockChild)
|
|
250
|
+
}));
|
|
251
|
+
|
|
252
|
+
const { startMCPServer, CENTRAL_MEM } = require('../index.js');
|
|
253
|
+
|
|
254
|
+
fs.mkdirSync(path.join(CENTRAL_MEM, '.git'), { recursive: true });
|
|
255
|
+
|
|
256
|
+
startMCPServer(['mcp']);
|
|
257
|
+
|
|
258
|
+
// Wait for async error callback
|
|
259
|
+
return new Promise(resolve => {
|
|
260
|
+
setTimeout(() => {
|
|
261
|
+
expect(console.error).toHaveBeenCalled();
|
|
262
|
+
process.exit = originalExit;
|
|
263
|
+
resolve();
|
|
264
|
+
}, 50);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('findMemDir with central mem index', () => {
|
|
270
|
+
test('returns central mem with exact index match', () => {
|
|
271
|
+
jest.resetModules();
|
|
272
|
+
|
|
273
|
+
const { findMemDir, saveIndex, CENTRAL_MEM } = require('../index.js');
|
|
274
|
+
|
|
275
|
+
// Make sure central mem exists
|
|
276
|
+
fs.mkdirSync(path.join(CENTRAL_MEM, '.git'), { recursive: true });
|
|
277
|
+
|
|
278
|
+
// Save index with mapping
|
|
279
|
+
saveIndex({ '/exact/path': 'task/exact' });
|
|
280
|
+
|
|
281
|
+
const result = findMemDir('/exact/path');
|
|
282
|
+
|
|
283
|
+
expect(result).not.toBeNull();
|
|
284
|
+
expect(result.taskBranch).toBe('task/exact');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('returns central mem with parent directory match', () => {
|
|
288
|
+
jest.resetModules();
|
|
289
|
+
|
|
290
|
+
const { findMemDir, saveIndex, CENTRAL_MEM } = require('../index.js');
|
|
291
|
+
|
|
292
|
+
fs.mkdirSync(path.join(CENTRAL_MEM, '.git'), { recursive: true });
|
|
293
|
+
|
|
294
|
+
// Save index with parent path mapping
|
|
295
|
+
saveIndex({ '/parent/path': 'task/parent' });
|
|
296
|
+
|
|
297
|
+
const result = findMemDir('/parent/path/child/subdir');
|
|
298
|
+
|
|
299
|
+
expect(result).not.toBeNull();
|
|
300
|
+
expect(result.taskBranch).toBe('task/parent');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('returns unmapped when central exists but no mapping', () => {
|
|
304
|
+
jest.resetModules();
|
|
305
|
+
|
|
306
|
+
const { findMemDir, saveIndex, CENTRAL_MEM } = require('../index.js');
|
|
307
|
+
|
|
308
|
+
fs.mkdirSync(path.join(CENTRAL_MEM, '.git'), { recursive: true });
|
|
309
|
+
saveIndex({}); // Empty index
|
|
310
|
+
|
|
311
|
+
const result = findMemDir('/unmapped/path');
|
|
312
|
+
|
|
313
|
+
expect(result).not.toBeNull();
|
|
314
|
+
expect(result.unmapped).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for initialization and task creation functions
|
|
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
|
+
cmdTasks,
|
|
28
|
+
writeMemFile,
|
|
29
|
+
readMemFile,
|
|
30
|
+
git,
|
|
31
|
+
} = require('../index.js');
|
|
32
|
+
|
|
33
|
+
const { spawnSync, execSync } = require('child_process');
|
|
34
|
+
|
|
35
|
+
const testDir = path.join(os.tmpdir(), 'memx-test-init-' + Date.now());
|
|
36
|
+
let memDir;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
memDir = path.join(testDir, '.mem');
|
|
40
|
+
fs.mkdirSync(memDir, { recursive: true });
|
|
41
|
+
fs.mkdirSync(path.join(memDir, '.git'), { recursive: true });
|
|
42
|
+
spawnSync.mockClear();
|
|
43
|
+
execSync.mockClear();
|
|
44
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
if (fs.existsSync(testDir)) {
|
|
49
|
+
fs.rmSync(testDir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
jest.restoreAllMocks();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('cmdInit', () => {
|
|
55
|
+
test('shows usage when name missing and no existing memDir', async () => {
|
|
56
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
57
|
+
await cmdInit([], null);
|
|
58
|
+
// With no memDir and no name, it should show usage
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('creates task branch in existing repo', async () => {
|
|
62
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
63
|
+
|
|
64
|
+
await cmdInit(['new-task'], memDir);
|
|
65
|
+
|
|
66
|
+
expect(spawnSync).toHaveBeenCalledWith(
|
|
67
|
+
'git',
|
|
68
|
+
['checkout', '-b', 'task/new-task'],
|
|
69
|
+
expect.any(Object)
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('creates task with goal', async () => {
|
|
74
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
75
|
+
|
|
76
|
+
await cmdInit(['my-task', 'Build', 'a', 'feature'], memDir);
|
|
77
|
+
|
|
78
|
+
// Should create files
|
|
79
|
+
expect(spawnSync).toHaveBeenCalledWith(
|
|
80
|
+
'git',
|
|
81
|
+
['add', '-A'],
|
|
82
|
+
expect.any(Object)
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('cmdTasks', () => {
|
|
88
|
+
test('prints warning when no memDir', () => {
|
|
89
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
90
|
+
cmdTasks([], null);
|
|
91
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('shows no tasks when empty', () => {
|
|
95
|
+
spawnSync
|
|
96
|
+
.mockReturnValueOnce({ status: 0, stdout: 'main\n', stderr: '' }) // getCurrentBranch
|
|
97
|
+
.mockReturnValueOnce({ status: 0, stdout: '* main', stderr: '' }); // branch list
|
|
98
|
+
|
|
99
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
100
|
+
cmdTasks([], memDir);
|
|
101
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('No tasks');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('lists tasks in non-TTY mode', () => {
|
|
105
|
+
// Mock process.stdin.isTTY to be false
|
|
106
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: false, writable: true });
|
|
107
|
+
|
|
108
|
+
spawnSync
|
|
109
|
+
.mockReturnValueOnce({ status: 0, stdout: 'task/test\n', stderr: '' })
|
|
110
|
+
.mockReturnValueOnce({ status: 0, stdout: ' main\n* task/test', stderr: '' })
|
|
111
|
+
.mockReturnValueOnce({ status: 0, stdout: '', stderr: '' })
|
|
112
|
+
.mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
|
|
113
|
+
|
|
114
|
+
writeMemFile(memDir, 'state.md', '---\nstatus: active\n---\n\n');
|
|
115
|
+
writeMemFile(memDir, 'goal.md', '# Goal\n\nTest\n\n## Progress: 50%');
|
|
116
|
+
|
|
117
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
118
|
+
cmdTasks([], memDir);
|
|
119
|
+
|
|
120
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for interactive async functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
// Variables for mock prompt responses - must be prefixed with 'mock'
|
|
10
|
+
let mockPromptResponses = [];
|
|
11
|
+
let mockPromptIndex = 0;
|
|
12
|
+
|
|
13
|
+
// Mock child_process before requiring index.js
|
|
14
|
+
jest.mock('child_process', () => ({
|
|
15
|
+
execSync: jest.fn(),
|
|
16
|
+
spawnSync: jest.fn(() => ({ status: 0, stdout: '', stderr: '' })),
|
|
17
|
+
spawn: jest.fn()
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// Mock readline with controlled responses
|
|
21
|
+
jest.mock('readline', () => ({
|
|
22
|
+
createInterface: jest.fn(() => ({
|
|
23
|
+
question: jest.fn((q, cb) => {
|
|
24
|
+
const response = mockPromptResponses[mockPromptIndex] || '';
|
|
25
|
+
mockPromptIndex++;
|
|
26
|
+
cb(response);
|
|
27
|
+
}),
|
|
28
|
+
close: jest.fn(),
|
|
29
|
+
on: jest.fn()
|
|
30
|
+
}))
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
cmdDone,
|
|
35
|
+
interactiveInit,
|
|
36
|
+
setupRemote,
|
|
37
|
+
writeMemFile,
|
|
38
|
+
readMemFile,
|
|
39
|
+
CENTRAL_MEM,
|
|
40
|
+
} = require('../index.js');
|
|
41
|
+
|
|
42
|
+
const { spawnSync, execSync } = require('child_process');
|
|
43
|
+
|
|
44
|
+
const testDir = path.join(os.tmpdir(), 'memx-test-interactive-' + Date.now());
|
|
45
|
+
let memDir;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
memDir = path.join(testDir, '.mem');
|
|
49
|
+
fs.mkdirSync(memDir, { recursive: true });
|
|
50
|
+
fs.mkdirSync(path.join(memDir, '.git'), { recursive: true });
|
|
51
|
+
spawnSync.mockClear();
|
|
52
|
+
execSync.mockClear();
|
|
53
|
+
mockPromptResponses = [];
|
|
54
|
+
mockPromptIndex = 0;
|
|
55
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
if (fs.existsSync(testDir)) {
|
|
60
|
+
fs.rmSync(testDir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
jest.restoreAllMocks();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('cmdDone', () => {
|
|
66
|
+
test('shows warning when no memDir', async () => {
|
|
67
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
68
|
+
await cmdDone(null);
|
|
69
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('shows warning when on main branch', async () => {
|
|
73
|
+
spawnSync.mockReturnValueOnce({ status: 0, stdout: 'main', stderr: '' });
|
|
74
|
+
|
|
75
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
76
|
+
await cmdDone(memDir);
|
|
77
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('Already on main');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('completes task with no learnings to promote', async () => {
|
|
81
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
82
|
+
spawnSync.mockReturnValueOnce({ status: 0, stdout: 'task/test', stderr: '' });
|
|
83
|
+
|
|
84
|
+
writeMemFile(memDir, 'memory.md', '# Learnings\n\n');
|
|
85
|
+
|
|
86
|
+
mockPromptResponses = ['none', 'n'];
|
|
87
|
+
|
|
88
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
89
|
+
await cmdDone(memDir);
|
|
90
|
+
|
|
91
|
+
const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
92
|
+
expect(output).toContain('Completing task');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('promotes learnings on done', async () => {
|
|
96
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
97
|
+
spawnSync.mockReturnValueOnce({ status: 0, stdout: 'task/test', stderr: '' });
|
|
98
|
+
|
|
99
|
+
writeMemFile(memDir, 'memory.md', '# Learnings\n\n- 2024-01-01: First insight\n- 2024-01-02: Second insight');
|
|
100
|
+
writeMemFile(memDir, 'playbook.md', '# Playbook\n\n');
|
|
101
|
+
|
|
102
|
+
mockPromptResponses = ['1', 'n'];
|
|
103
|
+
|
|
104
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
105
|
+
await cmdDone(memDir);
|
|
106
|
+
|
|
107
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('deletes branch when requested', async () => {
|
|
111
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
112
|
+
spawnSync.mockReturnValueOnce({ status: 0, stdout: 'task/test', stderr: '' });
|
|
113
|
+
|
|
114
|
+
writeMemFile(memDir, 'memory.md', '# Learnings\n\n');
|
|
115
|
+
|
|
116
|
+
mockPromptResponses = ['none', 'y'];
|
|
117
|
+
|
|
118
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
119
|
+
await cmdDone(memDir);
|
|
120
|
+
|
|
121
|
+
const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
122
|
+
expect(output).toContain('Merged');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('handles merge failure gracefully', async () => {
|
|
126
|
+
spawnSync
|
|
127
|
+
.mockReturnValueOnce({ status: 0, stdout: 'task/test', stderr: '' })
|
|
128
|
+
.mockReturnValueOnce({ status: 0, stdout: '', stderr: '' })
|
|
129
|
+
.mockReturnValueOnce({ status: 1, stdout: '', stderr: 'merge conflict' })
|
|
130
|
+
.mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
|
|
131
|
+
|
|
132
|
+
writeMemFile(memDir, 'memory.md', '# Learnings\n\n');
|
|
133
|
+
|
|
134
|
+
mockPromptResponses = ['none'];
|
|
135
|
+
|
|
136
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
137
|
+
await cmdDone(memDir);
|
|
138
|
+
|
|
139
|
+
const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
140
|
+
expect(output).toContain('Merge failed');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('interactiveInit', () => {
|
|
145
|
+
test('creates task from interactive input', async () => {
|
|
146
|
+
mockPromptResponses = [
|
|
147
|
+
'Build a feature',
|
|
148
|
+
'Users can login',
|
|
149
|
+
'Tests pass',
|
|
150
|
+
''
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
154
|
+
execSync.mockReturnValue('');
|
|
155
|
+
|
|
156
|
+
const result = await interactiveInit();
|
|
157
|
+
|
|
158
|
+
if (result) {
|
|
159
|
+
expect(result.name).toBe('build-a-feature');
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('handles empty goal input', async () => {
|
|
164
|
+
mockPromptResponses = [''];
|
|
165
|
+
|
|
166
|
+
const result = await interactiveInit();
|
|
167
|
+
expect(result).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('setupRemote', () => {
|
|
172
|
+
test('connects to existing remote', async () => {
|
|
173
|
+
mockPromptResponses = ['https://github.com/user/repo.git'];
|
|
174
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
175
|
+
|
|
176
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
177
|
+
await setupRemote(memDir, 'test-task');
|
|
178
|
+
|
|
179
|
+
const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
180
|
+
expect(output).toContain('Connected');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('handles remote connection failure', async () => {
|
|
184
|
+
mockPromptResponses = ['https://github.com/user/repo.git'];
|
|
185
|
+
spawnSync.mockReturnValueOnce({ status: 1, stdout: '', stderr: 'remote error' });
|
|
186
|
+
|
|
187
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
188
|
+
await setupRemote(memDir, 'test-task');
|
|
189
|
+
|
|
190
|
+
const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
191
|
+
expect(output).toContain('Could not connect');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('shows message when gh CLI not found', async () => {
|
|
195
|
+
mockPromptResponses = [''];
|
|
196
|
+
execSync.mockImplementation(() => { throw new Error('not found'); });
|
|
197
|
+
|
|
198
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
199
|
+
await setupRemote(memDir, 'test-task');
|
|
200
|
+
|
|
201
|
+
const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
202
|
+
expect(output).toContain('GitHub CLI');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('creates new repo with gh CLI', async () => {
|
|
206
|
+
mockPromptResponses = ['', 'my-repo', 'y'];
|
|
207
|
+
execSync.mockReturnValue('');
|
|
208
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
209
|
+
|
|
210
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
211
|
+
await setupRemote(memDir, 'test-task');
|
|
212
|
+
|
|
213
|
+
const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
214
|
+
expect(output).toContain('Created repo');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('handles gh repo create failure', async () => {
|
|
218
|
+
mockPromptResponses = ['', 'my-repo', 'n'];
|
|
219
|
+
execSync
|
|
220
|
+
.mockReturnValueOnce('')
|
|
221
|
+
.mockImplementationOnce(() => { throw new Error('gh error'); });
|
|
222
|
+
|
|
223
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
224
|
+
await setupRemote(memDir, 'test-task');
|
|
225
|
+
|
|
226
|
+
const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
227
|
+
expect(output).toContain('Could not create repo');
|
|
228
|
+
});
|
|
229
|
+
});
|