Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

branch-rename.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.

branch-rename.test.tsBlame341 lines · 1 contributor
d6daf49Claude1/**
2 * Block J24 — Branch rename. Pure validation + plan tests.
3 */
4
5import { describe, it, expect } from "bun:test";
6import {
7 MAX_BRANCH_NAME_LENGTH,
8 validateBranchName,
9 branchValidationMessage,
10 planRename,
11 shouldRewriteProtectionPattern,
12 __internal,
13} from "../lib/branch-rename";
14
15describe("branch-rename — validateBranchName (valid cases)", () => {
16 it("accepts simple names", () => {
17 expect(validateBranchName("main").ok).toBe(true);
18 expect(validateBranchName("develop").ok).toBe(true);
19 expect(validateBranchName("release").ok).toBe(true);
20 });
21
22 it("accepts slash-separated components", () => {
23 expect(validateBranchName("feat/login").ok).toBe(true);
24 expect(validateBranchName("release/v1.0").ok).toBe(true);
25 expect(validateBranchName("release/v1.0.0-rc.1").ok).toBe(true);
26 });
27
28 it("accepts numeric + underscore + hyphen", () => {
29 expect(validateBranchName("123abc").ok).toBe(true);
30 expect(validateBranchName("snake_case_branch").ok).toBe(true);
31 expect(validateBranchName("kebab-case-branch").ok).toBe(true);
32 });
33
34 it("accepts names up to MAX_BRANCH_NAME_LENGTH", () => {
35 const maxName = "a".repeat(MAX_BRANCH_NAME_LENGTH);
36 expect(validateBranchName(maxName).ok).toBe(true);
37 });
38
39 it("accepts single dots inside components", () => {
40 expect(validateBranchName("v1.0.0").ok).toBe(true);
41 });
42});
43
44describe("branch-rename — validateBranchName (invalid cases)", () => {
45 it("rejects non-string", () => {
46 expect(validateBranchName(null).ok).toBe(false);
47 expect(validateBranchName(undefined).ok).toBe(false);
48 expect(validateBranchName(123).ok).toBe(false);
49 });
50
51 it("rejects empty string", () => {
52 const r = validateBranchName("");
53 expect(r.ok).toBe(false);
54 if (!r.ok) expect(r.reason).toBe("empty");
55 });
56
57 it("rejects too-long names", () => {
58 const r = validateBranchName("a".repeat(MAX_BRANCH_NAME_LENGTH + 1));
59 expect(r.ok).toBe(false);
60 if (!r.ok) expect(r.reason).toBe("too_long");
61 });
62
63 it("rejects a bare '@'", () => {
64 const r = validateBranchName("@");
65 expect(r.ok).toBe(false);
66 if (!r.ok) expect(r.reason).toBe("only_at");
67 });
68
69 it("rejects '@{'", () => {
70 const r = validateBranchName("foo@{1}");
71 expect(r.ok).toBe(false);
72 if (!r.ok) expect(r.reason).toBe("at_brace");
73 });
74
75 it("rejects leading or trailing '/'", () => {
76 expect(validateBranchName("/foo").ok).toBe(false);
77 expect(validateBranchName("foo/").ok).toBe(false);
78 });
79
80 it("rejects '//' anywhere", () => {
81 expect(validateBranchName("foo//bar").ok).toBe(false);
82 });
83
84 it("rejects leading '-'", () => {
85 const r = validateBranchName("-no");
86 expect(r.ok).toBe(false);
87 if (!r.ok) expect(r.reason).toBe("leading_dash");
88 });
89
90 it("rejects leading or trailing '.'", () => {
91 expect(validateBranchName(".foo").ok).toBe(false);
92 expect(validateBranchName("foo.").ok).toBe(false);
93 });
94
95 it("rejects '..'", () => {
96 const r = validateBranchName("foo..bar");
97 expect(r.ok).toBe(false);
98 if (!r.ok) expect(r.reason).toBe("double_dot");
99 });
100
101 it("rejects '.lock' suffix", () => {
102 expect(validateBranchName("foo.lock").ok).toBe(false);
103 });
104
105 it("rejects '.lock' on a component", () => {
106 const r = validateBranchName("release/v1.lock");
107 expect(r.ok).toBe(false);
108 if (!r.ok) {
109 // either `lock_suffix` (whole name) or `lock_component` — both valid
110 expect(["lock_suffix", "lock_component"]).toContain(r.reason);
111 }
112 });
113
114 it("rejects whitespace", () => {
115 expect(validateBranchName("has space").ok).toBe(false);
116 expect(validateBranchName("has\ttab").ok).toBe(false);
117 });
118
119 it("rejects forbidden characters", () => {
120 for (const ch of ["~", "^", ":", "?", "*", "[", "\\"]) {
121 expect(validateBranchName(`foo${ch}bar`).ok).toBe(false);
122 }
123 });
124
125 it("rejects control characters", () => {
126 expect(validateBranchName("foo\u0001bar").ok).toBe(false);
127 expect(validateBranchName("foo\u001fbar").ok).toBe(false);
128 expect(validateBranchName("foo\u007fbar").ok).toBe(false);
129 });
130
131 it("rejects component starting or ending with '.'", () => {
132 expect(validateBranchName("release/.hidden").ok).toBe(false);
133 expect(validateBranchName("release/trailing.").ok).toBe(false);
134 });
135});
136
137describe("branch-rename — branchValidationMessage", () => {
138 it("returns non-empty strings for every reason code", () => {
139 const reasons = [
140 "not_string",
141 "empty",
142 "too_long",
143 "slash_boundary",
144 "leading_dash",
145 "dot_boundary",
146 "consecutive_slashes",
147 "double_dot",
148 "at_brace",
149 "only_at",
150 "lock_suffix",
151 "forbidden_char",
152 "control_char",
153 "empty_component",
154 "dot_component",
155 "lock_component",
156 ] as const;
157 for (const r of reasons) {
158 const msg = branchValidationMessage(r);
159 expect(typeof msg).toBe("string");
160 expect(msg.length).toBeGreaterThan(0);
161 }
162 });
163});
164
165describe("branch-rename — planRename", () => {
166 const existing = ["main", "develop", "feat/login"];
167
168 it("approves a valid rename", () => {
169 const r = planRename({
170 from: "feat/login",
171 to: "feat/auth",
172 existingBranches: existing,
173 defaultBranch: "main",
174 });
175 expect(r.ok).toBe(true);
176 if (r.ok) {
177 expect(r.from).toBe("feat/login");
178 expect(r.to).toBe("feat/auth");
179 expect(r.updatesDefault).toBe(false);
180 }
181 });
182
183 it("flags rename of the default branch", () => {
184 const r = planRename({
185 from: "main",
186 to: "trunk",
187 existingBranches: existing,
188 defaultBranch: "main",
189 });
190 expect(r.ok).toBe(true);
191 if (r.ok) expect(r.updatesDefault).toBe(true);
192 });
193
194 it("rejects same-name rename", () => {
195 const r = planRename({
196 from: "main",
197 to: "main",
198 existingBranches: existing,
199 defaultBranch: "main",
200 });
201 expect(r.ok).toBe(false);
202 if (!r.ok) expect(r.reason).toBe("same_name");
203 });
204
205 it("rejects missing source branch", () => {
206 const r = planRename({
207 from: "ghost",
208 to: "new",
209 existingBranches: existing,
210 defaultBranch: "main",
211 });
212 expect(r.ok).toBe(false);
213 if (!r.ok) expect(r.reason).toBe("from_missing");
214 });
215
216 it("rejects when destination already exists", () => {
217 const r = planRename({
218 from: "feat/login",
219 to: "develop",
220 existingBranches: existing,
221 defaultBranch: "main",
222 });
223 expect(r.ok).toBe(false);
224 if (!r.ok) expect(r.reason).toBe("to_exists");
225 });
226
227 it("rejects invalid source name", () => {
228 const r = planRename({
229 from: "bad..name",
230 to: "ok",
231 existingBranches: existing,
232 defaultBranch: "main",
233 });
234 expect(r.ok).toBe(false);
235 if (!r.ok) {
236 expect(r.reason).toBe("invalid_from");
237 expect(r.detail).toBe("double_dot");
238 }
239 });
240
241 it("rejects invalid destination name", () => {
242 const r = planRename({
243 from: "main",
244 to: "has space",
245 existingBranches: existing,
246 defaultBranch: "main",
247 });
248 expect(r.ok).toBe(false);
249 if (!r.ok) {
250 expect(r.reason).toBe("invalid_to");
251 expect(r.detail).toBe("forbidden_char");
252 }
253 });
254
255 it("is case-sensitive ('Main' and 'main' are different)", () => {
256 const r = planRename({
257 from: "main",
258 to: "Main",
259 existingBranches: ["main"],
260 defaultBranch: "main",
261 });
262 expect(r.ok).toBe(true);
263 });
264
265 it("handles empty existingBranches", () => {
266 const r = planRename({
267 from: "main",
268 to: "trunk",
269 existingBranches: [],
270 defaultBranch: null,
271 });
272 expect(r.ok).toBe(false);
273 if (!r.ok) expect(r.reason).toBe("from_missing");
274 });
275
276 it("does not mark updatesDefault when defaultBranch is null", () => {
277 const r = planRename({
278 from: "main",
279 to: "trunk",
280 existingBranches: ["main"],
281 defaultBranch: null,
282 });
283 expect(r.ok).toBe(true);
284 if (r.ok) expect(r.updatesDefault).toBe(false);
285 });
286});
287
288describe("branch-rename — shouldRewriteProtectionPattern", () => {
289 it("rewrites an exact match", () => {
290 expect(shouldRewriteProtectionPattern("main", "main")).toBe(true);
291 });
292
293 it("does not rewrite a different exact", () => {
294 expect(shouldRewriteProtectionPattern("main", "develop")).toBe(false);
295 });
296
297 it("does not rewrite globs even if they match", () => {
298 expect(shouldRewriteProtectionPattern("release/*", "release/*")).toBe(
299 false
300 );
301 });
302
303 it("does not rewrite ? or [ globs", () => {
304 expect(shouldRewriteProtectionPattern("feat/?", "feat/?")).toBe(false);
305 expect(shouldRewriteProtectionPattern("feat/[abc]", "feat/[abc]")).toBe(
306 false
307 );
308 });
309});
310
311describe("branch-rename — routes", () => {
312 it("GET /settings/branches redirects unauthenticated users", async () => {
313 const { default: app } = await import("../app");
314 const res = await app.request("/alice/repo/settings/branches");
315 expect([302, 401, 403, 404]).toContain(res.status);
316 });
317
318 it("POST rename redirects unauthenticated users", async () => {
319 const { default: app } = await import("../app");
320 const form = new FormData();
321 form.append("from", "main");
322 form.append("to", "trunk");
323 const res = await app.request(
324 "/alice/repo/settings/branches/rename",
325 { method: "POST", body: form }
326 );
327 expect([302, 401, 403, 404]).toContain(res.status);
328 });
329});
330
331describe("branch-rename — __internal parity", () => {
332 it("re-exports helpers", () => {
333 expect(__internal.validateBranchName).toBe(validateBranchName);
334 expect(__internal.planRename).toBe(planRename);
335 expect(__internal.branchValidationMessage).toBe(branchValidationMessage);
336 expect(__internal.shouldRewriteProtectionPattern).toBe(
337 shouldRewriteProtectionPattern
338 );
339 expect(__internal.MAX_BRANCH_NAME_LENGTH).toBe(MAX_BRANCH_NAME_LENGTH);
340 });
341});