onbuzz 3.6.1 → 3.6.2
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/package.json +1 -1
- package/src/__test-utils__/fixtures/malformedJson.js +31 -0
- package/src/__test-utils__/globalSetup.js +9 -0
- package/src/__test-utils__/globalTeardown.js +12 -0
- package/src/__test-utils__/mockFactories.js +101 -0
- package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -0
- package/src/analyzers/__tests__/ConfigValidator.test.js +362 -0
- package/src/analyzers/__tests__/ESLintAnalyzer.test.js +271 -0
- package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -0
- package/src/analyzers/__tests__/PrettierFormatter.test.js +197 -0
- package/src/analyzers/__tests__/PythonAnalyzer.test.js +208 -0
- package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -0
- package/src/analyzers/__tests__/SparrowAnalyzer.test.js +270 -0
- package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -0
- package/src/core/__tests__/agentPool.test.js +601 -0
- package/src/core/__tests__/agentScheduler.test.js +576 -0
- package/src/core/__tests__/contextManager.test.js +252 -0
- package/src/core/__tests__/flowExecutor.test.js +262 -0
- package/src/core/__tests__/messageProcessor.test.js +627 -0
- package/src/core/__tests__/orchestrator.test.js +257 -0
- package/src/core/__tests__/stateManager.test.js +375 -0
- package/src/core/agentPool.js +11 -1
- package/src/index.js +25 -9
- package/src/interfaces/terminal/__tests__/smoke/imports.test.js +3 -5
- package/src/services/__tests__/agentActivityService.test.js +319 -0
- package/src/services/__tests__/apiKeyManager.test.js +206 -0
- package/src/services/__tests__/benchmarkService.test.js +184 -0
- package/src/services/__tests__/budgetService.test.js +211 -0
- package/src/services/__tests__/contextInjectionService.test.js +205 -0
- package/src/services/__tests__/conversationCompactionService.test.js +280 -0
- package/src/services/__tests__/credentialVault.test.js +469 -0
- package/src/services/__tests__/errorHandler.test.js +314 -0
- package/src/services/__tests__/fileAttachmentService.test.js +278 -0
- package/src/services/__tests__/flowContextService.test.js +199 -0
- package/src/services/__tests__/memoryService.test.js +450 -0
- package/src/services/__tests__/modelRouterService.test.js +388 -0
- package/src/services/__tests__/modelsService.test.js +261 -0
- package/src/services/__tests__/portRegistry.test.js +123 -0
- package/src/services/__tests__/projectDetector.test.js +34 -0
- package/src/services/__tests__/promptService.test.js +242 -0
- package/src/services/__tests__/qualityInspector.test.js +97 -0
- package/src/services/__tests__/scheduleService.test.js +308 -0
- package/src/services/__tests__/serviceRegistry.test.js +74 -0
- package/src/services/__tests__/skillsService.test.js +402 -0
- package/src/services/__tests__/tokenCountingService.test.js +48 -0
- package/src/tools/__tests__/agentCommunicationTool.test.js +500 -0
- package/src/tools/__tests__/agentDelayTool.test.js +342 -0
- package/src/tools/__tests__/asyncToolManager.test.js +344 -0
- package/src/tools/__tests__/baseTool.test.js +420 -0
- package/src/tools/__tests__/codeMapTool.test.js +348 -0
- package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -0
- package/src/tools/__tests__/fileTreeTool.test.js +274 -0
- package/src/tools/__tests__/filesystemTool.test.js +717 -0
- package/src/tools/__tests__/helpTool.test.js +204 -0
- package/src/tools/__tests__/jobDoneTool.test.js +296 -0
- package/src/tools/__tests__/memoryTool.test.js +297 -0
- package/src/tools/__tests__/seekTool.test.js +282 -0
- package/src/tools/__tests__/skillsTool.test.js +226 -0
- package/src/tools/__tests__/staticAnalysisTool.test.js +509 -0
- package/src/tools/__tests__/taskManagerTool.test.js +725 -0
- package/src/tools/__tests__/terminalTool.test.js +384 -0
- package/src/tools/__tests__/userPromptTool.test.js +297 -0
- package/src/tools/__tests__/webTool.e2e.test.js +25 -11
- package/src/tools/webTool.js +6 -12
- package/src/types/__tests__/agent.test.js +499 -0
- package/src/types/__tests__/contextReference.test.js +606 -0
- package/src/types/__tests__/conversation.test.js +555 -0
- package/src/types/__tests__/toolCommand.test.js +584 -0
- package/src/types/contextReference.js +1 -1
- package/src/utilities/__tests__/attachmentValidator.test.js +80 -0
- package/src/utilities/__tests__/configManager.test.js +397 -0
- package/src/utilities/__tests__/constants.test.js +49 -0
- package/src/utilities/__tests__/directoryAccessManager.test.js +388 -0
- package/src/utilities/__tests__/fileProcessor.test.js +104 -0
- package/src/utilities/__tests__/jsonRepair.test.js +104 -0
- package/src/utilities/__tests__/logger.test.js +129 -0
- package/src/utilities/__tests__/platformUtils.test.js +87 -0
- package/src/utilities/__tests__/structuredFileValidator.test.js +263 -0
- package/src/utilities/__tests__/tagParser.test.js +887 -0
- package/src/utilities/__tests__/toolConstants.test.js +94 -0
- package/src/utilities/tagParser.js +2 -2
- package/src/tools/browserTool.js +0 -897
- package/src/utilities/platformUtils.test.js +0 -98
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { jest, describe, test, expect, beforeEach } from '@jest/globals';
|
|
2
|
+
import { createMockLogger } from '../../__test-utils__/mockFactories.js';
|
|
3
|
+
|
|
4
|
+
// Mock fs
|
|
5
|
+
const mockReadFile = jest.fn();
|
|
6
|
+
const mockWriteFile = jest.fn().mockResolvedValue(undefined);
|
|
7
|
+
jest.unstable_mockModule('fs', () => ({
|
|
8
|
+
promises: {
|
|
9
|
+
readFile: mockReadFile,
|
|
10
|
+
writeFile: mockWriteFile
|
|
11
|
+
}
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// Mock crypto with real-enough implementations
|
|
15
|
+
const mockRandomBytes = jest.fn().mockReturnValue(Buffer.from('deadbeef', 'hex'));
|
|
16
|
+
const mockPbkdf2Sync = jest.fn();
|
|
17
|
+
const mockCreateCipheriv = jest.fn();
|
|
18
|
+
const mockCreateDecipheriv = jest.fn();
|
|
19
|
+
jest.unstable_mockModule('crypto', () => ({
|
|
20
|
+
default: {
|
|
21
|
+
randomBytes: mockRandomBytes,
|
|
22
|
+
pbkdf2Sync: mockPbkdf2Sync,
|
|
23
|
+
createCipheriv: mockCreateCipheriv,
|
|
24
|
+
createDecipheriv: mockCreateDecipheriv
|
|
25
|
+
},
|
|
26
|
+
randomBytes: mockRandomBytes,
|
|
27
|
+
pbkdf2Sync: mockPbkdf2Sync,
|
|
28
|
+
createCipheriv: mockCreateCipheriv,
|
|
29
|
+
createDecipheriv: mockCreateDecipheriv
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
// Mock os
|
|
33
|
+
jest.unstable_mockModule('os', () => ({
|
|
34
|
+
default: {
|
|
35
|
+
hostname: () => 'test-host',
|
|
36
|
+
homedir: () => '/home/test',
|
|
37
|
+
userInfo: () => ({ username: 'testuser' })
|
|
38
|
+
}
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Mock userDataDir
|
|
42
|
+
const mockGetUserDataPaths = jest.fn().mockReturnValue({
|
|
43
|
+
agents: '/tmp/test-agents',
|
|
44
|
+
settings: '/tmp/test-settings'
|
|
45
|
+
});
|
|
46
|
+
const mockEnsureUserDataDirs = jest.fn().mockResolvedValue(undefined);
|
|
47
|
+
jest.unstable_mockModule('../../utilities/userDataDir.js', () => ({
|
|
48
|
+
getUserDataPaths: mockGetUserDataPaths,
|
|
49
|
+
ensureUserDataDirs: mockEnsureUserDataDirs
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// Mock stealthConstants
|
|
53
|
+
jest.unstable_mockModule('../../utilities/stealthConstants.js', () => ({
|
|
54
|
+
CREDENTIAL_CONFIG: {
|
|
55
|
+
ENCRYPTION_ALGORITHM: 'aes-256-gcm',
|
|
56
|
+
KEY_DERIVATION_ITERATIONS: 1000, // Low for testing
|
|
57
|
+
SALT_LENGTH: 32,
|
|
58
|
+
IV_LENGTH: 16,
|
|
59
|
+
AUTH_TAG_LENGTH: 16,
|
|
60
|
+
SESSION_EXPIRY_MS: 7 * 24 * 60 * 60 * 1000,
|
|
61
|
+
REQUEST_TIMEOUT_MS: 5 * 60 * 1000,
|
|
62
|
+
STORAGE: {
|
|
63
|
+
CREDENTIALS_FILE: 'credentials.enc',
|
|
64
|
+
SESSIONS_FILE: 'sessions.enc',
|
|
65
|
+
SETTINGS_DIR: 'settings'
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
KNOWN_SITES: {
|
|
69
|
+
linkedin: {
|
|
70
|
+
name: 'LinkedIn',
|
|
71
|
+
loginUrl: 'https://www.linkedin.com/login',
|
|
72
|
+
selectors: { username: '#username', password: '#password' },
|
|
73
|
+
usernameType: 'email',
|
|
74
|
+
multiStep: false
|
|
75
|
+
},
|
|
76
|
+
github: {
|
|
77
|
+
name: 'GitHub',
|
|
78
|
+
loginUrl: 'https://github.com/login',
|
|
79
|
+
selectors: { username: '#login_field', password: '#password' },
|
|
80
|
+
usernameType: 'username',
|
|
81
|
+
multiStep: false
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
CREDENTIAL_EVENTS: {
|
|
85
|
+
REQUEST: 'credential_request',
|
|
86
|
+
RESPONSE: 'credential_response',
|
|
87
|
+
CANCEL: 'credential_cancel',
|
|
88
|
+
STATUS: 'credential_status'
|
|
89
|
+
}
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
const { default: CredentialVault, getCredentialVault } = await import('../../services/credentialVault.js');
|
|
93
|
+
|
|
94
|
+
describe('CredentialVault', () => {
|
|
95
|
+
let vault;
|
|
96
|
+
let logger;
|
|
97
|
+
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
logger = createMockLogger();
|
|
100
|
+
vault = new CredentialVault(logger);
|
|
101
|
+
jest.clearAllMocks();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ── Constructor ──
|
|
105
|
+
test('constructor initializes with defaults', () => {
|
|
106
|
+
expect(vault.logger).toBe(logger);
|
|
107
|
+
expect(vault.credentials).toBeInstanceOf(Map);
|
|
108
|
+
expect(vault.sessions).toBeInstanceOf(Map);
|
|
109
|
+
expect(vault.pendingRequests).toBeInstanceOf(Map);
|
|
110
|
+
expect(vault.initialized).toBe(false);
|
|
111
|
+
expect(vault.encryptionKey).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('constructor works without logger', () => {
|
|
115
|
+
const v = new CredentialVault();
|
|
116
|
+
expect(v.logger).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── _getMachineIdentifier ──
|
|
120
|
+
test('_getMachineIdentifier returns consistent string', () => {
|
|
121
|
+
const id = vault._getMachineIdentifier();
|
|
122
|
+
expect(typeof id).toBe('string');
|
|
123
|
+
expect(id).toContain('loxia-credential-vault-v1');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ── _maskUsername ──
|
|
127
|
+
test('_maskUsername masks usernames', () => {
|
|
128
|
+
expect(vault._maskUsername('john@example.com')).toBe('j***om');
|
|
129
|
+
expect(vault._maskUsername('ab')).toBe('***');
|
|
130
|
+
expect(vault._maskUsername(null)).toBe('***');
|
|
131
|
+
expect(vault._maskUsername('')).toBe('***');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── Credential CRUD (without encryption) ──
|
|
135
|
+
test('saveCredentials stores credential entry', async () => {
|
|
136
|
+
// Don't persist - just test in-memory
|
|
137
|
+
vault.credentialsFile = null;
|
|
138
|
+
vault.encryptionKey = null;
|
|
139
|
+
|
|
140
|
+
await vault.saveCredentials('LinkedIn', {
|
|
141
|
+
username: 'user@test.com',
|
|
142
|
+
password: 'pass123'
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(vault.credentials.has('linkedin')).toBe(true);
|
|
146
|
+
const entry = vault.credentials.get('linkedin');
|
|
147
|
+
expect(entry.username).toBe('user@test.com');
|
|
148
|
+
expect(entry.name).toBe('LinkedIn');
|
|
149
|
+
expect(entry.siteId).toBe('linkedin');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('saveCredentials throws for missing fields', async () => {
|
|
153
|
+
await expect(vault.saveCredentials('test', { username: 'u' }))
|
|
154
|
+
.rejects.toThrow('siteId, username, and password are required');
|
|
155
|
+
await expect(vault.saveCredentials('', { username: 'u', password: 'p' }))
|
|
156
|
+
.rejects.toThrow();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('saveCredentials uses known site info', async () => {
|
|
160
|
+
vault.credentialsFile = null;
|
|
161
|
+
vault.encryptionKey = null;
|
|
162
|
+
|
|
163
|
+
await vault.saveCredentials('github', {
|
|
164
|
+
username: 'octocat',
|
|
165
|
+
password: 'pass'
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const entry = vault.credentials.get('github');
|
|
169
|
+
expect(entry.name).toBe('GitHub');
|
|
170
|
+
expect(entry.loginUrl).toBe('https://github.com/login');
|
|
171
|
+
expect(entry.usernameType).toBe('username');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('saveCredentials uses custom values over known site', async () => {
|
|
175
|
+
vault.credentialsFile = null;
|
|
176
|
+
vault.encryptionKey = null;
|
|
177
|
+
|
|
178
|
+
await vault.saveCredentials('github', {
|
|
179
|
+
username: 'octocat',
|
|
180
|
+
password: 'pass',
|
|
181
|
+
loginUrl: 'https://custom.github.com/login'
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const entry = vault.credentials.get('github');
|
|
185
|
+
expect(entry.loginUrl).toBe('https://custom.github.com/login');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('getCredentials returns credential and updates lastUsed', () => {
|
|
189
|
+
vault.credentialsFile = null;
|
|
190
|
+
vault.encryptionKey = null;
|
|
191
|
+
vault.credentials.set('github', {
|
|
192
|
+
siteId: 'github',
|
|
193
|
+
username: 'user',
|
|
194
|
+
password: 'pass',
|
|
195
|
+
lastUsed: null
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const result = vault.getCredentials('GitHub');
|
|
199
|
+
expect(result.username).toBe('user');
|
|
200
|
+
expect(result.lastUsed).toBeDefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('getCredentials returns null for unknown site', () => {
|
|
204
|
+
expect(vault.getCredentials('nonexistent')).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('hasCredentials checks existence', () => {
|
|
208
|
+
vault.credentials.set('github', { siteId: 'github' });
|
|
209
|
+
expect(vault.hasCredentials('GitHub')).toBe(true);
|
|
210
|
+
expect(vault.hasCredentials('unknown')).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('deleteCredentials removes credential', async () => {
|
|
214
|
+
vault.credentialsFile = null;
|
|
215
|
+
vault.encryptionKey = null;
|
|
216
|
+
vault.sessionsFile = null;
|
|
217
|
+
vault.credentials.set('github', { siteId: 'github' });
|
|
218
|
+
|
|
219
|
+
const deleted = await vault.deleteCredentials('GitHub');
|
|
220
|
+
expect(deleted).toBe(true);
|
|
221
|
+
expect(vault.credentials.has('github')).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('deleteCredentials returns false for non-existent', async () => {
|
|
225
|
+
vault.credentialsFile = null;
|
|
226
|
+
vault.encryptionKey = null;
|
|
227
|
+
|
|
228
|
+
const deleted = await vault.deleteCredentials('nonexistent');
|
|
229
|
+
expect(deleted).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('listCredentials returns masked summaries', () => {
|
|
233
|
+
vault.credentials.set('github', {
|
|
234
|
+
siteId: 'github',
|
|
235
|
+
name: 'GitHub',
|
|
236
|
+
username: 'octocat@github.com',
|
|
237
|
+
loginUrl: 'https://github.com/login',
|
|
238
|
+
createdAt: Date.now(),
|
|
239
|
+
lastUsed: null
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const list = vault.listCredentials();
|
|
243
|
+
expect(list.length).toBe(1);
|
|
244
|
+
expect(list[0].username).not.toBe('octocat@github.com');
|
|
245
|
+
expect(list[0].siteId).toBe('github');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ── Session Management ──
|
|
249
|
+
test('saveSession stores session cookies', async () => {
|
|
250
|
+
vault.sessionsFile = null;
|
|
251
|
+
vault.encryptionKey = null;
|
|
252
|
+
|
|
253
|
+
await vault.saveSession('github', [{ name: 'session', value: 'abc123' }]);
|
|
254
|
+
|
|
255
|
+
const session = vault.sessions.get('github');
|
|
256
|
+
expect(session.cookies.length).toBe(1);
|
|
257
|
+
expect(session.expiresAt).toBeGreaterThan(Date.now());
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('getSession returns session if not expired', () => {
|
|
261
|
+
vault.sessions.set('github', {
|
|
262
|
+
siteId: 'github',
|
|
263
|
+
cookies: [{ name: 'session' }],
|
|
264
|
+
savedAt: Date.now(),
|
|
265
|
+
expiresAt: Date.now() + 86400000
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const session = vault.getSession('GitHub');
|
|
269
|
+
expect(session).not.toBeNull();
|
|
270
|
+
expect(session.cookies.length).toBe(1);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('getSession returns null for expired session', () => {
|
|
274
|
+
vault.sessions.set('github', {
|
|
275
|
+
siteId: 'github',
|
|
276
|
+
cookies: [],
|
|
277
|
+
savedAt: Date.now() - 86400000,
|
|
278
|
+
expiresAt: Date.now() - 1000 // expired
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const session = vault.getSession('GitHub');
|
|
282
|
+
expect(session).toBeNull();
|
|
283
|
+
expect(vault.sessions.has('github')).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('getSession returns null for non-existent', () => {
|
|
287
|
+
expect(vault.getSession('unknown')).toBeNull();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('getAllSessions skips expired sessions', () => {
|
|
291
|
+
vault.sessions.set('github', {
|
|
292
|
+
siteId: 'github',
|
|
293
|
+
cookies: [],
|
|
294
|
+
expiresAt: Date.now() + 86400000
|
|
295
|
+
});
|
|
296
|
+
vault.sessions.set('linkedin', {
|
|
297
|
+
siteId: 'linkedin',
|
|
298
|
+
cookies: [],
|
|
299
|
+
expiresAt: Date.now() - 1000 // expired
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const all = vault.getAllSessions();
|
|
303
|
+
expect(Object.keys(all).length).toBe(1);
|
|
304
|
+
expect(all.github).toBeDefined();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('deleteSession removes session', async () => {
|
|
308
|
+
vault.sessionsFile = null;
|
|
309
|
+
vault.encryptionKey = null;
|
|
310
|
+
vault.sessions.set('github', { siteId: 'github' });
|
|
311
|
+
|
|
312
|
+
const deleted = await vault.deleteSession('GitHub');
|
|
313
|
+
expect(deleted).toBe(true);
|
|
314
|
+
expect(vault.sessions.has('github')).toBe(false);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('deleteSession returns false for non-existent', async () => {
|
|
318
|
+
vault.sessionsFile = null;
|
|
319
|
+
vault.encryptionKey = null;
|
|
320
|
+
|
|
321
|
+
const deleted = await vault.deleteSession('unknown');
|
|
322
|
+
expect(deleted).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// ── _cleanupExpiredSessions ──
|
|
326
|
+
test('_cleanupExpiredSessions removes expired sessions', () => {
|
|
327
|
+
vault.sessionsFile = null;
|
|
328
|
+
vault.encryptionKey = null;
|
|
329
|
+
vault.sessions.set('expired', { expiresAt: Date.now() - 1000 });
|
|
330
|
+
vault.sessions.set('active', { expiresAt: Date.now() + 86400000 });
|
|
331
|
+
|
|
332
|
+
vault._cleanupExpiredSessions();
|
|
333
|
+
expect(vault.sessions.has('expired')).toBe(false);
|
|
334
|
+
expect(vault.sessions.has('active')).toBe(true);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('_cleanupExpiredSessions does nothing when none expired', () => {
|
|
338
|
+
vault.sessions.set('active', { expiresAt: Date.now() + 86400000 });
|
|
339
|
+
|
|
340
|
+
vault._cleanupExpiredSessions();
|
|
341
|
+
expect(vault.sessions.has('active')).toBe(true);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// ── Credential Request Flow ──
|
|
345
|
+
test('createCredentialRequest returns requestInfo and promise', async () => {
|
|
346
|
+
const { requestInfo, promise } = vault.createCredentialRequest('github');
|
|
347
|
+
expect(requestInfo.requestId).toMatch(/^cred_/);
|
|
348
|
+
expect(requestInfo.siteId).toBe('github');
|
|
349
|
+
expect(requestInfo.siteName).toBe('GitHub');
|
|
350
|
+
expect(promise).toBeInstanceOf(Promise);
|
|
351
|
+
|
|
352
|
+
// Clean up — catch the rejection from cancel
|
|
353
|
+
vault.cancelCredentialRequest(requestInfo.requestId);
|
|
354
|
+
await expect(promise).rejects.toThrow();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('submitCredentials resolves pending request', async () => {
|
|
358
|
+
const { requestInfo, promise } = vault.createCredentialRequest('github');
|
|
359
|
+
|
|
360
|
+
vault.credentialsFile = null;
|
|
361
|
+
vault.encryptionKey = null;
|
|
362
|
+
|
|
363
|
+
// Submit credentials
|
|
364
|
+
await vault.submitCredentials(requestInfo.requestId, {
|
|
365
|
+
username: 'user',
|
|
366
|
+
password: 'pass'
|
|
367
|
+
}, false);
|
|
368
|
+
|
|
369
|
+
const result = await promise;
|
|
370
|
+
expect(result.credentials.username).toBe('user');
|
|
371
|
+
expect(result.siteId).toBe('github');
|
|
372
|
+
expect(result.saved).toBe(false);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('submitCredentials throws for unknown request', async () => {
|
|
376
|
+
await expect(vault.submitCredentials('nonexistent', {}))
|
|
377
|
+
.rejects.toThrow('No pending credential request found');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('cancelCredentialRequest rejects pending promise', async () => {
|
|
381
|
+
const { requestInfo, promise } = vault.createCredentialRequest('github');
|
|
382
|
+
|
|
383
|
+
vault.cancelCredentialRequest(requestInfo.requestId);
|
|
384
|
+
|
|
385
|
+
await expect(promise).rejects.toThrow('cancelled by user');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('cancelCredentialRequest does nothing for unknown request', () => {
|
|
389
|
+
// Should not throw
|
|
390
|
+
vault.cancelCredentialRequest('nonexistent');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test('getPendingRequest returns request info', async () => {
|
|
394
|
+
const { requestInfo, promise } = vault.createCredentialRequest('github');
|
|
395
|
+
|
|
396
|
+
const pending = vault.getPendingRequest(requestInfo.requestId);
|
|
397
|
+
expect(pending.siteId).toBe('github');
|
|
398
|
+
|
|
399
|
+
// Cleanup — catch rejection
|
|
400
|
+
vault.cancelCredentialRequest(requestInfo.requestId);
|
|
401
|
+
await expect(promise).rejects.toThrow();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('getPendingRequest returns null for unknown', () => {
|
|
405
|
+
expect(vault.getPendingRequest('nonexistent')).toBeNull();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ── Known Sites ──
|
|
409
|
+
test('getKnownSite returns site config for known sites', () => {
|
|
410
|
+
const site = vault.getKnownSite('LinkedIn');
|
|
411
|
+
expect(site).not.toBeNull();
|
|
412
|
+
expect(site.name).toBe('LinkedIn');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test('getKnownSite returns null for unknown sites', () => {
|
|
416
|
+
expect(vault.getKnownSite('fakebook')).toBeNull();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test('listKnownSites returns all known sites', () => {
|
|
420
|
+
const sites = vault.listKnownSites();
|
|
421
|
+
expect(sites.length).toBeGreaterThan(0);
|
|
422
|
+
expect(sites[0]).toHaveProperty('siteId');
|
|
423
|
+
expect(sites[0]).toHaveProperty('name');
|
|
424
|
+
expect(sites[0]).toHaveProperty('hasCredentials');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// ── Static methods ──
|
|
428
|
+
test('getEventTypes returns credential events', () => {
|
|
429
|
+
const events = CredentialVault.getEventTypes();
|
|
430
|
+
expect(events).toHaveProperty('REQUEST');
|
|
431
|
+
expect(events).toHaveProperty('RESPONSE');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// ── Encryption ──
|
|
435
|
+
test('_encrypt throws when key not initialized', () => {
|
|
436
|
+
vault.encryptionKey = null;
|
|
437
|
+
expect(() => vault._encrypt('test')).toThrow('Encryption key not initialized');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test('_decrypt throws when key not initialized', () => {
|
|
441
|
+
vault.encryptionKey = null;
|
|
442
|
+
expect(() => vault._decrypt('test')).toThrow('Encryption key not initialized');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// ── Persistence (edge cases) ──
|
|
446
|
+
test('_persistCredentials does nothing without file/key', async () => {
|
|
447
|
+
vault.credentialsFile = null;
|
|
448
|
+
await vault._persistCredentials();
|
|
449
|
+
expect(mockWriteFile).not.toHaveBeenCalled();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test('_persistSessions does nothing without file/key', async () => {
|
|
453
|
+
vault.sessionsFile = null;
|
|
454
|
+
await vault._persistSessions();
|
|
455
|
+
expect(mockWriteFile).not.toHaveBeenCalled();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test('_loadCredentials does nothing without file/key', async () => {
|
|
459
|
+
vault.credentialsFile = null;
|
|
460
|
+
await vault._loadCredentials();
|
|
461
|
+
expect(mockReadFile).not.toHaveBeenCalled();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test('_loadSessions does nothing without file/key', async () => {
|
|
465
|
+
vault.sessionsFile = null;
|
|
466
|
+
await vault._loadSessions();
|
|
467
|
+
expect(mockReadFile).not.toHaveBeenCalled();
|
|
468
|
+
});
|
|
469
|
+
});
|