mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-05 22:32:12 +00:00
fix(google-gemini-cli-auth): fix Gemini CLI OAuth failures on Windows (#40729)
* fix(google-gemini-cli-auth): fix Gemini CLI OAuth failures on Windows Two issues prevented Gemini CLI OAuth from working on Windows: 1. resolveGeminiCliDirs: the first candidate `dirname(dirname(resolvedPath))` can resolve to an unrelated ancestor directory (e.g. the nvm root `C:\Users\<user>\AppData\Local\nvm`) when gemini is installed via nvm. The subsequent `findFile` recursive search (depth 10) then picks up an `oauth2.js` from a completely different package (e.g. `discord-api-types/payloads/v10/oauth2.js`), which naturally does not contain Google OAuth credentials, causing silent extraction failure. Fix: validate candidate directories before including them — only keep candidates that contain a `package.json` or a `node_modules/@google/ gemini-cli-core` subdirectory. 2. resolvePlatform: returns "WINDOWS" on win32, but Google's loadCodeAssist API rejects it as an invalid Platform enum value (400 INVALID_ARGUMENT), just like it rejects "LINUX". Fix: use "PLATFORM_UNSPECIFIED" for all non-macOS platforms. * test(google-gemini-cli-auth): keep oauth regressions portable * chore(changelog): add google gemini cli auth fix note --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -108,6 +108,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/auth: serialize async shared-secret auth attempts per client so concurrent Tailscale-capable failures cannot overrun the intended auth rate-limit budget. Thanks @Telecaster2147.
|
||||
- Doctor/config: compare normalized `talk` configs by deep structural equality instead of key-order-sensitive serialization so `openclaw doctor --fix` stops repeatedly reporting/applying no-op `talk.provider/providers` normalization. (#59911) Thanks @ejames-dev.
|
||||
- Gateway/device auth: reuse cached device-token scopes only for cached-token reconnects, while keeping explicit `deviceToken` scope requests and empty-cache fallbacks intact so reconnects preserve `operator.read` without breaking explicit auth flows. (#46032) Thanks @caicongyang.
|
||||
- Google Gemini CLI auth: improve OAuth credential discovery across Windows nvm and Homebrew libexec installs, and align Code Assist metadata so Gemini login stops failing on packaged CLI layouts. (#40729) Thanks @hughcube.
|
||||
- Mattermost/config schema: accept `groups.*.requireMention` again so existing Mattermost configs no longer fail strict validation after upgrade. (#58271) Thanks @MoerAI.
|
||||
- Providers/OpenRouter failover: classify `403 "Key limit exceeded"` spending-limit responses as billing so model fallback continues instead of stopping on generic auth. (#59892) Thanks @rockcent.
|
||||
- Device pairing/security: keep non-operator device scope checks bound to the requested role prefix so bootstrap verification cannot redeem `operator.*` scopes through `node` auth. (#57258) Thanks @jlapenna.
|
||||
|
||||
@@ -56,53 +56,18 @@ export function extractGeminiCliCredentials(): { clientId: string; clientSecret:
|
||||
const resolvedPath = credentialFs.realpathSync(geminiPath);
|
||||
const geminiCliDirs = resolveGeminiCliDirs(geminiPath, resolvedPath);
|
||||
|
||||
let content: string | null = null;
|
||||
for (const geminiCliDir of geminiCliDirs) {
|
||||
const searchPaths = [
|
||||
join(
|
||||
geminiCliDir,
|
||||
"node_modules",
|
||||
"@google",
|
||||
"gemini-cli-core",
|
||||
"dist",
|
||||
"src",
|
||||
"code_assist",
|
||||
"oauth2.js",
|
||||
),
|
||||
join(
|
||||
geminiCliDir,
|
||||
"node_modules",
|
||||
"@google",
|
||||
"gemini-cli-core",
|
||||
"dist",
|
||||
"code_assist",
|
||||
"oauth2.js",
|
||||
),
|
||||
];
|
||||
for (const path of searchPaths) {
|
||||
if (credentialFs.existsSync(path)) {
|
||||
content = credentialFs.readFileSync(path, "utf8");
|
||||
break;
|
||||
}
|
||||
const directCredentials = readGeminiCliCredentialsFromKnownPaths(geminiCliDir);
|
||||
if (directCredentials) {
|
||||
cachedGeminiCliCredentials = directCredentials;
|
||||
return directCredentials;
|
||||
}
|
||||
if (content) {
|
||||
break;
|
||||
}
|
||||
const found = findFile(geminiCliDir, "oauth2.js", 10);
|
||||
if (found) {
|
||||
content = credentialFs.readFileSync(found, "utf8");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const idMatch = content.match(/(\d+-[a-z0-9]+\.apps\.googleusercontent\.com)/);
|
||||
const secretMatch = content.match(/(GOCSPX-[A-Za-z0-9_-]+)/);
|
||||
if (idMatch && secretMatch) {
|
||||
cachedGeminiCliCredentials = { clientId: idMatch[1], clientSecret: secretMatch[1] };
|
||||
return cachedGeminiCliCredentials;
|
||||
const discoveredCredentials = findGeminiCliCredentialsInTree(geminiCliDir, 10);
|
||||
if (discoveredCredentials) {
|
||||
cachedGeminiCliCredentials = discoveredCredentials;
|
||||
return discoveredCredentials;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Gemini CLI not installed or extraction failed
|
||||
@@ -123,17 +88,35 @@ function resolveGeminiCliDirs(geminiPath: string, resolvedPath: string): string[
|
||||
const deduped: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const candidate of candidates) {
|
||||
const key =
|
||||
process.platform === "win32" ? candidate.replace(/\\/g, "/").toLowerCase() : candidate;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
for (const searchDir of resolveGeminiCliSearchDirs(candidate)) {
|
||||
const key =
|
||||
process.platform === "win32" ? searchDir.replace(/\\/g, "/").toLowerCase() : searchDir;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
deduped.push(searchDir);
|
||||
}
|
||||
seen.add(key);
|
||||
deduped.push(candidate);
|
||||
}
|
||||
return deduped;
|
||||
}
|
||||
|
||||
function resolveGeminiCliSearchDirs(candidate: string): string[] {
|
||||
const searchDirs = [
|
||||
candidate,
|
||||
join(candidate, "node_modules", "@google", "gemini-cli"),
|
||||
join(candidate, "lib", "node_modules", "@google", "gemini-cli"),
|
||||
];
|
||||
return searchDirs.filter(looksLikeGeminiCliDir);
|
||||
}
|
||||
|
||||
function looksLikeGeminiCliDir(candidate: string): boolean {
|
||||
return (
|
||||
credentialFs.existsSync(join(candidate, "package.json")) ||
|
||||
credentialFs.existsSync(join(candidate, "node_modules", "@google", "gemini-cli-core"))
|
||||
);
|
||||
}
|
||||
|
||||
function findInPath(name: string): string | null {
|
||||
const exts = process.platform === "win32" ? [".cmd", ".bat", ".exe", ""] : [""];
|
||||
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
||||
@@ -147,18 +130,84 @@ function findInPath(name: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function findFile(dir: string, name: string, depth: number): string | null {
|
||||
function readGeminiCliCredentialsFile(
|
||||
path: string,
|
||||
): { clientId: string; clientSecret: string } | null {
|
||||
try {
|
||||
return parseGeminiCliCredentials(credentialFs.readFileSync(path, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseGeminiCliCredentials(
|
||||
content: string,
|
||||
): { clientId: string; clientSecret: string } | null {
|
||||
const idMatch = content.match(/(\d+-[a-z0-9]+\.apps\.googleusercontent\.com)/);
|
||||
const secretMatch = content.match(/(GOCSPX-[A-Za-z0-9_-]+)/);
|
||||
if (!idMatch || !secretMatch) {
|
||||
return null;
|
||||
}
|
||||
return { clientId: idMatch[1], clientSecret: secretMatch[1] };
|
||||
}
|
||||
|
||||
function readGeminiCliCredentialsFromKnownPaths(
|
||||
geminiCliDir: string,
|
||||
): { clientId: string; clientSecret: string } | null {
|
||||
const searchPaths = [
|
||||
join(
|
||||
geminiCliDir,
|
||||
"node_modules",
|
||||
"@google",
|
||||
"gemini-cli-core",
|
||||
"dist",
|
||||
"src",
|
||||
"code_assist",
|
||||
"oauth2.js",
|
||||
),
|
||||
join(
|
||||
geminiCliDir,
|
||||
"node_modules",
|
||||
"@google",
|
||||
"gemini-cli-core",
|
||||
"dist",
|
||||
"code_assist",
|
||||
"oauth2.js",
|
||||
),
|
||||
];
|
||||
|
||||
for (const path of searchPaths) {
|
||||
if (!credentialFs.existsSync(path)) {
|
||||
continue;
|
||||
}
|
||||
const credentials = readGeminiCliCredentialsFile(path);
|
||||
if (credentials) {
|
||||
return credentials;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findGeminiCliCredentialsInTree(
|
||||
dir: string,
|
||||
depth: number,
|
||||
): { clientId: string; clientSecret: string } | null {
|
||||
if (depth <= 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
for (const entry of credentialFs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isFile() && entry.name === name) {
|
||||
return path;
|
||||
if (entry.isFile() && entry.name === "oauth2.js") {
|
||||
const credentials = readGeminiCliCredentialsFile(path);
|
||||
if (credentials) {
|
||||
return credentials;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
||||
const found = findFile(path, name, depth - 1);
|
||||
const found = findGeminiCliCredentialsInTree(path, depth - 1);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -8,15 +8,11 @@ import {
|
||||
USERINFO_URL,
|
||||
} from "./oauth.shared.js";
|
||||
|
||||
function resolvePlatform(): "WINDOWS" | "MACOS" | "PLATFORM_UNSPECIFIED" {
|
||||
if (process.platform === "win32") {
|
||||
return "WINDOWS";
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
return "MACOS";
|
||||
}
|
||||
return "PLATFORM_UNSPECIFIED";
|
||||
}
|
||||
const LOAD_CODE_ASSIST_METADATA = {
|
||||
ideType: "IDE_UNSPECIFIED",
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
} as const;
|
||||
|
||||
async function getUserEmail(accessToken: string): Promise<string | undefined> {
|
||||
try {
|
||||
@@ -97,24 +93,18 @@ export async function resolveGoogleOAuthIdentity(accessToken: string): Promise<{
|
||||
|
||||
async function discoverProject(accessToken: string): Promise<string> {
|
||||
const envProject = process.env.GOOGLE_CLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT_ID;
|
||||
const platform = resolvePlatform();
|
||||
const metadata = {
|
||||
ideType: "ANTIGRAVITY",
|
||||
platform,
|
||||
pluginType: "GEMINI",
|
||||
};
|
||||
const headers = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "google-api-nodejs-client/9.15.1",
|
||||
"X-Goog-Api-Client": `gl-node/${process.versions.node}`,
|
||||
"Client-Metadata": JSON.stringify(metadata),
|
||||
"Client-Metadata": JSON.stringify(LOAD_CODE_ASSIST_METADATA),
|
||||
};
|
||||
|
||||
const loadBody = {
|
||||
...(envProject ? { cloudaicompanionProject: envProject } : {}),
|
||||
metadata: {
|
||||
...metadata,
|
||||
...LOAD_CODE_ASSIST_METADATA,
|
||||
...(envProject ? { duetProject: envProject } : {}),
|
||||
},
|
||||
};
|
||||
@@ -193,7 +183,7 @@ async function discoverProject(accessToken: string): Promise<string> {
|
||||
const onboardBody: Record<string, unknown> = {
|
||||
tierId,
|
||||
metadata: {
|
||||
...metadata,
|
||||
...LOAD_CODE_ASSIST_METADATA,
|
||||
},
|
||||
};
|
||||
if (tierId !== TIER_FREE && envProject) {
|
||||
|
||||
@@ -91,11 +91,18 @@ describe("extractGeminiCliCredentials", () => {
|
||||
const layout = makeFakeLayout();
|
||||
process.env.PATH = layout.binDir;
|
||||
|
||||
// resolveGeminiCliDirs checks package.json to validate candidate directories
|
||||
const geminiCliDir = join(rootDir, "fake", "lib", "node_modules", "@google", "gemini-cli");
|
||||
const packageJsonPath = normalizePath(join(geminiCliDir, "package.json"));
|
||||
|
||||
mockExistsSync.mockImplementation((p: string) => {
|
||||
const normalized = normalizePath(p);
|
||||
if (normalized === normalizePath(layout.geminiPath)) {
|
||||
return true;
|
||||
}
|
||||
if (normalized === packageJsonPath) {
|
||||
return true;
|
||||
}
|
||||
if (params.oauth2Exists && normalized === normalizePath(layout.oauth2Path)) {
|
||||
return true;
|
||||
}
|
||||
@@ -116,11 +123,9 @@ describe("extractGeminiCliCredentials", () => {
|
||||
const binDir = join(rootDir, "fake", "npm-bin");
|
||||
const geminiPath = join(binDir, "gemini");
|
||||
const resolvedPath = geminiPath;
|
||||
const geminiCliDir = join(binDir, "node_modules", "@google", "gemini-cli");
|
||||
const oauth2Path = join(
|
||||
binDir,
|
||||
"node_modules",
|
||||
"@google",
|
||||
"gemini-cli",
|
||||
geminiCliDir,
|
||||
"node_modules",
|
||||
"@google",
|
||||
"gemini-cli-core",
|
||||
@@ -129,6 +134,7 @@ describe("extractGeminiCliCredentials", () => {
|
||||
"code_assist",
|
||||
"oauth2.js",
|
||||
);
|
||||
const packageJsonPath = normalizePath(join(geminiCliDir, "package.json"));
|
||||
process.env.PATH = binDir;
|
||||
|
||||
mockExistsSync.mockImplementation((p: string) => {
|
||||
@@ -136,6 +142,9 @@ describe("extractGeminiCliCredentials", () => {
|
||||
if (normalized === normalizePath(geminiPath)) {
|
||||
return true;
|
||||
}
|
||||
if (normalized === packageJsonPath) {
|
||||
return true;
|
||||
}
|
||||
if (params.oauth2Exists && normalized === normalizePath(oauth2Path)) {
|
||||
return true;
|
||||
}
|
||||
@@ -147,6 +156,140 @@ describe("extractGeminiCliCredentials", () => {
|
||||
}
|
||||
}
|
||||
|
||||
function installHomebrewLibexecLayout(params: { oauth2Content: string }) {
|
||||
const brewPrefix = join(rootDir, "opt", "homebrew");
|
||||
const cellarRoot = join(brewPrefix, "Cellar", "gemini-cli", "1.2.3");
|
||||
const binDir = join(brewPrefix, "bin");
|
||||
const geminiPath = join(binDir, "gemini");
|
||||
const resolvedPath = join(cellarRoot, "libexec", "bin", "gemini");
|
||||
const geminiCliDir = join(
|
||||
cellarRoot,
|
||||
"libexec",
|
||||
"lib",
|
||||
"node_modules",
|
||||
"@google",
|
||||
"gemini-cli",
|
||||
);
|
||||
const packageJsonPath = normalizePath(join(geminiCliDir, "package.json"));
|
||||
const oauth2Path = join(
|
||||
geminiCliDir,
|
||||
"node_modules",
|
||||
"@google",
|
||||
"gemini-cli-core",
|
||||
"dist",
|
||||
"src",
|
||||
"code_assist",
|
||||
"oauth2.js",
|
||||
);
|
||||
|
||||
process.env.PATH = binDir;
|
||||
mockExistsSync.mockImplementation((p: string) => {
|
||||
const normalized = normalizePath(p);
|
||||
return (
|
||||
normalized === normalizePath(geminiPath) ||
|
||||
normalized === packageJsonPath ||
|
||||
normalized === normalizePath(oauth2Path)
|
||||
);
|
||||
});
|
||||
mockRealpathSync.mockReturnValue(resolvedPath);
|
||||
mockReadFileSync.mockImplementation((p: string) => {
|
||||
if (normalizePath(p) === normalizePath(oauth2Path)) {
|
||||
return params.oauth2Content;
|
||||
}
|
||||
throw new Error(`Unexpected read for ${p}`);
|
||||
});
|
||||
}
|
||||
|
||||
function installWindowsNvmLayoutWithUnrelatedOauth(params: {
|
||||
oauth2Content: string;
|
||||
unrelatedOauth2Content: string;
|
||||
}) {
|
||||
const nvmRoot = join(rootDir, "fake", "Users", "lobster", "AppData", "Local", "nvm");
|
||||
const versionDir = join(nvmRoot, "v24.1.0");
|
||||
const geminiPath = join(versionDir, process.platform === "win32" ? "gemini.cmd" : "gemini");
|
||||
const resolvedPath = geminiPath;
|
||||
const geminiCliDir = join(versionDir, "node_modules", "@google", "gemini-cli");
|
||||
const packageJsonPath = normalizePath(join(geminiCliDir, "package.json"));
|
||||
const oauth2Path = join(
|
||||
geminiCliDir,
|
||||
"node_modules",
|
||||
"@google",
|
||||
"gemini-cli-core",
|
||||
"dist",
|
||||
"src",
|
||||
"code_assist",
|
||||
"oauth2.js",
|
||||
);
|
||||
const unrelatedOauth2Path = join(
|
||||
nvmRoot,
|
||||
"node_modules",
|
||||
"discord-api-types",
|
||||
"payloads",
|
||||
"v10",
|
||||
"oauth2.js",
|
||||
);
|
||||
|
||||
process.env.PATH = versionDir;
|
||||
mockExistsSync.mockImplementation((p: string) => {
|
||||
const normalized = normalizePath(p);
|
||||
return (
|
||||
normalized === normalizePath(geminiPath) ||
|
||||
normalized === packageJsonPath ||
|
||||
normalized === normalizePath(oauth2Path)
|
||||
);
|
||||
});
|
||||
mockRealpathSync.mockReturnValue(resolvedPath);
|
||||
mockReadFileSync.mockImplementation((p: string) => {
|
||||
const normalized = normalizePath(p);
|
||||
if (normalized === normalizePath(oauth2Path)) {
|
||||
return params.oauth2Content;
|
||||
}
|
||||
if (normalized === normalizePath(unrelatedOauth2Path)) {
|
||||
return params.unrelatedOauth2Content;
|
||||
}
|
||||
throw new Error(`Unexpected read for ${p}`);
|
||||
});
|
||||
mockReaddirSync.mockImplementation((p: string) => {
|
||||
const normalized = normalizePath(p);
|
||||
if (normalized === normalizePath(nvmRoot)) {
|
||||
return [dirent("node_modules", true)];
|
||||
}
|
||||
if (normalized === normalizePath(join(nvmRoot, "node_modules"))) {
|
||||
return [dirent("discord-api-types", true)];
|
||||
}
|
||||
if (normalized === normalizePath(join(nvmRoot, "node_modules", "discord-api-types"))) {
|
||||
return [dirent("payloads", true)];
|
||||
}
|
||||
if (
|
||||
normalized === normalizePath(join(nvmRoot, "node_modules", "discord-api-types", "payloads"))
|
||||
) {
|
||||
return [dirent("v10", true)];
|
||||
}
|
||||
if (
|
||||
normalized ===
|
||||
normalizePath(join(nvmRoot, "node_modules", "discord-api-types", "payloads", "v10"))
|
||||
) {
|
||||
return [dirent("oauth2.js", false)];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
return { unrelatedOauth2Path };
|
||||
}
|
||||
|
||||
function dirent(name: string, isDirectory: boolean) {
|
||||
return {
|
||||
name,
|
||||
isBlockDevice: () => false,
|
||||
isCharacterDevice: () => false,
|
||||
isDirectory: () => isDirectory,
|
||||
isFIFO: () => false,
|
||||
isFile: () => !isDirectory,
|
||||
isSocket: () => false,
|
||||
isSymbolicLink: () => false,
|
||||
};
|
||||
}
|
||||
|
||||
function expectFakeCliCredentials(result: unknown) {
|
||||
expect(result).toEqual({
|
||||
clientId: FAKE_CLIENT_ID,
|
||||
@@ -196,6 +339,15 @@ describe("extractGeminiCliCredentials", () => {
|
||||
expectFakeCliCredentials(result);
|
||||
});
|
||||
|
||||
it("extracts credentials from Homebrew libexec installs", async () => {
|
||||
installHomebrewLibexecLayout({ oauth2Content: FAKE_OAUTH2_CONTENT });
|
||||
|
||||
clearCredentialsCache();
|
||||
const result = extractGeminiCliCredentials();
|
||||
|
||||
expectFakeCliCredentials(result);
|
||||
});
|
||||
|
||||
it("returns null when oauth2.js cannot be found", async () => {
|
||||
installGeminiLayout({ oauth2Exists: false, readdir: [] });
|
||||
|
||||
@@ -225,6 +377,23 @@ describe("extractGeminiCliCredentials", () => {
|
||||
expect(result2).toEqual(result1);
|
||||
expect(mockReadFileSync.mock.calls.length).toBe(readCount);
|
||||
});
|
||||
|
||||
it("skips unrelated oauth2.js files when gemini resolves inside a Windows nvm root", async () => {
|
||||
const { unrelatedOauth2Path } = installWindowsNvmLayoutWithUnrelatedOauth({
|
||||
oauth2Content: FAKE_OAUTH2_CONTENT,
|
||||
unrelatedOauth2Content: "// unrelated oauth file",
|
||||
});
|
||||
|
||||
clearCredentialsCache();
|
||||
const result = extractGeminiCliCredentials();
|
||||
|
||||
expectFakeCliCredentials(result);
|
||||
expect(
|
||||
mockReadFileSync.mock.calls.some(
|
||||
([path]) => normalizePath(String(path)) === normalizePath(unrelatedOauth2Path),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loginGeminiCliOAuth", () => {
|
||||
@@ -244,16 +413,11 @@ describe("loginGeminiCliOAuth", () => {
|
||||
"GOOGLE_CLOUD_PROJECT_ID",
|
||||
] as const;
|
||||
|
||||
function getExpectedPlatform(): "WINDOWS" | "MACOS" | "PLATFORM_UNSPECIFIED" {
|
||||
if (process.platform === "win32") {
|
||||
return "WINDOWS";
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
return "MACOS";
|
||||
}
|
||||
// Matches updated resolvePlatform() which uses PLATFORM_UNSPECIFIED for Linux
|
||||
return "PLATFORM_UNSPECIFIED";
|
||||
}
|
||||
const EXPECTED_LOAD_CODE_ASSIST_METADATA = {
|
||||
ideType: "IDE_UNSPECIFIED",
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
} as const;
|
||||
|
||||
function getRequestUrl(input: string | URL | Request): string {
|
||||
return typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
@@ -390,19 +554,11 @@ describe("loginGeminiCliOAuth", () => {
|
||||
|
||||
const clientMetadata = getHeaderValue(firstHeaders, "Client-Metadata");
|
||||
expect(clientMetadata).toBeDefined();
|
||||
expect(JSON.parse(clientMetadata as string)).toEqual({
|
||||
ideType: "ANTIGRAVITY",
|
||||
platform: getExpectedPlatform(),
|
||||
pluginType: "GEMINI",
|
||||
});
|
||||
expect(JSON.parse(clientMetadata as string)).toEqual(EXPECTED_LOAD_CODE_ASSIST_METADATA);
|
||||
|
||||
const body = JSON.parse(String(loadRequests[0]?.init?.body));
|
||||
expect(body).toEqual({
|
||||
metadata: {
|
||||
ideType: "ANTIGRAVITY",
|
||||
platform: getExpectedPlatform(),
|
||||
pluginType: "GEMINI",
|
||||
},
|
||||
metadata: EXPECTED_LOAD_CODE_ASSIST_METADATA,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user