Skip to content
Source
Sign inSign up

BranchToolbar.logic.test.ts

329 lines10.1 KB

1import { ProviderInstanceId, RunId, type VcsRef } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3import { dedupeRemoteBranchesWithLocalMatches } from "@t3tools/shared/git";
4import {
5 deriveLocalBranchNameFromRemoteRef,
6 isWorktreeChangeBlocked,
7 resolveBranchSelectionTarget,
8 resolveDraftEnvModeAfterBranchChange,
9 resolveEffectiveEnvMode,
10 resolveBranchToolbarPrBranch,
11 resolveLocalCheckoutBranchMismatch,
12 resolveLockedWorkspaceLabel,
13 sanitizeNewRefName,
14 shouldIncludeBranchPickerItem,
15} from "./BranchToolbar.logic";
16​
17describe("resolveDraftEnvModeAfterBranchChange", () => {
18 it("switches to local mode when returning from an existing worktree to the main worktree", () => {
19 expect(
20 resolveDraftEnvModeAfterBranchChange({
21 nextWorktreePath: null,
22 currentWorktreePath: "/repo/.t3/worktrees/feature-a",
23 effectiveEnvMode: "worktree",
24 }),
25 ).toBe("local");
26 });
27​
28 it("uses worktree mode when selecting a ref already attached to a worktree", () => {
29 expect(
30 resolveDraftEnvModeAfterBranchChange({
31 nextWorktreePath: "/repo/.t3/worktrees/feature-a",
32 currentWorktreePath: null,
33 effectiveEnvMode: "local",
34 }),
35 ).toBe("worktree");
36 });
37});
38​
39describe("resolveBranchToolbarPrBranch", () => {
40 it("hides PR state while an optimistic branch switch is in flight", () => {
41 expect(
42 resolveBranchToolbarPrBranch({
43 activeThreadBranch: "feature/current",
44 resolvedActiveBranch: "feature/next",
45 }),
46 ).toBeNull();
47 });
48});
49​
50describe("resolveLocalCheckoutBranchMismatch", () => {
51 it("detects when a local thread is associated with a different branch than the checkout", () => {
52 expect(
53 resolveLocalCheckoutBranchMismatch({
54 effectiveEnvMode: "local",
55 activeWorktreePath: null,
56 activeThreadBranch: "feature/thread",
57 currentGitBranch: "feature/current",
58 }),
59 ).toEqual({
60 threadBranch: "feature/thread",
61 currentBranch: "feature/current",
62 });
63 });
64​
65 it("ignores dedicated worktrees because their checkout is already thread-scoped", () => {
66 expect(
67 resolveLocalCheckoutBranchMismatch({
68 effectiveEnvMode: "worktree",
69 activeWorktreePath: "/repo/.t3/worktrees/feature-thread",
70 activeThreadBranch: "feature/thread",
71 currentGitBranch: "feature/current",
72 }),
73 ).toBeNull();
74 });
75});
76​
77describe("resolveEffectiveEnvMode", () => {
78 it("keeps a server thread in worktree mode while its worktree is still being created", () => {
79 expect(
80 resolveEffectiveEnvMode({
81 activeWorktreePath: null,
82 hasServerThread: true,
83 draftThreadEnvMode: undefined,
84 preparingWorktree: true,
85 }),
86 ).toBe("worktree");
87 expect(
88 resolveEffectiveEnvMode({
89 activeWorktreePath: null,
90 hasServerThread: true,
91 draftThreadEnvMode: undefined,
92 }),
93 ).toBe("local");
94 });
95});
96​
97describe("resolveLockedWorkspaceLabel", () => {
98 it.each([
99 ["/home/me/.t3/worktrees/app/t3-ff466c42", "t3-ff466c42"],
100 ["/home/me/.t3/worktrees/app/t3-ff466c42/", "t3-ff466c42"],
101 ["C:\\Users\\me\\worktrees\\feature-x", "feature-x"],
102 ])("names the worktree at %s", (path, label) => {
103 expect(resolveLockedWorkspaceLabel(path, "worktree")).toBe(label);
104 });
105});
106​
107describe("deriveLocalBranchNameFromRemoteRef", () => {
108 it("supports remote names that contain slashes", () => {
109 expect(deriveLocalBranchNameFromRemoteRef("my-org/upstream/feature/demo")).toBe(
110 "upstream/feature/demo",
111 );
112 });
113});
114​
115describe("dedupeRemoteBranchesWithLocalMatches", () => {
116 it.each([
117 ["feature/demo", "origin/feature/demo", true],
118 ["feature/demo", "feature/demo", false],
119 ["demo", "demo", true],
120 ["origin/", "origin/", true],
121 ["/demo", "/demo", true],
122 ["", "", false],
123 ])("filters origin ref %s / %s (hidden: %s)", (localName, remoteName, hidden) => {
124 const input: VcsRef[] = [
125 {
126 name: localName,
127 current: false,
128 isDefault: false,
129 worktreePath: null,
130 },
131 {
132 name: remoteName,
133 isRemote: true,
134 remoteName: "origin",
135 current: false,
136 isDefault: false,
137 worktreePath: null,
138 },
139 {
140 name: "origin/feature/remote-only",
141 isRemote: true,
142 remoteName: "origin",
143 current: false,
144 isDefault: false,
145 worktreePath: null,
146 },
147 ];
148​
149 const result = dedupeRemoteBranchesWithLocalMatches(input);
150 const expected = hidden ? [input[0], input[2]] : input;
151 expect(result).toEqual(expected);
152 for (const [index, ref] of result.entries()) {
153 expect(ref).toBe(expected[index]);
154 }
155 });
156​
157 it("keeps non-origin remote refs visible when git tracks with first-slash local naming", () => {
158 const input: VcsRef[] = [
159 {
160 name: "upstream/feature",
161 current: false,
162 isDefault: false,
163 worktreePath: null,
164 },
165 {
166 name: "my-org/upstream/feature",
167 isRemote: true,
168 remoteName: "my-org/upstream",
169 current: false,
170 isDefault: false,
171 worktreePath: null,
172 },
173 ];
174​
175 expect(dedupeRemoteBranchesWithLocalMatches(input).map((ref) => ref.name)).toEqual([
176 "upstream/feature",
177 "my-org/upstream/feature",
178 ]);
179 });
180});
181​
182describe("resolveBranchSelectionTarget", () => {
183 it("reuses an existing secondary worktree for the selected ref", () => {
184 expect(
185 resolveBranchSelectionTarget({
186 activeProjectCwd: "/repo/nested-checkout/packages/app",
187 activeProjectCheckoutRoot: "/repo/nested-checkout",
188 activeWorktreePath: null,
189 refName: { isDefault: true, worktreePath: "/repo" },
190 }),
191 ).toEqual({
192 checkoutCwd: "/repo",
193 nextWorktreePath: "/repo",
194 reuseExistingWorktree: true,
195 });
196 expect(
197 resolveBranchSelectionTarget({
198 activeProjectCwd: "/repo",
199 activeWorktreePath: "/repo/.t3/worktrees/feature-a",
200 refName: {
201 isDefault: false,
202 worktreePath: "/repo/.t3/worktrees/feature-b",
203 },
204 }),
205 ).toEqual({
206 checkoutCwd: "/repo/.t3/worktrees/feature-b",
207 nextWorktreePath: "/repo/.t3/worktrees/feature-b",
208 reuseExistingWorktree: true,
209 });
210 });
211​
212 it("switches back to the main repo when the ref already lives there", () => {
213 // A subdirectory project's own checkout is reported at the repository root.
214 // The checkout root is undefined until the project's status loads.
215 for (const [activeProjectCwd, activeWorktreePath, activeProjectCheckoutRoot] of [
216 ["/repo", "/repo/.t3/worktrees/feature-a", "/repo"],
217 ["/repo/packages/app", "/repo/.t3/worktrees/feature-a", "/repo"],
218 ["/repo/packages/app", null, "/repo"],
219 ["/repo/packages/app", "/repo/.t3/worktrees/feature-a", undefined],
220 ["/repo/packages/app", null, undefined],
221 ] as const) {
222 expect(
223 resolveBranchSelectionTarget({
224 activeProjectCwd,
225 activeProjectCheckoutRoot,
226 activeWorktreePath,
227 refName: {
228 isDefault: true,
229 worktreePath: "/repo",
230 },
231 }),
232 ).toEqual({
233 checkoutCwd: "/repo",
234 nextWorktreePath: null,
235 reuseExistingWorktree: true,
236 });
237 }
238 });
239​
240 it("keeps checkout in the current worktree for non-default refs", () => {
241 expect(
242 resolveBranchSelectionTarget({
243 activeProjectCwd: "/repo",
244 activeWorktreePath: "/repo/.t3/worktrees/feature-a",
245 refName: {
246 isDefault: false,
247 worktreePath: null,
248 },
249 }),
250 ).toEqual({
251 checkoutCwd: "/repo/.t3/worktrees/feature-a",
252 nextWorktreePath: "/repo/.t3/worktrees/feature-a",
253 reuseExistingWorktree: false,
254 });
255 });
256});
257​
258describe("isWorktreeChangeBlocked", () => {
259 const runtime = (status: "preparing" | "running" | "completed") => ({
260 status,
261 activeRunId: status === "completed" ? null : RunId.make("run"),
262 providerInstanceId: ProviderInstanceId.make("codex"),
263 providerName: null,
264 lastError: null,
265 });
266​
267 it.each([
268 ["preparing", "/repo/wt-a", null, true],
269 ["running", "/repo/wt-a", "/repo/wt-b", true],
270 ["running", null, "/repo/wt-b", true],
271 ["running", "/repo/wt-a", "/repo/wt-a", false],
272 ["completed", "/repo/wt-a", null, false],
273 ] as const)(
274 "a %s turn moving %s -> %s is blocked: %s",
275 (status, currentWorktreePath, nextWorktreePath, blocked) => {
276 expect(
277 isWorktreeChangeBlocked({
278 runtime: runtime(status),
279 currentWorktreePath,
280 nextWorktreePath,
281 }),
282 ).toBe(blocked);
283 },
284 );
285});
286​
287describe("shouldIncludeBranchPickerItem", () => {
288 it("keeps the synthetic checkout PR item visible for gh pr checkout input", () => {
289 expect(
290 shouldIncludeBranchPickerItem({
291 itemValue: "__checkout_pull_request__:1359",
292 normalizedQuery: "gh pr checkout 1359",
293 createBranchItemValue: "__create_new_branch__:gh pr checkout 1359",
294 checkoutPullRequestItemValue: "__checkout_pull_request__:1359",
295 }),
296 ).toBe(true);
297 });
298​
299 it("surfaces an existing ref matching the sanitized query", () => {
300 expect(
301 shouldIncludeBranchPickerItem({
302 itemValue: "new-branch",
303 normalizedQuery: "new branch",
304 createBranchItemValue: null,
305 checkoutPullRequestItemValue: null,
306 }),
307 ).toBe(true);
308 });
309});
310​
311describe("sanitizeNewRefName", () => {
312 it("collapses a run of whitespace into a single dash", () => {
313 expect(sanitizeNewRefName("new branch")).toBe("new-branch");
314 });
315​
316 it("replaces tabs, which git rejects just like spaces", () => {
317 expect(sanitizeNewRefName("new\tbranch")).toBe("new-branch");
318 });
319​
320 it("preserves whitespace that git accepts", () => {
321 expect(sanitizeNewRefName("new\u00a0branch")).toBe("new\u00a0branch");
322 expect(sanitizeNewRefName("new\u2009branch")).toBe("new\u2009branch");
323 });
324​
325 it("does not collapse dashes the user typed", () => {
326 expect(sanitizeNewRefName("new - branch")).toBe("new---branch");
327 expect(sanitizeNewRefName("foo--bar")).toBe("foo--bar");
328 });
329});