Skip to content

Commit ca140c4

Browse files
refactor(source-control): GitCafe creates repositories and pull requests over REST (#22495)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4c48118 commit ca140c4

2 files changed

Lines changed: 170 additions & 122 deletions

File tree

‎packages/source-control-gitcafe/src/server/GitCafeSourceControlProvider.test.ts‎

Lines changed: 112 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { SourceControlProviderKind } from "@t3tools/contracts";
22
import { assert, describe, it } from "@effect/vitest";
33
import { GitCommandError } from "@t3tools/contracts";
44
import * as Effect from "effect/Effect";
5+
import * as FileSystem from "effect/FileSystem";
56
import * as Fiber from "effect/Fiber";
67
import * as Layer from "effect/Layer";
78
import * as Option from "effect/Option";
@@ -18,6 +19,7 @@ import * as GitCafeCredentials from "./GitCafeCredentials.ts";
1819
import * as GitCafeSourceControlProvider from "./GitCafeSourceControlProvider.ts";
1920

2021
const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
22+
const decodeJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
2123

2224
const authAnswer = (input: { stdout?: unknown; stderr?: unknown; exitCode?: number }) => ({
2325
stdout: input.stdout === undefined ? "" : encodeJson(input.stdout),
@@ -29,6 +31,36 @@ const failure = (code: string, status: number | null) => ({
2931
error: { code, status, message: `Failure ${code}` },
3032
});
3133

34+
/** Most calls touch no file; one that does fails the test. */
35+
const NoFiles = FileSystem.layerNoop({});
36+
37+
/** A provider on git.cafe whose checkout has no remote and which may not run `cafe`. */
38+
const layerFor = (
39+
client: HttpClient.HttpClient,
40+
options: { readonly files?: Layer.Layer<FileSystem.FileSystem> } = {},
41+
) =>
42+
Layer.merge(GitCafeApi.layer, options.files ?? NoFiles).pipe(
43+
Layer.provideMerge(GitCafeCredentials.layer),
44+
Layer.provideMerge(
45+
TestSourceControlHost.layer({
46+
git: {
47+
resolvePrimaryRemoteName: () =>
48+
Effect.fail(
49+
new GitCommandError({
50+
operation: "resolvePrimaryRemoteName",
51+
command: "git remote",
52+
cwd: "/repo",
53+
detail: "No git remote is configured.",
54+
}),
55+
),
56+
},
57+
}),
58+
),
59+
Layer.provide(Layer.succeed(HostProcess.Environment, { CAFE_TOKEN: "env-token" })),
60+
Layer.provide(Layer.succeed(HostProcess.WorkingDirectory, "/server")),
61+
Layer.provide(Layer.succeed(HttpClient.HttpClient, client)),
62+
);
63+
3264
describe("GitCafeSourceControlProvider", () => {
3365
it("reads a signed-in account and keeps transient failures apart from sign-out", () => {
3466
const { parseAuth } = GitCafeSourceControlProvider;
@@ -94,7 +126,7 @@ describe("GitCafeSourceControlProvider", () => {
94126
]);
95127
}).pipe(
96128
Effect.provide(
97-
GitCafeApi.layer.pipe(
129+
Layer.merge(GitCafeApi.layer, NoFiles).pipe(
98130
Layer.provideMerge(GitCafeCredentials.layer),
99131
Layer.provideMerge(TestSourceControlHost.layer()),
100132
Layer.provide(
@@ -140,7 +172,7 @@ describe("GitCafeSourceControlProvider", () => {
140172
assert.deepStrictEqual(urls, ["https://git.cafe/api/repos/team/project"]);
141173
}).pipe(
142174
Effect.provide(
143-
GitCafeApi.layer.pipe(
175+
Layer.merge(GitCafeApi.layer, NoFiles).pipe(
144176
Layer.provideMerge(GitCafeCredentials.layer),
145177
Layer.provideMerge(
146178
TestSourceControlHost.layer({
@@ -222,7 +254,7 @@ describe("GitCafeSourceControlProvider", () => {
222254
assert.deepStrictEqual(remotes, [expected]);
223255
}).pipe(
224256
Effect.provide(
225-
GitCafeApi.layer.pipe(
257+
Layer.merge(GitCafeApi.layer, NoFiles).pipe(
226258
Layer.provideMerge(GitCafeCredentials.layer),
227259
Layer.provideMerge(
228260
TestSourceControlHost.layer({
@@ -246,22 +278,19 @@ describe("GitCafeSourceControlProvider", () => {
246278
);
247279
});
248280

249-
it.effect("waits for a created repository's admission before handing out its URLs", () => {
250-
const urls: Array<string> = [];
251-
const states = ["running", "complete"];
281+
it.effect("creates a repository over REST and waits for its admission", () => {
282+
const sent: Array<{ method: string; url: string; body: unknown }> = [];
252283
const client = HttpClient.make((request) => {
253-
urls.push(request.url);
284+
const body =
285+
request.body._tag === "Uint8Array"
286+
? decodeJson(new TextDecoder().decode(request.body.body))
287+
: undefined;
288+
sent.push({ method: request.method, url: request.url, body });
289+
const state = request.method === "POST" ? "running" : "complete";
254290
return Effect.succeed(
255291
HttpClientResponse.fromWeb(
256292
request,
257-
new Response(
258-
encodeJson({
259-
repoId: "repo_1",
260-
state: states.shift(),
261-
owner: "team",
262-
name: "project",
263-
}),
264-
),
293+
new Response(encodeJson({ repoId: "repo_1", state, owner: "team", name: "project" })),
265294
),
266295
);
267296
});
@@ -276,51 +305,76 @@ describe("GitCafeSourceControlProvider", () => {
276305
url: "https://git.cafe/team/project",
277306
sshUrl: "ssh@git.cafe:team/project.git",
278307
});
279-
assert.deepStrictEqual(urls, [
280-
"https://git.cafe/api/orgs/team/admissions/repo_1",
281-
"https://git.cafe/api/orgs/team/admissions/repo_1",
282-
]);
283-
}).pipe(
284-
Effect.provide(
285-
GitCafeApi.layer.pipe(
286-
Layer.provideMerge(GitCafeCredentials.layer),
287-
Layer.provideMerge(
288-
TestSourceControlHost.layer({
289-
process: {
290-
run: () =>
291-
Effect.succeed(
292-
TestSourceControlHost.processOutput(
293-
encodeJson({
294-
schemaVersion: 1,
295-
data: {
296-
resource: {
297-
repoId: "repo_1",
298-
state: "running",
299-
owner: "team",
300-
name: "project",
301-
},
302-
},
303-
}),
304-
),
305-
),
306-
},
307-
git: {
308-
resolvePrimaryRemoteName: () =>
309-
Effect.fail(
310-
new GitCommandError({
311-
operation: "resolvePrimaryRemoteName",
312-
command: "git remote",
313-
cwd: "/repo",
314-
detail: "No git remote is configured.",
315-
}),
316-
),
317-
},
318-
}),
308+
assert.deepStrictEqual(
309+
sent.map((request) => `${request.method} ${request.url}`),
310+
[
311+
"POST https://git.cafe/api/orgs/team/repos",
312+
"GET https://git.cafe/api/orgs/team/admissions/repo_1",
313+
],
314+
);
315+
const { requestKey, ...body } = sent[0]?.body as { requestKey: string };
316+
assert.match(requestKey, /^[0-9a-f-]{36}$/u);
317+
assert.deepStrictEqual(body, {
318+
name: "project",
319+
visibility: "private",
320+
defaultBranch: "main",
321+
});
322+
}).pipe(Effect.provide(layerFor(client)));
323+
});
324+
325+
it.effect("opens a pull request from a fork over REST with the body file's text", () => {
326+
const sent: Array<{ method: string; url: string; body: unknown }> = [];
327+
const client = HttpClient.make((request) => {
328+
const body =
329+
request.body._tag === "Uint8Array"
330+
? decodeJson(new TextDecoder().decode(request.body.body))
331+
: undefined;
332+
sent.push({ method: request.method, url: request.url, body });
333+
return Effect.succeed(
334+
HttpClientResponse.fromWeb(
335+
request,
336+
new Response(
337+
encodeJson(
338+
request.method === "POST"
339+
? { number: 3 }
340+
: { id: "repo_fork", name: "project", defaultBranch: "main" },
341+
),
319342
),
320-
Layer.provide(Layer.succeed(HostProcess.Environment, { CAFE_TOKEN: "env-token" })),
321-
Layer.provide(Layer.succeed(HostProcess.WorkingDirectory, "/server")),
322-
Layer.provide(Layer.succeed(HttpClient.HttpClient, client)),
323343
),
344+
);
345+
});
346+
return Effect.gen(function* () {
347+
const provider = yield* GitCafeSourceControlProvider.make;
348+
yield* provider.createChangeRequest({
349+
cwd: "/repo",
350+
target: { repository: "team/project", refName: "main" },
351+
baseRefName: "main",
352+
headSelector: "fork:feature",
353+
title: "Add a thing",
354+
bodyFile: "/tmp/body.md",
355+
});
356+
assert.deepStrictEqual(
357+
sent.map((request) => `${request.method} ${request.url}`),
358+
[
359+
"GET https://git.cafe/api/repos/fork/project",
360+
"POST https://git.cafe/api/repos/team/project/pulls",
361+
],
362+
);
363+
assert.deepStrictEqual(sent[1]?.body, {
364+
repo: "repo_fork",
365+
source: "feature",
366+
target: "main",
367+
title: "Add a thing",
368+
description: "Body",
369+
});
370+
}).pipe(
371+
Effect.provide(
372+
layerFor(client, {
373+
files: FileSystem.layerNoop({
374+
readFileString: (path) =>
375+
path === "/tmp/body.md" ? Effect.succeed("Body") : Effect.die(path),
376+
}),
377+
}),
324378
),
325379
);
326380
});

0 commit comments

Comments
 (0)