1import { describe, expect, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import { ChildProcessSpawner } from "effect/process";
6
7import { gitDiffFileStats, githubRepository } from "./JjStackService.ts";
8import * as JjStackService from "./JjStackService.ts";
9import { VcsProcess } from "./VcsProcess.ts";
10
11describe("gitDiffFileStats", () => {
12 it("counts lines per file and keeps rename sources", () => {
13 const diff = [
14 "diff --git a/src/old.ts b/src/new.ts",
15 "rename from src/old.ts",
16 "rename to src/new.ts",
17 "--- a/src/old.ts",
18 "+++ b/src/new.ts",
19 "@@ -1,2 +1,2 @@",
20 "-const a = 1;",
21 "+const a = 2;",
22 " export { a };",
23 "diff --git a/README.md b/README.md",
24 "new file mode 100644",
25 "--- /dev/null",
26 "+++ b/README.md",
27 "@@ -0,0 +1,2 @@",
28 "+# Title",
29 "+--- a line that looks like a header",
30 ].join("\n");
31 expect(gitDiffFileStats(diff)).toEqual([
32 { path: "src/new.ts", previousPath: "src/old.ts", additions: 1, deletions: 1 },
33 { path: "README.md", previousPath: null, additions: 2, deletions: 0 },
34 ]);
35 });
36});
37
38describe("githubRepository", () => {
39 const remotes = [
40 "origin git@github.com:acme/app.git",
41 "upstream https://github.com/acme/upstream",
42 "mirror https://git.example.com/acme/app.git",
43 "lookalike https://notgithub.com/acme/app.git",
44 ].join("\n");
45
46 it("reads owner/name from the push remote's GitHub URL", () => {
47 expect(githubRepository(remotes, "origin")).toBe("acme/app");
48 expect(githubRepository(remotes, "upstream")).toBe("acme/upstream");
49 });
50
51 it("returns null for other hosts and unknown remotes", () => {
52 expect(githubRepository(remotes, "mirror")).toBeNull();
53 expect(githubRepository(remotes, "lookalike")).toBeNull();
54 expect(githubRepository(remotes, "fork")).toBeNull();
55 });
56});
57
58const pushLayer = (bookmarks: string) =>
59 JjStackService.layer.pipe(
60 Layer.provide(
61 Layer.succeed(
62 VcsProcess,
63 VcsProcess.of({
64 run: (input) =>
65 Effect.succeed({
66 exitCode: ChildProcessSpawner.ExitCode(0),
67 stdout:
68 input.operation === "JjStackService.push-template"
69 ? '"review-" ++ change_id.short()'
70 : input.operation === "JjStackService.bookmark-name"
71 ? "review-selected"
72 : input.operation === "JjStackService.pushed-bookmark"
73 ? bookmarks
74 : "",
75 stderr: "",
76 stdoutTruncated: false,
77 stderrTruncated: false,
78 }),
79 }),
80 ),
81 ),
82 Layer.provide(NodeServices.layer),
83 );
84
85describe("JjStackService.push", () => {
86 it.effect("returns the bookmark it pushed when other bookmarks point at the same commit", () =>
87 Effect.gen(function* () {
88 const service = yield* JjStackService.JjStackService;
89 const result = yield* service.push({ cwd: "/workspace", changeId: "selected" });
90 expect(result).toEqual({ bookmark: "review-selected", remote: "origin" });
91 }).pipe(Effect.provide(pushLayer("another-pr\torigin\nreview-selected\torigin"))),
92 );
93
94 it.effect("refuses to report success when only another bookmark is pushed", () =>
95 Effect.gen(function* () {
96 const service = yield* JjStackService.JjStackService;
97 const result = yield* service
98 .push({ cwd: "/workspace", changeId: "selected" })
99 .pipe(Effect.flip);
100 expect(result).toMatchObject({ _tag: "JjError", operation: "push" });
101 }).pipe(Effect.provide(pushLayer("another-pr\torigin"))),
102 );
103});