@aku11i/phantom 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,316 +0,0 @@
1
- import { strictEqual } from "node:assert";
2
- import { before, describe, it, mock } from "node:test";
3
-
4
- describe("deleteGarden", () => {
5
- let accessMock: ReturnType<typeof mock.fn>;
6
- let execMock: ReturnType<typeof mock.fn>;
7
- let deleteGarden: typeof import("./delete.ts").deleteGarden;
8
-
9
- before(async () => {
10
- accessMock = mock.fn();
11
- execMock = mock.fn();
12
-
13
- mock.module("node:fs/promises", {
14
- namedExports: {
15
- access: accessMock,
16
- },
17
- });
18
-
19
- mock.module("node:child_process", {
20
- namedExports: {
21
- exec: execMock,
22
- },
23
- });
24
-
25
- mock.module("node:util", {
26
- namedExports: {
27
- promisify: (fn: unknown) => fn,
28
- },
29
- });
30
-
31
- ({ deleteGarden } = await import("./delete.ts"));
32
- });
33
-
34
- it("should return error when name is not provided", async () => {
35
- const result = await deleteGarden("");
36
- strictEqual(result.success, false);
37
- strictEqual(result.message, "Error: garden name required");
38
- });
39
-
40
- it("should return error when garden does not exist", async () => {
41
- accessMock.mock.resetCalls();
42
- execMock.mock.resetCalls();
43
-
44
- // Mock getGitRoot
45
- execMock.mock.mockImplementation((cmd: string) => {
46
- if (cmd === "git rev-parse --show-toplevel") {
47
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
48
- }
49
- return Promise.resolve({ stdout: "", stderr: "" });
50
- });
51
-
52
- // Mock garden doesn't exist
53
- accessMock.mock.mockImplementation(() => {
54
- return Promise.reject(new Error("ENOENT"));
55
- });
56
-
57
- const result = await deleteGarden("nonexistent-garden");
58
-
59
- strictEqual(result.success, false);
60
- strictEqual(
61
- result.message,
62
- "Error: Garden 'nonexistent-garden' does not exist",
63
- );
64
- });
65
-
66
- it("should delete clean garden successfully", async () => {
67
- accessMock.mock.resetCalls();
68
- execMock.mock.resetCalls();
69
-
70
- // Mock git commands
71
- execMock.mock.mockImplementation(
72
- (cmd: string, options?: { cwd?: string }) => {
73
- if (cmd === "git rev-parse --show-toplevel") {
74
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
75
- }
76
- if (cmd === "git status --porcelain") {
77
- return Promise.resolve({ stdout: "", stderr: "" }); // Clean status
78
- }
79
- if (cmd.includes("git worktree remove")) {
80
- return Promise.resolve({ stdout: "", stderr: "" });
81
- }
82
- if (cmd.includes("git branch -D")) {
83
- return Promise.resolve({ stdout: "", stderr: "" });
84
- }
85
- return Promise.resolve({ stdout: "", stderr: "" });
86
- },
87
- );
88
-
89
- // Mock garden exists
90
- accessMock.mock.mockImplementation(() => Promise.resolve());
91
-
92
- const result = await deleteGarden("clean-garden");
93
-
94
- strictEqual(result.success, true);
95
- strictEqual(
96
- result.message,
97
- "Deleted garden 'clean-garden' and its branch 'phantom/gardens/clean-garden'",
98
- );
99
- strictEqual(result.hasUncommittedChanges, false);
100
- });
101
-
102
- it("should refuse to delete dirty garden without --force", async () => {
103
- accessMock.mock.resetCalls();
104
- execMock.mock.resetCalls();
105
-
106
- // Mock git commands
107
- execMock.mock.mockImplementation(
108
- (cmd: string, options?: { cwd?: string }) => {
109
- if (cmd === "git rev-parse --show-toplevel") {
110
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
111
- }
112
- if (cmd === "git status --porcelain") {
113
- return Promise.resolve({
114
- stdout: " M file1.ts\n?? file2.ts\n",
115
- stderr: "",
116
- }); // Dirty status with 2 files
117
- }
118
- return Promise.resolve({ stdout: "", stderr: "" });
119
- },
120
- );
121
-
122
- // Mock garden exists
123
- accessMock.mock.mockImplementation(() => Promise.resolve());
124
-
125
- const result = await deleteGarden("dirty-garden");
126
-
127
- strictEqual(result.success, false);
128
- strictEqual(
129
- result.message,
130
- "Error: Garden 'dirty-garden' has uncommitted changes (2 files). Use --force to delete anyway.",
131
- );
132
- strictEqual(result.hasUncommittedChanges, true);
133
- strictEqual(result.changedFiles, 2);
134
- });
135
-
136
- it("should delete dirty garden with --force", async () => {
137
- accessMock.mock.resetCalls();
138
- execMock.mock.resetCalls();
139
-
140
- // Mock git commands
141
- execMock.mock.mockImplementation(
142
- (cmd: string, options?: { cwd?: string }) => {
143
- if (cmd === "git rev-parse --show-toplevel") {
144
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
145
- }
146
- if (cmd === "git status --porcelain") {
147
- return Promise.resolve({
148
- stdout: " M file1.ts\n?? file2.ts\n",
149
- stderr: "",
150
- }); // Dirty status with 2 files
151
- }
152
- if (cmd.includes("git worktree remove")) {
153
- return Promise.resolve({ stdout: "", stderr: "" });
154
- }
155
- if (cmd.includes("git branch -D")) {
156
- return Promise.resolve({ stdout: "", stderr: "" });
157
- }
158
- return Promise.resolve({ stdout: "", stderr: "" });
159
- },
160
- );
161
-
162
- // Mock garden exists
163
- accessMock.mock.mockImplementation(() => Promise.resolve());
164
-
165
- const result = await deleteGarden("dirty-garden", { force: true });
166
-
167
- strictEqual(result.success, true);
168
- strictEqual(
169
- result.message,
170
- "Warning: Garden 'dirty-garden' had uncommitted changes (2 files)\nDeleted garden 'dirty-garden' and its branch 'phantom/gardens/dirty-garden'",
171
- );
172
- strictEqual(result.hasUncommittedChanges, true);
173
- strictEqual(result.changedFiles, 2);
174
- });
175
-
176
- it("should handle worktree remove failure and try force removal", async () => {
177
- accessMock.mock.resetCalls();
178
- execMock.mock.resetCalls();
179
-
180
- // Mock git commands
181
- execMock.mock.mockImplementation(
182
- (cmd: string, options?: { cwd?: string }) => {
183
- if (cmd === "git rev-parse --show-toplevel") {
184
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
185
- }
186
- if (cmd === "git status --porcelain") {
187
- return Promise.resolve({ stdout: "", stderr: "" });
188
- }
189
- if (cmd.includes("git worktree remove") && !cmd.includes("--force")) {
190
- return Promise.reject(new Error("Worktree remove failed"));
191
- }
192
- if (cmd.includes("git worktree remove --force")) {
193
- return Promise.resolve({ stdout: "", stderr: "" });
194
- }
195
- if (cmd.includes("git branch -D")) {
196
- return Promise.resolve({ stdout: "", stderr: "" });
197
- }
198
- return Promise.resolve({ stdout: "", stderr: "" });
199
- },
200
- );
201
-
202
- // Mock garden exists
203
- accessMock.mock.mockImplementation(() => Promise.resolve());
204
-
205
- const result = await deleteGarden("stubborn-garden");
206
-
207
- strictEqual(result.success, true);
208
- strictEqual(
209
- result.message,
210
- "Deleted garden 'stubborn-garden' and its branch 'phantom/gardens/stubborn-garden'",
211
- );
212
- });
213
-
214
- it("should handle case where branch doesn't exist", async () => {
215
- accessMock.mock.resetCalls();
216
- execMock.mock.resetCalls();
217
-
218
- // Mock git commands
219
- execMock.mock.mockImplementation(
220
- (cmd: string, options?: { cwd?: string }) => {
221
- if (cmd === "git rev-parse --show-toplevel") {
222
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
223
- }
224
- if (cmd === "git status --porcelain") {
225
- return Promise.resolve({ stdout: "", stderr: "" });
226
- }
227
- if (cmd.includes("git worktree remove")) {
228
- return Promise.resolve({ stdout: "", stderr: "" });
229
- }
230
- if (cmd.includes("git branch -D")) {
231
- return Promise.reject(new Error("Branch not found"));
232
- }
233
- return Promise.resolve({ stdout: "", stderr: "" });
234
- },
235
- );
236
-
237
- // Mock garden exists
238
- accessMock.mock.mockImplementation(() => Promise.resolve());
239
-
240
- const result = await deleteGarden("branch-missing-garden");
241
-
242
- strictEqual(result.success, true);
243
- strictEqual(
244
- result.message,
245
- "Deleted garden 'branch-missing-garden' and its branch 'phantom/gardens/branch-missing-garden'",
246
- );
247
- });
248
-
249
- it("should return error when force worktree removal also fails", async () => {
250
- accessMock.mock.resetCalls();
251
- execMock.mock.resetCalls();
252
-
253
- // Mock git commands
254
- execMock.mock.mockImplementation(
255
- (cmd: string, options?: { cwd?: string }) => {
256
- if (cmd === "git rev-parse --show-toplevel") {
257
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
258
- }
259
- if (cmd === "git status --porcelain") {
260
- return Promise.resolve({ stdout: "", stderr: "" });
261
- }
262
- if (cmd.includes("git worktree remove")) {
263
- return Promise.reject(new Error("Worktree removal failed"));
264
- }
265
- return Promise.resolve({ stdout: "", stderr: "" });
266
- },
267
- );
268
-
269
- // Mock garden exists
270
- accessMock.mock.mockImplementation(() => Promise.resolve());
271
-
272
- const result = await deleteGarden("impossible-garden");
273
-
274
- strictEqual(result.success, false);
275
- strictEqual(
276
- result.message,
277
- "Error: Failed to remove worktree for garden 'impossible-garden'",
278
- );
279
- });
280
-
281
- it("should handle git status errors gracefully", async () => {
282
- accessMock.mock.resetCalls();
283
- execMock.mock.resetCalls();
284
-
285
- // Mock git commands
286
- execMock.mock.mockImplementation(
287
- (cmd: string, options?: { cwd?: string }) => {
288
- if (cmd === "git rev-parse --show-toplevel") {
289
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
290
- }
291
- if (cmd === "git status --porcelain") {
292
- return Promise.reject(new Error("Git status failed"));
293
- }
294
- if (cmd.includes("git worktree remove")) {
295
- return Promise.resolve({ stdout: "", stderr: "" });
296
- }
297
- if (cmd.includes("git branch -D")) {
298
- return Promise.resolve({ stdout: "", stderr: "" });
299
- }
300
- return Promise.resolve({ stdout: "", stderr: "" });
301
- },
302
- );
303
-
304
- // Mock garden exists
305
- accessMock.mock.mockImplementation(() => Promise.resolve());
306
-
307
- const result = await deleteGarden("status-error-garden");
308
-
309
- strictEqual(result.success, true);
310
- strictEqual(
311
- result.message,
312
- "Deleted garden 'status-error-garden' and its branch 'phantom/gardens/status-error-garden'",
313
- );
314
- strictEqual(result.hasUncommittedChanges, false);
315
- });
316
- });
@@ -1,134 +0,0 @@
1
- import { exec } from "node:child_process";
2
- import { access } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { exit } from "node:process";
5
- import { promisify } from "node:util";
6
- import { getGitRoot } from "../../git/libs/get-git-root.ts";
7
-
8
- const execAsync = promisify(exec);
9
-
10
- export async function deleteGarden(
11
- name: string,
12
- options: { force?: boolean } = {},
13
- ): Promise<{
14
- success: boolean;
15
- message: string;
16
- hasUncommittedChanges?: boolean;
17
- changedFiles?: number;
18
- }> {
19
- if (!name) {
20
- return { success: false, message: "Error: garden name required" };
21
- }
22
-
23
- const { force = false } = options;
24
-
25
- try {
26
- const gitRoot = await getGitRoot();
27
- const gardensPath = join(gitRoot, ".git", "phantom", "gardens");
28
- const gardenPath = join(gardensPath, name);
29
-
30
- // Check if garden exists
31
- try {
32
- await access(gardenPath);
33
- } catch {
34
- return {
35
- success: false,
36
- message: `Error: Garden '${name}' does not exist`,
37
- };
38
- }
39
-
40
- // Check for uncommitted changes
41
- let hasUncommittedChanges = false;
42
- let changedFiles = 0;
43
- try {
44
- const { stdout } = await execAsync("git status --porcelain", {
45
- cwd: gardenPath,
46
- });
47
- const changes = stdout.trim();
48
- if (changes) {
49
- hasUncommittedChanges = true;
50
- changedFiles = changes.split("\n").length;
51
- }
52
- } catch {
53
- // If git status fails, assume no changes
54
- hasUncommittedChanges = false;
55
- }
56
-
57
- // If garden has uncommitted changes and --force is not specified, refuse deletion
58
- if (hasUncommittedChanges && !force) {
59
- return {
60
- success: false,
61
- message: `Error: Garden '${name}' has uncommitted changes (${changedFiles} files). Use --force to delete anyway.`,
62
- hasUncommittedChanges: true,
63
- changedFiles,
64
- };
65
- }
66
-
67
- // Remove git worktree
68
- try {
69
- await execAsync(`git worktree remove "${gardenPath}"`, {
70
- cwd: gitRoot,
71
- });
72
- } catch (error) {
73
- // If worktree remove fails, try force removal
74
- try {
75
- await execAsync(`git worktree remove --force "${gardenPath}"`, {
76
- cwd: gitRoot,
77
- });
78
- } catch {
79
- return {
80
- success: false,
81
- message: `Error: Failed to remove worktree for garden '${name}'`,
82
- };
83
- }
84
- }
85
-
86
- // Delete associated branch
87
- const branchName = `phantom/gardens/${name}`;
88
- try {
89
- await execAsync(`git branch -D "${branchName}"`, {
90
- cwd: gitRoot,
91
- });
92
- } catch {
93
- // Branch might not exist or already deleted - this is not an error
94
- // We'll still report success for the worktree removal
95
- }
96
-
97
- let message = `Deleted garden '${name}' and its branch '${branchName}'`;
98
- if (hasUncommittedChanges) {
99
- message = `Warning: Garden '${name}' had uncommitted changes (${changedFiles} files)\n${message}`;
100
- }
101
-
102
- return {
103
- success: true,
104
- message,
105
- hasUncommittedChanges,
106
- changedFiles: hasUncommittedChanges ? changedFiles : undefined,
107
- };
108
- } catch (error) {
109
- const errorMessage = error instanceof Error ? error.message : String(error);
110
- return {
111
- success: false,
112
- message: `Error deleting garden: ${errorMessage}`,
113
- };
114
- }
115
- }
116
-
117
- export async function gardensDeleteHandler(args: string[]): Promise<void> {
118
- // Parse arguments for --force flag
119
- const forceIndex = args.indexOf("--force");
120
- const force = forceIndex !== -1;
121
-
122
- // Remove --force from args to get the garden name
123
- const filteredArgs = args.filter((arg) => arg !== "--force");
124
- const name = filteredArgs[0];
125
-
126
- const result = await deleteGarden(name, { force });
127
-
128
- if (!result.success) {
129
- console.error(result.message);
130
- exit(1);
131
- }
132
-
133
- console.log(result.message);
134
- }
@@ -1,243 +0,0 @@
1
- import { deepStrictEqual, strictEqual } from "node:assert";
2
- import { before, describe, it, mock } from "node:test";
3
-
4
- describe("listGardens", () => {
5
- let accessMock: ReturnType<typeof mock.fn>;
6
- let readdirMock: ReturnType<typeof mock.fn>;
7
- let execMock: ReturnType<typeof mock.fn>;
8
- let listGardens: typeof import("./list.ts").listGardens;
9
-
10
- before(async () => {
11
- accessMock = mock.fn();
12
- readdirMock = mock.fn();
13
- execMock = mock.fn();
14
-
15
- mock.module("node:fs/promises", {
16
- namedExports: {
17
- access: accessMock,
18
- readdir: readdirMock,
19
- },
20
- });
21
-
22
- mock.module("node:child_process", {
23
- namedExports: {
24
- exec: execMock,
25
- },
26
- });
27
-
28
- mock.module("node:util", {
29
- namedExports: {
30
- promisify: (fn: unknown) => fn,
31
- },
32
- });
33
-
34
- ({ listGardens } = await import("./list.ts"));
35
- });
36
-
37
- it("should return empty array when gardens directory doesn't exist", async () => {
38
- accessMock.mock.resetCalls();
39
- readdirMock.mock.resetCalls();
40
- execMock.mock.resetCalls();
41
-
42
- // Mock getGitRoot
43
- execMock.mock.mockImplementation((cmd: string) => {
44
- if (cmd === "git rev-parse --show-toplevel") {
45
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
46
- }
47
- return Promise.resolve({ stdout: "", stderr: "" });
48
- });
49
-
50
- // Mock gardens directory doesn't exist
51
- accessMock.mock.mockImplementation((path: string) => {
52
- if (path === "/test/repo/.git/phantom/gardens") {
53
- return Promise.reject(new Error("ENOENT"));
54
- }
55
- return Promise.resolve();
56
- });
57
-
58
- const result = await listGardens();
59
-
60
- strictEqual(result.success, true);
61
- deepStrictEqual(result.gardens, []);
62
- strictEqual(
63
- result.message,
64
- "No gardens found (gardens directory doesn't exist)",
65
- );
66
- });
67
-
68
- it("should return empty array when gardens directory is empty", async () => {
69
- accessMock.mock.resetCalls();
70
- readdirMock.mock.resetCalls();
71
- execMock.mock.resetCalls();
72
-
73
- // Mock getGitRoot
74
- execMock.mock.mockImplementation((cmd: string) => {
75
- if (cmd === "git rev-parse --show-toplevel") {
76
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
77
- }
78
- return Promise.resolve({ stdout: "", stderr: "" });
79
- });
80
-
81
- // Mock gardens directory exists but is empty
82
- accessMock.mock.mockImplementation(() => Promise.resolve());
83
- readdirMock.mock.mockImplementation(() => Promise.resolve([]));
84
-
85
- const result = await listGardens();
86
-
87
- strictEqual(result.success, true);
88
- deepStrictEqual(result.gardens, []);
89
- strictEqual(result.message, "No gardens found");
90
- });
91
-
92
- it("should list gardens with clean status", async () => {
93
- accessMock.mock.resetCalls();
94
- readdirMock.mock.resetCalls();
95
- execMock.mock.resetCalls();
96
-
97
- // Mock getGitRoot and git commands
98
- execMock.mock.mockImplementation(
99
- (cmd: string, options?: { cwd?: string }) => {
100
- if (cmd === "git rev-parse --show-toplevel") {
101
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
102
- }
103
- if (cmd === "git branch --show-current") {
104
- if (options?.cwd?.includes("test-garden-1")) {
105
- return Promise.resolve({ stdout: "feature/test\n", stderr: "" });
106
- }
107
- if (options?.cwd?.includes("test-garden-2")) {
108
- return Promise.resolve({ stdout: "main\n", stderr: "" });
109
- }
110
- }
111
- if (cmd === "git status --porcelain") {
112
- return Promise.resolve({ stdout: "", stderr: "" }); // Clean status
113
- }
114
- return Promise.resolve({ stdout: "", stderr: "" });
115
- },
116
- );
117
-
118
- // Mock gardens directory and contents
119
- accessMock.mock.mockImplementation(() => Promise.resolve());
120
- readdirMock.mock.mockImplementation(() =>
121
- Promise.resolve(["test-garden-1", "test-garden-2"]),
122
- );
123
-
124
- const result = await listGardens();
125
-
126
- strictEqual(result.success, true);
127
- strictEqual(result.gardens?.length, 2);
128
- strictEqual(result.gardens?.[0].name, "test-garden-1");
129
- strictEqual(result.gardens?.[0].branch, "feature/test");
130
- strictEqual(result.gardens?.[0].status, "clean");
131
- strictEqual(result.gardens?.[1].name, "test-garden-2");
132
- strictEqual(result.gardens?.[1].branch, "main");
133
- strictEqual(result.gardens?.[1].status, "clean");
134
- });
135
-
136
- it("should list gardens with dirty status", async () => {
137
- accessMock.mock.resetCalls();
138
- readdirMock.mock.resetCalls();
139
- execMock.mock.resetCalls();
140
-
141
- // Mock getGitRoot and git commands
142
- execMock.mock.mockImplementation(
143
- (cmd: string, options?: { cwd?: string }) => {
144
- if (cmd === "git rev-parse --show-toplevel") {
145
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
146
- }
147
- if (cmd === "git branch --show-current") {
148
- return Promise.resolve({ stdout: "feature/dirty\n", stderr: "" });
149
- }
150
- if (cmd === "git status --porcelain") {
151
- return Promise.resolve({
152
- stdout: " M file1.ts\n?? file2.ts\n",
153
- stderr: "",
154
- }); // Dirty status with 2 files
155
- }
156
- return Promise.resolve({ stdout: "", stderr: "" });
157
- },
158
- );
159
-
160
- // Mock gardens directory and contents
161
- accessMock.mock.mockImplementation(() => Promise.resolve());
162
- readdirMock.mock.mockImplementation(() =>
163
- Promise.resolve(["dirty-garden"]),
164
- );
165
-
166
- const result = await listGardens();
167
-
168
- strictEqual(result.success, true);
169
- strictEqual(result.gardens?.length, 1);
170
- strictEqual(result.gardens?.[0].name, "dirty-garden");
171
- strictEqual(result.gardens?.[0].branch, "feature/dirty");
172
- strictEqual(result.gardens?.[0].status, "dirty");
173
- strictEqual(result.gardens?.[0].changedFiles, 2);
174
- });
175
-
176
- it("should handle git command errors gracefully", async () => {
177
- accessMock.mock.resetCalls();
178
- readdirMock.mock.resetCalls();
179
- execMock.mock.resetCalls();
180
-
181
- // Mock getGitRoot and failing git commands
182
- execMock.mock.mockImplementation((cmd: string) => {
183
- if (cmd === "git rev-parse --show-toplevel") {
184
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
185
- }
186
- if (cmd === "git branch --show-current") {
187
- return Promise.reject(new Error("Not a git repository"));
188
- }
189
- if (cmd === "git status --porcelain") {
190
- return Promise.reject(new Error("Git command failed"));
191
- }
192
- return Promise.resolve({ stdout: "", stderr: "" });
193
- });
194
-
195
- // Mock gardens directory and contents
196
- accessMock.mock.mockImplementation(() => Promise.resolve());
197
- readdirMock.mock.mockImplementation(() =>
198
- Promise.resolve(["error-garden"]),
199
- );
200
-
201
- const result = await listGardens();
202
-
203
- strictEqual(result.success, true);
204
- strictEqual(result.gardens?.length, 1);
205
- strictEqual(result.gardens?.[0].name, "error-garden");
206
- strictEqual(result.gardens?.[0].branch, "unknown");
207
- strictEqual(result.gardens?.[0].status, "clean");
208
- });
209
-
210
- it("should handle detached HEAD state", async () => {
211
- accessMock.mock.resetCalls();
212
- readdirMock.mock.resetCalls();
213
- execMock.mock.resetCalls();
214
-
215
- // Mock getGitRoot and git commands
216
- execMock.mock.mockImplementation((cmd: string) => {
217
- if (cmd === "git rev-parse --show-toplevel") {
218
- return Promise.resolve({ stdout: "/test/repo\n", stderr: "" });
219
- }
220
- if (cmd === "git branch --show-current") {
221
- return Promise.resolve({ stdout: "\n", stderr: "" }); // Empty output = detached HEAD
222
- }
223
- if (cmd === "git status --porcelain") {
224
- return Promise.resolve({ stdout: "", stderr: "" });
225
- }
226
- return Promise.resolve({ stdout: "", stderr: "" });
227
- });
228
-
229
- // Mock gardens directory and contents
230
- accessMock.mock.mockImplementation(() => Promise.resolve());
231
- readdirMock.mock.mockImplementation(() =>
232
- Promise.resolve(["detached-garden"]),
233
- );
234
-
235
- const result = await listGardens();
236
-
237
- strictEqual(result.success, true);
238
- strictEqual(result.gardens?.length, 1);
239
- strictEqual(result.gardens?.[0].name, "detached-garden");
240
- strictEqual(result.gardens?.[0].branch, "detached HEAD");
241
- strictEqual(result.gardens?.[0].status, "clean");
242
- });
243
- });