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,256 +0,0 @@
1
- /**
2
- * Tests for rate limiter utilities
3
- */
4
- import { describe, test, expect } from "bun:test";
5
- import { Semaphore, RateLimiter, processBatch } from "../../../src/utils/rate-limiter.ts";
6
-
7
- describe("Semaphore", () => {
8
- test("should limit concurrent operations", async () => {
9
- const semaphore = new Semaphore(2);
10
- let concurrent = 0;
11
- let maxConcurrent = 0;
12
-
13
- const task = async () => {
14
- await semaphore.acquire();
15
- concurrent++;
16
- maxConcurrent = Math.max(maxConcurrent, concurrent);
17
- await new Promise(resolve => setTimeout(resolve, 50));
18
- concurrent--;
19
- semaphore.release();
20
- };
21
-
22
- await Promise.all([task(), task(), task(), task()]);
23
-
24
- expect(maxConcurrent).toBeLessThanOrEqual(2);
25
- });
26
-
27
- test("should run function with automatic release", async () => {
28
- const semaphore = new Semaphore(1);
29
-
30
- const result = await semaphore.run(async () => {
31
- return "success";
32
- });
33
-
34
- expect(result).toBe("success");
35
- });
36
-
37
- test("should release on error", async () => {
38
- const semaphore = new Semaphore(1);
39
-
40
- try {
41
- await semaphore.run(async () => {
42
- throw new Error("test error");
43
- });
44
- } catch {
45
- // Expected
46
- }
47
-
48
- // Should be able to acquire again after error
49
- const result = await semaphore.run(async () => "recovered");
50
- expect(result).toBe("recovered");
51
- });
52
- });
53
-
54
- describe("RateLimiter", () => {
55
- test("should enforce minimum delay between operations", async () => {
56
- const limiter = new RateLimiter(100);
57
- const times: number[] = [];
58
-
59
- for (let i = 0; i < 3; i++) {
60
- await limiter.wait();
61
- times.push(Date.now());
62
- }
63
-
64
- // Check delays between operations
65
- for (let i = 1; i < times.length; i++) {
66
- const delay = times[i]! - times[i-1]!;
67
- expect(delay).toBeGreaterThanOrEqual(95); // Allow small timing variance
68
- }
69
- });
70
- });
71
-
72
- describe("processBatch", () => {
73
- test("should process all items", async () => {
74
- const items = [1, 2, 3, 4, 5];
75
- const results = await processBatch(
76
- items,
77
- async (n) => n * 2,
78
- { concurrency: 2 }
79
- );
80
-
81
- expect(results).toEqual([
82
- { success: true, result: 2 },
83
- { success: true, result: 4 },
84
- { success: true, result: 6 },
85
- { success: true, result: 8 },
86
- { success: true, result: 10 }
87
- ]);
88
- });
89
-
90
- test("should call progress callback", async () => {
91
- const items = [1, 2, 3];
92
- const progressCalls: Array<[number, number]> = [];
93
-
94
- const results = await processBatch(
95
- items,
96
- async (n) => n,
97
- {
98
- concurrency: 1,
99
- onProgress: (completed, total) => {
100
- progressCalls.push([completed, total]);
101
- }
102
- }
103
- );
104
-
105
- expect(progressCalls).toEqual([[1, 3], [2, 3], [3, 3]]);
106
- expect(results).toHaveLength(3);
107
- expect(results.every(r => r.success)).toBe(true);
108
- });
109
-
110
- test("should handle errors with onError callback", async () => {
111
- const items = [1, 2, 3];
112
- const errors: Error[] = [];
113
-
114
- const results = await processBatch(
115
- items,
116
- async (n) => {
117
- if (n === 2) throw new Error("test error");
118
- return n;
119
- },
120
- {
121
- concurrency: 1,
122
- onError: (err, _item) => {
123
- errors.push(err);
124
- return true; // Continue processing
125
- }
126
- }
127
- );
128
-
129
- expect(results).toEqual([
130
- { success: true, result: 1 },
131
- { success: false, error: new Error("test error") },
132
- { success: true, result: 3 }
133
- ]);
134
- expect(errors).toHaveLength(1);
135
- });
136
-
137
- test("should respect concurrency limit", async () => {
138
- let concurrent = 0;
139
- let maxConcurrent = 0;
140
-
141
- const results = await processBatch(
142
- [1, 2, 3, 4, 5, 6],
143
- async () => {
144
- concurrent++;
145
- maxConcurrent = Math.max(maxConcurrent, concurrent);
146
- await new Promise(resolve => setTimeout(resolve, 20));
147
- concurrent--;
148
- },
149
- { concurrency: 3 }
150
- );
151
-
152
- expect(maxConcurrent).toBeLessThanOrEqual(3);
153
- expect(results).toHaveLength(6);
154
- expect(results.every(r => r.success)).toBe(true);
155
- });
156
-
157
- test("should stop processing when onError returns false", async () => {
158
- const items = [1, 2, 3, 4, 5];
159
- const processed: number[] = [];
160
- let errorThrown = false;
161
-
162
- try {
163
- await processBatch(
164
- items,
165
- async (n) => {
166
- processed.push(n);
167
- if (n === 3) {
168
- errorThrown = true;
169
- throw new Error("stop error");
170
- }
171
- return n;
172
- },
173
- {
174
- concurrency: 1,
175
- onError: (err, _item) => {
176
- return false; // Stop processing
177
- }
178
- }
179
- );
180
- // Should not reach here
181
- throw new Error("Expected processBatch to throw");
182
- } catch (error) {
183
- expect((error as Error).message).toBe("stop error");
184
- // With concurrency 1, items are processed sequentially
185
- // So we should have processed items 1, 2, and 3 before stopping
186
- expect(errorThrown).toBe(true);
187
- expect(processed).toContain(1);
188
- expect(processed).toContain(2);
189
- expect(processed).toContain(3);
190
- // Due to Promise.all creating all promises upfront, some later items
191
- // might have started processing before the error was thrown
192
- }
193
- });
194
-
195
- test("should capture error details in failed results", async () => {
196
- const items = [1, 2, 3];
197
- const customError = new Error("custom error message");
198
-
199
- const results = await processBatch(
200
- items,
201
- async (n) => {
202
- if (n === 2) throw customError;
203
- return n * 10;
204
- },
205
- {
206
- concurrency: 1,
207
- onError: (err, item) => {
208
- expect(err).toBe(customError);
209
- expect(item).toBe(2);
210
- return true; // Continue processing
211
- }
212
- }
213
- );
214
-
215
- expect(results).toHaveLength(3);
216
- expect(results[0]).toEqual({ success: true, result: 10 });
217
- expect(results[1]).toEqual({ success: false, error: customError });
218
- expect(results[2]).toEqual({ success: true, result: 30 });
219
- });
220
-
221
- test("should handle multiple errors with continue", async () => {
222
- const items = [1, 2, 3, 4, 5];
223
- const errorItems = new Set([2, 4]);
224
-
225
- const results = await processBatch(
226
- items,
227
- async (n) => {
228
- if (errorItems.has(n)) {
229
- throw new Error(`Error for item ${n}`);
230
- }
231
- return n * 2;
232
- },
233
- {
234
- concurrency: 2,
235
- onError: (err, _item) => {
236
- return true; // Continue on all errors
237
- }
238
- }
239
- );
240
-
241
- expect(results).toHaveLength(5);
242
- expect(results.filter(r => r.success)).toHaveLength(3);
243
- expect(results.filter(r => !r.success)).toHaveLength(2);
244
-
245
- // Check successful results
246
- expect(results[0]).toEqual({ success: true, result: 2 });
247
- expect(results[2]).toEqual({ success: true, result: 6 });
248
- expect(results[4]).toEqual({ success: true, result: 10 });
249
-
250
- // Check failed results
251
- expect(results[1]?.success).toBe(false);
252
- expect(results[1]?.error?.message).toBe("Error for item 2");
253
- expect(results[3]?.success).toBe(false);
254
- expect(results[3]?.error?.message).toBe("Error for item 4");
255
- });
256
- });
@@ -1,165 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { withRetry, RetryError, shouldRetryGitHubError } from "../../../src/utils/retry";
3
-
4
- describe("retry utilities", () => {
5
- describe("withRetry", () => {
6
- test("should succeed on first attempt", async () => {
7
- const fn = () => Promise.resolve("success");
8
- const result = await withRetry(fn);
9
- expect(result).toBe("success");
10
- });
11
-
12
- test("should retry on failure and eventually succeed", async () => {
13
- let attempts = 0;
14
- const fn = () => {
15
- attempts++;
16
- if (attempts < 3) {
17
- return Promise.reject(new Error("temporary error"));
18
- }
19
- return Promise.resolve("success");
20
- };
21
-
22
- const result = await withRetry(fn, { maxAttempts: 3 });
23
- expect(result).toBe("success");
24
- expect(attempts).toBe(3);
25
- });
26
-
27
- test("should throw RetryError after max attempts", async () => {
28
- const fn = () => Promise.reject(new Error("persistent error"));
29
-
30
- try {
31
- await withRetry(fn, { maxAttempts: 3 });
32
- expect(true).toBe(false); // Should not reach here
33
- } catch (error) {
34
- expect(error).toBeInstanceOf(RetryError);
35
- expect((error as RetryError).attempts).toBe(3);
36
- expect((error as RetryError).lastError.message).toBe("persistent error");
37
- }
38
- });
39
-
40
- test("should use exponential backoff", async () => {
41
- const delays: number[] = [];
42
- let lastTime = Date.now();
43
-
44
- const fn = () => {
45
- const currentTime = Date.now();
46
- delays.push(currentTime - lastTime);
47
- lastTime = currentTime;
48
- return Promise.reject(new Error("error"));
49
- };
50
-
51
- try {
52
- await withRetry(fn, {
53
- maxAttempts: 4,
54
- initialDelay: 100,
55
- backoffFactor: 2,
56
- onRetry: () => {
57
- // Track delays between retries
58
- }
59
- });
60
- } catch (error) {
61
- // Expected to fail
62
- }
63
-
64
- // First attempt should be immediate (no delay)
65
- expect(delays[0]).toBeLessThan(50);
66
-
67
- // Subsequent delays should increase exponentially
68
- expect(delays[1]).toBeGreaterThan(80); // ~100ms
69
- expect(delays[2]).toBeGreaterThan(180); // ~200ms
70
- expect(delays[3]).toBeGreaterThan(380); // ~400ms
71
- });
72
-
73
- test("should respect maxDelay", async () => {
74
- const fn = () => Promise.reject(new Error("error"));
75
- const startTime = Date.now();
76
-
77
- try {
78
- await withRetry(fn, {
79
- maxAttempts: 3,
80
- initialDelay: 1000,
81
- maxDelay: 1500,
82
- backoffFactor: 3 // Would result in 3000ms delay without maxDelay
83
- });
84
- } catch (error) {
85
- // Expected to fail
86
- }
87
-
88
- const totalTime = Date.now() - startTime;
89
- expect(totalTime).toBeLessThan(3000); // Should be faster than 1000 + 3000ms
90
- });
91
-
92
- test("should not retry when shouldRetry returns false", async () => {
93
- let attempts = 0;
94
- const fn = () => {
95
- attempts++;
96
- return Promise.reject(new Error("do not retry"));
97
- };
98
-
99
- const shouldRetry = (error: Error) => error.message !== "do not retry";
100
-
101
- try {
102
- await withRetry(fn, { maxAttempts: 3, shouldRetry });
103
- } catch (error) {
104
- // Expected to fail
105
- }
106
-
107
- expect(attempts).toBe(1); // Only one attempt
108
- });
109
-
110
- test("should call onRetry callback", async () => {
111
- const retryCallbacks: Array<{ error: Error; attempt: number; delay: number }> = [];
112
-
113
- const fn = () => Promise.reject(new Error("error"));
114
- const onRetry = (error: Error, attempt: number, nextDelay: number) => {
115
- retryCallbacks.push({ error, attempt, delay: nextDelay });
116
- };
117
-
118
- try {
119
- await withRetry(fn, { maxAttempts: 3, onRetry });
120
- } catch (error) {
121
- // Expected to fail
122
- }
123
-
124
- expect(retryCallbacks).toHaveLength(2);
125
- expect(retryCallbacks[0]?.attempt).toBe(1);
126
- expect(retryCallbacks[1]?.attempt).toBe(2);
127
- expect(retryCallbacks[0]?.delay).toBe(1000);
128
- expect(retryCallbacks[1]?.delay).toBe(2000);
129
- });
130
- });
131
-
132
- describe("shouldRetryGitHubError", () => {
133
- test("should retry rate limit errors", () => {
134
- expect(shouldRetryGitHubError(new Error("Rate limit exceeded"), 1)).toBe(true);
135
- expect(shouldRetryGitHubError(new Error("API rate limit exceeded"), 1)).toBe(true);
136
- expect(shouldRetryGitHubError(new Error("429 Too Many Requests"), 1)).toBe(true);
137
- });
138
-
139
- test("should retry server errors", () => {
140
- expect(shouldRetryGitHubError(new Error("Internal Server Error 500"), 1)).toBe(true);
141
- expect(shouldRetryGitHubError(new Error("502 Bad Gateway"), 1)).toBe(true);
142
- expect(shouldRetryGitHubError(new Error("503 Service Unavailable"), 1)).toBe(true);
143
- expect(shouldRetryGitHubError(new Error("504 Gateway Timeout"), 1)).toBe(true);
144
- });
145
-
146
- test("should retry network errors", () => {
147
- expect(shouldRetryGitHubError(new Error("Network error"), 1)).toBe(true);
148
- expect(shouldRetryGitHubError(new Error("ECONNRESET"), 1)).toBe(true);
149
- expect(shouldRetryGitHubError(new Error("ETIMEDOUT"), 1)).toBe(true);
150
- expect(shouldRetryGitHubError(new Error("fetch failed"), 1)).toBe(true);
151
- });
152
-
153
- test("should not retry client errors", () => {
154
- expect(shouldRetryGitHubError(new Error("401 Unauthorized"), 1)).toBe(false);
155
- expect(shouldRetryGitHubError(new Error("403 Forbidden"), 1)).toBe(false);
156
- expect(shouldRetryGitHubError(new Error("404 Not Found"), 1)).toBe(false);
157
- expect(shouldRetryGitHubError(new Error("422 Unprocessable Entity"), 1)).toBe(false);
158
- });
159
-
160
- test("should retry other errors by default", () => {
161
- expect(shouldRetryGitHubError(new Error("Unknown error"), 1)).toBe(true);
162
- expect(shouldRetryGitHubError(new Error("Some random error"), 1)).toBe(true);
163
- });
164
- });
165
- });
@@ -1,13 +0,0 @@
1
- /**
2
- * Strip ANSI escape codes from a string
3
- * Used for comparing ink component output in tests
4
- *
5
- * This is a fallback utility if NO_COLOR environment variable doesn't work.
6
- * The preload.ts script sets NO_COLOR=1, but some libraries may ignore it.
7
- */
8
- export function stripAnsi(str: string | undefined): string {
9
- if (!str) return "";
10
- // Match ANSI escape sequences: ESC[ followed by params and command
11
- const ansiPattern = /\u001b\[[0-9;]*[a-zA-Z]/g;
12
- return str.replace(ansiPattern, "");
13
- }
@@ -1,93 +0,0 @@
1
- /**
2
- * Tests for timeout utility
3
- */
4
-
5
- import { describe, test, expect } from "bun:test";
6
- import { withTimeout, createTimeoutController, TimeoutError } from "../../../src/utils/timeout.ts";
7
-
8
- describe("withTimeout", () => {
9
- test("should resolve when promise completes before timeout", async () => {
10
- const promise = Promise.resolve("success");
11
- const result = await withTimeout(promise, 1000, "test operation");
12
- expect(result).toBe("success");
13
- });
14
-
15
- test("should reject with TimeoutError when promise takes too long", async () => {
16
- const promise = new Promise((resolve) => setTimeout(() => resolve("too late"), 2000));
17
-
18
- await expect(
19
- withTimeout(promise, 100, "slow operation")
20
- ).rejects.toThrow(TimeoutError);
21
- });
22
-
23
- test("should include operation name in timeout error", async () => {
24
- const promise = new Promise((resolve) => setTimeout(() => resolve("too late"), 2000));
25
-
26
- try {
27
- await withTimeout(promise, 100, "git clone");
28
- } catch (error) {
29
- expect(error).toBeInstanceOf(TimeoutError);
30
- expect((error as TimeoutError).operation).toBe("git clone");
31
- expect((error as Error).message).toContain("git clone");
32
- expect((error as Error).message).toContain("100ms");
33
- }
34
- });
35
-
36
- test("should handle already resolved promises", async () => {
37
- const result = await withTimeout(Promise.resolve("immediate"), 1000, "test");
38
- expect(result).toBe("immediate");
39
- });
40
-
41
- test("should handle already rejected promises", async () => {
42
- const error = new Error("test error");
43
- await expect(
44
- withTimeout(Promise.reject(error), 1000, "test")
45
- ).rejects.toThrow("test error");
46
- });
47
- });
48
-
49
- describe("createTimeoutController", () => {
50
- test("should abort after timeout", async () => {
51
- const controller = createTimeoutController(100, "test operation");
52
-
53
- expect(controller.signal.aborted).toBe(false);
54
-
55
- await new Promise(resolve => setTimeout(resolve, 150));
56
-
57
- expect(controller.signal.aborted).toBe(true);
58
- });
59
-
60
- test("should not abort before timeout", async () => {
61
- const controller = createTimeoutController(1000, "test operation");
62
-
63
- expect(controller.signal.aborted).toBe(false);
64
-
65
- await new Promise(resolve => setTimeout(resolve, 100));
66
-
67
- expect(controller.signal.aborted).toBe(false);
68
- });
69
-
70
- test("should include timeout error in abort reason", async () => {
71
- const controller = createTimeoutController(100, "test operation");
72
-
73
- await new Promise(resolve => setTimeout(resolve, 150));
74
-
75
- expect(controller.signal.reason).toBeInstanceOf(TimeoutError);
76
- expect((controller.signal.reason as TimeoutError).operation).toBe("test operation");
77
- });
78
-
79
- test("should clean up timer when manually aborted", async () => {
80
- const controller = createTimeoutController(1000, "test operation");
81
-
82
- // Manually abort
83
- controller.abort();
84
-
85
- expect(controller.signal.aborted).toBe(true);
86
-
87
- // Wait past the original timeout
88
- await new Promise(resolve => setTimeout(resolve, 1100));
89
-
90
- // Should still be aborted (no double abort)
91
- expect(controller.signal.aborted).toBe(true);
92
- });
93
- });
package/tsconfig.json DELETED
@@ -1,29 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- // Environment setup & latest features
4
- "lib": ["ESNext"],
5
- "target": "ESNext",
6
- "module": "Preserve",
7
- "moduleDetection": "force",
8
- "jsx": "react-jsx",
9
- "allowJs": true,
10
-
11
- // Bundler mode
12
- "moduleResolution": "bundler",
13
- "allowImportingTsExtensions": true,
14
- "verbatimModuleSyntax": true,
15
- "noEmit": true,
16
-
17
- // Best practices
18
- "strict": true,
19
- "skipLibCheck": true,
20
- "noFallthroughCasesInSwitch": true,
21
- "noUncheckedIndexedAccess": true,
22
- "noImplicitOverride": true,
23
-
24
- // Some stricter flags (disabled by default)
25
- "noUnusedLocals": false,
26
- "noUnusedParameters": false,
27
- "noPropertyAccessFromIndexSignature": false
28
- }
29
- }