1import type { ReactElement } from "react";
2import {
3 EnvironmentId,
4 ProjectId,
5 ProviderDriverKind,
6 ProviderInstanceId,
7 ThreadId,
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 atoms = vi.hoisted(() => ({
16 list: Symbol("list-acp-sessions"),
17 import: Symbol("import-acp-session"),
18 delete: Symbol("delete-acp-session"),
19 listProviders: Symbol("list-acp-providers"),
20 setProvider: Symbol("set-acp-provider"),
21 disableProvider: Symbol("disable-acp-provider"),
22 logout: Symbol("logout-acp"),
23}));
24
25const commands = vi.hoisted(() => ({
26 list: vi.fn(),
27 import: vi.fn(),
28 delete: vi.fn(),
29 listProviders: vi.fn(),
30 setProvider: vi.fn(),
31 disableProvider: vi.fn(),
32 logout: vi.fn(),
33}));
34
35const grants = vi.hoisted(() => ({ scopes: new Set<string>() }));
36vi.mock("~/state/session", async (original) => ({
37 ...(await original<typeof import("~/state/session")>()),
38 useEnvironmentScope: (_id: unknown, scope: string) => grants.scopes.has(scope),
39 readEnvironmentScope: (_id: unknown, scope: string) => grants.scopes.has(scope),
40}));
41const dialogs = vi.hoisted(() => ({ confirm: vi.fn() }));
42
43vi.mock("react", async (importOriginal) => {
44 const actual = await importOriginal<typeof import("react")>();
45 const { reactHookHarness } = await import("../../test/reactHookHarness");
46 return {
47 ...actual,
48 useState: reactHookHarness.useState,
49 };
50});
51
52vi.mock("react/compiler-runtime", async () => {
53 const { reactHookHarness } = await import("../../test/reactHookHarness");
54 return { c: reactHookHarness.useMemoCache };
55});
56
57vi.mock("../../state/server", () => ({
58 serverEnvironment: {
59 listAcpRegistrySessions: atoms.list,
60 importAcpRegistrySession: atoms.import,
61 deleteAcpRegistrySession: atoms.delete,
62 listAcpRegistryProviders: atoms.listProviders,
63 setAcpRegistryProvider: atoms.setProvider,
64 disableAcpRegistryProvider: atoms.disableProvider,
65 logoutAcpRegistry: atoms.logout,
66 },
67}));
68
69vi.mock("../../state/use-atom-command", () => ({
70 useAtomCommand: (atom: symbol) => {
71 if (atom === atoms.list) return commands.list;
72 if (atom === atoms.import) return commands.import;
73 if (atom === atoms.delete) return commands.delete;
74 if (atom === atoms.listProviders) return commands.listProviders;
75 if (atom === atoms.setProvider) return commands.setProvider;
76 if (atom === atoms.disableProvider) return commands.disableProvider;
77 return commands.logout;
78 },
79}));
80
81vi.mock("../../localApi", () => ({
82 ensureLocalApi: () => ({ dialogs }),
83}));
84
85import { AcpSessionManagementSection } from "./AcpSessionManagementSection";
86
87const environmentId = EnvironmentId.make("remote-device");
88const instanceId = ProviderInstanceId.make("acpRegistry_antigravity");
89const projectId = ProjectId.make("project-antigravity");
90const session = {
91 sessionId: "native-session-1",
92 cwd: "/workspace/antigravity",
93 additionalDirectories: [],
94 title: "Native Antigravity session",
95 updatedAt: "2026-08-23T00:00:00Z",
96 importedThreadId: null,
97} as const;
98const provider = {
99 instanceId,
100 driver: ProviderDriverKind.make("acpRegistry"),
101 enabled: true,
102 installed: true,
103 version: "1.0.0",
104 status: "ready",
105 auth: { status: "authenticated", canLogout: true },
106 nativeSessions: { canList: true, canLoad: true, canResume: true, canDelete: true },
107 configurableProviders: true,
108 checkedAt: "2026-08-23T00:00:00.000Z",
109 models: [],
110 slashCommands: [],
111 skills: [],
112} satisfies ServerProvider;
113
114function render(readOnly = false): ReactElement<Record<string, unknown>> {
115 hooks.beginRender();
116 return AcpSessionManagementSection({
117 environmentId,
118 instanceId,
119 provider,
120 projects: [{ id: projectId, title: "Antigravity", workspaceRoot: session.cwd }],
121 readOnly,
122 }) as ReactElement<Record<string, unknown>>;
123}
124
125function findByLabel(
126 tree: ReactElement<Record<string, unknown>>,
127 label: string,
128): ReactElement<Record<string, unknown>> {
129 const found = visitElements(tree, (element) => element.props.children === label);
130 expect(found).not.toBeNull();
131 return found!;
132}
133
134function findByAriaLabel(
135 tree: ReactElement<Record<string, unknown>>,
136 label: string,
137): ReactElement<Record<string, unknown>> {
138 const found = visitElements(tree, (element) => element.props["aria-label"] === label);
139 expect(found).not.toBeNull();
140 return found!;
141}
142
143async function flushPromises(): Promise<void> {
144 await Promise.resolve();
145 await Promise.resolve();
146}
147
148beforeEach(() => {
149 hooks.reset();
150 grants.scopes = new Set(["orchestration:read", "orchestration:operate", "providers:manage"]);
151 commands.list.mockReset().mockResolvedValue({
152 _tag: "Success",
153 value: {
154 sessions: [session],
155 nextCursor: null,
156 canLoad: true,
157 canResume: true,
158 canDelete: true,
159 },
160 });
161 commands.import.mockReset().mockResolvedValue({
162 _tag: "Success",
163 value: { threadId: ThreadId.make("thread-imported"), imported: true },
164 });
165 commands.delete.mockReset().mockResolvedValue({
166 _tag: "Success",
167 value: { deleted: true },
168 });
169 commands.listProviders.mockReset().mockResolvedValue({
170 _tag: "Success",
171 value: {
172 providers: [
173 {
174 providerId: "google",
175 supported: ["openai"],
176 required: false,
177 current: { apiType: "openai", baseUrl: "https://api.example.test/v1" },
178 },
179 ],
180 },
181 });
182 commands.setProvider.mockReset().mockResolvedValue({
183 _tag: "Success",
184 value: { configured: true },
185 });
186 commands.disableProvider.mockReset().mockResolvedValue({
187 _tag: "Success",
188 value: { disabled: true },
189 });
190 commands.logout.mockReset().mockResolvedValue({
191 _tag: "Success",
192 value: { loggedOut: true },
193 });
194 dialogs.confirm.mockReset().mockResolvedValue(true);
195});
196
197describe("AcpSessionManagementSection", () => {
198 it("lists and imports native sessions through the owning environment", async () => {
199 const initial = render();
200 (findByLabel(initial, "List sessions").props.onClick as (() => void) | undefined)?.();
201 await flushPromises();
202
203 expect(commands.list).toHaveBeenCalledWith({
204 environmentId,
205 input: { instanceId, projectId },
206 });
207
208 const listed = render();
209 (findByLabel(listed, "Import").props.onClick as (() => void) | undefined)?.();
210 await flushPromises();
211
212 expect(commands.import).toHaveBeenCalledWith({
213 environmentId,
214 input: {
215 instanceId,
216 projectId,
217 sessionId: session.sessionId,
218 title: session.title,
219 updatedAt: session.updatedAt,
220 },
221 });
222 expect(findByLabel(render(), "Imported")).not.toBeNull();
223 });
224
225 it("logs out the provider instance through the owning environment", async () => {
226 const tree = render();
227 (findByLabel(tree, "Log out").props.onClick as (() => void) | undefined)?.();
228 await flushPromises();
229
230 expect(commands.logout).toHaveBeenCalledWith({
231 environmentId,
232 input: { instanceId },
233 });
234 });
235
236 it("deletes unimported native sessions after destructive confirmation", async () => {
237 (findByLabel(render(), "List sessions").props.onClick as (() => void) | undefined)?.();
238 await flushPromises();
239 (findByLabel(render(), "Delete").props.onClick as (() => void) | undefined)?.();
240 await flushPromises();
241
242 expect(dialogs.confirm).toHaveBeenCalledOnce();
243 expect(commands.delete).toHaveBeenCalledWith({
244 environmentId,
245 input: { instanceId, projectId, sessionId: session.sessionId },
246 });
247 });
248
249 it("locks the project picker while a project-scoped request is pending", async () => {
250 let resolveProviders!: (result: {
251 readonly _tag: "Success";
252 readonly value: { readonly providers: [] };
253 }) => void;
254 commands.listProviders.mockReturnValueOnce(
255 new Promise((resolve) => {
256 resolveProviders = resolve;
257 }),
258 );
259 (findByLabel(render(), "List providers").props.onClick as (() => void) | undefined)?.();
260
261 const projectSelect = visitElements(
262 render(),
263 (element) =>
264 element.props.value === projectId && typeof element.props.onValueChange === "function",
265 );
266 expect(projectSelect?.props.disabled).toBe(true);
267 resolveProviders({ _tag: "Success", value: { providers: [] } });
268 await flushPromises();
269 });
270
271 it("lists, saves, and disables configurable ACP providers", async () => {
272 (findByLabel(render(), "List providers").props.onClick as (() => void) | undefined)?.();
273 await flushPromises();
274
275 expect(commands.listProviders).toHaveBeenCalledWith({
276 environmentId,
277 input: { instanceId, projectId },
278 });
279 const providers = render();
280 expect(findByAriaLabel(providers, "google protocol").props.size).toBe("sm");
281 (findByLabel(providers, "Save").props.onClick as (() => void) | undefined)?.();
282 await flushPromises();
283 expect(commands.setProvider).toHaveBeenCalledWith({
284 environmentId,
285 input: {
286 instanceId,
287 projectId,
288 providerId: "google",
289 apiType: "openai",
290 baseUrl: "https://api.example.test/v1",
291 },
292 });
293
294 (findByLabel(render(), "Disable").props.onClick as (() => void) | undefined)?.();
295 await flushPromises();
296 expect(commands.disableProvider).toHaveBeenCalledWith({
297 environmentId,
298 input: { instanceId, projectId, providerId: "google" },
299 });
300 });
301});
302
303it("imports with task access even when provider configuration is read-only", async () => {
304 grants.scopes.delete("providers:manage");
305 (findByLabel(render(true), "List sessions").props.onClick as () => void)();
306 await flushPromises();
307 (findByLabel(render(true), "Import").props.onClick as () => void)();
308 await flushPromises();
309 expect(commands.import).toHaveBeenCalledOnce();
310});
311
312it("does not import with provider management alone", async () => {
313 grants.scopes.delete("orchestration:operate");
314 (findByLabel(render(), "List sessions").props.onClick as () => void)();
315 await flushPromises();
316 (findByLabel(render(), "Import").props.onClick as () => void)();
317 await flushPromises();
318 expect(commands.import).not.toHaveBeenCalled();
319});
320
321it("rechecks task access after confirming deletion", async () => {
322 (findByLabel(render(), "List sessions").props.onClick as () => void)();
323 await flushPromises();
324 dialogs.confirm.mockImplementationOnce(async () => {
325 grants.scopes.delete("orchestration:operate");
326 return true;
327 });
328 (findByLabel(render(), "Delete").props.onClick as () => void)();
329 await flushPromises();
330 expect(commands.delete).not.toHaveBeenCalled();
331});