| 1 | import * as ScriptProgress from "@t3tools/provider-core/server/ScriptProgress"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import { expect, it } from "@effect/vitest"; |
| 4 | import { |
| 5 | ANTIGRAVITY_DEFAULT_MODEL, |
| 6 | ProviderInstanceId, |
| 7 | type AntigravitySettings, |
| 8 | } from "@t3tools/contracts"; |
| 9 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 10 | import * as Effect from "effect/Effect"; |
| 11 | import * as Deferred from "effect/Deferred"; |
| 12 | import * as Fiber from "effect/Fiber"; |
| 13 | import * as FileSystem from "effect/FileSystem"; |
| 14 | import * as Layer from "effect/Layer"; |
| 15 | import * as Path from "effect/Path"; |
| 16 | import * as Schema from "effect/Schema"; |
| 17 | import * as TestClock from "effect/testing/TestClock"; |
| 18 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; |
| 19 | import { HttpClient } from "effect/http"; |
| 20 | |
| 21 | import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts"; |
| 22 | import * as ServerConfig from "../../config.ts"; |
| 23 | import * as ServerSettings from "../../serverSettings.ts"; |
| 24 | import * as AntigravityInstallation from "../AntigravityInstallation.ts"; |
| 25 | import { |
| 26 | ANTIGRAVITY_AUTH_STDOUT_PREFIX, |
| 27 | resolveAntigravityInstanceDirectories, |
| 28 | } from "../antigravityAuthSupport.ts"; |
| 29 | import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers"; |
| 30 | import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; |
| 31 | import * as ModelManifest from "../ModelManifest.ts"; |
| 32 | import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; |
| 33 | import { AntigravityDriver } from "./AntigravityDriver.ts"; |
| 34 | import * as ProviderHostLive from "../ProviderHostLive.ts"; |
| 35 | import * as ServerSecretStore from "../../auth/ServerSecretStore.ts"; |
| 36 | |
| 37 | const hostPlatform = HostProcess.Platform.defaultValue(); |
| 38 | const windowsHost = hostPlatform === "win32"; |
| 39 | const decodeRequest = Schema.decodeEffect( |
| 40 | Schema.fromJsonString( |
| 41 | Schema.Struct({ |
| 42 | method: Schema.optional(Schema.String), |
| 43 | params: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), |
| 44 | }), |
| 45 | ), |
| 46 | ); |
| 47 | const blockedCredentialKeys = new Set([ |
| 48 | "GEMINI_API_KEY", |
| 49 | "GOOGLE_API_KEY", |
| 50 | "GOOGLE_APPLICATION_CREDENTIALS", |
| 51 | "GOOGLE_GENAI_USE_VERTEXAI", |
| 52 | ]); |
| 53 | |
| 54 | function shellQuote(value: string): string { |
| 55 | return `'${value.replaceAll("'", `'"'"'`)}'`; |
| 56 | } |
| 57 | |
| 58 | const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( |
| 59 | options: { readonly config?: Partial<AntigravitySettings>; readonly enabled?: boolean } = {}, |
| 60 | ) { |
| 61 | const fs = yield* FileSystem.FileSystem; |
| 62 | const path = yield* Path.Path; |
| 63 | const config = yield* ServerConfig.ServerConfig; |
| 64 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 65 | const nodePath = yield* HostProcess.ExecutablePath; |
| 66 | const baseEnv = yield* HostProcess.Environment; |
| 67 | const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-antigravity-driver-" }); |
| 68 | const instanceId = ProviderInstanceId.make(path.basename(root)); |
| 69 | const mockAgentPath = yield* path.fromFileUrl( |
| 70 | new URL("../../../scripts/acp-mock-agent.ts", import.meta.url), |
| 71 | ); |
| 72 | const requestLog = path.join(root, "requests.jsonl"); |
| 73 | const directories = yield* resolveAntigravityInstanceDirectories(config.stateDir, instanceId); |
| 74 | const profileDirectory = directories.profile; |
| 75 | const instancePath = `${path.join(root, "instance-bin")}:${baseEnv.PATH ?? ""}`; |
| 76 | |
| 77 | const makeExecutable = Effect.fn("AntigravityDriverTest.makeExecutable")(function* ( |
| 78 | name: string, |
| 79 | loginRequired = false, |
| 80 | ) { |
| 81 | const directory = path.join(root, name); |
| 82 | const executablePath = path.join(directory, "agy_acp_server.par"); |
| 83 | const harnessPath = path.join(directory, "localharness_external"); |
| 84 | yield* fs.makeDirectory(directory, { recursive: true }); |
| 85 | const authorizationUrl = |
| 86 | "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A51234%2F&state=fixture-state"; |
| 87 | yield* fs.writeFileString( |
| 88 | executablePath, |
| 89 | [ |
| 90 | "#!/bin/sh", |
| 91 | ...(loginRequired |
| 92 | ? [`printf '%s\\n' ${shellQuote(ANTIGRAVITY_AUTH_STDOUT_PREFIX + authorizationUrl)}`] |
| 93 | : []), |
| 94 | `exec ${shellQuote(nodePath)} ${shellQuote(mockAgentPath)} "$@"`, |
| 95 | "", |
| 96 | ].join("\n"), |
| 97 | ); |
| 98 | yield* fs.writeFileString(harnessPath, "#!/bin/sh\nexit 99\n"); |
| 99 | yield* fs.chmod(executablePath, 0o755); |
| 100 | yield* fs.chmod(harnessPath, 0o755); |
| 101 | return { |
| 102 | executablePath, |
| 103 | harnessPath, |
| 104 | source: "managed", |
| 105 | version: name, |
| 106 | managedVersionDirectory: directory, |
| 107 | } satisfies AntigravityInstallation.AntigravityExecutable; |
| 108 | }); |
| 109 | |
| 110 | const first = yield* makeExecutable("runtime 'one"); |
| 111 | const second = yield* makeExecutable("runtime two"); |
| 112 | const signedOut = yield* makeExecutable("runtime signed-out", true); |
| 113 | const controls = { selected: first, failResolution: false, beforeAcquire: Effect.void }; |
| 114 | const acquisitions: Array<{ binaryPath: string | undefined; path: string | undefined }> = []; |
| 115 | const releases: Array<string | null> = []; |
| 116 | const launches: Array<{ |
| 117 | command: string; |
| 118 | args: ReadonlyArray<string>; |
| 119 | cwd: string | undefined; |
| 120 | extendEnv: boolean | undefined; |
| 121 | profileDirectory: string | undefined; |
| 122 | harnessPath: string | undefined; |
| 123 | forceFileStorage: string | undefined; |
| 124 | credentialKeys: ReadonlyArray<string>; |
| 125 | geminiApiKey: string | undefined; |
| 126 | tempDirectory: string | undefined; |
| 127 | handle: ChildProcessSpawner.ChildProcessHandle; |
| 128 | }> = []; |
| 129 | |
| 130 | const layerInstallation = Layer.mock(AntigravityInstallation.AntigravityInstallation)({ |
| 131 | managedDirectory: root, |
| 132 | resolve: () => |
| 133 | Effect.gen(function* () { |
| 134 | if (controls.failResolution) { |
| 135 | return yield* new AntigravityInstallation.AntigravityInstallationError({ |
| 136 | operation: "resolve", |
| 137 | detail: "Fixture resolution failed.", |
| 138 | }); |
| 139 | } |
| 140 | return controls.selected; |
| 141 | }), |
| 142 | acquire: (binaryPath, environment) => |
| 143 | Effect.gen(function* () { |
| 144 | acquisitions.push({ binaryPath, path: environment?.PATH }); |
| 145 | yield* controls.beforeAcquire; |
| 146 | if (controls.failResolution) { |
| 147 | return yield* new AntigravityInstallation.AntigravityInstallationError({ |
| 148 | operation: "resolve", |
| 149 | detail: "Fixture resolution failed.", |
| 150 | }); |
| 151 | } |
| 152 | const selected = controls.selected; |
| 153 | yield* Effect.addFinalizer(() => |
| 154 | Effect.sync(() => { |
| 155 | releases.push(selected.version); |
| 156 | }), |
| 157 | ); |
| 158 | return selected; |
| 159 | }), |
| 160 | }); |
| 161 | const observedSpawner = ChildProcessSpawner.make((command) => |
| 162 | Effect.gen(function* () { |
| 163 | if (command._tag !== "StandardCommand") |
| 164 | return yield* Effect.die("Unexpected process pipeline."); |
| 165 | const handle = yield* spawner.spawn(command); |
| 166 | const environment = command.options.env ?? {}; |
| 167 | launches.push({ |
| 168 | command: command.command, |
| 169 | args: command.args, |
| 170 | cwd: command.options.cwd, |
| 171 | extendEnv: command.options.extendEnv, |
| 172 | profileDirectory: environment.GEMINI_HOME, |
| 173 | harnessPath: environment.ANTIGRAVITY_HARNESS_PATH, |
| 174 | forceFileStorage: environment.AGY_ACP_FORCE_FILE_STORAGE, |
| 175 | credentialKeys: Object.keys(environment).filter((key) => |
| 176 | blockedCredentialKeys.has(key.toUpperCase()), |
| 177 | ), |
| 178 | geminiApiKey: environment.GEMINI_API_KEY, |
| 179 | // Only the agent gets a per-process temp directory. Other launches |
| 180 | // inherit the host TMPDIR. |
| 181 | tempDirectory: |
| 182 | environment.ANTIGRAVITY_HARNESS_PATH === undefined ? undefined : environment.TMPDIR, |
| 183 | handle, |
| 184 | }); |
| 185 | return handle; |
| 186 | }), |
| 187 | ); |
| 188 | const instance = yield* AntigravityDriver.create({ |
| 189 | instanceId, |
| 190 | displayName: "Google test account", |
| 191 | enabled: options.enabled ?? true, |
| 192 | config: { ...AntigravityDriver.defaultConfig(), ...options.config }, |
| 193 | environment: [ |
| 194 | { name: "PATH", value: instancePath }, |
| 195 | { name: "T3_ACP_ANTIGRAVITY", value: "1" }, |
| 196 | { name: "T3_ACP_REQUEST_LOG_PATH", value: requestLog }, |
| 197 | { name: "GEMINI_API_KEY", value: "must-not-be-used" }, |
| 198 | { name: "google_api_key", value: "must-not-be-used" }, |
| 199 | { name: "GOOGLE_APPLICATION_CREDENTIALS", value: "/must-not-be-used.json" }, |
| 200 | { name: "GOOGLE_GENAI_USE_VERTEXAI", value: "true" }, |
| 201 | { name: "GEMINI_HOME", value: "/must-not-be-used" }, |
| 202 | { name: "ANTIGRAVITY_HARNESS_PATH", value: "/must-not-be-used" }, |
| 203 | { name: "BROWSER", value: "must-not-run" }, |
| 204 | ].map((variable) => ({ ...variable, sensitive: false })), |
| 205 | }).pipe( |
| 206 | Effect.provide(layerInstallation), |
| 207 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, observedSpawner), |
| 208 | ); |
| 209 | const refresh = instance.refreshModels; |
| 210 | if (!refresh) return yield* Effect.die("Antigravity does not expose model refresh."); |
| 211 | const readRequests = Effect.gen(function* () { |
| 212 | if (!(yield* fs.exists(requestLog))) return []; |
| 213 | const text = yield* fs.readFileString(requestLog); |
| 214 | return yield* Effect.forEach(text.split(/\r?\n/u).filter(Boolean), (line) => |
| 215 | decodeRequest(line), |
| 216 | ); |
| 217 | }); |
| 218 | const assertClosed = Effect.gen(function* () { |
| 219 | for (const launch of launches) { |
| 220 | // Cancelled startup can report a signal instead of a numeric exit code. |
| 221 | yield* launch.handle.exitCode.pipe(Effect.ignore); |
| 222 | expect(yield* launch.handle.isRunning).toBe(false); |
| 223 | if (launch.cwd) expect(yield* fs.exists(launch.cwd)).toBe(false); |
| 224 | if (launch.tempDirectory) expect(yield* fs.exists(launch.tempDirectory)).toBe(false); |
| 225 | } |
| 226 | }); |
| 227 | return { |
| 228 | instance, |
| 229 | refresh, |
| 230 | fs, |
| 231 | path, |
| 232 | profileDirectory, |
| 233 | directories, |
| 234 | instancePath, |
| 235 | first, |
| 236 | second, |
| 237 | signedOut, |
| 238 | controls, |
| 239 | acquisitions, |
| 240 | releases, |
| 241 | launches, |
| 242 | readRequests, |
| 243 | assertClosed, |
| 244 | }; |
| 245 | }); |
| 246 | |
| 247 | const layerDeps = ServerConfig.layerTest(process.cwd(), { |
| 248 | prefix: "t3-antigravity-driver-config-", |
| 249 | }).pipe( |
| 250 | Layer.provideMerge(NodeServices.layer), |
| 251 | Layer.provideMerge( |
| 252 | Layer.succeed( |
| 253 | HttpClient.HttpClient, |
| 254 | HttpClient.make(() => Effect.die("Unexpected quota request")), |
| 255 | ), |
| 256 | ), |
| 257 | Layer.provideMerge(ServerSettings.layerTest()), |
| 258 | Layer.provideMerge( |
| 259 | Layer.mock(BackgroundPolicy.BackgroundPolicy)({ |
| 260 | shouldRunScopeWork: () => Effect.succeed(false), |
| 261 | }), |
| 262 | ), |
| 263 | Layer.provideMerge( |
| 264 | Layer.succeed( |
| 265 | ProviderEventLoggers.ProviderEventLoggers, |
| 266 | ProviderEventLoggers.NoOpProviderEventLoggers, |
| 267 | ), |
| 268 | ), |
| 269 | Layer.provideMerge(ModelManifest.layerTest), |
| 270 | Layer.provideMerge(IdAllocator.layer), |
| 271 | Layer.provideMerge(Layer.merge(McpProviderSessions.layer, ScriptProgress.layerUnavailable)), |
| 272 | ); |
| 273 | const layerTest = ProviderHostLive.layer.pipe( |
| 274 | Layer.provideMerge(ServerSecretStore.layer), |
| 275 | Layer.provideMerge(layerDeps), |
| 276 | ); |
| 277 | |
| 278 | it.layer(layerTest)("AntigravityDriver", (it) => { |
| 279 | it.effect.skipIf(windowsHost)( |
| 280 | "preserves the Node install message when starting a standalone provider", |
| 281 | () => |
| 282 | Effect.gen(function* () { |
| 283 | const h = yield* makeHarness(); |
| 284 | const error = yield* h.refresh().pipe(Effect.flip); |
| 285 | expect(error.detail).toContain("Install Node.js"); |
| 286 | expect(h.launches).toEqual([]); |
| 287 | }).pipe( |
| 288 | Effect.scoped, |
| 289 | Effect.provideService(HostProcess.IsExecutable, true), |
| 290 | Effect.provideService(HostProcess.Environment, { PATH: "" }), |
| 291 | ), |
| 292 | ); |
| 293 | |
| 294 | it.effect.skipIf(windowsHost)("does not launch a process for a disabled instance", () => |
| 295 | Effect.gen(function* () { |
| 296 | const h = yield* makeHarness({ enabled: false }); |
| 297 | const snapshot = yield* h.instance.snapshot.refresh; |
| 298 | expect(snapshot.status).toBe("disabled"); |
| 299 | expect(h.acquisitions).toEqual([]); |
| 300 | expect(h.launches).toEqual([]); |
| 301 | expect(yield* h.fs.exists(h.profileDirectory)).toBe(false); |
| 302 | }).pipe(Effect.scoped), |
| 303 | ); |
| 304 | |
| 305 | it.effect.skipIf(windowsHost)("refreshes models after slow process startup", () => |
| 306 | Effect.gen(function* () { |
| 307 | const h = yield* makeHarness(); |
| 308 | const entered = yield* Deferred.make<void>(); |
| 309 | const ready = yield* Deferred.make<void>(); |
| 310 | h.controls.beforeAcquire = Deferred.succeed(entered, undefined).pipe( |
| 311 | Effect.andThen(Deferred.await(ready)), |
| 312 | ); |
| 313 | const refresh = yield* h.refresh().pipe(Effect.forkScoped); |
| 314 | yield* Deferred.await(entered); |
| 315 | yield* TestClock.adjust("70 seconds"); |
| 316 | yield* Deferred.succeed(ready, undefined); |
| 317 | yield* Fiber.join(refresh); |
| 318 | const snapshot = yield* h.instance.snapshot.getSnapshot; |
| 319 | expect(snapshot.auth.status).toBe("authenticated"); |
| 320 | expect(snapshot.models.length).toBeGreaterThan(0); |
| 321 | yield* h.assertClosed; |
| 322 | }).pipe(Effect.scoped), |
| 323 | ); |
| 324 | |
| 325 | it.effect.skipIf(windowsHost)( |
| 326 | "refreshes through the selected executable and personal Google ACP", |
| 327 | () => |
| 328 | Effect.gen(function* () { |
| 329 | const h = yield* makeHarness(); |
| 330 | yield* h.refresh(); |
| 331 | const snapshot = yield* h.instance.snapshot.getSnapshot; |
| 332 | expect(snapshot.status).toBe("ready"); |
| 333 | expect(snapshot.auth.status).toBe("authenticated"); |
| 334 | expect(snapshot.models.map((model) => model.slug)).toEqual([ |
| 335 | "gemini-test-low", |
| 336 | "gemini-test-high", |
| 337 | ]); |
| 338 | expect(snapshot.models[0]?.aliases).toContain(ANTIGRAVITY_DEFAULT_MODEL); |
| 339 | expect(snapshot.models.every((model) => model.isLegacy !== true)).toBe(true); |
| 340 | expect(snapshot.slashCommands.map((command) => command.name)).toEqual(["plan", "logout"]); |
| 341 | expect(snapshot.supportsTextGeneration).toBe(true); |
| 342 | h.controls.selected = h.second; |
| 343 | yield* h.refresh(); |
| 344 | |
| 345 | expect(h.acquisitions).toEqual([ |
| 346 | { binaryPath: "", path: h.instancePath }, |
| 347 | { binaryPath: "", path: h.instancePath }, |
| 348 | ]); |
| 349 | const nativeLaunches = h.launches.filter((launch) => launch.harnessPath !== undefined); |
| 350 | expect(nativeLaunches.map((launch) => launch.command)).toEqual([ |
| 351 | h.first.executablePath, |
| 352 | h.second.executablePath, |
| 353 | ]); |
| 354 | expect(nativeLaunches.map((launch) => launch.harnessPath)).toEqual([ |
| 355 | h.first.harnessPath, |
| 356 | h.second.harnessPath, |
| 357 | ]); |
| 358 | for (const launch of nativeLaunches) { |
| 359 | expect(launch.args).toEqual(hostPlatform === "linux" ? ["--uid="] : []); |
| 360 | expect(launch.profileDirectory).toBe(h.profileDirectory); |
| 361 | expect(launch.forceFileStorage).toBe("1"); |
| 362 | expect(launch.extendEnv).toBe(false); |
| 363 | } |
| 364 | for (const launch of h.launches) expect(launch.credentialKeys).toEqual([]); |
| 365 | expect(h.releases).toEqual([h.first.version, h.second.version]); |
| 366 | const requests = yield* h.readRequests; |
| 367 | expect(requests.map((request) => request.method)).toEqual([ |
| 368 | "initialize", |
| 369 | "auth/login", |
| 370 | "session/new", |
| 371 | "initialize", |
| 372 | "auth/login", |
| 373 | "session/new", |
| 374 | ]); |
| 375 | expect( |
| 376 | requests |
| 377 | .filter((request) => request.method === "auth/login") |
| 378 | .map((request) => request.params?.methodId), |
| 379 | ).toEqual(["oauth-personal", "oauth-personal"]); |
| 380 | expect( |
| 381 | requests |
| 382 | .filter((request) => request.method === "session/new") |
| 383 | .map((request) => request.params?.mcpServers), |
| 384 | ).toEqual([[], []]); |
| 385 | yield* h.assertClosed; |
| 386 | }).pipe(Effect.scoped), |
| 387 | ); |
| 388 | |
| 389 | it.effect.skipIf(windowsHost)( |
| 390 | "authenticates with the configured API key method and labels the account by method", |
| 391 | () => |
| 392 | Effect.gen(function* () { |
| 393 | const h = yield* makeHarness({ |
| 394 | config: { authMethod: "gemini-api-key", apiKey: "fixture-gemini-key" }, |
| 395 | }); |
| 396 | yield* h.refresh(); |
| 397 | const snapshot = yield* h.instance.snapshot.getSnapshot; |
| 398 | expect(snapshot.auth).toMatchObject({ |
| 399 | status: "authenticated", |
| 400 | type: "gemini-api-key", |
| 401 | label: "Gemini API key", |
| 402 | }); |
| 403 | expect(snapshot.models.length).toBeGreaterThan(0); |
| 404 | const nativeLaunches = h.launches.filter((launch) => launch.harnessPath !== undefined); |
| 405 | expect(nativeLaunches.map((launch) => launch.geminiApiKey)).toEqual(["fixture-gemini-key"]); |
| 406 | const requests = yield* h.readRequests; |
| 407 | expect( |
| 408 | requests |
| 409 | .filter((request) => request.method === "auth/login") |
| 410 | .map((request) => request.params?.methodId), |
| 411 | ).toEqual(["gemini-api-key"]); |
| 412 | yield* h.assertClosed; |
| 413 | }).pipe(Effect.scoped), |
| 414 | ); |
| 415 | |
| 416 | it.effect.skipIf(windowsHost)("reports the missing credential before launching a process", () => |
| 417 | Effect.gen(function* () { |
| 418 | const h = yield* makeHarness({ config: { authMethod: "gemini-api-key" } }); |
| 419 | const error = yield* h.refresh().pipe(Effect.flip); |
| 420 | expect(error.detail).toContain("API key"); |
| 421 | expect(h.launches.filter((launch) => launch.harnessPath !== undefined)).toEqual([]); |
| 422 | }).pipe(Effect.scoped), |
| 423 | ); |
| 424 | |
| 425 | it.effect.skipIf(windowsHost)( |
| 426 | "closes refresh processes and clears account metadata when Google sign-in is required", |
| 427 | () => |
| 428 | Effect.gen(function* () { |
| 429 | const h = yield* makeHarness(); |
| 430 | yield* h.refresh(); |
| 431 | h.controls.selected = h.signedOut; |
| 432 | const error = yield* h.refresh().pipe(Effect.flip); |
| 433 | expect(error.detail).toContain("Sign in to Antigravity"); |
| 434 | const snapshot = yield* h.instance.snapshot.getSnapshot; |
| 435 | expect(snapshot.auth.status).toBe("unauthenticated"); |
| 436 | expect(snapshot.models).toEqual([]); |
| 437 | expect(snapshot.slashCommands).toEqual([]); |
| 438 | expect(snapshot.supportsTextGeneration).toBe(false); |
| 439 | expect(h.acquisitions).toHaveLength(2); |
| 440 | expect(h.releases).toEqual([h.first.version, h.signedOut.version]); |
| 441 | yield* h.assertClosed; |
| 442 | }).pipe(Effect.scoped), |
| 443 | ); |
| 444 | |
| 445 | it.effect.skipIf(windowsHost)( |
| 446 | "clears account metadata when a text helper needs Google sign-in", |
| 447 | () => |
| 448 | Effect.gen(function* () { |
| 449 | const h = yield* makeHarness(); |
| 450 | yield* h.refresh(); |
| 451 | h.controls.selected = h.signedOut; |
| 452 | const error = yield* h.instance.textGeneration |
| 453 | .generateThreadTitle({ |
| 454 | cwd: h.profileDirectory, |
| 455 | message: "Repair Google login", |
| 456 | modelSelection: { instanceId: h.instance.instanceId, model: "gemini-test-low" }, |
| 457 | }) |
| 458 | .pipe(Effect.flip); |
| 459 | expect(error._tag).toBe("TextGenerationError"); |
| 460 | const snapshot = yield* h.instance.snapshot.getSnapshot; |
| 461 | expect(snapshot.auth.status).toBe("unauthenticated"); |
| 462 | expect(snapshot.models).toEqual([]); |
| 463 | expect(snapshot.supportsTextGeneration).toBe(false); |
| 464 | expect(h.releases).toEqual([h.first.version, h.signedOut.version]); |
| 465 | yield* h.assertClosed; |
| 466 | }).pipe(Effect.scoped), |
| 467 | ); |
| 468 | |
| 469 | it.effect.skipIf(windowsHost)("keeps the previous catalog when executable resolution fails", () => |
| 470 | Effect.gen(function* () { |
| 471 | const h = yield* makeHarness(); |
| 472 | yield* h.refresh(); |
| 473 | const before = yield* h.instance.snapshot.getSnapshot; |
| 474 | h.controls.failResolution = true; |
| 475 | const error = yield* h.refresh().pipe(Effect.flip); |
| 476 | expect(error.detail).toContain("previous model list is unchanged"); |
| 477 | const after = yield* h.instance.snapshot.getSnapshot; |
| 478 | expect(after.models).toEqual(before.models); |
| 479 | expect(after.auth).toEqual(before.auth); |
| 480 | expect(h.acquisitions).toHaveLength(2); |
| 481 | expect(h.releases).toEqual([h.first.version]); |
| 482 | yield* h.assertClosed; |
| 483 | }).pipe(Effect.scoped), |
| 484 | ); |
| 485 | |
| 486 | it.effect.skipIf(windowsHost)( |
| 487 | "gives each process its own temp directory and removes it when the process closes", |
| 488 | () => |
| 489 | Effect.gen(function* () { |
| 490 | const h = yield* makeHarness(); |
| 491 | const tempRoot = h.directories.runtimeTemp; |
| 492 | yield* h.refresh(); |
| 493 | yield* h.refresh(); |
| 494 | const directories = h.launches.flatMap((launch) => |
| 495 | launch.tempDirectory === undefined ? [] : [launch.tempDirectory], |
| 496 | ); |
| 497 | expect(directories).toHaveLength(2); |
| 498 | for (const directory of directories) { |
| 499 | expect(h.path.dirname(directory)).toBe(tempRoot); |
| 500 | } |
| 501 | expect(new Set(directories).size).toBe(2); |
| 502 | yield* h.assertClosed; |
| 503 | }).pipe(Effect.scoped), |
| 504 | ); |
| 505 | |
| 506 | it.effect.skipIf(windowsHost)( |
| 507 | "removes runtime temp directories left by a previous server on create", |
| 508 | () => |
| 509 | Effect.gen(function* () { |
| 510 | const fs = yield* FileSystem.FileSystem; |
| 511 | const path = yield* Path.Path; |
| 512 | const config = yield* ServerConfig.ServerConfig; |
| 513 | const instanceId = ProviderInstanceId.make("antigravity-orphan-sweep"); |
| 514 | const directories = yield* resolveAntigravityInstanceDirectories( |
| 515 | config.stateDir, |
| 516 | instanceId, |
| 517 | ); |
| 518 | // Older builds unpacked inside the profile. |
| 519 | const legacyRoot = path.join(directories.profile, "antigravity-acp", "tmp"); |
| 520 | for (const root of [directories.runtimeTemp, legacyRoot]) { |
| 521 | const orphan = path.join(root, "run-orphan", "_MEI123", "google3"); |
| 522 | yield* fs.makeDirectory(orphan, { recursive: true }); |
| 523 | yield* fs.writeFileString(path.join(orphan, "payload.bin"), "stale"); |
| 524 | } |
| 525 | yield* AntigravityDriver.create({ |
| 526 | instanceId, |
| 527 | displayName: "Sweep", |
| 528 | enabled: false, |
| 529 | config: AntigravityDriver.defaultConfig(), |
| 530 | environment: [], |
| 531 | }).pipe( |
| 532 | Effect.provide( |
| 533 | Layer.mock(AntigravityInstallation.AntigravityInstallation)({ |
| 534 | managedDirectory: config.stateDir, |
| 535 | resolve: () => Effect.die("unused"), |
| 536 | acquire: () => Effect.die("unused"), |
| 537 | }), |
| 538 | ), |
| 539 | ); |
| 540 | expect(yield* fs.exists(directories.runtimeTemp)).toBe(false); |
| 541 | expect(yield* fs.exists(legacyRoot)).toBe(false); |
| 542 | }).pipe(Effect.scoped), |
| 543 | ); |
| 544 | |
| 545 | it.effect("probes through installation resolution without launching a process", () => |
| 546 | Effect.gen(function* () { |
| 547 | const h = yield* makeHarness({ enabled: true }); |
| 548 | const snapshot = yield* h.instance.snapshot.refresh; |
| 549 | expect(snapshot.installed).toBe(true); |
| 550 | expect(snapshot.version).toBe(h.first.version); |
| 551 | expect(h.launches).toEqual([]); |
| 552 | expect(h.acquisitions).toEqual([]); |
| 553 | }).pipe(Effect.scoped), |
| 554 | ); |
| 555 | }); |