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