@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
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for cmdContext function
|
|
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
|
+
cmdContext,
|
|
27
|
+
writeMemFile,
|
|
28
|
+
readMemFile
|
|
29
|
+
} = require('../index.js');
|
|
30
|
+
|
|
31
|
+
const { spawnSync } = require('child_process');
|
|
32
|
+
|
|
33
|
+
const testDir = path.join(os.tmpdir(), 'memx-test-context-' + Date.now());
|
|
34
|
+
let memDir;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
memDir = path.join(testDir, '.mem');
|
|
38
|
+
fs.mkdirSync(memDir, { recursive: true });
|
|
39
|
+
fs.mkdirSync(path.join(memDir, '.git'), { recursive: true });
|
|
40
|
+
spawnSync.mockClear();
|
|
41
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
if (fs.existsSync(testDir)) {
|
|
46
|
+
fs.rmSync(testDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
jest.restoreAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('cmdContext', () => {
|
|
52
|
+
test('prints warning when no memDir', () => {
|
|
53
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
54
|
+
cmdContext(null);
|
|
55
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('No .mem repo found');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('displays context output', () => {
|
|
59
|
+
spawnSync.mockReturnValue({ status: 0, stdout: 'task/test-feature\n', stderr: '' });
|
|
60
|
+
|
|
61
|
+
writeMemFile(memDir, 'goal.md', `---
|
|
62
|
+
task: test-feature
|
|
63
|
+
created: 2024-01-01
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
# Goal
|
|
67
|
+
|
|
68
|
+
Build a test feature
|
|
69
|
+
`);
|
|
70
|
+
|
|
71
|
+
writeMemFile(memDir, 'state.md', `---
|
|
72
|
+
status: active
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Next Step
|
|
76
|
+
|
|
77
|
+
Write unit tests
|
|
78
|
+
|
|
79
|
+
## Checkpoints
|
|
80
|
+
|
|
81
|
+
- [x] 2024-01-01: Started project
|
|
82
|
+
`);
|
|
83
|
+
|
|
84
|
+
writeMemFile(memDir, 'memory.md', `# Learnings
|
|
85
|
+
|
|
86
|
+
- 2024-01-01: Testing is important
|
|
87
|
+
`);
|
|
88
|
+
|
|
89
|
+
writeMemFile(memDir, 'playbook.md', `# Playbook
|
|
90
|
+
|
|
91
|
+
- Always write tests first
|
|
92
|
+
`);
|
|
93
|
+
|
|
94
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
95
|
+
cmdContext(memDir);
|
|
96
|
+
|
|
97
|
+
const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
98
|
+
expect(output).toContain('Context');
|
|
99
|
+
expect(output).toContain('Branch');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('shows goal in context', () => {
|
|
103
|
+
spawnSync.mockReturnValue({ status: 0, stdout: 'task/test\n', stderr: '' });
|
|
104
|
+
|
|
105
|
+
writeMemFile(memDir, 'goal.md', `# Goal
|
|
106
|
+
|
|
107
|
+
Test goal text
|
|
108
|
+
`);
|
|
109
|
+
writeMemFile(memDir, 'state.md', '---\nstatus: active\n---\n\n');
|
|
110
|
+
|
|
111
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
112
|
+
cmdContext(memDir);
|
|
113
|
+
|
|
114
|
+
const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
115
|
+
expect(output).toContain('Goal');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('shows state in context', () => {
|
|
119
|
+
spawnSync.mockReturnValue({ status: 0, stdout: 'task/test\n', stderr: '' });
|
|
120
|
+
|
|
121
|
+
writeMemFile(memDir, 'goal.md', '# Goal\n\nTest');
|
|
122
|
+
writeMemFile(memDir, 'state.md', `---
|
|
123
|
+
status: active
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Next Step
|
|
127
|
+
|
|
128
|
+
Do something
|
|
129
|
+
`);
|
|
130
|
+
|
|
131
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
132
|
+
cmdContext(memDir);
|
|
133
|
+
|
|
134
|
+
const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
135
|
+
expect(output).toContain('State');
|
|
136
|
+
expect(output).toContain('Do something');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('shows learnings in context', () => {
|
|
140
|
+
spawnSync.mockReturnValue({ status: 0, stdout: 'task/test\n', stderr: '' });
|
|
141
|
+
|
|
142
|
+
writeMemFile(memDir, 'goal.md', '# Goal\n\nTest');
|
|
143
|
+
writeMemFile(memDir, 'state.md', '---\nstatus: active\n---\n\n');
|
|
144
|
+
writeMemFile(memDir, 'memory.md', `# Learnings
|
|
145
|
+
|
|
146
|
+
- 2024-01-01: Important insight
|
|
147
|
+
`);
|
|
148
|
+
|
|
149
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
150
|
+
cmdContext(memDir);
|
|
151
|
+
|
|
152
|
+
const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
153
|
+
expect(output).toContain('Learnings');
|
|
154
|
+
expect(output).toContain('Important insight');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('shows playbook in context', () => {
|
|
158
|
+
spawnSync.mockReturnValue({ status: 0, stdout: 'task/test\n', stderr: '' });
|
|
159
|
+
|
|
160
|
+
writeMemFile(memDir, 'goal.md', '# Goal\n\nTest');
|
|
161
|
+
writeMemFile(memDir, 'state.md', '---\nstatus: active\n---\n\n');
|
|
162
|
+
writeMemFile(memDir, 'playbook.md', `# Playbook
|
|
163
|
+
|
|
164
|
+
- Global learning
|
|
165
|
+
`);
|
|
166
|
+
|
|
167
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
168
|
+
cmdContext(memDir);
|
|
169
|
+
|
|
170
|
+
const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
171
|
+
expect(output).toContain('Playbook');
|
|
172
|
+
expect(output).toContain('Global learning');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('handles missing files gracefully', () => {
|
|
176
|
+
spawnSync.mockReturnValue({ status: 0, stdout: 'task/test\n', stderr: '' });
|
|
177
|
+
|
|
178
|
+
// Don't create any files
|
|
179
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
180
|
+
cmdContext(memDir);
|
|
181
|
+
|
|
182
|
+
// Should not throw
|
|
183
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Additional tests for code 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
|
+
loadConfig,
|
|
27
|
+
saveConfig,
|
|
28
|
+
loadIndex,
|
|
29
|
+
saveIndex,
|
|
30
|
+
findMemDir,
|
|
31
|
+
cmdInit,
|
|
32
|
+
cmdStatus,
|
|
33
|
+
cmdGoal,
|
|
34
|
+
cmdNext,
|
|
35
|
+
cmdCheckpoint,
|
|
36
|
+
cmdStuck,
|
|
37
|
+
cmdPlaybook,
|
|
38
|
+
cmdSet,
|
|
39
|
+
cmdGet,
|
|
40
|
+
cmdAppend,
|
|
41
|
+
writeMemFile,
|
|
42
|
+
readMemFile,
|
|
43
|
+
parseFrontmatter,
|
|
44
|
+
serializeFrontmatter,
|
|
45
|
+
CONFIG_DIR,
|
|
46
|
+
CONFIG_FILE,
|
|
47
|
+
CENTRAL_MEM,
|
|
48
|
+
INDEX_FILE,
|
|
49
|
+
} = require('../index.js');
|
|
50
|
+
|
|
51
|
+
const { spawnSync, execSync } = require('child_process');
|
|
52
|
+
|
|
53
|
+
const testDir = path.join(os.tmpdir(), 'memx-test-coverage-' + Date.now());
|
|
54
|
+
let memDir;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
memDir = path.join(testDir, '.mem');
|
|
58
|
+
fs.mkdirSync(memDir, { recursive: true });
|
|
59
|
+
fs.mkdirSync(path.join(memDir, '.git'), { recursive: true });
|
|
60
|
+
spawnSync.mockClear();
|
|
61
|
+
execSync.mockClear();
|
|
62
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
if (fs.existsSync(testDir)) {
|
|
67
|
+
fs.rmSync(testDir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
jest.restoreAllMocks();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('Config functions', () => {
|
|
73
|
+
test('loadConfig with valid JSON file', () => {
|
|
74
|
+
// Create a mock config file
|
|
75
|
+
const configPath = path.join(testDir, 'config.json');
|
|
76
|
+
const mockDir = path.dirname(configPath);
|
|
77
|
+
fs.mkdirSync(mockDir, { recursive: true });
|
|
78
|
+
|
|
79
|
+
// Config functions use fixed paths, test that function works
|
|
80
|
+
const config = loadConfig();
|
|
81
|
+
expect(typeof config).toBe('object');
|
|
82
|
+
expect(config).toHaveProperty('repos');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('loadConfig returns object with repos', () => {
|
|
86
|
+
const config = loadConfig();
|
|
87
|
+
expect(typeof config).toBe('object');
|
|
88
|
+
expect(config).toHaveProperty('repos');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('saveConfig creates directory and file', () => {
|
|
92
|
+
// This test verifies the function doesn't throw
|
|
93
|
+
expect(() => saveConfig({ repos: {} })).not.toThrow();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('Index functions', () => {
|
|
98
|
+
test('loadIndex returns empty object for missing file', () => {
|
|
99
|
+
const index = loadIndex();
|
|
100
|
+
expect(typeof index).toBe('object');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('saveIndex and loadIndex roundtrip', () => {
|
|
104
|
+
// Save original index, modify, then restore
|
|
105
|
+
const original = loadIndex();
|
|
106
|
+
const testKey = '/memx-test-path-' + Date.now();
|
|
107
|
+
const testIndex = { ...original, [testKey]: 'task/test' };
|
|
108
|
+
saveIndex(testIndex);
|
|
109
|
+
const loaded = loadIndex();
|
|
110
|
+
expect(loaded[testKey]).toBe('task/test');
|
|
111
|
+
// Restore original
|
|
112
|
+
saveIndex(original);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('findMemDir', () => {
|
|
117
|
+
test('finds local .mem with git', () => {
|
|
118
|
+
const localMemDir = path.join(testDir, 'project', '.mem');
|
|
119
|
+
fs.mkdirSync(path.join(localMemDir, '.git'), { recursive: true });
|
|
120
|
+
|
|
121
|
+
const result = findMemDir(path.join(testDir, 'project'));
|
|
122
|
+
expect(result).not.toBeNull();
|
|
123
|
+
expect(result.isLocal).toBe(true);
|
|
124
|
+
expect(result.memDir).toBe(localMemDir);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('finds parent .mem directory', () => {
|
|
128
|
+
const parentMem = path.join(testDir, 'parent', '.mem');
|
|
129
|
+
const childDir = path.join(testDir, 'parent', 'child', 'subdir');
|
|
130
|
+
fs.mkdirSync(path.join(parentMem, '.git'), { recursive: true });
|
|
131
|
+
fs.mkdirSync(childDir, { recursive: true });
|
|
132
|
+
|
|
133
|
+
const result = findMemDir(childDir);
|
|
134
|
+
expect(result).not.toBeNull();
|
|
135
|
+
expect(result.isLocal).toBe(true);
|
|
136
|
+
expect(result.memDir).toBe(parentMem);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('cmdInit edge cases', () => {
|
|
141
|
+
test('cmdInit with existing .mem warns', async () => {
|
|
142
|
+
// Create a local .mem in current testDir
|
|
143
|
+
const localMem = path.join(testDir, 'existing', '.mem');
|
|
144
|
+
fs.mkdirSync(path.join(localMem, '.git'), { recursive: true });
|
|
145
|
+
|
|
146
|
+
const originalCwd = process.cwd;
|
|
147
|
+
process.cwd = jest.fn(() => path.join(testDir, 'existing'));
|
|
148
|
+
|
|
149
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
150
|
+
|
|
151
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
152
|
+
await cmdInit(['test-task'], localMem);
|
|
153
|
+
|
|
154
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
155
|
+
|
|
156
|
+
process.cwd = originalCwd;
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('cmdStatus edge cases', () => {
|
|
161
|
+
test('handles missing goal.md', () => {
|
|
162
|
+
spawnSync.mockReturnValue({ status: 0, stdout: 'task/test', stderr: '' });
|
|
163
|
+
writeMemFile(memDir, 'state.md', '---\nstatus: active\n---\n\n');
|
|
164
|
+
|
|
165
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
166
|
+
cmdStatus(memDir);
|
|
167
|
+
|
|
168
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('handles missing state.md', () => {
|
|
172
|
+
spawnSync.mockReturnValue({ status: 0, stdout: 'task/test', stderr: '' });
|
|
173
|
+
writeMemFile(memDir, 'goal.md', '---\ntask: test\n---\n\n# Goal\n\nTest');
|
|
174
|
+
|
|
175
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
176
|
+
cmdStatus(memDir);
|
|
177
|
+
|
|
178
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('cmdGoal edge cases', () => {
|
|
183
|
+
test('sets goal with text', () => {
|
|
184
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
185
|
+
writeMemFile(memDir, 'goal.md', '---\ntask: test\n---\n\n# Goal\n\nOld goal');
|
|
186
|
+
|
|
187
|
+
cmdGoal(['New', 'goal', 'text'], memDir);
|
|
188
|
+
|
|
189
|
+
const content = readMemFile(memDir, 'goal.md');
|
|
190
|
+
expect(content).toContain('New goal text');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('creates goal.md if not exists', () => {
|
|
194
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
195
|
+
|
|
196
|
+
cmdGoal(['First', 'goal'], memDir);
|
|
197
|
+
|
|
198
|
+
const content = readMemFile(memDir, 'goal.md');
|
|
199
|
+
expect(content).toContain('First goal');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('cmdStuck edge cases', () => {
|
|
204
|
+
test('sets new blocker', () => {
|
|
205
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
206
|
+
writeMemFile(memDir, 'state.md', '---\nstatus: active\n---\n\n# State');
|
|
207
|
+
|
|
208
|
+
cmdStuck(['Waiting', 'for', 'API'], memDir);
|
|
209
|
+
|
|
210
|
+
const content = readMemFile(memDir, 'state.md');
|
|
211
|
+
expect(content).toContain('status: blocked');
|
|
212
|
+
expect(content).toContain('blocker: Waiting for API');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('clears blocker with clear command', () => {
|
|
216
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
217
|
+
writeMemFile(memDir, 'state.md', '---\nstatus: blocked\nblocker: Old blocker\n---\n\n');
|
|
218
|
+
|
|
219
|
+
cmdStuck(['clear'], memDir);
|
|
220
|
+
|
|
221
|
+
const content = readMemFile(memDir, 'state.md');
|
|
222
|
+
expect(content).toContain('status: active');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('cmdPlaybook edge cases', () => {
|
|
227
|
+
test('shows no playbook message', () => {
|
|
228
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
229
|
+
cmdPlaybook(memDir);
|
|
230
|
+
|
|
231
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('No playbook');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('shows playbook content', () => {
|
|
235
|
+
writeMemFile(memDir, 'playbook.md', '# Playbook\n\n- Rule 1\n- Rule 2');
|
|
236
|
+
|
|
237
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
238
|
+
cmdPlaybook(memDir);
|
|
239
|
+
|
|
240
|
+
const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
241
|
+
expect(output).toContain('Playbook');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('cmdSet edge cases', () => {
|
|
246
|
+
test('shows usage when no args', () => {
|
|
247
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
248
|
+
cmdSet([], memDir);
|
|
249
|
+
|
|
250
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('sets value in frontmatter', () => {
|
|
254
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
255
|
+
writeMemFile(memDir, 'state.md', '---\nstatus: active\n---\n\n# State');
|
|
256
|
+
|
|
257
|
+
cmdSet(['priority', 'high'], memDir);
|
|
258
|
+
|
|
259
|
+
const content = readMemFile(memDir, 'state.md');
|
|
260
|
+
expect(content).toContain('priority: high');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('handles file not found', () => {
|
|
264
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
265
|
+
|
|
266
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
267
|
+
cmdSet(['key', 'value'], memDir);
|
|
268
|
+
|
|
269
|
+
// Should create file or show error
|
|
270
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('cmdGet edge cases', () => {
|
|
275
|
+
test('shows usage when no key', () => {
|
|
276
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
277
|
+
cmdGet([], memDir);
|
|
278
|
+
|
|
279
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('gets value from frontmatter', () => {
|
|
283
|
+
writeMemFile(memDir, 'state.md', '---\nstatus: active\npriority: high\n---\n\n');
|
|
284
|
+
|
|
285
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
286
|
+
cmdGet(['priority'], memDir);
|
|
287
|
+
|
|
288
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('high');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('shows not set for missing key', () => {
|
|
292
|
+
writeMemFile(memDir, 'state.md', '---\nstatus: active\n---\n\n');
|
|
293
|
+
|
|
294
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
295
|
+
cmdGet(['nonexistent'], memDir);
|
|
296
|
+
|
|
297
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('Not set');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('cmdAppend edge cases', () => {
|
|
302
|
+
test('shows usage when no section', () => {
|
|
303
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
304
|
+
cmdAppend([], memDir);
|
|
305
|
+
|
|
306
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('Usage');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('appends to learnings section', () => {
|
|
310
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
311
|
+
writeMemFile(memDir, 'memory.md', '# Learnings\n\n- First');
|
|
312
|
+
|
|
313
|
+
cmdAppend(['learnings', 'Second', 'item'], memDir);
|
|
314
|
+
|
|
315
|
+
const content = readMemFile(memDir, 'memory.md');
|
|
316
|
+
expect(content).toContain('Second item');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('shows error for unknown list', () => {
|
|
320
|
+
const consoleSpy = jest.spyOn(console, 'log');
|
|
321
|
+
cmdAppend(['unknown', 'item'], memDir);
|
|
322
|
+
|
|
323
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('Unknown list');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('appends to playbook', () => {
|
|
327
|
+
spawnSync.mockReturnValue({ status: 0, stdout: '', stderr: '' });
|
|
328
|
+
writeMemFile(memDir, 'playbook.md', '# Playbook\n\n- First rule');
|
|
329
|
+
|
|
330
|
+
cmdAppend(['playbook', 'New', 'rule'], memDir);
|
|
331
|
+
|
|
332
|
+
const content = readMemFile(memDir, 'playbook.md');
|
|
333
|
+
expect(content).toContain('New rule');
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('serializeFrontmatter edge cases', () => {
|
|
338
|
+
test('handles empty frontmatter', () => {
|
|
339
|
+
const result = serializeFrontmatter({}, 'Body text');
|
|
340
|
+
// Empty frontmatter still outputs the delimiters
|
|
341
|
+
expect(result).toContain('---');
|
|
342
|
+
expect(result).toContain('Body text');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('handles frontmatter with special characters', () => {
|
|
346
|
+
const result = serializeFrontmatter({ title: 'Test: with colon' }, 'Body');
|
|
347
|
+
expect(result).toContain('title:');
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe('parseFrontmatter edge cases', () => {
|
|
352
|
+
test('handles frontmatter with nested structure', () => {
|
|
353
|
+
const content = '---\nkey: value\nnested:\n sub: item\n---\n\nBody';
|
|
354
|
+
const result = parseFrontmatter(content);
|
|
355
|
+
expect(result.frontmatter.key).toBe('value');
|
|
356
|
+
expect(result.body).toBe('Body');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('handles multiline body', () => {
|
|
360
|
+
const content = '---\nstatus: active\n---\n\nLine 1\nLine 2\nLine 3';
|
|
361
|
+
const result = parseFrontmatter(content);
|
|
362
|
+
expect(result.body).toContain('Line 1');
|
|
363
|
+
expect(result.body).toContain('Line 2');
|
|
364
|
+
expect(result.body).toContain('Line 3');
|
|
365
|
+
});
|
|
366
|
+
});
|