1import { describe, expect, it } from "vite-plus/test";
2import * as Schema from "effect/Schema";
3
4import {
5 VcsCreateWorktreeInput,
6 GitPreparePullRequestThreadInput,
7 GitPreparePullRequestThreadResult,
8 GitRunStackedActionResult,
9 GitRunStackedActionInput,
10 GitResolvePullRequestResult,
11 VcsStatusLocalResult,
12} from "./git.ts";
13
14const decodeCreateWorktreeInput = Schema.decodeUnknownSync(VcsCreateWorktreeInput);
15const decodePreparePullRequestThreadInput = Schema.decodeUnknownSync(
16 GitPreparePullRequestThreadInput,
17);
18const decodePreparePullRequestThreadResult = Schema.decodeUnknownSync(
19 GitPreparePullRequestThreadResult,
20);
21const decodeRunStackedActionInput = Schema.decodeUnknownSync(GitRunStackedActionInput);
22const decodeRunStackedActionResult = Schema.decodeUnknownSync(GitRunStackedActionResult);
23const decodeResolvePullRequestResult = Schema.decodeUnknownSync(GitResolvePullRequestResult);
24const decodeVcsStatusLocalResult = Schema.decodeUnknownSync(VcsStatusLocalResult);
25
26describe("VcsCreateWorktreeInput", () => {
27 it("accepts omitted newRefName for existing-refName worktrees", () => {
28 const parsed = decodeCreateWorktreeInput({
29 cwd: "/repo",
30 refName: "feature/existing",
31 path: "/tmp/worktree",
32 });
33
34 expect(parsed.newRefName).toBeUndefined();
35 expect(parsed.refName).toBe("feature/existing");
36 });
37
38 it("accepts baseRefName metadata for a new worktree ref", () => {
39 const parsed = decodeCreateWorktreeInput({
40 cwd: "/repo",
41 refName: "0123456789abcdef",
42 newRefName: "feature/new",
43 baseRefName: "origin/main",
44 path: "/tmp/worktree",
45 });
46
47 expect(parsed.baseRefName).toBe("origin/main");
48 });
49});
50
51describe("GitPreparePullRequestThreadInput", () => {
52 it("accepts pull request references and mode", () => {
53 const parsed = decodePreparePullRequestThreadInput({
54 cwd: "/repo",
55 reference: "#42",
56 mode: "worktree",
57 });
58
59 expect(parsed.reference).toBe("#42");
60 expect(parsed.mode).toBe("worktree");
61 });
62});
63
64describe("GitPreparePullRequestThreadResult", () => {
65 it("defaults legacy responses to the pull request head", () => {
66 const parsed = decodePreparePullRequestThreadResult({
67 pullRequest: {
68 number: 42,
69 title: "PR threads",
70 url: "https://github.com/pingdotgg/codething-mvp/pull/42",
71 baseBranch: "main",
72 headBranch: "feature/pr-threads",
73 state: "open",
74 },
75 branch: "feature/pr-threads",
76 worktreePath: "/tmp/pr-threads",
77 });
78
79 expect(parsed.isOnPullRequestHead).toBe(true);
80 });
81
82 it("preserves an explicit stale pull request checkout result", () => {
83 const parsed = decodePreparePullRequestThreadResult({
84 pullRequest: {
85 number: 42,
86 title: "PR threads",
87 url: "https://github.com/pingdotgg/codething-mvp/pull/42",
88 baseBranch: "main",
89 headBranch: "feature/pr-threads",
90 state: "open",
91 },
92 branch: "feature/pr-threads",
93 worktreePath: "/tmp/pr-threads",
94 isOnPullRequestHead: false,
95 });
96
97 expect(parsed.isOnPullRequestHead).toBe(false);
98 });
99});
100
101describe("GitResolvePullRequestResult", () => {
102 it("decodes resolved pull request metadata", () => {
103 const parsed = decodeResolvePullRequestResult({
104 pullRequest: {
105 number: 42,
106 title: "PR threads",
107 url: "https://github.com/pingdotgg/codething-mvp/pull/42",
108 baseBranch: "main",
109 headBranch: "feature/pr-threads",
110 state: "open",
111 },
112 });
113
114 expect(parsed.pullRequest.number).toBe(42);
115 expect(parsed.pullRequest.headBranch).toBe("feature/pr-threads");
116 });
117});
118
119describe("GitRunStackedActionInput", () => {
120 it("accepts explicit stacked actions and requires a client-provided actionId", () => {
121 const parsed = decodeRunStackedActionInput({
122 actionId: "action-1",
123 cwd: "/repo",
124 action: "create_pr",
125 });
126
127 expect(parsed.actionId).toBe("action-1");
128 expect(parsed.action).toBe("create_pr");
129 });
130
131 it("preserves leading and trailing whitespace in file paths", () => {
132 const parsed = decodeRunStackedActionInput({
133 actionId: "action-1",
134 cwd: "/repo",
135 action: "commit",
136 filePaths: [" leading.txt", "trailing.txt "],
137 });
138
139 expect(parsed.filePaths).toEqual([" leading.txt", "trailing.txt "]);
140 });
141
142 it("rejects an empty file path", () => {
143 expect(() =>
144 decodeRunStackedActionInput({
145 actionId: "action-1",
146 cwd: "/repo",
147 action: "commit",
148 filePaths: [""],
149 }),
150 ).toThrow();
151 });
152});
153
154describe("VcsStatusLocalResult", () => {
155 it("preserves leading and trailing whitespace in working-tree paths", () => {
156 const parsed = decodeVcsStatusLocalResult({
157 isRepo: true,
158 hasPrimaryRemote: true,
159 isDefaultRef: false,
160 refName: "main",
161 hasWorkingTreeChanges: true,
162 workingTree: {
163 files: [
164 { path: " leading.txt", insertions: 1, deletions: 0 },
165 { path: "trailing.txt ", insertions: 0, deletions: 1 },
166 ],
167 insertions: 1,
168 deletions: 1,
169 },
170 });
171
172 expect(parsed.workingTree.files.map((file) => file.path)).toEqual([
173 " leading.txt",
174 "trailing.txt ",
175 ]);
176 });
177
178 it("rejects an empty working-tree path", () => {
179 expect(() =>
180 decodeVcsStatusLocalResult({
181 isRepo: true,
182 hasPrimaryRemote: true,
183 isDefaultRef: false,
184 refName: "main",
185 hasWorkingTreeChanges: true,
186 workingTree: {
187 files: [{ path: "", insertions: 0, deletions: 0 }],
188 insertions: 0,
189 deletions: 0,
190 },
191 }),
192 ).toThrow();
193 });
194});
195
196describe("GitRunStackedActionResult", () => {
197 it("decodes a server-authored completion toast", () => {
198 const parsed = decodeRunStackedActionResult({
199 action: "commit_push",
200 branch: {
201 status: "created",
202 name: "feature/server-owned-toast",
203 },
204 commit: {
205 status: "created",
206 commitSha: "89abcdef01234567",
207 subject: "feat: move toast state into git manager",
208 },
209 push: {
210 status: "pushed",
211 branch: "feature/server-owned-toast",
212 upstreamBranch: "origin/feature/server-owned-toast",
213 },
214 pr: {
215 status: "skipped_not_requested",
216 },
217 toast: {
218 title: "Pushed 89abcde to origin/feature/server-owned-toast",
219 description: "feat: move toast state into git manager",
220 cta: {
221 kind: "run_action",
222 label: "Create PR",
223 action: {
224 kind: "create_pr",
225 },
226 },
227 },
228 });
229
230 expect(parsed.toast.cta.kind).toBe("run_action");
231 if (parsed.toast.cta.kind === "run_action") {
232 expect(parsed.toast.cta.action.kind).toBe("create_pr");
233 }
234 });
235});