Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
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.

pr-auto-merge.test.tsBlame161 lines · 1 contributor
21ff9beClaude1/**
2 * Block J16 — PR auto-merge. Pure state-machine + route-auth smokes.
3 */
4
5import { describe, it, expect } from "bun:test";
6import app from "../app";
7import {
8 MERGE_METHODS,
9 isValidMergeMethod,
10 computeAutoMergeAction,
11 __internal,
12} from "../lib/pr-auto-merge";
13
14describe("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
28describe("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
125describe("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
155describe("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});