gitforest 0.1.0 → 1.1.0

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.
Files changed (184) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +24 -4
  3. package/src/components/onboarding/DirectoriesStep.tsx +19 -19
  4. package/src/github/auth.ts +3 -3
  5. package/src/utils/debug.ts +4 -4
  6. package/.bunignore +0 -7
  7. package/.github/workflows/ci.yml +0 -73
  8. package/CLAUDE.md +0 -111
  9. package/CONTRIBUTING.md +0 -145
  10. package/bun.lock +0 -267
  11. package/bunfig.toml +0 -15
  12. package/cli +0 -0
  13. package/docs/ai/IMPROVEMENT_PLAN.md +0 -341
  14. package/docs/ai/VERIFICATION_REPORT.md +0 -87
  15. package/docs/ai/architecture.md +0 -169
  16. package/docs/ai/checks/check-2025-12-02-tests.md +0 -40
  17. package/docs/ai/checks/check-2025-12-02.md +0 -55
  18. package/docs/ai/checks/test-verification-report.md +0 -85
  19. package/docs/ai/implementation-guide.md +0 -776
  20. package/docs/ai/research/gitty-codebase-analysis.md +0 -221
  21. package/docs/ai/tickets/GENERAL-sitrep.md +0 -30
  22. package/docs/ai/tickets/TASK-database-tests-sitrep.md +0 -25
  23. package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +0 -28
  24. package/docs/ai/tickets/TASK-detail-modal-sitrep.md +0 -28
  25. package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +0 -24
  26. package/docs/ai/tickets/TASK-github-service-sitrep.md +0 -32
  27. package/docs/ai/tickets/TASK-github-token-sitrep.md +0 -51
  28. package/docs/ai/tickets/TASK-hascommits-sitrep.md +0 -35
  29. package/docs/ai/tickets/TASK-keybindings-sitrep.md +0 -26
  30. package/docs/ai/tickets/TASK-layout-sitrep.md +0 -25
  31. package/docs/ai/tickets/TASK-markdown-sitrep.md +0 -28
  32. package/docs/ai/tickets/TASK-project-item-sitrep.md +0 -79
  33. package/docs/ai/tickets/TASK-sitrep.md +0 -28
  34. package/docs/ai/tickets/TASK-state-sitrep.md +0 -26
  35. package/docs/ai/tickets/TASK-types-sitrep.md +0 -25
  36. package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +0 -26
  37. package/docs/ai/tickets/TKT-001-sitrep.md +0 -24
  38. package/docs/ai/tickets/TKT-002-sitrep.md +0 -25
  39. package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +0 -46
  40. package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +0 -135
  41. package/docs/ai/tickets/TKT-003-sitrep.md +0 -26
  42. package/docs/ai/tickets/TKT-004-sitrep.md +0 -27
  43. package/docs/ai/tickets/TKT-005-sitrep.md +0 -25
  44. package/docs/ai/tickets/TKT-006-sitrep.md +0 -26
  45. package/docs/ai/tickets/TKT-007-sitrep.md +0 -30
  46. package/docs/ai/tickets/TKT-008-sitrep.md +0 -32
  47. package/docs/ai/tickets/TKT-009-sitrep.md +0 -27
  48. package/docs/ai/tickets/TKT-010-sitrep.md +0 -27
  49. package/docs/ai/tickets/TKT-011-sitrep.md +0 -26
  50. package/docs/ai/tickets/TKT-012-sitrep.md +0 -25
  51. package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +0 -28
  52. package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +0 -25
  53. package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +0 -25
  54. package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +0 -24
  55. package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +0 -29
  56. package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +0 -29
  57. package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +0 -26
  58. package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +0 -30
  59. package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +0 -28
  60. package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +0 -26
  61. package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +0 -25
  62. package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +0 -30
  63. package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +0 -25
  64. package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +0 -29
  65. package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +0 -95
  66. package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +0 -61
  67. package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +0 -30
  68. package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +0 -27
  69. package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +0 -25
  70. package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +0 -27
  71. package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +0 -25
  72. package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +0 -28
  73. package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +0 -25
  74. package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +0 -26
  75. package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +0 -25
  76. package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +0 -26
  77. package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +0 -24
  78. package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +0 -25
  79. package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +0 -29
  80. package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +0 -25
  81. package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +0 -24
  82. package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +0 -24
  83. package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +0 -27
  84. package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +0 -25
  85. package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +0 -25
  86. package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +0 -27
  87. package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +0 -32
  88. package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +0 -27
  89. package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +0 -30
  90. package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +0 -25
  91. package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +0 -29
  92. package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +0 -25
  93. package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +0 -27
  94. package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +0 -25
  95. package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +0 -26
  96. package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +0 -25
  97. package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +0 -27
  98. package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +0 -75
  99. package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +0 -29
  100. package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +0 -29
  101. package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +0 -25
  102. package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +0 -25
  103. package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +0 -32
  104. package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +0 -64
  105. package/docs/ai/tkt-001-fix-database-error.md +0 -217
  106. package/docs/ai/ui-enhancement-plan.md +0 -562
  107. package/test/integration/app.isolated.tsx +0 -240
  108. package/test/integration/cli-commands.test.ts +0 -287
  109. package/test/integration/cli-validation.test.ts +0 -264
  110. package/test/integration/git-operations.test.ts +0 -218
  111. package/test/integration/scanner.test.ts +0 -228
  112. package/test/preload.ts +0 -18
  113. package/test/unit/cli/commands.test.ts +0 -13
  114. package/test/unit/cli/formatters.test.ts +0 -1116
  115. package/test/unit/cli/github-commands.test.ts +0 -12
  116. package/test/unit/components/CloneDialog.test.tsx +0 -240
  117. package/test/unit/components/ColumnHeader.test.tsx +0 -128
  118. package/test/unit/components/CommandPalette.test.tsx +0 -355
  119. package/test/unit/components/ConfirmDialog.test.tsx +0 -111
  120. package/test/unit/components/ErrorBoundary.test.tsx +0 -139
  121. package/test/unit/components/FilterBar.test.tsx +0 -43
  122. package/test/unit/components/FilterOptionsOverlay.test.tsx +0 -197
  123. package/test/unit/components/HelpOverlay.test.tsx +0 -90
  124. package/test/unit/components/Layout.test.tsx +0 -328
  125. package/test/unit/components/MarkdownRenderer.test.tsx +0 -45
  126. package/test/unit/components/ProgressBar.test.tsx +0 -138
  127. package/test/unit/components/ProjectItem.test.tsx +0 -182
  128. package/test/unit/components/ProjectList.test.tsx +0 -311
  129. package/test/unit/components/RepoDetailModal.test.tsx +0 -445
  130. package/test/unit/components/StatusBar.test.tsx +0 -112
  131. package/test/unit/components/UnifiedProjectItem.test.tsx +0 -618
  132. package/test/unit/components/ViewModeIndicator.test.tsx +0 -137
  133. package/test/unit/components/test-utils.tsx +0 -63
  134. package/test/unit/config/loader.test.ts +0 -692
  135. package/test/unit/db/database.test.ts +0 -978
  136. package/test/unit/db/index.test.ts +0 -314
  137. package/test/unit/fixtures/setup.ts +0 -186
  138. package/test/unit/git/commands-untested.test.ts +0 -205
  139. package/test/unit/git/commands.test.ts +0 -269
  140. package/test/unit/git/operations.test.ts +0 -322
  141. package/test/unit/git/status.test.ts +0 -219
  142. package/test/unit/github/auth.test.ts +0 -317
  143. package/test/unit/github/cache.test.ts +0 -1028
  144. package/test/unit/github/cli.test.ts +0 -135
  145. package/test/unit/github/unified.test.ts +0 -1201
  146. package/test/unit/graceful-shutdown.test.ts +0 -83
  147. package/test/unit/hooks/useBackgroundFetch.test.tsx +0 -239
  148. package/test/unit/hooks/useConfirmDialogActions.test.tsx +0 -81
  149. package/test/unit/hooks/useKeyBindings.isolated.ts +0 -715
  150. package/test/unit/hooks/useProjects.test.tsx +0 -186
  151. package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +0 -115
  152. package/test/unit/hooks/useUnifiedRepos.test.tsx +0 -177
  153. package/test/unit/mocks/config.ts +0 -109
  154. package/test/unit/mocks/git-service.ts +0 -274
  155. package/test/unit/mocks/github-service.ts +0 -250
  156. package/test/unit/mocks/index.ts +0 -72
  157. package/test/unit/mocks/project.ts +0 -148
  158. package/test/unit/mocks/state-mocks.ts +0 -187
  159. package/test/unit/mocks/unified.ts +0 -169
  160. package/test/unit/operations/batch.test.ts +0 -216
  161. package/test/unit/operations/commands.test.ts +0 -550
  162. package/test/unit/scanner/errors.test.ts +0 -297
  163. package/test/unit/scanner/index.test.ts +0 -1011
  164. package/test/unit/scanner/markers.test.ts +0 -150
  165. package/test/unit/scanner/submodules.test.ts +0 -99
  166. package/test/unit/services/git-errors.test.ts +0 -190
  167. package/test/unit/services/git.test.ts +0 -442
  168. package/test/unit/services/github-errors.test.ts +0 -293
  169. package/test/unit/services/github.test.ts +0 -200
  170. package/test/unit/state/actions.test.ts +0 -217
  171. package/test/unit/state/reducer.test.ts +0 -745
  172. package/test/unit/state/store.test.tsx +0 -711
  173. package/test/unit/types/commands.test.ts +0 -220
  174. package/test/unit/types/schema.test.ts +0 -179
  175. package/test/unit/utils/array.test.ts +0 -73
  176. package/test/unit/utils/debug.test.ts +0 -23
  177. package/test/unit/utils/errors.test.ts +0 -295
  178. package/test/unit/utils/markdown.test.ts +0 -163
  179. package/test/unit/utils/project-utils.test.ts +0 -756
  180. package/test/unit/utils/rate-limiter.test.ts +0 -256
  181. package/test/unit/utils/retry.test.ts +0 -165
  182. package/test/unit/utils/strip-ansi.ts +0 -13
  183. package/test/unit/utils/timeout.test.ts +0 -93
  184. package/tsconfig.json +0 -29
