fix(token-refresh): discover projectId during token refresh (#8860)

* fix(token-refresh): discover projectId during token refresh

The token refresh path (tokenRefresh.ts) did not discover projectId
for antigravity/agy accounts. Dashboard and health check refresh use
this path, not the executor path.

Add ensureAntigravityProjectAssigned call after refreshGoogleToken
for antigravity/agy providers when projectId is empty.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* chore(quality): rebaseline the token-refresh test file + changelog fragment

tests/unit/token-refresh-service.test.ts 1311->1378 (+67) — the four cases
covering projectId discovery on the tokenRefresh.ts path. Growth is the tests
this PR adds, nothing else.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Bob.Hou
2026-07-28 11:40:08 -04:00
committed by GitHub
parent 26a7783521
commit ec0edef499
4 changed files with 114 additions and 4 deletions

View File

@@ -0,0 +1 @@
- **Antigravity**: the shared token-refresh service now recovers a missing `projectId` too — #8842 fixed the executor path, but the Dashboard and the health check refresh through `open-sse/services/tokenRefresh.ts`, which kept returning credentials with an empty project and left those accounts on 422

View File

@@ -197,7 +197,7 @@
"tests/unit/search-handler-extended.test.ts": 1071,
"tests/unit/sse-auth.test.ts": 1600,
"tests/unit/stream-utils.test.ts": 2445,
"tests/unit/token-refresh-service.test.ts": 1311,
"tests/unit/token-refresh-service.test.ts": 1378,
"tests/unit/translator-openai-responses-req.test.ts": 1194,
"tests/unit/translator-openai-to-gemini.test.ts": 1616,
"tests/unit/translator-openai-to-kiro.test.ts": 1250,
@@ -409,5 +409,6 @@
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
"_rebaseline_2026_07_27_v3849_train3": "Merge-train 3 (13 PRs) — owner-approved 2026-07-27. Both entries are genuine irreducible growth at existing chokepoints, not new branches: src/lib/db/apiKeys.ts 1518->1529 (#8805 cx/* ≡ codex/* API-key model permissions); open-sse/handlers/chatCore.ts 5006->5020 (#8806 real response payload into plugin onResponse hooks). Covered by tests/unit/db-apiKeys-crud.test.ts (4 new cases) and the two plugin-hook test files updated in #8806 respectively.",
"_rebaseline_2026_07_28_8842_antigravity_projectid_refresh": "PR #8842 (fix/antigravity-projectid-refresh) own growth: open-sse/executors/antigravity.ts 1493->1528 (+35 = projectId discovery in refreshCredentials: import ensureAntigravityProjectAssigned + trim projectId + call ensureAntigravityProjectAssigned with 8s timeout + persistDiscoveredAntigravityProjectId + log success/failure). Irreducible wiring at the existing credential-refresh chokepoint. Covered by tests/unit/executor-antigravity.test.ts (4 new test cases).",
"_rebaseline_2026_07_28_8842_antigravity_test": "PR #8842 test growth: tests/unit/executor-antigravity.test.ts new testFrozen 1098 (4 new test cases for projectId discovery during refresh: discovers when empty, skips when set, handles failure, skips when access_token not a string). All above cap 1000 on day one."
"_rebaseline_2026_07_28_8842_antigravity_test": "PR #8842 test growth: tests/unit/executor-antigravity.test.ts new testFrozen 1098 (4 new test cases for projectId discovery during refresh: discovers when empty, skips when set, handles failure, skips when access_token not a string). All above cap 1000 on day one.",
"_rebaseline_2026_07_28_8860_tokenrefresh_projectid": "PR #8860 (fix/antigravity-projectid-centralized) own test growth: tests/unit/token-refresh-service.test.ts 1311->1378 (+67 = 4 cases covering projectId discovery on the tokenRefresh.ts path — the Dashboard/health-check refresh route, which #8842 did not reach since that fixed the executor path). Covered by the same file."
}

View File

