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,618 +0,0 @@
1
- /**
2
- * Tests for UnifiedProjectItem component
3
- *
4
- * Note: This component uses Nerd Font icons via Unicode escape sequences.
5
- * Tests check for repo names and text content rather than specific icon characters,
6
- * since icon rendering depends on terminal font support.
7
- */
8
-
9
- import { describe, test, expect } from "bun:test";
10
- import { render } from "ink-testing-library";
11
- import { UnifiedProjectItem } from "../../../src/components/UnifiedProjectItem.tsx";
12
- import type { UnifiedRepo, GitHubRepoInfo, Project } from "../../../src/types/index.ts";
13
-
14
- // Mock data helpers
15
- function createMockGitHubRepo(overrides: Partial<GitHubRepoInfo> = {}): GitHubRepoInfo {
16
- return {
17
- name: "test-repo",
18
- fullName: "user/test-repo",
19
- owner: "user",
20
- description: "Test repository",
21
- htmlUrl: "https://github.com/user/test-repo",
22
- sshUrl: "git@github.com:user/test-repo.git",
23
- cloneUrl: "https://github.com/user/test-repo.git",
24
- isPrivate: false,
25
- isArchived: false,
26
- isFork: false,
27
- pushedAt: new Date("2024-01-15T09:00:00Z"),
28
- updatedAt: new Date("2024-01-14T08:00:00Z"),
29
- defaultBranch: "main",
30
- language: "TypeScript",
31
- size: 1024,
32
- ...overrides,
33
- };
34
- }
35
-
36
- function createMockProject(overrides: Partial<Project> = {}): Project {
37
- return {
38
- id: "test-project",
39
- name: "test-project",
40
- path: "/home/user/projects/test-project",
41
- type: "git",
42
- projectMarker: "package.json",
43
- status: {
44
- hasUnstagedChanges: false,
45
- hasStagedChanges: false,
46
- hasUntrackedFiles: false,
47
- modifiedCount: 0,
48
- stagedCount: 0,
49
- untrackedCount: 0,
50
- currentBranch: "main",
51
- trackingBranch: "origin/main",
52
- unpushedCommits: 0,
53
- unpulledCommits: 0,
54
- hasRemote: true,
55
- remoteUrl: "https://github.com/user/test-project.git",
56
- lastLocalCommit: new Date("2024-01-15T10:00:00Z"),
57
- lastRemoteActivity: new Date("2024-01-15T09:00:00Z"),
58
- hasCommits: true,
59
- isDirty: false,
60
- isAhead: false,
61
- isBehind: false,
62
- isOutOfSync: false,
63
- },
64
- submodule: null,
65
- lastScanned: new Date(),
66
- lastModified: null,
67
- ...overrides,
68
- };
69
- }
70
-
71
- describe("UnifiedProjectItem", () => {
72
- describe("GitHub-only repos", () => {
73
- test("renders repo name", () => {
74
- const repo: UnifiedRepo = {
75
- id: "github-only",
76
- name: "github-only-repo",
77
- source: "github",
78
- local: null,
79
- github: createMockGitHubRepo({
80
- name: "github-only-repo",
81
- fullName: "user/github-only-repo",
82
- }),
83
- isCloned: false,
84
- isOnGitHub: true,
85
- localPath: null,
86
- };
87
-
88
- const { lastFrame } = render(
89
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
90
- );
91
-
92
- const output = lastFrame();
93
- expect(output).toContain("github-only-repo");
94
- });
95
-
96
- test("renders private repo name", () => {
97
- const repo: UnifiedRepo = {
98
- id: "github-private",
99
- name: "private-repo",
100
- source: "github",
101
- local: null,
102
- github: createMockGitHubRepo({
103
- name: "private-repo",
104
- fullName: "user/private-repo",
105
- isPrivate: true,
106
- }),
107
- isCloned: false,
108
- isOnGitHub: true,
109
- localPath: null,
110
- };
111
-
112
- const { lastFrame } = render(
113
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
114
- );
115
-
116
- const output = lastFrame();
117
- expect(output).toContain("private-repo");
118
- });
119
-
120
- test("renders public repo name", () => {
121
- const repo: UnifiedRepo = {
122
- id: "github-public",
123
- name: "public-repo",
124
- source: "github",
125
- local: null,
126
- github: createMockGitHubRepo({
127
- name: "public-repo",
128
- fullName: "user/public-repo",
129
- isPrivate: false,
130
- }),
131
- isCloned: false,
132
- isOnGitHub: true,
133
- localPath: null,
134
- };
135
-
136
- const { lastFrame } = render(
137
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
138
- );
139
-
140
- const output = lastFrame();
141
- expect(output).toContain("public-repo");
142
- });
143
-
144
- test("shows last activity time", () => {
145
- const repo: UnifiedRepo = {
146
- id: "github-lang",
147
- name: "lang-repo",
148
- source: "github",
149
- local: null,
150
- github: createMockGitHubRepo({
151
- name: "lang-repo",
152
- fullName: "user/lang-repo",
153
- language: "Rust",
154
- pushedAt: new Date(Date.now() - 86400000), // 1 day ago
155
- }),
156
- isCloned: false,
157
- isOnGitHub: true,
158
- localPath: null,
159
- };
160
-
161
- const { lastFrame } = render(
162
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
163
- );
164
-
165
- const output = lastFrame();
166
- // Date now shows just "1d" without "ago"
167
- expect(output).toContain("1d");
168
- });
169
- });
170
-
171
- describe("Local-only repos", () => {
172
- test("renders local repo name", () => {
173
- const repo: UnifiedRepo = {
174
- id: "local-only",
175
- name: "local-only-project",
176
- source: "local",
177
- local: createMockProject({
178
- name: "local-only-project",
179
- status: {
180
- ...createMockProject().status!,
181
- hasRemote: false,
182
- remoteUrl: null,
183
- },
184
- }),
185
- github: null,
186
- isCloned: true,
187
- isOnGitHub: false,
188
- localPath: "/home/user/local-only-project",
189
- };
190
-
191
- const { lastFrame } = render(
192
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
193
- );
194
-
195
- const output = lastFrame();
196
- expect(output).toContain("local-only-project");
197
- expect(output).toContain("local"); // Status shows "local" for no remote
198
- });
199
-
200
- test("shows current branch name", () => {
201
- const repo: UnifiedRepo = {
202
- id: "local-branch",
203
- name: "branch-project",
204
- source: "local",
205
- local: createMockProject({
206
- name: "branch-project",
207
- status: {
208
- ...createMockProject().status!,
209
- currentBranch: "develop",
210
- },
211
- }),
212
- github: null,
213
- isCloned: true,
214
- isOnGitHub: false,
215
- localPath: "/home/user/branch-project",
216
- };
217
-
218
- const { lastFrame } = render(
219
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
220
- );
221
-
222
- const output = lastFrame();
223
- expect(output).toContain("develop");
224
- });
225
-
226
- test("renders dirty repo", () => {
227
- const repo: UnifiedRepo = {
228
- id: "local-dirty",
229
- name: "dirty-project",
230
- source: "local",
231
- local: createMockProject({
232
- name: "dirty-project",
233
- status: {
234
- ...createMockProject().status!,
235
- isDirty: true,
236
- },
237
- }),
238
- github: null,
239
- isCloned: true,
240
- isOnGitHub: false,
241
- localPath: "/home/user/dirty-project",
242
- };
243
-
244
- const { lastFrame } = render(
245
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
246
- );
247
-
248
- const output = lastFrame();
249
- expect(output).toContain("dirty-project");
250
- // Dirty status is shown via icon, just verify the repo renders
251
- });
252
-
253
- test("renders submodule repo", () => {
254
- const repo: UnifiedRepo = {
255
- id: "local-submodule",
256
- name: "submodule-project",
257
- source: "local",
258
- local: createMockProject({
259
- name: "submodule-project",
260
- type: "git-submodule",
261
- }),
262
- github: null,
263
- isCloned: true,
264
- isOnGitHub: false,
265
- localPath: "/home/user/submodule-project",
266
- };
267
-
268
- const { lastFrame } = render(
269
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
270
- );
271
-
272
- const output = lastFrame();
273
- expect(output).toContain("submodule-project");
274
- // Submodule indicator not currently implemented in component
275
- });
276
-
277
- test("renders non-git project", () => {
278
- const repo: UnifiedRepo = {
279
- id: "local-marker",
280
- name: "marker-project",
281
- source: "local",
282
- local: createMockProject({
283
- name: "marker-project",
284
- type: "non-git",
285
- projectMarker: "package.json",
286
- }),
287
- github: null,
288
- isCloned: true,
289
- isOnGitHub: false,
290
- localPath: "/home/user/marker-project",
291
- };
292
-
293
- const { lastFrame } = render(
294
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
295
- );
296
-
297
- const output = lastFrame();
298
- expect(output).toContain("marker-project");
299
- // Project marker indicator not currently implemented in component
300
- });
301
- });
302
-
303
- describe("Synced repos (both)", () => {
304
- test("renders synced repo name", () => {
305
- const repo: UnifiedRepo = {
306
- id: "synced",
307
- name: "synced-project",
308
- source: "both",
309
- local: createMockProject({
310
- name: "synced-project",
311
- }),
312
- github: createMockGitHubRepo({
313
- name: "synced-project",
314
- fullName: "user/synced-project",
315
- }),
316
- isCloned: true,
317
- isOnGitHub: true,
318
- localPath: "/home/user/synced-project",
319
- };
320
-
321
- const { lastFrame } = render(
322
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
323
- );
324
-
325
- const output = lastFrame();
326
- expect(output).toContain("synced-project");
327
- expect(output).toContain("sync"); // Sync status
328
- });
329
-
330
- test("shows unpushed commits count", () => {
331
- const repo: UnifiedRepo = {
332
- id: "synced-ahead",
333
- name: "ahead-project",
334
- source: "both",
335
- local: createMockProject({
336
- name: "ahead-project",
337
- status: {
338
- ...createMockProject().status!,
339
- isAhead: true,
340
- unpushedCommits: 3,
341
- },
342
- }),
343
- github: createMockGitHubRepo({
344
- name: "ahead-project",
345
- fullName: "user/ahead-project",
346
- }),
347
- isCloned: true,
348
- isOnGitHub: true,
349
- localPath: "/home/user/ahead-project",
350
- };
351
-
352
- const { lastFrame } = render(
353
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
354
- );
355
-
356
- const output = lastFrame();
357
- // Component uses Nerd Font arrow icon + number
358
- expect(output).toContain("3");
359
- });
360
-
361
- test("shows unpulled commits count", () => {
362
- const repo: UnifiedRepo = {
363
- id: "synced-behind",
364
- name: "behind-project",
365
- source: "both",
366
- local: createMockProject({
367
- name: "behind-project",
368
- status: {
369
- ...createMockProject().status!,
370
- isBehind: true,
371
- unpulledCommits: 5,
372
- },
373
- }),
374
- github: createMockGitHubRepo({
375
- name: "behind-project",
376
- fullName: "user/behind-project",
377
- }),
378
- isCloned: true,
379
- isOnGitHub: true,
380
- localPath: "/home/user/behind-project",
381
- };
382
-
383
- const { lastFrame } = render(
384
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
385
- );
386
-
387
- const output = lastFrame();
388
- // Component uses Nerd Font arrow icon + number
389
- expect(output).toContain("5");
390
- });
391
-
392
- test("shows no-remote status", () => {
393
- const repo: UnifiedRepo = {
394
- id: "synced-no-remote",
395
- name: "no-remote-project",
396
- source: "both",
397
- local: createMockProject({
398
- name: "no-remote-project",
399
- status: {
400
- ...createMockProject().status!,
401
- hasRemote: false,
402
- remoteUrl: null,
403
- },
404
- }),
405
- github: createMockGitHubRepo({
406
- name: "no-remote-project",
407
- fullName: "user/no-remote-project",
408
- }),
409
- isCloned: true,
410
- isOnGitHub: true,
411
- localPath: "/home/user/no-remote-project",
412
- };
413
-
414
- const { lastFrame } = render(
415
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
416
- );
417
-
418
- const output = lastFrame();
419
- expect(output).toContain("no-rem");
420
- });
421
- });
422
-
423
- describe("Cursor and selection states", () => {
424
- test("shows cursor indicator when isCursor is true", () => {
425
- const repo: UnifiedRepo = {
426
- id: "cursor-test",
427
- name: "cursor-test",
428
- source: "local",
429
- local: createMockProject({ name: "cursor-test" }),
430
- github: null,
431
- isCloned: true,
432
- isOnGitHub: false,
433
- localPath: "/home/user/cursor-test",
434
- };
435
-
436
- const { lastFrame } = render(
437
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={true} />
438
- );
439
-
440
- const output = lastFrame();
441
- // Cursor styling is applied via ink - verify repo renders with cursor state
442
- expect(output).toContain("cursor-test");
443
- expect(output).toContain("synced"); // Sync shows synced
444
- });
445
-
446
- test("shows selection checkbox when selected", () => {
447
- const repo: UnifiedRepo = {
448
- id: "selection-test",
449
- name: "selection-test",
450
- source: "local",
451
- local: createMockProject({ name: "selection-test" }),
452
- github: null,
453
- isCloned: true,
454
- isOnGitHub: false,
455
- localPath: "/home/user/selection-test",
456
- };
457
-
458
- const { lastFrame } = render(
459
- <UnifiedProjectItem repo={repo} isSelected={true} isCursor={false} />
460
- );
461
-
462
- const output = lastFrame();
463
- expect(output).toContain("[x]"); // Selected checkbox
464
- });
465
-
466
- test("shows unselected checkbox when not selected", () => {
467
- const repo: UnifiedRepo = {
468
- id: "no-selection-test",
469
- name: "no-selection-test",
470
- source: "local",
471
- local: createMockProject({ name: "no-selection-test" }),
472
- github: null,
473
- isCloned: true,
474
- isOnGitHub: false,
475
- localPath: "/home/user/no-selection-test",
476
- };
477
-
478
- const { lastFrame } = render(
479
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
480
- );
481
-
482
- const output = lastFrame();
483
- expect(output).toContain("[ ]"); // Unselected checkbox
484
- });
485
-
486
- test("combines cursor and selection states", () => {
487
- const repo: UnifiedRepo = {
488
- id: "combined-test",
489
- name: "combined-test",
490
- source: "local",
491
- local: createMockProject({ name: "combined-test" }),
492
- github: null,
493
- isCloned: true,
494
- isOnGitHub: false,
495
- localPath: "/home/user/combined-test",
496
- };
497
-
498
- const { lastFrame } = render(
499
- <UnifiedProjectItem repo={repo} isSelected={true} isCursor={true} />
500
- );
501
-
502
- const output = lastFrame();
503
- // Verify both cursor and selection states apply - cursor styling via ink
504
- expect(output).toContain("combined-test");
505
- expect(output).toContain("[x]"); // Selected
506
- });
507
- });
508
-
509
- describe("Last activity formatting", () => {
510
- test("shows 'today' for recent activity", () => {
511
- const repo: UnifiedRepo = {
512
- id: "today-test",
513
- name: "today-test",
514
- source: "local",
515
- local: createMockProject({
516
- name: "today-test",
517
- status: {
518
- ...createMockProject().status!,
519
- lastLocalCommit: new Date(),
520
- },
521
- }),
522
- github: null,
523
- isCloned: true,
524
- isOnGitHub: false,
525
- localPath: "/home/user/today-test",
526
- };
527
-
528
- const { lastFrame } = render(
529
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
530
- );
531
-
532
- const output = lastFrame();
533
- expect(output).toContain("today");
534
- });
535
-
536
- test("shows '1d ago' for 1 day ago", () => {
537
- const yesterday = new Date();
538
- yesterday.setDate(yesterday.getDate() - 1);
539
-
540
- const repo: UnifiedRepo = {
541
- id: "yesterday-test",
542
- name: "yesterday-test",
543
- source: "github",
544
- local: null,
545
- github: createMockGitHubRepo({
546
- name: "yesterday-test",
547
- pushedAt: yesterday,
548
- }),
549
- isCloned: false,
550
- isOnGitHub: true,
551
- localPath: null,
552
- };
553
-
554
- const { lastFrame } = render(
555
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
556
- );
557
-
558
- const output = lastFrame();
559
- // Date now shows just "1d" without "ago"
560
- expect(output).toContain("1d");
561
- });
562
-
563
- test("shows days ago for recent activity", () => {
564
- const threeDaysAgo = new Date();
565
- threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
566
-
567
- const repo: UnifiedRepo = {
568
- id: "days-test",
569
- name: "days-test",
570
- source: "github",
571
- local: null,
572
- github: createMockGitHubRepo({
573
- name: "days-test",
574
- pushedAt: threeDaysAgo,
575
- }),
576
- isCloned: false,
577
- isOnGitHub: true,
578
- localPath: null,
579
- };
580
-
581
- const { lastFrame } = render(
582
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
583
- );
584
-
585
- const output = lastFrame();
586
- // Date now shows just "3d" without "ago"
587
- expect(output).toContain("3d");
588
- });
589
-
590
- test("shows 'empty' status for repos with no commits", () => {
591
- const repo: UnifiedRepo = {
592
- id: "empty-test",
593
- name: "empty-test",
594
- source: "local",
595
- local: createMockProject({
596
- name: "empty-test",
597
- status: {
598
- ...createMockProject().status!,
599
- hasCommits: false,
600
- lastLocalCommit: null,
601
- },
602
- }),
603
- github: null,
604
- isCloned: true,
605
- isOnGitHub: false,
606
- localPath: "/home/user/empty-test",
607
- };
608
-
609
- const { lastFrame } = render(
610
- <UnifiedProjectItem repo={repo} isSelected={false} isCursor={false} />
611
- );
612
-
613
- const output = lastFrame();
614
- // Status column shows "empty" for repos with no commits
615
- expect(output).toContain("empty");
616
- });
617
- });
618
- });