CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
pr-auto-merge.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 21ff9be | 1 | /** |
| 2 | * Block J16 — PR auto-merge. Pure state-machine + route-auth smokes. | |
| 3 | */ | |
| 4 | ||
| 5 | import { describe, it, expect } from "bun:test"; | |
| 6 | import app from "../app"; | |
| 7 | import { | |
| 8 | MERGE_METHODS, | |
| 9 | isValidMergeMethod, | |
| 10 | computeAutoMergeAction, | |
| 11 | __internal, | |
| 12 | } from "../lib/pr-auto-merge"; | |
| 13 | ||
| 14 | describe("pr-auto-merge — isValidMergeMethod", () => { | |
| 15 | it("accepts the three canonical methods", () => { | |
| 16 | for (const m of MERGE_METHODS) expect(isValidMergeMethod(m)).toBe(true); | |
| 17 | }); | |
| 18 | ||
| 19 | it("rejects unknown methods + non-strings", () => { | |
| 20 | expect(isValidMergeMethod("fast-forward")).toBe(false); | |
| 21 | expect(isValidMergeMethod("")).toBe(false); | |
| 22 | expect(isValidMergeMethod(null)).toBe(false); | |
| 23 | expect(isValidMergeMethod(undefined)).toBe(false); | |
| 24 | expect(isValidMergeMethod(42)).toBe(false); | |
| 25 | }); | |
| 26 | }); | |
| 27 | ||
| 28 | describe("pr-auto-merge — computeAutoMergeAction", () => { | |
| 29 | const base = { | |
| 30 | autoMergeEnabled: true, | |
| 31 | prState: "open", | |
| 32 | isDraft: false, | |
| 33 | combinedState: "success" as const, | |
| 34 | totalChecks: 3, | |
| 35 | }; | |
| 36 | ||
| 37 | it("skips when auto-merge is not enabled", () => { | |
| 38 | const r = computeAutoMergeAction({ ...base, autoMergeEnabled: false }); | |
| 39 | expect(r.action).toBe("skip"); | |
| 40 | expect(r.reason).toBe("not_enabled"); | |
| 41 | }); | |
| 42 | ||
| 43 | it("skips when the PR is not open", () => { | |
| 44 | expect( | |
| 45 | computeAutoMergeAction({ ...base, prState: "closed" }).reason | |
| 46 | ).toBe("pr_closed"); | |
| 47 | expect( | |
| 48 | computeAutoMergeAction({ ...base, prState: "merged" }).reason | |
| 49 | ).toBe("pr_closed"); | |
| 50 | }); | |
| 51 | ||
| 52 | it("skips draft PRs", () => { | |
| 53 | const r = computeAutoMergeAction({ ...base, isDraft: true }); | |
| 54 | expect(r.action).toBe("skip"); | |
| 55 | expect(r.reason).toBe("pr_draft"); | |
| 56 | }); | |
| 57 | ||
| 58 | it("waits when no checks have reported yet", () => { | |
| 59 | const r = computeAutoMergeAction({ | |
| 60 | ...base, | |
| 61 | combinedState: null, | |
| 62 | totalChecks: 0, | |
| 63 | }); | |
| 64 | expect(r.action).toBe("wait"); | |
| 65 | expect(r.reason).toBe("no_checks"); | |
| 66 | }); | |
| 67 | ||
| 68 | it("waits when combined state is pending", () => { | |
| 69 | const r = computeAutoMergeAction({ | |
| 70 | ...base, | |
| 71 | combinedState: "pending", | |
| 72 | totalChecks: 2, | |
| 73 | }); | |
| 74 | expect(r.action).toBe("wait"); | |
| 75 | expect(r.reason).toBe("checks_pending"); | |
| 76 | }); | |
| 77 | ||
| 78 | it("skips on any failure/error", () => { | |
| 79 | expect( | |
| 80 | computeAutoMergeAction({ ...base, combinedState: "failure" }).reason | |
| 81 | ).toBe("checks_failed"); | |
| 82 | expect( | |
| 83 | computeAutoMergeAction({ ...base, combinedState: "error" }).reason | |
| 84 | ).toBe("checks_failed"); | |
| 85 | }); | |
| 86 | ||
| 87 | it("merges when combined state is success and checks > 0", () => { | |
| 88 | const r = computeAutoMergeAction(base); | |
| 89 | expect(r.action).toBe("merge"); | |
| 90 | expect(r.reason).toBe("checks_passed"); | |
| 91 | }); | |
| 92 | ||
| 93 | it("waits (not merges) when combined state is success but totalChecks is 0", () => { | |
| 94 | // Defensive — the "success with zero checks" combined output means | |
| 95 | // the reducer returned success for an empty list. We should not flip | |
| 96 | // to merge in that case. | |
| 97 | const r = computeAutoMergeAction({ | |
| 98 | ...base, | |
| 99 | combinedState: "success", | |
| 100 | totalChecks: 0, | |
| 101 | }); | |
| 102 | expect(r.action).toBe("wait"); | |
| 103 | expect(r.reason).toBe("no_checks"); | |
| 104 | }); | |
| 105 | ||
| 106 | it("draft check beats checks failure — still skip as draft", () => { | |
| 107 | const r = computeAutoMergeAction({ | |
| 108 | ...base, | |
| 109 | isDraft: true, | |
| 110 | combinedState: "failure", | |
| 111 | }); | |
| 112 | expect(r.reason).toBe("pr_draft"); | |
| 113 | }); | |
| 114 | ||
| 115 | it("disabled beats draft — still not_enabled", () => { | |
| 116 | const r = computeAutoMergeAction({ | |
| 117 | ...base, | |
| 118 | autoMergeEnabled: false, | |
| 119 | isDraft: true, | |
| 120 | }); | |
| 121 | expect(r.reason).toBe("not_enabled"); | |
| 122 | }); | |
| 123 | }); | |
| 124 | ||
| 125 | describe("pr-auto-merge — routes", () => { | |
| 126 | it("POST /:o/:r/pulls/:n/auto-merge requires auth", async () => { | |
| 127 | const res = await app.request( | |
| 128 | "/alice/nope/pulls/1/auto-merge", | |
| 129 | { method: "POST", body: "mergeMethod=merge" } | |
| 130 | ); | |
| 131 | expect([302, 401, 404].includes(res.status)).toBe(true); | |
| 132 | }); | |
| 133 | ||
| 134 | it("POST .../auto-merge/disable requires auth", async () => { | |
| 135 | const res = await app.request( | |
| 136 | "/alice/nope/pulls/1/auto-merge/disable", | |
| 137 | { method: "POST" } | |
| 138 | ); | |
| 139 | expect([302, 401, 404].includes(res.status)).toBe(true); | |
| 140 | }); | |
| 141 | ||
| 142 | it("POST with invalid bearer → 401 JSON", async () => { | |
| 143 | const res = await app.request( | |
| 144 | "/alice/nope/pulls/1/auto-merge", | |
| 145 | { | |
| 146 | method: "POST", | |
| 147 | headers: { authorization: "Bearer glc_garbage" }, | |
| 148 | body: "mergeMethod=merge", | |
| 149 | } | |
| 150 | ); | |
| 151 | expect(res.status).toBe(401); | |
| 152 | }); | |
| 153 | }); | |
| 154 | ||
| 155 | describe("pr-auto-merge — __internal", () => { | |
| 156 | it("exposes the pure helpers for parity", () => { | |
| 157 | expect(__internal.computeAutoMergeAction).toBe(computeAutoMergeAction); | |
| 158 | expect(__internal.isValidMergeMethod).toBe(isValidMergeMethod); | |
| 159 | expect(__internal.MERGE_METHODS).toBe(MERGE_METHODS); | |
| 160 | }); | |
| 161 | }); |