forked from boos/BoosAPI
fix: address latest coderabbit review findings
This commit is contained in:
@@ -1084,4 +1084,65 @@ describe('refreshModelsForAccount credential discovery', () => {
|
||||
'gemini-3-pro-preview',
|
||||
]);
|
||||
});
|
||||
|
||||
it('continues antigravity discovery after fetch errors and trims the oauth project id before posting', async () => {
|
||||
getApiTokenMock.mockResolvedValue(null);
|
||||
getModelsMock.mockRejectedValue(new Error('antigravity oauth discovery should not call adapter.getModels'));
|
||||
undiciFetchMock
|
||||
.mockRejectedValueOnce(new Error('network boom'))
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
models: {
|
||||
'gemini-3-pro-preview': { displayName: 'Gemini 3 Pro Preview' },
|
||||
},
|
||||
}),
|
||||
text: async () => JSON.stringify({ ok: true }),
|
||||
});
|
||||
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'antigravity-site-trimmed',
|
||||
url: 'https://cloudcode-pa.googleapis.com',
|
||||
platform: 'antigravity',
|
||||
status: 'active',
|
||||
}).returning().get();
|
||||
|
||||
const account = await db.insert(schema.accounts).values({
|
||||
siteId: site.id,
|
||||
username: 'antigravity-trimmed@example.com',
|
||||
accessToken: 'antigravity-access-token',
|
||||
apiToken: null,
|
||||
status: 'active',
|
||||
oauthProvider: 'antigravity',
|
||||
oauthAccountKey: 'antigravity-trimmed@example.com',
|
||||
oauthProjectId: ' project-demo ',
|
||||
extraConfig: JSON.stringify({
|
||||
credentialMode: 'session',
|
||||
oauth: {
|
||||
provider: 'antigravity',
|
||||
email: 'antigravity-trimmed@example.com',
|
||||
},
|
||||
}),
|
||||
}).returning().get();
|
||||
|
||||
const result = await refreshModelsForAccount(account.id);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
accountId: account.id,
|
||||
refreshed: true,
|
||||
status: 'success',
|
||||
errorCode: null,
|
||||
tokenScanned: 0,
|
||||
discoveredByCredential: true,
|
||||
discoveredApiToken: false,
|
||||
modelCount: 1,
|
||||
modelsPreview: ['gemini-3-pro-preview'],
|
||||
});
|
||||
expect(undiciFetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(String(undiciFetchMock.mock.calls[1]?.[0] || '')).toBe('https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels');
|
||||
expect(JSON.parse(String(undiciFetchMock.mock.calls[1]?.[1]?.body || '{}'))).toEqual({
|
||||
project: 'project-demo',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,10 +4,11 @@ import {
|
||||
buildOauthInfoFromAccount,
|
||||
buildStoredOauthStateFromAccount,
|
||||
getOauthInfoFromAccount,
|
||||
isOauthProvider,
|
||||
} from './oauthAccount.js';
|
||||
|
||||
describe('oauth account identity helpers', () => {
|
||||
it('prefers structured oauth identity columns over extraConfig metadata', () => {
|
||||
it('prefers structured oauth provider/accountKey/projectId columns while preserving a legacy accountId', () => {
|
||||
const oauth = getOauthInfoFromAccount({
|
||||
oauthProvider: 'gemini-cli',
|
||||
oauthAccountKey: 'structured-user@example.com',
|
||||
@@ -25,7 +26,7 @@ describe('oauth account identity helpers', () => {
|
||||
|
||||
expect(oauth).toEqual(expect.objectContaining({
|
||||
provider: 'gemini-cli',
|
||||
accountId: 'structured-user@example.com',
|
||||
accountId: 'json-user',
|
||||
accountKey: 'structured-user@example.com',
|
||||
projectId: 'structured-project',
|
||||
refreshToken: 'refresh-token',
|
||||
@@ -82,6 +83,29 @@ describe('oauth account identity helpers', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('preserves legacy accountId when structured oauthAccountKey differs', () => {
|
||||
const oauth = getOauthInfoFromAccount({
|
||||
oauthProvider: 'codex',
|
||||
oauthAccountKey: 'structured-account-key',
|
||||
oauthProjectId: null,
|
||||
extraConfig: JSON.stringify({
|
||||
oauth: {
|
||||
provider: 'codex',
|
||||
accountId: 'legacy-account-id',
|
||||
accountKey: 'legacy-account-key',
|
||||
refreshToken: 'refresh-token',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(oauth).toEqual(expect.objectContaining({
|
||||
provider: 'codex',
|
||||
accountId: 'legacy-account-id',
|
||||
accountKey: 'structured-account-key',
|
||||
refreshToken: 'refresh-token',
|
||||
}));
|
||||
});
|
||||
|
||||
it('builds patched oauth state from structured identity columns and current runtime state', () => {
|
||||
const oauth = buildOauthInfoFromAccount({
|
||||
oauthProvider: 'codex',
|
||||
@@ -135,6 +159,32 @@ describe('oauth account identity helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('still recognizes oauth providers from account rows after persisted state strips identity fields', () => {
|
||||
const account = {
|
||||
oauthProvider: 'codex',
|
||||
oauthAccountKey: 'structured-account',
|
||||
oauthProjectId: 'structured-project',
|
||||
extraConfig: JSON.stringify({
|
||||
oauth: buildStoredOauthStateFromAccount({
|
||||
oauthProvider: 'codex',
|
||||
oauthAccountKey: 'structured-account',
|
||||
oauthProjectId: 'structured-project',
|
||||
extraConfig: JSON.stringify({
|
||||
oauth: {
|
||||
provider: 'codex',
|
||||
accountId: 'legacy-account',
|
||||
refreshToken: 'refresh-token',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
expect(isOauthProvider(account)).toBe(true);
|
||||
expect(isOauthProvider(account, 'codex')).toBe(true);
|
||||
expect(isOauthProvider(account, 'gemini-cli')).toBe(false);
|
||||
});
|
||||
|
||||
it('builds a structured identity backfill patch from legacy oauth metadata only for missing columns', () => {
|
||||
expect(buildOauthIdentityBackfillPatch({
|
||||
oauthProvider: null,
|
||||
|
||||
@@ -179,11 +179,12 @@ export function getOauthInfoFromAccount(account?: OauthIdentityCarrier | null):
|
||||
if (!provider) return null;
|
||||
const structuredAccountKey = asTrimmedString(account.oauthAccountKey);
|
||||
const accountKey = structuredAccountKey || storedIdentity?.accountKey || storedIdentity?.accountId;
|
||||
const accountId = storedIdentity?.accountId || structuredAccountKey || accountKey;
|
||||
const projectId = asTrimmedString(account.oauthProjectId) || storedIdentity?.projectId;
|
||||
return {
|
||||
...(storedRuntime || {}),
|
||||
provider,
|
||||
accountId: structuredAccountKey || storedIdentity?.accountId || accountKey,
|
||||
accountId,
|
||||
accountKey,
|
||||
projectId,
|
||||
};
|
||||
@@ -275,13 +276,12 @@ export function buildStoredOauthStateFromAccount(
|
||||
}
|
||||
|
||||
export function isOauthProvider(
|
||||
account: Pick<typeof schema.accounts.$inferSelect, 'extraConfig'> | string | null | undefined,
|
||||
account: OauthIdentityCarrier | string | null | undefined,
|
||||
provider?: string,
|
||||
): boolean {
|
||||
const extraConfig = typeof account === 'string' || account == null
|
||||
? account
|
||||
: account.extraConfig;
|
||||
const oauth = getOauthInfoFromExtraConfig(extraConfig);
|
||||
const oauth = typeof account === 'string' || account == null
|
||||
? getOauthInfoFromExtraConfig(account)
|
||||
: getOauthInfoFromAccount(account);
|
||||
if (!oauth) return false;
|
||||
if (!provider) return true;
|
||||
return oauth.provider === provider;
|
||||
|
||||
@@ -218,34 +218,39 @@ export async function discoverAntigravityModelsFromCloud(input: {
|
||||
}
|
||||
|
||||
const oauth = getOauthInfoFromAccount(input.account);
|
||||
const requestBody = oauth?.projectId ? { project: oauth.projectId } : {};
|
||||
const projectId = (oauth?.projectId || '').trim();
|
||||
const requestBody = projectId ? { project: projectId } : {};
|
||||
let lastError = '';
|
||||
|
||||
for (const baseUrl of buildAntigravityDiscoveryBaseUrls(input.site.url || ANTIGRAVITY_UPSTREAM_BASE_URL)) {
|
||||
const response = await fetch(
|
||||
`${baseUrl}/v1internal:fetchAvailableModels`,
|
||||
withSiteRecordProxyRequestInit(input.site, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': ANTIGRAVITY_MODELS_USER_AGENT,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
}),
|
||||
);
|
||||
if (!response.ok) {
|
||||
lastError = await response.text().catch(() => '') || `HTTP ${response.status}`;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${baseUrl}/v1internal:fetchAvailableModels`,
|
||||
withSiteRecordProxyRequestInit(input.site, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': ANTIGRAVITY_MODELS_USER_AGENT,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
}),
|
||||
);
|
||||
if (!response.ok) {
|
||||
lastError = await response.text().catch(() => '') || `HTTP ${response.status}`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
const models = normalizeDiscoveredModels(extractAntigravityModelIds(payload));
|
||||
if (models.length > 0) {
|
||||
return models;
|
||||
const payload = await response.json();
|
||||
const models = normalizeDiscoveredModels(extractAntigravityModelIds(payload));
|
||||
if (models.length > 0) {
|
||||
return models;
|
||||
}
|
||||
lastError = '未获取到可用模型';
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? `${baseUrl}: ${error.message}` : String(error);
|
||||
}
|
||||
lastError = '未获取到可用模型';
|
||||
}
|
||||
|
||||
throw new Error(lastError || '未获取到可用模型');
|
||||
|
||||
Reference in New Issue
Block a user