History

feat(mcp): add project management tools

Julius Marminge committed 2 months ago

Commit 04bd6a81 parent 415ed0f29 files · +2143 −121Browse files

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

Unified · Split

apps/server/src/sourceControl/SourceControlRepositoryService.test.ts+282−4
@@ -3,15 +3,17 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; · 2 unmodified lines
3import { assert, it } from "@effect/vitest";3import { assert, it } from "@effect/vitest";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
8import * as PlatformError from "effect/PlatformError";7import * as PlatformError from "effect/PlatformError";
9import { ChildProcessSpawner } from "effect/unstable/process";8import { ChildProcessSpawner } from "effect/unstable/process";
10 9
11import { GitCommandError, SourceControlProviderError } from "@t3tools/contracts";10import { GitCommandError, SourceControlProviderError } from "@t3tools/contracts";
12 11
12import * as Deferred from "effect/Deferred";
13import * as Fiber from "effect/Fiber";
13import * as ServerConfig from "../config.ts";14import * as ServerConfig from "../config.ts";
14import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";15import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
16import * as VcsProcess from "../vcs/VcsProcess.ts";
15import type * as SourceControlProvider from "./SourceControlProvider.ts";17import type * as SourceControlProvider from "./SourceControlProvider.ts";
16import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";18import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";
17import * as SourceControlRepositoryService from "./SourceControlRepositoryService.ts";19import * as SourceControlRepositoryService from "./SourceControlRepositoryService.ts";
@@ -54,6 +56,10 @@ function processOutput(): GitVcsDriver.ExecuteGitResult { · 36 unmodified lines
54 };56 };
55}57}
56 58
59function processOutputWithStdout(stdout: string): GitVcsDriver.ExecuteGitResult {
60 return { ...processOutput(), stdout };
61}
62
57function makeLayer(input: {63function makeLayer(input: {
58 readonly provider?: SourceControlProvider.SourceControlProvider["Service"];64 readonly provider?: SourceControlProvider.SourceControlProvider["Service"];
59 readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>;65 readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>;
@@ -95,6 +101,23 @@ function makeLayer(input: { · 35 unmodified lines
95 : serviceLayer.pipe(Layer.provideMerge(NodeServices.layer));101 : serviceLayer.pipe(Layer.provideMerge(NodeServices.layer));
96}102}
97 103
104function makeRealGitLayer() {
105 const gitLayer = GitVcsDriver.layer.pipe(
106 Layer.provide(VcsProcess.layer),
107 Layer.provide(NodeServices.layer),
108 );
109 return SourceControlRepositoryService.layer.pipe(
110 Layer.provide(
111 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({
112 get: () => Effect.succeed(makeProvider()),
113 }),
114 ),
115 Layer.provideMerge(gitLayer),
116 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-source-control-real-git-" })),
117 Layer.provideMerge(NodeServices.layer),
118 );
119}
120
98it.effect("looks up repositories through the requested provider without search", () => {121it.effect("looks up repositories through the requested provider without search", () => {
99 const calls: Array<{ cwd: string; repository: string }> = [];122 const calls: Array<{ cwd: string; repository: string }> = [];
100 const provider = makeProvider({123 const provider = makeProvider({
@@ -154,12 +177,20 @@ it.effect("preserves provider failures without deriving the repository message f · 53 unmodified lines
154it.effect("clones a looked-up repository into the requested destination", () =>177it.effect("clones a looked-up repository into the requested destination", () =>
155 Effect.gen(function* () {178 Effect.gen(function* () {
156 const fs = yield* FileSystem.FileSystem;179 const fs = yield* FileSystem.FileSystem;
157 const path = yield* Path.Path;
158 const parent = yield* fs.makeTempDirectoryScoped({180 const parent = yield* fs.makeTempDirectoryScoped({
159 prefix: "t3-source-control-clone-parent-",181 prefix: "t3-source-control-clone-parent-",
160 });182 });
161 const destinationPath = path.join(parent, "t3code");183 const destinationPath = `${parent}/missing/nested/t3code`;
162 const cloneCalls: Array<{ cwd: string; args: ReadonlyArray<string> }> = [];184 const cloneCalls: Array<{ cwd: string; args: ReadonlyArray<string> }> = [];
185 const lookupCwds: Array<string> = [];
186 const provider = makeProvider({
187 getRepositoryCloneUrls: (input) =>
188 Effect.gen(function* () {
189 assert.strictEqual(yield* fs.exists(input.cwd).pipe(Effect.orDie), true);
190 lookupCwds.push(input.cwd);
191 return CLONE_URLS;
192 }),
193 });
163 194
164 yield* Effect.gen(function* () {195 yield* Effect.gen(function* () {
165 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;196 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
@@ -177,7 +208,7 @@ it.effect("clones a looked-up repository into the requested destination", () => · 11 unmodified lines
177 });208 });
178 assert.deepStrictEqual(cloneCalls, [209 assert.deepStrictEqual(cloneCalls, [
179 {210 {
180 cwd: parent,211 cwd: `${parent}/missing/nested`,
181 args: ["clone", CLONE_URLS.url, "t3code"],212 args: ["clone", CLONE_URLS.url, "t3code"],
182 },213 },
183 ]);214 ]);
@@ -191,6 +222,253 @@ it.effect("clones a looked-up repository into the requested destination", () => · 7 unmodified lines
191 return processOutput();222 return processOutput();
192 }),223 }),
193 },224 },
225 provider,
226 }),
227 ),
228 );
229 assert.deepStrictEqual(lookupCwds, [`${parent}/missing/nested`]);
230 }).pipe(Effect.provide(NodeServices.layer)),
231);
232
233it.effect("recovers a matching completed clone and rejects unrelated contents", () =>
234 Effect.gen(function* () {
235 const fs = yield* FileSystem.FileSystem;
236 const parent = yield* fs.makeTempDirectoryScoped({
237 prefix: "t3-source-control-recover-clone-",
238 });
239 const matchingDestination = `${parent}/matching`;
240 const unrelatedDestination = `${parent}/unrelated`;
241 const wrongPortDestination = `${parent}/wrong-port`;
242 const wrongCaseDestination = `${parent}/wrong-case`;
243 const wrongUserDestination = `${parent}/wrong-user`;
244 const absoluteSshDestination = `${parent}/absolute-ssh`;
245 yield* fs.makeDirectory(matchingDestination);
246 yield* fs.makeDirectory(unrelatedDestination);
247 yield* fs.makeDirectory(wrongPortDestination);
248 yield* fs.makeDirectory(wrongCaseDestination);
249 yield* fs.makeDirectory(wrongUserDestination);
250 yield* fs.makeDirectory(absoluteSshDestination);
251 yield* fs.writeFileString(`${matchingDestination}/README.md`, "matching clone");
252 yield* fs.writeFileString(`${unrelatedDestination}/README.md`, "unrelated repository");
253 yield* fs.writeFileString(`${wrongPortDestination}/README.md`, "different port");
254 yield* fs.writeFileString(`${wrongCaseDestination}/README.md`, "different case");
255 yield* fs.writeFileString(`${wrongUserDestination}/README.md`, "different user");
256 yield* fs.writeFileString(`${absoluteSshDestination}/README.md`, "absolute SSH path");
257 const cloneCalls: Array<ReadonlyArray<string>> = [];
258
259 yield* Effect.gen(function* () {
260 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
261 const recovered = yield* service.cloneRepository({
262 remoteUrl: CLONE_URLS.url,
263 destinationPath: matchingDestination,
264 });
265 assert.strictEqual(recovered.cwd, matchingDestination);
266 assert.deepStrictEqual(cloneCalls, []);
267
268 const mismatch = yield* Effect.flip(
269 service.cloneRepository({
270 remoteUrl: CLONE_URLS.url,
271 destinationPath: unrelatedDestination,
272 }),
273 );
274 assert.strictEqual(
275 mismatch.detail,
276 "Destination path already exists and is not a clone of the requested repository.",
277 );
278
279 for (const [destinationPath, remoteUrl] of [
280 [wrongPortDestination, "https://git.example:8444/org/repo.git"],
281 [wrongCaseDestination, "https://git.example/org/repo.git"],
282 [wrongUserDestination, "bob@git.example:repo.git"],
283 [absoluteSshDestination, "git@git.example:repo.git"],
284 ] as const) {
285 const strictMismatch = yield* Effect.flip(
286 service.cloneRepository({ remoteUrl, destinationPath }),
287 );
288 assert.strictEqual(
289 strictMismatch.detail,
290 "Destination path already exists and is not a clone of the requested repository.",
291 );
292 }
293 }).pipe(
294 Effect.provide(
295 makeLayer({
296 git: {
297 execute: (input) => {
298 if (input.args[0] === "rev-parse") {
299 return Effect.succeed(
300 processOutputWithStdout(
301 input.cwd === matchingDestination ? matchingDestination : unrelatedDestination,
302 ),
303 );
304 }
305 if (input.args[0] === "for-each-ref") {
306 return Effect.succeed(processOutputWithStdout(input.cwd));
307 }
308 if (input.args[0] === "status") return Effect.succeed(processOutput());
309 cloneCalls.push(input.args);
310 return Effect.succeed(processOutput());
311 },
312 readConfigValue: (cwd) =>
313 Effect.succeed(
314 cwd === matchingDestination
315 ? CLONE_URLS.url
316 : cwd === wrongPortDestination
317 ? "https://git.example:8443/org/repo.git"
318 : cwd === wrongCaseDestination
319 ? "https://git.example/org/Repo.git"
320 : cwd === wrongUserDestination
321 ? "alice@git.example:repo.git"
322 : cwd === absoluteSshDestination
323 ? "ssh://git@git.example/repo.git"
324 : "https://github.com/acme/other.git",
325 ),
326 },
327 }),
328 ),
329 );
330 }).pipe(Effect.provide(NodeServices.layer)),
331);
332
333it.effect("recovers a real completed clone after a lost result and rejects unsafe adoption", () =>
334 Effect.gen(function* () {
335 const fs = yield* FileSystem.FileSystem;
336 const git = yield* GitVcsDriver.GitVcsDriver;
337 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
338 const parent = yield* fs.makeTempDirectoryScoped({
339 prefix: "t3-source-control-real-recovery-",
340 });
341 const sourcePath = `${parent}/source`;
342 const otherSourcePath = `${parent}/other-source`;
343 const destinationPath = `${parent}/destination`;
344 const incompletePath = `${parent}/incomplete`;
345 const unrelatedCleanPath = `${parent}/unrelated-clean`;
346
347 const initializeRepository = (cwd: string, contents: string) =>
348 Effect.gen(function* () {
349 yield* fs.makeDirectory(cwd);
350 yield* git.execute({ operation: "test.init", cwd, args: ["init"] });
351 yield* git.execute({
352 operation: "test.config.email",
353 cwd,
354 args: ["config", "user.email", "tests@t3.codes"],
355 });
356 yield* git.execute({
357 operation: "test.config.name",
358 cwd,
359 args: ["config", "user.name", "T3 Tests"],
360 });
361 yield* fs.writeFileString(`${cwd}/README.md`, contents);
362 yield* git.execute({ operation: "test.add", cwd, args: ["add", "README.md"] });
363 yield* git.execute({ operation: "test.commit", cwd, args: ["commit", "-m", "initial"] });
364 });
365
366 yield* initializeRepository(sourcePath, "source");
367 yield* initializeRepository(otherSourcePath, "other source");
368
369 const cloned = yield* service.cloneRepository({ remoteUrl: sourcePath, destinationPath });
370 assert.strictEqual(cloned.cwd, destinationPath);
371
372 // Simulate the caller losing the successful clone result before project registration.
373 const recovered = yield* service.cloneRepository({ remoteUrl: sourcePath, destinationPath });
374 assert.strictEqual(recovered.cwd, destinationPath);
375
376 const unrelated = yield* Effect.flip(
377 service.cloneRepository({ remoteUrl: otherSourcePath, destinationPath }),
378 );
379 assert.strictEqual(
380 unrelated.detail,
381 "Destination path already exists and is not a clone of the requested repository.",
382 );
383
384 yield* initializeRepository(unrelatedCleanPath, "unrelated history");
385 yield* git.execute({
386 operation: "test.unrelated.remote",
387 cwd: unrelatedCleanPath,
388 args: ["remote", "add", "origin", sourcePath],
389 });
390 const unrelatedHistory = yield* Effect.flip(
391 service.cloneRepository({ remoteUrl: sourcePath, destinationPath: unrelatedCleanPath }),
392 );
393 assert.strictEqual(
394 unrelatedHistory.detail,
395 "Destination path already exists and is not a clone of the requested repository.",
396 );
397
398 yield* git.execute({
399 operation: "test.incomplete.clone",
400 cwd: parent,
401 args: ["clone", "--no-checkout", sourcePath, "incomplete"],
402 });
403 const incomplete = yield* Effect.flip(
404 service.cloneRepository({ remoteUrl: sourcePath, destinationPath: incompletePath }),
405 );
406 assert.strictEqual(
407 incomplete.detail,
408 "Destination path already exists and is not a clone of the requested repository.",
409 );
410 }).pipe(Effect.provide(makeRealGitLayer())),
411);
412
413it.effect("serializes overlapping clones to one destination and reuses the verified result", () =>
414 Effect.gen(function* () {
415 const fs = yield* FileSystem.FileSystem;
416 const parent = yield* fs.makeTempDirectoryScoped({
417 prefix: "t3-source-control-overlap-clone-",
418 });
419 const realParent = `${parent}/real`;
420 const aliasParent = `${parent}/alias`;
421 yield* fs.makeDirectory(realParent);
422 yield* fs.symlink(realParent, aliasParent);
423 const destinationPath = `${realParent}/t3code`;
424 const aliasDestinationPath = `${aliasParent}/t3code`;
425 const cloneEntered = yield* Deferred.make<void>();
426 const allowClone = yield* Deferred.make<void>();
427 let cloneCount = 0;
428
429 yield* Effect.gen(function* () {
430 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
431 const first = yield* Effect.forkChild(
432 service.cloneRepository({ remoteUrl: CLONE_URLS.url, destinationPath }),
433 { startImmediately: true },
434 );
435 yield* Deferred.await(cloneEntered);
436 const second = yield* Effect.forkChild(
437 service.cloneRepository({
438 remoteUrl: CLONE_URLS.url,
439 destinationPath: aliasDestinationPath,
440 }),
441 { startImmediately: true },
442 );
443 yield* Deferred.succeed(allowClone, undefined);
444 const results = yield* Effect.all([Fiber.join(first), Fiber.join(second)]);
445 assert.deepStrictEqual(
446 results.map((result) => result.cwd),
447 [destinationPath, aliasDestinationPath],
448 );
449 assert.strictEqual(cloneCount, 1);
450 }).pipe(
451 Effect.provide(
452 makeLayer({
453 git: {
454 execute: (input) => {
455 if (input.args[0] === "clone") {
456 cloneCount += 1;
457 return fs
458 .makeDirectory(destinationPath)
459 .pipe(
460 Effect.andThen(fs.writeFileString(`${destinationPath}/README.md`, "cloned")),
461 Effect.andThen(Deferred.succeed(cloneEntered, undefined)),
462 Effect.andThen(Deferred.await(allowClone)),
463 Effect.as(processOutput()),
464 Effect.orDie,
465 );
466 }
467 if (input.args[0] === "status") return Effect.succeed(processOutput());
468 return Effect.succeed(processOutputWithStdout(destinationPath));
469 },
470 readConfigValue: () => Effect.succeed(CLONE_URLS.url),
471 },
194 }),472 }),
195 ),473 ),
196 );474 );