| 1 | import { afterEach, beforeEach, describe, expect, it } from "vitest"; |
| 2 | import { parseRemote } from "../src/git.js"; |
| 3 | import { fakeServer, gitRepo, isolatedEnv, run, type FakeServer } from "./helpers.js"; |
| 4 | |
| 5 | const pull = (n: number, extra: Record<string, unknown> = {}) => ({ |
| 6 | number: n, |
| 7 | title: "Add thing", |
| 8 | body: "", |
| 9 | state: "open", |
| 10 | user: { login: "alice", type: "User" }, |
| 11 | labels: [], |
| 12 | assignees: [], |
| 13 | comments: 0, |
| 14 | created_at: new Date().toISOString(), |
| 15 | updated_at: new Date().toISOString(), |
| 16 | html_url: `http://x/acme/widgets/pull/${n}`, |
| 17 | head: { ref: "feature/x", sha: "abc" }, |
| 18 | base: { ref: "main" }, |
| 19 | ...extra, |
| 20 | }); |
| 21 | |
| 22 | describe("remote parsing", () => { |
| 23 | it("recognizes https, ssh and scp-style remotes for the configured host", () => { |
| 24 | expect(parseRemote("https://diffinery.com/acme/widgets.git", "diffinery.com")).toEqual({ owner: "acme", name: "widgets" }); |
| 25 | expect(parseRemote("http://127.0.0.1:8787/acme/widgets", "127.0.0.1:8787")).toEqual({ owner: "acme", name: "widgets" }); |
| 26 | expect(parseRemote("git@diffinery.com:acme/widgets.git", "diffinery.com")).toEqual({ owner: "acme", name: "widgets" }); |
| 27 | expect(parseRemote("https://github.com/acme/widgets.git", "diffinery.com")).toBeNull(); |
| 28 | }); |
| 29 | }); |
| 30 | |
| 31 | describe("pull requests", () => { |
| 32 | let srv: FakeServer; |
| 33 | let env: NodeJS.ProcessEnv; |
| 34 | beforeEach(async () => { |
| 35 | srv = await fakeServer(); |
| 36 | env = isolatedEnv({ DIFFINERY_URL: srv.url, DIFFINERY_TOKEN: "dfy_t" }); |
| 37 | }); |
| 38 | afterEach(() => srv.close()); |
| 39 | |
| 40 | it("pr create infers the repo from origin and the head from the current branch", async () => { |
| 41 | srv.on("POST", "/api/v3/repos/acme/widgets/pulls", { status: 201, body: pull(7) }); |
| 42 | const cwd = gitRepo(env, `${srv.url}/acme/widgets.git`, "feature/x"); |
| 43 | const r = await run(["pr", "create", "--title", "Add thing", "-b", "Body text", "--base", "main", "--draft", "--intent", "Make widgets faster"], { env, cwd }); |
| 44 | expect(r.stderr).toBe(""); |
| 45 | expect(r.code).toBe(0); |
| 46 | expect(r.stdout).toContain("#7"); |
| 47 | const req = srv.find("POST", "/api/v3/repos/acme/widgets/pulls")[0]!; |
| 48 | expect(req.headers.authorization).toBe("Bearer dfy_t"); |
| 49 | expect(req.body).toEqual({ title: "Add thing", body: "Body text", head: "feature/x", base: "main", draft: true, intent: "Make widgets faster" }); |
| 50 | }); |
| 51 | |
| 52 | it("pr create with -R and --head omits unset fields", async () => { |
| 53 | srv.on("POST", "/api/v3/repos/o/r/pulls", { status: 201, body: pull(1) }); |
| 54 | const r = await run(["pr", "create", "-R", "o/r", "--title", "T", "--head", "topic"], { env }); |
| 55 | expect(r.code).toBe(0); |
| 56 | expect(srv.find("POST", "/api/v3/repos/o/r/pulls")[0]!.body).toEqual({ title: "T", head: "topic" }); |
| 57 | }); |
| 58 | |
| 59 | it("pr create requires a title", async () => { |
| 60 | const r = await run(["pr", "create", "-R", "o/r"], { env }); |
| 61 | expect(r.code).toBe(2); |
| 62 | expect(r.stderr).toContain("--title is required"); |
| 63 | }); |
| 64 | |
| 65 | it("pr merge sends the method and delete_branch", async () => { |
| 66 | srv.on("PUT", "/api/v3/repos/o/r/pulls/12/merge", { body: { merged: true, sha: "0123456789abcdef", method: "squash" } }); |
| 67 | const r = await run(["pr", "merge", "12", "-R", "o/r", "--squash", "--delete-branch"], { env }); |
| 68 | expect(r.code).toBe(0); |
| 69 | expect(r.stdout).toContain("Merged #12"); |
| 70 | expect(srv.find("PUT", "/api/v3/repos/o/r/pulls/12/merge")[0]!.body).toEqual({ merge_method: "squash", delete_branch: true }); |
| 71 | }); |
| 72 | |
| 73 | it("pr merge defaults to a merge commit and rejects conflicting flags", async () => { |
| 74 | srv.on("PUT", "/api/v3/repos/o/r/pulls/3/merge", { body: { merged: true, sha: "abc", method: "merge" } }); |
| 75 | expect((await run(["pr", "merge", "3", "-R", "o/r"], { env })).code).toBe(0); |
| 76 | expect(srv.find("PUT", "/api/v3/repos/o/r/pulls/3/merge")[0]!.body).toEqual({ merge_method: "merge" }); |
| 77 | const bad = await run(["pr", "merge", "3", "-R", "o/r", "--squash", "--rebase"], { env }); |
| 78 | expect(bad.code).toBe(2); |
| 79 | }); |
| 80 | |
| 81 | it("pr merge queues the pull request when the base branch uses a merge queue", async () => { |
| 82 | srv.on("PUT", "/api/v3/repos/o/r/pulls/6/merge", { status: 405, body: { message: "main requires a merge queue: add the pull request to the queue" } }); |
| 83 | srv.on("PUT", "/api/v3/repos/o/r/pulls/6/merge-queue", { status: 201, body: { state: "queued", entry: { position: 2 }, merge_commit_sha: null, reason: null } }); |
| 84 | const r = await run(["pr", "merge", "6", "-R", "o/r", "-d"], { env }); |
| 85 | expect(r.code).toBe(0); |
| 86 | expect(r.stdout).toContain("Added #6 to the merge queue (position 2)"); |
| 87 | expect(srv.find("PUT", "/api/v3/repos/o/r/pulls/6/merge-queue")[0]!.body).toEqual({ delete_branch: true }); |
| 88 | // Like gh, --disable-auto turns auto-merge off; a pull request already in the queue leaves it too. |
| 89 | srv.on("DELETE", "/api/v3/repos/o/r/pulls/6/auto-merge", { status: 204 }); |
| 90 | srv.on("DELETE", "/api/v3/repos/o/r/pulls/6/merge-queue", { status: 204 }); |
| 91 | const off = await run(["pr", "merge", "6", "-R", "o/r", "--disable-auto"], { env }); |
| 92 | expect(off.code).toBe(0); |
| 93 | expect(off.stdout).toContain("Turned off auto-merge for #6 and removed it from the merge queue"); |
| 94 | expect(srv.find("DELETE", "/api/v3/repos/o/r/pulls/6/auto-merge")).toHaveLength(1); |
| 95 | const admin = await run(["pr", "merge", "6", "-R", "o/r", "--admin"], { env }); |
| 96 | expect(admin.code).toBe(1); |
| 97 | expect(srv.find("PUT", "/api/v3/repos/o/r/pulls/6/merge").at(-1)!.body).toEqual({ merge_method: "merge", bypass_merge_queue: true, bypass_rules: true }); |
| 98 | }); |
| 99 | |
| 100 | it("pr merge --auto enables auto-merge like gh, and --disable-auto turns it off", async () => { |
| 101 | srv.on("PUT", "/api/v3/repos/o/r/pulls/8/auto-merge", { body: { state: "enabled", auto_merge: { merge_method: "squash", enabled_by: { login: "alice" } }, merge_commit_sha: null } }); |
| 102 | const r = await run(["pr", "merge", "8", "-R", "o/r", "--auto", "--squash", "-d", "-t", "Ship it", "--match-head-commit", "abc"], { env }); |
| 103 | expect(r.stderr).toBe(""); |
| 104 | expect(r.code).toBe(0); |
| 105 | expect(r.stdout).toContain("Auto-merge is on for #8 (squash)"); |
| 106 | expect(srv.find("PUT", "/api/v3/repos/o/r/pulls/8/auto-merge")[0]!.body).toEqual({ merge_method: "squash", delete_branch: true, commit_title: "Ship it", sha: "abc" }); |
| 107 | // It never tries a direct merge or the queue itself. |
| 108 | expect(srv.find("PUT", "/api/v3/repos/o/r/pulls/8/merge")).toHaveLength(0); |
| 109 | expect(srv.find("PUT", "/api/v3/repos/o/r/pulls/8/merge-queue")).toHaveLength(0); |
| 110 | // Without a method flag the server picks one (the queue's, or squash under linear history). |
| 111 | expect((await run(["pr", "merge", "8", "-R", "o/r", "--auto"], { env })).code).toBe(0); |
| 112 | expect(srv.find("PUT", "/api/v3/repos/o/r/pulls/8/auto-merge")[1]!.body).toEqual({}); |
| 113 | // When it is ready already, it merges (or joins the queue) at once. |
| 114 | srv.on("PUT", "/api/v3/repos/o/r/pulls/9/auto-merge", { body: { state: "merged", auto_merge: null, merge_commit_sha: "0123456789abcdef" } }); |
| 115 | expect((await run(["pr", "merge", "9", "-R", "o/r", "--auto"], { env })).stdout).toContain("Merged #9 0123456789ab"); |
| 116 | srv.on("PUT", "/api/v3/repos/o/r/pulls/10/auto-merge", { body: { state: "queued", auto_merge: null, merge_commit_sha: null } }); |
| 117 | expect((await run(["pr", "merge", "10", "-R", "o/r", "--auto"], { env })).stdout).toContain("Added #10 to the merge queue"); |
| 118 | const json = await run(["pr", "merge", "8", "-R", "o/r", "--auto", "--json"], { env }); |
| 119 | expect(JSON.parse(json.stdout).state).toBe("enabled"); |
| 120 | // --auto can't be combined with --admin (a bypass) or --disable-auto. |
| 121 | expect((await run(["pr", "merge", "8", "-R", "o/r", "--auto", "--admin"], { env })).code).toBe(2); |
| 122 | expect((await run(["pr", "merge", "8", "-R", "o/r", "--auto", "--disable-auto"], { env })).code).toBe(2); |
| 123 | // --disable-auto sends DELETE to the same path; not being in a queue is fine. |
| 124 | srv.on("DELETE", "/api/v3/repos/o/r/pulls/8/auto-merge", { status: 204 }); |
| 125 | srv.on("DELETE", "/api/v3/repos/o/r/pulls/8/merge-queue", { status: 404, body: { message: "#8 is not in the merge queue" } }); |
| 126 | const off = await run(["pr", "merge", "8", "-R", "o/r", "--disable-auto"], { env }); |
| 127 | expect(off.stderr).toBe(""); |
| 128 | expect(off.code).toBe(0); |
| 129 | expect(off.stdout).toContain("Turned off auto-merge for #8"); |
| 130 | expect(off.stdout).not.toContain("merge queue"); |
| 131 | expect(srv.find("DELETE", "/api/v3/repos/o/r/pulls/8/auto-merge")).toHaveLength(1); |
| 132 | expect(JSON.parse((await run(["pr", "merge", "8", "-R", "o/r", "--disable-auto", "--json"], { env })).stdout)).toEqual({ auto_merge: null, dequeued: false }); |
| 133 | // Other refusals still surface. |
| 134 | srv.on("DELETE", "/api/v3/repos/o/r/pulls/11/auto-merge", { status: 403, body: { message: "write access is required to disable auto-merge" } }); |
| 135 | const denied = await run(["pr", "merge", "11", "-R", "o/r", "--disable-auto"], { env }); |
| 136 | expect(denied.code).toBe(1); |
| 137 | expect(denied.stderr).toContain("write access is required"); |
| 138 | }); |
| 139 | |
| 140 | it("surfaces the server's { message } on errors", async () => { |
| 141 | srv.on("PUT", "/api/v3/repos/o/r/pulls/4/merge", { status: 405, body: { message: "pull request is not mergeable: 1 approval required" } }); |
| 142 | const r = await run(["pr", "merge", "4", "-R", "o/r"], { env }); |
| 143 | expect(r.code).toBe(1); |
| 144 | expect(r.stderr).toContain("1 approval required"); |
| 145 | }); |
| 146 | |
| 147 | it("pr review maps flags to review events", async () => { |
| 148 | srv.on("POST", "/api/v3/repos/o/r/pulls/5/reviews", { status: 201, body: { id: "rv1", state: "APPROVED" } }); |
| 149 | expect((await run(["pr", "review", "5", "-R", "o/r", "--approve"], { env })).code).toBe(0); |
| 150 | expect((await run(["pr", "review", "5", "-R", "o/r", "--request-changes", "-b", "fix it"], { env })).code).toBe(0); |
| 151 | expect((await run(["pr", "review", "5", "-R", "o/r", "--comment"], { env })).code).toBe(2); |
| 152 | expect(srv.find("POST", "/api/v3/repos/o/r/pulls/5/reviews").map((r) => r.body)).toEqual([{ event: "APPROVE" }, { event: "REQUEST_CHANGES", body: "fix it" }]); |
| 153 | }); |
| 154 | |
| 155 | it("pr list and --json", async () => { |
| 156 | srv.on("GET", "/api/v3/repos/o/r/pulls", { body: [pull(1), pull(2, { draft: true, title: "WIP" })] }); |
| 157 | const r = await run(["pr", "list", "-R", "o/r"], { env }); |
| 158 | expect(r.code).toBe(0); |
| 159 | expect(r.stdout).toMatch(/#1\s+Add thing/); |
| 160 | expect(r.stdout).toMatch(/#2\s+WIP.*draft/); |
| 161 | const j = await run(["pr", "list", "-R", "o/r", "--json"], { env }); |
| 162 | expect(JSON.parse(j.stdout)).toHaveLength(2); |
| 163 | expect(srv.find("GET", "/api/v3/repos/o/r/pulls")[0]!.query.state).toBe("open"); |
| 164 | }); |
| 165 | |
| 166 | it("pr checks exits 1 on failure", async () => { |
| 167 | srv.on("GET", "/api/v3/repos/o/r/pulls/9", { body: { ...pull(9), checks: [{ name: "ci / test", state: "failure" }, { name: "lint", state: "success" }] } }); |
| 168 | const r = await run(["pr", "checks", "9", "-R", "o/r"], { env }); |
| 169 | expect(r.code).toBe(1); |
| 170 | expect(r.stdout).toContain("ci / test"); |
| 171 | }); |
| 172 | |
| 173 | it("pr list --head only lists pull requests from that branch", async () => { |
| 174 | srv.on("GET", "/api/v3/repos/o/r/pulls", { body: [] }); |
| 175 | const r = await run(["pr", "list", "-R", "o/r", "--head", "topic"], { env }); |
| 176 | expect(r.code).toBe(0); |
| 177 | expect(r.stderr).toContain("No open pull requests from topic in o/r."); |
| 178 | expect(srv.find("GET", "/api/v3/repos/o/r/pulls")[0]!.query).toMatchObject({ head: "o:topic", state: "open" }); |
| 179 | }); |
| 180 | |
| 181 | it("pr view, checks, diff, comment, review, ready and merge default to the current branch's pull request", async () => { |
| 182 | srv.on("GET", "/api/v3/repos/acme/widgets/pulls", (req) => ({ body: req.query.head === "acme:feature/x" ? [pull(7)] : [] })); |
| 183 | srv.on("GET", "/api/v3/repos/acme/widgets/pulls/7", { body: { ...pull(7), checks: [{ name: "ci / test", state: "success" }] } }); |
| 184 | srv.on("GET", "/api/v3/repos/acme/widgets/pulls/7/files", { body: [{ filename: "a.txt", status: "modified" }] }); |
| 185 | srv.on("POST", "/api/v3/repos/acme/widgets/issues/7/comments", { status: 201, body: { id: "c1", body: "hi", user: { login: "alice" }, created_at: "" } }); |
| 186 | srv.on("POST", "/api/v3/repos/acme/widgets/pulls/7/reviews", { status: 201, body: { id: "rv1", state: "APPROVED" } }); |
| 187 | srv.on("PATCH", "/api/v3/repos/acme/widgets/pulls/7", { body: pull(7) }); |
| 188 | srv.on("PUT", "/api/v3/repos/acme/widgets/pulls/7/merge", { body: { merged: true, sha: "abc", method: "squash" } }); |
| 189 | const cwd = gitRepo(env, `${srv.url}/acme/widgets.git`, "feature/x"); |
| 190 | const view = await run(["pr", "view"], { env, cwd }); |
| 191 | expect(view.stderr).toBe(""); |
| 192 | expect(view.code).toBe(0); |
| 193 | expect(view.stdout).toContain("#7"); |
| 194 | expect(srv.find("GET", "/api/v3/repos/acme/widgets/pulls")[0]!.query).toMatchObject({ head: "acme:feature/x", state: "open" }); |
| 195 | expect((await run(["pr", "checks"], { env, cwd })).code).toBe(0); |
| 196 | expect((await run(["pr", "diff", "--name-only"], { env, cwd })).stdout).toBe("a.txt\n"); |
| 197 | expect((await run(["pr", "comment", "-b", "hi"], { env, cwd })).stdout).toContain("Commented on #7"); |
| 198 | expect((await run(["pr", "review", "--approve"], { env, cwd })).stdout).toContain("Approved #7"); |
| 199 | expect((await run(["pr", "ready"], { env, cwd })).stdout).toContain("Marked #7 ready"); |
| 200 | expect((await run(["pr", "merge", "--squash"], { env, cwd })).stdout).toContain("Merged #7"); |
| 201 | // Usage errors are reported before any lookup. |
| 202 | const before = srv.requests.length; |
| 203 | expect((await run(["pr", "merge", "--auto", "--admin"], { env, cwd })).code).toBe(2); |
| 204 | expect((await run(["pr", "comment"], { env, cwd, stdinTTY: true })).code).toBe(2); |
| 205 | expect(srv.requests.length).toBe(before); |
| 206 | }); |
| 207 | |
| 208 | it("pr view shows auto-merge and, for an agent, what is left before it lands", async () => { |
| 209 | const detail = { mergeable: false, mergeable_state: "blocked", merge_blockers: ["needs 1 more approving review(s)", "required check 'ci' is in_progress"], approvals: 0, required_approvals: 1, checks: [] }; |
| 210 | srv.on("GET", "/api/v3/repos/o/r/pulls/3", { body: { ...pull(3), ...detail, auto_merge: { merge_method: "squash" }, auto_merge_blockers: ["required check 'ci' is in_progress"] } }); |
| 211 | srv.on("GET", "/api/v3/repos/o/r/pulls/4", { body: { ...pull(4), ...detail, auto_merge: { merge_method: "merge" }, auto_merge_blockers: [] } }); |
| 212 | srv.on("GET", "/api/v3/repos/o/r/pulls/5", { body: { ...pull(5), ...detail, auto_merge: { merge_method: "rebase" } } }); |
| 213 | srv.on("GET", "/api/v3/repos/o/r/pulls/6", { body: { ...pull(6), ...detail, auto_merge: null } }); |
| 214 | const agent = (await run(["pr", "view", "3", "-R", "o/r"], { env })).stdout; |
| 215 | expect(agent).toContain("auto-merge: on (squash), it merges by itself once these are done:\n • required check 'ci' is in_progress\n"); |
| 216 | expect((await run(["pr", "view", "4", "-R", "o/r"], { env })).stdout).toContain("auto-merge: on (merge), nothing is left, it merges now"); |
| 217 | expect((await run(["pr", "view", "5", "-R", "o/r"], { env })).stdout).toContain("auto-merge: on (rebase), it merges by itself once every requirement is met"); |
| 218 | expect((await run(["pr", "view", "6", "-R", "o/r"], { env })).stdout).not.toContain("auto-merge"); |
| 219 | }); |
| 220 | |
| 221 | it("without a number, fails clearly when there is no current branch or no open pull request for it", async () => { |
| 222 | srv.on("GET", "/api/v3/repos/acme/widgets/pulls", { body: [] }); |
| 223 | const cwd = gitRepo(env, `${srv.url}/acme/widgets.git`, "lonely"); |
| 224 | const none = await run(["pr", "checks"], { env, cwd }); |
| 225 | expect(none.code).toBe(1); |
| 226 | expect(none.stderr).toContain("no open pull request for the current branch (lonely) in acme/widgets; pass a pull request number"); |
| 227 | const outside = await run(["pr", "view", "-R", "o/r"], { env }); |
| 228 | expect(outside.code).toBe(1); |
| 229 | expect(outside.stderr).toContain("no current branch"); |
| 230 | expect(outside.stderr).toContain("pass a pull request number"); |
| 231 | }); |
| 232 | |
| 233 | it("pr checks --watch polls until nothing is pending", async () => { |
| 234 | const states = [["queued", "queued"], ["in_progress", "success"], ["in_progress", "success"], ["failure", "success"]]; |
| 235 | let i = 0; |
| 236 | srv.on("GET", "/api/v3/repos/o/r/pulls/9", () => { |
| 237 | const [a, b] = states[Math.min(i++, states.length - 1)]!; |
| 238 | return { body: { ...pull(9), checks: [{ name: "ci / test", state: a }, { name: "lint", state: b }] } }; |
| 239 | }); |
| 240 | // Piped: one table, the final one. |
| 241 | const r = await run(["pr", "checks", "9", "-R", "o/r", "--watch", "-i", "0"], { env }); |
| 242 | expect(r.code).toBe(1); |
| 243 | expect(srv.find("GET", "/api/v3/repos/o/r/pulls/9")).toHaveLength(4); |
| 244 | expect(r.stdout.match(/ci \/ test/g)).toHaveLength(1); |
| 245 | expect(r.stdout).toMatch(/ci \/ test\s+failure/); |
| 246 | // On a terminal: the table each time it changes. |
| 247 | i = 0; |
| 248 | const tty = await run(["pr", "checks", "9", "-R", "o/r", "--watch", "--interval", "0"], { env: { ...env, NO_COLOR: "1" }, stdoutTTY: true }); |
| 249 | expect(tty.code).toBe(1); |
| 250 | expect(tty.stdout.match(/ci \/ test/g)).toHaveLength(3); |
| 251 | expect(tty.stdout).toMatch(/ci \/ test\s+queued[\s\S]*ci \/ test\s+in progress[\s\S]*ci \/ test\s+failure/); |
| 252 | // --json prints only the final list, and all passing exits 0. |
| 253 | states.splice(3, 1, ["success", "skipped"]); |
| 254 | i = 0; |
| 255 | const j = await run(["pr", "checks", "9", "-R", "o/r", "--watch", "-i", "0", "--json"], { env }); |
| 256 | expect(j.code).toBe(0); |
| 257 | expect(JSON.parse(j.stdout)).toEqual([{ name: "ci / test", state: "success" }, { name: "lint", state: "skipped" }]); |
| 258 | // Without --watch, pending checks exit 8. |
| 259 | i = 0; |
| 260 | expect((await run(["pr", "checks", "9", "-R", "o/r"], { env })).code).toBe(8); |
| 261 | }); |
| 262 | |
| 263 | it("pr checks --watch waits for the first check right after a push, and gives up after 2 minutes", async () => { |
| 264 | let i = 0; |
| 265 | srv.on("GET", "/api/v3/repos/o/r/pulls/9", () => ({ body: { ...pull(9), checks: i++ < 2 ? [] : [{ name: "ci / test", state: i < 4 ? "in_progress" : "success" }] } })); |
| 266 | const r = await run(["pr", "checks", "9", "-R", "o/r", "--watch", "-i", "0"], { env }); |
| 267 | expect(r.code).toBe(0); |
| 268 | expect(srv.find("GET", "/api/v3/repos/o/r/pulls/9")).toHaveLength(4); |
| 269 | expect(r.stdout).toMatch(/ci \/ test\s+success/); |
| 270 | // A repository without CI: no checks ever appear, so it stops after 2 minutes of polls (1 s each at -i 0). |
| 271 | srv.on("GET", "/api/v3/repos/o/r/pulls/10", { body: { ...pull(10), checks: [] } }); |
| 272 | const none = await run(["pr", "checks", "10", "-R", "o/r", "--watch", "-i", "0"], { env }); |
| 273 | expect(none.code).toBe(0); |
| 274 | expect(none.stderr).toContain("No checks reported"); |
| 275 | expect(srv.find("GET", "/api/v3/repos/o/r/pulls/10")).toHaveLength(121); |
| 276 | }); |
| 277 | |
| 278 | it("pr comment posts to the issue comments endpoint", async () => { |
| 279 | srv.on("POST", "/api/v3/repos/o/r/issues/9/comments", { status: 201, body: { id: "c1", body: "hi", user: { login: "alice" }, created_at: "" } }); |
| 280 | expect((await run(["pr", "comment", "9", "-R", "o/r", "-b", "hi"], { env })).code).toBe(0); |
| 281 | expect(srv.find("POST", "/api/v3/repos/o/r/issues/9/comments")[0]!.body).toEqual({ body: "hi" }); |
| 282 | }); |
| 283 | }); |