| 1 | import * as NodeChildProcess from "node:child_process"; |
| 2 | import * as NodeFSP from "node:fs/promises"; |
| 3 | import * as NodeOS from "node:os"; |
| 4 | import * as NodePath from "node:path"; |
| 5 | import * as NodeUtil from "node:util"; |
| 6 | import { EnvironmentId } from "@t3tools/contracts"; |
| 7 | import { settlePromise, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; |
| 8 | import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; |
| 9 | |
| 10 | import { checkoutNewTaskBranch } from "./checkout-new-task-branch"; |
| 11 | |
| 12 | const exec = NodeUtil.promisify(NodeChildProcess.execFile); |
| 13 | let directory: string; |
| 14 | let cwd: string; |
| 15 | const git = (...args: string[]) => exec("git", ["-C", cwd, ...args]); |
| 16 | const branch = { name: "feature/a", current: false, isDefault: false, worktreePath: null }; |
| 17 | const environmentId = EnvironmentId.make("branch-test-environment"); |
| 18 | |
| 19 | beforeEach(async () => { |
| 20 | directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-branch-selection-")); |
| 21 | cwd = NodePath.join(directory, "project"); |
| 22 | await exec("git", ["init", "-b", "main", cwd]); |
| 23 | await git("config", "user.name", "Branch test"); |
| 24 | await git("config", "user.email", "branch-test@example.com"); |
| 25 | await NodeFSP.writeFile(NodePath.join(cwd, "file.txt"), "main\n"); |
| 26 | await git("add", "."); |
| 27 | await git("commit", "-m", "main"); |
| 28 | await git("checkout", "-b", branch.name); |
| 29 | await NodeFSP.writeFile(NodePath.join(cwd, "file.txt"), "feature\n"); |
| 30 | await git("commit", "-am", "feature"); |
| 31 | await git("checkout", "main"); |
| 32 | }); |
| 33 | |
| 34 | afterEach(async () => { |
| 35 | await NodeFSP.rm(directory, { recursive: true, force: true }); |
| 36 | }); |
| 37 | |
| 38 | function selectBranch(switchRef: Parameters<typeof checkoutNewTaskBranch>[0]["switchRef"]) { |
| 39 | return checkoutNewTaskBranch({ |
| 40 | branch, |
| 41 | project: { environmentId, workspaceRoot: cwd }, |
| 42 | workspaceMode: "local", |
| 43 | switchRef, |
| 44 | }); |
| 45 | } |
| 46 | |
| 47 | const switchRef: Parameters<typeof checkoutNewTaskBranch>[0]["switchRef"] = (request) => |
| 48 | settlePromise(async () => { |
| 49 | expect(request.environmentId).toBe(environmentId); |
| 50 | await exec("git", ["-C", request.input.cwd, "checkout", request.input.refName]); |
| 51 | const { stdout } = await exec("git", ["-C", request.input.cwd, "branch", "--show-current"]); |
| 52 | return { refName: stdout.trim() }; |
| 53 | }); |
| 54 | |
| 55 | describe("new-task branch checkout", () => { |
| 56 | it("switches main to the older thread's feature branch before returning a selection", async () => { |
| 57 | const result = await selectBranch(switchRef); |
| 58 | expect(result._tag).toBe("Success"); |
| 59 | if (result._tag !== "Success") throw new Error("Checkout failed"); |
| 60 | expect(result.value.name).toBe("feature/a"); |
| 61 | expect(result.value.current).toBe(true); |
| 62 | expect((await git("branch", "--show-current")).stdout.trim()).toBe("feature/a"); |
| 63 | expect(await NodeFSP.readFile(NodePath.join(cwd, "file.txt"), "utf8")).toBe("feature\n"); |
| 64 | }); |
| 65 | |
| 66 | it("does not release the selection while checkout is still pending", async () => { |
| 67 | const entered = Promise.withResolvers<void>(); |
| 68 | const release = Promise.withResolvers<void>(); |
| 69 | let completed = false; |
| 70 | const selection = selectBranch(async (request) => { |
| 71 | entered.resolve(); |
| 72 | await release.promise; |
| 73 | return switchRef(request); |
| 74 | }).then((result) => { |
| 75 | completed = true; |
| 76 | return result; |
| 77 | }); |
| 78 | await entered.promise; |
| 79 | expect(completed).toBe(false); |
| 80 | expect((await git("branch", "--show-current")).stdout.trim()).toBe("main"); |
| 81 | release.resolve(); |
| 82 | expect((await selection)._tag).toBe("Success"); |
| 83 | expect((await git("branch", "--show-current")).stdout.trim()).toBe("feature/a"); |
| 84 | }); |
| 85 | |
| 86 | it("returns checkout failure without selecting the branch or losing dirty files", async () => { |
| 87 | await NodeFSP.writeFile(NodePath.join(cwd, "file.txt"), "unsaved local changes\n"); |
| 88 | const result = await selectBranch(switchRef); |
| 89 | expect(result._tag).toBe("Failure"); |
| 90 | if (result._tag !== "Failure") throw new Error("Expected checkout to fail"); |
| 91 | expect(String(squashAtomCommandFailure(result))).toContain("would be overwritten"); |
| 92 | expect((await git("branch", "--show-current")).stdout.trim()).toBe("main"); |
| 93 | expect(await NodeFSP.readFile(NodePath.join(cwd, "file.txt"), "utf8")).toBe( |
| 94 | "unsaved local changes\n", |
| 95 | ); |
| 96 | }); |
| 97 | |
| 98 | it("fails when the source project is unavailable instead of releasing a composer selection", async () => { |
| 99 | const result = await checkoutNewTaskBranch({ |
| 100 | branch, |
| 101 | project: null, |
| 102 | workspaceMode: "local", |
| 103 | switchRef: () => { |
| 104 | throw new Error("An unavailable project must not run checkout"); |
| 105 | }, |
| 106 | }); |
| 107 | expect(result._tag).toBe("Failure"); |
| 108 | if (result._tag !== "Failure") throw new Error("Expected an unavailable-project failure"); |
| 109 | expect(String(squashAtomCommandFailure(result))).toContain("selected project is unavailable"); |
| 110 | expect((await git("branch", "--show-current")).stdout.trim()).toBe("main"); |
| 111 | }); |
| 112 | |
| 113 | it("reuses an existing worktree without switching the project checkout", async () => { |
| 114 | const worktreePath = NodePath.join(directory, "worktree"); |
| 115 | await git("worktree", "add", worktreePath, "feature/a"); |
| 116 | const result = await checkoutNewTaskBranch({ |
| 117 | branch: { ...branch, worktreePath }, |
| 118 | project: { environmentId, workspaceRoot: cwd }, |
| 119 | workspaceMode: "local", |
| 120 | switchRef: () => { |
| 121 | throw new Error("Existing worktrees must not switch the project checkout"); |
| 122 | }, |
| 123 | }); |
| 124 | expect(result._tag).toBe("Success"); |
| 125 | if (result._tag !== "Success") throw new Error("Worktree selection failed"); |
| 126 | expect(result.value.worktreePath).toBe(worktreePath); |
| 127 | expect((await git("branch", "--show-current")).stdout.trim()).toBe("main"); |
| 128 | expect(await NodeFSP.readFile(NodePath.join(worktreePath, "file.txt"), "utf8")).toBe( |
| 129 | "feature\n", |
| 130 | ); |
| 131 | }); |
| 132 | }); |