Skip to content
Source
Sign inSign up
Open History to see the last commit

AcpSessionManagementSection.browser.test.tsx

450 lines16.1 KB

1import {
2 EnvironmentId,
3 ProjectId,
4 ProviderDriverKind,
5 ProviderInstanceId,
6 ThreadId,
7 type ServerProvider,
8 type AcpRegistryConfigurableProvider,
9} from "@t3tools/contracts";
10import type { ComponentProps } from "react";
11import { expect, it, vi } from "vite-plus/test";
12import { userEvent } from "vite-plus/test/browser";
13​
14import { act, render } from "../../../test/render";
15​
16const commands = vi.hoisted(() => ({
17 list: vi.fn(),
18 import: vi.fn(),
19 delete: vi.fn(),
20 listProviders: vi.fn(),
21 setProvider: vi.fn(),
22 disableProvider: vi.fn(),
23 logout: vi.fn(),
24}));
25const grants = vi.hoisted(() => ({ scopes: new Set<string>() }));
26const dialogs = vi.hoisted(() => ({ confirm: vi.fn() }));
27​
28vi.mock(import("~/state/session"), async (original) => ({
29 ...(await original()),
30 useEnvironmentScope: ((_id: unknown, scope: string) => grants.scopes.has(scope)) as never,
31 readEnvironmentScope: ((_id: unknown, scope: string) => grants.scopes.has(scope)) as never,
32}));
33vi.mock(import("../../state/use-atom-command"), async (original) => {
34 const { serverEnvironment } = await import("../../state/server");
35 const byAtom = new Map<unknown, unknown>([
36 [serverEnvironment.listAcpRegistrySessions, commands.list],
37 [serverEnvironment.importAcpRegistrySession, commands.import],
38 [serverEnvironment.deleteAcpRegistrySession, commands.delete],
39 [serverEnvironment.listAcpRegistryProviders, commands.listProviders],
40 [serverEnvironment.setAcpRegistryProvider, commands.setProvider],
41 [serverEnvironment.disableAcpRegistryProvider, commands.disableProvider],
42 [serverEnvironment.logoutAcpRegistry, commands.logout],
43 ]);
44 return {
45 ...(await original()),
46 useAtomCommand: ((atom: unknown) => byAtom.get(atom)) as never,
47 };
48});
49vi.mock(import("../../localApi"), async (original) => ({
50 ...(await original()),
51 ensureLocalApi: () => ({ dialogs }) as never,
52}));
53​
54import { AcpSessionManagementSection } from "./AcpSessionManagementSection";
55​
56const environmentId = EnvironmentId.make("remote-device");
57const instanceId = ProviderInstanceId.make("acpRegistry_antigravity");
58const projectId = ProjectId.make("project-antigravity");
59const session = {
60 sessionId: "native-session-1",
61 cwd: "/workspace/antigravity",
62 additionalDirectories: [],
63 title: "Native Antigravity session",
64 updatedAt: "2026-08-23T00:00:00Z",
65 importedThreadId: null,
66} as const;
67const provider = {
68 instanceId,
69 driver: ProviderDriverKind.make("acpRegistry"),
70 enabled: true,
71 installed: true,
72 version: "1.0.0",
73 status: "ready",
74 auth: { status: "authenticated", canLogout: true },
75 nativeSessions: { canList: true, canLoad: true, canResume: true, canDelete: true },
76 configurableProviders: true,
77 checkedAt: "2026-08-23T00:00:00.000Z",
78 models: [],
79 slashCommands: [],
80 skills: [],
81} satisfies ServerProvider;
82​
83const project = { id: projectId, title: "Antigravity", workspaceRoot: session.cwd };
84const replacement = {
85 id: ProjectId.make("replacement"),
86 title: "Replacement",
87 workspaceRoot: "/replacement",
88};
89type Projects = ComponentProps<typeof AcpSessionManagementSection>["projects"];
90​
91async function renderSection(options: { canList?: boolean; projects?: Projects } = {}) {
92 const section = (projects = options.projects ?? [project]) => (
93 <AcpSessionManagementSection
94 environmentId={environmentId}
95 instanceId={instanceId}
96 provider={{
97 ...provider,
98 nativeSessions: { ...provider.nativeSessions, canList: options.canList ?? true },
99 }}
100 projects={projects}
101 readOnly={false}
102 />
103 );
104 const screen = await render(section());
105 const button = (label: string) => {
106 const found = [...screen.container.querySelectorAll("button")].find(
107 (element) => element.textContent === label,
108 );
109 expect(found, `button ${label}`).toBeDefined();
110 return found!;
111 };
112 return Object.assign(screen, {
113 button,
114 click: (label: string) => act(async () => button(label).click()),
115 setProjects: (projects: Projects) => screen.rerender(section(projects)),
116 });
118​
119function setup() {
120 grants.scopes = new Set(["orchestration:read", "orchestration:operate", "providers:manage"]);
121 commands.list.mockReset().mockResolvedValue({
122 _tag: "Success",
123 value: {
124 sessions: [session],
125 nextCursor: null,
126 canLoad: true,
127 canResume: true,
128 canDelete: true,
129 },
130 });
131 commands.import.mockReset().mockResolvedValue({
132 _tag: "Success",
133 value: { threadId: ThreadId.make("thread-imported"), imported: true },
134 });
135 commands.delete.mockReset().mockResolvedValue({
136 _tag: "Success",
137 value: { deleted: true },
138 });
139 commands.listProviders.mockReset().mockResolvedValue({
140 _tag: "Success",
141 value: {
142 providers: [
143 {
144 providerId: "google",
145 supported: ["openai"],
146 required: false,
147 current: { apiType: "openai", baseUrl: "https://api.example.test/v1" },
148 },
149 ],
150 },
151 });
152 commands.setProvider.mockReset().mockResolvedValue({
153 _tag: "Success",
154 value: { configured: true },
155 });
156 commands.disableProvider.mockReset().mockResolvedValue({
157 _tag: "Success",
158 value: { disabled: true },
159 });
160 commands.logout.mockReset().mockResolvedValue({
161 _tag: "Success",
162 value: { loggedOut: true },
163 });
164 dialogs.confirm.mockReset().mockResolvedValue(true);
166​
167it("does not import with provider management alone", async () => {
168 setup();
169 grants.scopes.delete("orchestration:operate");
170 await using screen = await renderSection();
171 await screen.click("List sessions");
172 expect(screen.button("Import").disabled).toBe(true);
173 await screen.click("Import");
174 expect(commands.import).not.toHaveBeenCalled();
175});
176​
177it("rechecks task access after confirming deletion", async () => {
178 setup();
179 await using screen = await renderSection();
180 await screen.click("List sessions");
181 dialogs.confirm.mockImplementationOnce(async () => {
182 grants.scopes.delete("orchestration:operate");
183 return true;
184 });
185 await screen.click("Delete");
186 expect(commands.delete).not.toHaveBeenCalled();
187});
188​
189it.each(["Save", "Disable"])("keeps other provider edits through a %s refetch", async (action) => {
190 setup();
191 const providers: AcpRegistryConfigurableProvider[] = ["google", "other"].map((providerId) => ({
192 providerId,
193 supported: ["openai"],
194 required: false,
195 current: { apiType: "openai", baseUrl: "https://api.example.test/v1" },
196 }));
197 const response = { _tag: "Success" as const, value: { providers } };
198 commands.listProviders.mockResolvedValue(response);
199 await using screen = await renderSection();
200 await screen.click("List providers");
201 const input = (label: string) =>
202 screen.container.querySelector<HTMLInputElement>(`[aria-label="${label}"]`)!;
203 await userEvent.fill(input("other base URL"), "https://other.example.test/v1");
204 await userEvent.fill(input("other write-only headers JSON"), '{"X-Test":"unsaved"}');
205 await userEvent.fill(input("google write-only headers JSON"), '{"X-Test":"saved"}');
206​
207 let resolveReload!: (value: typeof response) => void;
208 const reload = new Promise<typeof response>((resolve) => {
209 resolveReload = resolve;
210 });
211 commands.listProviders.mockReturnValueOnce(reload);
212 let resolveMutation!: () => void;
213 const mutation = new Promise<void>((resolve) => {
214 resolveMutation = resolve;
215 });
216 const command = action === "Save" ? commands.setProvider : commands.disableProvider;
217 command.mockReturnValueOnce(
218 mutation.then(() => ({
219 _tag: "Success",
220 value: action === "Save" ? { configured: true } : { disabled: true },
221 })),
222 );
223 await screen.click(action);
224 expect(screen.button("Refresh").disabled).toBe(true);
225 await act(async () => resolveMutation());
226 for (const button of screen.container.querySelectorAll<HTMLButtonElement>("button")) {
227 if (button.textContent === "Save" || button.textContent === "Disable") {
228 expect(button.disabled).toBe(true);
229 }
230 }
231 await userEvent.fill(input("other base URL"), "https://newer.example.test/v1");
232 await act(async () => resolveReload(response));
233​
234 expect(input("other base URL").value).toBe("https://newer.example.test/v1");
235 expect(input("other write-only headers JSON").value).toBe('{"X-Test":"unsaved"}');
236 expect(input("google write-only headers JSON").value).toBe("");
237 await screen.click("Refresh");
238 expect(input("other base URL").value).toBe("https://api.example.test/v1");
239 expect(input("other write-only headers JSON").value).toBe("");
240});
241​
242it.each([true, false])(
243 "enables project actions after projects arrive (sessions: %s)",
244 async (canList) => {
245 setup();
246 await using screen = await renderSection({ canList, projects: [] });
247 await screen.setProjects([project]);
248 const action = screen.getByRole("button", {
249 name: canList ? "List sessions" : "List providers",
250 });
251 await expect.element(action).toBeEnabled();
252 if (!canList) {
253 await expect.element(screen.getByRole("combobox")).not.toBeInTheDocument();
254 }
255 await action.click();
256 if (canList) {
257 await expect.element(screen.getByText(session.title)).toBeVisible();
258 expect(commands.list).toHaveBeenCalledWith({
259 environmentId,
260 input: { instanceId, projectId },
261 });
262 } else {
263 await expect
264 .element(screen.getByRole("textbox", { name: "google base URL" }))
265 .toHaveValue("https://api.example.test/v1");
266 expect(commands.listProviders).toHaveBeenCalledWith({
267 environmentId,
268 input: { instanceId, projectId },
269 });
270 }
271 },
272);
273​
274it.each([true, false])(
275 "preserves a valid selection and clears a removed project's data (sessions: %s)",
276 async (canList) => {
277 setup();
278 const second = { id: ProjectId.make("second"), title: "Second", workspaceRoot: "/second" };
279 commands.list.mockResolvedValue({
280 _tag: "Success",
281 value: { sessions: [session], nextCursor: "page-2" },
282 });
283 await using screen = await renderSection({ canList, projects: [project, second] });
284 await screen.getByRole("combobox").click();
285 if (canList) {
286 // Keys sent before the popup takes focus only move focus onto the selected option.
287 await expect.element(screen.getByRole("option", { name: "Antigravity" })).toHaveFocus();
288 await userEvent.keyboard("{ArrowDown}{Enter}");
289 } else await screen.getByRole("option", { name: "Second", exact: true }).click();
290 await expect
291 .element(
292 screen.getByRole("combobox", {
293 name: canList ? "Project for ACP sessions" : "Project for ACP providers",
294 }),
295 )
296 .toHaveFocus();
297 if (canList) await screen.getByRole("button", { name: "List sessions" }).click();
298 await screen.getByRole("button", { name: "List providers" }).click();
299 await userEvent.fill(
300 screen.getByRole("textbox", { name: "google base URL" }),
301 "https://draft.test",
302 );
303 await userEvent.fill(
304 screen.getByLabelText("google write-only headers JSON"),
305 '{"Secret":"draft"}',
306 );
307​
308 await screen.setProjects([project, replacement, second]);
309 await expect
310 .element(
311 screen.getByRole("combobox", {
312 name: canList ? "Project for ACP sessions" : "Project for ACP providers",
313 }),
314 )
315 .toHaveTextContent("Second");
316 await expect
317 .element(screen.getByRole("textbox", { name: "google base URL" }))
318 .toHaveValue("https://draft.test");
319 await screen.getByRole("button", { name: "Save", exact: true }).click();
320 expect(commands.setProvider).toHaveBeenLastCalledWith({
321 environmentId,
322 input: {
323 instanceId,
324 projectId: second.id,
325 providerId: "google",
326 apiType: "openai",
327 baseUrl: "https://draft.test",
328 headers: { Secret: "draft" },
329 },
330 });
331​
332 await screen.setProjects([project]);
333 await expect.element(screen.getByText(session.title)).not.toBeInTheDocument();
334 await expect.element(screen.getByRole("button", { name: "Load more" })).not.toBeInTheDocument();
335 await expect
336 .element(screen.getByRole("textbox", { name: "google base URL" }))
337 .not.toBeInTheDocument();
338 await screen.getByRole("button", { name: "List providers" }).click();
339 expect(commands.listProviders).toHaveBeenLastCalledWith({
340 environmentId,
341 input: { instanceId, projectId },
342 });
343 await expect.element(screen.getByLabelText("google write-only headers JSON")).toHaveValue("");
344 await expect
345 .element(screen.getByRole("textbox", { name: "google base URL" }))
346 .toHaveValue("https://api.example.test/v1");
347​
348 await screen.setProjects([second, project]);
349 await expect
350 .element(
351 screen.getByRole("combobox", {
352 name: canList ? "Project for ACP sessions" : "Project for ACP providers",
353 }),
354 )
355 .toHaveTextContent("Antigravity");
356 await expect
357 .element(screen.getByRole("textbox", { name: "google base URL" }))
358 .toHaveValue("https://api.example.test/v1");
359​
360 await screen.setProjects([]);
361 await expect.element(screen.getByRole("button", { name: "List providers" })).toBeDisabled();
362 await expect
363 .element(screen.getByRole("textbox", { name: "google base URL" }))
364 .not.toBeInTheDocument();
365 },
366);
367​
368it("ignores a late listing after project replacement", async () => {
369 setup();
370 let finish!: (value: unknown) => void;
371 commands.list.mockReturnValueOnce(
372 new Promise((resolve) => {
373 finish = resolve;
374 }),
375 );
376 await using screen = await renderSection();
377 await screen.click("List sessions");
378 await screen.setProjects([replacement]);
379 await act(async () =>
380 finish({ _tag: "Success", value: { sessions: [session], nextCursor: "old-page" } }),
381 );
382 await expect.element(screen.getByText(session.title)).not.toBeInTheDocument();
383 await expect.element(screen.getByRole("button", { name: "Load more" })).not.toBeInTheDocument();
384 await expect.element(screen.getByRole("button", { name: "List sessions" })).toBeEnabled();
385});
386​
387it.each(["Save", "Disable"])("does not refetch the old project after a late %s", async (action) => {
388 setup();
389 const command = action === "Save" ? commands.setProvider : commands.disableProvider;
390 let finish!: () => void;
391 command.mockReturnValueOnce(
392 new Promise<void>((resolve) => {
393 finish = resolve;
394 }).then(() => ({
395 _tag: "Success",
396 value: action === "Save" ? { configured: true } : { disabled: true },
397 })),
398 );
399 await using screen = await renderSection();
400 await screen.click("List providers");
401 await screen.click(action);
402 expect(command).toHaveBeenCalledOnce();
403 await screen.setProjects([replacement]);
404 await act(async () => finish());
405 expect(commands.listProviders).toHaveBeenCalledOnce();
406 await expect.element(screen.getByRole("button", { name: "List providers" })).toBeEnabled();
407});
408​
409it.each(["Delete", "Disable"])(
410 "does not act on a replaced project after confirming %s",
411 async (action) => {
412 setup();
413 let finish!: (confirmed: boolean) => void;
414 dialogs.confirm.mockReturnValueOnce(
415 new Promise<boolean>((resolve) => {
416 finish = resolve;
417 }),
418 );
419 await using screen = await renderSection();
420 await screen.click(action === "Delete" ? "List sessions" : "List providers");
421 await screen.click(action);
422 await screen.setProjects([replacement]);
423 await act(async () => finish(true));
424 expect(action === "Delete" ? commands.delete : commands.disableProvider).not.toHaveBeenCalled();
425 },
426);
427​
428it("keeps logout pending across project replacement and clears replacement sessions", async () => {
429 setup();
430 let finish!: () => void;
431 commands.logout.mockReturnValueOnce(
432 new Promise<void>((resolve) => {
433 finish = resolve;
434 }).then(() => ({
435 _tag: "Success",
436 value: { loggedOut: true },
437 })),
438 );
439 await using screen = await renderSection();
440 await screen.click("Log out");
441 await screen.setProjects([replacement]);
442 await screen.click("List sessions");
443 await expect.element(screen.getByText(session.title)).toBeVisible();
444 await expect.element(screen.getByRole("button", { name: "Logging out" })).toBeDisabled();
445 await screen.click("Logging out");
446 expect(commands.logout).toHaveBeenCalledOnce();
447 await act(async () => finish());
448 await expect.element(screen.getByText(session.title)).not.toBeInTheDocument();
449 await expect.element(screen.getByRole("button", { name: "Log out", exact: true })).toBeEnabled();
450});