235 lines · 9.5 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import { expect, it } from "@effect/vitest";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path";import { TestClock } from "effect/testing"; import * as ProcessRunner from "../processRunner.ts";import * as RepositoryIdentityResolver from "./RepositoryIdentityResolver.ts"; const normalizePathSeparators = (value: string) => value.replaceAll("\\", "/");const normalizeResolvedPath = (value: string) => normalizePathSeparators(value); const git = (cwd: string, args: ReadonlyArray<string>) =>  Effect.gen(function* () {    const processRunner = yield* ProcessRunner.ProcessRunner;    return yield* processRunner.run({      command: "git",      args: ["-C", cwd, ...args],    });  }).pipe(Effect.provide(ProcessRunner.layer)); const makeRepositoryIdentityResolverTestLayer = (options: {  readonly positiveCacheTtl?: Duration.Input;  readonly negativeCacheTtl?: Duration.Input;}) =>  Layer.effect(    RepositoryIdentityResolver.RepositoryIdentityResolver,    RepositoryIdentityResolver.make({      cacheCapacity: 16,      ...options,    }),  ).pipe(Layer.provide(ProcessRunner.layer)); it.layer(NodeServices.layer)("RepositoryIdentityResolverLive", (it) => {  it.effect("normalizes equivalent GitHub remotes into a stable repository identity", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const cwd = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-repository-identity-test-",      });       yield* git(cwd, ["init"]);      yield* git(cwd, ["remote", "add", "origin", "git@github.com:T3Tools/t3code.git"]);       const resolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;      const identity = yield* resolver.resolve(cwd);      const resolvedIdentityRoot =        identity?.rootPath === undefined ? "" : yield* fileSystem.realPath(identity.rootPath);      const resolvedCwd = yield* fileSystem.realPath(cwd);       expect(identity).not.toBeNull();      expect(identity?.canonicalKey).toBe("github.com/t3tools/t3code");      expect(normalizeResolvedPath(resolvedIdentityRoot)).toBe(normalizeResolvedPath(resolvedCwd));      expect(identity?.displayName).toBe("t3tools/t3code");      expect(identity?.provider).toBe("github");      expect(identity?.owner).toBe("t3tools");      expect(identity?.name).toBe("t3code");    }).pipe(Effect.provide(RepositoryIdentityResolver.layer)),  );   it.effect("returns the git top-level root path when resolving from a nested workspace", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const path = yield* Path.Path;      const repoRoot = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-repository-identity-nested-root-test-",      });      const nestedWorkspace = path.join(repoRoot, "packages", "web");       yield* fileSystem.makeDirectory(nestedWorkspace, { recursive: true });      yield* git(repoRoot, ["init"]);      yield* git(repoRoot, ["remote", "add", "origin", "git@github.com:T3Tools/t3code.git"]);       const resolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;      const identity = yield* resolver.resolve(nestedWorkspace);      const resolvedIdentityRoot =        identity?.rootPath === undefined ? "" : yield* fileSystem.realPath(identity.rootPath);      const resolvedRepoRoot = yield* fileSystem.realPath(repoRoot);       expect(identity).not.toBeNull();      expect(identity?.canonicalKey).toBe("github.com/t3tools/t3code");      expect(normalizeResolvedPath(resolvedIdentityRoot)).toBe(        normalizeResolvedPath(resolvedRepoRoot),      );    }).pipe(Effect.provide(RepositoryIdentityResolver.layer)),  );   it.effect("returns null for non-git folders and repos without remotes", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const nonGitDir = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-repository-identity-non-git-",      });      const gitDir = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-repository-identity-no-remote-",      });       yield* git(gitDir, ["init"]);       const resolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;      const nonGitIdentity = yield* resolver.resolve(nonGitDir);      const noRemoteIdentity = yield* resolver.resolve(gitDir);       expect(nonGitIdentity).toBeNull();      expect(noRemoteIdentity).toBeNull();    }).pipe(Effect.provide(RepositoryIdentityResolver.layer)),  );   it.effect("prefers upstream over origin when both remotes are configured", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const cwd = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-repository-identity-upstream-test-",      });       yield* git(cwd, ["init"]);      yield* git(cwd, ["remote", "add", "origin", "git@github.com:julius/t3code.git"]);      yield* git(cwd, ["remote", "add", "upstream", "git@github.com:T3Tools/t3code.git"]);       const resolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;      const identity = yield* resolver.resolve(cwd);       expect(identity).not.toBeNull();      expect(identity?.locator.remoteName).toBe("upstream");      expect(identity?.canonicalKey).toBe("github.com/t3tools/t3code");      expect(identity?.displayName).toBe("t3tools/t3code");    }).pipe(Effect.provide(RepositoryIdentityResolver.layer)),  );   it.effect("uses the last remote path segment as the repository name for nested groups", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const cwd = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-repository-identity-nested-group-test-",      });       yield* git(cwd, ["init"]);      yield* git(cwd, ["remote", "add", "origin", "git@gitlab.com:T3Tools/platform/t3code.git"]);       const resolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;      const identity = yield* resolver.resolve(cwd);       expect(identity).not.toBeNull();      expect(identity?.canonicalKey).toBe("gitlab.com/t3tools/platform/t3code");      expect(identity?.displayName).toBe("t3tools/platform/t3code");      expect(identity?.owner).toBe("t3tools");      expect(identity?.name).toBe("t3code");    }).pipe(Effect.provide(RepositoryIdentityResolver.layer)),  );   it.effect(    "keeps null identities cached across repeated resolves until the negative TTL expires",    () =>      Effect.gen(function* () {        const fileSystem = yield* FileSystem.FileSystem;        const cwd = yield* fileSystem.makeTempDirectoryScoped({          prefix: "t3-repository-identity-late-remote-test-",        });         yield* git(cwd, ["init"]);         const resolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;        const initialIdentity = yield* resolver.resolve(cwd);        expect(initialIdentity).toBeNull();         yield* git(cwd, ["remote", "add", "origin", "git@github.com:T3Tools/t3code.git"]);         for (const _attempt of [1, 2, 3]) {          const cachedIdentity = yield* resolver.resolve(cwd);          expect(cachedIdentity).toBeNull();        }         yield* TestClock.adjust(Duration.millis(120));         const refreshedIdentity = yield* resolver.resolve(cwd);        expect(refreshedIdentity).not.toBeNull();        expect(refreshedIdentity?.canonicalKey).toBe("github.com/t3tools/t3code");        expect(refreshedIdentity?.name).toBe("t3code");      }).pipe(        Effect.provide(          Layer.merge(            TestClock.layer(),            makeRepositoryIdentityResolverTestLayer({              negativeCacheTtl: Duration.millis(50),              positiveCacheTtl: Duration.seconds(1),            }),          ),        ),      ),  );   it.effect("refreshes cached identities after the positive TTL when a remote changes", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const cwd = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-repository-identity-remote-change-test-",      });       yield* git(cwd, ["init"]);      yield* git(cwd, ["remote", "add", "origin", "git@github.com:T3Tools/t3code.git"]);       const resolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;      const initialIdentity = yield* resolver.resolve(cwd);      expect(initialIdentity).not.toBeNull();      expect(initialIdentity?.canonicalKey).toBe("github.com/t3tools/t3code");       yield* git(cwd, ["remote", "set-url", "origin", "git@github.com:T3Tools/t3code-next.git"]);       const cachedIdentity = yield* resolver.resolve(cwd);      expect(cachedIdentity).not.toBeNull();      expect(cachedIdentity?.canonicalKey).toBe("github.com/t3tools/t3code");       yield* TestClock.adjust(Duration.millis(180));       const refreshedIdentity = yield* resolver.resolve(cwd);      expect(refreshedIdentity).not.toBeNull();      expect(refreshedIdentity?.canonicalKey).toBe("github.com/t3tools/t3code-next");      expect(refreshedIdentity?.displayName).toBe("t3tools/t3code-next");      expect(refreshedIdentity?.name).toBe("t3code-next");    }).pipe(      Effect.provide(        Layer.merge(          TestClock.layer(),          makeRepositoryIdentityResolverTestLayer({            negativeCacheTtl: Duration.millis(50),            positiveCacheTtl: Duration.millis(100),          }),        ),      ),    ),  );});