mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-17 20:52:15 +03:00
* fix(antigravity): heal empty-projectId accounts via retryable auto-onboarding Accounts with an empty Cloud Code projectId get a permanent 422 "Missing Google projectId" when loadCodeAssist returns no project. The 3.8.50 bootstrap attempts to CREATE the project via onboardUser, but a single failed attempt (transient network/upstream error) was memoized forever in onboardAttemptedCache: every later request in the process skipped onboarding and 422'd, even though a retry would succeed. Replace the permanent per-token Set with a failure-backoff map: failed onboard attempts are retried after a 5-minute backoff (bounded, self-healing), the in-flight lock still dedupes concurrent calls, and success clears the failure marker and memoizes the project as before. Accounts that CAN be onboarded now heal automatically on a later request or token refresh — no user action. Tests: the existing "does not retry" case is now framed as the backoff window; a new case proves the account heals (retries onboarding and recovers the project) once the backoff expires. * chore(changelog): fragment for #10424 antigravity project autocreate * feat(antigravity): BYOP fast-fail + manual GCP project-id override Port decolua/9router#2934 + VansRouter 802a859: - tryOnboardUser now returns a three-way status; a 200 onboardUser response WITHOUT cloudaicompanionProject means Google deprecated automatic project creation for standard-tier (personal) accounts (BYOP). Such accounts are cached permanently (no pointless ~18s re-onboard) and the executor fails fast with 403 GCP_PROJECT_REQUIRED + actionable 'enter your project id' message instead of the generic 422 or a delayed 429. - Transient onboard failures keep the existing 5-min backoff heal. - Manual project-id override: the EditConnectionModal now stamps providerSpecificData.isProjectIdManual when the operator enters a project id, and tokenRefresh skips auto-discovery for flagged accounts so the manual value is never overwritten. * chore(changelog): cover BYOP fast-fail + manual override in #10424 fragment * test(antigravity): expect fast 403 GCP_PROJECT_REQUIRED when loadCodeAssist finds no project (#10424) Google now marks accounts without an onboarded project as BYOP (automatic project creation deprecated for standard-tier accounts, #2934). The PR's BYOP fast-fail path returns 403 gcp_project_required instead of the old generic 422 missing_project_id; align the #2334 executor test with that contract so CI unit-test shard 2/4 passes. * fix(antigravity): persist isProjectIdManual, fix BYOP citation, dodge refresh-retry Review follow-up on #10424: 1. EditConnectionModal: isProjectIdManual was set on updates.providerSpecificData right after the project-id field, then the OAuth path (Antigravity is always OAuth) rebuilt providerSpecificData from connection.providerSpecificData before the request went out, discarding the flag — tokenRefresh.ts was guarding a field never actually persisted. The flag now lands in the single surviving antigravity merge, with a jsdom regression test (modeled on edit-connection-modal-openai-store-toggle). 2. The '#2934' citation for the Google BYOP claim pointed at an unrelated closed issue. Swapped for the real tracking issue #8491 (empty Google projectId -> 422 class) across bootstrap/executor/test comments. 3. BYOP fast-fail now returns 422 instead of 403: chatCore's generic 401/403 -> refresh-and-retry path was hitting Google's OAuth token endpoint on every request from an affected account (pointless — refreshing cannot create a GCP project), and 422 matches the sibling missing_project_id error the client already maps to an action-needed prompt. Also: eslint-disable-next-line for the pre-existing react-hooks/set-state-in-effect baseline noise in the modal (repo convention, same pattern as 11 other dashboard files). * chore(ci): drop unused eslint-disable in EditConnectionModal form hydration The react-hooks/set-state-in-effect disable added in the previous commit is unused under the repo's pinned eslint-plugin-react-hooks (7.0.1) — the rule does not fire on this line at that version, so the unused directive tripped the whole-repo 'No new ESLint warnings' gate (max-warnings 0). Verified with the lockfile-pinned plugin: lint:json is clean (0 errors, 0 warnings). * fix(build): bound and retry the opencode-plugin npm install in prepublish The plugin's node_modules is gitignored, so every fresh CI checkout runs a full npm install inside @omniroute/opencode-plugin during build:cli. npm's unbounded fetch retries turn a stalled registry CDN connection (the recurring onnxruntime-class ETIMEDOUT flake) into a 20-30 minute hang — the DAST 'Build CLI bundle' step has been cancelled at the 30m cap repeatedly. - Bound npm fetch: --fetch-timeout 60s, 2 retries with capped backoff — a stalled connection now fails fast instead of hanging the job. - Retry the install up to 3 times with a 10s pause between attempts, so transient CDN failures recover in-build. Net effect: the step either completes (network OK) or fails quickly with a clear error (network down) — it can no longer eat the whole job budget. * ci(quality): use the npm-ci-retry action on every install step Fast Quality Gates failed on the recurring onnxruntime-node postinstall ETIMEDOUT (Microsoft CDN 150.171.x.x) - the same transient flake that has hit Vitest and dast-smoke today. Only the Build job used the retry action; the other five jobs (Docs, Fast Quality Gates, Vitest, Unit Tests, changelog) still ran a bare install and die on any CDN hiccup. Use the existing retry action (3 attempts, exponential backoff) on every install step for consistency. * Merge branch 'release/v3.8.50' into fix/antigravity-project-autocreate * test(fix): refresh expired alibaba quota sample validity and onnxruntime pin for v3.8.50 base - alibaba-free-tier-quota-fetcher.test.ts: sample quotaValidityPeriod (2026-08-16 16:00 UTC) is in the past, making every quota entry classify as expired/not_capable; bump to 2028-01-01 UTC so the text/merge classification tests exercise the intended path again. - optional-transformers-dependency.test.ts: onnxruntime-node pin assertion updated from ~1.24.3 to ~1.27.0 to match package.json (bumped by #10403); the regular-not-optional intent is unchanged. * test(fix): widen modelsDevSync lastSync wait from 200ms default to 2000ms The truthy-spellings loop asserted each enabled case completes its first fetch within waitFor's 200ms default timeout, which trips under CI runner load (observed on PR 10424 shard 2/4). Match the file's other lastSync waits (2000ms) so the sync-completion assertion is load-tolerant. --------- Co-authored-by: Rouzbeh <rqzbeh@users.noreply.github.com>
445 lines
18 KiB
TypeScript
445 lines
18 KiB
TypeScript
/**
|
|
* Tests: antigravity loadCodeAssist bootstrap before :models discovery.
|
|
*
|
|
* The Google Cloud Code Assist /v1internal:models endpoint requires a prior
|
|
* /v1internal:loadCodeAssist call to assign a project context to the OAuth
|
|
* token. Without this bootstrap, :models returns 404 for all three base URLs.
|
|
*
|
|
* These tests verify:
|
|
* 1. ensureAntigravityProjectAssigned calls loadCodeAssist before returning.
|
|
* 2. The call is memoized — repeated calls for the same token do not re-hit
|
|
* the network.
|
|
* 3. Non-fatal: if loadCodeAssist fails, the function resolves without throwing.
|
|
* 4. The loadCodeAssist request uses the correct headers (Authorization, User-Agent).
|
|
* 5. Ordering guarantee — in a full discovery flow, loadCodeAssist is called
|
|
* BEFORE any :models request.
|
|
*/
|
|
|
|
import { test, describe, beforeEach } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
ensureAntigravityProjectAssigned,
|
|
clearAntigravityProjectCache,
|
|
clearAntigravityOnboardBackoff,
|
|
getAntigravityProjectFromCache,
|
|
getAntigravityLoadCodeAssistUrls,
|
|
ANTIGRAVITY_REQUIRES_MANUAL_PROJECT,
|
|
} from "../../open-sse/services/antigravityProjectBootstrap.ts";
|
|
|
|
// Reset the module-level memoization cache between tests.
|
|
beforeEach(() => {
|
|
clearAntigravityProjectCache();
|
|
});
|
|
|
|
describe("ensureAntigravityProjectAssigned", () => {
|
|
test("calls loadCodeAssist and caches the returned project id", async () => {
|
|
const calls: string[] = [];
|
|
|
|
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
|
|
calls.push(url);
|
|
if (url.endsWith(":loadCodeAssist")) {
|
|
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-from-bootstrap" }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
return new Response("Not Found", { status: 404 });
|
|
};
|
|
|
|
const projectId = await ensureAntigravityProjectAssigned("fake-token-1", mockFetch);
|
|
|
|
const loadCalls = calls.filter((u) => u.endsWith(":loadCodeAssist"));
|
|
assert.ok(loadCalls.length >= 1, ":loadCodeAssist must be called at least once");
|
|
assert.equal(projectId, "proj-from-bootstrap", "project id must be returned");
|
|
assert.equal(
|
|
getAntigravityProjectFromCache("fake-token-1"),
|
|
"proj-from-bootstrap",
|
|
"project id must be memoized after first call"
|
|
);
|
|
});
|
|
|
|
test("subsequent calls for the same token skip the network", async () => {
|
|
let networkCalls = 0;
|
|
|
|
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
|
|
networkCalls += 1;
|
|
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-cached" }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
};
|
|
|
|
await ensureAntigravityProjectAssigned("fake-token-2", mockFetch);
|
|
await ensureAntigravityProjectAssigned("fake-token-2", mockFetch);
|
|
await ensureAntigravityProjectAssigned("fake-token-2", mockFetch);
|
|
|
|
assert.equal(networkCalls, 1, "network must be called exactly once for the same token");
|
|
});
|
|
|
|
test("different tokens each trigger their own loadCodeAssist call", async () => {
|
|
const calledFor: string[] = [];
|
|
|
|
const mockFetch = async (url: string, init?: RequestInit): Promise<Response> => {
|
|
const auth = new Headers(init?.headers).get("Authorization") ?? "";
|
|
calledFor.push(auth);
|
|
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-x" }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
};
|
|
|
|
await ensureAntigravityProjectAssigned("token-A", mockFetch);
|
|
await ensureAntigravityProjectAssigned("token-B", mockFetch);
|
|
|
|
assert.equal(calledFor.length, 2, "each unique token should trigger one network call");
|
|
});
|
|
|
|
test("does not throw when loadCodeAssist returns non-200", async () => {
|
|
const mockFetch = async (_url: string, _init?: RequestInit): Promise<Response> => {
|
|
return new Response("Service Unavailable", { status: 503 });
|
|
};
|
|
|
|
// Must resolve without throwing even if all endpoints fail.
|
|
await assert.doesNotReject(ensureAntigravityProjectAssigned("fail-token", mockFetch));
|
|
});
|
|
|
|
test("does not throw when fetch rejects (network error)", async () => {
|
|
const mockFetch = async (_url: string, _init?: RequestInit): Promise<Response> => {
|
|
throw new Error("ECONNREFUSED");
|
|
};
|
|
|
|
await assert.doesNotReject(ensureAntigravityProjectAssigned("throw-token", mockFetch));
|
|
});
|
|
|
|
test("sets Authorization header with Bearer token", async () => {
|
|
let capturedAuth: string | null = null;
|
|
|
|
const mockFetch = async (_url: string, init?: RequestInit): Promise<Response> => {
|
|
capturedAuth = new Headers(init?.headers).get("Authorization") ?? null;
|
|
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-auth-check" }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
};
|
|
|
|
await ensureAntigravityProjectAssigned("my-secret-token", mockFetch);
|
|
|
|
assert.equal(capturedAuth, "Bearer my-secret-token", "Authorization header must be set");
|
|
});
|
|
|
|
test("uses the official CLI content headers when requested", async () => {
|
|
let capturedHeaders: Headers | null = null;
|
|
|
|
const mockFetch = async (_url: string, init?: RequestInit): Promise<Response> => {
|
|
capturedHeaders = new Headers(init?.headers);
|
|
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-harness" }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
};
|
|
|
|
await ensureAntigravityProjectAssigned("cli-token", mockFetch, "cli");
|
|
|
|
assert.match(
|
|
capturedHeaders?.get("User-Agent") || "",
|
|
/^antigravity\/cli\/1\.1\.5 \(aidev_client; os_type=.+; arch=.+; auth_method=consumer\)$/
|
|
);
|
|
assert.equal(capturedHeaders?.get("X-Goog-Api-Client"), null);
|
|
assert.equal(capturedHeaders?.get("Client-Metadata"), null);
|
|
});
|
|
|
|
test("uses the official IDE native content headers by default", async () => {
|
|
let capturedHeaders: Headers | null = null;
|
|
const mockFetch = async (_url: string, init?: RequestInit): Promise<Response> => {
|
|
capturedHeaders = new Headers(init?.headers);
|
|
return Response.json({ cloudaicompanionProject: "proj-ide" });
|
|
};
|
|
|
|
await ensureAntigravityProjectAssigned("ide-token", mockFetch);
|
|
|
|
assert.match(capturedHeaders?.get("User-Agent") || "", /^antigravity\/ide\/2\.1\.1 /);
|
|
assert.equal(capturedHeaders?.get("X-Goog-Api-Client"), null);
|
|
assert.equal(capturedHeaders?.get("Client-Metadata"), null);
|
|
});
|
|
|
|
test("bootstrap tries loadCodeAssist then onboardUser on 404, non-fatal", async () => {
|
|
const hitUrls: string[] = [];
|
|
|
|
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
|
|
hitUrls.push(url);
|
|
return new Response("not found", { status: 404 });
|
|
};
|
|
|
|
const projectId = await ensureAntigravityProjectAssigned("bootstrap-404-token", mockFetch);
|
|
|
|
// loadCodeAssist returns no project on 404, so the fallback calls
|
|
// onboardUser (also 404). Total: 2 URLs (loadCodeAssist + onboardUser).
|
|
assert.equal(hitUrls.length, 2, "must try loadCodeAssist then onboardUser");
|
|
for (const url of hitUrls) {
|
|
assert.equal(new URL(url).hostname, "cloudcode-pa.googleapis.com");
|
|
}
|
|
assert.equal(projectId, undefined, "a 404 bootstrap is non-fatal and returns undefined");
|
|
});
|
|
|
|
test("getAntigravityLoadCodeAssistUrls returns URLs matching ANTIGRAVITY_BASE_URLS", () => {
|
|
const urls = getAntigravityLoadCodeAssistUrls();
|
|
assert.ok(urls.length >= 1, "must return at least one URL");
|
|
for (const url of urls) {
|
|
assert.ok(url.endsWith(":loadCodeAssist"), `URL must end with :loadCodeAssist, got: ${url}`);
|
|
assert.ok(url.startsWith("https://"), `URL must be HTTPS, got: ${url}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── Ordering guarantee: loadCodeAssist BEFORE :models ─────────────────────────
|
|
//
|
|
// This test simulates the full discovery flow: a test-controlled fetch
|
|
// that records call order, and verifies that :loadCodeAssist precedes
|
|
// any :models request. The integration is verified by calling
|
|
// ensureAntigravityProjectAssigned then simulating a :models request.
|
|
|
|
describe("ordering guarantee: loadCodeAssist before :models", () => {
|
|
test("loadCodeAssist is called before :models in a simulated discovery flow", async () => {
|
|
const callOrder: string[] = [];
|
|
|
|
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
|
|
if (url.endsWith(":loadCodeAssist")) {
|
|
callOrder.push("loadCodeAssist");
|
|
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-order-test" }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
if (url.endsWith(":models")) {
|
|
callOrder.push("models");
|
|
return new Response(
|
|
JSON.stringify({
|
|
models: [{ id: "gemini-3-pro-antigravity", displayName: "Gemini 3 Pro" }],
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
};
|
|
|
|
// Step 1: bootstrap project (what route.ts now does before the models loop).
|
|
await ensureAntigravityProjectAssigned("order-token", mockFetch);
|
|
|
|
// Step 2: simulate a :models discovery request (what the loop does).
|
|
const modelsUrl = "https://cloudcode-pa.googleapis.com/v1internal:models";
|
|
await mockFetch(modelsUrl);
|
|
|
|
const loadIdx = callOrder.indexOf("loadCodeAssist");
|
|
const modelsIdx = callOrder.indexOf("models");
|
|
|
|
assert.ok(loadIdx >= 0, ":loadCodeAssist must be called");
|
|
assert.ok(modelsIdx >= 0, ":models must be called");
|
|
assert.ok(loadIdx < modelsIdx, ":loadCodeAssist must be called BEFORE :models");
|
|
});
|
|
});
|
|
|
|
// ── onboardUser fallback when loadCodeAssist returns no project ──────────
|
|
|
|
describe("onboardUser fallback", () => {
|
|
test("calls onboardUser when loadCodeAssist returns empty, then retries loadCodeAssist", async () => {
|
|
let loadCalls = 0;
|
|
let onboardCalls = 0;
|
|
|
|
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
|
|
if (url.endsWith(":loadCodeAssist")) {
|
|
loadCalls++;
|
|
// First call returns empty, second returns project after onboarding.
|
|
if (loadCalls >= 2) {
|
|
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-after-onboard" }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({}), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
if (url.endsWith(":onboardUser")) {
|
|
onboardCalls++;
|
|
// Google's LRO returns the created project inside the response — a body
|
|
// WITHOUT cloudaicompanionProject means BYOP (manual project required).
|
|
return new Response(
|
|
JSON.stringify({ done: true, cloudaicompanionProject: "proj-onboarded" }),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
return new Response("Not Found", { status: 404 });
|
|
};
|
|
|
|
const projectId = await ensureAntigravityProjectAssigned("onboard-test-token", mockFetch);
|
|
|
|
assert.equal(projectId, "proj-after-onboard");
|
|
assert.equal(onboardCalls, 1, "onboardUser must be called exactly once");
|
|
assert.ok(loadCalls >= 2, "loadCodeAssist must be called twice (before and after onboard)");
|
|
});
|
|
|
|
test("returns undefined when both loadCodeAssist and onboardUser fail", async () => {
|
|
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
|
|
if (url.endsWith(":loadCodeAssist")) {
|
|
return new Response(JSON.stringify({}), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
if (url.endsWith(":onboardUser")) {
|
|
return new Response("Forbidden", { status: 403 });
|
|
}
|
|
return new Response("Not Found", { status: 404 });
|
|
};
|
|
|
|
const projectId = await ensureAntigravityProjectAssigned("both-fail-token", mockFetch);
|
|
assert.equal(projectId, undefined, "must return undefined when both fail");
|
|
});
|
|
|
|
test("does not re-attempt onboardUser within the failure backoff window", async () => {
|
|
let onboardCalls = 0;
|
|
|
|
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
|
|
if (url.endsWith(":loadCodeAssist")) {
|
|
return new Response(JSON.stringify({}), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
if (url.endsWith(":onboardUser")) {
|
|
onboardCalls++;
|
|
// Transient upstream failure (500) — NOT the BYOP signal, so the
|
|
// failure-backoff semantics are what is under test here.
|
|
return new Response("Upstream error", {
|
|
status: 500,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
return new Response("Not Found", { status: 404 });
|
|
};
|
|
|
|
await ensureAntigravityProjectAssigned("dedup-token", mockFetch);
|
|
await ensureAntigravityProjectAssigned("dedup-token", mockFetch);
|
|
|
|
assert.equal(onboardCalls, 1, "onboardUser must be attempted once within the backoff window");
|
|
});
|
|
|
|
test("retries onboardUser after the failure backoff expires (account heals itself)", async () => {
|
|
let onboardCalls = 0;
|
|
|
|
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
|
|
if (url.endsWith(":loadCodeAssist")) {
|
|
// Only the retry AFTER the second (healed) onboard attempt yields a project.
|
|
if (onboardCalls >= 2) {
|
|
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-healed" }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({}), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
if (url.endsWith(":onboardUser")) {
|
|
onboardCalls++;
|
|
if (onboardCalls === 1) {
|
|
// First attempt: transient upstream failure -> failure backoff.
|
|
return new Response("Upstream error", {
|
|
status: 500,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
// Second (healed) attempt: Google returns the created project.
|
|
return new Response(
|
|
JSON.stringify({ done: true, cloudaicompanionProject: "proj-healed-onboard" }),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
return new Response("Not Found", { status: 404 });
|
|
};
|
|
|
|
// First attempt: onboard fails transiently -> failure recorded.
|
|
const first = await ensureAntigravityProjectAssigned("heal-token", mockFetch);
|
|
assert.equal(first, undefined);
|
|
assert.equal(onboardCalls, 1);
|
|
|
|
// Immediately after: backoff blocks a re-attempt.
|
|
const second = await ensureAntigravityProjectAssigned("heal-token", mockFetch);
|
|
assert.equal(second, undefined);
|
|
assert.equal(onboardCalls, 1, "no re-attempt inside the backoff window");
|
|
|
|
// Simulate the backoff expiring: the next request heals the account.
|
|
clearAntigravityOnboardBackoff();
|
|
const healed = await ensureAntigravityProjectAssigned("heal-token", mockFetch);
|
|
assert.equal(healed, "proj-healed");
|
|
assert.equal(onboardCalls, 2, "onboardUser must be retried after backoff expiry");
|
|
});
|
|
|
|
test("returns the BYOP sentinel when onboardUser completes without a project (Google #8491)", async () => {
|
|
let onboardCalls = 0;
|
|
|
|
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
|
|
if (url.endsWith(":loadCodeAssist")) {
|
|
return new Response(JSON.stringify({}), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
if (url.endsWith(":onboardUser")) {
|
|
onboardCalls++;
|
|
// 200 done WITHOUT cloudaicompanionProject = BYOP: Google deprecated
|
|
// automatic project creation for standard-tier personal accounts.
|
|
return new Response(JSON.stringify({ done: true }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
return new Response("Not Found", { status: 404 });
|
|
};
|
|
|
|
const first = await ensureAntigravityProjectAssigned("byop-token", mockFetch);
|
|
assert.equal(first, ANTIGRAVITY_REQUIRES_MANUAL_PROJECT);
|
|
|
|
// The account is cached as BYOP — a second call must NOT re-run the
|
|
// pointless ~18s onboard round-trip (no extra fetch, same sentinel).
|
|
const second = await ensureAntigravityProjectAssigned("byop-token", mockFetch);
|
|
assert.equal(second, ANTIGRAVITY_REQUIRES_MANUAL_PROJECT);
|
|
assert.equal(onboardCalls, 1, "onboardUser must not be re-attempted for a cached BYOP account");
|
|
});
|
|
|
|
test("skips onboardUser when loadCodeAssist succeeds on first try", async () => {
|
|
let onboardCalls = 0;
|
|
|
|
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
|
|
if (url.endsWith(":loadCodeAssist")) {
|
|
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-exists" }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
if (url.endsWith(":onboardUser")) {
|
|
onboardCalls++;
|
|
return new Response(JSON.stringify({ done: true }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
return new Response("Not Found", { status: 404 });
|
|
};
|
|
|
|
const projectId = await ensureAntigravityProjectAssigned("already-ok-token", mockFetch);
|
|
|
|
assert.equal(projectId, "proj-exists");
|
|
assert.equal(onboardCalls, 0, "onboardUser must NOT be called when loadCodeAssist succeeds");
|
|
});
|
|
});
|