@mndrk/memx 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +98 -77
  2. package/coverage/clover.xml +1160 -0
  3. package/coverage/coverage-final.json +3 -0
  4. package/coverage/lcov-report/base.css +224 -0
  5. package/coverage/lcov-report/block-navigation.js +87 -0
  6. package/coverage/lcov-report/favicon.png +0 -0
  7. package/coverage/lcov-report/index.html +131 -0
  8. package/coverage/lcov-report/index.js.html +7255 -0
  9. package/coverage/lcov-report/mcp.js.html +1009 -0
  10. package/coverage/lcov-report/prettify.css +1 -0
  11. package/coverage/lcov-report/prettify.js +2 -0
  12. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  13. package/coverage/lcov-report/sorter.js +210 -0
  14. package/coverage/lcov.info +2017 -0
  15. package/index.js +651 -243
  16. package/package.json +24 -2
  17. package/test/additional.test.js +373 -0
  18. package/test/branches.test.js +247 -0
  19. package/test/commands.test.js +663 -0
  20. package/test/context.test.js +185 -0
  21. package/test/coverage.test.js +366 -0
  22. package/test/dispatch.test.js +220 -0
  23. package/test/edge-coverage.test.js +250 -0
  24. package/test/edge.test.js +434 -0
  25. package/test/final-coverage.test.js +316 -0
  26. package/test/final-edges.test.js +199 -0
  27. package/test/init-local.test.js +316 -0
  28. package/test/init.test.js +122 -0
  29. package/test/interactive.test.js +229 -0
  30. package/test/main-dispatch.test.js +164 -0
  31. package/test/main-full.test.js +590 -0
  32. package/test/main.test.js +197 -0
  33. package/test/mcp-server.test.js +320 -0
  34. package/test/mcp.test.js +288 -0
  35. package/test/more.test.js +312 -0
  36. package/test/new.test.js +175 -0
  37. package/test/skill.test.js +247 -0
  38. package/test/tasks-interactive.test.js +243 -0
  39. package/test/utils.test.js +367 -0
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Tests for main dispatcher and edge cases
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
+ cmdBranch,
27
+ cmdCommit,
28
+ cmdSwitch,
29
+ cmdLearn,
30
+ cmdLearnings,
31
+ cmdPlaybook,
32
+ cmdHistory,
33
+ cmdLog,
34
+ cmdQuery,
35
+ writeMemFile,
36
+ readMemFile,
37
+ CENTRAL_MEM,
38
+ } = require('../index.js');
39
+
40
+ const { spawnSync, execSync } = require('child_process');
41
+
42
+ const testDir = path.join(os.tmpdir(), 'memx-test-dispatch-' + Date.now());
43
+ let memDir;
44
+
45
+ beforeEach(() => {
46
+ memDir = path.join(testDir, '.mem');
47
+ fs.mkdirSync(memDir, { recursive: true });
48
+ fs.mkdirSync(path.join(memDir, '.git'), { recursive: true });
49
+ spawnSync.mockReset();
50
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
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('cmdBranch initialization', () => {
63
+ test('switches to existing branch', () => {
64
+ spawnSync
65
+ .mockReturnValueOnce({ status: 0, stdout: ' main\n task/existing', stderr: '' })
66
+ .mockReturnValueOnce({ status: 0, stdout: ' main\n task/existing', stderr: '' })
67
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
68
+
69
+ const consoleSpy = jest.spyOn(console, 'log');
70
+ cmdBranch(['existing'], memDir);
71
+
72
+ expect(consoleSpy.mock.calls[0][0]).toContain('Switched to');
73
+ });
74
+ });
75
+
76
+ describe('cmdCommit edge cases', () => {
77
+ test('uses central mem when no memDir provided', () => {
78
+ spawnSync
79
+ .mockReturnValueOnce({ status: 0, stdout: 'M file.md', stderr: '' })
80
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' })
81
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
82
+
83
+ cmdCommit(['test'], null);
84
+
85
+ // Should work with central mem if it exists
86
+ expect(spawnSync).toHaveBeenCalled();
87
+ });
88
+ });
89
+
90
+ describe('cmdSwitch edge cases', () => {
91
+ test('switches to task branch without prefix', () => {
92
+ spawnSync
93
+ .mockReturnValueOnce({ status: 1, stdout: '', stderr: 'not found' })
94
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
95
+
96
+ const consoleSpy = jest.spyOn(console, 'log');
97
+ cmdSwitch(['feature'], memDir);
98
+
99
+ expect(consoleSpy.mock.calls[0][0]).toContain('Switched');
100
+ });
101
+
102
+ test('shows error when both attempts fail', () => {
103
+ // Both with and without task/ prefix fail
104
+ spawnSync
105
+ .mockReturnValueOnce({ status: 1, stdout: '', stderr: 'error1' })
106
+ .mockReturnValueOnce({ status: 1, stdout: '', stderr: 'error2' });
107
+
108
+ const consoleSpy = jest.spyOn(console, 'log');
109
+ cmdSwitch(['nonexistent'], memDir);
110
+
111
+ // The function tries task/nonexistent first, then nonexistent
112
+ expect(consoleSpy).toHaveBeenCalled();
113
+ });
114
+
115
+ test('shows usage when no args', () => {
116
+ const consoleSpy = jest.spyOn(console, 'log');
117
+ cmdSwitch([], memDir);
118
+
119
+ expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
120
+ });
121
+ });
122
+
123
+ describe('cmdLearn edge cases', () => {
124
+ test('learns to playbook with -g flag', () => {
125
+ writeMemFile(memDir, 'playbook.md', '# Playbook\n\n');
126
+
127
+ cmdLearn(['-g', 'Global', 'insight'], memDir);
128
+
129
+ const content = readMemFile(memDir, 'playbook.md');
130
+ expect(content).toContain('Global insight');
131
+ });
132
+
133
+ test('shows usage when no text', () => {
134
+ const consoleSpy = jest.spyOn(console, 'log');
135
+ cmdLearn([], memDir);
136
+
137
+ expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
138
+ });
139
+ });
140
+
141
+ describe('cmdLearnings edge cases', () => {
142
+ test('shows global learnings with -g flag', () => {
143
+ writeMemFile(memDir, 'playbook.md', '# Playbook\n\n- Global rule 1\n- Global rule 2');
144
+
145
+ const consoleSpy = jest.spyOn(console, 'log');
146
+ cmdLearnings(['-g'], memDir);
147
+
148
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
149
+ expect(output).toContain('Global');
150
+ });
151
+ });
152
+
153
+ describe('cmdPlaybook edge cases', () => {
154
+ test('creates playbook if empty', () => {
155
+ writeMemFile(memDir, 'playbook.md', '# Playbook\n\n');
156
+
157
+ const consoleSpy = jest.spyOn(console, 'log');
158
+ cmdPlaybook(memDir);
159
+
160
+ expect(consoleSpy.mock.calls[0][0]).toContain('Playbook');
161
+ });
162
+ });
163
+
164
+ describe('cmdHistory edge cases', () => {
165
+ test('shows history with multiple commits', () => {
166
+ spawnSync.mockReturnValue({
167
+ status: 0,
168
+ stdout: 'abc123 first commit\ndef456 second commit\nghi789 third commit',
169
+ stderr: ''
170
+ });
171
+
172
+ const consoleSpy = jest.spyOn(console, 'log');
173
+ cmdHistory(memDir);
174
+
175
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
176
+ expect(output).toContain('History');
177
+ });
178
+ });
179
+
180
+ describe('cmdLog edge cases', () => {
181
+ test('shows formatted log', () => {
182
+ spawnSync.mockReturnValue({
183
+ status: 0,
184
+ stdout: '* abc123 (HEAD -> task/test) first\n* def456 second',
185
+ stderr: ''
186
+ });
187
+
188
+ const consoleSpy = jest.spyOn(console, 'log');
189
+ cmdLog(memDir);
190
+
191
+ expect(consoleSpy).toHaveBeenCalled();
192
+ });
193
+
194
+ test('handles empty log', () => {
195
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
196
+
197
+ const consoleSpy = jest.spyOn(console, 'log');
198
+ cmdLog(memDir);
199
+
200
+ expect(consoleSpy.mock.calls[0][0]).toBe('');
201
+ });
202
+ });
203
+
204
+ describe('cmdQuery edge cases', () => {
205
+ test('searches in all markdown files', () => {
206
+ execSync.mockReturnValue('goal.md:5:Test match\nstate.md:10:Another match');
207
+
208
+ const consoleSpy = jest.spyOn(console, 'log');
209
+ cmdQuery(['test'], memDir);
210
+
211
+ expect(consoleSpy).toHaveBeenCalled();
212
+ });
213
+
214
+ test('shows usage when no search term', () => {
215
+ const consoleSpy = jest.spyOn(console, 'log');
216
+ cmdQuery([], memDir);
217
+
218
+ expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
219
+ });
220
+ });
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Tests for edge case coverage to reach 90%+
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
+ cmdBranch,
27
+ cmdCommit,
28
+ cmdInit,
29
+ cmdSwitch,
30
+ cmdSync,
31
+ findMemDir,
32
+ saveConfig,
33
+ saveIndex,
34
+ loadIndex,
35
+ writeMemFile,
36
+ readMemFile,
37
+ CENTRAL_MEM,
38
+ CONFIG_DIR,
39
+ c
40
+ } = require('../index.js');
41
+
42
+ const { spawnSync, execSync } = require('child_process');
43
+
44
+ const testDir = path.join(os.tmpdir(), 'memx-edge-' + 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
+ jest.spyOn(console, 'log').mockImplementation();
54
+ jest.spyOn(console, 'error').mockImplementation();
55
+ });
56
+
57
+ afterEach(() => {
58
+ if (fs.existsSync(testDir)) {
59
+ fs.rmSync(testDir, { recursive: true });
60
+ }
61
+ jest.restoreAllMocks();
62
+ });
63
+
64
+ describe('cmdInit error handling', () => {
65
+ test('handles git init failure', async () => {
66
+ spawnSync.mockReturnValue({ status: 1, stdout: '', stderr: 'git error' });
67
+
68
+ const consoleSpy = jest.spyOn(console, 'log');
69
+ await cmdInit(['test-task'], memDir);
70
+
71
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
72
+ expect(output).toContain('Error');
73
+ });
74
+
75
+ test('creates goal file with provided goal text', async () => {
76
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
77
+
78
+ await cmdInit(['my-task', 'Build', 'something', 'cool'], memDir);
79
+
80
+ expect(spawnSync).toHaveBeenCalled();
81
+ });
82
+ });
83
+
84
+ describe('cmdSwitch edge cases', () => {
85
+ test('handles checkout to task/ prefixed name', () => {
86
+ spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
87
+
88
+ cmdSwitch(['task/feature'], memDir);
89
+
90
+ expect(spawnSync).toHaveBeenCalledWith(
91
+ 'git',
92
+ ['checkout', 'task/feature'],
93
+ expect.any(Object)
94
+ );
95
+ });
96
+
97
+ test('handles fallback when task/ prefix fails', () => {
98
+ spawnSync
99
+ .mockReturnValueOnce({ status: 1, stdout: '', stderr: 'error' })
100
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
101
+
102
+ cmdSwitch(['feature'], memDir);
103
+
104
+ // Should have tried task/feature then feature
105
+ expect(spawnSync).toHaveBeenCalledTimes(2);
106
+ });
107
+ });
108
+
109
+ describe('cmdSync edge cases', () => {
110
+ test('handles no remote configured', () => {
111
+ spawnSync.mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
112
+
113
+ const consoleSpy = jest.spyOn(console, 'log');
114
+ cmdSync(memDir);
115
+
116
+ expect(consoleSpy.mock.calls[0][0]).toContain('No remote');
117
+ });
118
+
119
+ test('handles successful sync', () => {
120
+ spawnSync
121
+ .mockReturnValueOnce({ status: 0, stdout: 'origin', stderr: '' })
122
+ .mockReturnValueOnce({ status: 0, stdout: 'task/test', stderr: '' })
123
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' })
124
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
125
+
126
+ const consoleSpy = jest.spyOn(console, 'log');
127
+ cmdSync(memDir);
128
+
129
+ const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
130
+ expect(output).toContain('Syncing');
131
+ });
132
+ });
133
+
134
+ describe('cmdBranch edge cases', () => {
135
+ test('lists branches without args', () => {
136
+ spawnSync.mockReturnValue({ status: 0, stdout: '* main\n task/test', stderr: '' });
137
+
138
+ const consoleSpy = jest.spyOn(console, 'log');
139
+ cmdBranch([], memDir);
140
+
141
+ expect(consoleSpy).toHaveBeenCalled();
142
+ });
143
+
144
+ test('creates new branch when not exists', () => {
145
+ spawnSync
146
+ .mockReturnValueOnce({ status: 0, stdout: '* main', stderr: '' })
147
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
148
+
149
+ cmdBranch(['new-feature'], memDir);
150
+
151
+ expect(spawnSync).toHaveBeenCalledWith(
152
+ 'git',
153
+ ['checkout', '-b', 'task/new-feature'],
154
+ expect.any(Object)
155
+ );
156
+ });
157
+
158
+ test('switches to existing branch', () => {
159
+ spawnSync
160
+ .mockReturnValueOnce({ status: 0, stdout: '* main\n task/existing', stderr: '' })
161
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
162
+
163
+ cmdBranch(['existing'], memDir);
164
+
165
+ expect(spawnSync).toHaveBeenCalledWith(
166
+ 'git',
167
+ ['checkout', 'task/existing'],
168
+ expect.any(Object)
169
+ );
170
+ });
171
+ });
172
+
173
+ describe('cmdCommit edge cases', () => {
174
+ test('handles no changes to commit', () => {
175
+ spawnSync.mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
176
+
177
+ const consoleSpy = jest.spyOn(console, 'log');
178
+ cmdCommit(['test message'], memDir);
179
+
180
+ expect(consoleSpy.mock.calls[0][0]).toContain('No changes');
181
+ });
182
+
183
+ test('commits with message', () => {
184
+ spawnSync
185
+ .mockReturnValueOnce({ status: 0, stdout: 'M file.md', stderr: '' })
186
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' })
187
+ .mockReturnValueOnce({ status: 0, stdout: '', stderr: '' });
188
+
189
+ const consoleSpy = jest.spyOn(console, 'log');
190
+ cmdCommit(['my', 'message'], memDir);
191
+
192
+ expect(spawnSync).toHaveBeenCalledWith(
193
+ 'git',
194
+ ['commit', '-m', 'my message'],
195
+ expect.any(Object)
196
+ );
197
+ });
198
+ });
199
+
200
+ describe('findMemDir edge cases', () => {
201
+ test('returns unmapped result for central mem', () => {
202
+ // Create a temp dir that has no .mem
203
+ const noMemDir = path.join(testDir, 'no-mem-here');
204
+ fs.mkdirSync(noMemDir, { recursive: true });
205
+
206
+ const result = findMemDir(noMemDir);
207
+ // Result depends on whether CENTRAL_MEM exists
208
+ expect(result === null || typeof result === 'object').toBe(true);
209
+ });
210
+
211
+ test('finds local .mem in parent directory', () => {
212
+ const subDir = path.join(testDir, 'subdir', 'nested');
213
+ fs.mkdirSync(subDir, { recursive: true });
214
+
215
+ const result = findMemDir(subDir);
216
+ expect(result).not.toBeNull();
217
+ expect(result.memDir).toBe(memDir);
218
+ expect(result.isLocal).toBe(true);
219
+ });
220
+ });
221
+
222
+ describe('config functions', () => {
223
+ test('saveConfig handles existing directory', () => {
224
+ // This just tests the function doesn't throw
225
+ expect(() => saveConfig({ repos: {} })).not.toThrow();
226
+ });
227
+
228
+ test('saveIndex handles existing directory', () => {
229
+ expect(() => saveIndex({})).not.toThrow();
230
+ });
231
+
232
+ test('loadIndex returns object', () => {
233
+ const result = loadIndex();
234
+ expect(typeof result).toBe('object');
235
+ });
236
+ });
237
+
238
+ describe('color constants', () => {
239
+ test('all required colors are present', () => {
240
+ expect(c.reset).toBeDefined();
241
+ expect(c.bold).toBeDefined();
242
+ expect(c.dim).toBeDefined();
243
+ expect(c.green).toBeDefined();
244
+ expect(c.yellow).toBeDefined();
245
+ expect(c.red).toBeDefined();
246
+ expect(c.cyan).toBeDefined();
247
+ expect(c.blue).toBeDefined();
248
+ expect(c.magenta).toBeDefined();
249
+ });
250
+ });