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,293 +0,0 @@
1
- /**
2
- * Tests for GitHub service error handling
3
- */
4
- import { describe, test, expect } from "bun:test";
5
- import { shouldRetryGitHubError, withRetry } from "../../../src/utils/retry.ts";
6
- import { createMockGitHubService } from "../mocks/github-service.ts";
7
-
8
- describe("GitHub Error Handling", () => {
9
- describe("shouldRetryGitHubError", () => {
10
- test("should retry on rate limit (429)", () => {
11
- const error = new Error("rate limit exceeded 429");
12
- expect(shouldRetryGitHubError(error, 1)).toBe(true);
13
- });
14
-
15
- test("should retry on server errors (5xx)", () => {
16
- expect(shouldRetryGitHubError(new Error("500 Internal Server Error"), 1)).toBe(true);
17
- expect(shouldRetryGitHubError(new Error("502 Bad Gateway"), 1)).toBe(true);
18
- expect(shouldRetryGitHubError(new Error("503 Service Unavailable"), 1)).toBe(true);
19
- expect(shouldRetryGitHubError(new Error("504 Gateway Timeout"), 1)).toBe(true);
20
- });
21
-
22
- test("should retry on network errors", () => {
23
- expect(shouldRetryGitHubError(new Error("network error"), 1)).toBe(true);
24
- expect(shouldRetryGitHubError(new Error("ECONNRESET"), 1)).toBe(true);
25
- expect(shouldRetryGitHubError(new Error("ETIMEDOUT"), 1)).toBe(true);
26
- expect(shouldRetryGitHubError(new Error("fetch failed"), 1)).toBe(true);
27
- });
28
-
29
- test("should NOT retry on client errors (4xx except 429)", () => {
30
- expect(shouldRetryGitHubError(new Error("401 Unauthorized"), 1)).toBe(false);
31
- expect(shouldRetryGitHubError(new Error("403 Forbidden"), 1)).toBe(false);
32
- expect(shouldRetryGitHubError(new Error("404 Not Found"), 1)).toBe(false);
33
- expect(shouldRetryGitHubError(new Error("422 Unprocessable Entity"), 1)).toBe(false);
34
- });
35
-
36
- test("should NOT retry on authentication failures with detailed message", () => {
37
- const shouldRetry = shouldRetryGitHubError(new Error("401 Unauthorized: Bad credentials"), 1);
38
- expect(shouldRetry).toBe(false);
39
- });
40
-
41
- test("should NOT retry on 404 with detailed message", () => {
42
- const shouldRetry = shouldRetryGitHubError(new Error("404 Not Found"), 1);
43
- expect(shouldRetry).toBe(false);
44
- });
45
- });
46
-
47
- describe("Rate Limiting Recovery", () => {
48
- test("should retry on rate limit error", async () => {
49
- let attempts = 0;
50
-
51
- const operation = async () => {
52
- attempts++;
53
- if (attempts < 3) {
54
- throw new Error("API rate limit exceeded (429)");
55
- }
56
- // Return a mock user on success
57
- return { login: "testuser", name: "Test User", type: "User" as const };
58
- };
59
-
60
- const result = await withRetry(operation, {
61
- shouldRetry: shouldRetryGitHubError,
62
- maxAttempts: 3,
63
- initialDelay: 100
64
- });
65
-
66
- expect(result).toBeDefined();
67
- expect(result.login).toBe("testuser");
68
- expect(attempts).toBe(3);
69
- });
70
-
71
- test("should handle rate limit with retry-after header", async () => {
72
- let attempts = 0;
73
-
74
- const operation = async () => {
75
- attempts++;
76
- throw new Error("rate limit exceeded 429");
77
- };
78
-
79
- await expect(
80
- withRetry(operation, {
81
- shouldRetry: shouldRetryGitHubError,
82
- maxAttempts: 2,
83
- initialDelay: 50
84
- })
85
- ).rejects.toThrow("Failed after 2 attempts");
86
-
87
- expect(attempts).toBe(2);
88
- });
89
- });
90
-
91
- describe("Authentication Failure Handling", () => {
92
- test("should not retry on authentication failures", async () => {
93
- let attempts = 0;
94
- let retryCalls = 0;
95
-
96
- const operation = async () => {
97
- attempts++;
98
- throw new Error("401 Unauthorized: Bad credentials");
99
- };
100
-
101
- // Test the retry function directly
102
- const shouldRetry = shouldRetryGitHubError(new Error("401 Unauthorized: Bad credentials"), 1);
103
- expect(shouldRetry).toBe(false);
104
-
105
- try {
106
- await withRetry(operation, {
107
- shouldRetry: (error, attempt) => {
108
- retryCalls++;
109
- return shouldRetryGitHubError(error, attempt);
110
- },
111
- maxAttempts: 3,
112
- initialDelay: 50
113
- });
114
- } catch (error: any) {
115
- // Should fail after 1 attempt since 401 errors are not retryable
116
- expect(retryCalls).toBe(1); // Only called once for the first error
117
- }
118
-
119
- expect(attempts).toBe(1);
120
- });
121
-
122
- test("should handle token expiration", async () => {
123
- const githubService = createMockGitHubService({
124
- hasToken: false,
125
- token: null
126
- });
127
-
128
- expect(githubService.hasToken()).toBe(false);
129
- expect(githubService.getToken()).toBeNull();
130
- });
131
- });
132
-
133
- describe("Retry Logic with Different Error Types", () => {
134
- test("should retry on server errors", async () => {
135
- let attempts = 0;
136
-
137
- const operation = async () => {
138
- attempts++;
139
- if (attempts < 2) {
140
- throw new Error("500 Internal Server Error");
141
- }
142
- return { success: true };
143
- };
144
-
145
- const result = await withRetry(operation, {
146
- shouldRetry: shouldRetryGitHubError,
147
- maxAttempts: 3,
148
- initialDelay: 50
149
- });
150
-
151
- expect(result).toEqual({ success: true });
152
- expect(attempts).toBe(2);
153
- });
154
-
155
- test("should retry on network errors", async () => {
156
- let attempts = 0;
157
-
158
- const operation = async () => {
159
- attempts++;
160
- if (attempts < 2) {
161
- throw new Error("ECONNRESET");
162
- }
163
- return { success: true };
164
- };
165
-
166
- const result = await withRetry(operation, {
167
- shouldRetry: shouldRetryGitHubError,
168
- maxAttempts: 3,
169
- initialDelay: 50
170
- });
171
-
172
- expect(result).toEqual({ success: true });
173
- expect(attempts).toBe(2);
174
- });
175
-
176
- test("should not retry on client errors", async () => {
177
- // Test the retry function directly
178
- const shouldRetry404 = shouldRetryGitHubError(new Error("404 Not Found"), 1);
179
- expect(shouldRetry404).toBe(false);
180
-
181
- let attempts = 0;
182
- let retryCalls = 0;
183
-
184
- const operation = async () => {
185
- attempts++;
186
- throw new Error("404 Not Found");
187
- };
188
-
189
- try {
190
- await withRetry(operation, {
191
- shouldRetry: (error, attempt) => {
192
- retryCalls++;
193
- return shouldRetryGitHubError(error, attempt);
194
- },
195
- maxAttempts: 3,
196
- initialDelay: 50
197
- });
198
- } catch (error: any) {
199
- // Should fail after 1 attempt since 404 errors are not retryable
200
- expect(retryCalls).toBe(1); // Only called once for the first error
201
- }
202
-
203
- expect(attempts).toBe(1);
204
- });
205
- });
206
-
207
- describe("Max Retry Exhaustion", () => {
208
- test("should fail after max attempts", async () => {
209
- let attempts = 0;
210
-
211
- const operation = async () => {
212
- attempts++;
213
- throw new Error("503 Service Unavailable");
214
- };
215
-
216
- await expect(
217
- withRetry(operation, {
218
- shouldRetry: shouldRetryGitHubError,
219
- maxAttempts: 3,
220
- initialDelay: 50
221
- })
222
- ).rejects.toThrow("Failed after 3 attempts");
223
-
224
- expect(attempts).toBe(3);
225
- });
226
-
227
- test("should include last error in retry error", async () => {
228
- const lastError = new Error("504 Gateway Timeout");
229
-
230
- const operation = async () => {
231
- throw lastError;
232
- };
233
-
234
- try {
235
- await withRetry(operation, {
236
- shouldRetry: shouldRetryGitHubError,
237
- maxAttempts: 2,
238
- initialDelay: 50
239
- });
240
- } catch (error: any) {
241
- expect(error.message).toContain("Failed after 2 attempts");
242
- expect(error.lastError).toBe(lastError);
243
- expect(error.attempts).toBe(2);
244
- }
245
- });
246
-
247
- test("should use exponential backoff", async () => {
248
- const delays: number[] = [];
249
- let lastTime = Date.now();
250
-
251
- const operation = async () => {
252
- const now = Date.now();
253
- delays.push(now - lastTime);
254
- lastTime = now;
255
- throw new Error("502 Bad Gateway");
256
- };
257
-
258
- await expect(
259
- withRetry(operation, {
260
- shouldRetry: shouldRetryGitHubError,
261
- maxAttempts: 4,
262
- initialDelay: 100,
263
- backoffFactor: 2,
264
- onRetry: (error, attempt, nextDelay) => {
265
- // Verify exponential backoff
266
- expect(nextDelay).toBe(100 * Math.pow(2, attempt - 1));
267
- }
268
- })
269
- ).rejects.toThrow("Failed after 4 attempts");
270
- });
271
- });
272
- });
273
-
274
- describe("Token Validation", () => {
275
- test("should validate classic token format", () => {
276
- // Classic tokens are 40 hex characters
277
- const validClassic = "a".repeat(40);
278
- expect(validClassic).toMatch(/^[a-f0-9]{40}$/);
279
- });
280
-
281
- test("should validate new PAT format", () => {
282
- // New PATs start with ghp_
283
- const validPAT = "ghp_" + "a".repeat(36);
284
- expect(validPAT).toMatch(/^ghp_[a-zA-Z0-9]{36}$/);
285
- });
286
-
287
- test("should validate fine-grained PAT format", () => {
288
- // Fine-grained PATs have specific format
289
- const pattern = /^github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}$/;
290
- const validFineGrained = "github_pat_" + "a".repeat(22) + "_" + "b".repeat(59);
291
- expect(validFineGrained).toMatch(pattern);
292
- });
293
- });
@@ -1,200 +0,0 @@
1
- /**
2
- * Tests for GitHub service implementation
3
- * Tests for token validation and basic functionality
4
- */
5
-
6
- import { describe, test, expect, beforeEach, afterEach } from "bun:test";
7
- import { apiGitHubService } from "../../../src/services/github.ts";
8
-
9
- describe("GitHub Service - apiGitHubService", () => {
10
- let originalEnv: Record<string, string | undefined>;
11
-
12
- beforeEach(() => {
13
- originalEnv = { ...process.env };
14
- });
15
-
16
- afterEach(() => {
17
- process.env = originalEnv;
18
- });
19
-
20
- describe("Token handling", () => {
21
- test("hasToken returns true when GITHUB_TOKEN is set", () => {
22
- process.env.GITHUB_TOKEN = "ghp_test123";
23
- expect(apiGitHubService.hasToken()).toBe(true);
24
- });
25
-
26
- test("hasToken returns true when GH_TOKEN is set", () => {
27
- process.env.GH_TOKEN = "ghp_test123";
28
- expect(apiGitHubService.hasToken()).toBe(true);
29
- });
30
-
31
- test("hasToken returns false when no token is set", () => {
32
- delete process.env.GITHUB_TOKEN;
33
- delete process.env.GH_TOKEN;
34
- expect(apiGitHubService.hasToken()).toBe(false);
35
- });
36
-
37
- test("getToken returns GITHUB_TOKEN value", () => {
38
- process.env.GITHUB_TOKEN = "ghp_test123";
39
- expect(apiGitHubService.getToken()).toBe("ghp_test123");
40
- });
41
-
42
- test("getToken falls back to GH_TOKEN", () => {
43
- process.env.GH_TOKEN = "ghp_fallback123";
44
- expect(apiGitHubService.getToken()).toBe("ghp_fallback123");
45
- });
46
-
47
- test("getToken returns null when no token is set", () => {
48
- delete process.env.GITHUB_TOKEN;
49
- delete process.env.GH_TOKEN;
50
- expect(apiGitHubService.getToken()).toBeNull();
51
- });
52
-
53
- test("getToken warns on invalid token format", () => {
54
- const originalWarn = console.warn;
55
- const warnings: string[] = [];
56
- console.warn = (message: string) => warnings.push(message);
57
-
58
- process.env.GITHUB_TOKEN = "invalid-token";
59
- apiGitHubService.getToken();
60
-
61
- expect(warnings.length).toBeGreaterThan(0);
62
- expect(warnings[0]).toContain("GITHUB_TOKEN format appears invalid");
63
-
64
- console.warn = originalWarn;
65
- });
66
- });
67
-
68
- describe("Token format validation", () => {
69
- test("validates classic 40-char hex tokens", () => {
70
- const validToken = "a".repeat(40);
71
- expect(validToken).toMatch(/^[a-f0-9]{40}$/);
72
- });
73
-
74
- test("validates ghp_ prefix tokens", () => {
75
- const validToken = "ghp_" + "a".repeat(36);
76
- expect(validToken).toMatch(/^ghp_[a-zA-Z0-9]{36}$/);
77
- });
78
-
79
- test("validates github_pat_ fine-grained tokens", () => {
80
- const validToken = "github_pat_" + "a".repeat(22) + "_" + "b".repeat(59);
81
- expect(validToken).toMatch(/^github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}$/);
82
- });
83
-
84
- test("validates gho_ OAuth tokens", () => {
85
- const validToken = "gho_" + "a".repeat(40);
86
- expect(validToken).toMatch(/^gho_[a-zA-Z0-9]{35,40}$/);
87
- });
88
-
89
- test("validates ghs_ GitHub App tokens", () => {
90
- const validToken = "ghs_" + "a".repeat(36);
91
- expect(validToken).toMatch(/^ghs_[a-zA-Z0-9]{36}$/);
92
- });
93
-
94
- test("validates ghr_ refresh tokens", () => {
95
- const validToken = "ghr_" + "a".repeat(36);
96
- expect(validToken).toMatch(/^ghr_[a-zA-Z0-9]{36}$/);
97
- });
98
-
99
- test("rejects invalid formats", () => {
100
- const invalidTokens = [
101
- "invalid",
102
- "123",
103
- "ghp_" + "a".repeat(35), // Too short
104
- "github_pat_" + "a".repeat(21) + "_" + "b".repeat(58), // Too short
105
- ];
106
-
107
- for (const token of invalidTokens) {
108
- const patterns = [
109
- /^ghp_[a-zA-Z0-9]{36}$/,
110
- /^github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}$/,
111
- /^gho_[a-zA-Z0-9]{35,40}$/,
112
- /^ghs_[a-zA-Z0-9]{36}$/,
113
- /^ghr_[a-zA-Z0-9]{36}$/,
114
- /^[a-f0-9]{40}$/,
115
- ];
116
-
117
- const isValid = patterns.some(pattern => pattern.test(token));
118
- expect(isValid).toBe(false);
119
- }
120
- });
121
- });
122
-
123
- describe("Error handling", () => {
124
- test("handles missing token in createRepo", async () => {
125
- delete process.env.GITHUB_TOKEN;
126
- delete process.env.GH_TOKEN;
127
-
128
- const result = await apiGitHubService.createRepo({
129
- name: "test-repo",
130
- });
131
-
132
- expect(result.success).toBe(false);
133
- expect(result.error).toBe("GITHUB_TOKEN not set");
134
- });
135
- });
136
-
137
- describe("Repository operations", () => {
138
- test("createRepo handles missing token", async () => {
139
- delete process.env.GITHUB_TOKEN;
140
- delete process.env.GH_TOKEN;
141
-
142
- const result = await apiGitHubService.createRepo({
143
- name: "test-repo",
144
- });
145
-
146
- expect(result.success).toBe(false);
147
- expect(result.error).toBe("GITHUB_TOKEN not set");
148
- });
149
-
150
- test("archiveRepo handles missing token", async () => {
151
- delete process.env.GITHUB_TOKEN;
152
- delete process.env.GH_TOKEN;
153
-
154
- const result = await apiGitHubService.archiveRepo("user/repo");
155
-
156
- expect(result.success).toBe(false);
157
- expect(result.error).toBe("GITHUB_TOKEN not set");
158
- });
159
-
160
- test("unarchiveRepo handles missing token", async () => {
161
- delete process.env.GITHUB_TOKEN;
162
- delete process.env.GH_TOKEN;
163
-
164
- const result = await apiGitHubService.unarchiveRepo("user/repo");
165
-
166
- expect(result.success).toBe(false);
167
- expect(result.error).toBe("GITHUB_TOKEN not set");
168
- });
169
-
170
- test("deleteRepo handles missing token", async () => {
171
- delete process.env.GITHUB_TOKEN;
172
- delete process.env.GH_TOKEN;
173
-
174
- const result = await apiGitHubService.deleteRepo("user/repo");
175
-
176
- expect(result.success).toBe(false);
177
- expect(result.error).toBe("GITHUB_TOKEN not set");
178
- });
179
- });
180
-
181
- describe("cloneRepo", () => {
182
- test("cloneRepo handles missing token", async () => {
183
- delete process.env.GITHUB_TOKEN;
184
- delete process.env.GH_TOKEN;
185
-
186
- const result = await apiGitHubService.cloneRepo(
187
- {
188
- fullName: "user/repo",
189
- sshUrl: "git@github.com:user/repo.git",
190
- cloneUrl: "https://github.com/user/repo.git",
191
- } as any,
192
- { targetDir: "/tmp/clone-test" }
193
- );
194
-
195
- // cloneRepo doesn't require token, should work
196
- expect(result.operation).toBe("clone");
197
- expect(typeof result.duration).toBe("number");
198
- });
199
- });
200
- });
@@ -1,217 +0,0 @@
1
- /**
2
- * Tests for state action creators
3
- *
4
- * NOTE: This file must NOT have any module mocks as it tests real implementations.
5
- * We restore any mocks at the start to ensure isolation.
6
- */
7
-
8
- import { describe, test, expect, beforeAll, afterAll, mock } from "bun:test";
9
-
10
- // Restore any mocks from other test files before importing real modules
11
- mock.restore();
12
-
13
- import {
14
- setProjects,
15
- setLoading,
16
- setError,
17
- setMessage,
18
- moveCursor,
19
- toggleSelection,
20
- selectAll,
21
- deselectAll,
22
- setFilter,
23
- setSort,
24
- cycleSort,
25
- setMode,
26
- startAction,
27
- endAction,
28
- setScrollOffset,
29
- updateProject,
30
- } from "../../../src/state/actions.ts";
31
- import { createMockProject } from "../mocks/index.ts";
32
-
33
- describe("state action creators", () => {
34
- describe("setProjects", () => {
35
- test("creates SET_PROJECTS action", () => {
36
- const projects = [createMockProject(), createMockProject()];
37
- const action = setProjects(projects);
38
-
39
- expect(action.type).toBe("SET_PROJECTS");
40
- expect(action.payload).toBe(projects);
41
- });
42
- });
43
-
44
- describe("setLoading", () => {
45
- test("creates SET_LOADING action with true", () => {
46
- const action = setLoading(true);
47
-
48
- expect(action.type).toBe("SET_LOADING");
49
- expect(action.payload).toBe(true);
50
- });
51
-
52
- test("creates SET_LOADING action with false", () => {
53
- const action = setLoading(false);
54
-
55
- expect(action.type).toBe("SET_LOADING");
56
- expect(action.payload).toBe(false);
57
- });
58
- });
59
-
60
- describe("setError", () => {
61
- test("creates SET_ERROR action with error message", () => {
62
- const action = setError("Something went wrong");
63
-
64
- expect(action.type).toBe("SET_ERROR");
65
- expect(action.payload).toBe("Something went wrong");
66
- });
67
-
68
- test("creates SET_ERROR action with null", () => {
69
- const action = setError(null);
70
-
71
- expect(action.type).toBe("SET_ERROR");
72
- expect(action.payload).toBe(null);
73
- });
74
- });
75
-
76
- describe("setMessage", () => {
77
- test("creates SET_MESSAGE action", () => {
78
- const action = setMessage("Operation successful");
79
-
80
- expect(action.type).toBe("SET_MESSAGE");
81
- expect(action.payload).toBe("Operation successful");
82
- });
83
- });
84
-
85
- describe("moveCursor", () => {
86
- test("creates MOVE_CURSOR action", () => {
87
- const action = moveCursor(5);
88
-
89
- expect(action.type).toBe("MOVE_CURSOR");
90
- expect(action.payload).toBe(5);
91
- });
92
- });
93
-
94
- describe("toggleSelection", () => {
95
- test("creates TOGGLE_SELECTION action", () => {
96
- const action = toggleSelection(3);
97
-
98
- expect(action.type).toBe("TOGGLE_SELECTION");
99
- expect(action.payload).toBe(3);
100
- });
101
- });
102
-
103
- describe("selectAll", () => {
104
- test("creates SELECT_ALL action", () => {
105
- const action = selectAll();
106
-
107
- expect(action.type).toBe("SELECT_ALL");
108
- });
109
- });
110
-
111
- describe("deselectAll", () => {
112
- test("creates DESELECT_ALL action", () => {
113
- const action = deselectAll();
114
-
115
- expect(action.type).toBe("DESELECT_ALL");
116
- });
117
- });
118
-
119
- describe("setFilter", () => {
120
- test("creates SET_FILTER action", () => {
121
- const action = setFilter("my-project");
122
-
123
- expect(action.type).toBe("SET_FILTER");
124
- expect(action.payload).toBe("my-project");
125
- });
126
- });
127
-
128
- describe("setSort", () => {
129
- test("creates SET_SORT action", () => {
130
- const action = setSort("name", "asc");
131
-
132
- expect(action.type).toBe("SET_SORT");
133
- expect(action.payload).toEqual({ by: "name", direction: "asc" });
134
- });
135
-
136
- test("creates SET_SORT action with status desc", () => {
137
- const action = setSort("status", "desc");
138
-
139
- expect(action.type).toBe("SET_SORT");
140
- expect(action.payload).toEqual({ by: "status", direction: "desc" });
141
- });
142
- });
143
-
144
- describe("cycleSort", () => {
145
- test("creates CYCLE_SORT action", () => {
146
- const action = cycleSort();
147
-
148
- expect(action.type).toBe("CYCLE_SORT");
149
- });
150
- });
151
-
152
- describe("setMode", () => {
153
- test("creates SET_MODE action for normal", () => {
154
- const action = setMode("normal");
155
-
156
- expect(action.type).toBe("SET_MODE");
157
- expect(action.payload).toBe("normal");
158
- });
159
-
160
- test("creates SET_MODE action for filter", () => {
161
- const action = setMode("filter");
162
-
163
- expect(action.type).toBe("SET_MODE");
164
- expect(action.payload).toBe("filter");
165
- });
166
-
167
- test("creates SET_MODE action for help", () => {
168
- const action = setMode("help");
169
-
170
- expect(action.type).toBe("SET_MODE");
171
- expect(action.payload).toBe("help");
172
- });
173
- });
174
-
175
- describe("startAction", () => {
176
- test("creates START_ACTION action", () => {
177
- const action = startAction("Pushing 5 projects");
178
-
179
- expect(action.type).toBe("START_ACTION");
180
- expect(action.payload).toBe("Pushing 5 projects");
181
- });
182
- });
183
-
184
- describe("endAction", () => {
185
- test("creates END_ACTION action", () => {
186
- const action = endAction();
187
-
188
- expect(action.type).toBe("END_ACTION");
189
- });
190
- });
191
-
192
- describe("setScrollOffset", () => {
193
- test("creates SET_SCROLL_OFFSET action", () => {
194
- const action = setScrollOffset(10);
195
-
196
- expect(action.type).toBe("SET_SCROLL_OFFSET");
197
- expect(action.payload).toBe(10);
198
- });
199
- });
200
-
201
- describe("updateProject", () => {
202
- test("creates UPDATE_PROJECT action", () => {
203
- const action = updateProject("project-123", { name: "new-name" });
204
-
205
- expect(action.type).toBe("UPDATE_PROJECT");
206
- expect(action.payload).toEqual({
207
- id: "project-123",
208
- updates: { name: "new-name" },
209
- });
210
- });
211
- });
212
- });
213
-
214
- // Restore all mocks after all tests complete
215
- afterAll(() => {
216
- mock.restore();
217
- });