1import {
2 AuthProvidersManageScope,
3 EnvironmentId,
4 ProjectId,
5 ProviderInstanceId,
6 type ServerSettings,
7} from "@t3tools/contracts";
8import { describe, expect, it } from "vite-plus/test";
9
10import {
11 buildProviderEnvironmentOptions,
12 buildProviderInstanceOverridePatch,
13 classifyProviderEnvironmentAccess,
14 isProviderSettingsEnvironmentAvailable,
15 resolvePrimaryOperateAccess,
16 resolveRemoteOperateAccess,
17 resolveSelectedProviderEnvironmentId,
18} from "./ProviderSettingsPanel.logic";
19
20const primaryId = EnvironmentId.make("primary");
21const relayId = EnvironmentId.make("relay");
22const sshId = EnvironmentId.make("ssh");
23
24const environments = [
25 { environmentId: sshId, label: "Zulu SSH" },
26 { environmentId: relayId, label: "Alpha Relay" },
27 { environmentId: primaryId, label: "This device" },
28] as const;
29
30describe("provider environment selection", () => {
31 it("requires a connected environment with server config for searchable provider settings", () => {
32 expect(
33 isProviderSettingsEnvironmentAvailable({
34 connectionPhase: "connected",
35 hasServerConfig: true,
36 }),
37 ).toBe(true);
38 expect(
39 isProviderSettingsEnvironmentAvailable({
40 connectionPhase: "reconnecting",
41 hasServerConfig: true,
42 }),
43 ).toBe(false);
44 expect(
45 isProviderSettingsEnvironmentAvailable({
46 connectionPhase: "connected",
47 hasServerConfig: false,
48 }),
49 ).toBe(false);
50 });
51
52 it("sorts the primary environment first and the rest by label", () => {
53 expect(
54 buildProviderEnvironmentOptions(environments, primaryId).map(
55 (environment) => environment.environmentId,
56 ),
57 ).toEqual([primaryId, relayId, sshId]);
58 });
59
60 it("keeps a valid selection, then falls back to primary or the first environment", () => {
61 const options = buildProviderEnvironmentOptions(environments, primaryId);
62
63 expect(resolveSelectedProviderEnvironmentId(options, sshId, primaryId)).toBe(sshId);
64 expect(
65 resolveSelectedProviderEnvironmentId(
66 options.filter((environment) => environment.environmentId !== sshId),
67 sshId,
68 primaryId,
69 ),
70 ).toBe(primaryId);
71 expect(resolveSelectedProviderEnvironmentId(options.slice(1), primaryId, primaryId)).toBe(
72 relayId,
73 );
74 expect(resolveSelectedProviderEnvironmentId([], null, primaryId)).toBeNull();
75 });
76});
77
78describe("provider environment access", () => {
79 it("allows connected environments with config and operate access", () => {
80 expect(
81 classifyProviderEnvironmentAccess({
82 connectionPhase: "connected",
83 hasServerConfig: true,
84 operateAccess: "granted",
85 }),
86 ).toEqual({ kind: "editable" });
87 });
88
89 it("waits for config before exposing controls", () => {
90 expect(
91 classifyProviderEnvironmentAccess({
92 connectionPhase: "connected",
93 hasServerConfig: false,
94 operateAccess: "granted",
95 }),
96 ).toEqual({ kind: "loading", reason: "config" });
97 });
98
99 it("waits for unresolved operate access instead of assuming it is editable", () => {
100 expect(
101 classifyProviderEnvironmentAccess({
102 connectionPhase: "connected",
103 hasServerConfig: true,
104 operateAccess: "pending",
105 }),
106 ).toEqual({ kind: "loading", reason: "permissions" });
107 });
108
109 it("represents known missing operate access as read only", () => {
110 expect(
111 classifyProviderEnvironmentAccess({
112 connectionPhase: "connected",
113 hasServerConfig: true,
114 operateAccess: "denied",
115 }),
116 ).toEqual({ kind: "read-only" });
117 });
118
119 it.each(["available", "offline", "connecting", "reconnecting"] as const)(
120 "keeps %s environments unavailable",
121 (connectionPhase) => {
122 expect(
123 classifyProviderEnvironmentAccess({
124 connectionPhase,
125 hasServerConfig: true,
126 operateAccess: "granted",
127 }),
128 ).toEqual({ kind: "unavailable" });
129 },
130 );
131
132 it("separates connection errors from other unavailable states", () => {
133 expect(
134 classifyProviderEnvironmentAccess({
135 connectionPhase: "error",
136 hasServerConfig: true,
137 operateAccess: "granted",
138 }),
139 ).toEqual({ kind: "error" });
140 });
141});
142
143describe("primary operate access", () => {
144 const authenticated = {
145 authenticated: true as const,
146 scopes: [AuthProvidersManageScope],
147 };
148
149 it("keeps cached session data authoritative while SWR revalidates", () => {
150 expect(
151 resolvePrimaryOperateAccess({
152 isPrimary: true,
153 hasDesktopBridge: false,
154 session: authenticated,
155 isPending: true,
156 hasError: false,
157 }),
158 ).toBe("granted");
159 });
160
161 it("reports pending only before any session has resolved", () => {
162 expect(
163 resolvePrimaryOperateAccess({
164 isPrimary: true,
165 hasDesktopBridge: false,
166 session: null,
167 isPending: true,
168 hasError: false,
169 }),
170 ).toBe("pending");
171 });
172
173 it("denies writes when the session fetch fails", () => {
174 expect(
175 resolvePrimaryOperateAccess({
176 isPrimary: true,
177 hasDesktopBridge: false,
178 session: null,
179 isPending: false,
180 hasError: true,
181 }),
182 ).toBe("denied");
183 });
184
185 it("denies unauthenticated sessions and sessions without the operate scope", () => {
186 expect(
187 resolvePrimaryOperateAccess({
188 isPrimary: true,
189 hasDesktopBridge: false,
190 session: { authenticated: false },
191 isPending: false,
192 hasError: false,
193 }),
194 ).toBe("denied");
195 expect(
196 resolvePrimaryOperateAccess({
197 isPrimary: true,
198 hasDesktopBridge: false,
199 session: { authenticated: true, scopes: ["orchestration:read"] },
200 isPending: false,
201 hasError: false,
202 }),
203 ).toBe("denied");
204 expect(
205 resolvePrimaryOperateAccess({
206 isPrimary: true,
207 hasDesktopBridge: false,
208 session: null,
209 isPending: false,
210 hasError: false,
211 }),
212 ).toBe("denied");
213 });
214
215 it("waits for explicit grants on desktop and remote environments", () => {
216 expect(
217 resolvePrimaryOperateAccess({
218 isPrimary: true,
219 hasDesktopBridge: true,
220 session: null,
221 isPending: true,
222 hasError: false,
223 }),
224 ).toBe("pending");
225 expect(
226 resolvePrimaryOperateAccess({
227 isPrimary: false,
228 hasDesktopBridge: false,
229 session: null,
230 isPending: true,
231 hasError: false,
232 }),
233 ).toBe("pending");
234 });
235});
236
237describe("buildProviderInstanceOverridePatch", () => {
238 const projectId = ProjectId.make("project-a");
239 const instanceId = ProviderInstanceId.make("codex_work");
240 const otherInstanceId = ProviderInstanceId.make("claudeAgent_personal");
241
242 it("writes the same choice to every checkout of the project", () => {
243 const siblingId = ProjectId.make("project-a-worktree");
244 const settings: Pick<ServerSettings, "projectSettingsOverrides"> = {
245 projectSettingsOverrides: {
246 [siblingId]: { newWorktreesStartFromOrigin: true },
247 },
248 };
249
250 expect(
251 buildProviderInstanceOverridePatch(settings, [projectId, siblingId], instanceId, false),
252 ).toEqual({
253 [projectId]: { providerInstances: { [instanceId]: { enabled: false } } },
254 [siblingId]: {
255 newWorktreesStartFromOrigin: true,
256 providerInstances: { [instanceId]: { enabled: false } },
257 },
258 });
259 });
260
261 it("sets an override when the project has none yet", () => {
262 const settings: Pick<ServerSettings, "projectSettingsOverrides"> = {
263 projectSettingsOverrides: {},
264 };
265
266 expect(buildProviderInstanceOverridePatch(settings, [projectId], instanceId, true)).toEqual({
267 [projectId]: { providerInstances: { [instanceId]: { enabled: true } } },
268 });
269 });
270
271 it("adds an instance override alongside the project's other overrides", () => {
272 const settings: Pick<ServerSettings, "projectSettingsOverrides"> = {
273 projectSettingsOverrides: {
274 [projectId]: {
275 newWorktreesStartFromOrigin: true,
276 providerInstances: { [otherInstanceId]: { enabled: false } },
277 },
278 },
279 };
280
281 expect(buildProviderInstanceOverridePatch(settings, [projectId], instanceId, true)).toEqual({
282 [projectId]: {
283 newWorktreesStartFromOrigin: true,
284 providerInstances: {
285 [otherInstanceId]: { enabled: false },
286 [instanceId]: { enabled: true },
287 },
288 },
289 });
290 });
291
292 it("resets the only override by clearing the whole project entry", () => {
293 const settings: Pick<ServerSettings, "projectSettingsOverrides"> = {
294 projectSettingsOverrides: {
295 [projectId]: { providerInstances: { [instanceId]: { enabled: true } } },
296 },
297 };
298
299 expect(
300 buildProviderInstanceOverridePatch(settings, [projectId], instanceId, undefined),
301 ).toEqual({ [projectId]: null });
302 });
303
304 it("resets one instance while another instance's override and other keys survive", () => {
305 const settings: Pick<ServerSettings, "projectSettingsOverrides"> = {
306 projectSettingsOverrides: {
307 [projectId]: {
308 newWorktreesStartFromOrigin: true,
309 providerInstances: {
310 [instanceId]: { enabled: true },
311 [otherInstanceId]: { enabled: false },
312 },
313 },
314 },
315 };
316
317 expect(
318 buildProviderInstanceOverridePatch(settings, [projectId], instanceId, undefined),
319 ).toEqual({
320 [projectId]: {
321 newWorktreesStartFromOrigin: true,
322 providerInstances: { [otherInstanceId]: { enabled: false } },
323 },
324 });
325 });
326
327 it("sends providerInstances: {} explicitly when clearing the last instance override leaves other keys", () => {
328 // The server keeps a stored providerInstances map when an incoming entry
329 // omits the key (so older clients cannot wipe it by editing another
330 // setting). This clear must say so explicitly, or the server would put
331 // the just-cleared override right back.
332 const settings: Pick<ServerSettings, "projectSettingsOverrides"> = {
333 projectSettingsOverrides: {
334 [projectId]: {
335 newWorktreesStartFromOrigin: true,
336 providerInstances: { [instanceId]: { enabled: true } },
337 },
338 },
339 };
340
341 expect(
342 buildProviderInstanceOverridePatch(settings, [projectId], instanceId, undefined),
343 ).toEqual({
344 [projectId]: { newWorktreesStartFromOrigin: true, providerInstances: {} },
345 });
346 });
347});
348
349describe("remote operate access", () => {
350 it("does not treat the old orchestration grant as provider management", () => {
351 expect(
352 resolveRemoteOperateAccess({
353 session: {
354 authenticated: true,
355 scopes: ["orchestration:operate"],
356 auth: { serverUpdateScope: "environment:maintain" },
357 },
358 isPending: false,
359 hasError: false,
360 }),
361 ).toBe("denied");
362 });
363
364 it("accepts the orchestration grant from a server that predates providers:manage", () => {
365 expect(
366 resolveRemoteOperateAccess({
367 session: { authenticated: true, scopes: ["orchestration:operate"], auth: {} },
368 isPending: false,
369 hasError: false,
370 }),
371 ).toBe("granted");
372 });
373 it("derives access from the environment session's granted scopes", () => {
374 expect(
375 resolveRemoteOperateAccess({
376 session: { authenticated: true, scopes: [AuthProvidersManageScope] },
377 isPending: false,
378 hasError: false,
379 }),
380 ).toBe("granted");
381 expect(
382 resolveRemoteOperateAccess({
383 session: { authenticated: true, scopes: ["orchestration:read"] },
384 isPending: false,
385 hasError: false,
386 }),
387 ).toBe("denied");
388 expect(
389 resolveRemoteOperateAccess({
390 session: { authenticated: false },
391 isPending: false,
392 hasError: false,
393 }),
394 ).toBe("denied");
395 });
396
397 it("reports pending before the first session resolve, then keeps cached data", () => {
398 expect(resolveRemoteOperateAccess({ session: null, isPending: true, hasError: false })).toBe(
399 "pending",
400 );
401 expect(
402 resolveRemoteOperateAccess({
403 session: { authenticated: true, scopes: [AuthProvidersManageScope] },
404 isPending: true,
405 hasError: false,
406 }),
407 ).toBe("granted");
408 });
409
410 it("denies writes when the session fetch fails or scopes are missing", () => {
411 expect(resolveRemoteOperateAccess({ session: null, isPending: false, hasError: true })).toBe(
412 "denied",
413 );
414 expect(
415 resolveRemoteOperateAccess({
416 session: { authenticated: true },
417 isPending: false,
418 hasError: false,
419 }),
420 ).toBe("denied");
421 });
422});