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
33 import { assert, it } from "@effect/vitest";
44 import * as Effect from "effect/Effect";
55 import * as FileSystem from "effect/FileSystem";
6−import * as Path from "effect/Path";
76 import * as Layer from "effect/Layer";
87 import * as PlatformError from "effect/PlatformError";
98 import { ChildProcessSpawner } from "effect/unstable/process";
109
1110 import { GitCommandError, SourceControlProviderError } from "@t3tools/contracts";
1211
12+import * as Deferred from "effect/Deferred";
13+import * as Fiber from "effect/Fiber";
1314 import * as ServerConfig from "../config.ts";
1415 import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
16+import * as VcsProcess from "../vcs/VcsProcess.ts";
1517 import type * as SourceControlProvider from "./SourceControlProvider.ts";
1618 import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";
1719 import * as SourceControlRepositoryService from "./SourceControlRepositoryService.ts";
@@ -54,6 +56,10 @@ function processOutput(): GitVcsDriver.ExecuteGitResult { · 36 unmodified lines
5456 };
5557 }
5658
59+function processOutputWithStdout(stdout: string): GitVcsDriver.ExecuteGitResult {
60+ return { ...processOutput(), stdout };
61+}
62+
5763 function makeLayer(input: {
5864 readonly provider?: SourceControlProvider.SourceControlProvider["Service"];
5965 readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>;
@@ -95,6 +101,23 @@ function makeLayer(input: { · 35 unmodified lines
95101 : serviceLayer.pipe(Layer.provideMerge(NodeServices.layer));
96102 }
97103
104+function 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+
98121 it.effect("looks up repositories through the requested provider without search", () => {
99122 const calls: Array<{ cwd: string; repository: string }> = [];
100123 const provider = makeProvider({
@@ -154,12 +177,20 @@ it.effect("preserves provider failures without deriving the repository message f · 53 unmodified lines
154177 it.effect("clones a looked-up repository into the requested destination", () =>
155178 Effect.gen(function* () {
156179 const fs = yield* FileSystem.FileSystem;
157− const path = yield* Path.Path;
158180 const parent = yield* fs.makeTempDirectoryScoped({
159181 prefix: "t3-source-control-clone-parent-",
160182 });
161− const destinationPath = path.join(parent, "t3code");
183+ const destinationPath = `${parent}/missing/nested/t3code`;
162184 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+ });
163194
164195 yield* Effect.gen(function* () {
165196 const service = yield* SourceControlRepositoryService.SourceControlRepositoryService;
@@ -177,7 +208,7 @@ it.effect("clones a looked-up repository into the requested destination", () => · 11 unmodified lines
177208 });
178209 assert.deepStrictEqual(cloneCalls, [
179210 {
180− cwd: parent,
211+ cwd: `${parent}/missing/nested`,
181212 args: ["clone", CLONE_URLS.url, "t3code"],
182213 },
183214 ]);
@@ -191,6 +222,253 @@ it.effect("clones a looked-up repository into the requested destination", () => · 7 unmodified lines
191222 return processOutput();
192223 }),
193224 },
225+ provider,
226+ }),
227+ ),
228+ );
229+ assert.deepStrictEqual(lookupCwds, [`${parent}/missing/nested`]);
230+ }).pipe(Effect.provide(NodeServices.layer)),
231+);
232+
233+it.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+
333+it.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+
413+it.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+ },
194472 }),
195473 ),
196474 );