| author | |
| committer | |
| log | f25e4428961982e3935300cfd3c78f7d0fac4ee9 |
| tree | 687a5f4e7de18a420cb93486f62abc1339f9ff51 |
| parent | 498ab9c399d5e8c3097a286be14d03238e071ac1 |
| signature | Signed by PGP key B5690EEEBB952194 |
7 files changed, 107 insertions(+), 8 deletions(-)
apps/server/src/provider/AntigravityAuth.test.ts+42-1| ... | @@ -56,6 +56,7 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* ( | ... | @@ -56,6 +56,7 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* ( |
| 56 | readonly interactive?: boolean; | 56 | readonly interactive?: boolean; |
| 57 | readonly authorizationUrls?: ReadonlyArray<string>; | 57 | readonly authorizationUrls?: ReadonlyArray<string>; |
| 58 | readonly supportsLogout?: boolean; | 58 | readonly supportsLogout?: boolean; |
| 59 | readonly beforeInitialize?: Effect.Effect<void>; | ||
| 59 | readonly forwardCallback?: Effect.Effect<void, ProviderSetupError>; | 60 | readonly forwardCallback?: Effect.Effect<void, ProviderSetupError>; |
| 60 | } = {}, | 61 | } = {}, |
| 61 | ) { | 62 | ) { |
| ... | @@ -82,8 +83,9 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* ( | ... | @@ -82,8 +83,9 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* ( |
| 82 | ); | 83 | ); |
| 83 | return { | 84 | return { |
| 84 | initialize: () => | 85 | initialize: () => |
| 85 | Effect.sync(() => { | 86 | Effect.gen(function* () { |
| 86 | events.push("initialize"); | 87 | events.push("initialize"); |
| 88 | yield* options.beforeInitialize ?? Effect.void; | ||
| 87 | return options.supportsLogout === false | 89 | return options.supportsLogout === false |
| 88 | ? { ...initialized, agentCapabilities: {} } | 90 | ? { ...initialized, agentCapabilities: {} } |
| 89 | : initialized; | 91 | : initialized; |
| ... | @@ -394,6 +396,45 @@ it.layer(NodeServices.layer)("AntigravityAuth", (it) => { | ... | @@ -394,6 +396,45 @@ it.layer(NodeServices.layer)("AntigravityAuth", (it) => { |
| 394 | }), | 396 | }), |
| 395 | ); | 397 | ); |
| 396 | 398 | ||
| 399 | it.effect("signs out after a slow packaged runtime starts", () => | ||
| 400 | Effect.gen(function* () { | ||
| 401 | const entered = yield* Deferred.make<void>(); | ||
| 402 | const initialized = yield* Deferred.make<void>(); | ||
| 403 | const harness = yield* makeHarness({ | ||
| 404 | beforeInitialize: Deferred.succeed(entered, undefined).pipe( | ||
| 405 | Effect.andThen(Deferred.await(initialized)), | ||
| 406 | ), | ||
| 407 | }); | ||
| 408 | const logout = yield* harness.auth.controller.logout(Effect.void).pipe(Effect.forkScoped); | ||
| 409 | yield* Deferred.await(entered); | ||
| 410 | yield* TestClock.adjust("47 seconds"); | ||
| 411 | yield* Deferred.succeed(initialized, undefined); | ||
| 412 | assert.equal((yield* Fiber.join(logout)).phase, "idle"); | ||
| 413 | assert.include(harness.events, "logout"); | ||
| 414 | assert.deepEqual(harness.catalog(), []); | ||
| 415 | yield* Deferred.await(harness.closed); | ||
| 416 | }), | ||
| 417 | ); | ||
| 418 | |||
| 419 | it.effect("closes a stalled sign-out process without clearing its account catalog", () => | ||
| 420 | Effect.gen(function* () { | ||
| 421 | const entered = yield* Deferred.make<void>(); | ||
| 422 | const harness = yield* makeHarness({ | ||
| 423 | beforeInitialize: Deferred.succeed(entered, undefined).pipe(Effect.andThen(Effect.never)), | ||
| 424 | }); | ||
| 425 | const logout = yield* harness.auth.controller | ||
| 426 | .logout(Effect.void) | ||
| 427 | .pipe(Effect.exit, Effect.forkScoped); | ||
| 428 | yield* Deferred.await(entered); | ||
| 429 | yield* TestClock.adjust("90 seconds"); | ||
| 430 | assert.isTrue(Exit.isFailure(yield* Fiber.join(logout))); | ||
| 431 | yield* Deferred.await(harness.closed); | ||
| 432 | assert.notInclude(harness.events, "logout"); | ||
| 433 | assert.deepEqual(harness.catalog(), ["previous-account-model"]); | ||
| 434 | yield* harness.auth.withProcess(Effect.void, Effect.void).pipe(Effect.scoped); | ||
| 435 | }), | ||
| 436 | ); | ||
| 437 | |||
| 397 | it.effect( | 438 | it.effect( |
| 398 | "sign-out interrupts startup without interrupting its caller after startup returns", | 439 | "sign-out interrupts startup without interrupting its caller after startup returns", |
| 399 | () => | 440 | () => |
apps/server/src/provider/AntigravityAuth.ts+1-1| ... | @@ -480,7 +480,7 @@ export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* < | ... | @@ -480,7 +480,7 @@ export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* < |
| 480 | }).pipe( | 480 | }).pipe( |
| 481 | Effect.scoped, | 481 | Effect.scoped, |
| 482 | Effect.timeoutOrElse({ | 482 | Effect.timeoutOrElse({ |
| 483 | duration: "30 seconds", | 483 | duration: "90 seconds", |
| 484 | orElse: () => Effect.fail(setupError("logout", "Antigravity sign-out timed out.")), | 484 | orElse: () => Effect.fail(setupError("logout", "Antigravity sign-out timed out.")), |
| 485 | }), | 485 | }), |
| 486 | ), | 486 | ), |
apps/server/src/provider/Drivers/AntigravityDriver.test.ts+25-1| ... | @@ -11,10 +11,13 @@ import { | ... | @@ -11,10 +11,13 @@ import { |
| 11 | HostProcessPlatform, | 11 | HostProcessPlatform, |
| 12 | } from "@t3tools/shared/hostProcess"; | 12 | } from "@t3tools/shared/hostProcess"; |
| 13 | import * as Effect from "effect/Effect"; | 13 | import * as Effect from "effect/Effect"; |
| 14 | import * as Deferred from "effect/Deferred"; | ||
| 15 | import * as Fiber from "effect/Fiber"; | ||
| 14 | import * as FileSystem from "effect/FileSystem"; | 16 | import * as FileSystem from "effect/FileSystem"; |
| 15 | import * as Layer from "effect/Layer"; | 17 | import * as Layer from "effect/Layer"; |
| 16 | import * as Path from "effect/Path"; | 18 | import * as Path from "effect/Path"; |
| 17 | import * as Schema from "effect/Schema"; | 19 | import * as Schema from "effect/Schema"; |
| 20 | import * as TestClock from "effect/testing/TestClock"; | ||
| 18 | import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; | 21 | import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; |
| 19 | 22 | ||
| 20 | import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts"; | 23 | import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts"; |
| ... | @@ -108,7 +111,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( | ... | @@ -108,7 +111,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( |
| 108 | const first = yield* makeExecutable("runtime 'one"); | 111 | const first = yield* makeExecutable("runtime 'one"); |
| 109 | const second = yield* makeExecutable("runtime two"); | 112 | const second = yield* makeExecutable("runtime two"); |
| 110 | const signedOut = yield* makeExecutable("runtime signed-out", true); | 113 | const signedOut = yield* makeExecutable("runtime signed-out", true); |
| 111 | const controls = { selected: first, failResolution: false }; | 114 | const controls = { selected: first, failResolution: false, beforeAcquire: Effect.void }; |
| 112 | const acquisitions: Array<{ binaryPath: string | undefined; path: string | undefined }> = []; | 115 | const acquisitions: Array<{ binaryPath: string | undefined; path: string | undefined }> = []; |
| 113 | const releases: Array<string | null> = []; | 116 | const releases: Array<string | null> = []; |
| 114 | const launches: Array<{ | 117 | const launches: Array<{ |
| ... | @@ -129,6 +132,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( | ... | @@ -129,6 +132,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( |
| 129 | acquire: (binaryPath, environment) => | 132 | acquire: (binaryPath, environment) => |
| 130 | Effect.gen(function* () { | 133 | Effect.gen(function* () { |
| 131 | acquisitions.push({ binaryPath, path: environment?.PATH }); | 134 | acquisitions.push({ binaryPath, path: environment?.PATH }); |
| 135 | yield* controls.beforeAcquire; | ||
| 132 | if (controls.failResolution) { | 136 | if (controls.failResolution) { |
| 133 | return yield* new AntigravityInstallationError({ | 137 | return yield* new AntigravityInstallationError({ |
| 134 | operation: "resolve", | 138 | operation: "resolve", |
| ... | @@ -249,6 +253,26 @@ it.layer(testLayer)("AntigravityDriver", (it) => { | ... | @@ -249,6 +253,26 @@ it.layer(testLayer)("AntigravityDriver", (it) => { |
| 249 | }).pipe(Effect.scoped), | 253 | }).pipe(Effect.scoped), |
| 250 | ); | 254 | ); |
| 251 | 255 | ||
| 256 | it.effect.skipIf(windowsHost)("refreshes models after slow process startup", () => | ||
| 257 | Effect.gen(function* () { | ||
| 258 | const h = yield* makeHarness(); | ||
| 259 | const entered = yield* Deferred.make<void>(); | ||
| 260 | const ready = yield* Deferred.make<void>(); | ||
| 261 | h.controls.beforeAcquire = Deferred.succeed(entered, undefined).pipe( | ||
| 262 | Effect.andThen(Deferred.await(ready)), | ||
| 263 | ); | ||
| 264 | const refresh = yield* h.refresh().pipe(Effect.forkScoped); | ||
| 265 | yield* Deferred.await(entered); | ||
| 266 | yield* TestClock.adjust("70 seconds"); | ||
| 267 | yield* Deferred.succeed(ready, undefined); | ||
| 268 | yield* Fiber.join(refresh); | ||
| 269 | const snapshot = yield* h.instance.snapshot.getSnapshot; | ||
| 270 | expect(snapshot.auth.status).toBe("authenticated"); | ||
| 271 | expect(snapshot.models.length).toBeGreaterThan(0); | ||
| 272 | yield* h.assertClosed; | ||
| 273 | }).pipe(Effect.scoped), | ||
| 274 | ); | ||
| 275 | |||
| 252 | it.effect.skipIf(windowsHost)( | 276 | it.effect.skipIf(windowsHost)( |
| 253 | "refreshes a disabled instance through the selected executable and personal Google ACP", | 277 | "refreshes a disabled instance through the selected executable and personal Google ACP", |
| 254 | () => | 278 | () => |
apps/server/src/provider/Drivers/AntigravityDriver.ts+1-1| ... | @@ -332,7 +332,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD | ... | @@ -332,7 +332,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD |
| 332 | }, | 332 | }, |
| 333 | Effect.scoped, | 333 | Effect.scoped, |
| 334 | Effect.timeoutOrElse({ | 334 | Effect.timeoutOrElse({ |
| 335 | duration: "60 seconds", | 335 | duration: "90 seconds", |
| 336 | orElse: () => | 336 | orElse: () => |
| 337 | Effect.fail( | 337 | Effect.fail( |
| 338 | new ProviderDriverError({ | 338 | new ProviderDriverError({ |
apps/server/src/provider/Layers/AntigravityProvider.test.ts+34-3| ... | @@ -417,11 +417,17 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => { | ... | @@ -417,11 +417,17 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => { |
| 417 | Effect.gen(function* () { | 417 | Effect.gen(function* () { |
| 418 | const harness = yield* makeHarness(); | 418 | const harness = yield* makeHarness(); |
| 419 | yield* harness.initialize; | 419 | yield* harness.initialize; |
| 420 | yield* Ref.set(harness.probe, Effect.sleep("20 seconds").pipe(Effect.as(initializeResult))); | 420 | const entered = yield* Deferred.make<void>(); |
| 421 | const initialized = yield* Deferred.make<EffectAcpSchema.InitializeResponse>(); | ||
| 422 | yield* Ref.set( | ||
| 423 | harness.probe, | ||
| 424 | Deferred.succeed(entered, undefined).pipe(Effect.andThen(Deferred.await(initialized))), | ||
| 425 | ); | ||
| 421 | 426 | ||
| 422 | const refresh = yield* harness.provider.snapshot.refresh.pipe(Effect.forkChild); | 427 | const refresh = yield* harness.provider.snapshot.refresh.pipe(Effect.forkChild); |
| 423 | yield* Effect.yieldNow; | 428 | yield* Deferred.await(entered); |
| 424 | yield* TestClock.adjust("20 seconds"); | 429 | yield* TestClock.adjust("47 seconds"); |
| 430 | yield* Deferred.succeed(initialized, initializeResult); | ||
| 425 | const snapshot = yield* Fiber.join(refresh); | 431 | const snapshot = yield* Fiber.join(refresh); |
| 426 | 432 | ||
| 427 | expect(snapshot).toMatchObject({ | 433 | expect(snapshot).toMatchObject({ |
| ... | @@ -433,6 +439,31 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => { | ... | @@ -433,6 +439,31 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => { |
| 433 | ), | 439 | ), |
| 434 | ); | 440 | ); |
| 435 | 441 | ||
| 442 | it.effect("closes a stalled health probe at its deadline", () => | ||
| 443 | Effect.scoped( | ||
| 444 | Effect.gen(function* () { | ||
| 445 | const harness = yield* makeHarness(); | ||
| 446 | yield* harness.initialize; | ||
| 447 | const entered = yield* Deferred.make<void>(); | ||
| 448 | const closed = yield* Deferred.make<void>(); | ||
| 449 | yield* Ref.set( | ||
| 450 | harness.probe, | ||
| 451 | Deferred.succeed(entered, undefined).pipe( | ||
| 452 | Effect.andThen(Effect.never), | ||
| 453 | Effect.ensuring(Deferred.succeed(closed, undefined)), | ||
| 454 | ), | ||
| 455 | ); | ||
| 456 | const refresh = yield* harness.provider.snapshot.refresh.pipe(Effect.forkChild); | ||
| 457 | yield* Deferred.await(entered); | ||
| 458 | yield* TestClock.adjust("90 seconds"); | ||
| 459 | const snapshot = yield* Fiber.join(refresh); | ||
| 460 | yield* Deferred.await(closed); | ||
| 461 | expect(snapshot.status).toBe("error"); | ||
| 462 | expect(snapshot.message).toContain("90 seconds"); | ||
| 463 | }), | ||
| 464 | ), | ||
| 465 | ); | ||
| 466 | |||
| 436 | it.effect("distinguishes missing executables from a failed installed executable", () => | 467 | it.effect("distinguishes missing executables from a failed installed executable", () => |
| 437 | Effect.scoped( | 468 | Effect.scoped( |
| 438 | Effect.gen(function* () { | 469 | Effect.gen(function* () { |
apps/server/src/provider/Layers/AntigravityProvider.ts+1-1| ... | @@ -32,7 +32,7 @@ import { | ... | @@ -32,7 +32,7 @@ import { |
| 32 | 32 | ||
| 33 | const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] }); | 33 | const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] }); |
| 34 | const MAX_WORKSPACE_SNAPSHOTS = 32; | 34 | const MAX_WORKSPACE_SNAPSHOTS = 32; |
| 35 | const HEALTH_CHECK_TIMEOUT = "45 seconds"; | 35 | const HEALTH_CHECK_TIMEOUT = "90 seconds"; |
| 36 | const SIGN_IN_MESSAGE = "Sign in with Google to use Antigravity."; | 36 | const SIGN_IN_MESSAGE = "Sign in with Google to use Antigravity."; |
| 37 | const AUTH_UNCHECKED_MESSAGE = | 37 | const AUTH_UNCHECKED_MESSAGE = |
| 38 | "Antigravity is installed. Google account access is not checked yet."; | 38 | "Antigravity is installed. Google account access is not checked yet."; |
docs/user/providers-antigravity.md+3| ... | @@ -169,6 +169,9 @@ in web or desktop provider settings, or **Refresh models** in the mobile model p | ... | @@ -169,6 +169,9 @@ in web or desktop provider settings, or **Refresh models** in the mobile model p |
| 169 | uses saved Google sign-in and does not open a login page. If sign-in is required, use the | 169 | uses saved Google sign-in and does not open a login page. If sign-in is required, use the |
| 170 | provider's setup controls. Automatic status checks verify the installation only. | 170 | provider's setup controls. Automatic status checks verify the installation only. |
| 171 | 171 | ||
| 172 | The packaged runtime can be slow to start, especially on Windows. Health checks, model refresh, | ||
| 173 | and sign-out each allow up to 90 seconds before reporting a timeout. | ||
| 174 | |||
| 172 | If Google reports `SUBSCRIPTION_REQUIRED`, an account restriction, or a usage limit, read the | 175 | If Google reports `SUBSCRIPTION_REQUIRED`, an account restriction, or a usage limit, read the |
| 173 | provider's message. A finished turn can contain an upstream error instead of completed work. | 176 | provider's message. A finished turn can contain an upstream error instead of completed work. |
| 174 | Use any retry time Google supplies. T3 Code does not switch to an API key to get past the limit. | 177 | Use any retry time Google supplies. T3 Code does not switch to an API key to get past the limit. |