History

Add remote repository publish and discovery support (#2482)

Julius Marminge committed 6 months ago

Commit d7969261 parent f7748a033 files · +2502 −157Browse files

Showing the changes to apps/server/src/sourceControl/SourceControlRepositoryService.test.ts (1 of 33 files). Show all

Unified · Split

apps/server/src/sourceControl/SourceControlRepositoryService.test.ts+322−0
@@ -0,0 +1,322 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";
3import { Effect, FileSystem, Layer } from "effect";
4import { ChildProcessSpawner } from "effect/unstable/process";
5
6import { GitCommandError, type SourceControlProviderError } from "@t3tools/contracts";
7
8import { ServerConfig } from "../config.ts";
9import {
10 GitVcsDriver,
11 type ExecuteGitResult,
12 type GitVcsDriverShape,
13} from "../vcs/GitVcsDriver.ts";
14import { SourceControlProviderRegistry } from "./SourceControlProviderRegistry.ts";
15import type { SourceControlProviderShape } from "./SourceControlProvider.ts";
16import { SourceControlRepositoryService, layer } from "./SourceControlRepositoryService.ts";
17
18const CLONE_URLS = {
19 nameWithOwner: "octocat/t3code",
20 url: "https://github.com/octocat/t3code",
21 sshUrl: "git@github.com:octocat/t3code.git",
22};
23
24function makeProvider(
25 overrides: Partial<SourceControlProviderShape> = {},
26): SourceControlProviderShape {
27 const unsupported = (operation: string) =>
28 Effect.die(`unexpected provider operation ${operation}`) as Effect.Effect<
29 never,
30 SourceControlProviderError
31 >;
32
33 return {
34 kind: "github",
35 listChangeRequests: () => unsupported("listChangeRequests"),
36 getChangeRequest: () => unsupported("getChangeRequest"),
37 createChangeRequest: () => unsupported("createChangeRequest"),
38 getRepositoryCloneUrls: () => Effect.succeed(CLONE_URLS),
39 createRepository: () => Effect.succeed(CLONE_URLS),
40 getDefaultBranch: () => Effect.succeed(null),
41 checkoutChangeRequest: () => unsupported("checkoutChangeRequest"),
42 ...overrides,
43 };
44}
45
46function processOutput(): ExecuteGitResult {
47 return {
48 exitCode: ChildProcessSpawner.ExitCode(0),
49 stdout: "",
50 stderr: "",
51 stdoutTruncated: false,
52 stderrTruncated: false,
53 };
54}
55
56function makeLayer(input: {
57 readonly provider?: SourceControlProviderShape;
58 readonly git?: Partial<GitVcsDriverShape>;
59}) {
60 return layer.pipe(
61 Layer.provide(
62 Layer.mock(SourceControlProviderRegistry)({
63 get: () => Effect.succeed(input.provider ?? makeProvider()),
64 }),
65 ),
66 Layer.provide(
67 Layer.mock(GitVcsDriver)({
68 execute: () => Effect.succeed(processOutput()),
69 ensureRemote: () => Effect.succeed("origin"),
70 pushCurrentBranch: () =>
71 Effect.succeed({
72 status: "pushed" as const,
73 branch: "feature/remote-v1",
74 upstreamBranch: "origin/feature/remote-v1",
75 setUpstream: true,
76 }),
77 ...input.git,
78 }),
79 ),
80 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-source-control-repos-" })),
81 Layer.provideMerge(NodeServices.layer),
82 );
83}
84
85it.effect("looks up repositories through the requested provider without search", () => {
86 const calls: Array<{ cwd: string; repository: string }> = [];
87 const provider = makeProvider({
88 getRepositoryCloneUrls: (input) =>
89 Effect.sync(() => {
90 calls.push({ cwd: input.cwd, repository: input.repository });
91 return CLONE_URLS;
92 }),
93 });
94
95 return Effect.gen(function* () {
96 const service = yield* SourceControlRepositoryService;
97 const result = yield* service.lookupRepository({
98 provider: "github",
99 repository: "octocat/t3code",
100 cwd: "/workspace",
101 });
102
103 assert.deepStrictEqual(result, { provider: "github", ...CLONE_URLS });
104 assert.deepStrictEqual(calls, [{ cwd: "/workspace", repository: "octocat/t3code" }]);
105 }).pipe(Effect.provide(makeLayer({ provider })));
106});
107
108it.effect("clones a looked-up repository into the requested destination", () =>
109 Effect.gen(function* () {
110 const fs = yield* FileSystem.FileSystem;
111 const parent = yield* fs.makeTempDirectoryScoped({
112 prefix: "t3-source-control-clone-parent-",
113 });
114 const destinationPath = `${parent}/t3code`;
115 const cloneCalls: Array<{ cwd: string; args: ReadonlyArray<string> }> = [];
116
117 yield* Effect.gen(function* () {
118 const service = yield* SourceControlRepositoryService;
119 const result = yield* service.cloneRepository({
120 provider: "github",
121 repository: "octocat/t3code",
122 destinationPath,
123 protocol: "https",
124 });
125
126 assert.deepStrictEqual(result, {
127 cwd: destinationPath,
128 remoteUrl: CLONE_URLS.url,
129 repository: { provider: "github", ...CLONE_URLS },
130 });
131 assert.deepStrictEqual(cloneCalls, [
132 {
133 cwd: parent,
134 args: ["clone", CLONE_URLS.url, "t3code"],
135 },
136 ]);
137 }).pipe(
138 Effect.provide(
139 makeLayer({
140 git: {
141 execute: (input) =>
142 Effect.sync(() => {
143 cloneCalls.push({ cwd: input.cwd, args: input.args });
144 return processOutput();
145 }),
146 },
147 }),
148 ),
149 );
150 }).pipe(Effect.provide(NodeServices.layer)),
151);
152
153it.effect("publishes by creating the repository, adding a remote, and pushing upstream", () => {
154 const createCalls: Array<{ cwd: string; repository: string; visibility: string }> = [];
155 const remoteCalls: Array<{ cwd: string; preferredName: string; url: string }> = [];
156 const pushCalls: Array<{ cwd: string; remoteName: string | null | undefined }> = [];
157 const provider = makeProvider({
158 createRepository: (input) =>
159 Effect.sync(() => {
160 createCalls.push({
161 cwd: input.cwd,
162 repository: input.repository,
163 visibility: input.visibility,
164 });
165 return CLONE_URLS;
166 }),
167 });
168
169 return Effect.gen(function* () {
170 const service = yield* SourceControlRepositoryService;
171 const result = yield* service.publishRepository({
172 cwd: "/workspace",
173 provider: "github",
174 repository: "octocat/t3code",
175 visibility: "private",
176 remoteName: "origin",
177 protocol: "ssh",
178 });
179
180 assert.deepStrictEqual(result, {
181 repository: { provider: "github", ...CLONE_URLS },
182 remoteName: "origin",
183 remoteUrl: CLONE_URLS.sshUrl,
184 branch: "feature/remote-v1",
185 upstreamBranch: "origin/feature/remote-v1",
186 status: "pushed",
187 });
188 assert.deepStrictEqual(createCalls, [
189 { cwd: "/workspace", repository: "octocat/t3code", visibility: "private" },
190 ]);
191 assert.deepStrictEqual(remoteCalls, [
192 { cwd: "/workspace", preferredName: "origin", url: CLONE_URLS.sshUrl },
193 ]);
194 assert.deepStrictEqual(pushCalls, [{ cwd: "/workspace", remoteName: "origin" }]);
195 }).pipe(
196 Effect.provide(
197 makeLayer({
198 provider,
199 git: {
200 ensureRemote: (input) =>
201 Effect.sync(() => {
202 remoteCalls.push(input);
203 return "origin";
204 }),
205 pushCurrentBranch: (cwd, _fallbackBranch, options) =>
206 Effect.sync(() => {
207 pushCalls.push({ cwd, remoteName: options?.remoteName });
208 return {
209 status: "pushed" as const,
210 branch: "feature/remote-v1",
211 upstreamBranch: "origin/feature/remote-v1",
212 setUpstream: true,
213 };
214 }),
215 },
216 }),
217 ),
218 );
219});
220
221it.effect("publishes to the remote name returned by ensureRemote", () => {
222 const pushCalls: Array<{ cwd: string; remoteName: string | null | undefined }> = [];
223
224 return Effect.gen(function* () {
225 const service = yield* SourceControlRepositoryService;
226 const result = yield* service.publishRepository({
227 cwd: "/workspace",
228 provider: "github",
229 repository: "octocat/t3code",
230 visibility: "private",
231 remoteName: "origin",
232 protocol: "ssh",
233 });
234
235 assert.equal(result.remoteName, "origin-1");
236 assert.deepStrictEqual(pushCalls, [{ cwd: "/workspace", remoteName: "origin-1" }]);
237 }).pipe(
238 Effect.provide(
239 makeLayer({
240 git: {
241 ensureRemote: () => Effect.succeed("origin-1"),
242 pushCurrentBranch: (cwd, _fallbackBranch, options) =>
243 Effect.sync(() => {
244 pushCalls.push({ cwd, remoteName: options?.remoteName });
245 return {
246 status: "pushed" as const,
247 branch: "feature/remote-v1",
248 upstreamBranch: `${options?.remoteName ?? "missing"}/feature/remote-v1`,
249 setUpstream: true,
250 };
251 }),
252 },
253 }),
254 ),
255 );
256});
257
258it.effect("publish succeeds with status remote_added when the local repo has no commits", () => {
259 let pushCalls = 0;
260 return Effect.gen(function* () {
261 const service = yield* SourceControlRepositoryService;
262 const result = yield* service.publishRepository({
263 cwd: "/workspace",
264 provider: "github",
265 repository: "octocat/t3code",
266 visibility: "private",
267 remoteName: "origin",
268 protocol: "ssh",
269 });
270
271 assert.deepStrictEqual(result, {
272 repository: { provider: "github", ...CLONE_URLS },
273 remoteName: "origin",
274 remoteUrl: CLONE_URLS.sshUrl,
275 branch: "main",
276 status: "remote_added",
277 });
278 assert.strictEqual(pushCalls, 0);
279 }).pipe(
280 Effect.provide(
281 makeLayer({
282 git: {
283 execute: (input) =>
284 input.args[0] === "rev-parse"
285 ? Effect.fail(
286 new GitCommandError({
287 operation: input.operation,
288 command: "git rev-parse --verify HEAD",
289 cwd: input.cwd,
290 detail: "fatal: Needed a single revision",
291 }),
292 )
293 : Effect.succeed(processOutput()),
294 statusDetails: () =>
295 Effect.succeed({
296 isRepo: true,
297 hasOriginRemote: true,
298 isDefaultBranch: true,
299 branch: "main",
300 upstreamRef: null,
301 hasWorkingTreeChanges: false,
302 workingTree: { files: [], insertions: 0, deletions: 0 },
303 hasUpstream: false,
304 aheadCount: 0,
305 behindCount: 0,
306 aheadOfDefaultCount: 0,
307 }),
308 pushCurrentBranch: () =>
309 Effect.sync(() => {
310 pushCalls += 1;
311 return {
312 status: "pushed" as const,
313 branch: "main",
314 upstreamBranch: "origin/main",
315 setUpstream: true,
316 };
317 }),
318 },
319 }),
320 ),
321 );
322});