Files
2026-07-13 12:22:33 +08:00

372 lines
14 KiB
Swift

import Foundation
struct GeminiTestEnvironment {
enum GeminiCLILayout {
case npmNested
case nixShare
case fnmBundle
case homebrewBundle
}
let homeURL: URL
private let geminiDir: URL
private let antigravityDir: URL
init() throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let geminiDir = root.appendingPathComponent(".gemini")
try FileManager.default.createDirectory(at: geminiDir, withIntermediateDirectories: true)
let antigravityDir = root
.appendingPathComponent(".codexbar")
.appendingPathComponent("antigravity")
try FileManager.default.createDirectory(at: antigravityDir, withIntermediateDirectories: true)
self.homeURL = root
self.geminiDir = geminiDir
self.antigravityDir = antigravityDir
}
func cleanup() {
try? FileManager.default.removeItem(at: self.homeURL)
}
func writeSettings(authType: String) throws {
let payload: [String: Any] = [
"security": [
"auth": [
"selectedType": authType,
],
],
]
let data = try JSONSerialization.data(withJSONObject: payload)
try data.write(to: self.geminiDir.appendingPathComponent("settings.json"), options: .atomic)
}
func writeCredentials(
accessToken: String?,
refreshToken: String?,
expiry: Date,
idToken: String?) throws
{
var payload: [String: Any] = [
"expiry_date": expiry.timeIntervalSince1970 * 1000,
]
if let accessToken { payload["access_token"] = accessToken }
if let refreshToken { payload["refresh_token"] = refreshToken }
if let idToken { payload["id_token"] = idToken }
let data = try JSONSerialization.data(withJSONObject: payload)
try data.write(to: self.geminiDir.appendingPathComponent("oauth_creds.json"), options: .atomic)
}
func readCredentials() throws -> [String: Any] {
let url = self.geminiDir.appendingPathComponent("oauth_creds.json")
let data = try Data(contentsOf: url)
let object = try JSONSerialization.jsonObject(with: data)
return object as? [String: Any] ?? [:]
}
func writeAntigravityCredentials(
accessToken: String,
refreshToken: String?,
expiry: Date,
idToken: String? = nil,
email: String? = nil,
projectID: String? = nil,
clientID: String? = nil,
clientSecret: String? = nil) throws
{
var payload: [String: Any] = [
"access_token": accessToken,
"expiry_date": expiry.timeIntervalSince1970 * 1000,
]
if let refreshToken { payload["refresh_token"] = refreshToken }
if let idToken { payload["id_token"] = idToken }
if let email { payload["email"] = email }
if let projectID { payload["project_id"] = projectID }
if let clientID { payload["client_id"] = clientID }
if let clientSecret { payload["client_secret"] = clientSecret }
let data = try JSONSerialization.data(withJSONObject: payload)
try data.write(to: self.antigravityDir.appendingPathComponent("oauth_creds.json"), options: .atomic)
}
func readAntigravityCredentials() throws -> [String: Any] {
let url = self.antigravityDir.appendingPathComponent("oauth_creds.json")
let data = try Data(contentsOf: url)
let object = try JSONSerialization.jsonObject(with: data)
return object as? [String: Any] ?? [:]
}
func writeFakeGeminiCLI(includeOAuth: Bool = true, layout: GeminiCLILayout = .npmNested) throws -> URL {
let base = self.homeURL.appendingPathComponent("gemini-cli")
let binDir = base.appendingPathComponent("bin")
try FileManager.default.createDirectory(at: binDir, withIntermediateDirectories: true)
switch layout {
case .npmNested:
let oauthPath = base
.appendingPathComponent("lib")
.appendingPathComponent("node_modules")
.appendingPathComponent("@google")
.appendingPathComponent("gemini-cli")
.appendingPathComponent("node_modules")
.appendingPathComponent("@google")
.appendingPathComponent("gemini-cli-core")
.appendingPathComponent("dist")
.appendingPathComponent("src")
.appendingPathComponent("code_assist")
.appendingPathComponent("oauth2.js")
if includeOAuth {
try FileManager.default.createDirectory(
at: oauthPath.deletingLastPathComponent(),
withIntermediateDirectories: true)
let oauthContent = """
const OAUTH_CLIENT_ID = 'test-client-id';
const OAUTH_CLIENT_SECRET = 'test-client-secret';
"""
try oauthContent.write(to: oauthPath, atomically: true, encoding: .utf8)
}
let geminiBinary = binDir.appendingPathComponent("gemini")
try "#!/bin/bash\nexit 0\n".write(to: geminiBinary, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes(
[.posixPermissions: 0o755],
ofItemAtPath: geminiBinary.path)
return geminiBinary
case .nixShare:
let oauthPath = base
.appendingPathComponent("share")
.appendingPathComponent("gemini-cli")
.appendingPathComponent("node_modules")
.appendingPathComponent("@google")
.appendingPathComponent("gemini-cli-core")
.appendingPathComponent("dist")
.appendingPathComponent("src")
.appendingPathComponent("code_assist")
.appendingPathComponent("oauth2.js")
if includeOAuth {
try FileManager.default.createDirectory(
at: oauthPath.deletingLastPathComponent(),
withIntermediateDirectories: true)
let oauthContent = """
const OAUTH_CLIENT_ID = 'test-client-id';
const OAUTH_CLIENT_SECRET = 'test-client-secret';
"""
try oauthContent.write(to: oauthPath, atomically: true, encoding: .utf8)
}
let geminiBinary = binDir.appendingPathComponent("gemini")
try "#!/bin/bash\nexit 0\n".write(to: geminiBinary, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes(
[.posixPermissions: 0o755],
ofItemAtPath: geminiBinary.path)
return geminiBinary
case .fnmBundle:
// Mirror a real fnm multishell layout: bin/gemini is a single relative
// symlink into the same multishell's lib/node_modules/@google/gemini-cli,
// which is a plain directory with the real package.json + bundle/*.js.
let multishellRoot = self.homeURL
.appendingPathComponent("Library")
.appendingPathComponent("Caches")
.appendingPathComponent("fnm_multishells")
.appendingPathComponent("12345_67890")
let binDir = multishellRoot.appendingPathComponent("bin")
let packageRoot = multishellRoot
.appendingPathComponent("lib")
.appendingPathComponent("node_modules")
.appendingPathComponent("@google")
.appendingPathComponent("gemini-cli")
let bundleDir = packageRoot.appendingPathComponent("bundle")
try FileManager.default.createDirectory(at: binDir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: bundleDir, withIntermediateDirectories: true)
let packageJSON = """
{
"name": "@google/gemini-cli"
}
"""
try packageJSON.write(
to: packageRoot.appendingPathComponent("package.json"),
atomically: true,
encoding: .utf8)
let chunkName = "chunk-TEST123.js"
let geminiEntry = bundleDir.appendingPathComponent("gemini.js")
let geminiContent = """
#!/usr/bin/env node
import { start } from "./\(chunkName)";
start();
"""
try geminiContent.write(to: geminiEntry, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes(
[.posixPermissions: 0o755],
ofItemAtPath: geminiEntry.path)
let chunkContent = if includeOAuth {
"""
export const start = () => {};
const OAUTH_CLIENT_ID = 'test-client-id';
const OAUTH_CLIENT_SECRET = 'test-client-secret';
"""
} else {
"export const start = () => {};\n"
}
try chunkContent.write(
to: bundleDir.appendingPathComponent(chunkName),
atomically: true,
encoding: .utf8)
// Relative symlink matching what fnm actually creates:
// fnm_multishells/XXX/bin/gemini -> ../lib/node_modules/@google/gemini-cli/bundle/gemini.js
// Use the path-based API so the target is stored as a literal relative
// string; the URL-based API resolves URL(fileURLWithPath: "../...") against
// the process CWD, which produces a bogus absolute target.
let geminiBinary = binDir.appendingPathComponent("gemini")
try FileManager.default.createSymbolicLink(
atPath: geminiBinary.path,
withDestinationPath: "../lib/node_modules/@google/gemini-cli/bundle/gemini.js")
return geminiBinary
case .homebrewBundle:
return try self.writeFakeHomebrewGeminiCLI(base: base, includeOAuth: includeOAuth)
}
}
private func writeFakeHomebrewGeminiCLI(base: URL, includeOAuth: Bool) throws -> URL {
let cellarRoot = base
.appendingPathComponent("Cellar")
.appendingPathComponent("gemini-cli")
.appendingPathComponent("0.41.2")
let binDir = cellarRoot.appendingPathComponent("bin")
let packageRoot = cellarRoot
.appendingPathComponent("libexec")
.appendingPathComponent("lib")
.appendingPathComponent("node_modules")
.appendingPathComponent("@google")
.appendingPathComponent("gemini-cli")
let bundleDir = packageRoot.appendingPathComponent("bundle")
try FileManager.default.createDirectory(at: binDir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: bundleDir, withIntermediateDirectories: true)
let packageJSON = """
{
"name": "@google/gemini-cli"
}
"""
try packageJSON.write(
to: packageRoot.appendingPathComponent("package.json"),
atomically: true,
encoding: .utf8)
let entry = bundleDir.appendingPathComponent("gemini.js")
try "#!/usr/bin/env node\nawait import('./gemini-HASH.js');\n".write(
to: entry,
atomically: true,
encoding: .utf8)
try FileManager.default.setAttributes(
[.posixPermissions: 0o755],
ofItemAtPath: entry.path)
try "export const run = () => {};\n".write(
to: bundleDir.appendingPathComponent("gemini-HASH.js"),
atomically: true,
encoding: .utf8)
let chunkContent = if includeOAuth {
"""
var OAUTH_CLIENT_ID = "test-client-id";
var OAUTH_CLIENT_SECRET = "test-client-secret";
"""
} else {
"export const unrelated = true;\n"
}
try chunkContent.write(
to: bundleDir.appendingPathComponent("chunk-OAUTH.js"),
atomically: true,
encoding: .utf8)
let geminiBinary = binDir.appendingPathComponent("gemini")
try FileManager.default.createSymbolicLink(
atPath: geminiBinary.path,
withDestinationPath: "../libexec/lib/node_modules/@google/gemini-cli/bundle/gemini.js")
return geminiBinary
}
func writeFakeFnm(
currentVersion: String = "v24.6.0",
npmRoot: String? = nil,
geminiPackageJSONPath: String,
holdNpmRootStdoutOpen: Bool = false) throws -> URL
{
let binDir = self.homeURL.appendingPathComponent("bin")
try FileManager.default.createDirectory(at: binDir, withIntermediateDirectories: true)
let fnmPath = binDir.appendingPathComponent("fnm")
let stdoutHolder = holdNpmRootStdoutOpen
? #"""
python3 - <<'PY'
import os
import subprocess
import sys
child = subprocess.Popen(
[sys.executable, "-c", "import time; time.sleep(30)"],
stdin=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
with open(os.environ["CODEXBAR_TEST_CHILD_PID_FILE"], "w") as handle:
handle.write(str(child.pid))
PY
"""#
: ":"
let script = if let npmRoot {
"""
#!/bin/bash
if [ "$1" = "current" ]; then
printf '%s\n' "\(currentVersion)"
exit 0
fi
if [ "$1" = "exec" ] && [ "$4" = "npm" ] && [ "$5" = "root" ] && [ "$6" = "-g" ]; then
\(stdoutHolder)
printf '%s\n' "\(npmRoot)"
exit 0
fi
if [ "$1" = "exec" ] && [ "$4" = "node" ]; then
printf '%s\n' "\(geminiPackageJSONPath)"
exit 0
fi
exit 1
"""
} else {
"""
#!/bin/bash
if [ "$1" = "current" ]; then
printf '%s\n' "\(currentVersion)"
exit 0
fi
if [ "$1" = "exec" ]; then
printf '%s\n' "\(geminiPackageJSONPath)"
exit 0
fi
exit 1
"""
}
try script.write(to: fnmPath, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes(
[.posixPermissions: 0o755],
ofItemAtPath: fnmPath.path)
return fnmPath
}
}