1import { cloneElement, isValidElement, type FunctionComponent, type ReactElement } from "react";
2import {
3 EnvironmentId,
4 ProviderDriverKind,
5 ProviderInstanceId,
6 type ProviderAuthState,
7 type ProviderInstallState,
8 type ServerProvider,
9} from "@t3tools/contracts";
10import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
11
12import { visitElements } from "../../test/reactElementTree";
13import { reactHookHarness as hooks } from "../../test/reactHookHarness";
14
15const setup = vi.hoisted(() => ({
16 auth: null as ProviderAuthState | null,
17 installation: null as ProviderInstallState | null,
18 authState: vi.fn(() => "auth"),
19 installState: vi.fn(() => "installation"),
20 startAuth: vi.fn(),
21 completeAuth: vi.fn(),
22 respondAuth: vi.fn(),
23 cancelAuth: vi.fn(),
24 logoutAuth: vi.fn(),
25 startInstall: vi.fn(),
26 cancelInstall: vi.fn(),
27 removeInstall: vi.fn(),
28 confirm: vi.fn(),
29}));
30
31vi.mock("react", async (importOriginal) => {
32 const actual = await importOriginal<typeof import("react")>();
33 const { reactHookHarness } = await import("../../test/reactHookHarness");
34 return {
35 ...actual,
36 useRef: reactHookHarness.useRef,
37 useState: reactHookHarness.useState,
38 };
39});
40
41vi.mock("react/compiler-runtime", async () => {
42 const { reactHookHarness } = await import("../../test/reactHookHarness");
43 return { c: reactHookHarness.useMemoCache };
44});
45
46vi.mock("../../state/server", () => ({
47 serverEnvironment: {
48 providerAuthState: setup.authState,
49 providerInstallState: setup.installState,
50 startProviderAuth: setup.startAuth,
51 completeProviderAuth: setup.completeAuth,
52 respondProviderAuth: setup.respondAuth,
53 cancelProviderAuth: setup.cancelAuth,
54 logoutProviderAuth: setup.logoutAuth,
55 startProviderInstall: setup.startInstall,
56 cancelProviderInstall: setup.cancelInstall,
57 removeProviderInstallation: setup.removeInstall,
58 },
59}));
60
61vi.mock("../../state/use-atom-command", () => ({
62 useAtomCommand: (command: unknown) => command,
63}));
64
65vi.mock("../../state/query", () => ({
66 useEnvironmentQuery: (atom: string) => ({
67 data: atom === "auth" ? setup.auth : setup.installation,
68 error: null,
69 isPending: false,
70 refresh: vi.fn(),
71 }),
72}));
73
74vi.mock("../../localApi", () => ({
75 ensureLocalApi: () => ({ dialogs: { confirm: setup.confirm } }),
76}));
77
78import { ProviderAuthenticationSection } from "./ProviderAuthenticationSection";
79import { ProviderSetupSection } from "./ProviderSetupSection";
80
81const environmentId = EnvironmentId.make("remote-google");
82const instanceId = ProviderInstanceId.make("antigravity_work");
83const provider: ServerProvider = {
84 instanceId,
85 driver: ProviderDriverKind.make("antigravity"),
86 installed: true,
87 enabled: true,
88 version: "test-version",
89 status: "error",
90 auth: { status: "unauthenticated" },
91 checkedAt: "2026-09-02T00:00:00.000Z",
92 models: [],
93 skills: [],
94 slashCommands: [],
95 setup: { canAuthenticate: true, canInstall: true },
96};
97
98function authState(patch: Partial<ProviderAuthState> = {}): ProviderAuthState {
99 return {
100 instanceId,
101 phase: "waiting",
102 flowId: "flow-1",
103 authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth?state=test-only",
104 expiresAt: "2026-09-02T00:05:00.000Z",
105 message: null,
106 ...patch,
107 };
108}
109
110function renderSetup(
111 options: {
112 readOnly?: boolean;
113 provider?: ServerProvider;
114 enabled?: boolean;
115 binaryPath?: string;
116 } = {},
117) {
118 hooks.beginRender();
119 const view = ProviderSetupSection({
120 environmentId,
121 environmentLabel: "Remote Google device",
122 instanceId,
123 provider: options.provider ?? provider,
124 binaryPath: options.binaryPath,
125 enabled: options.enabled ?? true,
126 readOnly: options.readOnly ?? false,
127 onEnable: vi.fn(),
128 });
129 const actions = visitElements(
130 view,
131 (element) =>
132 typeof element.type === "function" &&
133 element.props.environmentId === environmentId &&
134 element.props.instanceId === instanceId,
135 );
136 if (!actions) return view;
137 const Actions = actions.type as FunctionComponent<Record<string, unknown>>;
138 function expand(node: unknown): unknown {
139 if (Array.isArray(node)) return node.map(expand);
140 if (!isValidElement<Record<string, unknown>>(node)) return node;
141 if (node.type === ProviderAuthenticationSection)
142 return ProviderAuthenticationSection(
143 node.props as Parameters<typeof ProviderAuthenticationSection>[0],
144 );
145 return cloneElement(
146 node,
147 Object.fromEntries(Object.entries(node.props).map(([key, value]) => [key, expand(value)])),
148 );
149 }
150 return expand(Actions(actions.props));
151}
152
153function button(view: unknown, label: string) {
154 return visitElements(
155 view,
156 (element) =>
157 (element.props.children === label || element.props["aria-label"] === label) &&
158 typeof element.props.onClick === "function",
159 );
160}
161
162function countElements(
163 node: unknown,
164 predicate: (element: ReactElement<Record<string, unknown>>) => boolean,
165): number {
166 if (Array.isArray(node)) {
167 return node.reduce((total, child) => total + countElements(child, predicate), 0);
168 }
169 if (!isValidElement<Record<string, unknown>>(node)) return 0;
170 return (
171 Number(predicate(node)) +
172 Object.values(node.props).reduce<number>(
173 (total, value) => total + countElements(value, predicate),
174 0,
175 )
176 );
177}
178
179function click(view: unknown, label: string) {
180 const target = button(view, label);
181 if (!target) throw new Error(`Missing button: ${label}`);
182 if (target.props.disabled) throw new Error(`Button is disabled: ${label}`);
183 (target.props.onClick as () => void)();
184}
185
186function setCallback(view: unknown, value: string) {
187 const input = visitElements(
188 view,
189 (element) => element.props.id === `provider-callback-${instanceId}`,
190 );
191 if (!input) throw new Error("Missing callback input.");
192 (input.props.onChange as (event: { target: { value: string } }) => void)({ target: { value } });
193}
194
195function submitCallback(view: unknown) {
196 const form = visitElements(view, (element) => element.type === "form");
197 if (!form) throw new Error("Missing callback form.");
198 (form.props.onSubmit as (event: { preventDefault: () => void }) => void)({
199 preventDefault: vi.fn(),
200 });
201}
202
203async function flushPromises() {
204 await Promise.resolve();
205 await Promise.resolve();
206 await Promise.resolve();
207}
208
209describe("Antigravity setup", () => {
210 beforeEach(() => {
211 hooks.reset();
212 vi.clearAllMocks();
213 setup.auth = authState();
214 setup.installation = {
215 driver: ProviderDriverKind.make("antigravity"),
216 operationId: null,
217 phase: "idle",
218 downloadedBytes: 0,
219 totalBytes: null,
220 version: null,
221 installedVersion: null,
222 canRemove: false,
223 message: null,
224 };
225 for (const command of [
226 setup.startAuth,
227 setup.completeAuth,
228 setup.cancelAuth,
229 setup.logoutAuth,
230 setup.startInstall,
231 setup.cancelInstall,
232 setup.removeInstall,
233 ]) {
234 command.mockReset().mockResolvedValue({ _tag: "Success", value: undefined });
235 }
236 setup.confirm.mockReset().mockResolvedValue(false);
237 });
238
239 it("waits for verified auth after submitting a callback to the selected environment", async () => {
240 const callbackUrl = "http://127.0.0.1:5555/?state=test-only&code=test-only";
241 setCallback(renderSetup(), callbackUrl);
242 submitCallback(renderSetup());
243 await flushPromises();
244
245 expect(setup.completeAuth).toHaveBeenCalledWith({
246 environmentId,
247 input: { instanceId, flowId: "flow-1", callbackUrl },
248 });
249 let view = renderSetup();
250 expect(visitElements(view, (element) => element.props.children === "Signed in.")).toBeNull();
251 expect(
252 visitElements(view, (element) => element.props.id === `provider-callback-${instanceId}`)
253 ?.props.value,
254 ).toBe("");
255
256 setup.auth = authState({ phase: "verifying", authorizationUrl: null });
257 expect(
258 visitElements(renderSetup(), (element) => element.props.children === "Signed in."),
259 ).toBeNull();
260 setup.auth = authState({ phase: "succeeded", authorizationUrl: null });
261 view = renderSetup({
262 provider: { ...provider, status: "ready", auth: { status: "authenticated" } },
263 });
264 expect(
265 visitElements(view, (element) => element.props.children === "Signed in."),
266 ).not.toBeNull();
267 });
268
269 it("offers sign-in again when credentials expire after a completed auth flow", () => {
270 setup.auth = authState({
271 phase: "succeeded",
272 authorizationUrl: null,
273 message: "Google sign-in complete.",
274 });
275 renderSetup({
276 provider: { ...provider, status: "ready", auth: { status: "authenticated" } },
277 });
278 const expired = renderSetup();
279 expect(button(expired, "Sign in")).not.toBeNull();
280 expect(visitElements(expired, (element) => element.props.children === "Signed in.")).toBeNull();
281 expect(
282 visitElements(expired, (element) => element.props.children === "Google sign-in complete."),
283 ).toBeNull();
284 });
285
286 it("does not send a callback left over from a replaced sign-in flow", async () => {
287 const interaction = {
288 type: "browser" as const,
289 id: "same-interaction",
290 url: "https://example.com/login",
291 requiresConsent: false,
292 acceptsCallback: true,
293 };
294 setup.auth = authState({ interaction });
295 setCallback(renderSetup(), "http://127.0.0.1:5555/?state=old-flow&code=test-only");
296 setup.auth = authState({ flowId: "flow-2", interaction });
297 submitCallback(renderSetup());
298 await flushPromises();
299
300 expect(setup.completeAuth).not.toHaveBeenCalled();
301 expect(setup.authState).toHaveBeenLastCalledWith({ environmentId, input: { instanceId } });
302 });
303
304 it("coalesces repeated sign-in clicks while start is pending", async () => {
305 setup.auth = authState({ phase: "idle", flowId: null, authorizationUrl: null });
306 let completeStart: (value: { _tag: "Success"; value: undefined }) => void = () => {
307 throw new Error("Missing start resolver.");
308 };
309 const pending = new Promise<{ _tag: "Success"; value: undefined }>((resolve) => {
310 completeStart = resolve;
311 });
312 setup.startAuth.mockReturnValueOnce(pending);
313 const view = renderSetup();
314 click(view, "Sign in");
315 click(view, "Sign in");
316
317 expect(setup.startAuth).toHaveBeenCalledTimes(1);
318 expect(setup.startAuth).toHaveBeenCalledWith({ environmentId, input: { instanceId } });
319 completeStart({ _tag: "Success", value: undefined });
320 await flushPromises();
321 });
322
323 it("shows a repeated runtime status message only once", () => {
324 setup.installation = {
325 ...setup.installation!,
326 operationId: "install-1",
327 phase: "verifying",
328 message: "Checking the downloaded runtime.",
329 };
330
331 const view = renderSetup();
332 expect(
333 countElements(
334 view,
335 (element) => element.props.children === "Checking the downloaded runtime.",
336 ),
337 ).toBe(1);
338 });
339
340 it("removes an owned damaged runtime only after confirmation", async () => {
341 setup.auth = authState({ phase: "idle", flowId: null, authorizationUrl: null });
342 setup.installation = {
343 ...setup.installation!,
344 phase: "failed",
345 canRemove: true,
346 installedVersion: null,
347 };
348 const view = renderSetup();
349 click(view, "Remove downloaded runtime");
350 await flushPromises();
351 expect(setup.removeInstall).not.toHaveBeenCalled();
352
353 setup.confirm.mockResolvedValue(true);
354 click(view, "Remove downloaded runtime");
355 await flushPromises();
356 expect(setup.removeInstall).toHaveBeenCalledWith({ environmentId, input: { instanceId } });
357 });
358
359 it.each([true, false])(
360 "can sign out a verified account when its instance is enabled=%s",
361 async (enabled) => {
362 setup.auth = authState({ phase: "idle", flowId: null, authorizationUrl: null });
363 setup.confirm.mockResolvedValue(true);
364 const view = renderSetup({
365 enabled,
366 provider: {
367 ...provider,
368 enabled,
369 installed: false,
370 status: enabled ? "warning" : "disabled",
371 auth: { status: "authenticated" },
372 },
373 });
374 click(view, "Sign out");
375 await flushPromises();
376 expect(setup.logoutAuth).toHaveBeenCalledWith({ environmentId, input: { instanceId } });
377 },
378 );
379
380 it.each(["unauthenticated", "unknown"] as const)(
381 "does not offer sign-out for an unverified %s account",
382 (status) => {
383 setup.auth = authState({ phase: "idle", flowId: null, authorizationUrl: null });
384 const view = renderSetup({
385 provider: { ...provider, auth: { status, canLogout: true } },
386 });
387 expect(button(view, "Sign in")).not.toBeNull();
388 expect(button(view, "Sign out")).toBeNull();
389 expect(button(view, "Change account")).toBeNull();
390 },
391 );
392
393 it("offers account actions after verified login when discovery cannot identify auth", () => {
394 setup.auth = authState({ phase: "succeeded", flowId: null, authorizationUrl: null });
395 const view = renderSetup({ provider: { ...provider, auth: { status: "unknown" } } });
396 expect(button(view, "Change account")).not.toBeNull();
397 expect(button(view, "Sign out")).not.toBeNull();
398 expect(button(view, "Sign in")).toBeNull();
399 });
400
401 it("does not let a shared managed install hide an invalid custom binary path", () => {
402 setup.auth = authState({ phase: "idle", flowId: null, authorizationUrl: null });
403 setup.installation = {
404 ...setup.installation!,
405 installedVersion: "test-version",
406 canRemove: true,
407 };
408 const view = renderSetup({
409 provider: { ...provider, installed: false },
410 binaryPath: "/missing/antigravity",
411 });
412 expect(button(view, "Sign in")?.props.disabled).toBe(true);
413 expect(setup.startAuth).not.toHaveBeenCalled();
414 });
415
416 it.each(["read-only", "older-server"] as const)(
417 "does not open private setup subscriptions for a %s view",
418 (mode) => {
419 const { setup: _setup, ...olderProvider } = provider;
420 renderSetup(mode === "read-only" ? { readOnly: true } : { provider: olderProvider });
421 expect(setup.authState).not.toHaveBeenCalled();
422 expect(setup.installState).not.toHaveBeenCalled();
423 expect(setup.startAuth).not.toHaveBeenCalled();
424 },
425 );
426});