@@ -1,1116 +0,0 @@
1
- /**
2
- * Tests for CLI formatters
3
- */
4
-
5
- import { describe, test, expect } from "bun:test";
6
- import {
7
- getProjectTypeIcon,
8
- formatProjectStatus,
9
- formatProject,
10
- formatBatchResult,
11
- formatProgress,
12
- formatWarning,
13
- formatError,
14
- formatInfo,
15
- formatSuccess,
16
- formatScanning,
17
- formatOperationItem,
18
- formatOperationSummary,
19
- calculateStatusSummary,
20
- calculateProjectStats,
21
- formatProjectStats,
22
- formatProjectList,
23
- formatStatusSummary,
24
- formatStatusSummaryJson,
25
- formatDirtyRepos,
26
- getSourceIcon,
27
- formatUnifiedRepoStatus,
28
- formatUnifiedRepo,
29
- formatUnifiedStats,
30
- formatAuthSuccess,
31
- formatAuthFailure,
32
- formatNoToken,
33
- formatCloneItem,
34
- } from "../../../src/cli/formatters.ts";
35
- import {
36
- createMockProject,
37
- createDirtyProject,
38
- createAheadProject,
39
- createNonGitProject,
40
- createSubmoduleProject,
41
- defaultMockStatus,
42
- createDirtyStatus,
43
- createBehindStatus,
44
- createNoRemoteStatus,
45
- } from "../mocks/index.ts";
46
-
47
- describe("CLI formatters", () => {
48
- describe("getProjectTypeIcon", () => {
49
- test("returns green dot for git", () => {
50
- const icon = getProjectTypeIcon("git");
51
- expect(icon).toContain("●");
52
- });
53
-
54
- test("returns magenta circle for submodule", () => {
55
- const icon = getProjectTypeIcon("git-submodule");
56
- expect(icon).toContain("○");
57
- });
58
-
59
- test("returns gray dash for non-git", () => {
60
- const icon = getProjectTypeIcon("non-git");
61
- expect(icon).toContain("-");
62
- });
63
- });
64
-
65
- describe("formatProjectStatus", () => {
66
- test("returns 'clean' for clean project", () => {
67
- const project = createMockProject();
68
- const status = formatProjectStatus(project);
69
-
70
- expect(status).toContain("clean");
71
- });
72
-
73
- test("includes modified count for dirty project", () => {
74
- const project = createDirtyProject();
75
- const status = formatProjectStatus(project);
76
-
77
- expect(status).toContain("M");
78
- });
79
-
80
- test("includes unpushed indicator for ahead project", () => {
81
- const project = createAheadProject("test", 3);
82
- const status = formatProjectStatus(project);
83
-
84
- expect(status).toContain("↑");
85
- expect(status).toContain("3");
86
- });
87
-
88
- test("includes unpulled indicator for behind project", () => {
89
- const project = createMockProject({
90
- status: createBehindStatus(2),
91
- });
92
- const status = formatProjectStatus(project);
93
-
94
- expect(status).toContain("↓");
95
- expect(status).toContain("2");
96
- });
97
-
98
- test("includes no-remote for project without remote", () => {
99
- const project = createMockProject({
100
- status: createNoRemoteStatus(),
101
- });
102
- const status = formatProjectStatus(project);
103
-
104
- expect(status).toContain("no-remote");
105
- });
106
-
107
- test("handles non-git project", () => {
108
- const project = createNonGitProject("npm-project");
109
- const status = formatProjectStatus(project);
110
-
111
- expect(status).toContain("clean");
112
- });
113
-
114
- test("includes untracked files count", () => {
115
- const project = createMockProject({
116
- status: {
117
- ...defaultMockStatus,
118
- untrackedCount: 5,
119
- hasUntrackedFiles: true,
120
- },
121
- });
122
- const status = formatProjectStatus(project);
123
-
124
- expect(status).toContain("5?");
125
- });
126
-
127
- test("combines multiple status indicators", () => {
128
- const project = createMockProject({
129
- status: {
130
- ...createDirtyStatus(),
131
- unpushedCommits: 2,
132
- isAhead: true,
133
- unpulledCommits: 1,
134
- isBehind: true,
135
- untrackedCount: 3,
136
- hasUntrackedFiles: true,
137
- },
138
- });
139
- const status = formatProjectStatus(project);
140
-
141
- expect(status).toContain("2M");
142
- expect(status).toContain("3?");
143
- expect(status).toContain("↑2");
144
- expect(status).toContain("↓1");
145
- });
146
- });
147
-
148
- describe("formatProject", () => {
149
- test("includes project name", () => {
150
- const project = createMockProject({ name: "my-project" });
151
- const output = formatProject(project);
152
-
153
- expect(output).toContain("my-project");
154
- });
155
-
156
- test("includes type icon", () => {
157
- const project = createMockProject();
158
- const output = formatProject(project);
159
-
160
- expect(output).toContain("●");
161
- });
162
-
163
- test("includes path in verbose mode", () => {
164
- const project = createMockProject({
165
- name: "test",
166
- path: "/home/user/test",
167
- });
168
- const output = formatProject(project, true);
169
-
170
- expect(output).toContain("/home/user/test");
171
- });
172
-
173
- test("includes branch in verbose mode", () => {
174
- const project = createMockProject({
175
- name: "test",
176
- status: {
177
- ...defaultMockStatus,
178
- currentBranch: "feature-branch",
179
- },
180
- });
181
- const output = formatProject(project, true);
182
-
183
- expect(output).toContain("feature-branch");
184
- });
185
-
186
- test("handles missing branch in verbose mode", () => {
187
- const project = createNonGitProject("test");
188
- const output = formatProject(project, true);
189
-
190
- expect(output).toContain("N/A");
191
- });
192
- });
193
-
194
- describe("formatBatchResult", () => {
195
- test("formats successful batch", () => {
196
- const result = {
197
- total: 5,
198
- successful: 5,
199
- failed: 0,
200
- results: [],
201
- duration: 1000,
202
- };
203
-
204
- const output = formatBatchResult(result, "Pull");
205
-
206
- expect(output).toContain("Pull");
207
- expect(output).toContain("5/5");
208
- expect(output).toContain("1000ms");
209
- });
210
-
211
- test("formats batch with failures", () => {
212
- const result = {
213
- total: 5,
214
- successful: 3,
215
- failed: 2,
216
- results: [],
217
- duration: 2000,
218
- };
219
-
220
- const output = formatBatchResult(result, "Push");
221
-
222
- expect(output).toContain("Push");
223
- expect(output).toContain("3/5");
224
- expect(output).toContain("2 failed");
225
- expect(output).toContain("2000ms");
226
- });
227
-
228
- test("formats all failed batch", () => {
229
- const result = {
230
- total: 3,
231
- successful: 0,
232
- failed: 3,
233
- results: [],
234
- duration: 500,
235
- };
236
-
237
- const output = formatBatchResult(result, "Fetch");
238
-
239
- expect(output).toContain("Fetch");
240
- expect(output).toContain("0/3");
241
- expect(output).toContain("3 failed");
242
- expect(output).toContain("500ms");
243
- });
244
-
245
- test("includes error details for failed operations", () => {
246
- const result = {
247
- total: 2,
248
- successful: 1,
249
- failed: 1,
250
- results: [
251
- { projectPath: "/project1", success: true, operation: "pull", message: "Success", duration: 100 },
252
- { projectPath: "/project2", success: false, operation: "pull", error: "Permission denied", duration: 50 },
253
- ],
254
- duration: 150,
255
- };
256
-
257
- const output = formatBatchResult(result, "Pull");
258
-
259
- expect(output).toContain("Permission denied");
260
- expect(output).toContain("/project2");
261
- });
262
- });
263
-
264
- describe("formatProgress", () => {
265
- test("formats progress with completed and total", () => {
266
- const output = formatProgress(50, 100);
267
-
268
- expect(output).toContain("50");
269
- expect(output).toContain("100");
270
- expect(output).toContain("Progress:");
271
- });
272
-
273
- test("handles zero total", () => {
274
- const output = formatProgress(0, 0);
275
-
276
- expect(output).toContain("0/0");
277
- });
278
-
279
- test("handles completion", () => {
280
- const output = formatProgress(100, 100);
281
-
282
- expect(output).toContain("100/100");
283
- });
284
- });
285
-
286
- describe("message formatters", () => {
287
- test("formatWarning includes warning text", () => {
288
- const output = formatWarning("This is a warning");
289
-
290
- expect(output).toContain("This is a warning");
291
- });
292
-
293
- test("formatError includes error text", () => {
294
- const output = formatError("Something went wrong");
295
-
296
- expect(output).toContain("Something went wrong");
297
- });
298
-
299
- test("formatInfo includes info text", () => {
300
- const output = formatInfo("Information message");
301
-
302
- expect(output).toContain("Information message");
303
- });
304
-
305
- test("formatSuccess includes success text", () => {
306
- const output = formatSuccess("Operation completed");
307
-
308
- expect(output).toContain("Operation completed");
309
- });
310
-
311
- test("formatScanning includes scanning text", () => {
312
- const output = formatScanning("Scanning repositories...");
313
-
314
- expect(output).toContain("Scanning repositories...");
315
- });
316
-
317
- test("formatScanning uses default message", () => {
318
- const output = formatScanning();
319
-
320
- expect(output).toContain("Scanning directories...");
321
- });
322
- });
323
-
324
- describe("formatOperationItem", () => {
325
- test("formats successful operation", () => {
326
- const output = formatOperationItem("Deploy", true);
327
-
328
- expect(output).toContain("✓");
329
- expect(output).toContain("Deploy");
330
- });
331
-
332
- test("formats failed operation", () => {
333
- const output = formatOperationItem("Deploy", false, "Connection timeout");
334
-
335
- expect(output).toContain("✗");
336
- expect(output).toContain("Deploy");
337
- expect(output).toContain("Connection timeout");
338
- });
339
-
340
- test("formats failed operation without error", () => {
341
- const output = formatOperationItem("Deploy", false);
342
-
343
- expect(output).toContain("✗");
344
- expect(output).toContain("Deploy");
345
- expect(output).not.toContain(":");
346
- });
347
- });
348
-
349
- describe("formatOperationSummary", () => {
350
- test("formats summary with single item", () => {
351
- const output = formatOperationSummary("Deploy", 1, 1);
352
-
353
- expect(output).toContain("Deploy");
354
- expect(output).toContain("1/1");
355
- expect(output).toContain("item");
356
- });
357
-
358
- test("formats summary with multiple items", () => {
359
- const output = formatOperationSummary("Deploy", 5, 5);
360
-
361
- expect(output).toContain("Deploy");
362
- expect(output).toContain("5/5");
363
- expect(output).toContain("items");
364
- });
365
- });
366
-
367
- describe("calculateStatusSummary", () => {
368
- test("calculates summary for mixed projects", () => {
369
- const projects = [
370
- createMockProject({ name: "clean" }),
371
- createDirtyProject("dirty"),
372
- createAheadProject("ahead", 2),
373
- createNonGitProject("npm"),
374
- ];
375
-
376
- const summary = calculateStatusSummary(projects);
377
-
378
- expect(summary.total).toBe(4);
379
- expect(summary.dirty).toHaveLength(1);
380
- expect(summary.unpushed).toHaveLength(1);
381
- expect(summary.nonGit).toHaveLength(1);
382
- });
383
-
384
- test("identifies no-remote projects", () => {
385
- const projects = [
386
- createMockProject({ status: createNoRemoteStatus() }),
387
- createMockProject(), // Has remote
388
- ];
389
-
390
- const summary = calculateStatusSummary(projects);
391
-
392
- expect(summary.noRemote).toHaveLength(1);
393
- });
394
-
395
- test("identifies unpulled projects", () => {
396
- const projects = [
397
- createMockProject({ status: createBehindStatus(3) }),
398
- createMockProject(), // Up to date
399
- ];
400
-
401
- const summary = calculateStatusSummary(projects);
402
-
403
- expect(summary.unpulled).toHaveLength(1);
404
- });
405
-
406
- test("handles empty project list", () => {
407
- const summary = calculateStatusSummary([]);
408
-
409
- expect(summary.total).toBe(0);
410
- expect(summary.dirty).toHaveLength(0);
411
- expect(summary.unpushed).toHaveLength(0);
412
- expect(summary.unpulled).toHaveLength(0);
413
- expect(summary.noRemote).toHaveLength(0);
414
- expect(summary.nonGit).toHaveLength(0);
415
- });
416
- });
417
-
418
- describe("calculateProjectStats", () => {
419
- test("counts project types correctly", () => {
420
- const projects = [
421
- createMockProject(), // git
422
- createMockProject(), // git
423
- createSubmoduleProject("sub"),
424
- createNonGitProject("npm"),
425
- ];
426
-
427
- const stats = calculateProjectStats(projects);
428
-
429
- expect(stats.total).toBe(4);
430
- expect(stats.gitCount).toBe(2);
431
- expect(stats.submoduleCount).toBe(1);
432
- expect(stats.nonGitCount).toBe(1);
433
- });
434
-
435
- test("counts dirty and sync status", () => {
436
- const projects = [
437
- createDirtyProject(),
438
- createAheadProject("ahead"),
439
- createMockProject({ status: createBehindStatus() }),
440
- ];
441
-
442
- const stats = calculateProjectStats(projects);
443
-
444
- expect(stats.dirtyCount).toBe(1);
445
- expect(stats.unpushedCount).toBe(1);
446
- expect(stats.unpulledCount).toBe(1);
447
- });
448
-
449
- test("handles empty project list", () => {
450
- const stats = calculateProjectStats([]);
451
-
452
- expect(stats.total).toBe(0);
453
- expect(stats.gitCount).toBe(0);
454
- expect(stats.submoduleCount).toBe(0);
455
- expect(stats.nonGitCount).toBe(0);
456
- expect(stats.dirtyCount).toBe(0);
457
- expect(stats.unpushedCount).toBe(0);
458
- expect(stats.unpulledCount).toBe(0);
459
- });
460
- });
461
-
462
- describe("formatProjectStats", () => {
463
- test("formats stats with all counts", () => {
464
- const stats = {
465
- total: 10,
466
- gitCount: 6,
467
- submoduleCount: 2,
468
- nonGitCount: 2,
469
- dirtyCount: 3,
470
- unpushedCount: 1,
471
- unpulledCount: 2,
472
- };
473
-
474
- const output = formatProjectStats(stats);
475
-
476
- expect(output).toContain("6 git");
477
- expect(output).toContain("2 submodules");
478
- expect(output).toContain("2 non-git");
479
- expect(output).toContain("3 dirty");
480
- expect(output).toContain("1 unpushed");
481
- });
482
-
483
- test("formats stats with zero counts", () => {
484
- const stats = {
485
- total: 0,
486
- gitCount: 0,
487
- submoduleCount: 0,
488
- nonGitCount: 0,
489
- dirtyCount: 0,
490
- unpushedCount: 0,
491
- unpulledCount: 0,
492
- };
493
-
494
- const output = formatProjectStats(stats);
495
-
496
- expect(output).toContain("0 git");
497
- expect(output).toContain("0 submodules");
498
- expect(output).toContain("0 non-git");
499
- expect(output).toContain("0 dirty");
500
- expect(output).toContain("0 unpushed");
501
- });
502
- });
503
-
504
- describe("formatProjectList", () => {
505
- test("formats project list in default mode", () => {
506
- const projects = [
507
- createMockProject({ name: "project1" }),
508
- createDirtyProject("project2"),
509
- createNonGitProject("project3"),
510
- ];
511
-
512
- const output = formatProjectList(projects);
513
-
514
- expect(output).toContain("Found 3 projects");
515
- expect(output).toContain("project1");
516
- expect(output).toContain("project2");
517
- expect(output).toContain("project3");
518
- expect(output).toContain("---");
519
- expect(output).toContain("2 git");
520
- expect(output).toContain("1 non-git");
521
- expect(output).toContain("1 dirty");
522
- });
523
-
524
- test("formats project list in verbose mode", () => {
525
- const projects = [
526
- createMockProject({ name: "project1", path: "/path/to/project1" }),
527
- ];
528
-
529
- const output = formatProjectList(projects, { verbose: true });
530
-
531
- expect(output).toContain("Path: /path/to/project1");
532
- expect(output).toContain("Status:");
533
- expect(output).toContain("Branch:");
534
- });
535
-
536
- test("formats project list as JSON", () => {
537
- const projects = [
538
- createMockProject({ name: "project1" }),
539
- createMockProject({ name: "project2" }),
540
- ];
541
-
542
- const output = formatProjectList(projects, { json: true });
543
-
544
- const parsed = JSON.parse(output);
545
- expect(parsed).toHaveLength(2);
546
- expect(parsed[0].name).toBe("project1");
547
- expect(parsed[1].name).toBe("project2");
548
- });
549
- });
550
-
551
- describe("formatStatusSummary", () => {
552
- test("formats summary with all categories", () => {
553
- const summary = calculateStatusSummary([
554
- createMockProject({ name: "clean" }),
555
- createDirtyProject("dirty1"),
556
- createDirtyProject("dirty2"),
557
- createAheadProject("ahead1", 2),
558
- createAheadProject("ahead2", 5),
559
- createMockProject({ name: "behind", status: createBehindStatus(3) }),
560
- createMockProject({ name: "no-remote", status: createNoRemoteStatus() }),
561
- createNonGitProject("npm"),
562
- ]);
563
-
564
- const output = formatStatusSummary(summary);
565
-
566
- expect(output).toContain("Status Summary (8 projects)");
567
- expect(output).toContain("Dirty (2)");
568
- expect(output).toContain("dirty1");
569
- expect(output).toContain("dirty2");
570
- expect(output).toContain("Unpushed (2)");
571
- expect(output).toContain("ahead1 (↑2)");
572
- expect(output).toContain("Unpulled (1)");
573
- expect(output).toContain("behind (↓3)");
574
- expect(output).toContain("No Remote (1)");
575
- expect(output).toContain("no-remote");
576
- });
577
-
578
- test("shows clean message when all repos are clean", () => {
579
- const summary = calculateStatusSummary([
580
- createMockProject({ name: "clean1" }),
581
- createMockProject({ name: "clean2" }),
582
- ]);
583
-
584
- const output = formatStatusSummary(summary);
585
-
586
- expect(output).toContain("Status Summary (2 projects)");
587
- expect(output).toContain("All repositories are clean and in sync!");
588
- });
589
-
590
- test("handles empty summary", () => {
591
- const summary = calculateStatusSummary([]);
592
-
593
- const output = formatStatusSummary(summary);
594
-
595
- expect(output).toContain("Status Summary (0 projects)");
596
- });
597
- });
598
-
599
- describe("formatStatusSummaryJson", () => {
600
- test("formats summary as JSON", () => {
601
- const summary = calculateStatusSummary([
602
- createMockProject({ name: "clean" }),
603
- createDirtyProject("dirty"),
604
- createAheadProject("ahead", 2),
605
- createNonGitProject("npm"),
606
- ]);
607
-
608
- const output = formatStatusSummaryJson(summary);
609
- const parsed = JSON.parse(output);
610
-
611
- expect(parsed.total).toBe(4);
612
- expect(parsed.dirty).toEqual(["dirty"]);
613
- expect(parsed.unpushed).toEqual(["ahead"]);
614
- expect(parsed.nonGit).toEqual(["npm"]);
615
- });
616
- });
617
-
618
- describe("formatDirtyRepos", () => {
619
- test("formats empty dirty list", () => {
620
- const output = formatDirtyRepos([]);
621
-
622
- expect(output).toContain("All repositories are clean!");
623
- });
624
-
625
- test("formats dirty repos list", () => {
626
- const dirty = [
627
- createDirtyProject("project1"),
628
- createMockProject({
629
- name: "project2",
630
- status: {
631
- ...createDirtyStatus(),
632
- stagedCount: 2,
633
- untrackedCount: 1,
634
- },
635
- }),
636
- ];
637
-
638
- const output = formatDirtyRepos(dirty);
639
-
640
- expect(output).toContain("2 dirty repositories");
641
- expect(output).toContain("project1");
642
- expect(output).toContain("project2");
643
- expect(output).toContain("2 modified");
644
- expect(output).toContain("2 staged");
645
- expect(output).toContain("1 untracked");
646
- });
647
-
648
- test("formats as JSON", () => {
649
- const dirty = [
650
- createDirtyProject("project1"),
651
- createDirtyProject("project2"),
652
- ];
653
-
654
- const output = formatDirtyRepos(dirty, true);
655
- const parsed = JSON.parse(output);
656
-
657
- expect(parsed).toHaveLength(2);
658
- expect(parsed[0].name).toBe("project1");
659
- expect(parsed[1].name).toBe("project2");
660
- });
661
- });
662
-
663
- describe("getSourceIcon", () => {
664
- test("returns L for local", () => {
665
- const icon = getSourceIcon("local");
666
- expect(icon).toContain("L");
667
- });
668
-
669
- test("returns G for github", () => {
670
- const icon = getSourceIcon("github");
671
- expect(icon).toContain("G");
672
- });
673
-
674
- test("returns checkmark for both", () => {
675
- const icon = getSourceIcon("both");
676
- expect(icon).toContain("✓");
677
- });
678
- });
679
-
680
- describe("formatUnifiedRepoStatus", () => {
681
- test("shows not cloned for github-only repo", () => {
682
- const repo = {
683
- id: "test-repo",
684
- name: "test-repo",
685
- source: "github" as const,
686
- github: {
687
- name: "test-repo",
688
- fullName: "user/test-repo",
689
- owner: "user",
690
- description: null,
691
- htmlUrl: "https://github.com/user/test-repo",
692
- sshUrl: "git@github.com:user/test-repo.git",
693
- cloneUrl: "https://github.com/user/test-repo.git",
694
- isPrivate: false,
695
- isArchived: false,
696
- isFork: false,
697
- pushedAt: new Date(),
698
- updatedAt: new Date(),
699
- defaultBranch: "main",
700
- language: "TypeScript",
701
- size: 1024,
702
- },
703
- local: null,
704
- isCloned: false,
705
- isOnGitHub: true,
706
- localPath: null,
707
- };
708
-
709
- const status = formatUnifiedRepoStatus(repo);
710
-
711
- expect(status).toContain("not cloned");
712
- });
713
-
714
- test("shows local-only for repo not on GitHub", () => {
715
- const repo = {
716
- id: "test-repo",
717
- name: "test-repo",
718
- source: "local" as const,
719
- github: null,
720
- local: createMockProject({ name: "test-repo" }),
721
- isCloned: true,
722
- isOnGitHub: false,
723
- localPath: "/path/to/repo",
724
- };
725
-
726
- const status = formatUnifiedRepoStatus(repo);
727
-
728
- expect(status).toContain("local-only");
729
- });
730
-
731
- test("shows synced for clean synced repo", () => {
732
- const repo = {
733
- id: "test-repo",
734
- name: "test-repo",
735
- source: "both" as const,
736
- github: {
737
- name: "test-repo",
738
- fullName: "user/test-repo",
739
- owner: "user",
740
- description: null,
741
- htmlUrl: "https://github.com/user/test-repo",
742
- sshUrl: "git@github.com:user/test-repo.git",
743
- cloneUrl: "https://github.com/user/test-repo.git",
744
- isPrivate: false,
745
- isArchived: false,
746
- isFork: false,
747
- pushedAt: new Date(),
748
- updatedAt: new Date(),
749
- defaultBranch: "main",
750
- language: "TypeScript",
751
- size: 1024,
752
- },
753
- local: createMockProject({ name: "test-repo" }),
754
- isCloned: true,
755
- isOnGitHub: true,
756
- localPath: "/path/to/repo",
757
- };
758
-
759
- const status = formatUnifiedRepoStatus(repo);
760
-
761
- expect(status).toContain("synced");
762
- });
763
-
764
- test("shows dirty status", () => {
765
- const repo = {
766
- id: "test-repo",
767
- name: "test-repo",
768
- source: "both" as const,
769
- github: {
770
- name: "test-repo",
771
- fullName: "user/test-repo",
772
- owner: "user",
773
- description: null,
774
- htmlUrl: "https://github.com/user/test-repo",
775
- sshUrl: "git@github.com:user/test-repo.git",
776
- cloneUrl: "https://github.com/user/test-repo.git",
777
- isPrivate: false,
778
- isArchived: false,
779
- isFork: false,
780
- pushedAt: new Date(),
781
- updatedAt: new Date(),
782
- defaultBranch: "main",
783
- language: "TypeScript",
784
- size: 1024,
785
- },
786
- local: createDirtyProject("test-repo"),
787
- isCloned: true,
788
- isOnGitHub: true,
789
- localPath: "/path/to/repo",
790
- };
791
-
792
- const status = formatUnifiedRepoStatus(repo);
793
-
794
- expect(status).toContain("2M");
795
- });
796
-
797
- test("shows ahead status", () => {
798
- const repo = {
799
- id: "test-repo",
800
- name: "test-repo",
801
- source: "both" as const,
802
- github: {
803
- name: "test-repo",
804
- fullName: "user/test-repo",
805
- owner: "user",
806
- description: null,
807
- htmlUrl: "https://github.com/user/test-repo",
808
- sshUrl: "git@github.com:user/test-repo.git",
809
- cloneUrl: "https://github.com/user/test-repo.git",
810
- isPrivate: false,
811
- isArchived: false,
812
- isFork: false,
813
- pushedAt: new Date(),
814
- updatedAt: new Date(),
815
- defaultBranch: "main",
816
- language: "TypeScript",
817
- size: 1024,
818
- },
819
- local: createAheadProject("test-repo", 3),
820
- isCloned: true,
821
- isOnGitHub: true,
822
- localPath: "/path/to/repo",
823
- };
824
-
825
- const status = formatUnifiedRepoStatus(repo);
826
-
827
- expect(status).toContain("↑3");
828
- });
829
-
830
- test("shows behind status", () => {
831
- const repo = {
832
- id: "test-repo",
833
- name: "test-repo",
834
- source: "both" as const,
835
- github: {
836
- name: "test-repo",
837
- fullName: "user/test-repo",
838
- owner: "user",
839
- description: null,
840
- htmlUrl: "https://github.com/user/test-repo",
841
- sshUrl: "git@github.com:user/test-repo.git",
842
- cloneUrl: "https://github.com/user/test-repo.git",
843
- isPrivate: false,
844
- isArchived: false,
845
- isFork: false,
846
- pushedAt: new Date(),
847
- updatedAt: new Date(),
848
- defaultBranch: "main",
849
- language: "TypeScript",
850
- size: 1024,
851
- },
852
- local: createMockProject({
853
- name: "test-repo",
854
- status: createBehindStatus(2)
855
- }),
856
- isCloned: true,
857
- isOnGitHub: true,
858
- localPath: "/path/to/repo",
859
- };
860
-
861
- const status = formatUnifiedRepoStatus(repo);
862
-
863
- expect(status).toContain("↓2");
864
- });
865
- });
866
-
867
- describe("formatUnifiedRepo", () => {
868
- test("formats basic repo", () => {
869
- const repo = {
870
- id: "test-repo",
871
- name: "test-repo",
872
- source: "both" as const,
873
- github: {
874
- name: "test-repo",
875
- fullName: "user/test-repo",
876
- owner: "user",
877
- description: null,
878
- htmlUrl: "https://github.com/user/test-repo",
879
- sshUrl: "git@github.com:user/test-repo.git",
880
- cloneUrl: "https://github.com/user/test-repo.git",
881
- isPrivate: false,
882
- isArchived: false,
883
- isFork: false,
884
- pushedAt: new Date(),
885
- updatedAt: new Date(),
886
- defaultBranch: "main",
887
- language: "TypeScript",
888
- size: 1024,
889
- },
890
- local: createMockProject({ name: "test-repo" }),
891
- isCloned: true,
892
- isOnGitHub: true,
893
- localPath: "/path/to/repo",
894
- };
895
-
896
- const output = formatUnifiedRepo(repo);
897
-
898
- expect(output).toContain("test-repo");
899
- expect(output).toContain("synced");
900
- });
901
-
902
- test("formats with private indicator", () => {
903
- const repo = {
904
- id: "test-repo",
905
- name: "test-repo",
906
- source: "both" as const,
907
- github: {
908
- name: "test-repo",
909
- fullName: "user/test-repo",
910
- owner: "user",
911
- description: null,
912
- htmlUrl: "https://github.com/user/test-repo",
913
- sshUrl: "git@github.com:user/test-repo.git",
914
- cloneUrl: "https://github.com/user/test-repo.git",
915
- isPrivate: true,
916
- isArchived: false,
917
- isFork: false,
918
- pushedAt: new Date(),
919
- updatedAt: new Date(),
920
- defaultBranch: "main",
921
- language: "TypeScript",
922
- size: 1024,
923
- },
924
- local: createMockProject({ name: "test-repo" }),
925
- isCloned: true,
926
- isOnGitHub: true,
927
- localPath: "/path/to/repo",
928
- };
929
-
930
- const output = formatUnifiedRepo(repo);
931
-
932
- expect(output).toContain("test-repo");
933
- expect(output).toContain("(private)");
934
- });
935
-
936
- test("formats in verbose mode", () => {
937
- const repo = {
938
- id: "test-repo",
939
- name: "test-repo",
940
- source: "both" as const,
941
- github: {
942
- name: "test-repo",
943
- fullName: "user/test-repo",
944
- owner: "user",
945
- description: "A test repository for formatting",
946
- htmlUrl: "https://github.com/user/test-repo",
947
- sshUrl: "git@github.com:user/test-repo.git",
948
- cloneUrl: "https://github.com/user/test-repo.git",
949
- isPrivate: false,
950
- isArchived: false,
951
- isFork: false,
952
- pushedAt: new Date(),
953
- updatedAt: new Date(),
954
- defaultBranch: "main",
955
- language: "TypeScript",
956
- size: 1024,
957
- },
958
- local: createMockProject({ name: "test-repo" }),
959
- isCloned: true,
960
- isOnGitHub: true,
961
- localPath: "/path/to/repo",
962
- };
963
-
964
- const output = formatUnifiedRepo(repo, true);
965
-
966
- expect(output).toContain("test-repo");
967
- expect(output).toContain("Local: /path/to/repo");
968
- expect(output).toContain("GitHub: user/test-repo");
969
- expect(output).toContain("Desc: A test repository for formatting");
970
- expect(output).toContain("Status: synced");
971
- });
972
-
973
- test("handles github-only repo in verbose mode", () => {
974
- const repo = {
975
- id: "test-repo",
976
- name: "test-repo",
977
- source: "github" as const,
978
- github: {
979
- name: "test-repo",
980
- fullName: "user/test-repo",
981
- owner: "user",
982
- description: "Not cloned yet",
983
- htmlUrl: "https://github.com/user/test-repo",
984
- sshUrl: "git@github.com:user/test-repo.git",
985
- cloneUrl: "https://github.com/user/test-repo.git",
986
- isPrivate: false,
987
- isArchived: false,
988
- isFork: false,
989
- pushedAt: new Date(),
990
- updatedAt: new Date(),
991
- defaultBranch: "main",
992
- language: "TypeScript",
993
- size: 1024,
994
- },
995
- local: null,
996
- isCloned: false,
997
- isOnGitHub: true,
998
- localPath: null,
999
- };
1000
-
1001
- const output = formatUnifiedRepo(repo, true);
1002
-
1003
- expect(output).toContain("test-repo");
1004
- expect(output).not.toContain("Local:");
1005
- expect(output).toContain("GitHub: user/test-repo");
1006
- expect(output).toContain("Desc: Not cloned yet");
1007
- });
1008
- });
1009
-
1010
- describe("formatUnifiedStats", () => {
1011
- test("formats stats with all counts", () => {
1012
- const stats = {
1013
- total: 10,
1014
- both: 5,
1015
- localOnly: 2,
1016
- githubOnly: 3,
1017
- dirty: 1,
1018
- unpushed: 2,
1019
- unpulled: 1,
1020
- };
1021
-
1022
- const output = formatUnifiedStats(stats);
1023
-
1024
- expect(output).toContain("5 synced");
1025
- expect(output).toContain("2 local-only");
1026
- expect(output).toContain("3 github-only");
1027
- expect(output).toContain("1 dirty");
1028
- expect(output).toContain("2 unpushed");
1029
- });
1030
-
1031
- test("formats stats with zero counts", () => {
1032
- const stats = {
1033
- total: 0,
1034
- both: 0,
1035
- localOnly: 0,
1036
- githubOnly: 0,
1037
- dirty: 0,
1038
- unpushed: 0,
1039
- unpulled: 0,
1040
- };
1041
-
1042
- const output = formatUnifiedStats(stats);
1043
-
1044
- expect(output).toContain("0 synced");
1045
- expect(output).toContain("0 local-only");
1046
- expect(output).toContain("0 github-only");
1047
- expect(output).toContain("0 dirty");
1048
- expect(output).toContain("0 unpushed");
1049
- });
1050
- });
1051
-
1052
- describe("formatAuthSuccess", () => {
1053
- test("formats success with login only", () => {
1054
- const output = formatAuthSuccess("johndoe");
1055
-
1056
- expect(output).toContain("✓");
1057
- expect(output).toContain("Authenticated as johndoe");
1058
- expect(output).not.toContain("Name:");
1059
- });
1060
-
1061
- test("formats success with login and name", () => {
1062
- const output = formatAuthSuccess("johndoe", "John Doe");
1063
-
1064
- expect(output).toContain("✓");
1065
- expect(output).toContain("Authenticated as johndoe");
1066
- expect(output).toContain("Name: John Doe");
1067
- });
1068
- });
1069
-
1070
- describe("formatAuthFailure", () => {
1071
- test("formats failure without error", () => {
1072
- const output = formatAuthFailure();
1073
-
1074
- expect(output).toContain("✗");
1075
- expect(output).toContain("Authentication failed");
1076
- expect(output).not.toContain(":");
1077
- });
1078
-
1079
- test("formats failure with error", () => {
1080
- const output = formatAuthFailure("Invalid token");
1081
-
1082
- expect(output).toContain("✗");
1083
- expect(output).toContain("Authentication failed: Invalid token");
1084
- });
1085
- });
1086
-
1087
- describe("formatNoToken", () => {
1088
- test("formats no token message", () => {
1089
- const output = formatNoToken();
1090
-
1091
- expect(output).toContain("✗");
1092
- expect(output).toContain("GITHUB_TOKEN not set");
1093
- expect(output).toContain("gitforest login");
1094
- expect(output).toContain("export GITHUB_TOKEN=your_token");
1095
- });
1096
- });
1097
-
1098
- describe("formatCloneItem", () => {
1099
- test("formats successful clone", () => {
1100
- const output = formatCloneItem("user/repo", true, "/path/to/repo");
1101
-
1102
- expect(output).toContain("✓");
1103
- expect(output).toContain("user/repo");
1104
- expect(output).toContain("→ /path/to/repo");
1105
- });
1106
-
1107
- test("formats failed clone", () => {
1108
- const output = formatCloneItem("user/repo", false, undefined, "Already exists");
1109
-
1110
- expect(output).toContain("✗");
1111
- expect(output).toContain("user/repo");
1112
- expect(output).toContain("Already exists");
1113
- expect(output).not.toContain("→");
1114
- });
1115
- });
1116
- });