authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-09 16:52:28-07:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-09 22:50:45-07:00
log442bc9540ea52e82ad15aee06e52da2c5e16f948
treec4adbb1e139af75a22f7783bbf93f10d3aaf5e36
parentf7570712a741136999a77d5a8f6490a8c7d9e249

Normalize remote URL matching and migrate GitHubCli tests

- Treat remotes that only differ by trailing slash after `.git` as the same remote - Add a GitCore regression test for trailing-slash remote reuse - Refactor `GitHubCli` tests to `@effect/vitest` layer-based effect tests with reset cleanup

3 files changed, 113 insertions(+), 88 deletions(-)

apps/server/src/git/Layers/GitCore.test.ts+19
......@@ -1323,6 +1323,25 @@ it.layer(TestLayer)("git integration", (it) => {
13231323 }),
13241324 );
13251325
1326 it.effect("reuses an existing remote when the target URL only differs by a trailing slash after .git", () =>
1327 Effect.gen(function* () {
1328 const tmp = yield* makeTmpDir();
1329 yield* initRepoWithCommit(tmp);
1330 const core = yield* GitCore;
1331
1332 yield* git(tmp, ["remote", "add", "origin", "git@github.com:pingdotgg/t3code.git"]);
1333
1334 const remoteName = yield* core.ensureRemote({
1335 cwd: tmp,
1336 preferredName: "origin",
1337 url: "git@github.com:pingdotgg/t3code.git/",
1338 });
1339
1340 expect(remoteName).toBe("origin");
1341 expect((yield* git(tmp, ["remote"])).split("\n").filter(Boolean)).toEqual(["origin"]);
1342 }),
1343 );
1344
13261345 it.effect("reports status details and dirty state", () =>
13271346 Effect.gen(function* () {
13281347 const tmp = yield* makeTmpDir();
apps/server/src/git/Layers/GitCore.ts+1-1
......@@ -106,7 +106,7 @@ function sanitizeRemoteName(value: string): string {
106106}
107107
108108function normalizeRemoteUrl(value: string): string {
109 return value.trim().replace(/\.git$/i, "").replace(/\/+$/g, "").toLowerCase();
109 return value.trim().replace(/\/+$/g, "").replace(/\.git$/i, "").toLowerCase();
110110}
111111
112112function parseRemoteFetchUrls(stdout: string): Map<string, string> {
apps/server/src/git/Layers/GitHubCli.test.ts+93-87
......@@ -1,5 +1,6 @@
1import { describe, expect, it, vi } from "vitest";
2import { Effect } from "effect";
1import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";
3import { afterEach, expect, vi } from "vitest";
34
45vi.mock("../../processRunner", () => ({
56 runProcess: vi.fn(),
......@@ -10,113 +11,118 @@ import { GitHubCli } from "../Services/GitHubCli.ts";
1011import { GitHubCliLive } from "./GitHubCli.ts";
1112
1213const mockedRunProcess = vi.mocked(runProcess);
14const layer = it.layer(Layer.empty);
1315
14describe("GitHubCliLive", () => {
15 it("parses pull request view output", async () => {
16 mockedRunProcess.mockResolvedValueOnce({
17 stdout: JSON.stringify({
18 number: 42,
19 title: "Add PR thread creation",
20 url: "https://github.com/pingdotgg/codething-mvp/pull/42",
21 baseRefName: "main",
22 headRefName: "feature/pr-threads",
23 state: "OPEN",
24 mergedAt: null,
25 isCrossRepository: true,
26 headRepository: {
27 nameWithOwner: "octocat/codething-mvp",
28 },
29 headRepositoryOwner: {
30 login: "octocat",
31 },
32 }),
33 stderr: "",
34 code: 0,
35 signal: null,
36 timedOut: false,
37 });
16afterEach(() => {
17 mockedRunProcess.mockReset();
18});
19
20layer("GitHubCliLive", (it) => {
21 it.effect("parses pull request view output", () =>
22 Effect.gen(function* () {
23 mockedRunProcess.mockResolvedValueOnce({
24 stdout: JSON.stringify({
25 number: 42,
26 title: "Add PR thread creation",
27 url: "https://github.com/pingdotgg/codething-mvp/pull/42",
28 baseRefName: "main",
29 headRefName: "feature/pr-threads",
30 state: "OPEN",
31 mergedAt: null,
32 isCrossRepository: true,
33 headRepository: {
34 nameWithOwner: "octocat/codething-mvp",
35 },
36 headRepositoryOwner: {
37 login: "octocat",
38 },
39 }),
40 stderr: "",
41 code: 0,
42 signal: null,
43 timedOut: false,
44 });
3845
39 const result = await Effect.runPromise(
40 Effect.gen(function* () {
46 const result = yield* Effect.gen(function* () {
4147 const gh = yield* GitHubCli;
4248 return yield* gh.getPullRequest({
4349 cwd: "/repo",
4450 reference: "#42",
4551 });
46 }).pipe(Effect.provide(GitHubCliLive)),
47 );
52 }).pipe(Effect.provide(GitHubCliLive));
4853
49 expect(result).toEqual({
50 number: 42,
51 title: "Add PR thread creation",
52 url: "https://github.com/pingdotgg/codething-mvp/pull/42",
53 baseRefName: "main",
54 headRefName: "feature/pr-threads",
55 state: "open",
56 isCrossRepository: true,
57 headRepositoryNameWithOwner: "octocat/codething-mvp",
58 headRepositoryOwnerLogin: "octocat",
59 });
60 expect(mockedRunProcess).toHaveBeenCalledWith(
61 "gh",
62 [
63 "pr",
64 "view",
65 "#42",
66 "--json",
67 "number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner",
68 ],
69 expect.objectContaining({ cwd: "/repo" }),
70 );
71 });
54 assert.deepStrictEqual(result, {
55 number: 42,
56 title: "Add PR thread creation",
57 url: "https://github.com/pingdotgg/codething-mvp/pull/42",
58 baseRefName: "main",
59 headRefName: "feature/pr-threads",
60 state: "open",
61 isCrossRepository: true,
62 headRepositoryNameWithOwner: "octocat/codething-mvp",
63 headRepositoryOwnerLogin: "octocat",
64 });
65 expect(mockedRunProcess).toHaveBeenCalledWith(
66 "gh",
67 [
68 "pr",
69 "view",
70 "#42",
71 "--json",
72 "number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner",
73 ],
74 expect.objectContaining({ cwd: "/repo" }),
75 );
76 }),
77 );
7278
73 it("reads repository clone URLs", async () => {
74 mockedRunProcess.mockResolvedValueOnce({
75 stdout: JSON.stringify({
76 nameWithOwner: "octocat/codething-mvp",
77 url: "https://github.com/octocat/codething-mvp",
78 sshUrl: "git@github.com:octocat/codething-mvp.git",
79 }),
80 stderr: "",
81 code: 0,
82 signal: null,
83 timedOut: false,
84 });
79 it.effect("reads repository clone URLs", () =>
80 Effect.gen(function* () {
81 mockedRunProcess.mockResolvedValueOnce({
82 stdout: JSON.stringify({
83 nameWithOwner: "octocat/codething-mvp",
84 url: "https://github.com/octocat/codething-mvp",
85 sshUrl: "git@github.com:octocat/codething-mvp.git",
86 }),
87 stderr: "",
88 code: 0,
89 signal: null,
90 timedOut: false,
91 });
8592
86 const result = await Effect.runPromise(
87 Effect.gen(function* () {
93 const result = yield* Effect.gen(function* () {
8894 const gh = yield* GitHubCli;
8995 return yield* gh.getRepositoryCloneUrls({
9096 cwd: "/repo",
9197 repository: "octocat/codething-mvp",
9298 });
93 }).pipe(Effect.provide(GitHubCliLive)),
94 );
99 }).pipe(Effect.provide(GitHubCliLive));
95100
96 expect(result).toEqual({
97 nameWithOwner: "octocat/codething-mvp",
98 url: "https://github.com/octocat/codething-mvp",
99 sshUrl: "git@github.com:octocat/codething-mvp.git",
100 });
101 });
101 assert.deepStrictEqual(result, {
102 nameWithOwner: "octocat/codething-mvp",
103 url: "https://github.com/octocat/codething-mvp",
104 sshUrl: "git@github.com:octocat/codething-mvp.git",
105 });
106 }),
107 );
102108
103 it("surfaces a friendly error when the pull request is not found", async () => {
104 mockedRunProcess.mockRejectedValueOnce(
105 new Error(
106 "GraphQL: Could not resolve to a PullRequest with the number of 4888. (repository.pullRequest)",
107 ),
108 );
109 it.effect("surfaces a friendly error when the pull request is not found", () =>
110 Effect.gen(function* () {
111 mockedRunProcess.mockRejectedValueOnce(
112 new Error(
113 "GraphQL: Could not resolve to a PullRequest with the number of 4888. (repository.pullRequest)",
114 ),
115 );
109116
110 const error = await Effect.runPromise(
111 Effect.gen(function* () {
117 const error = yield* Effect.gen(function* () {
112118 const gh = yield* GitHubCli;
113119 return yield* gh.getPullRequest({
114120 cwd: "/repo",
115121 reference: "4888",
116122 });
117 }).pipe(Effect.provide(GitHubCliLive), Effect.flip),
118 );
123 }).pipe(Effect.provide(GitHubCliLive), Effect.flip);
119124
120 expect(error.message).toContain("Pull request not found");
121 });
125 assert.equal(error.message.includes("Pull request not found"), true);
126 }),
127 );
122128});