372 lines
14 KiB
Swift
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
|
|
}
|
|
}
|