@unifiedmemory/cli 1.3.1 → 1.3.7

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.
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Unit tests for lib/provider-detector.js
3
+ *
4
+ * Tests AI code assistant provider detection and configuration.
5
+ * Uses temporary directories to simulate different provider installations.
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
9
+ import fs from 'fs-extra';
10
+ import path from 'path';
11
+ import os from 'os';
12
+
13
+ // Create temp directories for testing
14
+ const TEST_HOME = path.join(os.tmpdir(), `um-cli-test-home-${Date.now()}`);
15
+ const TEST_PROJECT = path.join(os.tmpdir(), `um-cli-test-project-${Date.now()}`);
16
+
17
+ describe('provider-detector', () => {
18
+ beforeEach(async () => {
19
+ // Create test directories
20
+ await fs.ensureDir(TEST_HOME);
21
+ await fs.ensureDir(TEST_PROJECT);
22
+
23
+ // Reset modules for fresh imports
24
+ vi.resetModules();
25
+ });
26
+
27
+ afterEach(async () => {
28
+ // Clean up test directories
29
+ await fs.remove(TEST_HOME);
30
+ await fs.remove(TEST_PROJECT);
31
+ vi.restoreAllMocks();
32
+ });
33
+
34
+ describe('ProviderDetector', () => {
35
+ it('should export ProviderDetector class', async () => {
36
+ const { ProviderDetector } = await import('../../lib/provider-detector.js');
37
+
38
+ expect(ProviderDetector).toBeDefined();
39
+ expect(typeof ProviderDetector).toBe('function');
40
+ });
41
+
42
+ it('should create detector with project directory', async () => {
43
+ const { ProviderDetector } = await import('../../lib/provider-detector.js');
44
+
45
+ const detector = new ProviderDetector(TEST_PROJECT);
46
+
47
+ expect(detector).toBeDefined();
48
+ expect(detector.providers).toBeDefined();
49
+ expect(Array.isArray(detector.providers)).toBe(true);
50
+ });
51
+
52
+ it('should detect no providers when directories do not exist', async () => {
53
+ // Mock homedir to use our empty test directory
54
+ vi.spyOn(os, 'homedir').mockReturnValue(TEST_HOME);
55
+
56
+ // Fresh import after mock
57
+ vi.resetModules();
58
+ const { ProviderDetector } = await import('../../lib/provider-detector.js');
59
+
60
+ const detector = new ProviderDetector(TEST_PROJECT);
61
+ const detected = detector.detectAll();
62
+
63
+ // Claude Code always detects as true (project-level config)
64
+ const nonClaudeDetected = detected.filter(p => p.name !== 'Claude Code');
65
+ expect(nonClaudeDetected.length).toBe(0);
66
+ });
67
+
68
+ it('should detect Cursor when .cursor directory exists', async () => {
69
+ // Create Cursor directory
70
+ const cursorDir = path.join(TEST_HOME, '.cursor');
71
+ await fs.ensureDir(cursorDir);
72
+
73
+ // Mock homedir to use our test directory
74
+ vi.spyOn(os, 'homedir').mockReturnValue(TEST_HOME);
75
+
76
+ // Fresh import after mock
77
+ vi.resetModules();
78
+ const { ProviderDetector } = await import('../../lib/provider-detector.js');
79
+
80
+ const detector = new ProviderDetector(TEST_PROJECT);
81
+ const cursorProvider = detector.getByName('Cursor');
82
+
83
+ expect(cursorProvider).toBeDefined();
84
+ expect(cursorProvider.detect()).toBe(true);
85
+ });
86
+
87
+ it('should detect Cline when .cline directory exists', async () => {
88
+ // Create Cline directory
89
+ const clineDir = path.join(TEST_HOME, '.cline');
90
+ await fs.ensureDir(clineDir);
91
+
92
+ vi.spyOn(os, 'homedir').mockReturnValue(TEST_HOME);
93
+ vi.resetModules();
94
+ const { ProviderDetector } = await import('../../lib/provider-detector.js');
95
+
96
+ const detector = new ProviderDetector(TEST_PROJECT);
97
+ const clineProvider = detector.getByName('Cline');
98
+
99
+ expect(clineProvider).toBeDefined();
100
+ expect(clineProvider.detect()).toBe(true);
101
+ });
102
+
103
+ it('should detect Codex CLI when .codex directory exists', async () => {
104
+ // Create Codex directory
105
+ const codexDir = path.join(TEST_HOME, '.codex');
106
+ await fs.ensureDir(codexDir);
107
+
108
+ vi.spyOn(os, 'homedir').mockReturnValue(TEST_HOME);
109
+ vi.resetModules();
110
+ const { ProviderDetector } = await import('../../lib/provider-detector.js');
111
+
112
+ const detector = new ProviderDetector(TEST_PROJECT);
113
+ const codexProvider = detector.getByName('Codex CLI');
114
+
115
+ expect(codexProvider).toBeDefined();
116
+ expect(codexProvider.detect()).toBe(true);
117
+ });
118
+
119
+ it('should detect Gemini CLI when .gemini directory exists', async () => {
120
+ // Create Gemini directory
121
+ const geminiDir = path.join(TEST_HOME, '.gemini');
122
+ await fs.ensureDir(geminiDir);
123
+
124
+ vi.spyOn(os, 'homedir').mockReturnValue(TEST_HOME);
125
+ vi.resetModules();
126
+ const { ProviderDetector } = await import('../../lib/provider-detector.js');
127
+
128
+ const detector = new ProviderDetector(TEST_PROJECT);
129
+ const geminiProvider = detector.getByName('Gemini CLI');
130
+
131
+ expect(geminiProvider).toBeDefined();
132
+ expect(geminiProvider.detect()).toBe(true);
133
+ });
134
+
135
+ it('should find provider by name case-insensitively', async () => {
136
+ const { ProviderDetector } = await import('../../lib/provider-detector.js');
137
+
138
+ const detector = new ProviderDetector(TEST_PROJECT);
139
+
140
+ expect(detector.getByName('claude code')).toBeDefined();
141
+ expect(detector.getByName('CLAUDE CODE')).toBeDefined();
142
+ expect(detector.getByName('Claude Code')).toBeDefined();
143
+ });
144
+ });
145
+
146
+ describe('ClaudeProvider', () => {
147
+ it('should always detect as available (project-level config)', async () => {
148
+ const { ClaudeProvider } = await import('../../lib/provider-detector.js');
149
+
150
+ const provider = new ClaudeProvider(TEST_PROJECT);
151
+
152
+ expect(provider.detect()).toBe(true);
153
+ });
154
+
155
+ it('should configure MCP server in .mcp.json', async () => {
156
+ const { ClaudeProvider } = await import('../../lib/provider-detector.js');
157
+
158
+ const provider = new ClaudeProvider(TEST_PROJECT);
159
+ const success = provider.configureMCP();
160
+
161
+ expect(success).toBe(true);
162
+
163
+ // Verify .mcp.json was created
164
+ const mcpConfigPath = path.join(TEST_PROJECT, '.mcp.json');
165
+ expect(fs.existsSync(mcpConfigPath)).toBe(true);
166
+
167
+ const config = fs.readJSONSync(mcpConfigPath);
168
+ expect(config.mcpServers.unifiedmemory).toEqual({
169
+ command: 'um',
170
+ args: ['mcp', 'serve'],
171
+ });
172
+ });
173
+
174
+ it('should update Claude settings with tool permissions', async () => {
175
+ const { ClaudeProvider } = await import('../../lib/provider-detector.js');
176
+
177
+ const provider = new ClaudeProvider(TEST_PROJECT);
178
+ const permissions = ['mcp__unifiedmemory__create_note', 'mcp__unifiedmemory__search_notes'];
179
+
180
+ provider.configureMCP(permissions);
181
+
182
+ // Verify settings were created
183
+ const settingsPath = path.join(TEST_PROJECT, '.claude', 'settings.local.json');
184
+ expect(fs.existsSync(settingsPath)).toBe(true);
185
+
186
+ const settings = fs.readJSONSync(settingsPath);
187
+ expect(settings.enableAllProjectMcpServers).toBe(true);
188
+ expect(settings.enabledMcpjsonServers).toContain('unifiedmemory');
189
+ expect(settings.permissions.allow).toContain('mcp__unifiedmemory__create_note');
190
+ expect(settings.permissions.allow).toContain('mcp__unifiedmemory__search_notes');
191
+ });
192
+
193
+ it('should merge with existing Claude settings', async () => {
194
+ const { ClaudeProvider } = await import('../../lib/provider-detector.js');
195
+
196
+ // Create existing settings
197
+ const claudeDir = path.join(TEST_PROJECT, '.claude');
198
+ await fs.ensureDir(claudeDir);
199
+ const settingsPath = path.join(claudeDir, 'settings.local.json');
200
+ fs.writeJSONSync(settingsPath, {
201
+ existingSetting: true,
202
+ permissions: {
203
+ allow: ['existing_permission'],
204
+ },
205
+ });
206
+
207
+ const provider = new ClaudeProvider(TEST_PROJECT);
208
+ provider.configureMCP(['mcp__unifiedmemory__new_tool']);
209
+
210
+ const settings = fs.readJSONSync(settingsPath);
211
+ expect(settings.existingSetting).toBe(true);
212
+ expect(settings.permissions.allow).toContain('existing_permission');
213
+ expect(settings.permissions.allow).toContain('mcp__unifiedmemory__new_tool');
214
+ });
215
+ });
216
+
217
+ describe('CursorProvider', () => {
218
+ it('should configure MCP in ~/.cursor/mcp.json', async () => {
219
+ vi.spyOn(os, 'homedir').mockReturnValue(TEST_HOME);
220
+
221
+ // Create Cursor directory
222
+ const cursorDir = path.join(TEST_HOME, '.cursor');
223
+ await fs.ensureDir(cursorDir);
224
+
225
+ vi.resetModules();
226
+ const { CursorProvider } = await import('../../lib/provider-detector.js');
227
+
228
+ const provider = new CursorProvider();
229
+ const success = provider.configureMCP();
230
+
231
+ expect(success).toBe(true);
232
+
233
+ const mcpConfigPath = path.join(cursorDir, 'mcp.json');
234
+ expect(fs.existsSync(mcpConfigPath)).toBe(true);
235
+
236
+ const config = fs.readJSONSync(mcpConfigPath);
237
+ expect(config.mcpServers.unifiedmemory).toEqual({
238
+ command: 'um',
239
+ args: ['mcp', 'serve'],
240
+ });
241
+ });
242
+ });
243
+
244
+ describe('CodexProvider', () => {
245
+ it('should configure MCP in TOML format', async () => {
246
+ vi.spyOn(os, 'homedir').mockReturnValue(TEST_HOME);
247
+
248
+ // Create Codex directory
249
+ const codexDir = path.join(TEST_HOME, '.codex');
250
+ await fs.ensureDir(codexDir);
251
+
252
+ vi.resetModules();
253
+ const { CodexProvider } = await import('../../lib/provider-detector.js');
254
+
255
+ const provider = new CodexProvider();
256
+ const success = provider.configureMCP();
257
+
258
+ expect(success).toBe(true);
259
+
260
+ const configPath = path.join(codexDir, 'config.toml');
261
+ expect(fs.existsSync(configPath)).toBe(true);
262
+
263
+ const content = fs.readFileSync(configPath, 'utf8');
264
+ // TOML should contain the MCP server config
265
+ expect(content).toContain('[mcp_servers.unifiedmemory]');
266
+ expect(content).toContain('command = "um"');
267
+ });
268
+
269
+ it('should read existing TOML config', async () => {
270
+ vi.spyOn(os, 'homedir').mockReturnValue(TEST_HOME);
271
+
272
+ const codexDir = path.join(TEST_HOME, '.codex');
273
+ await fs.ensureDir(codexDir);
274
+
275
+ // Write existing TOML config
276
+ const existingToml = `
277
+ [existing_setting]
278
+ value = true
279
+
280
+ [mcp_servers.other_server]
281
+ command = "other"
282
+ `;
283
+ fs.writeFileSync(path.join(codexDir, 'config.toml'), existingToml);
284
+
285
+ vi.resetModules();
286
+ const { CodexProvider } = await import('../../lib/provider-detector.js');
287
+
288
+ const provider = new CodexProvider();
289
+ const config = provider.readConfig();
290
+
291
+ expect(config.existing_setting.value).toBe(true);
292
+ expect(config.mcp_servers.other_server.command).toBe('other');
293
+ });
294
+ });
295
+
296
+ describe('Memory Instructions', () => {
297
+ it('should create CLAUDE.md with memory instructions', async () => {
298
+ const { ClaudeProvider } = await import('../../lib/provider-detector.js');
299
+
300
+ const provider = new ClaudeProvider(TEST_PROJECT);
301
+ const result = provider.configureMemoryInstructions();
302
+
303
+ expect(result.status).toBe('created');
304
+
305
+ const claudeMdPath = path.join(TEST_PROJECT, 'CLAUDE.md');
306
+ expect(fs.existsSync(claudeMdPath)).toBe(true);
307
+
308
+ const content = fs.readFileSync(claudeMdPath, 'utf8');
309
+ expect(content).toContain('UnifiedMemory');
310
+ });
311
+
312
+ it('should skip if memory instructions already present', async () => {
313
+ const { ClaudeProvider } = await import('../../lib/provider-detector.js');
314
+
315
+ const provider = new ClaudeProvider(TEST_PROJECT);
316
+
317
+ // First call creates
318
+ provider.configureMemoryInstructions();
319
+
320
+ // Second call should skip
321
+ const result = provider.configureMemoryInstructions();
322
+
323
+ expect(result.status).toBe('skipped');
324
+ expect(result.reason).toBe('already_present');
325
+ });
326
+
327
+ it('should append to existing file without marker', async () => {
328
+ const { ClaudeProvider } = await import('../../lib/provider-detector.js');
329
+
330
+ // Create existing CLAUDE.md without marker
331
+ const claudeMdPath = path.join(TEST_PROJECT, 'CLAUDE.md');
332
+ fs.writeFileSync(claudeMdPath, '# Existing Content\n\nSome existing instructions.');
333
+
334
+ const provider = new ClaudeProvider(TEST_PROJECT);
335
+ const result = provider.configureMemoryInstructions();
336
+
337
+ expect(result.status).toBe('appended');
338
+
339
+ const content = fs.readFileSync(claudeMdPath, 'utf8');
340
+ expect(content).toContain('# Existing Content');
341
+ expect(content).toContain('UnifiedMemory');
342
+ });
343
+ });
344
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Unit tests for lib/token-storage.js
3
+ *
4
+ * Tests token storage mock factory and module interface.
5
+ *
6
+ * Note: Direct testing of token-storage.js filesystem operations is complex
7
+ * due to hardcoded paths using os.homedir(). Instead, we test:
8
+ * 1. The mock factory that simulates token storage behavior
9
+ * 2. The module exports (verifying the interface exists)
10
+ */
11
+
12
+ import { describe, it, expect, vi } from 'vitest';
13
+
14
+ // Test the mock factory from our test utilities
15
+ describe('token-storage mock', () => {
16
+ it('should provide mock implementations that work correctly', async () => {
17
+ const { createTokenStorageMock, mockAuthData } = await import('../mocks/token-storage.mock.js');
18
+
19
+ const mock = createTokenStorageMock();
20
+
21
+ // getToken returns initial mock data
22
+ expect(mock.getToken()).toEqual(mockAuthData);
23
+
24
+ // saveToken updates the data
25
+ const newData = { accessToken: 'new_token', decoded: { sub: 'user_new' } };
26
+ mock.saveToken(newData);
27
+ expect(mock.getToken()).toEqual(newData);
28
+
29
+ // clearToken removes the data
30
+ mock.clearToken();
31
+ expect(mock.getToken()).toBeNull();
32
+ });
33
+
34
+ it('should provide empty token storage mock', async () => {
35
+ const { createEmptyTokenStorageMock } = await import('../mocks/token-storage.mock.js');
36
+
37
+ const mock = createEmptyTokenStorageMock();
38
+
39
+ expect(mock.getToken()).toBeNull();
40
+ });
41
+
42
+ it('should provide expired token storage mock', async () => {
43
+ const { createExpiredTokenStorageMock, expiredAuthData } = await import('../mocks/token-storage.mock.js');
44
+
45
+ const mock = createExpiredTokenStorageMock();
46
+
47
+ const token = mock.getToken();
48
+ expect(token).toEqual(expiredAuthData);
49
+
50
+ // Verify the token is actually expired (exp is in the past)
51
+ const now = Math.floor(Date.now() / 1000);
52
+ expect(token.decoded.exp).toBeLessThan(now);
53
+ });
54
+
55
+ it('should track saveToken calls', async () => {
56
+ const { createTokenStorageMock } = await import('../mocks/token-storage.mock.js');
57
+
58
+ const mock = createTokenStorageMock();
59
+
60
+ mock.saveToken({ accessToken: 'token1' });
61
+ mock.saveToken({ accessToken: 'token2' });
62
+
63
+ expect(mock.saveToken).toHaveBeenCalledTimes(2);
64
+ expect(mock.saveToken).toHaveBeenLastCalledWith({ accessToken: 'token2' });
65
+ });
66
+
67
+ it('should handle updateSelectedOrg correctly', async () => {
68
+ const { createTokenStorageMock } = await import('../mocks/token-storage.mock.js');
69
+
70
+ const mock = createTokenStorageMock();
71
+
72
+ const newOrg = { id: 'org_new', name: 'New Org', role: 'member' };
73
+ mock.updateSelectedOrg(newOrg);
74
+
75
+ expect(mock.updateSelectedOrg).toHaveBeenCalledWith(newOrg);
76
+ expect(mock.getToken().selectedOrg).toEqual(newOrg);
77
+ });
78
+
79
+ it('should throw error when updating org with no token', async () => {
80
+ const { createEmptyTokenStorageMock } = await import('../mocks/token-storage.mock.js');
81
+
82
+ const mock = createEmptyTokenStorageMock();
83
+
84
+ expect(() => mock.updateSelectedOrg({ id: 'org_123' })).toThrow('No token found');
85
+ });
86
+
87
+ it('should allow resetting state via _reset helper', async () => {
88
+ const { createTokenStorageMock, mockAuthData } = await import('../mocks/token-storage.mock.js');
89
+
90
+ const mock = createTokenStorageMock();
91
+
92
+ // Clear token
93
+ mock.clearToken();
94
+ expect(mock.getToken()).toBeNull();
95
+
96
+ // Reset to default
97
+ mock._reset();
98
+ expect(mock.getToken()).toEqual(mockAuthData);
99
+
100
+ // Reset to custom data
101
+ const customData = { accessToken: 'custom' };
102
+ mock._reset(customData);
103
+ expect(mock.getToken()).toEqual(customData);
104
+ });
105
+
106
+ it('should provide getSelectedOrg that returns org from token', async () => {
107
+ const { createTokenStorageMock, mockAuthData } = await import('../mocks/token-storage.mock.js');
108
+
109
+ const mock = createTokenStorageMock();
110
+
111
+ expect(mock.getSelectedOrg()).toEqual(mockAuthData.selectedOrg);
112
+ });
113
+
114
+ it('should return null from getSelectedOrg when no token', async () => {
115
+ const { createEmptyTokenStorageMock } = await import('../mocks/token-storage.mock.js');
116
+
117
+ const mock = createEmptyTokenStorageMock();
118
+
119
+ expect(mock.getSelectedOrg()).toBeNull();
120
+ });
121
+ });
122
+
123
+ // Direct tests for the actual token-storage module behavior
124
+ describe('token-storage module behavior', () => {
125
+ // These tests verify the actual module behavior by directly testing
126
+ // specific aspects that don't require mocking the path
127
+
128
+ it('should export all required functions', async () => {
129
+ vi.resetModules();
130
+ const ts = await import('../../lib/token-storage.js');
131
+
132
+ expect(typeof ts.saveToken).toBe('function');
133
+ expect(typeof ts.getToken).toBe('function');
134
+ expect(typeof ts.clearToken).toBe('function');
135
+ expect(typeof ts.updateSelectedOrg).toBe('function');
136
+ expect(typeof ts.getSelectedOrg).toBe('function');
137
+ });
138
+ });
@@ -0,0 +1,37 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ // Use Node.js environment
6
+ environment: 'node',
7
+
8
+ // Global test setup
9
+ setupFiles: ['./tests/setup.js'],
10
+
11
+ // Test file patterns
12
+ include: ['tests/**/*.test.js'],
13
+
14
+ // Coverage configuration
15
+ coverage: {
16
+ provider: 'v8',
17
+ reporter: ['text', 'html', 'lcov'],
18
+ include: ['lib/**/*.js', 'commands/**/*.js'],
19
+ exclude: [
20
+ 'node_modules/**',
21
+ 'tests/**',
22
+ '**/*.test.js',
23
+ ],
24
+ },
25
+
26
+ // Timeout for tests (10 seconds)
27
+ testTimeout: 10000,
28
+
29
+ // Run tests in sequence for consistent filesystem state
30
+ sequence: {
31
+ shuffle: false,
32
+ },
33
+
34
+ // Disable watch mode in CI
35
+ watch: false,
36
+ },
37
+ });