gitforest 0.1.0 → 1.0.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 (183) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +24 -4
  3. package/src/github/auth.ts +3 -3
  4. package/src/utils/debug.ts +4 -4
  5. package/.bunignore +0 -7
  6. package/.github/workflows/ci.yml +0 -73
  7. package/CLAUDE.md +0 -111
  8. package/CONTRIBUTING.md +0 -145
  9. package/bun.lock +0 -267
  10. package/bunfig.toml +0 -15
  11. package/cli +0 -0
  12. package/docs/ai/IMPROVEMENT_PLAN.md +0 -341
  13. package/docs/ai/VERIFICATION_REPORT.md +0 -87
  14. package/docs/ai/architecture.md +0 -169
  15. package/docs/ai/checks/check-2025-12-02-tests.md +0 -40
  16. package/docs/ai/checks/check-2025-12-02.md +0 -55
  17. package/docs/ai/checks/test-verification-report.md +0 -85
  18. package/docs/ai/implementation-guide.md +0 -776
  19. package/docs/ai/research/gitty-codebase-analysis.md +0 -221
  20. package/docs/ai/tickets/GENERAL-sitrep.md +0 -30
  21. package/docs/ai/tickets/TASK-database-tests-sitrep.md +0 -25
  22. package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +0 -28
  23. package/docs/ai/tickets/TASK-detail-modal-sitrep.md +0 -28
  24. package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +0 -24
  25. package/docs/ai/tickets/TASK-github-service-sitrep.md +0 -32
  26. package/docs/ai/tickets/TASK-github-token-sitrep.md +0 -51
  27. package/docs/ai/tickets/TASK-hascommits-sitrep.md +0 -35
  28. package/docs/ai/tickets/TASK-keybindings-sitrep.md +0 -26
  29. package/docs/ai/tickets/TASK-layout-sitrep.md +0 -25
  30. package/docs/ai/tickets/TASK-markdown-sitrep.md +0 -28
  31. package/docs/ai/tickets/TASK-project-item-sitrep.md +0 -79
  32. package/docs/ai/tickets/TASK-sitrep.md +0 -28
  33. package/docs/ai/tickets/TASK-state-sitrep.md +0 -26
  34. package/docs/ai/tickets/TASK-types-sitrep.md +0 -25
  35. package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +0 -26
  36. package/docs/ai/tickets/TKT-001-sitrep.md +0 -24
  37. package/docs/ai/tickets/TKT-002-sitrep.md +0 -25
  38. package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +0 -46
  39. package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +0 -135
  40. package/docs/ai/tickets/TKT-003-sitrep.md +0 -26
  41. package/docs/ai/tickets/TKT-004-sitrep.md +0 -27
  42. package/docs/ai/tickets/TKT-005-sitrep.md +0 -25
  43. package/docs/ai/tickets/TKT-006-sitrep.md +0 -26
  44. package/docs/ai/tickets/TKT-007-sitrep.md +0 -30
  45. package/docs/ai/tickets/TKT-008-sitrep.md +0 -32
  46. package/docs/ai/tickets/TKT-009-sitrep.md +0 -27
  47. package/docs/ai/tickets/TKT-010-sitrep.md +0 -27
  48. package/docs/ai/tickets/TKT-011-sitrep.md +0 -26
  49. package/docs/ai/tickets/TKT-012-sitrep.md +0 -25
  50. package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +0 -28
  51. package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +0 -25
  52. package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +0 -25
  53. package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +0 -24
  54. package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +0 -29
  55. package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +0 -29
  56. package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +0 -26
  57. package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +0 -30
  58. package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +0 -28
  59. package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +0 -26
  60. package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +0 -25
  61. package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +0 -30
  62. package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +0 -25
  63. package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +0 -29
  64. package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +0 -95
  65. package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +0 -61
  66. package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +0 -30
  67. package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +0 -27
  68. package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +0 -25
  69. package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +0 -27
  70. package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +0 -25
  71. package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +0 -28
  72. package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +0 -25
  73. package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +0 -26
  74. package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +0 -25
  75. package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +0 -26
  76. package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +0 -24
  77. package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +0 -25
  78. package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +0 -29
  79. package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +0 -25
  80. package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +0 -24
  81. package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +0 -24
  82. package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +0 -27
  83. package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +0 -25
  84. package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +0 -25
  85. package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +0 -27
  86. package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +0 -32
  87. package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +0 -27
  88. package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +0 -30
  89. package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +0 -25
  90. package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +0 -29
  91. package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +0 -25
  92. package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +0 -27
  93. package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +0 -25
  94. package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +0 -26
  95. package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +0 -25
  96. package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +0 -27
  97. package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +0 -75
  98. package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +0 -29
  99. package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +0 -29
  100. package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +0 -25
  101. package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +0 -25
  102. package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +0 -32
  103. package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +0 -64
  104. package/docs/ai/tkt-001-fix-database-error.md +0 -217
  105. package/docs/ai/ui-enhancement-plan.md +0 -562
  106. package/test/integration/app.isolated.tsx +0 -240
  107. package/test/integration/cli-commands.test.ts +0 -287
  108. package/test/integration/cli-validation.test.ts +0 -264
  109. package/test/integration/git-operations.test.ts +0 -218
  110. package/test/integration/scanner.test.ts +0 -228
  111. package/test/preload.ts +0 -18
  112. package/test/unit/cli/commands.test.ts +0 -13
  113. package/test/unit/cli/formatters.test.ts +0 -1116
  114. package/test/unit/cli/github-commands.test.ts +0 -12
  115. package/test/unit/components/CloneDialog.test.tsx +0 -240
  116. package/test/unit/components/ColumnHeader.test.tsx +0 -128
  117. package/test/unit/components/CommandPalette.test.tsx +0 -355
  118. package/test/unit/components/ConfirmDialog.test.tsx +0 -111
  119. package/test/unit/components/ErrorBoundary.test.tsx +0 -139
  120. package/test/unit/components/FilterBar.test.tsx +0 -43
  121. package/test/unit/components/FilterOptionsOverlay.test.tsx +0 -197
  122. package/test/unit/components/HelpOverlay.test.tsx +0 -90
  123. package/test/unit/components/Layout.test.tsx +0 -328
  124. package/test/unit/components/MarkdownRenderer.test.tsx +0 -45
  125. package/test/unit/components/ProgressBar.test.tsx +0 -138
  126. package/test/unit/components/ProjectItem.test.tsx +0 -182
  127. package/test/unit/components/ProjectList.test.tsx +0 -311
  128. package/test/unit/components/RepoDetailModal.test.tsx +0 -445
  129. package/test/unit/components/StatusBar.test.tsx +0 -112
  130. package/test/unit/components/UnifiedProjectItem.test.tsx +0 -618
  131. package/test/unit/components/ViewModeIndicator.test.tsx +0 -137
  132. package/test/unit/components/test-utils.tsx +0 -63
  133. package/test/unit/config/loader.test.ts +0 -692
  134. package/test/unit/db/database.test.ts +0 -978
  135. package/test/unit/db/index.test.ts +0 -314
  136. package/test/unit/fixtures/setup.ts +0 -186
  137. package/test/unit/git/commands-untested.test.ts +0 -205
  138. package/test/unit/git/commands.test.ts +0 -269
  139. package/test/unit/git/operations.test.ts +0 -322
  140. package/test/unit/git/status.test.ts +0 -219
  141. package/test/unit/github/auth.test.ts +0 -317
  142. package/test/unit/github/cache.test.ts +0 -1028
  143. package/test/unit/github/cli.test.ts +0 -135
  144. package/test/unit/github/unified.test.ts +0 -1201
  145. package/test/unit/graceful-shutdown.test.ts +0 -83
  146. package/test/unit/hooks/useBackgroundFetch.test.tsx +0 -239
  147. package/test/unit/hooks/useConfirmDialogActions.test.tsx +0 -81
  148. package/test/unit/hooks/useKeyBindings.isolated.ts +0 -715
  149. package/test/unit/hooks/useProjects.test.tsx +0 -186
  150. package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +0 -115
  151. package/test/unit/hooks/useUnifiedRepos.test.tsx +0 -177
  152. package/test/unit/mocks/config.ts +0 -109
  153. package/test/unit/mocks/git-service.ts +0 -274
  154. package/test/unit/mocks/github-service.ts +0 -250
  155. package/test/unit/mocks/index.ts +0 -72
  156. package/test/unit/mocks/project.ts +0 -148
  157. package/test/unit/mocks/state-mocks.ts +0 -187
  158. package/test/unit/mocks/unified.ts +0 -169
  159. package/test/unit/operations/batch.test.ts +0 -216
  160. package/test/unit/operations/commands.test.ts +0 -550
  161. package/test/unit/scanner/errors.test.ts +0 -297
  162. package/test/unit/scanner/index.test.ts +0 -1011
  163. package/test/unit/scanner/markers.test.ts +0 -150
  164. package/test/unit/scanner/submodules.test.ts +0 -99
  165. package/test/unit/services/git-errors.test.ts +0 -190
  166. package/test/unit/services/git.test.ts +0 -442
  167. package/test/unit/services/github-errors.test.ts +0 -293
  168. package/test/unit/services/github.test.ts +0 -200
  169. package/test/unit/state/actions.test.ts +0 -217
  170. package/test/unit/state/reducer.test.ts +0 -745
  171. package/test/unit/state/store.test.tsx +0 -711
  172. package/test/unit/types/commands.test.ts +0 -220
  173. package/test/unit/types/schema.test.ts +0 -179
  174. package/test/unit/utils/array.test.ts +0 -73
  175. package/test/unit/utils/debug.test.ts +0 -23
  176. package/test/unit/utils/errors.test.ts +0 -295
  177. package/test/unit/utils/markdown.test.ts +0 -163
  178. package/test/unit/utils/project-utils.test.ts +0 -756
  179. package/test/unit/utils/rate-limiter.test.ts +0 -256
  180. package/test/unit/utils/retry.test.ts +0 -165
  181. package/test/unit/utils/strip-ansi.ts +0 -13
  182. package/test/unit/utils/timeout.test.ts +0 -93
  183. 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
- });