Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 42 additions & 9 deletions apps/server/src/provider/AntigravityAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,21 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* (
yield* Deferred.succeed(closed, undefined);
}),
);
// Cached like the real AcpSessionRuntime, so start() reuses an earlier initialize.
const initialize = yield* Effect.cached(
Effect.gen(function* () {
events.push("initialize");
yield* options.beforeInitialize ?? Effect.void;
return options.supportsLogout === false
? { ...initialized, agentCapabilities: {} }
: initialized;
}),
);
return {
initialize: () =>
Effect.gen(function* () {
events.push("initialize");
yield* options.beforeInitialize ?? Effect.void;
return options.supportsLogout === false
? { ...initialized, agentCapabilities: {} }
: initialized;
}),
initialize: () => initialize,
start: () =>
Effect.gen(function* () {
yield* initialize;
events.push("authenticate");
yield* Deferred.succeed(authenticateCalled, undefined);
if (options.interactive !== false && input.onAuthorizationUrl) {
Expand Down Expand Up @@ -278,6 +282,35 @@ it.layer(NodeServices.layer)("AntigravityAuth", (it) => {
}),
);

it.effect("does not blame the network when the agent never answers initialize", () =>
Effect.gen(function* () {
const initializing = yield* Deferred.make<void>();
const harness = yield* makeHarness({
beforeInitialize: Deferred.succeed(initializing, undefined).pipe(
Effect.andThen(Effect.never),
),
});
yield* harness.auth.controller.start(owner);
yield* Deferred.await(initializing);
const blamesNetwork = /reach(ed)? Google|network|ipv6/i;

yield* TestClock.adjust("30 seconds");
const stalled = yield* harness.auth.controller.subscribe(owner).pipe(
Stream.filter((state) => state.message !== "Starting Google sign-in."),
Stream.runHead,
Effect.map(Option.getOrThrow),
);
assert.equal(stalled.phase, "starting");
assert.notMatch(stalled.message ?? "", blamesNetwork);
assert.match(stalled.message ?? "", /not responded|stuck/i);

yield* TestClock.adjust("270 seconds");
const failed = yield* phase(harness.auth, "failed");
assert.notMatch(failed.message ?? "", blamesNetwork);
assert.match(failed.message ?? "", /not respond|stuck/i);
}),
);

it.effect("keeps the expired message once the Google link was shown", () =>
Effect.gen(function* () {
const harness = yield* makeHarness();
Expand Down Expand Up @@ -383,7 +416,7 @@ it.layer(NodeServices.layer)("AntigravityAuth", (it) => {
assert.isTrue(Exit.isFailure(competing));
const resumed = yield* phase(harness.auth, "waiting");
assert.equal(resumed.flowId, first.flowId);
assert.deepEqual(harness.events, ["process-open", "authenticate"]);
assert.deepEqual(harness.events, ["process-open", "initialize", "authenticate"]);
yield* harness.auth.controller.cancel(owner, first.flowId!);
}),
);
Expand Down
22 changes: 18 additions & 4 deletions apps/server/src/provider/AntigravityAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ import type { ProviderAuthController } from "./ProviderAuthService.ts";
const AUTH_TIMEOUT_MS = 300_000;
// A healthy agent shows the Google link or finishes within seconds. Its bundled Python has no
// IPv4 fallback race, so a host with broken IPv6 waits about two minutes per Google request.
// An agent that never answers initialize is hung locally, so the network is not to blame.
const NO_RESPONSE_HINT_MS = 30_000;
const NOT_STARTED_MESSAGE =
"Antigravity has not responded after 30 seconds. Its runtime may be stuck starting on this machine.";
const NOT_RESPONDING_MESSAGE =
"Antigravity did not respond. Its runtime may not run on this machine.";
const NO_RESPONSE_MESSAGE =
"Antigravity has not reached Google after 30 seconds. Check this machine's network connection. Broken IPv6 is a common cause.";
const UNREACHABLE_MESSAGE =
Expand All @@ -53,6 +58,7 @@ interface AuthFlow {
readonly expiresAtMillis: number;
state: ProviderAuthState;
pending: AntigravityAuthorizationUrl | undefined;
initialized: boolean;
callbackSent: boolean;
fiber: Fiber.Fiber<void> | undefined;
forwarding: Fiber.Fiber<void, ProviderSetupError> | undefined;
Expand Down Expand Up @@ -276,7 +282,10 @@ export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* <
lock.withPermits(1)(
Effect.gen(function* () {
if (activeFlow !== flow || flow.state.phase !== "starting") return;
yield* publishFlow(flow, { ...flow.state, message: NO_RESPONSE_MESSAGE });
yield* publishFlow(flow, {
...flow.state,
message: flow.initialized ? NO_RESPONSE_MESSAGE : NOT_STARTED_MESSAGE,
});
}),
),
),
Expand All @@ -285,6 +294,8 @@ export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* <
const runtime = yield* options.makeRuntime({
onAuthorizationUrl: (url) => receiveAuthorizationUrl(flow, url),
});
yield* runtime.initialize();
flow.initialized = true;
const started = yield* runtime.start();
yield* lock.withPermits(1)(
Effect.gen(function* () {
Expand All @@ -309,9 +320,11 @@ export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* <
Effect.fail(
setupError(
"start",
flow.state.phase === "starting"
? UNREACHABLE_MESSAGE
: "Google sign-in expired. Start sign-in again.",
flow.state.phase !== "starting"
? "Google sign-in expired. Start sign-in again."
: flow.initialized
? UNREACHABLE_MESSAGE
: NOT_RESPONDING_MESSAGE,
),
),
}),
Expand Down Expand Up @@ -398,6 +411,7 @@ export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* <
expiresAtMillis,
state,
pending: undefined,
initialized: false,
callbackSent: false,
fiber: undefined,
forwarding: undefined,
Expand Down
Loading