diff --git a/apps/server/src/provider/AntigravityAuth.test.ts b/apps/server/src/provider/AntigravityAuth.test.ts index 8a27ade0e6b26ae34d2c701d72bda5445e43585c..f009255e90a29e7182ff7d747b825b513eb7c7a8 100644 --- a/apps/server/src/provider/AntigravityAuth.test.ts +++ b/apps/server/src/provider/AntigravityAuth.test.ts @@ -56,6 +56,7 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* ( readonly interactive?: boolean; readonly authorizationUrls?: ReadonlyArray; readonly supportsLogout?: boolean; + readonly beforeInitialize?: Effect.Effect; readonly forwardCallback?: Effect.Effect; } = {}, ) { @@ -82,8 +83,9 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* ( ); return { initialize: () => - Effect.sync(() => { + Effect.gen(function* () { events.push("initialize"); + yield* options.beforeInitialize ?? Effect.void; return options.supportsLogout === false ? { ...initialized, agentCapabilities: {} } : initialized; @@ -394,6 +396,45 @@ it.layer(NodeServices.layer)("AntigravityAuth", (it) => { }), ); + it.effect("signs out after a slow packaged runtime starts", () => + Effect.gen(function* () { + const entered = yield* Deferred.make(); + const initialized = yield* Deferred.make(); + const harness = yield* makeHarness({ + beforeInitialize: Deferred.succeed(entered, undefined).pipe( + Effect.andThen(Deferred.await(initialized)), + ), + }); + const logout = yield* harness.auth.controller.logout(Effect.void).pipe(Effect.forkScoped); + yield* Deferred.await(entered); + yield* TestClock.adjust("47 seconds"); + yield* Deferred.succeed(initialized, undefined); + assert.equal((yield* Fiber.join(logout)).phase, "idle"); + assert.include(harness.events, "logout"); + assert.deepEqual(harness.catalog(), []); + yield* Deferred.await(harness.closed); + }), + ); + + it.effect("closes a stalled sign-out process without clearing its account catalog", () => + Effect.gen(function* () { + const entered = yield* Deferred.make(); + const harness = yield* makeHarness({ + beforeInitialize: Deferred.succeed(entered, undefined).pipe(Effect.andThen(Effect.never)), + }); + const logout = yield* harness.auth.controller + .logout(Effect.void) + .pipe(Effect.exit, Effect.forkScoped); + yield* Deferred.await(entered); + yield* TestClock.adjust("90 seconds"); + assert.isTrue(Exit.isFailure(yield* Fiber.join(logout))); + yield* Deferred.await(harness.closed); + assert.notInclude(harness.events, "logout"); + assert.deepEqual(harness.catalog(), ["previous-account-model"]); + yield* harness.auth.withProcess(Effect.void, Effect.void).pipe(Effect.scoped); + }), + ); + it.effect( "sign-out interrupts startup without interrupting its caller after startup returns", () => diff --git a/apps/server/src/provider/AntigravityAuth.ts b/apps/server/src/provider/AntigravityAuth.ts index 500bca120ebef66337fe57d6eadbd86c84808189..c7118bccad8329d58726b1388d612a9b7e40ab1d 100644 --- a/apps/server/src/provider/AntigravityAuth.ts +++ b/apps/server/src/provider/AntigravityAuth.ts @@ -480,7 +480,7 @@ export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* < }).pipe( Effect.scoped, Effect.timeoutOrElse({ - duration: "30 seconds", + duration: "90 seconds", orElse: () => Effect.fail(setupError("logout", "Antigravity sign-out timed out.")), }), ), diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts index 4d8b7b8a9fa49b269d06b81d75a62aeed6eb84e9..922a27df5768c0421562cf1b63380015d9567836 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts @@ -11,10 +11,13 @@ import { HostProcessPlatform, } from "@t3tools/shared/hostProcess"; import * as Effect from "effect/Effect"; +import * as Deferred from "effect/Deferred"; +import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; +import * as TestClock from "effect/testing/TestClock"; import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts"; @@ -108,7 +111,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( const first = yield* makeExecutable("runtime 'one"); const second = yield* makeExecutable("runtime two"); const signedOut = yield* makeExecutable("runtime signed-out", true); - const controls = { selected: first, failResolution: false }; + const controls = { selected: first, failResolution: false, beforeAcquire: Effect.void }; const acquisitions: Array<{ binaryPath: string | undefined; path: string | undefined }> = []; const releases: Array = []; const launches: Array<{ @@ -129,6 +132,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( acquire: (binaryPath, environment) => Effect.gen(function* () { acquisitions.push({ binaryPath, path: environment?.PATH }); + yield* controls.beforeAcquire; if (controls.failResolution) { return yield* new AntigravityInstallationError({ operation: "resolve", @@ -249,6 +253,26 @@ it.layer(testLayer)("AntigravityDriver", (it) => { }).pipe(Effect.scoped), ); + it.effect.skipIf(windowsHost)("refreshes models after slow process startup", () => + Effect.gen(function* () { + const h = yield* makeHarness(); + const entered = yield* Deferred.make(); + const ready = yield* Deferred.make(); + h.controls.beforeAcquire = Deferred.succeed(entered, undefined).pipe( + Effect.andThen(Deferred.await(ready)), + ); + const refresh = yield* h.refresh().pipe(Effect.forkScoped); + yield* Deferred.await(entered); + yield* TestClock.adjust("70 seconds"); + yield* Deferred.succeed(ready, undefined); + yield* Fiber.join(refresh); + const snapshot = yield* h.instance.snapshot.getSnapshot; + expect(snapshot.auth.status).toBe("authenticated"); + expect(snapshot.models.length).toBeGreaterThan(0); + yield* h.assertClosed; + }).pipe(Effect.scoped), + ); + it.effect.skipIf(windowsHost)( "refreshes a disabled instance through the selected executable and personal Google ACP", () => diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index f9f99a5bfa3891a409d4fad7b38bcba2c0d43b0b..9388916b49609bfbf873be46c266a830d12b6c40 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -332,7 +332,7 @@ export const AntigravityDriver: ProviderDriver Effect.fail( new ProviderDriverError({ diff --git a/apps/server/src/provider/Layers/AntigravityProvider.test.ts b/apps/server/src/provider/Layers/AntigravityProvider.test.ts index d4a16265d553139c9be3b563e0bd00136e500642..4794a8ff189e91c8700aeff7d22c9c9a7d4c6baf 100644 --- a/apps/server/src/provider/Layers/AntigravityProvider.test.ts +++ b/apps/server/src/provider/Layers/AntigravityProvider.test.ts @@ -417,11 +417,17 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => { Effect.gen(function* () { const harness = yield* makeHarness(); yield* harness.initialize; - yield* Ref.set(harness.probe, Effect.sleep("20 seconds").pipe(Effect.as(initializeResult))); + const entered = yield* Deferred.make(); + const initialized = yield* Deferred.make(); + yield* Ref.set( + harness.probe, + Deferred.succeed(entered, undefined).pipe(Effect.andThen(Deferred.await(initialized))), + ); const refresh = yield* harness.provider.snapshot.refresh.pipe(Effect.forkChild); - yield* Effect.yieldNow; - yield* TestClock.adjust("20 seconds"); + yield* Deferred.await(entered); + yield* TestClock.adjust("47 seconds"); + yield* Deferred.succeed(initialized, initializeResult); const snapshot = yield* Fiber.join(refresh); expect(snapshot).toMatchObject({ @@ -433,6 +439,31 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => { ), ); + it.effect("closes a stalled health probe at its deadline", () => + Effect.scoped( + Effect.gen(function* () { + const harness = yield* makeHarness(); + yield* harness.initialize; + const entered = yield* Deferred.make(); + const closed = yield* Deferred.make(); + yield* Ref.set( + harness.probe, + Deferred.succeed(entered, undefined).pipe( + Effect.andThen(Effect.never), + Effect.ensuring(Deferred.succeed(closed, undefined)), + ), + ); + const refresh = yield* harness.provider.snapshot.refresh.pipe(Effect.forkChild); + yield* Deferred.await(entered); + yield* TestClock.adjust("90 seconds"); + const snapshot = yield* Fiber.join(refresh); + yield* Deferred.await(closed); + expect(snapshot.status).toBe("error"); + expect(snapshot.message).toContain("90 seconds"); + }), + ), + ); + it.effect("distinguishes missing executables from a failed installed executable", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/provider/Layers/AntigravityProvider.ts b/apps/server/src/provider/Layers/AntigravityProvider.ts index c10a6ad5af22a48330fe0ea8a28b4a724b648fd3..9c91c24d799d252759e24097fbb7707c30e36d65 100644 --- a/apps/server/src/provider/Layers/AntigravityProvider.ts +++ b/apps/server/src/provider/Layers/AntigravityProvider.ts @@ -32,7 +32,7 @@ import { const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] }); const MAX_WORKSPACE_SNAPSHOTS = 32; -const HEALTH_CHECK_TIMEOUT = "45 seconds"; +const HEALTH_CHECK_TIMEOUT = "90 seconds"; const SIGN_IN_MESSAGE = "Sign in with Google to use Antigravity."; const AUTH_UNCHECKED_MESSAGE = "Antigravity is installed. Google account access is not checked yet."; diff --git a/docs/user/providers-antigravity.md b/docs/user/providers-antigravity.md index 74a841e075d5c7e338fd3e9f85c751b0a03b812e..23ebd6f644cf9e93a3691f63af6fe3d4b03403bb 100644 --- a/docs/user/providers-antigravity.md +++ b/docs/user/providers-antigravity.md @@ -169,6 +169,9 @@ in web or desktop provider settings, or **Refresh models** in the mobile model p uses saved Google sign-in and does not open a login page. If sign-in is required, use the provider's setup controls. Automatic status checks verify the installation only. +The packaged runtime can be slow to start, especially on Windows. Health checks, model refresh, +and sign-out each allow up to 90 seconds before reporting a timeout. + If Google reports `SUBSCRIPTION_REQUIRED`, an account restriction, or a usage limit, read the provider's message. A finished turn can contain an upstream error instead of completed work. Use any retry time Google supplies. T3 Code does not switch to an API key to get past the limit.