Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixes/8842-antigravity-projectid-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **Antigravity**: a token refresh now recovers a missing `projectId` instead of leaving the connection stuck on 422 — when the initial OAuth exchange fails to populate it (network timeout, account not yet onboarded to Gemini Code Assist), `refreshCredentials` retries discovery via `ensureAntigravityProjectAssigned` and persists the result, mirroring the runtime `transformRequest` path. Best-effort: a failed discovery leaves the next request to retry
9 changes: 6 additions & 3 deletions config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@
"tests/unit/translator-openai-to-kiro.test.ts": 1250,
"tests/unit/translator-resp-gemini-to-openai.test.ts": 1234,
"tests/unit/usage-service-hardening.test.ts": 1483,
"tests/unit/vscode-token-routes.test.ts": 1256
"tests/unit/vscode-token-routes.test.ts": 1256,
"tests/unit/executor-antigravity.test.ts": 1098
},
"_rebaseline_2026_06_09": "Re-baseline consciente pre-release v3.8.19: 9 arquivos cresceram durante o ciclo (features mergeadas: RequestLoggerV2 +281 request-logger rework, stream +101, combo +73, chatCore +45, catalog +32 fable-5/catalog-flag, callLogs +4, accountFallback +2, usageHistory novo 840) + core.ts +7 (fix resetAllDbModuleState, PR 3536). A catraca segue valendo destes valores — proximo crescimento falha. Decisao: encolher (esp. RequestLoggerV2/chatCore) e a issue #3501 ficam para o ciclo seguinte.",
"_rebaseline_2026_06_11_phase1f": "Phase 1f (#3501): ProviderDetailPageClient.tsx 4948→4062 (-886 LOC); 3 novos hooks extraídos. useProviderConnections.ts=954 acima do cap=800 — justificado: extração direta do god-component (zero lógica nova), própria redução do cliente supera o custo. useProviderSettings.ts=263 e useProviderModels.ts=154 já abaixo do cap.",
Expand Down Expand Up @@ -340,7 +341,7 @@
"_rebaseline_2026_07_25_adobe_firefly_reference_images": "Follow-up to #8006: storage upload + referenceBlobs for image/video and /v1/images/edits dispatch. adobeFireflyClient.ts 1958->2317 (+upload helpers, extract sources, resolve blob ids). Note: 2317 not 2316 — check-file-size.mjs counts LOC via split(\"\\n\").length (counts the trailing-newline empty element), which is 1 higher than `wc -l` on a file ending in \\n; the PR's original entry (2316) was measured with wc -l and undercounted by 1 against the actual gate.",
"_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).",
"_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.",
"open-sse/executors/antigravity.ts": 1493,
"open-sse/executors/antigravity.ts": 1528,
"open-sse/executors/base.ts": 1540,
"open-sse/executors/chatgpt-web.ts": 3241,
"open-sse/executors/codex.ts": 1534,
Expand Down Expand Up @@ -406,5 +407,7 @@
"open-sse/executors/hyperagent.ts": 1026
},
"_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_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."
}
39 changes: 37 additions & 2 deletions open-sse/executors/antigravity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,14 +750,49 @@ export class AntigravityExecutor extends BaseExecutor {
const tokens = (await response.json()) as Record<string, unknown>;
log?.info?.("TOKEN", "Antigravity refreshed");

const newAccessToken =
typeof tokens.access_token === "string" ? tokens.access_token : undefined;

// Discover projectId if the stored value is empty. The initial OAuth exchange
// may have failed to populate it (network timeout, account not yet onboarded to
// Gemini Code Assist). The runtime transformRequest path already does this, but
// a proactive discovery here prevents 422 errors on the next request when the
// per-token memoization cache is invalidated by the new access token.
let projectId = credentials.projectId?.trim() || "";
if (!projectId && newAccessToken) {
try {
const discovered = await ensureAntigravityProjectAssigned(
newAccessToken,
fetch,
getAntigravityClientProfile(credentials),
AbortSignal.timeout(8_000)
);
if (discovered) {
projectId = discovered;
await persistDiscoveredAntigravityProjectId(
credentials.connectionId,
discovered,
credentials.providerSpecificData
);
const okMsg = `Antigravity projectId discovered during refresh: ${discovered}`;
log?.info?.("TOKEN", okMsg);
}
} catch (discoveryError) {
// Best-effort: if discovery fails, the runtime path will retry on next request.
const msg =
discoveryError instanceof Error ? discoveryError.message : String(discoveryError);
log?.warn?.("TOKEN", `Antigravity projectId discovery during refresh failed: ${msg}`);
}
}

return {
accessToken: typeof tokens.access_token === "string" ? tokens.access_token : undefined,
accessToken: newAccessToken,
refreshToken:
typeof tokens.refresh_token === "string" && tokens.refresh_token
? tokens.refresh_token
: credentials.refreshToken,
expiresIn: typeof tokens.expires_in === "number" ? tokens.expires_in : undefined,
projectId: credentials.projectId,
projectId,
// Preserve providerSpecificData so a projectId stored there survives the refresh
// (the onCredentialsRefreshed DB write) instead of being dropped → 422 (#2480).
providerSpecificData: credentials.providerSpecificData,
Expand Down
167 changes: 167 additions & 0 deletions tests/unit/executor-antigravity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,173 @@ test("AntigravityExecutor.refreshCredentials refreshes Google OAuth tokens", asy
}
});

