keystone-cli 1.3.0 → 2.0.1
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 +127 -140
- package/package.json +6 -3
- package/src/cli.ts +54 -369
- package/src/commands/init.ts +15 -29
- package/src/db/memory-db.test.ts +45 -0
- package/src/db/memory-db.ts +47 -21
- package/src/db/sqlite-setup.ts +26 -3
- package/src/db/workflow-db.ts +12 -5
- package/src/parser/config-schema.ts +17 -13
- package/src/parser/schema.ts +4 -2
- package/src/runner/__test__/llm-mock-setup.ts +173 -0
- package/src/runner/__test__/llm-test-setup.ts +271 -0
- package/src/runner/engine-executor.test.ts +25 -18
- package/src/runner/executors/blueprint-executor.ts +0 -1
- package/src/runner/executors/dynamic-executor.ts +11 -6
- package/src/runner/executors/engine-executor.ts +5 -1
- package/src/runner/executors/llm-executor.ts +502 -1033
- package/src/runner/executors/memory-executor.ts +35 -19
- package/src/runner/executors/plan-executor.ts +0 -1
- package/src/runner/executors/types.ts +4 -4
- package/src/runner/llm-adapter.integration.test.ts +151 -0
- package/src/runner/llm-adapter.ts +270 -1398
- package/src/runner/llm-clarification.test.ts +91 -106
- package/src/runner/llm-executor.test.ts +217 -1181
- package/src/runner/memoization.test.ts +0 -1
- package/src/runner/recovery-security.test.ts +51 -20
- package/src/runner/reflexion.test.ts +55 -18
- package/src/runner/standard-tools-integration.test.ts +137 -87
- package/src/runner/step-executor.test.ts +36 -80
- package/src/runner/step-executor.ts +0 -2
- package/src/runner/test-harness.ts +3 -29
- package/src/runner/tool-integration.test.ts +122 -73
- package/src/runner/workflow-runner.ts +110 -49
- package/src/runner/workflow-scheduler.ts +11 -1
- package/src/runner/workflow-summary.ts +144 -0
- package/src/utils/auth-manager.test.ts +10 -520
- package/src/utils/auth-manager.ts +3 -756
- package/src/utils/config-loader.ts +12 -0
- package/src/utils/constants.ts +0 -17
- package/src/utils/process-sandbox.ts +15 -3
- package/src/runner/llm-adapter-runtime.test.ts +0 -209
- package/src/runner/llm-adapter.test.ts +0 -1012
|
@@ -45,10 +45,11 @@ describe('AuthManager', () => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
it('should load and parse auth data if file exists', () => {
|
|
48
|
-
|
|
48
|
+
const authData = { mcp_tokens: { test: { access_token: 'test-token' } } };
|
|
49
|
+
fs.writeFileSync(TEMP_AUTH_FILE, JSON.stringify(authData));
|
|
49
50
|
|
|
50
51
|
const data = AuthManager.load();
|
|
51
|
-
expect(data).toEqual(
|
|
52
|
+
expect(data).toEqual(authData);
|
|
52
53
|
});
|
|
53
54
|
|
|
54
55
|
it('should return empty object if JSON parsing fails', () => {
|
|
@@ -61,14 +62,16 @@ describe('AuthManager', () => {
|
|
|
61
62
|
|
|
62
63
|
describe('save()', () => {
|
|
63
64
|
it('should save data merged with current data', () => {
|
|
64
|
-
fs.writeFileSync(
|
|
65
|
+
fs.writeFileSync(
|
|
66
|
+
TEMP_AUTH_FILE,
|
|
67
|
+
JSON.stringify({ mcp_tokens: { s1: { access_token: 't1' } } })
|
|
68
|
+
);
|
|
65
69
|
|
|
66
|
-
AuthManager.save({
|
|
70
|
+
AuthManager.save({ mcp_tokens: { s2: { access_token: 't2' } } });
|
|
67
71
|
|
|
68
72
|
const content = fs.readFileSync(TEMP_AUTH_FILE, 'utf8');
|
|
69
73
|
expect(JSON.parse(content)).toEqual({
|
|
70
|
-
|
|
71
|
-
copilot_token: 'new-copilot-token',
|
|
74
|
+
mcp_tokens: { s2: { access_token: 't2' } },
|
|
72
75
|
});
|
|
73
76
|
});
|
|
74
77
|
});
|
|
@@ -85,524 +88,11 @@ describe('AuthManager', () => {
|
|
|
85
88
|
AuthManager.setLogger(mockLogger);
|
|
86
89
|
// Trigger a log through save failure to verify
|
|
87
90
|
process.env.KEYSTONE_AUTH_PATH = '/non/existent/path/auth.json';
|
|
88
|
-
AuthManager.save({
|
|
91
|
+
AuthManager.save({ mcp_tokens: { test: { access_token: 'test' } } });
|
|
89
92
|
expect(mockLogger.error).toHaveBeenCalled();
|
|
90
93
|
process.env.KEYSTONE_AUTH_PATH = TEMP_AUTH_FILE;
|
|
91
94
|
// Reset logger
|
|
92
95
|
AuthManager.setLogger(new ConsoleLogger());
|
|
93
96
|
});
|
|
94
97
|
});
|
|
95
|
-
|
|
96
|
-
describe('getCopilotToken()', () => {
|
|
97
|
-
it('should return undefined if no github_token', async () => {
|
|
98
|
-
fs.writeFileSync(TEMP_AUTH_FILE, JSON.stringify({}));
|
|
99
|
-
const token = await AuthManager.getCopilotToken();
|
|
100
|
-
expect(token).toBeUndefined();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should return cached token if valid', async () => {
|
|
104
|
-
const expires = Math.floor(Date.now() / 1000) + 1000;
|
|
105
|
-
fs.writeFileSync(
|
|
106
|
-
TEMP_AUTH_FILE,
|
|
107
|
-
JSON.stringify({
|
|
108
|
-
github_token: 'gh-token',
|
|
109
|
-
copilot_token: 'cached-token',
|
|
110
|
-
copilot_expires_at: expires,
|
|
111
|
-
})
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
const token = await AuthManager.getCopilotToken();
|
|
115
|
-
expect(token).toBe('cached-token');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should refresh token if expired', async () => {
|
|
119
|
-
fs.writeFileSync(
|
|
120
|
-
TEMP_AUTH_FILE,
|
|
121
|
-
JSON.stringify({
|
|
122
|
-
github_token: 'gh-token',
|
|
123
|
-
copilot_token: 'expired-token',
|
|
124
|
-
copilot_expires_at: Math.floor(Date.now() / 1000) - 1000,
|
|
125
|
-
})
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
// Mock fetch
|
|
129
|
-
const mockFetch = mock(() =>
|
|
130
|
-
Promise.resolve(
|
|
131
|
-
new Response(
|
|
132
|
-
JSON.stringify({
|
|
133
|
-
token: 'new-token',
|
|
134
|
-
expires_at: Math.floor(Date.now() / 1000) + 3600,
|
|
135
|
-
}),
|
|
136
|
-
{ status: 200 }
|
|
137
|
-
)
|
|
138
|
-
)
|
|
139
|
-
);
|
|
140
|
-
// @ts-ignore
|
|
141
|
-
global.fetch = mockFetch;
|
|
142
|
-
|
|
143
|
-
const token = await AuthManager.getCopilotToken();
|
|
144
|
-
expect(token).toBe('new-token');
|
|
145
|
-
expect(mockFetch).toHaveBeenCalled();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('should return undefined and log error if refresh fails', async () => {
|
|
149
|
-
fs.writeFileSync(
|
|
150
|
-
TEMP_AUTH_FILE,
|
|
151
|
-
JSON.stringify({
|
|
152
|
-
github_token: 'gh-token',
|
|
153
|
-
})
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
// Mock fetch failure
|
|
157
|
-
// @ts-ignore
|
|
158
|
-
global.fetch = mock(() =>
|
|
159
|
-
Promise.resolve(
|
|
160
|
-
new Response('Unauthorized', {
|
|
161
|
-
status: 401,
|
|
162
|
-
statusText: 'Unauthorized',
|
|
163
|
-
})
|
|
164
|
-
)
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
const consoleSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
168
|
-
const token = await AuthManager.getCopilotToken();
|
|
169
|
-
|
|
170
|
-
expect(token).toBeUndefined();
|
|
171
|
-
expect(consoleSpy).toHaveBeenCalled();
|
|
172
|
-
consoleSpy.mockRestore();
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe('Device Login', () => {
|
|
177
|
-
it('initGitHubDeviceLogin should return device code data', async () => {
|
|
178
|
-
const mockFetch = mock(() =>
|
|
179
|
-
Promise.resolve(
|
|
180
|
-
new Response(
|
|
181
|
-
JSON.stringify({
|
|
182
|
-
device_code: 'dev_code',
|
|
183
|
-
user_code: 'USER-CODE',
|
|
184
|
-
verification_uri: 'https://github.com/login/device',
|
|
185
|
-
expires_in: 900,
|
|
186
|
-
interval: 5,
|
|
187
|
-
}),
|
|
188
|
-
{ status: 200 }
|
|
189
|
-
)
|
|
190
|
-
)
|
|
191
|
-
);
|
|
192
|
-
// @ts-ignore
|
|
193
|
-
global.fetch = mockFetch;
|
|
194
|
-
|
|
195
|
-
const result = await AuthManager.initGitHubDeviceLogin();
|
|
196
|
-
expect(result.device_code).toBe('dev_code');
|
|
197
|
-
expect(result.user_code).toBe('USER-CODE');
|
|
198
|
-
expect(mockFetch).toHaveBeenCalled();
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('pollGitHubDeviceLogin should return token when successful', async () => {
|
|
202
|
-
let callCount = 0;
|
|
203
|
-
const mockFetch = mock(() => {
|
|
204
|
-
callCount++;
|
|
205
|
-
if (callCount === 1) {
|
|
206
|
-
return Promise.resolve(
|
|
207
|
-
new Response(
|
|
208
|
-
JSON.stringify({
|
|
209
|
-
error: 'authorization_pending',
|
|
210
|
-
}),
|
|
211
|
-
{ status: 200 }
|
|
212
|
-
)
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
return Promise.resolve(
|
|
216
|
-
new Response(
|
|
217
|
-
JSON.stringify({
|
|
218
|
-
access_token: 'gh_access_token',
|
|
219
|
-
}),
|
|
220
|
-
{ status: 200 }
|
|
221
|
-
)
|
|
222
|
-
);
|
|
223
|
-
});
|
|
224
|
-
// @ts-ignore
|
|
225
|
-
global.fetch = mockFetch;
|
|
226
|
-
|
|
227
|
-
// Mock setTimeout to resolve immediately
|
|
228
|
-
const originalTimeout = global.setTimeout;
|
|
229
|
-
// @ts-ignore
|
|
230
|
-
global.setTimeout = (fn) => fn();
|
|
231
|
-
|
|
232
|
-
try {
|
|
233
|
-
const token = await AuthManager.pollGitHubDeviceLogin('dev_code');
|
|
234
|
-
expect(token).toBe('gh_access_token');
|
|
235
|
-
expect(callCount).toBe(2);
|
|
236
|
-
} finally {
|
|
237
|
-
global.setTimeout = originalTimeout;
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('pollGitHubDeviceLogin should throw on other errors', async () => {
|
|
242
|
-
const mockFetch = mock(() =>
|
|
243
|
-
Promise.resolve(
|
|
244
|
-
new Response(
|
|
245
|
-
JSON.stringify({
|
|
246
|
-
error: 'expired_token',
|
|
247
|
-
error_description: 'The device code has expired',
|
|
248
|
-
}),
|
|
249
|
-
{ status: 200 }
|
|
250
|
-
)
|
|
251
|
-
)
|
|
252
|
-
);
|
|
253
|
-
// @ts-ignore
|
|
254
|
-
global.fetch = mockFetch;
|
|
255
|
-
|
|
256
|
-
await expect(AuthManager.pollGitHubDeviceLogin('dev_code')).rejects.toThrow(
|
|
257
|
-
'The device code has expired'
|
|
258
|
-
);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('pollGitHubDeviceLogin should timeout after 15 minutes', async () => {
|
|
262
|
-
// Mock fetch to always return authorization_pending
|
|
263
|
-
// @ts-ignore
|
|
264
|
-
global.fetch = mock(() =>
|
|
265
|
-
Promise.resolve(
|
|
266
|
-
new Response(
|
|
267
|
-
JSON.stringify({
|
|
268
|
-
error: 'authorization_pending',
|
|
269
|
-
}),
|
|
270
|
-
{ status: 200 }
|
|
271
|
-
)
|
|
272
|
-
)
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
// Mock Date.now to simulate time passing
|
|
276
|
-
let now = Date.now();
|
|
277
|
-
const dateSpy = spyOn(Date, 'now').mockImplementation(() => {
|
|
278
|
-
const current = now;
|
|
279
|
-
now += 1000 * 60 * 16; // Advance 16 minutes on each call to trigger timeout immediately
|
|
280
|
-
return current;
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
await expect(AuthManager.pollGitHubDeviceLogin('dev_code')).rejects.toThrow(
|
|
285
|
-
'Device login timed out'
|
|
286
|
-
);
|
|
287
|
-
} finally {
|
|
288
|
-
dateSpy.mockRestore();
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
describe('OAuth Helpers', () => {
|
|
294
|
-
it('generateCodeVerifier should return hex string', () => {
|
|
295
|
-
// @ts-ignore - access private
|
|
296
|
-
const verifier = AuthManager.generateCodeVerifier();
|
|
297
|
-
expect(verifier).toMatch(/^[0-9a-f]+$/);
|
|
298
|
-
expect(verifier.length).toBe(64);
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('createCodeChallenge should return base64url string', () => {
|
|
302
|
-
const verifier = 'test-verifier';
|
|
303
|
-
// @ts-ignore - access private
|
|
304
|
-
const challenge = AuthManager.createCodeChallenge(verifier);
|
|
305
|
-
expect(challenge).toBeDefined();
|
|
306
|
-
expect(challenge).not.toContain('+');
|
|
307
|
-
expect(challenge).not.toContain('/');
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
describe('Anthropic Claude', () => {
|
|
312
|
-
it('createAnthropicClaudeAuth should return url and verifier', () => {
|
|
313
|
-
const { url, verifier } = AuthManager.createAnthropicClaudeAuth();
|
|
314
|
-
expect(url).toContain('https://claude.ai/oauth/authorize');
|
|
315
|
-
expect(url).toContain('client_id=');
|
|
316
|
-
expect(verifier).toBeDefined();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it('exchangeAnthropicClaudeCode should return tokens', async () => {
|
|
320
|
-
const mockFetch = mock(() =>
|
|
321
|
-
Promise.resolve(
|
|
322
|
-
new Response(
|
|
323
|
-
JSON.stringify({
|
|
324
|
-
access_token: 'claude-access',
|
|
325
|
-
refresh_token: 'claude-refresh',
|
|
326
|
-
expires_in: 3600,
|
|
327
|
-
}),
|
|
328
|
-
{ status: 200 }
|
|
329
|
-
)
|
|
330
|
-
)
|
|
331
|
-
);
|
|
332
|
-
// @ts-ignore
|
|
333
|
-
global.fetch = mockFetch;
|
|
334
|
-
|
|
335
|
-
const result = await AuthManager.exchangeAnthropicClaudeCode('code#verifier', 'verifier');
|
|
336
|
-
expect(result.access_token).toBe('claude-access');
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it('getAnthropicClaudeToken should return cached token if valid', async () => {
|
|
340
|
-
const expires = Math.floor(Date.now() / 1000) + 1000;
|
|
341
|
-
fs.writeFileSync(
|
|
342
|
-
TEMP_AUTH_FILE,
|
|
343
|
-
JSON.stringify({
|
|
344
|
-
anthropic_claude: {
|
|
345
|
-
access_token: 'claude-cached',
|
|
346
|
-
refresh_token: 'refresh',
|
|
347
|
-
expires_at: expires,
|
|
348
|
-
},
|
|
349
|
-
})
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
const token = await AuthManager.getAnthropicClaudeToken();
|
|
353
|
-
expect(token).toBe('claude-cached');
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it('getAnthropicClaudeToken should refresh if expired', async () => {
|
|
357
|
-
fs.writeFileSync(
|
|
358
|
-
TEMP_AUTH_FILE,
|
|
359
|
-
JSON.stringify({
|
|
360
|
-
anthropic_claude: {
|
|
361
|
-
access_token: 'expired',
|
|
362
|
-
refresh_token: 'refresh',
|
|
363
|
-
expires_at: Math.floor(Date.now() / 1000) - 1000,
|
|
364
|
-
},
|
|
365
|
-
})
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
// @ts-ignore
|
|
369
|
-
global.fetch = mock(() =>
|
|
370
|
-
Promise.resolve(
|
|
371
|
-
new Response(
|
|
372
|
-
JSON.stringify({
|
|
373
|
-
access_token: 'new-claude-token',
|
|
374
|
-
refresh_token: 'new-refresh',
|
|
375
|
-
expires_in: 3600,
|
|
376
|
-
}),
|
|
377
|
-
{ status: 200 }
|
|
378
|
-
)
|
|
379
|
-
)
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
const token = await AuthManager.getAnthropicClaudeToken();
|
|
383
|
-
expect(token).toBe('new-claude-token');
|
|
384
|
-
});
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
describe('Google Gemini', () => {
|
|
388
|
-
it('getGoogleGeminiToken should return undefined if not logged in', async () => {
|
|
389
|
-
const token = await AuthManager.getGoogleGeminiToken();
|
|
390
|
-
expect(token).toBeUndefined();
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it('getGoogleGeminiToken should refresh if expired', async () => {
|
|
394
|
-
fs.writeFileSync(
|
|
395
|
-
TEMP_AUTH_FILE,
|
|
396
|
-
JSON.stringify({
|
|
397
|
-
google_gemini: {
|
|
398
|
-
access_token: 'expired',
|
|
399
|
-
refresh_token: 'refresh',
|
|
400
|
-
expires_at: Math.floor(Date.now() / 1000) - 1000,
|
|
401
|
-
},
|
|
402
|
-
})
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
process.env.GOOGLE_GEMINI_OAUTH_CLIENT_SECRET = 'secret';
|
|
406
|
-
|
|
407
|
-
// @ts-ignore
|
|
408
|
-
global.fetch = mock(() =>
|
|
409
|
-
Promise.resolve(
|
|
410
|
-
new Response(
|
|
411
|
-
JSON.stringify({
|
|
412
|
-
access_token: 'new-gemini-token',
|
|
413
|
-
expires_in: 3600,
|
|
414
|
-
}),
|
|
415
|
-
{ status: 200 }
|
|
416
|
-
)
|
|
417
|
-
)
|
|
418
|
-
);
|
|
419
|
-
|
|
420
|
-
const token = await AuthManager.getGoogleGeminiToken();
|
|
421
|
-
expect(token).toBe('new-gemini-token');
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it('fetchGoogleGeminiProjectId should return project ID from loadCodeAssist', async () => {
|
|
425
|
-
// @ts-ignore - access private
|
|
426
|
-
const spyFetch = mock(() =>
|
|
427
|
-
Promise.resolve(
|
|
428
|
-
new Response(
|
|
429
|
-
JSON.stringify({
|
|
430
|
-
cloudaicompanionProject: 'test-project-id',
|
|
431
|
-
}),
|
|
432
|
-
{ status: 200 }
|
|
433
|
-
)
|
|
434
|
-
)
|
|
435
|
-
);
|
|
436
|
-
// @ts-ignore
|
|
437
|
-
global.fetch = spyFetch;
|
|
438
|
-
|
|
439
|
-
// @ts-ignore - access private
|
|
440
|
-
const projectId = await AuthManager.fetchGoogleGeminiProjectId('access-token');
|
|
441
|
-
expect(projectId).toBe('test-project-id');
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
it('fetchGoogleGeminiProjectId should return project ID from nested object', async () => {
|
|
445
|
-
// @ts-ignore - access private
|
|
446
|
-
const spyFetch = mock(() =>
|
|
447
|
-
Promise.resolve(
|
|
448
|
-
new Response(
|
|
449
|
-
JSON.stringify({
|
|
450
|
-
cloudaicompanionProject: { id: 'nested-id' },
|
|
451
|
-
}),
|
|
452
|
-
{ status: 200 }
|
|
453
|
-
)
|
|
454
|
-
)
|
|
455
|
-
);
|
|
456
|
-
// @ts-ignore
|
|
457
|
-
global.fetch = spyFetch;
|
|
458
|
-
|
|
459
|
-
// @ts-ignore - access private
|
|
460
|
-
const projectId = await AuthManager.fetchGoogleGeminiProjectId('access-token');
|
|
461
|
-
expect(projectId).toBe('nested-id');
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
it('loginGoogleGemini should handle OAuth callback', async () => {
|
|
465
|
-
process.env.GOOGLE_GEMINI_OAUTH_CLIENT_SECRET = 'secret';
|
|
466
|
-
|
|
467
|
-
let fetchHandler: any;
|
|
468
|
-
const mockServer = {
|
|
469
|
-
port: 51121,
|
|
470
|
-
stop: mock(() => {}),
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
// @ts-ignore - mock Bun.serve
|
|
474
|
-
const serveSpy = spyOn(Bun, 'serve').mockImplementation((options: any) => {
|
|
475
|
-
fetchHandler = options.fetch;
|
|
476
|
-
return mockServer;
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
// Mock openBrowser to prevent browser opening
|
|
480
|
-
const openBrowserSpy = spyOn(AuthManager, 'openBrowser').mockImplementation(() => {});
|
|
481
|
-
|
|
482
|
-
try {
|
|
483
|
-
const loginPromise = AuthManager.loginGoogleGemini('test-project');
|
|
484
|
-
|
|
485
|
-
// Verify server was started
|
|
486
|
-
expect(serveSpy).toHaveBeenCalled();
|
|
487
|
-
expect(fetchHandler).toBeDefined();
|
|
488
|
-
|
|
489
|
-
// Simulating the fetch handler call with the mock server
|
|
490
|
-
const req = new Request('http://localhost:51121/oauth-callback?code=test-code');
|
|
491
|
-
// @ts-ignore
|
|
492
|
-
global.fetch = mock(() =>
|
|
493
|
-
Promise.resolve(
|
|
494
|
-
new Response(
|
|
495
|
-
JSON.stringify({
|
|
496
|
-
access_token: 'access',
|
|
497
|
-
refresh_token: 'refresh',
|
|
498
|
-
expires_in: 3600,
|
|
499
|
-
}),
|
|
500
|
-
{ status: 200 }
|
|
501
|
-
)
|
|
502
|
-
)
|
|
503
|
-
);
|
|
504
|
-
|
|
505
|
-
const response = await fetchHandler(req, mockServer);
|
|
506
|
-
expect(response.status).toBe(200);
|
|
507
|
-
|
|
508
|
-
await loginPromise;
|
|
509
|
-
const auth = AuthManager.load();
|
|
510
|
-
expect(auth.google_gemini?.access_token).toBe('access');
|
|
511
|
-
} finally {
|
|
512
|
-
serveSpy.mockRestore();
|
|
513
|
-
openBrowserSpy.mockRestore();
|
|
514
|
-
}
|
|
515
|
-
});
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
describe('OpenAI ChatGPT Login', () => {
|
|
519
|
-
it('loginOpenAIChatGPT should handle OAuth callback', async () => {
|
|
520
|
-
let fetchHandler: any;
|
|
521
|
-
const mockServer = {
|
|
522
|
-
stop: mock(() => {}),
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
// @ts-ignore - mock Bun.serve
|
|
526
|
-
const serveSpy = spyOn(Bun, 'serve').mockImplementation((options: any) => {
|
|
527
|
-
fetchHandler = options.fetch;
|
|
528
|
-
return mockServer;
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
// Mock openBrowser to prevent browser opening
|
|
532
|
-
const openBrowserSpy = spyOn(AuthManager, 'openBrowser').mockImplementation(() => {});
|
|
533
|
-
|
|
534
|
-
try {
|
|
535
|
-
const loginPromise = AuthManager.loginOpenAIChatGPT();
|
|
536
|
-
|
|
537
|
-
expect(serveSpy).toHaveBeenCalled();
|
|
538
|
-
expect(fetchHandler).toBeDefined();
|
|
539
|
-
|
|
540
|
-
// Simulate callback
|
|
541
|
-
const req = new Request('http://localhost:1455/auth/callback?code=test-code&state=xyz');
|
|
542
|
-
// The code expects the state to match. We can't easily get the random state,
|
|
543
|
-
// but since we are mocking, we can just ensure the branch is covered.
|
|
544
|
-
// Actually, let's just test that the handler exists and can be called.
|
|
545
|
-
|
|
546
|
-
// @ts-ignore
|
|
547
|
-
global.fetch = mock(() =>
|
|
548
|
-
Promise.resolve(
|
|
549
|
-
new Response(
|
|
550
|
-
JSON.stringify({
|
|
551
|
-
access_token: 'access',
|
|
552
|
-
refresh_token: 'refresh',
|
|
553
|
-
expires_in: 3600,
|
|
554
|
-
}),
|
|
555
|
-
{ status: 200 }
|
|
556
|
-
)
|
|
557
|
-
)
|
|
558
|
-
);
|
|
559
|
-
|
|
560
|
-
// We skip the state check by not providing it in the URL if we want to test failure,
|
|
561
|
-
// or we can try to find where it's stored.
|
|
562
|
-
// But for now, let's just trigger it.
|
|
563
|
-
|
|
564
|
-
const response = await fetchHandler(req);
|
|
565
|
-
// It should return 400 because of state mismatch in real code,
|
|
566
|
-
// unless we mock the state generation.
|
|
567
|
-
expect(response.status).toBe(400);
|
|
568
|
-
|
|
569
|
-
await expect(loginPromise).rejects.toThrow('Invalid OAuth state');
|
|
570
|
-
} finally {
|
|
571
|
-
serveSpy.mockRestore();
|
|
572
|
-
openBrowserSpy.mockRestore();
|
|
573
|
-
}
|
|
574
|
-
});
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
describe('OpenAI ChatGPT', () => {
|
|
578
|
-
it('getOpenAIChatGPTToken should refresh if expired', async () => {
|
|
579
|
-
fs.writeFileSync(
|
|
580
|
-
TEMP_AUTH_FILE,
|
|
581
|
-
JSON.stringify({
|
|
582
|
-
openai_chatgpt: {
|
|
583
|
-
access_token: 'expired',
|
|
584
|
-
refresh_token: 'refresh',
|
|
585
|
-
expires_at: Math.floor(Date.now() / 1000) - 1000,
|
|
586
|
-
},
|
|
587
|
-
})
|
|
588
|
-
);
|
|
589
|
-
|
|
590
|
-
// @ts-ignore
|
|
591
|
-
global.fetch = mock(() =>
|
|
592
|
-
Promise.resolve(
|
|
593
|
-
new Response(
|
|
594
|
-
JSON.stringify({
|
|
595
|
-
access_token: 'new-chatgpt-token',
|
|
596
|
-
refresh_token: 'new-refresh',
|
|
597
|
-
expires_in: 3600,
|
|
598
|
-
}),
|
|
599
|
-
{ status: 200 }
|
|
600
|
-
)
|
|
601
|
-
)
|
|
602
|
-
);
|
|
603
|
-
|
|
604
|
-
const token = await AuthManager.getOpenAIChatGPTToken();
|
|
605
|
-
expect(token).toBe('new-chatgpt-token');
|
|
606
|
-
});
|
|
607
|
-
});
|
|
608
98
|
});
|