1import { EnvironmentId, ProjectId } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3
4import type {
5 SidebarProjectGroupMember,
6 SidebarProjectSnapshot,
7} from "../../sidebarProjectGrouping";
8import { resolveSettingsScope, validateSettingsScopeSearch } from "./settingsScope";
9
10const laptopId = EnvironmentId.make("laptop");
11const serverId = EnvironmentId.make("server");
12const environments = [
13 { environmentId: laptopId, label: "Laptop" },
14 { environmentId: serverId, label: "Server" },
15];
16
17function member(id: string, environmentId: EnvironmentId): SidebarProjectGroupMember {
18 return {
19 id: ProjectId.make(id),
20 environmentId,
21 title: "T3 Code",
22 workspaceRoot: `/repos/${id}`,
23 physicalProjectKey: `${environmentId}:/repos/${id}`,
24 environmentLabel:
25 environments.find((environment) => environment.environmentId === environmentId)?.label ??
26 null,
27 defaultModelSelection: null,
28 scripts: [],
29 createdAt: "2026-09-07T00:00:00.000Z",
30 updatedAt: "2026-09-07T00:00:00.000Z",
31 };
32}
33
34const first = member("first", laptopId);
35const second = member("second", laptopId);
36const third = member("third", serverId);
37const other = member("other", serverId);
38
39function group(
40 projectKey: string,
41 members: readonly SidebarProjectGroupMember[],
42): SidebarProjectSnapshot {
43 return {
44 ...members[0]!,
45 projectKey,
46 displayName: projectKey,
47 memberProjects: members,
48 memberProjectRefs: members.map((project) => ({
49 environmentId: project.environmentId,
50 projectId: project.id,
51 })),
52 groupedProjectCount: members.length,
53 environmentPresence: "mixed",
54 allRemoteMembersAreDesktopLocal: false,
55 allRemoteMembersAreWsl: false,
56 remoteEnvironmentLabels: [],
57 };
58}
59
60const groups = [group("t3code", [first, second, third]), group("other", [other])];
61
62describe("settings scope search", () => {
63 it("ignores the retired scope key from older links", () => {
64 expect(validateSettingsScopeSearch({ scope: "device", project: "t3code" })).toEqual({
65 project: "t3code",
66 });
67 expect(validateSettingsScopeSearch({ scope: "all" })).toEqual({});
68 });
69
70 it("retains legacy project and machine links without inventing an explicit broad scope", () => {
71 expect(
72 validateSettingsScopeSearch({ project: "t3code", machine: laptopId, unused: true }),
73 ).toEqual({
74 project: "t3code",
75 machine: laptopId,
76 });
77 });
78
79 it("retains an orphan checkout so it cannot turn into all environments", () => {
80 const search = validateSettingsScopeSearch({ checkout: first.physicalProjectKey });
81 expect(search).toEqual({ checkout: first.physicalProjectKey });
82 expect(resolveSettingsScope(search, groups, environments)).toMatchObject({
83 kind: "unavailable",
84 reason: "project-required",
85 members: [],
86 environmentIds: [],
87 });
88 });
89});
90
91describe("settings scope resolution", () => {
92 it("defaults to every environment with no project", () => {
93 expect(resolveSettingsScope({}, groups, environments)).toMatchObject({
94 kind: "all",
95 members: [],
96 environmentIds: [laptopId, serverId],
97 });
98 });
99
100 it("resolves one environment without targeting its project overrides", () => {
101 expect(resolveSettingsScope({ machine: serverId }, groups, environments)).toMatchObject({
102 kind: "environment",
103 environmentId: serverId,
104 environmentIds: [serverId],
105 members: [],
106 });
107 });
108
109 it("keeps all physical members in a project aggregate, including several on one environment", () => {
110 expect(resolveSettingsScope({ project: "t3code" }, groups, environments)).toMatchObject({
111 kind: "project",
112 environmentId: null,
113 members: [first, second, third],
114 environmentIds: [laptopId, serverId],
115 });
116 });
117
118 it("preserves legacy project plus machine aggregates with multiple checkouts", () => {
119 expect(
120 resolveSettingsScope({ project: "t3code", machine: laptopId }, groups, environments),
121 ).toMatchObject({
122 kind: "project",
123 environmentId: laptopId,
124 label: "t3code / Laptop",
125 members: [first, second],
126 environmentIds: [laptopId],
127 });
128 });
129
130 it("narrows a checkout target to exactly one member, deriving its environment when omitted", () => {
131 expect(
132 resolveSettingsScope(
133 { project: "t3code", checkout: second.physicalProjectKey },
134 groups,
135 environments,
136 ),
137 ).toMatchObject({
138 kind: "checkout",
139 checkout: second,
140 environmentId: laptopId,
141 label: "t3code / Laptop · /repos/second",
142 members: [second],
143 environmentIds: [laptopId],
144 });
145 });
146
147 it.each([
148 { project: "missing" },
149 { machine: "removed" },
150 { project: "t3code", machine: "removed" },
151 { project: "t3code", checkout: "deleted" },
152 { project: "other", machine: laptopId },
153 { project: "other", checkout: first.physicalProjectKey },
154 { project: "t3code", machine: serverId, checkout: first.physicalProjectKey },
155 ])("never widens an invalid or stale target: %j", (search) => {
156 expect(resolveSettingsScope(search, groups, environments)).toMatchObject({
157 kind: "unavailable",
158 members: [],
159 environmentIds: [],
160 });
161 });
162
163 it("leaves a removed checkout unavailable while sibling checkouts remain", () => {
164 const search = {
165 project: "t3code",
166 machine: laptopId,
167 checkout: first.physicalProjectKey,
168 };
169 expect(resolveSettingsScope(search, groups, environments)).toMatchObject({
170 kind: "checkout",
171 members: [first],
172 });
173 expect(
174 resolveSettingsScope(search, [group("t3code", [second, third])], environments),
175 ).toMatchObject({ kind: "unavailable", members: [], environmentIds: [] });
176 });
177
178 it("does not select another environment after removing a project's last local checkout", () => {
179 const search = { project: "t3code", machine: laptopId };
180 expect(resolveSettingsScope(search, groups, environments)).toMatchObject({
181 kind: "project",
182 members: [first, second],
183 });
184 expect(resolveSettingsScope(search, [group("t3code", [third])], environments)).toMatchObject({
185 kind: "unavailable",
186 members: [],
187 environmentIds: [],
188 });
189 });
190
191 it("rejects a cached checkout whose environment was removed", () => {
192 expect(
193 resolveSettingsScope(
194 { project: "t3code", checkout: third.physicalProjectKey },
195 groups,
196 environments.slice(0, 1),
197 ),
198 ).toMatchObject({
199 kind: "unavailable",
200 reason: "environment-missing",
201 members: [],
202 environmentIds: [],
203 });
204 });
205});