test("AntigravityExecutor.refreshCredentials discovers projectId when stored value is empty", async () => {
const executor = new AntigravityExecutor();
const originalFetch = globalThis.fetch;
clearAntigravityProjectCache();

let fetchCalls: string[] = [];
globalThis.fetch = async (url, init) => {
const urlStr = String(url);
fetchCalls.push(urlStr);
// Token refresh endpoint
if (urlStr.includes("oauth2.googleapis.com/token")) {
return new Response(
JSON.stringify({
access_token: "new-token",
refresh_token: "new-refresh",
expires_in: 3600,
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
// loadCodeAssist endpoint - returns a projectId
if (urlStr.includes("loadCodeAssist")) {
return new Response(JSON.stringify({ cloudaicompanionProject: "discovered-project" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return new Response("not found", { status: 404 });
};

try {
const result = await executor.refreshCredentials(
{ refreshToken: "refresh", projectId: "", connectionId: "conn-1" },
null
);
assert.equal(result?.projectId, "discovered-project");
assert.equal(result?.accessToken, "new-token");
assert.equal(result?.refreshToken, "new-refresh");
assert.equal(result?.expiresIn, 3600);
assert.ok(
fetchCalls.some((u) => u.includes("oauth2.googleapis.com/token")),
"should call token endpoint"
);
assert.ok(
fetchCalls.some((u) => u.includes("loadCodeAssist")),
"should call loadCodeAssist"
);
} finally {
globalThis.fetch = originalFetch;
clearAntigravityProjectCache();
}
});

test("AntigravityExecutor.refreshCredentials skips discovery when projectId already set", async () => {
const executor = new AntigravityExecutor();
const originalFetch = globalThis.fetch;
clearAntigravityProjectCache();

let fetchCalls: string[] = [];
globalThis.fetch = async (url) => {
const urlStr = String(url);
fetchCalls.push(urlStr);
if (urlStr.includes("oauth2.googleapis.com/token")) {
return new Response(
JSON.stringify({
access_token: "new-token",
refresh_token: "new-refresh",
expires_in: 3600,
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
return new Response("not found", { status: 404 });
};

try {
const result = await executor.refreshCredentials(
{ refreshToken: "refresh", projectId: "existing-project" },
null
);
assert.equal(result?.projectId, "existing-project");
assert.ok(
!fetchCalls.some((u) => u.includes("loadCodeAssist")),
"should NOT call loadCodeAssist"
);
} finally {
globalThis.fetch = originalFetch;
clearAntigravityProjectCache();
}
});

test("AntigravityExecutor.refreshCredentials handles discovery failure gracefully", async () => {
const executor = new AntigravityExecutor();
const originalFetch = globalThis.fetch;
clearAntigravityProjectCache();

globalThis.fetch = async (url) => {
const urlStr = String(url);
if (urlStr.includes("oauth2.googleapis.com/token")) {
return new Response(
JSON.stringify({
access_token: "new-token",
refresh_token: "new-refresh",
expires_in: 3600,
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
// loadCodeAssist fails
if (urlStr.includes("loadCodeAssist")) {
return new Response("server error", { status: 500 });
}
return new Response("not found", { status: 404 });
};

try {
const result = await executor.refreshCredentials(
{ refreshToken: "refresh", projectId: "", connectionId: "conn-1" },
null
);
// Discovery failed, projectId stays empty but refresh still succeeds
assert.equal(result?.projectId, "");
assert.equal(result?.accessToken, "new-token");
assert.equal(result?.refreshToken, "new-refresh");
assert.equal(result?.expiresIn, 3600);
} finally {
globalThis.fetch = originalFetch;
clearAntigravityProjectCache();
}
});

test("AntigravityExecutor.refreshCredentials skips discovery when access_token is not a string", async () => {
const executor = new AntigravityExecutor();
const originalFetch = globalThis.fetch;
clearAntigravityProjectCache();

let fetchCalls: string[] = [];
globalThis.fetch = async (url) => {
const urlStr = String(url);
fetchCalls.push(urlStr);
if (urlStr.includes("oauth2.googleapis.com/token")) {
// access_token missing from response
return new Response(JSON.stringify({ refresh_token: "new-refresh", expires_in: 3600 }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return new Response("not found", { status: 404 });
};

try {
const result = await executor.refreshCredentials(
{ refreshToken: "refresh", projectId: "", connectionId: "conn-1" },
null
);
assert.equal(result?.projectId, "");
assert.equal(result?.accessToken, undefined);
assert.ok(
!fetchCalls.some((u) => u.includes("loadCodeAssist")),
"should NOT call loadCodeAssist"
);
} finally {
globalThis.fetch = originalFetch;
clearAntigravityProjectCache();
}
});

// The non-streaming passthrough drain test ("auto-retries short 429 responses and
// collects SSE for non-stream clients") lives in
// tests/unit/antigravity-streaming-passthrough.test.ts with the other passthrough tests.
Expand Down
Loading