fix: project id forwarding regression (#93)
* fix(proxy): avoid empty project context and hydrate project_id from account token * test(proxy): add regressions for empty project and token project_id hydration * test(scripts): add live request validator for project_id forwarding
This commit is contained in:
7 files changed
+534
-13
No files matched your search
@@ -3,6 +3,7 @@ import { CloudMonitorService } from '../../services/CloudMonitorService';
|
||||
import { CloudAccountRepo } from '../../ipc/database/cloudHandler';
|
||||
import { GoogleAPIService } from '../../services/GoogleAPIService';
|
||||
import { AutoSwitchService } from '../../services/AutoSwitchService';
|
||||
import { TokenManagerService } from '../../server/modules/proxy/token-manager.service';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
// Mock dependencies
|
||||
@@ -192,3 +193,98 @@ describe('CloudMonitorService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('TokenManagerService project-id hydration', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('hydrates missing project_id and persists it', async () => {
|
||||
const account = {
|
||||
id: 'acc-1',
|
||||
provider: 'google',
|
||||
email: '[email protected]',
|
||||
token: {
|
||||
access_token: 'access-token',
|
||||
refresh_token: 'refresh-token',
|
||||
token_type: 'Bearer',
|
||||
expires_in: 3600,
|
||||
expiry_timestamp: Math.floor(Date.now() / 1000) + 3600,
|
||||
},
|
||||
created_at: 1,
|
||||
last_used: 1,
|
||||
};
|
||||
|
||||
vi.mocked(CloudAccountRepo.getAccounts).mockResolvedValue([account] as never);
|
||||
vi.mocked(CloudAccountRepo.getAccount).mockResolvedValue(account as never);
|
||||
vi.mocked(CloudAccountRepo.updateToken).mockResolvedValue(undefined as never);
|
||||
vi.mocked(GoogleAPIService.fetchProjectId).mockResolvedValue('resolved-project' as never);
|
||||
|
||||
const service = new TokenManagerService();
|
||||
const selectedToken = await service.getNextToken();
|
||||
|
||||
expect(GoogleAPIService.fetchProjectId).toHaveBeenCalledWith('access-token');
|
||||
expect(CloudAccountRepo.updateToken).toHaveBeenCalledWith(
|
||||
'acc-1',
|
||||
expect.objectContaining({
|
||||
project_id: 'resolved-project',
|
||||
}),
|
||||
);
|
||||
expect(selectedToken?.token.project_id).toBe('resolved-project');
|
||||
});
|
||||
|
||||
it('keeps existing valid project_id without extra fetch', async () => {
|
||||
const account = {
|
||||
id: 'acc-2',
|
||||
provider: 'google',
|
||||
email: '[email protected]',
|
||||
token: {
|
||||
access_token: 'access-token-2',
|
||||
refresh_token: 'refresh-token-2',
|
||||
token_type: 'Bearer',
|
||||
expires_in: 3600,
|
||||
expiry_timestamp: Math.floor(Date.now() / 1000) + 3600,
|
||||
project_id: 'existing-project',
|
||||
},
|
||||
created_at: 1,
|
||||
last_used: 1,
|
||||
};
|
||||
|
||||
vi.mocked(CloudAccountRepo.getAccounts).mockResolvedValue([account] as never);
|
||||
vi.mocked(GoogleAPIService.fetchProjectId).mockResolvedValue('new-project' as never);
|
||||
|
||||
const service = new TokenManagerService();
|
||||
const selectedToken = await service.getNextToken();
|
||||
|
||||
expect(GoogleAPIService.fetchProjectId).not.toHaveBeenCalled();
|
||||
expect(selectedToken?.token.project_id).toBe('existing-project');
|
||||
});
|
||||
|
||||
it('keeps request usable when project_id cannot be resolved', async () => {
|
||||
const account = {
|
||||
id: 'acc-3',
|
||||
provider: 'google',
|
||||
email: '[email protected]',
|
||||
token: {
|
||||
access_token: 'access-token-3',
|
||||
refresh_token: 'refresh-token-3',
|
||||
token_type: 'Bearer',
|
||||
expires_in: 3600,
|
||||
expiry_timestamp: Math.floor(Date.now() / 1000) + 3600,
|
||||
project_id: 'cloud-code-123',
|
||||
},
|
||||
created_at: 1,
|
||||
last_used: 1,
|
||||
};
|
||||
|
||||
vi.mocked(CloudAccountRepo.getAccounts).mockResolvedValue([account] as never);
|
||||
vi.mocked(GoogleAPIService.fetchProjectId).mockResolvedValue(null as never);
|
||||
|
||||
const service = new TokenManagerService();
|
||||
const selectedToken = await service.getNextToken();
|
||||
|
||||
expect(GoogleAPIService.fetchProjectId).toHaveBeenCalledWith('access-token-3');
|
||||
expect(selectedToken?.token.project_id).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -454,10 +454,30 @@ describe('ProxyService Empty Stream Retry Logic', () => {
|
||||
expect(mockTokenManager.getNextToken).toHaveBeenCalledTimes(1);
|
||||
expect(mockGeminiClient.generateInternal).toHaveBeenCalledTimes(2);
|
||||
expect(mockGeminiClient.generateInternal.mock.calls[0][0].project).toBe('project-1');
|
||||
expect(mockGeminiClient.generateInternal.mock.calls[1][0].project).toBe('');
|
||||
expect(mockGeminiClient.generateInternal.mock.calls[1][0].project).toBeUndefined();
|
||||
expect(mockGeminiClient.generateInternal.mock.calls[1][0]).not.toHaveProperty('project');
|
||||
expect((result as any).candidates?.[0]?.content?.parts?.[0]?.text).toBe('ok');
|
||||
});
|
||||
|
||||
it('omits empty project id in Gemini internal payload', async () => {
|
||||
const service = new TestableProxyService();
|
||||
const token = createToken('acc-1');
|
||||
token.token.project_id = '';
|
||||
mockTokenManager.getNextToken.mockResolvedValue(token);
|
||||
mockGeminiClient.generateInternal.mockResolvedValue({
|
||||
candidates: [{ content: { parts: [{ text: 'ok' }] }, finishReason: 'STOP' }],
|
||||
usageMetadata: { totalTokenCount: 5 },
|
||||
});
|
||||
|
||||
await service.handleGeminiGenerateContent('models/gemini-2.5-flash', {
|
||||
contents: [{ role: 'user', parts: [{ text: 'hello' }] }],
|
||||
} as any);
|
||||
|
||||
const internalPayload = mockGeminiClient.generateInternal.mock.calls[0][0];
|
||||
expect(internalPayload.project).toBeUndefined();
|
||||
expect(internalPayload).not.toHaveProperty('project');
|
||||
});
|
||||
|
||||
it('uses generate-content requestType for Gemini stream internal payload', async () => {
|
||||
const service = new TestableProxyService();
|
||||
mockTokenManager.getNextToken.mockResolvedValue(createToken('acc-1'));
|
||||
|
||||
Reference in new issue
Block a user