1import type { EnvironmentId, ProjectId } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3
4import {
5 assignProjectsToEnvironments,
6 resolvePickableEnvironments,
7 type AssignableProject,
8} from "./pullRequestProjectAssignment.logic";
9
10const project = (
11 id: string,
12 environmentId: string,
13 canonicalKey?: string,
14): AssignableProject & { workspaceRoot: string } => ({
15 id: id as ProjectId,
16 environmentId: environmentId as EnvironmentId,
17 repositoryIdentity: canonicalKey === undefined ? null : { canonicalKey },
18 workspaceRoot: `/srv/${environmentId}/${id}`,
19});
20
21const envs = (...ids: ReadonlyArray<string>) => ids as ReadonlyArray<EnvironmentId>;
22
23const plain = (assignment: Map<EnvironmentId, ProjectId[]>) =>
24 Object.fromEntries([...assignment].map(([id, projectIds]) => [id, projectIds]));
25
26describe("one server per repository", () => {
27 it("lets the first server list a repository both hold", () => {
28 const assignment = assignProjectsToEnvironments(
29 [
30 project("a1", "env-1", "github.com/acme/app"),
31 project("b1", "env-1", "github.com/acme/tools"),
32 project("a2", "env-2", "github.com/acme/app"),
33 project("c2", "env-2", "github.com/acme/site"),
34 ],
35 envs("env-1", "env-2"),
36 "env-1" as EnvironmentId,
37 );
38 expect(plain(assignment)).toEqual({ "env-1": ["a1", "b1"], "env-2": ["c2"] });
39 });
40
41 it("prefers the named server over the first one", () => {
42 const assignment = assignProjectsToEnvironments(
43 [
44 project("a1", "env-1", "github.com/acme/app"),
45 project("a2", "env-2", "github.com/acme/app"),
46 ],
47 envs("env-1", "env-2"),
48 "env-2" as EnvironmentId,
49 );
50 expect(plain(assignment)).toEqual({ "env-2": ["a2"] });
51 });
52
53 it("drops a server with nothing of its own", () => {
54 const assignment = assignProjectsToEnvironments(
55 [
56 project("a1", "env-1", "github.com/acme/app"),
57 project("a2", "env-2", "github.com/acme/app"),
58 ],
59 envs("env-1", "env-2"),
60 "env-1" as EnvironmentId,
61 );
62 expect(assignment.has("env-2" as EnvironmentId)).toBe(false);
63 });
64
65 it("keeps every copy of a project that has no identity to compare", () => {
66 const assignment = assignProjectsToEnvironments(
67 [project("p1", "env-1"), project("p2", "env-2")],
68 envs("env-1", "env-2"),
69 "env-1" as EnvironmentId,
70 );
71 expect(plain(assignment)).toEqual({ "env-1": ["p1"], "env-2": ["p2"] });
72 });
73
74 it("keeps a repository listed by the one server that holds it", () => {
75 const assignment = assignProjectsToEnvironments(
76 [
77 project("a1", "env-1", "github.com/acme/app"),
78 project("b2", "env-2", "gitlab.com/acme/app"),
79 ],
80 envs("env-1", "env-2"),
81 "env-1" as EnvironmentId,
82 );
83 expect(plain(assignment)).toEqual({ "env-1": ["a1"], "env-2": ["b2"] });
84 });
85
86 it("keeps a server's own worktrees of the repository it lists", () => {
87 const assignment = assignProjectsToEnvironments(
88 [
89 project("a1", "env-1", "github.com/acme/app"),
90 project("a1-wt", "env-1", "GitHub.com/acme/app"),
91 project("a2", "env-2", "github.com/acme/app"),
92 ],
93 envs("env-1", "env-2"),
94 "env-1" as EnvironmentId,
95 );
96 expect(plain(assignment)).toEqual({ "env-1": ["a1", "a1-wt"] });
97 });
98
99 it("reads two servers' copies of one repository as one however the remote is cased", () => {
100 const assignment = assignProjectsToEnvironments(
101 [
102 project("a1", "env-1", "git.example.com/Team/App"),
103 project("a2", "env-2", "GIT.example.com/team/app"),
104 ],
105 envs("env-1", "env-2"),
106 "env-1" as EnvironmentId,
107 );
108 expect(plain(assignment)).toEqual({ "env-1": ["a1"] });
109 });
110
111 it("ignores a project on a server that is not being read", () => {
112 const assignment = assignProjectsToEnvironments(
113 [
114 project("a1", "env-1", "github.com/acme/app"),
115 project("a2", "env-2", "github.com/acme/app"),
116 ],
117 envs("env-2"),
118 "env-2" as EnvironmentId,
119 );
120 expect(plain(assignment)).toEqual({ "env-2": ["a2"] });
121 });
122
123 it("answers the same whatever order the projects arrive in", () => {
124 const projects = [
125 project("a2", "env-2", "github.com/acme/app"),
126 project("a1", "env-1", "github.com/acme/app"),
127 ];
128 const forward = assignProjectsToEnvironments(projects, envs("env-1", "env-2"), null);
129 const backward = assignProjectsToEnvironments(
130 projects.toReversed(),
131 envs("env-1", "env-2"),
132 null,
133 );
134 expect(plain(forward)).toEqual({ "env-1": ["a1"] });
135 expect(plain(backward)).toEqual(plain(forward));
136 });
137});
138
139const connected = (...ids: ReadonlyArray<string>) =>
140 ids.map((id) => ({ environmentId: id as EnvironmentId, label: `Server ${id}` }));
141
142const on = (environmentId: string, projectId: string) => ({
143 environmentId: environmentId as EnvironmentId,
144 projectId: projectId as ProjectId,
145});
146
147describe("where a pull request can be acted on", () => {
148 it("offers every server holding the repository, the panel's own first", () => {
149 const pickable = resolvePickableEnvironments(
150 on("env-2", "a2"),
151 [
152 project("a1", "env-1", "github.com/acme/app"),
153 project("a2", "env-2", "github.com/acme/app"),
154 project("c3", "env-3", "github.com/acme/site"),
155 ],
156 connected("env-1", "env-2", "env-3"),
157 );
158 expect(pickable).toEqual([
159 {
160 environmentId: "env-2",
161 projectId: "a2",
162 workspaceRoot: "/srv/env-2/a2",
163 label: "Server env-2",
164 },
165 {
166 environmentId: "env-1",
167 projectId: "a1",
168 workspaceRoot: "/srv/env-1/a1",
169 label: "Server env-1",
170 },
171 ]);
172 });
173
174 it("matches copies however the remote is cased", () => {
175 const pickable = resolvePickableEnvironments(
176 on("env-1", "a1"),
177 [
178 project("a1", "env-1", "github.com/acme/app"),
179 project("a2", "env-2", "GitHub.com/ACME/app"),
180 ],
181 connected("env-1", "env-2"),
182 );
183 expect(pickable.map((entry) => entry.environmentId)).toEqual(["env-1", "env-2"]);
184 });
185
186 it("offers nothing where one server holds the repository", () => {
187 expect(
188 resolvePickableEnvironments(
189 on("env-1", "a1"),
190 [
191 project("a1", "env-1", "github.com/acme/app"),
192 project("c2", "env-2", "github.com/acme/site"),
193 ],
194 connected("env-1", "env-2"),
195 ),
196 ).toEqual([]);
197 });
198
199 it("offers nothing for a project with no identity to compare", () => {
200 expect(
201 resolvePickableEnvironments(
202 on("env-1", "p1"),
203 [project("p1", "env-1"), project("p2", "env-2")],
204 connected("env-1", "env-2"),
205 ),
206 ).toEqual([]);
207 });
208
209 it("offers nothing while the projects are still arriving", () => {
210 expect(resolvePickableEnvironments(on("env-1", "a1"), [], connected("env-1", "env-2"))).toEqual(
211 [],
212 );
213 });
214
215 it("leaves out a server that is not connected", () => {
216 expect(
217 resolvePickableEnvironments(
218 on("env-1", "a1"),
219 [
220 project("a1", "env-1", "github.com/acme/app"),
221 project("a2", "env-2", "github.com/acme/app"),
222 ],
223 connected("env-1"),
224 ),
225 ).toEqual([]);
226 });
227
228 it("names one copy per server, so two worktrees are one choice", () => {
229 const pickable = resolvePickableEnvironments(
230 on("env-1", "a1"),
231 [
232 project("a1", "env-1", "github.com/acme/app"),
233 project("a1-wt", "env-1", "github.com/acme/app"),
234 project("a2", "env-2", "github.com/acme/app"),
235 project("a2-wt", "env-2", "github.com/acme/app"),
236 ],
237 connected("env-1", "env-2"),
238 );
239 expect(pickable.map((entry) => entry.projectId)).toEqual(["a1", "a2"]);
240 });
241
242 it("keeps the panel's own worktree rather than the server's first copy", () => {
243 const pickable = resolvePickableEnvironments(
244 on("env-1", "a1-wt"),
245 [
246 project("a1", "env-1", "github.com/acme/app"),
247 project("a1-wt", "env-1", "github.com/acme/app"),
248 project("a2", "env-2", "github.com/acme/app"),
249 ],
250 connected("env-1", "env-2"),
251 );
252 expect(pickable[0]).toEqual({
253 environmentId: "env-1",
254 projectId: "a1-wt",
255 workspaceRoot: "/srv/env-1/a1-wt",
256 label: "Server env-1",
257 });
258 });
259});