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,756 +0,0 @@
1
- import { describe, test, expect, beforeEach } from "bun:test";
2
- import { filterProjects, sortProjects } from "../../../src/utils/project-utils";
3
- import type { Project, GitStatus } from "../../../src/types/index";
4
- import {
5
- createMockProject,
6
- createCleanProject,
7
- createDirtyProject,
8
- createAheadProject,
9
- createNonGitProject,
10
- resetProjectIdCounter,
11
- createBehindStatus,
12
- createNoRemoteStatus,
13
- createDirtyStatus,
14
- defaultMockStatus
15
- } from "../mocks";
16
-
17
- // Reset project ID counter before each test
18
- beforeEach(() => {
19
- resetProjectIdCounter();
20
- });
21
-
22
- describe("filterProjects", () => {
23
- const mockProjects: Project[] = [
24
- {
25
- id: "1",
26
- name: "React App",
27
- path: "/home/user/projects/react-app",
28
- type: "git",
29
- projectMarker: "package.json",
30
- status: null,
31
- submodule: null,
32
- lastScanned: new Date(),
33
- lastModified: null,
34
- },
35
- {
36
- id: "2",
37
- name: "Vue Project",
38
- path: "/home/user/projects/vue-project",
39
- type: "git",
40
- projectMarker: "package.json",
41
- status: null,
42
- submodule: null,
43
- lastScanned: new Date(),
44
- lastModified: null,
45
- },
46
- {
47
- id: "3",
48
- name: "Rust Project",
49
- path: "/home/user/projects/rust-project",
50
- type: "git",
51
- projectMarker: "Cargo.toml",
52
- status: null,
53
- submodule: null,
54
- lastScanned: new Date(),
55
- lastModified: null,
56
- },
57
- {
58
- id: "4",
59
- name: "Python Scripts",
60
- path: "/home/user/scripts/python",
61
- type: "non-git",
62
- projectMarker: "pyproject.toml",
63
- status: null,
64
- submodule: null,
65
- lastScanned: new Date(),
66
- lastModified: null,
67
- },
68
- ];
69
-
70
- test("filters by name", () => {
71
- const result = filterProjects(mockProjects, "react");
72
- expect(result).toHaveLength(1);
73
- expect(result[0]?.name).toBe("React App");
74
- });
75
-
76
- test("filters by name case insensitive", () => {
77
- const result = filterProjects(mockProjects, "REACT");
78
- expect(result).toHaveLength(1);
79
- expect(result[0]?.name).toBe("React App");
80
- });
81
-
82
- test("filters by path", () => {
83
- const result = filterProjects(mockProjects, "scripts");
84
- expect(result).toHaveLength(1);
85
- expect(result[0]?.path).toBe("/home/user/scripts/python");
86
- });
87
-
88
- test("filters by project marker", () => {
89
- const result = filterProjects(mockProjects, "cargo");
90
- expect(result).toHaveLength(1);
91
- expect(result[0]?.projectMarker).toBe("Cargo.toml");
92
- });
93
-
94
- test("filters by type", () => {
95
- const result = filterProjects(mockProjects, "non-git");
96
- expect(result).toHaveLength(1);
97
- expect(result[0]?.type).toBe("non-git");
98
- });
99
-
100
- test("returns all for empty filter", () => {
101
- const result = filterProjects(mockProjects, "");
102
- expect(result).toHaveLength(4);
103
- });
104
-
105
- test("returns all for whitespace-only filter", () => {
106
- const result = filterProjects(mockProjects, " ");
107
- expect(result).toHaveLength(4);
108
- });
109
-
110
- test("handles special characters", () => {
111
- const specialProject: Project = {
112
- id: "5",
113
- name: "Project@2.0",
114
- path: "/home/user/projects/project-2.0",
115
- type: "git",
116
- projectMarker: null,
117
- status: null,
118
- submodule: null,
119
- lastScanned: new Date(),
120
- lastModified: null,
121
- };
122
- const specialProjects = [...mockProjects, specialProject];
123
-
124
- const result = filterProjects(specialProjects, "@2.0");
125
- expect(result).toHaveLength(1);
126
- expect(result[0]?.name).toBe("Project@2.0");
127
- });
128
-
129
- test("handles no matches", () => {
130
- const result = filterProjects(mockProjects, "nonexistent");
131
- expect(result).toHaveLength(0);
132
- });
133
-
134
- test("handles partial matches", () => {
135
- const result = filterProjects(mockProjects, "proj");
136
- expect(result).toHaveLength(4); // React App, Vue Project, Rust Project, Python Scripts (path contains "proj")
137
- });
138
- });
139
-
140
- describe("sortProjects", () => {
141
- const mockGitStatus: GitStatus = {
142
- hasUnstagedChanges: false,
143
- hasStagedChanges: false,
144
- hasUntrackedFiles: false,
145
- modifiedCount: 0,
146
- stagedCount: 0,
147
- untrackedCount: 0,
148
- currentBranch: "main",
149
- trackingBranch: "origin/main",
150
- unpushedCommits: 0,
151
- unpulledCommits: 0,
152
- hasRemote: true,
153
- remoteUrl: "https://github.com/user/repo.git",
154
- lastLocalCommit: new Date("2024-01-15"),
155
- lastRemoteActivity: new Date("2024-01-10"),
156
- hasCommits: true,
157
- isDirty: false,
158
- isAhead: false,
159
- isBehind: false,
160
- isOutOfSync: false,
161
- };
162
-
163
- const mockProjects: Project[] = [
164
- {
165
- id: "1",
166
- name: "Alpha Project",
167
- path: "/home/user/projects/alpha",
168
- type: "git",
169
- projectMarker: "package.json",
170
- status: { ...mockGitStatus, lastLocalCommit: new Date("2024-01-15") },
171
- submodule: null,
172
- lastScanned: new Date(),
173
- lastModified: null,
174
- },
175
- {
176
- id: "2",
177
- name: "Beta Project",
178
- path: "/home/user/projects/beta",
179
- type: "git",
180
- projectMarker: "package.json",
181
- status: { ...mockGitStatus, isDirty: true, lastLocalCommit: new Date("2024-01-10") },
182
- submodule: null,
183
- lastScanned: new Date(),
184
- lastModified: null,
185
- },
186
- {
187
- id: "3",
188
- name: "Gamma Project",
189
- path: "/home/user/projects/gamma",
190
- type: "git",
191
- projectMarker: "package.json",
192
- status: { ...mockGitStatus, isBehind: true, lastLocalCommit: new Date("2024-01-20") },
193
- submodule: null,
194
- lastScanned: new Date(),
195
- lastModified: null,
196
- },
197
- {
198
- id: "4",
199
- name: "Delta Project",
200
- path: "/home/user/projects/delta",
201
- type: "non-git",
202
- projectMarker: "package.json",
203
- status: null,
204
- submodule: null,
205
- lastScanned: new Date(),
206
- lastModified: new Date("2024-01-05"),
207
- },
208
- ];
209
-
210
- describe("sort by name", () => {
211
- test("sorts ascending", () => {
212
- const result = sortProjects(mockProjects, "name", "asc");
213
- expect(result[0]?.name).toBe("Alpha Project");
214
- expect(result[1]?.name).toBe("Beta Project");
215
- expect(result[2]?.name).toBe("Delta Project");
216
- expect(result[3]?.name).toBe("Gamma Project");
217
- });
218
-
219
- test("sorts descending", () => {
220
- const result = sortProjects(mockProjects, "name", "desc");
221
- expect(result[0]?.name).toBe("Gamma Project");
222
- expect(result[1]?.name).toBe("Delta Project");
223
- expect(result[2]?.name).toBe("Beta Project");
224
- expect(result[3]?.name).toBe("Alpha Project");
225
- });
226
- });
227
-
228
- describe("sort by status", () => {
229
- test("sorts by priority descending (most attention needed first)", () => {
230
- const result = sortProjects(mockProjects, "status", "desc");
231
- // Beta (dirty) should come first, then Gamma (behind), then Alpha (clean), then Delta (non-git)
232
- expect(result[0]?.name).toBe("Beta Project"); // dirty
233
- expect(result[1]?.name).toBe("Gamma Project"); // behind
234
- expect(result[2]?.name).toBe("Alpha Project"); // clean
235
- expect(result[3]?.name).toBe("Delta Project"); // non-git
236
- });
237
-
238
- test("sorts by priority ascending (least attention needed first)", () => {
239
- const result = sortProjects(mockProjects, "status", "asc");
240
- // Delta (non-git) should come first, then Alpha (clean), then Gamma (behind), then Beta (dirty)
241
- expect(result[0]?.name).toBe("Delta Project"); // non-git
242
- expect(result[1]?.name).toBe("Alpha Project"); // clean
243
- expect(result[2]?.name).toBe("Gamma Project"); // behind
244
- expect(result[3]?.name).toBe("Beta Project"); // dirty
245
- });
246
- });
247
-
248
- describe("sort by lastActivity", () => {
249
- test("sorts descending (most recent first)", () => {
250
- const result = sortProjects(mockProjects, "lastActivity", "desc");
251
- expect(result[0]?.name).toBe("Gamma Project"); // Jan 20
252
- expect(result[1]?.name).toBe("Alpha Project"); // Jan 15
253
- expect(result[2]?.name).toBe("Beta Project"); // Jan 10
254
- expect(result[3]?.name).toBe("Delta Project"); // no status
255
- });
256
-
257
- test("sorts ascending (oldest first)", () => {
258
- const result = sortProjects(mockProjects, "lastActivity", "asc");
259
- expect(result[0]?.name).toBe("Delta Project"); // no status
260
- expect(result[1]?.name).toBe("Beta Project"); // Jan 10
261
- expect(result[2]?.name).toBe("Alpha Project"); // Jan 15
262
- expect(result[3]?.name).toBe("Gamma Project"); // Jan 20
263
- });
264
-
265
- test("handles null dates", () => {
266
- const projectsWithNulls: Project[] = [
267
- ...mockProjects.slice(0, 3),
268
- {
269
- ...mockProjects[3]!,
270
- status: null,
271
- },
272
- ];
273
-
274
- const result = sortProjects(projectsWithNulls, "lastActivity", "desc");
275
- // Projects with null dates should be at the end for desc
276
- expect(result[3]?.name).toBe("Delta Project");
277
- });
278
-
279
- test("handles string timestamps", () => {
280
- const projectsWithStrings: Project[] = mockProjects.map(p => ({
281
- ...p,
282
- status: p.status ? {
283
- ...p.status,
284
- lastLocalCommit: p.status.lastLocalCommit ? new Date(p.status.lastLocalCommit) : null,
285
- } : null,
286
- }));
287
-
288
- const result = sortProjects(projectsWithStrings, "lastActivity", "desc");
289
- expect(result[0]?.name).toBe("Gamma Project");
290
- expect(result[3]?.name).toBe("Delta Project");
291
- });
292
- });
293
-
294
- test("handles empty array", () => {
295
- const result = sortProjects([], "name", "asc");
296
- expect(result).toEqual([]);
297
- });
298
-
299
- test("handles single item", () => {
300
- const result = sortProjects([mockProjects[0]!], "name", "asc");
301
- expect(result).toHaveLength(1);
302
- expect(result[0]?.name).toBe("Alpha Project");
303
- });
304
- });
305
-
306
- describe("Enhanced sortProjects tests", () => {
307
- describe("sort by name", () => {
308
- test("sorts by name ascending with mixed case", () => {
309
- const projects = [
310
- createMockProject({ name: "zebra" }),
311
- createMockProject({ name: "Alpha" }),
312
- createMockProject({ name: "beta" }),
313
- createMockProject({ name: "Charlie" }),
314
- ];
315
-
316
- const result = sortProjects(projects, "name", "asc");
317
- expect(result[0]?.name).toBe("Alpha");
318
- expect(result[1]?.name).toBe("beta");
319
- expect(result[2]?.name).toBe("Charlie");
320
- expect(result[3]?.name).toBe("zebra");
321
- });
322
-
323
- test("sorts by name descending with mixed case", () => {
324
- const projects = [
325
- createMockProject({ name: "zebra" }),
326
- createMockProject({ name: "Alpha" }),
327
- createMockProject({ name: "beta" }),
328
- createMockProject({ name: "Charlie" }),
329
- ];
330
-
331
- const result = sortProjects(projects, "name", "desc");
332
- expect(result[0]?.name).toBe("zebra");
333
- expect(result[1]?.name).toBe("Charlie");
334
- expect(result[2]?.name).toBe("beta");
335
- expect(result[3]?.name).toBe("Alpha");
336
- });
337
-
338
- test("handles projects with same name prefix", () => {
339
- const projects = [
340
- createMockProject({ name: "project-alpha" }),
341
- createMockProject({ name: "project-beta" }),
342
- createMockProject({ name: "project-gamma" }),
343
- ];
344
-
345
- const result = sortProjects(projects, "name", "asc");
346
- expect(result[0]?.name).toBe("project-alpha");
347
- expect(result[1]?.name).toBe("project-beta");
348
- expect(result[2]?.name).toBe("project-gamma");
349
- });
350
- });
351
-
352
- describe("sort by status priority", () => {
353
- test("dirty projects have highest priority in desc", () => {
354
- const projects = [
355
- createCleanProject("clean"),
356
- createDirtyProject("dirty"),
357
- createAheadProject("ahead", 2),
358
- ];
359
-
360
- const result = sortProjects(projects, "status", "desc");
361
- expect(result[0]?.name).toBe("dirty");
362
- expect(result[1]?.name).toBe("ahead");
363
- expect(result[2]?.name).toBe("clean");
364
- });
365
-
366
- test("behind projects have high priority in desc", () => {
367
- const behindProject = createMockProject({
368
- name: "behind",
369
- type: "git",
370
- status: {
371
- ...createBehindStatus(3),
372
- isBehind: true,
373
- unpulledCommits: 3,
374
- },
375
- });
376
-
377
- const projects = [
378
- createCleanProject("clean"),
379
- behindProject,
380
- createAheadProject("ahead", 1),
381
- ];
382
-
383
- const result = sortProjects(projects, "status", "desc");
384
- expect(result[0]?.name).toBe("behind");
385
- expect(result[1]?.name).toBe("ahead");
386
- expect(result[2]?.name).toBe("clean");
387
- });
388
-
389
- test("non-git projects have lowest priority", () => {
390
- const projects = [
391
- createCleanProject("clean"),
392
- createDirtyProject("dirty"),
393
- createNonGitProject("non-git"),
394
- ];
395
-
396
- const result = sortProjects(projects, "status", "desc");
397
- expect(result[0]?.name).toBe("dirty");
398
- expect(result[1]?.name).toBe("clean");
399
- expect(result[2]?.name).toBe("non-git");
400
- });
401
-
402
- test("projects without status have low priority", () => {
403
- const noStatusProject = createMockProject({
404
- name: "no-status",
405
- type: "git",
406
- status: null,
407
- });
408
-
409
- const projects = [
410
- createCleanProject("clean"),
411
- noStatusProject,
412
- createDirtyProject("dirty"),
413
- ];
414
-
415
- const result = sortProjects(projects, "status", "desc");
416
- expect(result[0]?.name).toBe("dirty");
417
- expect(result[1]?.name).toBe("clean");
418
- expect(result[2]?.name).toBe("no-status");
419
- });
420
-
421
- test("priority combines correctly (dirty + ahead)", () => {
422
- const dirtyAheadProject = createMockProject({
423
- name: "dirty-ahead",
424
- type: "git",
425
- status: {
426
- ...createDirtyStatus(),
427
- unpushedCommits: 5,
428
- isAhead: true,
429
- isOutOfSync: true,
430
- },
431
- });
432
-
433
- const justDirtyProject = createDirtyProject("just-dirty");
434
- const justAheadProject = createAheadProject("just-ahead", 5);
435
-
436
- const projects = [
437
- justAheadProject,
438
- justDirtyProject,
439
- dirtyAheadProject,
440
- ];
441
-
442
- const result = sortProjects(projects, "status", "desc");
443
- // dirty+ahead should come before just-dirty due to combined priority
444
- expect(result[0]?.name).toBe("dirty-ahead");
445
- expect(result[1]?.name).toBe("just-dirty");
446
- expect(result[2]?.name).toBe("just-ahead");
447
- });
448
-
449
- test("no remote projects have medium priority", () => {
450
- const noRemoteProject = createMockProject({
451
- name: "no-remote",
452
- type: "git",
453
- status: createNoRemoteStatus(),
454
- });
455
-
456
- const projects = [
457
- createCleanProject("clean"),
458
- noRemoteProject,
459
- createDirtyProject("dirty"),
460
- ];
461
-
462
- const result = sortProjects(projects, "status", "desc");
463
- expect(result[0]?.name).toBe("dirty");
464
- expect(result[1]?.name).toBe("no-remote");
465
- expect(result[2]?.name).toBe("clean");
466
- });
467
- });
468
-
469
- describe("sort by lastActivity", () => {
470
- test("handles Date objects for lastLocalCommit", () => {
471
- const projects = [
472
- createMockProject({
473
- name: "old",
474
- type: "git",
475
- status: {
476
- ...createCleanProject("").status!,
477
- lastLocalCommit: new Date("2024-01-01"),
478
- },
479
- }),
480
- createMockProject({
481
- name: "new",
482
- type: "git",
483
- status: {
484
- ...createCleanProject("").status!,
485
- lastLocalCommit: new Date("2024-12-31"),
486
- },
487
- }),
488
- ];
489
-
490
- const result = sortProjects(projects, "lastActivity", "desc");
491
- expect(result[0]?.name).toBe("new");
492
- expect(result[1]?.name).toBe("old");
493
- });
494
-
495
- test("handles string timestamps for lastLocalCommit", () => {
496
- const projects = [
497
- createMockProject({
498
- name: "old",
499
- type: "git",
500
- status: {
501
- ...defaultMockStatus,
502
- lastLocalCommit: new Date("2024-01-01T00:00:00Z"),
503
- },
504
- }),
505
- createMockProject({
506
- name: "new",
507
- type: "git",
508
- status: {
509
- ...defaultMockStatus,
510
- lastLocalCommit: new Date("2024-12-31T23:59:59Z"),
511
- },
512
- }),
513
- ];
514
-
515
- const result = sortProjects(projects, "lastActivity", "desc");
516
- expect(result[0]?.name).toBe("new");
517
- expect(result[1]?.name).toBe("old");
518
- });
519
-
520
- test("handles null lastLocalCommit", () => {
521
- const projects = [
522
- createMockProject({
523
- name: "no-commit-date",
524
- type: "git",
525
- status: {
526
- ...createCleanProject("").status!,
527
- lastLocalCommit: null,
528
- },
529
- }),
530
- createMockProject({
531
- name: "with-commit-date",
532
- type: "git",
533
- status: {
534
- ...createCleanProject("").status!,
535
- lastLocalCommit: new Date("2024-06-15"),
536
- },
537
- }),
538
- ];
539
-
540
- const result = sortProjects(projects, "lastActivity", "desc");
541
- // Projects with null dates should be at the end for desc
542
- expect(result[0]?.name).toBe("with-commit-date");
543
- expect(result[1]?.name).toBe("no-commit-date");
544
- });
545
-
546
- test("sorts by lastActivity ascending (oldest first)", () => {
547
- const projects = [
548
- createMockProject({
549
- name: "new",
550
- type: "git",
551
- status: {
552
- ...createCleanProject("").status!,
553
- lastLocalCommit: new Date("2024-12-31"),
554
- },
555
- }),
556
- createMockProject({
557
- name: "old",
558
- type: "git",
559
- status: {
560
- ...createCleanProject("").status!,
561
- lastLocalCommit: new Date("2024-01-01"),
562
- },
563
- }),
564
- createMockProject({
565
- name: "middle",
566
- type: "git",
567
- status: {
568
- ...createCleanProject("").status!,
569
- lastLocalCommit: new Date("2024-06-15"),
570
- },
571
- }),
572
- ];
573
-
574
- const result = sortProjects(projects, "lastActivity", "asc");
575
- expect(result[0]?.name).toBe("old");
576
- expect(result[1]?.name).toBe("middle");
577
- expect(result[2]?.name).toBe("new");
578
- });
579
-
580
- test("non-git projects are sorted to the end", () => {
581
- const projects = [
582
- createNonGitProject("non-git-1"),
583
- createMockProject({
584
- name: "git-project",
585
- type: "git",
586
- status: {
587
- ...createCleanProject("").status!,
588
- lastLocalCommit: new Date("2024-01-01"),
589
- },
590
- }),
591
- createNonGitProject("non-git-2"),
592
- ];
593
-
594
- const result = sortProjects(projects, "lastActivity", "desc");
595
- expect(result[0]?.name).toBe("git-project");
596
- expect(result[1]?.type).toBe("non-git");
597
- expect(result[2]?.type).toBe("non-git");
598
- });
599
- });
600
- });
601
-
602
- describe("Enhanced filterProjects tests", () => {
603
- test("filter by name is case insensitive", () => {
604
- const projects = [
605
- createMockProject({ name: "ReactApp" }),
606
- createMockProject({ name: "vueapp" }),
607
- createMockProject({ name: "ANGULAR_APP" }),
608
- ];
609
-
610
- const result = filterProjects(projects, "app");
611
- expect(result).toHaveLength(3);
612
- expect(result.map(p => p.name)).toEqual(["ReactApp", "vueapp", "ANGULAR_APP"]);
613
- });
614
-
615
- test("filter by path", () => {
616
- const projects = [
617
- createMockProject({ path: "/home/user/projects/react" }),
618
- createMockProject({ path: "/home/user/work/vue" }),
619
- createMockProject({ path: "/home/user/projects/angular" }),
620
- ];
621
-
622
- const result = filterProjects(projects, "projects");
623
- expect(result).toHaveLength(2);
624
- expect(result.map(p => p.path)).toContain("/home/user/projects/react");
625
- expect(result.map(p => p.path)).toContain("/home/user/projects/angular");
626
- });
627
-
628
- test("filter by projectMarker", () => {
629
- const projects = [
630
- createMockProject({ projectMarker: "package.json" }),
631
- createMockProject({ projectMarker: "Cargo.toml" }),
632
- createMockProject({ projectMarker: "pyproject.toml" }),
633
- ];
634
-
635
- const result = filterProjects(projects, "toml");
636
- expect(result).toHaveLength(2);
637
- expect(result.map(p => p.projectMarker)).toContain("Cargo.toml");
638
- expect(result.map(p => p.projectMarker)).toContain("pyproject.toml");
639
- });
640
-
641
- test("filter by type", () => {
642
- const projects = [
643
- createMockProject({ type: "git" }),
644
- createMockProject({ type: "non-git" }),
645
- createMockProject({ type: "git-submodule" }),
646
- ];
647
-
648
- const result = filterProjects(projects, "git");
649
- expect(result).toHaveLength(3); // Matches "git" in both "git" and "git-submodule" types
650
- expect(result.map(p => p.type)).toContain("git");
651
- expect(result.map(p => p.type)).toContain("git-submodule");
652
- });
653
-
654
- test("empty filter returns all projects", () => {
655
- const projects = [
656
- createMockProject({ name: "project1" }),
657
- createMockProject({ name: "project2" }),
658
- createMockProject({ name: "project3" }),
659
- ];
660
-
661
- const result = filterProjects(projects, "");
662
- expect(result).toHaveLength(3);
663
- });
664
-
665
- test("whitespace-only filter returns all projects", () => {
666
- const projects = [
667
- createMockProject({ name: "project1" }),
668
- createMockProject({ name: "project2" }),
669
- ];
670
-
671
- const result = filterProjects(projects, " \n\t ");
672
- expect(result).toHaveLength(2);
673
- });
674
-
675
- test("no matches returns empty array", () => {
676
- const projects = [
677
- createMockProject({ name: "react" }),
678
- createMockProject({ name: "vue" }),
679
- ];
680
-
681
- const result = filterProjects(projects, "angular");
682
- expect(result).toHaveLength(0);
683
- });
684
-
685
- test("partial matches work", () => {
686
- const projects = [
687
- createMockProject({ name: "my-react-app" }),
688
- createMockProject({ name: "react-component" }),
689
- createMockProject({ name: "vue-app" }),
690
- ];
691
-
692
- const result = filterProjects(projects, "react");
693
- expect(result).toHaveLength(2);
694
- expect(result.map(p => p.name)).toContain("my-react-app");
695
- expect(result.map(p => p.name)).toContain("react-component");
696
- });
697
-
698
- test("filters across multiple fields", () => {
699
- const projects = [
700
- createMockProject({ name: "react", path: "/apps/frontend", projectMarker: "package.json" }),
701
- createMockProject({ name: "backend", path: "/apps/backend", projectMarker: "requirements.txt" }),
702
- createMockProject({ name: "mobile", path: "/apps/react-native", projectMarker: "package.json" }),
703
- ];
704
-
705
- // Should match both react projects (name and path)
706
- const result = filterProjects(projects, "react");
707
- expect(result).toHaveLength(2);
708
- });
709
-
710
- test("handles projects with null projectMarker", () => {
711
- const projects = [
712
- createMockProject({ projectMarker: null }),
713
- createMockProject({ projectMarker: "package.json" }),
714
- ];
715
-
716
- const result = filterProjects(projects, "package");
717
- expect(result).toHaveLength(1);
718
- expect(result[0]?.projectMarker).toBe("package.json");
719
- });
720
-
721
- test("handles special characters in filter", () => {
722
- const projects = [
723
- createMockProject({ name: "project@2.0" }),
724
- createMockProject({ name: "project#1" }),
725
- createMockProject({ name: "project$test" }),
726
- ];
727
-
728
- const result1 = filterProjects(projects, "@2.0");
729
- expect(result1).toHaveLength(1);
730
- expect(result1[0]?.name).toBe("project@2.0");
731
-
732
- const result2 = filterProjects(projects, "#1");
733
- expect(result2).toHaveLength(1);
734
- expect(result2[0]?.name).toBe("project#1");
735
-
736
- const result3 = filterProjects(projects, "$test");
737
- expect(result3).toHaveLength(1);
738
- expect(result3[0]?.name).toBe("project$test");
739
- });
740
-
741
- test("handles unicode characters", () => {
742
- const projects = [
743
- createMockProject({ name: "项目" }),
744
- createMockProject({ name: "プロジェクト" }),
745
- createMockProject({ name: "project" }),
746
- ];
747
-
748
- const result1 = filterProjects(projects, "项目");
749
- expect(result1).toHaveLength(1);
750
- expect(result1[0]?.name).toBe("项目");
751
-
752
- const result2 = filterProjects(projects, "プロ");
753
- expect(result2).toHaveLength(1);
754
- expect(result2[0]?.name).toBe("プロジェクト");
755
- });
756
- });