1import {
2 DEFAULT_SERVER_SETTINGS,
3 EnvironmentAuthorizationError,
4 EnvironmentId,
5 ProjectId,
6 ProviderInstanceId,
7 type ServerSettings,
8} from "@t3tools/contracts";
9import { describe, expect, it, vi } from "vite-plus/test";
10import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
11import * as Cause from "effect/Cause";
12import { resolveWorktreeCleanup } from "@t3tools/shared/projectSettings";
13
14import type { SidebarProjectSnapshot } from "../../sidebarProjectGrouping";
15import {
16 listProjectOverrides,
17 persistScopedSettingsPatch,
18 planProjectOverridesClear,
19 planScopedSettingsClear,
20 planScopedSettingsPatch,
21 resolveScopedSettingsTargets,
22 scopedSettingsAreMixed,
23 scopedSettingsSource,
24 selectScopedSettingsEnvironments,
25} from "./scopedSettings";
26import { resolveSettingsScope } from "./settingsScope";
27
28function environment(
29 id: string,
30 options: {
31 connected?: boolean;
32 loaded?: boolean;
33 settings?: Partial<ServerSettings>;
34 projectOverrides?: boolean;
35 } = {},
36) {
37 return {
38 environmentId: EnvironmentId.make(id),
39 label: id,
40 connection: {
41 phase: options.connected === false ? ("offline" as const) : ("connected" as const),
42 },
43 serverConfig:
44 options.loaded === false
45 ? null
46 : {
47 settings: { ...DEFAULT_SERVER_SETTINGS, ...options.settings },
48 environment: {
49 capabilities: { projectSettingsOverrides: options.projectOverrides !== false },
50 },
51 },
52 };
53}
54
55const laptop = environment("Laptop");
56const server = environment("Server");
57const offline = environment("Offline", { connected: false });
58const loading = environment("Loading", { loaded: false });
59const environments = [laptop, server, offline, loading];
60const all = resolveSettingsScope({}, [], environments);
61const named = resolveSettingsScope({ machine: server.environmentId }, [], environments);
62
63const projectId = ProjectId.make("project");
64const laptopProjectId = ProjectId.make("laptop-project");
65const member = {
66 id: projectId,
67 environmentId: server.environmentId,
68 title: "Project",
69 workspaceRoot: "/repo",
70 physicalProjectKey: `${server.environmentId}:/repo`,
71 environmentLabel: server.label,
72 defaultModelSelection: null,
73 scripts: [],
74 createdAt: "2026-09-07T00:00:00.000Z",
75 updatedAt: "2026-09-07T00:00:00.000Z",
76};
77const laptopMember = {
78 ...member,
79 id: laptopProjectId,
80 environmentId: laptop.environmentId,
81 physicalProjectKey: `${laptop.environmentId}:/repo`,
82 environmentLabel: laptop.label,
83};
84const group: SidebarProjectSnapshot = {
85 ...member,
86 projectKey: "project-group",
87 displayName: "Project",
88 memberProjects: [member, laptopMember],
89 memberProjectRefs: [
90 { environmentId: server.environmentId, projectId },
91 { environmentId: laptop.environmentId, projectId: laptopProjectId },
92 ],
93 groupedProjectCount: 2,
94 environmentPresence: "remote-only",
95 allRemoteMembersAreDesktopLocal: false,
96 allRemoteMembersAreWsl: false,
97 remoteEnvironmentLabels: [server.label, laptop.label],
98};
99const project = resolveSettingsScope({ project: group.projectKey }, [group], environments);
100const checkout = resolveSettingsScope(
101 { project: group.projectKey, machine: server.environmentId, checkout: member.physicalProjectKey },
102 [group],
103 environments,
104);
105
106describe("scoped settings targets", () => {
107 it("uses the named environment even when a different primary is available", () => {
108 const selected = selectScopedSettingsEnvironments(named, environments, laptop.environmentId);
109 expect(selected.environments).toEqual([server]);
110 expect(selected.environment).toBe(server);
111 });
112
113 it("keeps an offline named environment selected without falling back to primary", () => {
114 const scope = resolveSettingsScope({ machine: offline.environmentId }, [], environments);
115 const selected = selectScopedSettingsEnvironments(scope, environments, laptop.environmentId);
116 expect(selected.environments).toEqual([offline]);
117 expect(selected.connectedEnvironments).toEqual([]);
118 expect(selected.environment).toBeNull();
119 expect(
120 planScopedSettingsPatch(scope, environments, { enableProviderUpdateChecks: false }),
121 ).toMatchObject({
122 serverWrites: [],
123 unavailableReason: "Connect Offline to save this setting.",
124 });
125 });
126
127 it("prefers the selected primary as the aggregate representative without including disconnected targets", () => {
128 const selected = selectScopedSettingsEnvironments(all, environments, server.environmentId);
129 expect(selected.environment).toBe(server);
130 expect(selected.environments).toEqual(environments);
131 expect(selected.connectedEnvironments).toEqual([laptop, server]);
132 });
133
134 it("resolves each member's effective settings and source at project scope", () => {
135 const overridden = environment("Server", {
136 settings: {
137 defaultAutoPull: false,
138 projectSettingsOverrides: { [projectId]: { defaultAutoPull: true } },
139 },
140 });
141 const targets = resolveScopedSettingsTargets(project, [laptop, overridden]);
142 expect(targets.map((target) => [target.projectId, target.settings.defaultAutoPull])).toEqual([
143 [projectId, true],
144 [laptopProjectId, false],
145 ]);
146 expect(scopedSettingsSource(targets, ["defaultAutoPull"])).toBe("mixed");
147 expect(scopedSettingsSource([targets[0]!], ["defaultAutoPull"])).toBe("project");
148 expect(scopedSettingsSource(targets, ["enableProviderUpdateChecks"])).toBe("environment");
149 expect(scopedSettingsAreMixed(targets, ["defaultAutoPull"])).toBe(true);
150 });
151});
152
153describe("scoped settings writes", () => {
154 it("edits the effective machine policy without changing other machines' rules", () => {
155 const custom = environment("Laptop", {
156 settings: {
157 worktreeCleanup: {
158 mode: "custom",
159 rules: {
160 worktreeAfterDays: 12,
161 worktreeSettledAfterDays: null,
162 worktreeOnDelete: true,
163 worktreeOnMerge: true,
164 worktreeUnchanged: false,
165 worktreeKeepWhen: "uncommitted-changes",
166 },
167 },
168 },
169 });
170 const disabled = environment("Server", {
171 settings: { worktreeCleanup: { mode: "off" } },
172 });
173 const targets = [custom, disabled];
174 const plan = planScopedSettingsPatch(all, targets, {
175 storageCleanup: { worktreeOnDelete: false },
176 });
177 const policies = plan.serverWrites.map((write, index) =>
178 resolveWorktreeCleanup(
179 applyServerSettingsPatch(targets[index]!.serverConfig!.settings, write.patch),
180 null,
181 ),
182 );
183 expect(policies).toEqual([
184 {
185 worktreeAfterDays: 12,
186 worktreeSettledAfterDays: null,
187 worktreeOnDelete: false,
188 worktreeOnMerge: true,
189 worktreeUnchanged: false,
190 worktreeKeepWhen: "uncommitted-changes",
191 },
192 {
193 worktreeAfterDays: null,
194 worktreeSettledAfterDays: null,
195 worktreeOnDelete: false,
196 worktreeOnMerge: false,
197 worktreeUnchanged: false,
198 worktreeKeepWhen: "uncommitted-changes",
199 },
200 ]);
201 });
202 it("isolates a formerly shared server preference to the named environment", async () => {
203 const persistServer = vi.fn().mockResolvedValue({ _tag: "Success" });
204 const persistClient = vi.fn();
205 await persistScopedSettingsPatch(
206 planScopedSettingsPatch(named, environments, { sidebarAutoSettleOnMerge: false }),
207 persistServer,
208 persistClient,
209 );
210 expect(persistServer.mock.calls).toEqual([
211 [
212 {
213 environmentId: server.environmentId,
214 input: { patch: { sidebarAutoSettleOnMerge: false } },
215 },
216 ],
217 ]);
218 expect(persistClient).not.toHaveBeenCalled();
219 });
220
221 it("writes an aggregate preference only to connected environments with loaded configuration", async () => {
222 const persistServer = vi.fn().mockResolvedValue({ _tag: "Success" });
223 const persistClient = vi.fn();
224 await persistScopedSettingsPatch(
225 planScopedSettingsPatch(all, environments, { enableProviderUpdateChecks: false }),
226 persistServer,
227 persistClient,
228 );
229 expect(persistServer.mock.calls.map(([input]) => input.environmentId)).toEqual([
230 laptop.environmentId,
231 server.environmentId,
232 ]);
233 expect(persistClient).not.toHaveBeenCalled();
234 });
235
236 describe("an environment that saves but drops keys it does not know", () => {
237 // Laptop knows every key; Server is an older build that keeps its settings as they were.
238 const upgradedLaptop = (saved: Partial<ServerSettings>) =>
239 vi.fn(async ({ environmentId }: { environmentId: EnvironmentId }) => ({
240 _tag: "Success" as const,
241 value: {
242 ...DEFAULT_SERVER_SETTINGS,
243 ...(environmentId === laptop.environmentId ? saved : {}),
244 },
245 }));
246 const outcome = async (
247 plan: ReturnType<typeof planScopedSettingsPatch>,
248 persistServer: ReturnType<typeof upgradedLaptop>,
249 ) => {
250 const result = await persistScopedSettingsPatch(plan, persistServer, vi.fn());
251 return {
252 saved: result.savedEnvironments.map(({ label }) => label),
253 failed: result.failedEnvironments.map(({ label, message }) => ({ label, message })),
254 };
255 };
256 const unsupported = [
257 {
258 label: server.label,
259 message: "This environment does not support this setting. Update T3 Code on it.",
260 },
261 ];
262
263 it("reports a dropped toggle", async () => {
264 expect(
265 await outcome(
266 planScopedSettingsPatch(all, environments, { reviewGuidesEnabled: true }),
267 upgradedLaptop({ reviewGuidesEnabled: true }),
268 ),
269 ).toEqual({ saved: [laptop.label], failed: unsupported });
270 });
271
272 it("reports dropped text but accepts the server trimming it", async () => {
273 expect(
274 await outcome(
275 planScopedSettingsPatch(all, environments, { reviewGuideInstructions: " Read tests " }),
276 upgradedLaptop({ reviewGuideInstructions: "Read tests" }),
277 ),
278 ).toEqual({ saved: [laptop.label], failed: unsupported });
279 });
280
281 it("reports a dropped key inside a project override", async () => {
282 expect(
283 await outcome(
284 planScopedSettingsPatch(project, environments, { defaultAutoPull: true }),
285 upgradedLaptop({
286 projectSettingsOverrides: { [laptopProjectId]: { defaultAutoPull: true } },
287 }),
288 ),
289 ).toEqual({ saved: [laptop.label], failed: unsupported });
290 });
291
292 it("reports a nested key dropped beside one that saved", async () => {
293 const patch = { defaultAutoPull: true, enableAgentBrowserAccess: false };
294 const persistServer = vi.fn(async ({ environmentId }: { environmentId: EnvironmentId }) => ({
295 _tag: "Success" as const,
296 value: {
297 ...DEFAULT_SERVER_SETTINGS,
298 projectSettingsOverrides:
299 environmentId === laptop.environmentId
300 ? { [laptopProjectId]: patch }
301 : { [projectId]: { defaultAutoPull: true } },
302 },
303 }));
304 expect(
305 await outcome(planScopedSettingsPatch(project, environments, patch), persistServer),
306 ).toEqual({ saved: [laptop.label], failed: unsupported });
307 });
308
309 it("accepts a write that matches what the environment already had", async () => {
310 expect(
311 await outcome(
312 planScopedSettingsPatch(all, environments, { reviewGuidesEnabled: false }),
313 upgradedLaptop({}),
314 ),
315 ).toEqual({ saved: [laptop.label, server.label], failed: [] });
316 });
317 });
318
319 it("persists client keys locally at any scope alongside server keys", async () => {
320 const persistServer = vi.fn().mockResolvedValue({ _tag: "Success" });
321 const persistClient = vi.fn();
322 await persistScopedSettingsPatch(
323 planScopedSettingsPatch(named, environments, {
324 diffIgnoreWhitespace: false,
325 enableProviderUpdateChecks: false,
326 }),
327 persistServer,
328 persistClient,
329 );
330 expect(persistClient).toHaveBeenCalledExactlyOnceWith({ diffIgnoreWhitespace: false });
331 expect(persistServer).toHaveBeenCalledExactlyOnceWith({
332 environmentId: server.environmentId,
333 input: { patch: { enableProviderUpdateChecks: false } },
334 });
335 });
336
337 it("writes project overrides into each member's entry on its environment", () => {
338 const withExisting = environment("Server", {
339 settings: {
340 projectSettingsOverrides: { [projectId]: { enableAgentBrowserAccess: false } },
341 },
342 });
343 const plan = planScopedSettingsPatch(project, [laptop, withExisting], {
344 defaultAutoPull: true,
345 });
346 expect(plan.unavailableReason).toBeNull();
347 expect(plan.serverWrites).toEqual([
348 {
349 environmentId: server.environmentId,
350 label: server.label,
351 previous: expect.anything(),
352 patch: {
353 projectSettingsOverrides: {
354 [projectId]: { enableAgentBrowserAccess: false, defaultAutoPull: true },
355 },
356 },
357 },
358 {
359 environmentId: laptop.environmentId,
360 label: laptop.label,
361 previous: expect.anything(),
362 patch: { projectSettingsOverrides: { [laptopProjectId]: { defaultAutoPull: true } } },
363 },
364 ]);
365 expect(
366 planScopedSettingsPatch(checkout, [laptop, server], { defaultAutoPull: true }),
367 ).toMatchObject({
368 serverWrites: [{ environmentId: server.environmentId }],
369 });
370 });
371
372 it("keeps each project's other cleanup rules when changing one rule across machines", () => {
373 const machine = environment("Laptop", {
374 settings: {
375 storageCleanup: { ...DEFAULT_SERVER_SETTINGS.storageCleanup, worktreeAfterDays: 30 },
376 },
377 });
378 const customized = environment("Server", {
379 settings: {
380 projectSettingsOverrides: {
381 [projectId]: {
382 defaultAutoPull: true,
383 worktreeCleanup: {
384 mode: "custom",
385 rules: {
386 worktreeAfterDays: 8,
387 worktreeSettledAfterDays: null,
388 worktreeOnDelete: false,
389 worktreeOnMerge: true,
390 worktreeUnchanged: false,
391 worktreeKeepWhen: "uncommitted-changes",
392 },
393 },
394 },
395 },
396 },
397 });
398 const plan = planScopedSettingsPatch(project, [machine, customized], {
399 worktreeCleanup: { mode: "custom", rules: { worktreeOnDelete: true } },
400 });
401 const settledPlan = planScopedSettingsPatch(project, [machine, customized], {
402 worktreeCleanup: { mode: "custom", rules: { worktreeSettledAfterDays: 0 } },
403 });
404 expect(
405 settledPlan.serverWrites.map((write) => write.patch.projectSettingsOverrides),
406 ).toMatchObject([
407 { [projectId]: { worktreeCleanup: { rules: { worktreeSettledAfterDays: 0 } } } },
408 { [laptopProjectId]: { worktreeCleanup: { rules: { worktreeSettledAfterDays: 0 } } } },
409 ]);
410 expect(plan.serverWrites.map((write) => write.patch.projectSettingsOverrides)).toEqual([
411 {
412 [projectId]: {
413 defaultAutoPull: true,
414 worktreeCleanup: {
415 mode: "custom",
416 rules: {
417 worktreeAfterDays: 8,
418 worktreeSettledAfterDays: null,
419 worktreeOnDelete: true,
420 worktreeOnMerge: true,
421 worktreeUnchanged: false,
422 worktreeKeepWhen: "uncommitted-changes",
423 },
424 },
425 },
426 },
427 {
428 [laptopProjectId]: {
429 worktreeCleanup: {
430 mode: "custom",
431 rules: {
432 worktreeAfterDays: 30,
433 worktreeSettledAfterDays: null,
434 worktreeOnDelete: true,
435 worktreeOnMerge: false,
436 worktreeUnchanged: false,
437 worktreeKeepWhen: "uncommitted-changes",
438 },
439 },
440 },
441 },
442 ]);
443 expect(
444 planScopedSettingsClear(checkout, [customized], ["worktreeCleanup"]).serverWrites[0]?.patch,
445 ).toEqual({
446 projectSettingsOverrides: { [projectId]: { defaultAutoPull: true } },
447 });
448 expect(
449 planScopedSettingsPatch(project, [machine, customized], {
450 storageCleanup: { browserArtifactsAfterDays: 8 },
451 }).serverWrites,
452 ).toEqual([]);
453 });
454
455 it("scopes agent device access to projects while keeping hub and hosts environment-wide", () => {
456 const plan = planScopedSettingsPatch(project, [laptop, server], {
457 enableAgentDeviceAccess: true,
458 });
459 expect(plan.serverWrites.map((write) => write.patch)).toEqual([
460 { projectSettingsOverrides: { [projectId]: { enableAgentDeviceAccess: true } } },
461 { projectSettingsOverrides: { [laptopProjectId]: { enableAgentDeviceAccess: true } } },
462 ]);
463 for (const patch of [{ enableDeviceSupport: true }, { deviceHosts: [] }]) {
464 expect(planScopedSettingsPatch(project, environments, patch).serverWrites).toEqual([]);
465 }
466 });
467
468 it("refuses environment-wide keys and older servers at project scope", () => {
469 expect(
470 planScopedSettingsPatch(project, environments, { enableProviderUpdateChecks: false }),
471 ).toMatchObject({
472 serverWrites: [],
473 unavailableReason: "This setting is environment-wide and cannot be overridden by a project.",
474 });
475 const legacy = environment("Server", { projectOverrides: false });
476 expect(
477 planScopedSettingsPatch(checkout, [laptop, legacy], { defaultAutoPull: true }),
478 ).toMatchObject({ serverWrites: [], unavailableReason: expect.stringContaining("update") });
479 });
480
481 it("clears overrides per member and removes an emptied entry", () => {
482 const withOverrides = environment("Server", {
483 settings: {
484 projectSettingsOverrides: {
485 [projectId]: { defaultAutoPull: true, enableAgentBrowserAccess: false },
486 },
487 },
488 });
489 const plan = planScopedSettingsClear(checkout, [laptop, withOverrides], ["defaultAutoPull"]);
490 expect(plan.serverWrites).toEqual([
491 {
492 environmentId: server.environmentId,
493 label: server.label,
494 previous: expect.anything(),
495 patch: { projectSettingsOverrides: { [projectId]: { enableAgentBrowserAccess: false } } },
496 },
497 ]);
498 expect(
499 planScopedSettingsClear(
500 checkout,
501 [laptop, withOverrides],
502 ["defaultAutoPull", "enableAgentBrowserAccess"],
503 ).serverWrites[0]?.patch,
504 ).toEqual({ projectSettingsOverrides: { [projectId]: null } });
505 });
506
507 it("never substitutes an environment-default write for an invalid scope", () => {
508 const scope = resolveSettingsScope({ machine: "removed" }, [group], environments);
509 const plan = planScopedSettingsPatch(scope, environments, { enableAgentBrowserAccess: false });
510 expect(plan.serverWrites).toEqual([]);
511 expect(plan.hasClientWrite).toBe(false);
512 expect(plan.unavailableReason).not.toBeNull();
513 });
514
515 it("waits for every target and identifies both RPC failures and rejected writes", async () => {
516 const third = environment("Third");
517 const fourth = environment("Fourth");
518 const selected = [...environments, third, fourth];
519 const scope = resolveSettingsScope({}, [], selected);
520 const persistServer = vi
521 .fn()
522 .mockResolvedValueOnce({ _tag: "Success" })
523 .mockResolvedValueOnce({
524 _tag: "Failure",
525 cause: Cause.fail(
526 new EnvironmentAuthorizationError({
527 requiredScope: "settings:write",
528 message: "This connection lacks permission to change settings.",
529 }),
530 ),
531 })
532 .mockRejectedValueOnce(new Error("Disconnected during save"))
533 .mockResolvedValueOnce({ _tag: "Success" });
534 const result = await persistScopedSettingsPatch(
535 planScopedSettingsPatch(scope, selected, { enableAgentBrowserAccess: false }),
536 persistServer,
537 vi.fn(),
538 );
539 expect(result.savedEnvironmentCount).toBe(2);
540 expect(result.savedEnvironments.map(({ label }) => label)).toEqual([
541 laptop.label,
542 fourth.label,
543 ]);
544 expect(result.failedEnvironments.map(({ message }) => message)).toEqual([
545 "This connection lacks permission to change settings.",
546 "Disconnected during save",
547 ]);
548 expect(result.failedEnvironments.map(({ label }) => label)).toEqual([
549 server.label,
550 third.label,
551 ]);
552 expect(persistServer).toHaveBeenCalledTimes(4);
553 });
554});
555
556describe("scoped settings mixed values", () => {
557 it("compares only requested settings across connected targets", () => {
558 const changed = environment("Changed", { settings: { enableAgentBrowserAccess: false } });
559 const targets = resolveScopedSettingsTargets(all, [laptop, changed]);
560 expect(scopedSettingsAreMixed(targets, ["enableAgentBrowserAccess"])).toBe(true);
561 expect(scopedSettingsAreMixed(targets, ["enableProviderUpdateChecks"])).toBe(false);
562 expect(scopedSettingsAreMixed([], ["enableAgentBrowserAccess"])).toBe(false);
563 });
564
565 it("treats independently decoded equal nested settings as the same value", () => {
566 const style = {
567 ...DEFAULT_SERVER_SETTINGS.sourceControlWritingStyle,
568 mode: "custom" as const,
569 customInstructions: "Use plain language",
570 };
571 const first = environment("First", { settings: { sourceControlWritingStyle: { ...style } } });
572 const second = environment("Second", { settings: { sourceControlWritingStyle: { ...style } } });
573 const targets = resolveScopedSettingsTargets(all, [first, second]);
574 expect(scopedSettingsAreMixed(targets, ["sourceControlWritingStyle"])).toBe(false);
575 });
576});
577
578describe("project overrides at environment scope", () => {
579 const laptop = EnvironmentId.make("laptop");
580 const desk = EnvironmentId.make("desk");
581 const fleet = ProjectId.make("fleet");
582 const t3 = ProjectId.make("t3");
583 const environment = (
584 environmentId: EnvironmentId,
585 overrides: ServerSettings["projectSettingsOverrides"],
586 ) => ({
587 environmentId,
588 label: environmentId,
589 connection: { phase: "connected" as const },
590 serverConfig: {
591 settings: { ...DEFAULT_SERVER_SETTINGS, projectSettingsOverrides: overrides },
592 environment: { capabilities: { projectSettingsOverrides: true } },
593 },
594 });
595
596 it("lists only the projects that override the keys", () => {
597 const entries = listProjectOverrides(
598 [
599 environment(laptop, {
600 [fleet]: { defaultAutoPull: true, defaultThreadEnvMode: "local" },
601 [t3]: { defaultThreadEnvMode: "local" },
602 }),
603 environment(desk, { [fleet]: { defaultAutoPull: false } }),
604 ],
605 ["defaultAutoPull"],
606 );
607 expect(entries).toEqual([
608 { environmentId: laptop, projectId: fleet },
609 { environmentId: desk, projectId: fleet },
610 ]);
611 });
612
613 it("clears only those keys and drops entries that become empty", () => {
614 const plan = planProjectOverridesClear(
615 [
616 environment(laptop, {
617 [fleet]: { defaultAutoPull: true, defaultThreadEnvMode: "local" },
618 [t3]: { defaultAutoPull: true },
619 }),
620 ],
621 [
622 { environmentId: laptop, projectId: fleet },
623 { environmentId: laptop, projectId: t3 },
624 ],
625 ["defaultAutoPull"],
626 );
627 expect(plan.serverWrites).toEqual([
628 {
629 environmentId: laptop,
630 label: laptop,
631 previous: expect.anything(),
632 patch: {
633 projectSettingsOverrides: { [fleet]: { defaultThreadEnvMode: "local" }, [t3]: null },
634 },
635 },
636 ]);
637 });
638});
639
640describe("null patches at project scope", () => {
641 it("removes the override for keys that cannot store null and keeps it for keys that can", () => {
642 const environmentId = EnvironmentId.make("laptop");
643 const projectId = ProjectId.make("fleet");
644 const scope = {
645 kind: "project" as const,
646 group: {} as never,
647 environmentId: null,
648 label: "fleet",
649 members: [{ id: projectId, environmentId } as never],
650 environmentIds: [environmentId],
651 };
652 const environments = [
653 {
654 environmentId,
655 label: "Laptop",
656 connection: { phase: "connected" as const },
657 serverConfig: {
658 settings: {
659 ...DEFAULT_SERVER_SETTINGS,
660 projectSettingsOverrides: {
661 [projectId]: { defaultThreadEnvMode: "worktree" as const, defaultAutoPull: true },
662 },
663 },
664 environment: { capabilities: { projectSettingsOverrides: true } },
665 },
666 },
667 ];
668 expect(
669 planScopedSettingsPatch(scope, environments, { defaultThreadEnvMode: null }).serverWrites[0]
670 ?.patch,
671 ).toEqual({ projectSettingsOverrides: { [projectId]: { defaultAutoPull: true } } });
672 expect(
673 planScopedSettingsPatch(scope, environments, { defaultModelSelection: null }).serverWrites[0]
674 ?.patch,
675 ).toEqual({
676 projectSettingsOverrides: {
677 [projectId]: {
678 defaultThreadEnvMode: "worktree",
679 defaultAutoPull: true,
680 defaultModelSelection: null,
681 },
682 },
683 });
684 });
685});
686
687describe("partial object patches at project scope", () => {
688 it("completes a writing style field patch from the target's effective value", () => {
689 const environmentId = EnvironmentId.make("laptop");
690 const projectId = ProjectId.make("fleet");
691 const member = {
692 id: projectId,
693 environmentId,
694 physicalProjectKey: "laptop:/repo",
695 environmentLabel: "Laptop",
696 title: "fleet",
697 workspaceRoot: "/repo",
698 defaultModelSelection: null,
699 scripts: [],
700 createdAt: "2026-01-01T00:00:00.000Z",
701 updatedAt: "2026-01-01T00:00:00.000Z",
702 };
703 const settings = {
704 ...DEFAULT_SERVER_SETTINGS,
705 projectSettingsOverrides: {
706 [projectId]: {
707 sourceControlWritingStyle: {
708 mode: "custom" as const,
709 customInstructions: "Keep it short.",
710 followChangeRequestTemplates: false,
711 },
712 },
713 },
714 };
715 const plan = planScopedSettingsPatch(
716 {
717 kind: "project",
718 group: {} as never,
719 environmentId: null,
720 label: "fleet",
721 members: [member as never],
722 environmentIds: [environmentId],
723 },
724 [
725 {
726 environmentId,
727 label: "Laptop",
728 connection: { phase: "connected" },
729 serverConfig: {
730 settings,
731 environment: { capabilities: { projectSettingsOverrides: true } },
732 },
733 },
734 ],
735 { sourceControlWritingStyle: { customInstructions: "Be terse." } },
736 );
737 expect(plan.serverWrites[0]?.patch).toEqual({
738 projectSettingsOverrides: {
739 [projectId]: {
740 sourceControlWritingStyle: {
741 mode: "custom",
742 customInstructions: "Be terse.",
743 followChangeRequestTemplates: false,
744 },
745 },
746 },
747 });
748 });
749});
750
751it("routes legacy model preference keys exclusively to the selected environment", () => {
752 const patch = {
753 favorites: [{ provider: ProviderInstanceId.make("codex"), model: "sol" }],
754 providerModelPreferences: {},
755 };
756 const plan = planScopedSettingsPatch(named, environments, patch);
757 expect(plan.clientPatch).toEqual({});
758 expect(plan.hasClientWrite).toBe(false);
759 expect(plan.serverWrites).toEqual([
760 {
761 environmentId: server.environmentId,
762 label: server.label,
763 previous: expect.anything(),
764 patch,
765 },
766 ]);
767});