1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";
3import { ProviderInstanceId, ProviderSetupError, type ProviderAuthState } from "@t3tools/contracts";
4import * as Deferred from "effect/Deferred";
5import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as Fiber from "effect/Fiber";
8import * as Option from "effect/Option";
9import * as Scope from "effect/Scope";
10import * as Stream from "effect/Stream";
11import * as TestClock from "effect/testing/TestClock";
12import * as AcpErrors from "effect-acp/errors";
13import type * as AcpSchema from "effect-acp/compat";
14
15import {
16 makeAntigravityAuth,
17 type AntigravityAuth,
18 type AntigravityAuthRuntime,
19} from "./AntigravityAuth.ts";
20import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
21
22const instanceId = ProviderInstanceId.make("antigravity-auth-test");
23const owner = "t3-auth-session-owner";
24const otherOwner = "t3-auth-session-other";
25const authorizationUrl =
26 "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A51234%2F&state=test-state";
27const callbackUrl = "http://127.0.0.1:51234/?state=test-state&code=test-code";
28
29const initialized = {
30 protocolVersion: 1,
31 authMethods: [{ id: "oauth-personal", name: "Log in with Google" }],
32 agentCapabilities: { auth: { logout: {} } },
33} satisfies AcpSchema.InitializeResponse;
34const started: AcpSessionRuntime.AcpSessionRuntimeStartResult = {
35 sessionId: "native-session",
36 initializeResult: initialized,
37 sessionSetupResult: {
38 sessionId: "native-session",
39 models: {
40 currentModelId: "gemini-test",
41 availableModels: [{ modelId: "gemini-test", name: "Gemini test" }],
42 },
43 },
44 modelConfigId: "model",
45};
46
47const phase = (auth: AntigravityAuth, value: ProviderAuthState["phase"], sessionId = owner) =>
48 auth.controller.subscribe(sessionId).pipe(
49 Stream.filter((state) => state.phase === value),
50 Stream.runHead,
51 Effect.map(Option.getOrThrow),
52 );
53
54const makeHarness = Effect.fn("makeAuthTestHarness")(function* (
55 options: {
56 readonly interactive?: boolean;
57 readonly authorizationUrls?: ReadonlyArray<string>;
58 readonly supportsLogout?: boolean;
59 readonly beforeInitialize?: Effect.Effect<void>;
60 readonly forwardCallback?: Effect.Effect<void, ProviderSetupError>;
61 } = {},
62) {
63 const authenticated = yield* Deferred.make<void, AcpErrors.AcpError>();
64 const discovered = yield* Deferred.make<void, AcpErrors.AcpError>();
65 const closed = yield* Deferred.make<void>();
66 const events: string[] = [];
67 let receiveAuthorizationUrl:
68 | ((url: string) => Effect.Effect<void, AcpErrors.AcpError>)
69 | undefined;
70 let forwarded = 0;
71 let catalog = ["previous-account-model"];
72 const auth = yield* makeAntigravityAuth({
73 instanceId,
74 makeRuntime: (input) =>
75 Effect.gen(function* () {
76 receiveAuthorizationUrl = input.onAuthorizationUrl;
77 events.push("process-open");
78 yield* Effect.addFinalizer(() =>
79 Effect.gen(function* () {
80 events.push("process-close");
81 yield* Deferred.succeed(closed, undefined);
82 }),
83 );
84 return {
85 initialize: () =>
86 Effect.gen(function* () {
87 events.push("initialize");
88 yield* options.beforeInitialize ?? Effect.void;
89 return options.supportsLogout === false
90 ? { ...initialized, agentCapabilities: {} }
91 : initialized;
92 }),
93 start: () =>
94 Effect.gen(function* () {
95 events.push("authenticate");
96 if (options.interactive !== false && input.onAuthorizationUrl) {
97 for (const url of options.authorizationUrls ?? [authorizationUrl]) {
98 yield* input.onAuthorizationUrl(url);
99 }
100 }
101 yield* Deferred.await(authenticated);
102 events.push("session-new");
103 yield* Deferred.await(discovered);
104 return started;
105 }),
106 request: (method) =>
107 Effect.sync(() => {
108 events.push(method);
109 return {};
110 }),
111 } satisfies AntigravityAuthRuntime;
112 }),
113 onAuthenticated: () =>
114 Effect.sync(() => {
115 catalog = ["gemini-test"];
116 events.push("catalog-published");
117 }),
118 onSignedOut: Effect.sync(() => {
119 catalog = [];
120 events.push("catalog-cleared");
121 }),
122 forwardCallback: () =>
123 options.forwardCallback ??
124 Effect.sync(() => {
125 forwarded += 1;
126 }),
127 });
128 return {
129 auth,
130 authenticated,
131 discovered,
132 closed,
133 events,
134 catalog: () => catalog,
135 forwarded: () => forwarded,
136 receiveAuthorizationUrl: (url: string) =>
137 Effect.suspend(() =>
138 receiveAuthorizationUrl
139 ? receiveAuthorizationUrl(url)
140 : Effect.die("Authorization URL receiver is not ready."),
141 ),
142 };
143});
144
145it.layer(NodeServices.layer)("AntigravityAuth", (it) => {
146 it.effect("accepts the same authorization URL from stderr and stdout", () =>
147 Effect.gen(function* () {
148 const harness = yield* makeHarness({
149 authorizationUrls: [authorizationUrl, authorizationUrl],
150 });
151 yield* harness.auth.controller.start(owner);
152 const waiting = yield* phase(harness.auth, "waiting");
153 assert.equal(waiting.authorizationUrl, authorizationUrl);
154
155 yield* Deferred.succeed(harness.authenticated, undefined);
156 yield* Deferred.succeed(harness.discovered, undefined);
157 yield* phase(harness.auth, "succeeded");
158 }),
159 );
160
161 it.effect("accepts a delayed duplicate after callback completion starts", () =>
162 Effect.gen(function* () {
163 const harness = yield* makeHarness();
164 const state = yield* harness.auth.controller.start(owner);
165 yield* phase(harness.auth, "waiting");
166 yield* harness.auth.controller.complete(owner, {
167 flowId: state.flowId!,
168 callbackUrl,
169 });
170
171 yield* harness.receiveAuthorizationUrl(authorizationUrl);
172
173 yield* Deferred.succeed(harness.authenticated, undefined);
174 yield* Deferred.succeed(harness.discovered, undefined);
175 yield* phase(harness.auth, "succeeded");
176 }),
177 );
178
179 it.effect("rejects a different second authorization URL", () =>
180 Effect.gen(function* () {
181 const harness = yield* makeHarness({
182 authorizationUrls: [authorizationUrl, `${authorizationUrl}&scope=another-request`],
183 });
184 yield* harness.auth.controller.start(owner);
185
186 const failed = yield* phase(harness.auth, "failed");
187 assert.isNull(failed.authorizationUrl);
188 assert.deepEqual(harness.catalog(), ["previous-account-model"]);
189 }),
190 );
191
192 it.effect("keeps a remote flow private and waits for native auth and catalog discovery", () =>
193 Effect.gen(function* () {
194 const harness = yield* makeHarness();
195 const state = yield* harness.auth.controller.start(owner);
196 assert.isNotNull(state.flowId);
197 const waiting = yield* phase(harness.auth, "waiting");
198 assert.equal(waiting.authorizationUrl, authorizationUrl);
199
200 const other = yield* phase(harness.auth, "waiting", otherOwner);
201 assert.isNull(other.authorizationUrl);
202 assert.isNull(other.flowId);
203 const stolen = yield* harness.auth.controller
204 .complete(otherOwner, { flowId: state.flowId!, callbackUrl })
205 .pipe(Effect.exit);
206 assert.isTrue(Exit.isFailure(stolen));
207 assert.equal(harness.forwarded(), 0);
208
209 const verifying = yield* harness.auth.controller.complete(owner, {
210 flowId: state.flowId!,
211 callbackUrl,
212 });
213 assert.equal(verifying.phase, "verifying");
214 assert.equal(harness.forwarded(), 1);
215 assert.deepEqual(harness.catalog(), ["previous-account-model"]);
216
217 yield* Deferred.succeed(harness.authenticated, undefined);
218 yield* Deferred.succeed(harness.discovered, undefined);
219 const succeeded = yield* phase(harness.auth, "succeeded");
220 assert.deepEqual(harness.catalog(), ["gemini-test"]);
221 assert.isNull(succeeded.authorizationUrl);
222 assert.isNull(succeeded.expiresAt);
223 assert.equal(harness.events.at(-1), "process-close");
224 }),
225 );
226
227 it.effect(
228 "distinguishes a post-authentication session failure without exposing its payload",
229 () =>
230 Effect.gen(function* () {
231 const harness = yield* makeHarness();
232 yield* harness.auth.controller.start(owner);
233 yield* phase(harness.auth, "waiting");
234 yield* Deferred.succeed(harness.authenticated, undefined);
235 yield* Deferred.fail(
236 harness.discovered,
237 new AcpErrors.AcpRequestError({
238 code: -32603,
239 errorMessage: `Internal error ${callbackUrl}`,
240 method: "session/new",
241 }),
242 );
243 const failed = yield* phase(harness.auth, "failed");
244 assert.equal(
245 failed.message,
246 "Antigravity authenticated, but could not initialize a session or load models.",
247 );
248 assert.isNull(failed.authorizationUrl);
249 assert.deepEqual(harness.catalog(), ["previous-account-model"]);
250 yield* Deferred.await(harness.closed);
251 }),
252 );
253
254 it.effect("does not call callback HTTP success a successful Google sign-in", () =>
255 Effect.gen(function* () {
256 const harness = yield* makeHarness();
257 const state = yield* harness.auth.controller.start(owner);
258 yield* phase(harness.auth, "waiting");
259 yield* harness.auth.controller.complete(owner, { flowId: state.flowId!, callbackUrl });
260 yield* Deferred.fail(
261 harness.authenticated,
262 AcpErrors.AcpRequestError.internalError(`access_denied ${callbackUrl}`),
263 );
264 const failed = yield* phase(harness.auth, "failed");
265 assert.include(failed.message ?? "", "not approved");
266 assert.notInclude(failed.message ?? "", "test-code");
267 assert.isNull(failed.authorizationUrl);
268 assert.deepEqual(harness.catalog(), ["previous-account-model"]);
269 yield* Deferred.await(harness.closed);
270 }),
271 );
272
273 it.effect("accepts direct local or cached completion without a callback RPC", () =>
274 Effect.gen(function* () {
275 const harness = yield* makeHarness({ interactive: false });
276 yield* harness.auth.controller.start(owner);
277 yield* Deferred.succeed(harness.authenticated, undefined);
278 yield* Deferred.succeed(harness.discovered, undefined);
279 yield* phase(harness.auth, "succeeded");
280 assert.equal(harness.forwarded(), 0);
281 assert.deepEqual(harness.catalog(), ["gemini-test"]);
282 }),
283 );
284
285 it.effect("rejects mismatched callbacks without sending any HTTP request", () =>
286 Effect.gen(function* () {
287 const harness = yield* makeHarness();
288 const state = yield* harness.auth.controller.start(owner);
289 yield* phase(harness.auth, "waiting");
290 for (const invalidUrl of [
291 callbackUrl.replace("51234", "51235"),
292 callbackUrl.replace("test-state", "wrong-state"),
293 callbackUrl.replace("/?", "/other?"),
294 `${callbackUrl}&state=test-state`,
295 ]) {
296 const result = yield* harness.auth.controller
297 .complete(owner, { flowId: state.flowId!, callbackUrl: invalidUrl })
298 .pipe(Effect.exit);
299 assert.isTrue(Exit.isFailure(result));
300 }
301 assert.equal(harness.forwarded(), 0);
302 assert.equal((yield* phase(harness.auth, "waiting")).authorizationUrl, authorizationUrl);
303 yield* harness.auth.controller.cancel(owner, state.flowId!);
304 }),
305 );
306
307 it.effect("fails the flow when delivery fails after the requesting client disconnects", () =>
308 Effect.gen(function* () {
309 const deliveryGate = yield* Deferred.make<void>();
310 const harness = yield* makeHarness({
311 forwardCallback: Deferred.await(deliveryGate).pipe(
312 Effect.andThen(
313 Effect.fail(
314 new ProviderSetupError({
315 instanceId,
316 operation: "complete",
317 detail: "loopback refused",
318 }),
319 ),
320 ),
321 ),
322 });
323 const state = yield* harness.auth.controller.start(owner);
324 yield* phase(harness.auth, "waiting");
325 // The client sends the callback, then its socket drops before Google answers.
326 const request = yield* harness.auth.controller
327 .complete(owner, { flowId: state.flowId!, callbackUrl })
328 .pipe(Effect.forkScoped);
329 yield* phase(harness.auth, "verifying");
330 yield* Fiber.interrupt(request);
331 yield* Deferred.succeed(deliveryGate, undefined);
332 const failed = yield* phase(harness.auth, "failed");
333 assert.include(failed.message ?? "", "Could not deliver");
334 assert.isTrue(harness.events.includes("process-close"));
335 }),
336 );
337
338 it.effect("cancel closes the owned process without forwarding a denial", () =>
339 Effect.gen(function* () {
340 const harness = yield* makeHarness();
341 const state = yield* harness.auth.controller.start(owner);
342 yield* phase(harness.auth, "waiting");
343 const wrongOwner = yield* harness.auth.controller
344 .cancel(otherOwner, state.flowId!)
345 .pipe(Effect.exit);
346 assert.isTrue(Exit.isFailure(wrongOwner));
347 const cancelled = yield* harness.auth.controller.cancel(owner, state.flowId!);
348 assert.equal(cancelled.phase, "cancelled");
349 assert.isNull(cancelled.authorizationUrl);
350 assert.equal(harness.forwarded(), 0);
351 yield* Deferred.await(harness.closed);
352 assert.deepEqual(harness.catalog(), ["previous-account-model"]);
353 }),
354 );
355
356 it.effect("expires the flow at the official deadline and removes its URL", () =>
357 Effect.gen(function* () {
358 const harness = yield* makeHarness();
359 const state = yield* harness.auth.controller.start(owner);
360 yield* phase(harness.auth, "waiting");
361 yield* TestClock.adjust("300 seconds");
362 const failed = yield* phase(harness.auth, "failed");
363 assert.include(failed.message ?? "", "expired");
364 assert.isNull(failed.authorizationUrl);
365 yield* Deferred.await(harness.closed);
366 const late = yield* harness.auth.controller
367 .complete(owner, { flowId: state.flowId!, callbackUrl })
368 .pipe(Effect.exit);
369 assert.isTrue(Exit.isFailure(late));
370 assert.equal(harness.forwarded(), 0);
371 }),
372 );
373
374 it.effect("survives subscriber disconnect and does not replace a competing client's flow", () =>
375 Effect.gen(function* () {
376 const harness = yield* makeHarness();
377 const first = yield* harness.auth.controller.start(owner);
378 yield* phase(harness.auth, "waiting");
379 const second = yield* harness.auth.controller.start(owner);
380 assert.equal(first.flowId, second.flowId);
381 const competing = yield* harness.auth.controller.start(otherOwner).pipe(Effect.exit);
382 assert.isTrue(Exit.isFailure(competing));
383 const resumed = yield* phase(harness.auth, "waiting");
384 assert.equal(resumed.flowId, first.flowId);
385 assert.deepEqual(harness.events, ["process-open", "authenticate"]);
386 yield* harness.auth.controller.cancel(owner, first.flowId!);
387 }),
388 );
389
390 it.effect("sign-out closes admission and every process before fresh native logout", () =>
391 Effect.gen(function* () {
392 const harness = yield* makeHarness();
393 const processScope = yield* Scope.make();
394 yield* Effect.addFinalizer(() => Scope.close(processScope, Exit.void));
395 const stop = Effect.gen(function* () {
396 harness.events.push("chat-close");
397 yield* Scope.close(processScope, Exit.void);
398 });
399 yield* harness.auth.withProcess(stop, Effect.void).pipe(Scope.provide(processScope));
400 const stopSessions = Effect.gen(function* () {
401 harness.events.push("sessions-stop");
402 const denied = yield* harness.auth
403 .withProcess(
404 Effect.void,
405 Effect.sync(() => harness.events.push("late-process")),
406 )
407 .pipe(Effect.scoped, Effect.exit);
408 assert.isTrue(Exit.isFailure(denied));
409 });
410 const result = yield* harness.auth.controller.logout(stopSessions);
411 assert.equal(result.phase, "idle");
412 assert.deepEqual(harness.events, [
413 "sessions-stop",
414 "chat-close",
415 "process-open",
416 "initialize",
417 "logout",
418 "catalog-cleared",
419 "process-close",
420 ]);
421 assert.deepEqual(harness.catalog(), []);
422 yield* harness.auth.withProcess(Effect.void, Effect.void).pipe(Effect.scoped);
423 }),
424 );
425
426 it.effect("signs out after a slow packaged runtime starts", () =>
427 Effect.gen(function* () {
428 const entered = yield* Deferred.make<void>();
429 const initialized = yield* Deferred.make<void>();
430 const harness = yield* makeHarness({
431 beforeInitialize: Deferred.succeed(entered, undefined).pipe(
432 Effect.andThen(Deferred.await(initialized)),
433 ),
434 });
435 const logout = yield* harness.auth.controller.logout(Effect.void).pipe(Effect.forkScoped);
436 yield* Deferred.await(entered);
437 yield* TestClock.adjust("47 seconds");
438 yield* Deferred.succeed(initialized, undefined);
439 assert.equal((yield* Fiber.join(logout)).phase, "idle");
440 assert.include(harness.events, "logout");
441 assert.deepEqual(harness.catalog(), []);
442 yield* Deferred.await(harness.closed);
443 }),
444 );
445
446 it.effect("closes a stalled sign-out process without clearing its account catalog", () =>
447 Effect.gen(function* () {
448 const entered = yield* Deferred.make<void>();
449 const harness = yield* makeHarness({
450 beforeInitialize: Deferred.succeed(entered, undefined).pipe(Effect.andThen(Effect.never)),
451 });
452 const logout = yield* harness.auth.controller
453 .logout(Effect.void)
454 .pipe(Effect.exit, Effect.forkScoped);
455 yield* Deferred.await(entered);
456 yield* TestClock.adjust("90 seconds");
457 assert.isTrue(Exit.isFailure(yield* Fiber.join(logout)));
458 yield* Deferred.await(harness.closed);
459 assert.notInclude(harness.events, "logout");
460 assert.deepEqual(harness.catalog(), ["previous-account-model"]);
461 yield* harness.auth.withProcess(Effect.void, Effect.void).pipe(Effect.scoped);
462 }),
463 );
464
465 it.effect(
466 "sign-out interrupts startup without interrupting its caller after startup returns",
467 () =>
468 Effect.gen(function* () {
469 const harness = yield* makeHarness();
470 const entering = yield* Deferred.make<void>();
471 const continueStartup = yield* Deferred.make<void>();
472 const processScope = yield* Scope.make();
473 yield* Effect.addFinalizer(() => Scope.close(processScope, Exit.void));
474 const task = Effect.gen(function* () {
475 yield* Deferred.succeed(entering, undefined);
476 yield* Deferred.await(continueStartup);
477 harness.events.push("late-spawn");
478 });
479 const startup = yield* harness.auth
480 .withProcess(Scope.close(processScope, Exit.void), task)
481 .pipe(Scope.provide(processScope), Effect.forkScoped);
482 yield* Deferred.await(entering);
483 yield* harness.auth.controller.logout(Effect.void);
484 yield* Deferred.succeed(continueStartup, undefined);
485 assert.isTrue(Exit.isFailure(yield* Fiber.await(startup)));
486 assert.notInclude(harness.events, "late-spawn");
487 assert.include(harness.events, "logout");
488 }),
489 );
490
491 it.effect("failed session stopping still closes owned processes and skips native logout", () =>
492 Effect.gen(function* () {
493 const harness = yield* makeHarness();
494 const processScope = yield* Scope.make();
495 yield* Effect.addFinalizer(() => Scope.close(processScope, Exit.void));
496 const stop = Effect.gen(function* () {
497 harness.events.push("chat-close");
498 yield* Scope.close(processScope, Exit.void);
499 });
500 yield* harness.auth.withProcess(stop, Effect.void).pipe(Scope.provide(processScope));
501 const result = yield* harness.auth.controller
502 .logout(
503 Effect.fail(
504 new ProviderSetupError({
505 instanceId,
506 operation: "stopSessions",
507 detail: "Stop failed.",
508 }),
509 ),
510 )
511 .pipe(Effect.exit);
512 assert.isTrue(Exit.isFailure(result));
513 assert.deepEqual(harness.events, ["chat-close"]);
514 assert.deepEqual(harness.catalog(), ["previous-account-model"]);
515 }),
516 );
517
518 it.effect("finishes sign-out when the requesting client disconnects", () =>
519 Effect.gen(function* () {
520 const harness = yield* makeHarness();
521 const stopping = yield* Deferred.make<void>();
522 const continueStop = yield* Deferred.make<void>();
523 const request = yield* harness.auth.controller
524 .logout(
525 Effect.gen(function* () {
526 yield* Deferred.succeed(stopping, undefined);
527 yield* Deferred.await(continueStop);
528 }),
529 )
530 .pipe(Effect.forkScoped);
531 yield* Deferred.await(stopping);
532 yield* Fiber.interrupt(request);
533 yield* Deferred.succeed(continueStop, undefined);
534 const result = yield* harness.auth.controller.subscribe(owner).pipe(
535 Stream.filter((state) => state.message === "Signed out of Google."),
536 Stream.runHead,
537 Effect.map(Option.getOrThrow),
538 );
539 assert.equal(result.phase, "idle");
540 assert.deepEqual(harness.catalog(), []);
541 assert.include(harness.events, "logout");
542 }),
543 );
544
545 it.effect("does not call logout unless the official process advertises it", () =>
546 Effect.gen(function* () {
547 const harness = yield* makeHarness({ supportsLogout: false });
548 const result = yield* harness.auth.controller.logout(Effect.void).pipe(Effect.exit);
549 assert.isTrue(Exit.isFailure(result));
550 assert.deepEqual(harness.events, ["process-open", "initialize", "process-close"]);
551 assert.deepEqual(harness.catalog(), ["previous-account-model"]);
552 }),
553 );
554});