@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.
- package/.github/workflows/test-and-publish.yml +96 -0
- package/index.js +8 -2
- package/lib/welcome.js +32 -4
- package/package.json +16 -4
- package/tests/fixtures/expired-auth.json +20 -0
- package/tests/fixtures/mock-auth.json +21 -0
- package/tests/fixtures/mock-project-config.json +12 -0
- package/tests/mocks/api.mock.js +200 -0
- package/tests/mocks/token-storage.mock.js +79 -0
- package/tests/setup.js +29 -0
- package/tests/unit/config.test.js +165 -0
- package/tests/unit/jwt-utils.test.js +217 -0
- package/tests/unit/mcp-proxy.test.js +459 -0
- package/tests/unit/provider-detector.test.js +344 -0
- package/tests/unit/token-storage.test.js +138 -0
- package/vitest.config.js +37 -0
|
@@ -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
|
+
});
|
package/vitest.config.js
ADDED
|
@@ -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
|
+
});
|