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,1201 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import {
3
- toGitHubRepoInfo,
4
- createUnifiedView,
5
- filterByViewMode,
6
- sortUnifiedRepos,
7
- filterUnifiedRepos,
8
- getUnifiedStats,
9
- cloneGitHubRepo,
10
- } from "../../../src/github/unified.ts";
11
- import type { Project, GitHubRepoInfo } from "../../../src/types/index.ts";
12
- import type { GitHubRepoData } from "../../../src/services/github.ts";
13
- import type { GitService } from "../../../src/services/git.ts";
14
- import {
15
- createMockProject,
16
- createMockGitHubRepoInfo,
17
- createMockUnifiedRepo,
18
- } from "../mocks/index.ts";
19
-
20
- describe("toGitHubRepoInfo", () => {
21
- test("converts GitHubRepoData to GitHubRepoInfo", () => {
22
- const repo: GitHubRepoData = {
23
- id: 123,
24
- name: "my-repo",
25
- fullName: "user/my-repo",
26
- description: "A test repo",
27
- htmlUrl: "https://github.com/user/my-repo",
28
- sshUrl: "git@github.com:user/my-repo.git",
29
- cloneUrl: "https://github.com/user/my-repo.git",
30
- isPrivate: true,
31
- isArchived: false,
32
- isFork: false,
33
- createdAt: "2024-01-01T00:00:00Z",
34
- updatedAt: "2024-06-01T00:00:00Z",
35
- pushedAt: "2024-06-15T00:00:00Z",
36
- size: 1024,
37
- stargazersCount: 10,
38
- forksCount: 5,
39
- openIssuesCount: 2,
40
- watchersCount: 15,
41
- topics: ["typescript", "test"],
42
- license: { name: "MIT" },
43
- hasIssues: true,
44
- hasWiki: true,
45
- hasDiscussions: false,
46
- language: "TypeScript",
47
- defaultBranch: "main",
48
- owner: {
49
- login: "user",
50
- type: "User",
51
- },
52
- };
53
-
54
- const result = toGitHubRepoInfo(repo);
55
-
56
- expect(result.name).toBe("my-repo");
57
- expect(result.fullName).toBe("user/my-repo");
58
- expect(result.owner).toBe("user");
59
- expect(result.description).toBe("A test repo");
60
- expect(result.isPrivate).toBe(true);
61
- expect(result.isArchived).toBe(false);
62
- expect(result.isFork).toBe(false);
63
- expect(result.language).toBe("TypeScript");
64
- expect(result.pushedAt).toBeInstanceOf(Date);
65
- });
66
-
67
- test("handles null pushed_at", () => {
68
- const repo: GitHubRepoData = {
69
- id: 123,
70
- name: "empty-repo",
71
- fullName: "user/empty-repo",
72
- description: null,
73
- htmlUrl: "https://github.com/user/empty-repo",
74
- sshUrl: "git@github.com:user/empty-repo.git",
75
- cloneUrl: "https://github.com/user/empty-repo.git",
76
- isPrivate: false,
77
- isArchived: false,
78
- isFork: false,
79
- createdAt: "2024-01-01T00:00:00Z",
80
- updatedAt: "2024-01-01T00:00:00Z",
81
- pushedAt: null,
82
- size: 0,
83
- stargazersCount: 0,
84
- forksCount: 0,
85
- openIssuesCount: 0,
86
- watchersCount: 0,
87
- topics: [],
88
- license: null,
89
- hasIssues: true,
90
- hasWiki: true,
91
- hasDiscussions: false,
92
- language: null,
93
- defaultBranch: "main",
94
- owner: {
95
- login: "user",
96
- type: "User",
97
- },
98
- };
99
-
100
- const result = toGitHubRepoInfo(repo);
101
-
102
- expect(result.pushedAt).toBeNull();
103
- expect(result.description).toBeNull();
104
- expect(result.language).toBeNull();
105
- });
106
-
107
- test("handles missing optional fields", () => {
108
- const repo: GitHubRepoData = {
109
- id: 123,
110
- name: "minimal-repo",
111
- fullName: "user/minimal-repo",
112
- description: "Minimal repo",
113
- htmlUrl: "https://github.com/user/minimal-repo",
114
- sshUrl: "git@github.com:user/minimal-repo.git",
115
- cloneUrl: "https://github.com/user/minimal-repo.git",
116
- isPrivate: false,
117
- isArchived: false,
118
- isFork: false,
119
- createdAt: "2024-01-01T00:00:00Z",
120
- updatedAt: "2024-01-01T00:00:00Z",
121
- pushedAt: "2024-01-01T00:00:00Z",
122
- size: 100,
123
- stargazersCount: 0,
124
- forksCount: 0,
125
- openIssuesCount: 0,
126
- watchersCount: 0,
127
- topics: [],
128
- license: null,
129
- hasIssues: false,
130
- hasWiki: false,
131
- hasDiscussions: false,
132
- language: null,
133
- defaultBranch: "main",
134
- owner: {
135
- login: "user",
136
- type: "User",
137
- },
138
- };
139
-
140
- const result = toGitHubRepoInfo(repo);
141
-
142
- expect(result.license).toBeNull();
143
- expect(result.hasIssues).toBe(false);
144
- expect(result.hasWiki).toBe(false);
145
- expect(result.hasDiscussions).toBe(false);
146
- });
147
- });
148
-
149
- describe("createUnifiedView", () => {
150
- test("matches local projects with GitHub repos", () => {
151
- const local = [
152
- createMockProject({
153
- name: "my-repo",
154
- status: {
155
- ...createMockProject().status!,
156
- remoteUrl: "git@github.com:user/my-repo.git",
157
- hasRemote: true
158
- }
159
- }),
160
- ];
161
- const github = [createMockGitHubRepoInfo({ name: "my-repo", owner: "user" })];
162
-
163
- const unified = createUnifiedView(local, github);
164
-
165
- expect(unified).toHaveLength(1);
166
- expect(unified[0]?.source).toBe("both");
167
- expect(unified[0]?.isCloned).toBe(true);
168
- expect(unified[0]?.isOnGitHub).toBe(true);
169
- });
170
-
171
- test("identifies local-only projects", () => {
172
- const local = [createMockProject({
173
- name: "local-only",
174
- status: {
175
- ...createMockProject().status!,
176
- remoteUrl: null,
177
- hasRemote: false
178
- }
179
- })];
180
- const github: GitHubRepoInfo[] = [];
181
-
182
- const unified = createUnifiedView(local, github);
183
-
184
- expect(unified).toHaveLength(1);
185
- expect(unified[0]?.source).toBe("local");
186
- expect(unified[0]?.isCloned).toBe(true);
187
- expect(unified[0]?.isOnGitHub).toBe(false);
188
- });
189
-
190
- test("identifies GitHub-only repos", () => {
191
- const local: Project[] = [];
192
- const github = [createMockGitHubRepoInfo({ name: "github-only" })];
193
-
194
- const unified = createUnifiedView(local, github);
195
-
196
- expect(unified).toHaveLength(1);
197
- expect(unified[0]?.source).toBe("github");
198
- expect(unified[0]?.isCloned).toBe(false);
199
- expect(unified[0]?.isOnGitHub).toBe(true);
200
- });
201
-
202
- test("handles mixed projects correctly", () => {
203
- const local = [
204
- createMockProject({
205
- name: "synced",
206
- status: {
207
- ...createMockProject().status!,
208
- remoteUrl: "git@github.com:user/synced.git",
209
- hasRemote: true
210
- }
211
- }),
212
- createMockProject({
213
- name: "local-only",
214
- status: {
215
- ...createMockProject().status!,
216
- remoteUrl: null,
217
- hasRemote: false
218
- }
219
- }),
220
- ];
221
- const github = [
222
- createMockGitHubRepoInfo({ name: "synced", owner: "user" }),
223
- createMockGitHubRepoInfo({ name: "github-only" }),
224
- ];
225
-
226
- const unified = createUnifiedView(local, github);
227
-
228
- expect(unified).toHaveLength(3);
229
-
230
- const synced = unified.find((r) => r.name === "synced");
231
- const localOnly = unified.find((r) => r.name === "local-only");
232
- const githubOnly = unified.find((r) => r.name === "github-only");
233
-
234
- expect(synced?.source).toBe("both");
235
- expect(localOnly?.source).toBe("local");
236
- expect(githubOnly?.source).toBe("github");
237
- });
238
-
239
- test("matches HTTPS remote URLs", () => {
240
- const local = [
241
- createMockProject({
242
- name: "https-repo",
243
- status: {
244
- ...createMockProject().status!,
245
- remoteUrl: "https://github.com/user/https-repo.git",
246
- hasRemote: true
247
- }
248
- }),
249
- ];
250
- const github = [createMockGitHubRepoInfo({ name: "https-repo", owner: "user" })];
251
-
252
- const unified = createUnifiedView(local, github);
253
-
254
- expect(unified).toHaveLength(1);
255
- expect(unified[0]?.source).toBe("both");
256
- });
257
-
258
- test("handles case-insensitive matching", () => {
259
- const local = [
260
- createMockProject({
261
- name: "MyRepo",
262
- status: {
263
- ...createMockProject().status!,
264
- remoteUrl: "git@github.com:user/MyRepo.git",
265
- hasRemote: true
266
- }
267
- }),
268
- ];
269
- const github = [createMockGitHubRepoInfo({ name: "myrepo", fullName: "user/myrepo" })];
270
-
271
- const unified = createUnifiedView(local, github);
272
-
273
- expect(unified).toHaveLength(1);
274
- expect(unified[0]?.source).toBe("both");
275
- });
276
-
277
- test("handles empty arrays", () => {
278
- const local: Project[] = [];
279
- const github: GitHubRepoInfo[] = [];
280
-
281
- const unified = createUnifiedView(local, github);
282
-
283
- expect(unified).toHaveLength(0);
284
- });
285
-
286
- test("handles multiple GitHub repos with same name but different owners", () => {
287
- const local = [
288
- createMockProject({
289
- name: "test-repo",
290
- status: {
291
- ...createMockProject().status!,
292
- remoteUrl: "git@github.com:user1/test-repo.git",
293
- hasRemote: true
294
- }
295
- }),
296
- ];
297
- const github = [
298
- createMockGitHubRepoInfo({ name: "test-repo", owner: "user1", fullName: "user1/test-repo" }),
299
- createMockGitHubRepoInfo({ name: "test-repo", owner: "user2", fullName: "user2/test-repo" }),
300
- ];
301
-
302
- const unified = createUnifiedView(local, github);
303
-
304
- expect(unified).toHaveLength(2);
305
- const matched = unified.find(r => r.source === "both");
306
- expect(matched?.github?.owner).toBe("user1");
307
- });
308
- });
309
-
310
- describe("filterByViewMode", () => {
311
- test("filters to local view", () => {
312
- const repos = [
313
- createMockUnifiedRepo({ name: "synced", source: "both", isCloned: true, isOnGitHub: true }),
314
- createMockUnifiedRepo({ name: "local-only", source: "local", isCloned: true, isOnGitHub: false }),
315
- createMockUnifiedRepo({ name: "github-only", source: "github", isCloned: false, isOnGitHub: true }),
316
- ];
317
-
318
- const filtered = filterByViewMode(repos, "local");
319
-
320
- expect(filtered).toHaveLength(2);
321
- expect(filtered.every((r) => r.isCloned)).toBe(true);
322
- });
323
-
324
- test("filters to github view", () => {
325
- const repos = [
326
- createMockUnifiedRepo({ name: "synced", source: "both", isCloned: true, isOnGitHub: true }),
327
- createMockUnifiedRepo({ name: "local-only", source: "local", isCloned: true, isOnGitHub: false }),
328
- createMockUnifiedRepo({ name: "github-only", source: "github", isCloned: false, isOnGitHub: true }),
329
- ];
330
-
331
- const filtered = filterByViewMode(repos, "github");
332
-
333
- expect(filtered).toHaveLength(2);
334
- expect(filtered.every((r) => r.isOnGitHub)).toBe(true);
335
- });
336
-
337
- test("returns all for combined view", () => {
338
- const repos = [
339
- createMockUnifiedRepo({ name: "synced", source: "both", isCloned: true, isOnGitHub: true }),
340
- createMockUnifiedRepo({ name: "local-only", source: "local", isCloned: true, isOnGitHub: false }),
341
- createMockUnifiedRepo({ name: "github-only", source: "github", isCloned: false, isOnGitHub: true }),
342
- ];
343
-
344
- const filtered = filterByViewMode(repos, "combined");
345
-
346
- expect(filtered).toHaveLength(3);
347
- });
348
- });
349
-
350
- describe("sortUnifiedRepos", () => {
351
- test("sorts by name ascending", () => {
352
- const repos = [
353
- createMockUnifiedRepo({ name: "zebra" }),
354
- createMockUnifiedRepo({ name: "alpha" }),
355
- ];
356
-
357
- const sorted = sortUnifiedRepos(repos, "name", "asc");
358
-
359
- expect(sorted[0]?.name).toBe("alpha");
360
- expect(sorted[1]?.name).toBe("zebra");
361
- });
362
-
363
- test("sorts by name descending", () => {
364
- const repos = [
365
- createMockUnifiedRepo({ name: "zebra" }),
366
- createMockUnifiedRepo({ name: "alpha" }),
367
- ];
368
-
369
- const sorted = sortUnifiedRepos(repos, "name", "desc");
370
-
371
- expect(sorted[0]?.name).toBe("zebra");
372
- expect(sorted[1]?.name).toBe("alpha");
373
- });
374
-
375
- test("sorts by status (github-only first in desc)", () => {
376
- const repos = [
377
- createMockUnifiedRepo({ name: "zebra", source: "both" }),
378
- createMockUnifiedRepo({ name: "alpha", source: "github", isCloned: false }),
379
- ];
380
-
381
- const sorted = sortUnifiedRepos(repos, "status", "desc");
382
-
383
- // GitHub-only repos should come first (not cloned = needs attention)
384
- expect(sorted[0]?.name).toBe("alpha");
385
- expect(sorted[0]?.source).toBe("github");
386
- });
387
-
388
- test("sorts by lastActivity (Date objects)", () => {
389
- const localProject = createMockProject({ name: "old-repo" });
390
- const repos = [
391
- createMockUnifiedRepo({
392
- name: "old-repo",
393
- source: "both",
394
- local: localProject,
395
- }),
396
- createMockUnifiedRepo({
397
- name: "new-repo",
398
- source: "github",
399
- local: null,
400
- github: createMockGitHubRepoInfo({ name: "new-repo", pushedAt: new Date("2024-06-01") }),
401
- }),
402
- ];
403
-
404
- // Set local commit date for old-repo
405
- if (repos[0]?.local?.status) {
406
- repos[0].local.status.lastLocalCommit = new Date("2024-01-01");
407
- }
408
-
409
- const sorted = sortUnifiedRepos(repos, "lastActivity", "desc");
410
-
411
- // Newer repo should come first
412
- expect(sorted[0]?.name).toBe("new-repo");
413
- expect(sorted[1]?.name).toBe("old-repo");
414
- });
415
-
416
- test("sorts by lastActivity (string dates)", () => {
417
- const repos = [
418
- createMockUnifiedRepo({
419
- name: "old-repo",
420
- source: "github",
421
- local: null,
422
- github: createMockGitHubRepoInfo({ name: "old-repo", pushedAt: new Date("2024-01-01") }),
423
- }),
424
- createMockUnifiedRepo({
425
- name: "new-repo",
426
- source: "github",
427
- local: null,
428
- github: createMockGitHubRepoInfo({ name: "new-repo", pushedAt: new Date("2024-06-01") }),
429
- }),
430
- ];
431
-
432
- const sorted = sortUnifiedRepos(repos, "lastActivity", "desc");
433
-
434
- // Newer repo should come first
435
- expect(sorted[0]?.name).toBe("new-repo");
436
- expect(sorted[1]?.name).toBe("old-repo");
437
- });
438
-
439
- test("sorts by stars", () => {
440
- const repos = [
441
- createMockUnifiedRepo({
442
- name: "low-stars",
443
- source: "github",
444
- local: null,
445
- github: createMockGitHubRepoInfo({ name: "low-stars", stargazersCount: 10 }),
446
- }),
447
- createMockUnifiedRepo({
448
- name: "high-stars",
449
- source: "github",
450
- local: null,
451
- github: createMockGitHubRepoInfo({ name: "high-stars", stargazersCount: 1000 }),
452
- }),
453
- ];
454
-
455
- const sorted = sortUnifiedRepos(repos, "stars", "desc");
456
-
457
- // Higher stars should come first
458
- expect(sorted[0]?.name).toBe("high-stars");
459
- expect(sorted[0]?.github?.stargazersCount).toBe(1000);
460
- expect(sorted[1]?.name).toBe("low-stars");
461
- expect(sorted[1]?.github?.stargazersCount).toBe(10);
462
- });
463
-
464
- test("sorts by size", () => {
465
- const repos = [
466
- createMockUnifiedRepo({
467
- name: "small-repo",
468
- source: "github",
469
- local: null,
470
- github: createMockGitHubRepoInfo({ name: "small-repo", size: 100 }),
471
- }),
472
- createMockUnifiedRepo({
473
- name: "large-repo",
474
- source: "github",
475
- local: null,
476
- github: createMockGitHubRepoInfo({ name: "large-repo", size: 10000 }),
477
- }),
478
- ];
479
-
480
- const sorted = sortUnifiedRepos(repos, "size", "desc");
481
-
482
- // Larger size should come first
483
- expect(sorted[0]?.name).toBe("large-repo");
484
- expect(sorted[0]?.github?.size).toBe(10000);
485
- expect(sorted[1]?.name).toBe("small-repo");
486
- expect(sorted[1]?.github?.size).toBe(100);
487
- });
488
-
489
- test("does not mutate original array", () => {
490
- const repos = [
491
- createMockUnifiedRepo({ name: "zebra" }),
492
- createMockUnifiedRepo({ name: "alpha" }),
493
- ];
494
- const originalFirst = repos[0]?.name;
495
- sortUnifiedRepos(repos, "name", "asc");
496
- expect(repos[0]?.name).toBe(originalFirst);
497
- });
498
-
499
- test("handles missing github data for stars and size sorts", () => {
500
- const repos = [
501
- createMockUnifiedRepo({
502
- name: "local-only",
503
- source: "local",
504
- local: createMockProject({ name: "local-only" }),
505
- github: null,
506
- }),
507
- createMockUnifiedRepo({
508
- name: "github-repo",
509
- source: "github",
510
- local: null,
511
- github: createMockGitHubRepoInfo({ name: "github-repo", size: 1000, stargazersCount: 100 }),
512
- }),
513
- ];
514
-
515
- const sortedByStars = sortUnifiedRepos(repos, "stars", "desc");
516
- expect(sortedByStars[0]?.name).toBe("github-repo");
517
- expect(sortedByStars[1]?.name).toBe("local-only");
518
-
519
- const sortedBySize = sortUnifiedRepos(repos, "size", "desc");
520
- expect(sortedBySize[0]?.name).toBe("github-repo");
521
- expect(sortedBySize[1]?.name).toBe("local-only");
522
- });
523
- });
524
-
525
- describe("filterUnifiedRepos", () => {
526
- test("filters by name", () => {
527
- const repos = [
528
- createMockUnifiedRepo({
529
- name: "my-awesome-project",
530
- source: "both",
531
- local: createMockProject({ name: "my-awesome-project" }),
532
- github: createMockGitHubRepoInfo({
533
- name: "my-awesome-project",
534
- description: "An awesome TypeScript project",
535
- language: "TypeScript",
536
- }),
537
- }),
538
- createMockUnifiedRepo({
539
- name: "rust-lib",
540
- source: "github",
541
- local: null,
542
- github: createMockGitHubRepoInfo({
543
- name: "rust-lib",
544
- description: "A Rust library",
545
- language: "Rust",
546
- }),
547
- }),
548
- ];
549
-
550
- const filtered = filterUnifiedRepos(repos, "awesome");
551
-
552
- expect(filtered).toHaveLength(1);
553
- expect(filtered[0]?.name).toBe("my-awesome-project");
554
- });
555
-
556
- test("filters by description", () => {
557
- const repos = [
558
- createMockUnifiedRepo({
559
- name: "my-awesome-project",
560
- source: "both",
561
- local: createMockProject({ name: "my-awesome-project" }),
562
- github: createMockGitHubRepoInfo({
563
- name: "my-awesome-project",
564
- description: "An awesome TypeScript project",
565
- language: "TypeScript",
566
- }),
567
- }),
568
- createMockUnifiedRepo({
569
- name: "rust-lib",
570
- source: "github",
571
- local: null,
572
- github: createMockGitHubRepoInfo({
573
- name: "rust-lib",
574
- description: "A Rust library",
575
- language: "Rust",
576
- }),
577
- }),
578
- ];
579
-
580
- const filtered = filterUnifiedRepos(repos, "library");
581
-
582
- expect(filtered).toHaveLength(1);
583
- expect(filtered[0]?.name).toBe("rust-lib");
584
- });
585
-
586
- test("filters by language", () => {
587
- const repos = [
588
- createMockUnifiedRepo({
589
- name: "my-awesome-project",
590
- source: "both",
591
- local: createMockProject({ name: "my-awesome-project" }),
592
- github: createMockGitHubRepoInfo({
593
- name: "my-awesome-project",
594
- description: "An awesome TypeScript project",
595
- language: "TypeScript",
596
- }),
597
- }),
598
- createMockUnifiedRepo({
599
- name: "rust-lib",
600
- source: "github",
601
- local: null,
602
- github: createMockGitHubRepoInfo({
603
- name: "rust-lib",
604
- description: "A Rust library",
605
- language: "Rust",
606
- }),
607
- }),
608
- ];
609
-
610
- const filtered = filterUnifiedRepos(repos, "rust");
611
-
612
- expect(filtered).toHaveLength(1);
613
- expect(filtered[0]?.name).toBe("rust-lib");
614
- });
615
-
616
- test("filters by path", () => {
617
- const repos = [
618
- createMockUnifiedRepo({
619
- name: "my-project",
620
- source: "local",
621
- local: createMockProject({ name: "my-project", path: "/home/user/projects/my-project" }),
622
- localPath: "/home/user/projects/my-project",
623
- }),
624
- createMockUnifiedRepo({
625
- name: "other-project",
626
- source: "local",
627
- local: createMockProject({ name: "other-project", path: "/home/user/other/other-project" }),
628
- localPath: "/home/user/other/other-project",
629
- }),
630
- ];
631
-
632
- const filtered = filterUnifiedRepos(repos, "projects");
633
-
634
- expect(filtered).toHaveLength(1);
635
- expect(filtered[0]?.name).toBe("my-project");
636
- });
637
-
638
- test("filters by full name", () => {
639
- const repos = [
640
- createMockUnifiedRepo({
641
- name: "my-project",
642
- source: "github",
643
- local: null,
644
- github: createMockGitHubRepoInfo({ name: "my-project", fullName: "user1/my-project" }),
645
- }),
646
- createMockUnifiedRepo({
647
- name: "other-project",
648
- source: "github",
649
- local: null,
650
- github: createMockGitHubRepoInfo({ name: "other-project", fullName: "user2/other-project" }),
651
- }),
652
- ];
653
-
654
- const filtered = filterUnifiedRepos(repos, "user1");
655
-
656
- expect(filtered).toHaveLength(1);
657
- expect(filtered[0]?.name).toBe("my-project");
658
- });
659
-
660
- test("filters by source type", () => {
661
- const repos = [
662
- createMockUnifiedRepo({ name: "local-repo", source: "local" }),
663
- createMockUnifiedRepo({ name: "github-repo", source: "github" }),
664
- createMockUnifiedRepo({ name: "synced-repo", source: "both" }),
665
- ];
666
-
667
- const filtered = filterUnifiedRepos(repos, "local");
668
-
669
- // Should only match "local" repos (exact match)
670
- expect(filtered).toHaveLength(1);
671
- expect(filtered[0]?.name).toBe("local-repo");
672
- expect(filtered[0]?.source).toBe("local");
673
- });
674
-
675
- test("filter is case-insensitive", () => {
676
- const repos = [
677
- createMockUnifiedRepo({
678
- name: "my-awesome-project",
679
- source: "both",
680
- local: createMockProject({ name: "my-awesome-project" }),
681
- github: createMockGitHubRepoInfo({
682
- name: "my-awesome-project",
683
- description: "An awesome TypeScript project",
684
- language: "TypeScript",
685
- }),
686
- }),
687
- createMockUnifiedRepo({
688
- name: "rust-lib",
689
- source: "github",
690
- local: null,
691
- github: createMockGitHubRepoInfo({
692
- name: "rust-lib",
693
- description: "A Rust library",
694
- language: "Rust",
695
- }),
696
- }),
697
- ];
698
-
699
- const filtered = filterUnifiedRepos(repos, "TYPESCRIPT");
700
-
701
- expect(filtered).toHaveLength(1);
702
- expect(filtered[0]?.name).toBe("my-awesome-project");
703
- });
704
-
705
- test("returns all when filter is empty", () => {
706
- const repos = [
707
- createMockUnifiedRepo({
708
- name: "my-awesome-project",
709
- source: "both",
710
- local: createMockProject({ name: "my-awesome-project" }),
711
- github: createMockGitHubRepoInfo({
712
- name: "my-awesome-project",
713
- description: "An awesome TypeScript project",
714
- language: "TypeScript",
715
- }),
716
- }),
717
- createMockUnifiedRepo({
718
- name: "rust-lib",
719
- source: "github",
720
- local: null,
721
- github: createMockGitHubRepoInfo({
722
- name: "rust-lib",
723
- description: "A Rust library",
724
- language: "Rust",
725
- }),
726
- }),
727
- ];
728
-
729
- const filtered = filterUnifiedRepos(repos, "");
730
-
731
- expect(filtered).toHaveLength(2);
732
- });
733
-
734
- test("handles null values in github data", () => {
735
- const repos = [
736
- createMockUnifiedRepo({
737
- name: "no-description",
738
- source: "github",
739
- local: null,
740
- github: createMockGitHubRepoInfo({ name: "no-description", description: null, language: null }),
741
- }),
742
- ];
743
-
744
- const filteredByDescription = filterUnifiedRepos(repos, "something");
745
- expect(filteredByDescription).toHaveLength(0);
746
-
747
- const filteredByLanguage = filterUnifiedRepos(repos, "javascript");
748
- expect(filteredByLanguage).toHaveLength(0);
749
- });
750
- });
751
-
752
- describe("getUnifiedStats", () => {
753
- test("calculates stats correctly", () => {
754
- const repos = [
755
- createMockUnifiedRepo({
756
- name: "synced",
757
- source: "both",
758
- local: createMockProject({ name: "synced", status: { ...createMockProject().status!, isDirty: true, isAhead: true } }),
759
- }),
760
- createMockUnifiedRepo({
761
- name: "local-only",
762
- source: "local",
763
- local: createMockProject({ name: "local-only" }),
764
- }),
765
- createMockUnifiedRepo({
766
- name: "github-only",
767
- source: "github",
768
- local: null,
769
- }),
770
- ];
771
-
772
- const stats = getUnifiedStats(repos);
773
-
774
- expect(stats.total).toBe(3);
775
- expect(stats.both).toBe(1);
776
- expect(stats.localOnly).toBe(1);
777
- expect(stats.githubOnly).toBe(1);
778
- expect(stats.dirty).toBe(1);
779
- expect(stats.unpushed).toBe(1);
780
- expect(stats.unpulled).toBe(0);
781
- });
782
-
783
- test("handles empty repos", () => {
784
- const stats = getUnifiedStats([]);
785
-
786
- expect(stats.total).toBe(0);
787
- expect(stats.both).toBe(0);
788
- expect(stats.localOnly).toBe(0);
789
- expect(stats.githubOnly).toBe(0);
790
- });
791
-
792
- test("counts multiple dirty repos", () => {
793
- const repos = [
794
- createMockUnifiedRepo({
795
- name: "dirty1",
796
- source: "local",
797
- local: createMockProject({ name: "dirty1", status: { ...createMockProject().status!, isDirty: true } }),
798
- }),
799
- createMockUnifiedRepo({
800
- name: "dirty2",
801
- source: "local",
802
- local: createMockProject({ name: "dirty2", status: { ...createMockProject().status!, isDirty: true } }),
803
- }),
804
- createMockUnifiedRepo({
805
- name: "clean",
806
- source: "local",
807
- local: createMockProject({ name: "clean", status: { ...createMockProject().status!, isDirty: false } }),
808
- }),
809
- ];
810
-
811
- const stats = getUnifiedStats(repos);
812
-
813
- expect(stats.dirty).toBe(2);
814
- });
815
-
816
- test("counts repos that are both ahead and behind", () => {
817
- const repos = [
818
- createMockUnifiedRepo({
819
- name: "ahead-repo",
820
- source: "local",
821
- local: createMockProject({ name: "ahead-repo", status: { ...createMockProject().status!, isAhead: true } }),
822
- }),
823
- createMockUnifiedRepo({
824
- name: "behind-repo",
825
- source: "local",
826
- local: createMockProject({ name: "behind-repo", status: { ...createMockProject().status!, isBehind: true } }),
827
- }),
828
- createMockUnifiedRepo({
829
- name: "synced-repo",
830
- source: "local",
831
- local: createMockProject({ name: "synced-repo", status: { ...createMockProject().status!, isAhead: false, isBehind: false } }),
832
- }),
833
- ];
834
-
835
- const stats = getUnifiedStats(repos);
836
-
837
- expect(stats.unpushed).toBe(1);
838
- expect(stats.unpulled).toBe(1);
839
- });
840
- });
841
-
842
- describe("cloneGitHubRepo", () => {
843
- test("successfully clones a GitHub repo", async () => {
844
- const mockGitService: GitService = {
845
- clone: async () => ({
846
- success: true,
847
- projectPath: "/tmp/test-repo",
848
- operation: "clone",
849
- duration: 100
850
- }),
851
- isGitRepo: async () => true,
852
- getGitRoot: async () => "/test/repo",
853
- isSubmodule: async () => false,
854
- getSubmoduleParent: async () => null,
855
- getStatus: async () => ({
856
- isDirty: false,
857
- hasUnstagedChanges: false,
858
- hasStagedChanges: false,
859
- hasUntrackedFiles: false,
860
- modifiedCount: 0,
861
- stagedCount: 0,
862
- untrackedCount: 0,
863
- currentBranch: "main",
864
- trackingBranch: null,
865
- unpushedCommits: 0,
866
- unpulledCommits: 0,
867
- hasRemote: true,
868
- remoteUrl: null,
869
- lastLocalCommit: new Date(),
870
- lastRemoteActivity: null,
871
- hasCommits: true,
872
- isAhead: false,
873
- isBehind: false,
874
- isOutOfSync: false,
875
- }),
876
- getStatusPorcelain: async () => "",
877
- getCurrentBranch: async () => "main",
878
- getTrackingBranch: async () => null,
879
- countUnpushedCommits: async () => 0,
880
- countUnpulledCommits: async () => 0,
881
- getRemoteUrl: async () => null,
882
- getLastCommitDate: async () => new Date(),
883
- getRemoteLastCommitDate: async () => new Date(),
884
- listSubmodules: async () => [],
885
- init: async () => ({
886
- success: true,
887
- projectPath: "/tmp/test-repo",
888
- operation: "init",
889
- duration: 100
890
- }),
891
- pull: async () => ({
892
- success: true,
893
- projectPath: "/tmp/test-repo",
894
- operation: "pull",
895
- duration: 100
896
- }),
897
- push: async () => ({
898
- success: true,
899
- projectPath: "/tmp/test-repo",
900
- operation: "push",
901
- duration: 100
902
- }),
903
- fetch: async () => ({
904
- success: true,
905
- projectPath: "/tmp/test-repo",
906
- operation: "fetch",
907
- duration: 100
908
- }),
909
- fetchAll: async () => ({
910
- success: true,
911
- projectPath: "/tmp/test-repo",
912
- operation: "fetchAll",
913
- duration: 100
914
- }),
915
- addRemote: async () => ({
916
- success: true,
917
- projectPath: "/tmp/test-repo",
918
- operation: "addRemote",
919
- duration: 100
920
- }),
921
- };
922
-
923
- const repo = createMockUnifiedRepo({ name: "test-repo", source: "github" });
924
- const result = await cloneGitHubRepo(repo, "/tmp", true, mockGitService);
925
-
926
- expect(result.success).toBe(true);
927
- expect(result.path).toBe("/tmp/test-repo");
928
- });
929
-
930
- test("handles clone failure", async () => {
931
- const mockGitService: GitService = {
932
- clone: async () => ({
933
- success: false,
934
- error: "Clone failed",
935
- projectPath: "/tmp/test-repo",
936
- operation: "clone",
937
- duration: 100
938
- }),
939
- isGitRepo: async () => true,
940
- getGitRoot: async () => "/test/repo",
941
- isSubmodule: async () => false,
942
- getSubmoduleParent: async () => null,
943
- getStatus: async () => ({
944
- isDirty: false,
945
- hasUnstagedChanges: false,
946
- hasStagedChanges: false,
947
- hasUntrackedFiles: false,
948
- modifiedCount: 0,
949
- stagedCount: 0,
950
- untrackedCount: 0,
951
- currentBranch: "main",
952
- trackingBranch: null,
953
- unpushedCommits: 0,
954
- unpulledCommits: 0,
955
- hasRemote: true,
956
- remoteUrl: null,
957
- lastLocalCommit: new Date(),
958
- lastRemoteActivity: null,
959
- hasCommits: true,
960
- isAhead: false,
961
- isBehind: false,
962
- isOutOfSync: false,
963
- }),
964
- getStatusPorcelain: async () => "",
965
- getCurrentBranch: async () => "main",
966
- getTrackingBranch: async () => null,
967
- countUnpushedCommits: async () => 0,
968
- countUnpulledCommits: async () => 0,
969
- getRemoteUrl: async () => null,
970
- getLastCommitDate: async () => new Date(),
971
- getRemoteLastCommitDate: async () => new Date(),
972
- listSubmodules: async () => [],
973
- init: async () => ({
974
- success: true,
975
- projectPath: "/tmp/test-repo",
976
- operation: "init",
977
- duration: 100
978
- }),
979
- pull: async () => ({
980
- success: true,
981
- projectPath: "/tmp/test-repo",
982
- operation: "pull",
983
- duration: 100
984
- }),
985
- push: async () => ({
986
- success: true,
987
- projectPath: "/tmp/test-repo",
988
- operation: "push",
989
- duration: 100
990
- }),
991
- fetch: async () => ({
992
- success: true,
993
- projectPath: "/tmp/test-repo",
994
- operation: "fetch",
995
- duration: 100
996
- }),
997
- fetchAll: async () => ({
998
- success: true,
999
- projectPath: "/tmp/test-repo",
1000
- operation: "fetchAll",
1001
- duration: 100
1002
- }),
1003
- addRemote: async () => ({
1004
- success: true,
1005
- projectPath: "/tmp/test-repo",
1006
- operation: "addRemote",
1007
- duration: 100
1008
- }),
1009
- };
1010
-
1011
- const repo = createMockUnifiedRepo({ name: "test-repo", source: "github" });
1012
- const result = await cloneGitHubRepo(repo, "/tmp", true, mockGitService);
1013
-
1014
- expect(result.success).toBe(false);
1015
- expect(result.error).toBe("Clone failed");
1016
- });
1017
-
1018
- test("returns error when repo has no GitHub info", async () => {
1019
- const repo = createMockUnifiedRepo({ name: "test-repo", source: "local", github: null });
1020
- const result = await cloneGitHubRepo(repo, "/tmp");
1021
-
1022
- expect(result.success).toBe(false);
1023
- expect(result.error).toBe("No GitHub info available for this repo");
1024
- });
1025
-
1026
- test("uses HTTPS URL when useSSH is false", async () => {
1027
- const mockGitService: GitService = {
1028
- clone: async (url: string, targetDir: string) => {
1029
- // Verify HTTPS URL is used
1030
- expect(url).toBe("https://github.com/user/test-repo.git");
1031
- return {
1032
- success: true,
1033
- projectPath: targetDir,
1034
- operation: "clone",
1035
- duration: 100
1036
- };
1037
- },
1038
- isGitRepo: async () => true,
1039
- getGitRoot: async () => "/test/repo",
1040
- isSubmodule: async () => false,
1041
- getSubmoduleParent: async () => null,
1042
- getStatus: async () => ({
1043
- isDirty: false,
1044
- hasUnstagedChanges: false,
1045
- hasStagedChanges: false,
1046
- hasUntrackedFiles: false,
1047
- modifiedCount: 0,
1048
- stagedCount: 0,
1049
- untrackedCount: 0,
1050
- currentBranch: "main",
1051
- trackingBranch: null,
1052
- unpushedCommits: 0,
1053
- unpulledCommits: 0,
1054
- hasRemote: true,
1055
- remoteUrl: null,
1056
- lastLocalCommit: new Date(),
1057
- lastRemoteActivity: null,
1058
- hasCommits: true,
1059
- isAhead: false,
1060
- isBehind: false,
1061
- isOutOfSync: false,
1062
- }),
1063
- getStatusPorcelain: async () => "",
1064
- getCurrentBranch: async () => "main",
1065
- getTrackingBranch: async () => null,
1066
- countUnpushedCommits: async () => 0,
1067
- countUnpulledCommits: async () => 0,
1068
- getRemoteUrl: async () => null,
1069
- getLastCommitDate: async () => new Date(),
1070
- getRemoteLastCommitDate: async () => new Date(),
1071
- listSubmodules: async () => [],
1072
- init: async () => ({
1073
- success: true,
1074
- projectPath: "/tmp/test-repo",
1075
- operation: "init",
1076
- duration: 100
1077
- }),
1078
- pull: async () => ({
1079
- success: true,
1080
- projectPath: "/tmp/test-repo",
1081
- operation: "pull",
1082
- duration: 100
1083
- }),
1084
- push: async () => ({
1085
- success: true,
1086
- projectPath: "/tmp/test-repo",
1087
- operation: "push",
1088
- duration: 100
1089
- }),
1090
- fetch: async () => ({
1091
- success: true,
1092
- projectPath: "/tmp/test-repo",
1093
- operation: "fetch",
1094
- duration: 100
1095
- }),
1096
- fetchAll: async () => ({
1097
- success: true,
1098
- projectPath: "/tmp/test-repo",
1099
- operation: "fetchAll",
1100
- duration: 100
1101
- }),
1102
- addRemote: async () => ({
1103
- success: true,
1104
- projectPath: "/tmp/test-repo",
1105
- operation: "addRemote",
1106
- duration: 100
1107
- }),
1108
- };
1109
-
1110
- const repo = createMockUnifiedRepo({ name: "test-repo", source: "github" });
1111
- await cloneGitHubRepo(repo, "/tmp", false, mockGitService);
1112
- });
1113
-
1114
- test("uses SSH URL when useSSH is true", async () => {
1115
- const mockGitService: GitService = {
1116
- clone: async (url: string, targetDir: string) => {
1117
- // Verify SSH URL is used
1118
- expect(url).toBe("git@github.com:user/test-repo.git");
1119
- return {
1120
- success: true,
1121
- projectPath: targetDir,
1122
- operation: "clone",
1123
- duration: 100
1124
- };
1125
- },
1126
- isGitRepo: async () => true,
1127
- getGitRoot: async () => "/test/repo",
1128
- isSubmodule: async () => false,
1129
- getSubmoduleParent: async () => null,
1130
- getStatus: async () => ({
1131
- isDirty: false,
1132
- hasUnstagedChanges: false,
1133
- hasStagedChanges: false,
1134
- hasUntrackedFiles: false,
1135
- modifiedCount: 0,
1136
- stagedCount: 0,
1137
- untrackedCount: 0,
1138
- currentBranch: "main",
1139
- trackingBranch: null,
1140
- unpushedCommits: 0,
1141
- unpulledCommits: 0,
1142
- hasRemote: true,
1143
- remoteUrl: null,
1144
- lastLocalCommit: new Date(),
1145
- lastRemoteActivity: null,
1146
- hasCommits: true,
1147
- isAhead: false,
1148
- isBehind: false,
1149
- isOutOfSync: false,
1150
- }),
1151
- getStatusPorcelain: async () => "",
1152
- getCurrentBranch: async () => "main",
1153
- getTrackingBranch: async () => null,
1154
- countUnpushedCommits: async () => 0,
1155
- countUnpulledCommits: async () => 0,
1156
- getRemoteUrl: async () => null,
1157
- getLastCommitDate: async () => new Date(),
1158
- getRemoteLastCommitDate: async () => new Date(),
1159
- listSubmodules: async () => [],
1160
- init: async () => ({
1161
- success: true,
1162
- projectPath: "/tmp/test-repo",
1163
- operation: "init",
1164
- duration: 100
1165
- }),
1166
- pull: async () => ({
1167
- success: true,
1168
- projectPath: "/tmp/test-repo",
1169
- operation: "pull",
1170
- duration: 100
1171
- }),
1172
- push: async () => ({
1173
- success: true,
1174
- projectPath: "/tmp/test-repo",
1175
- operation: "push",
1176
- duration: 100
1177
- }),
1178
- fetch: async () => ({
1179
- success: true,
1180
- projectPath: "/tmp/test-repo",
1181
- operation: "fetch",
1182
- duration: 100
1183
- }),
1184
- fetchAll: async () => ({
1185
- success: true,
1186
- projectPath: "/tmp/test-repo",
1187
- operation: "fetchAll",
1188
- duration: 100
1189
- }),
1190
- addRemote: async () => ({
1191
- success: true,
1192
- projectPath: "/tmp/test-repo",
1193
- operation: "addRemote",
1194
- duration: 100
1195
- }),
1196
- };
1197
-
1198
- const repo = createMockUnifiedRepo({ name: "test-repo", source: "github" });
1199
- await cloneGitHubRepo(repo, "/tmp", true, mockGitService);
1200
- });
1201
- });