gitforest 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/.bunignore +7 -0
  2. package/.github/workflows/ci.yml +73 -0
  3. package/CLAUDE.md +111 -0
  4. package/CONTRIBUTING.md +145 -0
  5. package/README.md +168 -0
  6. package/bun.lock +267 -0
  7. package/bunfig.toml +15 -0
  8. package/cli +0 -0
  9. package/config/gitforest.example.yaml +94 -0
  10. package/docs/ai/IMPROVEMENT_PLAN.md +341 -0
  11. package/docs/ai/VERIFICATION_REPORT.md +87 -0
  12. package/docs/ai/architecture.md +169 -0
  13. package/docs/ai/checks/check-2025-12-02-tests.md +40 -0
  14. package/docs/ai/checks/check-2025-12-02.md +55 -0
  15. package/docs/ai/checks/test-verification-report.md +85 -0
  16. package/docs/ai/implementation-guide.md +776 -0
  17. package/docs/ai/research/gitty-codebase-analysis.md +221 -0
  18. package/docs/ai/tickets/GENERAL-sitrep.md +30 -0
  19. package/docs/ai/tickets/TASK-database-tests-sitrep.md +25 -0
  20. package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +28 -0
  21. package/docs/ai/tickets/TASK-detail-modal-sitrep.md +28 -0
  22. package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +24 -0
  23. package/docs/ai/tickets/TASK-github-service-sitrep.md +32 -0
  24. package/docs/ai/tickets/TASK-github-token-sitrep.md +51 -0
  25. package/docs/ai/tickets/TASK-hascommits-sitrep.md +35 -0
  26. package/docs/ai/tickets/TASK-keybindings-sitrep.md +26 -0
  27. package/docs/ai/tickets/TASK-layout-sitrep.md +25 -0
  28. package/docs/ai/tickets/TASK-markdown-sitrep.md +28 -0
  29. package/docs/ai/tickets/TASK-project-item-sitrep.md +79 -0
  30. package/docs/ai/tickets/TASK-sitrep.md +28 -0
  31. package/docs/ai/tickets/TASK-state-sitrep.md +26 -0
  32. package/docs/ai/tickets/TASK-types-sitrep.md +25 -0
  33. package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +26 -0
  34. package/docs/ai/tickets/TKT-001-sitrep.md +24 -0
  35. package/docs/ai/tickets/TKT-002-sitrep.md +25 -0
  36. package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +46 -0
  37. package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +135 -0
  38. package/docs/ai/tickets/TKT-003-sitrep.md +26 -0
  39. package/docs/ai/tickets/TKT-004-sitrep.md +27 -0
  40. package/docs/ai/tickets/TKT-005-sitrep.md +25 -0
  41. package/docs/ai/tickets/TKT-006-sitrep.md +26 -0
  42. package/docs/ai/tickets/TKT-007-sitrep.md +30 -0
  43. package/docs/ai/tickets/TKT-008-sitrep.md +32 -0
  44. package/docs/ai/tickets/TKT-009-sitrep.md +27 -0
  45. package/docs/ai/tickets/TKT-010-sitrep.md +27 -0
  46. package/docs/ai/tickets/TKT-011-sitrep.md +26 -0
  47. package/docs/ai/tickets/TKT-012-sitrep.md +25 -0
  48. package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +28 -0
  49. package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +25 -0
  50. package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +25 -0
  51. package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +24 -0
  52. package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +29 -0
  53. package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +29 -0
  54. package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +26 -0
  55. package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +30 -0
  56. package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +28 -0
  57. package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +26 -0
  58. package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +25 -0
  59. package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +30 -0
  60. package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +25 -0
  61. package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +29 -0
  62. package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +95 -0
  63. package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +61 -0
  64. package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +30 -0
  65. package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +27 -0
  66. package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +25 -0
  67. package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +27 -0
  68. package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +25 -0
  69. package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +28 -0
  70. package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +25 -0
  71. package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +26 -0
  72. package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +25 -0
  73. package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +26 -0
  74. package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +24 -0
  75. package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +25 -0
  76. package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +29 -0
  77. package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +25 -0
  78. package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +24 -0
  79. package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +24 -0
  80. package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +27 -0
  81. package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +25 -0
  82. package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +25 -0
  83. package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +27 -0
  84. package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +32 -0
  85. package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +27 -0
  86. package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +30 -0
  87. package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +25 -0
  88. package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +29 -0
  89. package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +25 -0
  90. package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +27 -0
  91. package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +25 -0
  92. package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +26 -0
  93. package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +25 -0
  94. package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +27 -0
  95. package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +75 -0
  96. package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +29 -0
  97. package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +29 -0
  98. package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +25 -0
  99. package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +25 -0
  100. package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +32 -0
  101. package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +64 -0
  102. package/docs/ai/tkt-001-fix-database-error.md +217 -0
  103. package/docs/ai/ui-enhancement-plan.md +562 -0
  104. package/package.json +50 -0
  105. package/src/app.tsx +43 -0
  106. package/src/cli/config.ts +94 -0
  107. package/src/cli/formatters.ts +632 -0
  108. package/src/cli/index.ts +583 -0
  109. package/src/components/CloneDialog.tsx +137 -0
  110. package/src/components/ColumnHeader.tsx +128 -0
  111. package/src/components/CommandPalette.tsx +120 -0
  112. package/src/components/ConfirmDialog.tsx +105 -0
  113. package/src/components/ErrorBoundary.tsx +128 -0
  114. package/src/components/FilterBar.tsx +71 -0
  115. package/src/components/FilterOptionsOverlay.tsx +131 -0
  116. package/src/components/HelpOverlay.tsx +120 -0
  117. package/src/components/Layout.tsx +379 -0
  118. package/src/components/MarkdownRenderer.tsx +127 -0
  119. package/src/components/ProgressBar.tsx +53 -0
  120. package/src/components/ProjectItem.tsx +143 -0
  121. package/src/components/ProjectList.tsx +90 -0
  122. package/src/components/RepoDetailModal.tsx +367 -0
  123. package/src/components/StatusBar.tsx +188 -0
  124. package/src/components/UnifiedProjectItem.tsx +436 -0
  125. package/src/components/ViewModeIndicator.tsx +37 -0
  126. package/src/components/onboarding/CompleteStep.tsx +82 -0
  127. package/src/components/onboarding/DirectoriesStep.test.tsx +52 -0
  128. package/src/components/onboarding/DirectoriesStep.tsx +847 -0
  129. package/src/components/onboarding/DirectoriesStep.unit.test.ts +345 -0
  130. package/src/components/onboarding/GitHubAuthStep.tsx +268 -0
  131. package/src/components/onboarding/OnboardingWizard.tsx +130 -0
  132. package/src/components/onboarding/WelcomeStep.tsx +69 -0
  133. package/src/config/loader.ts +263 -0
  134. package/src/config/onboarding.ts +67 -0
  135. package/src/constants.ts +96 -0
  136. package/src/db/index.ts +147 -0
  137. package/src/db/schema.ts +70 -0
  138. package/src/git/commands.ts +283 -0
  139. package/src/git/index.ts +2 -0
  140. package/src/git/operations.ts +93 -0
  141. package/src/git/service.ts +539 -0
  142. package/src/git/status.ts +84 -0
  143. package/src/git/types.ts +5 -0
  144. package/src/github/auth.ts +311 -0
  145. package/src/github/cache.ts +231 -0
  146. package/src/github/cli.ts +22 -0
  147. package/src/github/unified.ts +415 -0
  148. package/src/hooks/useBackgroundFetch.ts +76 -0
  149. package/src/hooks/useConfirmDialogActions.ts +120 -0
  150. package/src/hooks/useKeyBindings.ts +656 -0
  151. package/src/hooks/useProjects.ts +47 -0
  152. package/src/hooks/useUnifiedRepos.ts +317 -0
  153. package/src/index.tsx +494 -0
  154. package/src/operations/batch.ts +280 -0
  155. package/src/operations/commands.ts +140 -0
  156. package/src/operations/index.ts +37 -0
  157. package/src/scanner/index.ts +424 -0
  158. package/src/scanner/markers.ts +43 -0
  159. package/src/scanner/submodules.ts +61 -0
  160. package/src/services/git.ts +484 -0
  161. package/src/services/github.ts +676 -0
  162. package/src/services/index.ts +28 -0
  163. package/src/services/types.ts +99 -0
  164. package/src/state/actions.ts +175 -0
  165. package/src/state/reducer.ts +294 -0
  166. package/src/state/store.tsx +216 -0
  167. package/src/state/types.ts +8 -0
  168. package/src/types/index.ts +383 -0
  169. package/src/ui/theme.ts +44 -0
  170. package/src/utils/array.ts +14 -0
  171. package/src/utils/debug.ts +38 -0
  172. package/src/utils/errors.ts +17 -0
  173. package/src/utils/index.ts +8 -0
  174. package/src/utils/markdown.ts +230 -0
  175. package/src/utils/project-utils.ts +129 -0
  176. package/src/utils/rate-limiter.ts +134 -0
  177. package/src/utils/retry.ts +147 -0
  178. package/src/utils/timeout.ts +56 -0
  179. package/test/integration/app.isolated.tsx +240 -0
  180. package/test/integration/cli-commands.test.ts +287 -0
  181. package/test/integration/cli-validation.test.ts +264 -0
  182. package/test/integration/git-operations.test.ts +218 -0
  183. package/test/integration/scanner.test.ts +228 -0
  184. package/test/preload.ts +18 -0
  185. package/test/unit/cli/commands.test.ts +13 -0
  186. package/test/unit/cli/formatters.test.ts +1116 -0
  187. package/test/unit/cli/github-commands.test.ts +12 -0
  188. package/test/unit/components/CloneDialog.test.tsx +240 -0
  189. package/test/unit/components/ColumnHeader.test.tsx +128 -0
  190. package/test/unit/components/CommandPalette.test.tsx +355 -0
  191. package/test/unit/components/ConfirmDialog.test.tsx +111 -0
  192. package/test/unit/components/ErrorBoundary.test.tsx +139 -0
  193. package/test/unit/components/FilterBar.test.tsx +43 -0
  194. package/test/unit/components/FilterOptionsOverlay.test.tsx +197 -0
  195. package/test/unit/components/HelpOverlay.test.tsx +90 -0
  196. package/test/unit/components/Layout.test.tsx +328 -0
  197. package/test/unit/components/MarkdownRenderer.test.tsx +45 -0
  198. package/test/unit/components/ProgressBar.test.tsx +138 -0
  199. package/test/unit/components/ProjectItem.test.tsx +182 -0
  200. package/test/unit/components/ProjectList.test.tsx +311 -0
  201. package/test/unit/components/RepoDetailModal.test.tsx +445 -0
  202. package/test/unit/components/StatusBar.test.tsx +112 -0
  203. package/test/unit/components/UnifiedProjectItem.test.tsx +618 -0
  204. package/test/unit/components/ViewModeIndicator.test.tsx +137 -0
  205. package/test/unit/components/test-utils.tsx +63 -0
  206. package/test/unit/config/loader.test.ts +692 -0
  207. package/test/unit/db/database.test.ts +978 -0
  208. package/test/unit/db/index.test.ts +314 -0
  209. package/test/unit/fixtures/setup.ts +186 -0
  210. package/test/unit/git/commands-untested.test.ts +205 -0
  211. package/test/unit/git/commands.test.ts +269 -0
  212. package/test/unit/git/operations.test.ts +322 -0
  213. package/test/unit/git/status.test.ts +219 -0
  214. package/test/unit/github/auth.test.ts +317 -0
  215. package/test/unit/github/cache.test.ts +1028 -0
  216. package/test/unit/github/cli.test.ts +135 -0
  217. package/test/unit/github/unified.test.ts +1201 -0
  218. package/test/unit/graceful-shutdown.test.ts +83 -0
  219. package/test/unit/hooks/useBackgroundFetch.test.tsx +239 -0
  220. package/test/unit/hooks/useConfirmDialogActions.test.tsx +81 -0
  221. package/test/unit/hooks/useKeyBindings.isolated.ts +715 -0
  222. package/test/unit/hooks/useProjects.test.tsx +186 -0
  223. package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +115 -0
  224. package/test/unit/hooks/useUnifiedRepos.test.tsx +177 -0
  225. package/test/unit/mocks/config.ts +109 -0
  226. package/test/unit/mocks/git-service.ts +274 -0
  227. package/test/unit/mocks/github-service.ts +250 -0
  228. package/test/unit/mocks/index.ts +72 -0
  229. package/test/unit/mocks/project.ts +148 -0
  230. package/test/unit/mocks/state-mocks.ts +187 -0
  231. package/test/unit/mocks/unified.ts +169 -0
  232. package/test/unit/operations/batch.test.ts +216 -0
  233. package/test/unit/operations/commands.test.ts +550 -0
  234. package/test/unit/scanner/errors.test.ts +297 -0
  235. package/test/unit/scanner/index.test.ts +1011 -0
  236. package/test/unit/scanner/markers.test.ts +150 -0
  237. package/test/unit/scanner/submodules.test.ts +99 -0
  238. package/test/unit/services/git-errors.test.ts +190 -0
  239. package/test/unit/services/git.test.ts +442 -0
  240. package/test/unit/services/github-errors.test.ts +293 -0
  241. package/test/unit/services/github.test.ts +200 -0
  242. package/test/unit/state/actions.test.ts +217 -0
  243. package/test/unit/state/reducer.test.ts +745 -0
  244. package/test/unit/state/store.test.tsx +711 -0
  245. package/test/unit/types/commands.test.ts +220 -0
  246. package/test/unit/types/schema.test.ts +179 -0
  247. package/test/unit/utils/array.test.ts +73 -0
  248. package/test/unit/utils/debug.test.ts +23 -0
  249. package/test/unit/utils/errors.test.ts +295 -0
  250. package/test/unit/utils/markdown.test.ts +163 -0
  251. package/test/unit/utils/project-utils.test.ts +756 -0
  252. package/test/unit/utils/rate-limiter.test.ts +256 -0
  253. package/test/unit/utils/retry.test.ts +165 -0
  254. package/test/unit/utils/strip-ansi.ts +13 -0
  255. package/test/unit/utils/timeout.test.ts +93 -0
  256. package/tsconfig.json +29 -0
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Tests for graceful shutdown functionality
3
+ */
4
+ import { describe, test, expect } from "bun:test";
5
+ import { spawn } from "bun";
6
+ import { join } from "path";
7
+
8
+ describe("Graceful Shutdown", () => {
9
+ test("should register signal handlers when main module loads", async () => {
10
+ // Test that the main module properly registers signal handlers
11
+ // by spawning the process and checking it handles signals gracefully
12
+
13
+ const gitforestPath = join(import.meta.dir, "../../src/index.tsx");
14
+ const proc = spawn(["bun", gitforestPath, "--help"], {
15
+ stdout: "pipe",
16
+ stderr: "pipe",
17
+ });
18
+
19
+ let stdout = "";
20
+ let stderr = "";
21
+
22
+ for await (const chunk of proc.stdout) {
23
+ stdout += new TextDecoder().decode(chunk);
24
+ }
25
+
26
+ for await (const chunk of proc.stderr) {
27
+ stderr += new TextDecoder().decode(chunk);
28
+ }
29
+
30
+ const exitCode = await proc.exited;
31
+
32
+ // Should exit cleanly with code 0
33
+ expect(exitCode).toBe(0);
34
+
35
+ // Should show help text
36
+ expect(stdout).toContain("gitforest - Git Repository Manager");
37
+ expect(stdout).toContain("Usage:");
38
+ }, { timeout: 5000 });
39
+
40
+ test("should handle multiple shutdown attempts gracefully", async () => {
41
+ // Test the isShuttingDown flag behavior by simulating the pattern
42
+ let isShuttingDown = false;
43
+ let shutdownCount = 0;
44
+
45
+ const gracefulShutdown = async (_signal: string) => {
46
+ if (isShuttingDown) return;
47
+ isShuttingDown = true;
48
+ shutdownCount++;
49
+ };
50
+
51
+ // Simulate multiple rapid shutdown calls
52
+ await Promise.all([
53
+ gracefulShutdown("SIGINT"),
54
+ gracefulShutdown("SIGINT"),
55
+ gracefulShutdown("SIGTERM")
56
+ ]);
57
+
58
+ // With protection, only the first should execute
59
+ expect(shutdownCount).toBe(1);
60
+ expect(isShuttingDown).toBe(true);
61
+ });
62
+
63
+ test("should close database on shutdown", async () => {
64
+ // Mock the database close function to verify it's called
65
+ let dbClosed = false;
66
+ const mockCloseDb = () => {
67
+ dbClosed = true;
68
+ };
69
+
70
+ // Simulate shutdown sequence
71
+ const gracefulShutdown = async () => {
72
+ try {
73
+ mockCloseDb();
74
+ } catch (error) {
75
+ console.error('Error during shutdown:', error);
76
+ }
77
+ };
78
+
79
+ await gracefulShutdown();
80
+
81
+ expect(dbClosed).toBe(true);
82
+ });
83
+ });
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Tests for useBackgroundFetch hook
3
+ *
4
+ * Uses TestStoreProvider and dependency injection instead of mock.module()
5
+ */
6
+
7
+ import { describe, test, expect, mock } from "bun:test";
8
+ import React from "react";
9
+ import { render } from "ink-testing-library";
10
+ import { Text } from "ink";
11
+ import { useBackgroundFetch } from "../../../src/hooks/useBackgroundFetch.ts";
12
+ import { TestStoreProvider } from "../../../src/state/store.tsx";
13
+ import { createMockUnifiedAppState } from "../mocks/index.ts";
14
+ import type { GitforestConfig, Project, UnifiedAppState } from "../../../src/types/index.ts";
15
+
16
+ // Mock config factory
17
+ const createDefaultConfig = (): GitforestConfig => ({
18
+ directories: [{ path: "/test", maxDepth: 2 }],
19
+ scan: {
20
+ ignore: ["node_modules", ".git"],
21
+ includeHidden: false,
22
+ concurrency: 5,
23
+ },
24
+ github: { defaultVisibility: "private" as const },
25
+ display: {
26
+ showSubmodules: true,
27
+ showNonGitProjects: true,
28
+ sortBy: "status" as const,
29
+ sortDirection: "desc" as const,
30
+ },
31
+ cache: {
32
+ ttlSeconds: 300,
33
+ githubTtlSeconds: 600,
34
+ enableBackgroundRefresh: true,
35
+ backgroundRefreshIntervalSeconds: 300,
36
+ },
37
+ commands: [],
38
+ });
39
+
40
+ // Create mock projects
41
+ const createGitProjectWithRemote = (id: string, name: string): Project => ({
42
+ id,
43
+ name,
44
+ path: `/test/${name}`,
45
+ type: "git",
46
+ projectMarker: null,
47
+ submodule: null,
48
+ lastScanned: new Date(),
49
+ lastModified: null,
50
+ status: {
51
+ hasUnstagedChanges: false,
52
+ hasStagedChanges: false,
53
+ hasUntrackedFiles: false,
54
+ modifiedCount: 0,
55
+ stagedCount: 0,
56
+ untrackedCount: 0,
57
+ currentBranch: "main",
58
+ trackingBranch: "origin/main",
59
+ unpushedCommits: 0,
60
+ unpulledCommits: 0,
61
+ hasRemote: true,
62
+ remoteUrl: "git@github.com:user/repo.git",
63
+ lastLocalCommit: null,
64
+ lastRemoteActivity: null,
65
+ hasCommits: true,
66
+ isDirty: false,
67
+ isAhead: false,
68
+ isBehind: false,
69
+ isOutOfSync: false,
70
+ },
71
+ });
72
+
73
+ /**
74
+ * Helper to create test component with store context
75
+ */
76
+ function createTestComponent(
77
+ config: GitforestConfig,
78
+ state: UnifiedAppState,
79
+ mockOnRefresh: () => Promise<void>,
80
+ mockBatchFetch: () => Promise<any>,
81
+ mockDispatch: (action: any) => void,
82
+ ) {
83
+ const TestComponent = () => {
84
+ useBackgroundFetch(config, mockOnRefresh, {
85
+ batchFetch: mockBatchFetch,
86
+ });
87
+ return <Text>Test</Text>;
88
+ };
89
+
90
+ return render(
91
+ <TestStoreProvider initialState={state} dispatch={mockDispatch}>
92
+ <TestComponent />
93
+ </TestStoreProvider>
94
+ );
95
+ }
96
+
97
+ describe("useBackgroundFetch", () => {
98
+ describe("Configuration tests", () => {
99
+ test("does not run when disabled in config", () => {
100
+ const config = {
101
+ ...createDefaultConfig(),
102
+ cache: {
103
+ ...createDefaultConfig().cache,
104
+ enableBackgroundRefresh: false,
105
+ },
106
+ };
107
+
108
+ const mockBatchFetch = mock(() => Promise.resolve());
109
+ const mockOnRefresh = mock(() => Promise.resolve());
110
+ const mockDispatch = mock(() => {});
111
+
112
+ const state = createMockUnifiedAppState({
113
+ isLoading: false,
114
+ projects: [createGitProjectWithRemote("1", "test-repo")],
115
+ });
116
+
117
+ const { unmount, lastFrame } = createTestComponent(
118
+ config,
119
+ state,
120
+ mockOnRefresh,
121
+ mockBatchFetch,
122
+ mockDispatch
123
+ );
124
+
125
+ expect(lastFrame()).toContain("Test");
126
+
127
+ unmount();
128
+ });
129
+
130
+ test("hook renders without error when enabled", () => {
131
+ const config = createDefaultConfig();
132
+ const mockBatchFetch = mock(() => Promise.resolve());
133
+ const mockOnRefresh = mock(() => Promise.resolve());
134
+ const mockDispatch = mock(() => {});
135
+
136
+ const state = createMockUnifiedAppState({
137
+ isLoading: true,
138
+ });
139
+
140
+ const { unmount, lastFrame } = createTestComponent(
141
+ config,
142
+ state,
143
+ mockOnRefresh,
144
+ mockBatchFetch,
145
+ mockDispatch
146
+ );
147
+
148
+ expect(lastFrame()).toContain("Test");
149
+
150
+ unmount();
151
+ });
152
+ });
153
+
154
+ describe("State-based skipping tests", () => {
155
+ test("does not run during loading state", () => {
156
+ const config = createDefaultConfig();
157
+ const mockBatchFetch = mock(() => Promise.resolve());
158
+ const mockOnRefresh = mock(() => Promise.resolve());
159
+ const mockDispatch = mock(() => {});
160
+
161
+ const state = createMockUnifiedAppState({
162
+ isLoading: true,
163
+ projects: [createGitProjectWithRemote("1", "test-repo")],
164
+ });
165
+
166
+ const { unmount, lastFrame } = createTestComponent(
167
+ config,
168
+ state,
169
+ mockOnRefresh,
170
+ mockBatchFetch,
171
+ mockDispatch
172
+ );
173
+
174
+ expect(lastFrame()).toContain("Test");
175
+ // Should not have dispatched any actions since loading
176
+ expect(mockDispatch).not.toHaveBeenCalled();
177
+
178
+ unmount();
179
+ });
180
+
181
+ test("does not run during action in progress", () => {
182
+ const config = createDefaultConfig();
183
+ const mockBatchFetch = mock(() => Promise.resolve());
184
+ const mockOnRefresh = mock(() => Promise.resolve());
185
+ const mockDispatch = mock(() => {});
186
+
187
+ const state = createMockUnifiedAppState({
188
+ isLoading: false,
189
+ actionInProgress: "some-action",
190
+ projects: [createGitProjectWithRemote("1", "test-repo")],
191
+ });
192
+
193
+ const { unmount, lastFrame } = createTestComponent(
194
+ config,
195
+ state,
196
+ mockOnRefresh,
197
+ mockBatchFetch,
198
+ mockDispatch
199
+ );
200
+
201
+ expect(lastFrame()).toContain("Test");
202
+ // Should not have dispatched since action in progress
203
+ expect(mockDispatch).not.toHaveBeenCalled();
204
+
205
+ unmount();
206
+ });
207
+ });
208
+
209
+ describe("Return value tests", () => {
210
+ test("returns null", () => {
211
+ const config = createDefaultConfig();
212
+ const mockBatchFetch = mock(() => Promise.resolve());
213
+ const mockOnRefresh = mock(() => Promise.resolve());
214
+ const mockDispatch = mock(() => {});
215
+
216
+ const state = createMockUnifiedAppState({
217
+ isLoading: true,
218
+ });
219
+
220
+ let returnValue: any;
221
+ const TestComponent = () => {
222
+ returnValue = useBackgroundFetch(config, mockOnRefresh, {
223
+ batchFetch: mockBatchFetch,
224
+ });
225
+ return <Text>Test</Text>;
226
+ };
227
+
228
+ const { unmount } = render(
229
+ <TestStoreProvider initialState={state} dispatch={mockDispatch}>
230
+ <TestComponent />
231
+ </TestStoreProvider>
232
+ );
233
+
234
+ expect(returnValue).toBeNull();
235
+
236
+ unmount();
237
+ });
238
+ });
239
+ });
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Tests for useConfirmDialogActions hook
3
+ *
4
+ * Note: React hooks can only be tested inside React components.
5
+ * These tests focus on the underlying operations and logic that don't
6
+ * require the hook context. The hook's integration is tested via
7
+ * component tests and integration tests.
8
+ */
9
+
10
+ import { describe, test, expect, mock, beforeEach } from "bun:test";
11
+
12
+ // Mock git init function
13
+ const mockInitGitInProject = mock(() => Promise.resolve({ success: true, operation: "init" as const }));
14
+
15
+ // Mock GitHub service
16
+ const mockCreateRepo = mock(() => Promise.resolve({ success: true, url: "https://github.com/test/repo" }));
17
+ const mockArchiveRepo = mock(() => Promise.resolve({ success: true }));
18
+
19
+ describe("useConfirmDialogActions operations", () => {
20
+ beforeEach(() => {
21
+ mockInitGitInProject.mockClear();
22
+ mockCreateRepo.mockClear();
23
+ mockArchiveRepo.mockClear();
24
+ });
25
+
26
+ describe("Git init operation", () => {
27
+ test("init returns success for valid directory", async () => {
28
+ mockInitGitInProject.mockResolvedValueOnce({ success: true, operation: "init" });
29
+
30
+ const result = await mockInitGitInProject();
31
+
32
+ expect(result.success).toBe(true);
33
+ expect(result.operation).toBe("init");
34
+ });
35
+
36
+ test("init returns failure for invalid directory", async () => {
37
+ mockInitGitInProject.mockResolvedValueOnce({ success: false, operation: "init" });
38
+
39
+ const result = await mockInitGitInProject();
40
+
41
+ expect(result.success).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe("GitHub repo creation", () => {
46
+ test("creates repo successfully", async () => {
47
+ mockCreateRepo.mockResolvedValueOnce({ success: true, url: "https://github.com/user/repo" });
48
+
49
+ const result = await mockCreateRepo();
50
+
51
+ expect(result.success).toBe(true);
52
+ expect(result.url).toContain("github.com");
53
+ });
54
+
55
+ test("handles creation failure", async () => {
56
+ mockCreateRepo.mockResolvedValueOnce({ success: false, url: "" });
57
+
58
+ const result = await mockCreateRepo();
59
+
60
+ expect(result.success).toBe(false);
61
+ });
62
+ });
63
+
64
+ describe("GitHub repo archiving", () => {
65
+ test("archives repo successfully", async () => {
66
+ mockArchiveRepo.mockResolvedValueOnce({ success: true });
67
+
68
+ const result = await mockArchiveRepo();
69
+
70
+ expect(result.success).toBe(true);
71
+ });
72
+
73
+ test("handles archive failure", async () => {
74
+ mockArchiveRepo.mockResolvedValueOnce({ success: false });
75
+
76
+ const result = await mockArchiveRepo();
77
+
78
+ expect(result.success).toBe(false);
79
+ });
80
+ });
81
+ });