@@ -45,6 +45,8 @@ import { refreshKimiCodingToken } from "./tokenRefresh/providers/kimiCoding.ts";
import { refreshGitLabDuoToken } from "./tokenRefresh/providers/gitlabDuo.ts";
import { refreshClaudeOAuthToken } from "./tokenRefresh/providers/claudeOAuth.ts";
import { refreshGoogleToken } from "./tokenRefresh/providers/google.ts";
import { ensureAntigravityProjectAssigned } from "./antigravityProjectBootstrap.ts";
import { persistDiscoveredAntigravityProjectId } from "./antigravityProjectPersist.ts";
import { refreshCodexToken } from "./tokenRefresh/providers/codex.ts";
import { refreshKiroToken } from "./tokenRefresh/providers/kiro.ts";
import { refreshQoderToken } from "./tokenRefresh/providers/qoder.ts";
@@ -275,8 +277,8 @@ async function _getAccessTokenInternal(provider, credentials, log, proxyConfig:
case "gemini":
case "antigravity":
case "agy":
return await refreshGoogleToken(
case "agy": {
const result = await refreshGoogleToken(
credentials.refreshToken,
PROVIDERS[provider].clientId,
PROVIDERS[provider].clientSecret,
@@ -284,6 +286,45 @@ async function _getAccessTokenInternal(provider, credentials, log, proxyConfig:
proxyConfig
);
// Google One AI accounts get no projectId at OAuth exchange time.
// Recover it via loadCodeAssist so downstream routing works.
if (
result?.accessToken &&
(provider === "antigravity" || provider === "agy") &&
!(credentials.projectId || credentials.providerSpecificData?.projectId)
) {
try {
const discovered = await ensureAntigravityProjectAssigned(
result.accessToken,
fetch
);
if (discovered) {
result.projectId = discovered;
result.providerSpecificData = {
...(credentials.providerSpecificData || {}),
...(result.providerSpecificData || {}),
projectId: discovered,
};
if (credentials.connectionId) {
await persistDiscoveredAntigravityProjectId(
credentials.connectionId,
discovered,
credentials.providerSpecificData
);
}
log?.info?.("TOKEN", "Antigravity projectId discovered during token refresh", {
projectId: discovered,
});
}
} catch (discoveryError) {
const msg = discoveryError instanceof Error ? discoveryError.message : String(discoveryError);
log?.warn?.("TOKEN", `Antigravity projectId discovery failed: ${msg}`);
}
}
return result;
}
case "claude":
return await refreshClaudeOAuthToken(credentials.refreshToken, log, proxyConfig);

View File

@@ -795,6 +795,73 @@ test("supportsTokenRefresh, isUnrecoverableRefreshError and formatProviderCreden
assert.equal(formatProviderCredentials("missing-provider", {}, log), null);
});
test("getAccessToken discovers projectId for antigravity when stored value is empty", async () => {
const log = createLog();
const { clearAntigravityProjectCache } = await import("../../open-sse/services/antigravityProjectBootstrap.ts");
clearAntigravityProjectCache();
let fetchCalls: string[] = [];
await withMockedFetch(async (url, init) => {
const urlStr = String(url);
fetchCalls.push(urlStr);
if (urlStr.includes("oauth2.googleapis.com/token")) {
return jsonResponse({
access_token: "new-token-1",
refresh_token: "new-refresh",
expires_in: 3600,
});
}
if (urlStr.includes("loadCodeAssist")) {
return jsonResponse({ cloudaicompanionProject: "discovered-project" });
}
return new Response("not found", { status: 404 });
}, async () => {
const result = await getAccessToken("antigravity", {
refreshToken: "refresh",
projectId: "",
connectionId: "conn-1",
providerSpecificData: {},
});
assert.equal(result.projectId, "discovered-project");
assert.ok(fetchCalls.some((u) => u.includes("loadCodeAssist")), "should call loadCodeAssist");
});
clearAntigravityProjectCache();
});
test("getAccessToken handles projectId discovery failure gracefully for antigravity", async () => {
const log = createLog();
const { clearAntigravityProjectCache } = await import("../../open-sse/services/antigravityProjectBootstrap.ts");
clearAntigravityProjectCache();
tokenRefresh._clearTokenRotationMap();
await withMockedFetch(async (url) => {
const urlStr = String(url);
if (urlStr.includes("oauth2.googleapis.com/token")) {
return jsonResponse({
access_token: "new-token-3",
refresh_token: "new-refresh",
expires_in: 3600,
});
}
if (urlStr.includes("loadCodeAssist")) {
return new Response("server error", { status: 500 });
}
return new Response("not found", { status: 404 });
}, async () => {
const result = await getAccessToken("antigravity", {
refreshToken: "refresh",
projectId: "",
connectionId: "conn-1",
providerSpecificData: {},
});
assert.equal(result.accessToken, "new-token-3");
assert.ok(result, "should return a result without throwing");
});
clearAntigravityProjectCache();
});
test("getAccessToken deduplicates concurrent refreshes for the same provider and token", async () => {
const log = createLog();
let fetchCount = 0;