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,220 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { CommandConfigSchema, RESERVED_KEYS, GitforestConfigSchema } from "../../../src/types/index.ts";
3
+
4
+ describe("CommandConfigSchema", () => {
5
+ test("validates a valid command config", () => {
6
+ const result = CommandConfigSchema.safeParse({
7
+ name: "Open Editor",
8
+ key: "e",
9
+ command: "code .",
10
+ });
11
+
12
+ expect(result.success).toBe(true);
13
+ if (result.success) {
14
+ expect(result.data.name).toBe("Open Editor");
15
+ expect(result.data.key).toBe("e");
16
+ expect(result.data.command).toBe("code .");
17
+ expect(result.data.confirm).toBe(false);
18
+ expect(result.data.background).toBe(false);
19
+ }
20
+ });
21
+
22
+ test("validates command with optional fields", () => {
23
+ const result = CommandConfigSchema.safeParse({
24
+ name: "Run Build",
25
+ key: "b",
26
+ command: "bun run build",
27
+ confirm: true,
28
+ background: true,
29
+ });
30
+
31
+ expect(result.success).toBe(true);
32
+ if (result.success) {
33
+ expect(result.data.confirm).toBe(true);
34
+ expect(result.data.background).toBe(true);
35
+ }
36
+ });
37
+
38
+ test("rejects empty name", () => {
39
+ const result = CommandConfigSchema.safeParse({
40
+ name: "",
41
+ key: "e",
42
+ command: "code .",
43
+ });
44
+
45
+ expect(result.success).toBe(false);
46
+ });
47
+
48
+ test("rejects empty command", () => {
49
+ const result = CommandConfigSchema.safeParse({
50
+ name: "Test",
51
+ key: "e",
52
+ command: "",
53
+ });
54
+
55
+ expect(result.success).toBe(false);
56
+ });
57
+
58
+ test("rejects multi-character key", () => {
59
+ const result = CommandConfigSchema.safeParse({
60
+ name: "Test",
61
+ key: "ab",
62
+ command: "echo test",
63
+ });
64
+
65
+ expect(result.success).toBe(false);
66
+ });
67
+
68
+ test("rejects reserved key 'j' (navigation)", () => {
69
+ const result = CommandConfigSchema.safeParse({
70
+ name: "Test",
71
+ key: "j",
72
+ command: "echo test",
73
+ });
74
+
75
+ expect(result.success).toBe(false);
76
+ });
77
+
78
+ test("rejects reserved key 'q' (quit)", () => {
79
+ const result = CommandConfigSchema.safeParse({
80
+ name: "Test",
81
+ key: "q",
82
+ command: "echo test",
83
+ });
84
+
85
+ expect(result.success).toBe(false);
86
+ });
87
+
88
+ test("rejects reserved key 'p' (push)", () => {
89
+ const result = CommandConfigSchema.safeParse({
90
+ name: "Test",
91
+ key: "p",
92
+ command: "echo test",
93
+ });
94
+
95
+ expect(result.success).toBe(false);
96
+ });
97
+
98
+ test("rejects reserved key '1' (quick filter)", () => {
99
+ const result = CommandConfigSchema.safeParse({
100
+ name: "Test",
101
+ key: "1",
102
+ command: "echo test",
103
+ });
104
+
105
+ expect(result.success).toBe(false);
106
+ });
107
+
108
+ test("accepts non-reserved key 'e'", () => {
109
+ const result = CommandConfigSchema.safeParse({
110
+ name: "Editor",
111
+ key: "e",
112
+ command: "code .",
113
+ });
114
+
115
+ expect(result.success).toBe(true);
116
+ });
117
+
118
+ test("accepts non-reserved key 't'", () => {
119
+ const result = CommandConfigSchema.safeParse({
120
+ name: "Test",
121
+ key: "t",
122
+ command: "bun test",
123
+ });
124
+
125
+ expect(result.success).toBe(true);
126
+ });
127
+ });
128
+
129
+ describe("RESERVED_KEYS", () => {
130
+ test("contains navigation keys", () => {
131
+ expect(RESERVED_KEYS.has("j")).toBe(true);
132
+ expect(RESERVED_KEYS.has("k")).toBe(true);
133
+ expect(RESERVED_KEYS.has("g")).toBe(true);
134
+ expect(RESERVED_KEYS.has("G")).toBe(true);
135
+ });
136
+
137
+ test("contains selection keys", () => {
138
+ expect(RESERVED_KEYS.has(" ")).toBe(true);
139
+ expect(RESERVED_KEYS.has("a")).toBe(true);
140
+ });
141
+
142
+ test("contains git operation keys", () => {
143
+ expect(RESERVED_KEYS.has("p")).toBe(true);
144
+ expect(RESERVED_KEYS.has("P")).toBe(true);
145
+ expect(RESERVED_KEYS.has("f")).toBe(true);
146
+ expect(RESERVED_KEYS.has("i")).toBe(true);
147
+ });
148
+
149
+ test("contains github operation keys", () => {
150
+ expect(RESERVED_KEYS.has("c")).toBe(true);
151
+ expect(RESERVED_KEYS.has("C")).toBe(true);
152
+ expect(RESERVED_KEYS.has("A")).toBe(true);
153
+ expect(RESERVED_KEYS.has("D")).toBe(true);
154
+ });
155
+
156
+ test("contains quick filter number keys", () => {
157
+ for (let i = 0; i <= 9; i++) {
158
+ expect(RESERVED_KEYS.has(String(i))).toBe(true);
159
+ }
160
+ });
161
+
162
+ test("contains command palette trigger key", () => {
163
+ expect(RESERVED_KEYS.has("x")).toBe(true);
164
+ });
165
+ });
166
+
167
+ describe("GitforestConfigSchema commands", () => {
168
+ const baseConfig = {
169
+ directories: [{ path: "~/projects", maxDepth: 2 }],
170
+ };
171
+
172
+ test("accepts config without commands", () => {
173
+ const result = GitforestConfigSchema.safeParse(baseConfig);
174
+
175
+ expect(result.success).toBe(true);
176
+ if (result.success) {
177
+ expect(result.data.commands).toEqual([]);
178
+ }
179
+ });
180
+
181
+ test("accepts config with valid commands", () => {
182
+ const result = GitforestConfigSchema.safeParse({
183
+ ...baseConfig,
184
+ commands: [
185
+ { name: "Editor", key: "e", command: "code ." },
186
+ { name: "Tests", key: "t", command: "bun test" },
187
+ ],
188
+ });
189
+
190
+ expect(result.success).toBe(true);
191
+ if (result.success) {
192
+ expect(result.data.commands).toHaveLength(2);
193
+ }
194
+ });
195
+
196
+ test("rejects duplicate command keys", () => {
197
+ const result = GitforestConfigSchema.safeParse({
198
+ ...baseConfig,
199
+ commands: [
200
+ { name: "Editor 1", key: "e", command: "code ." },
201
+ { name: "Editor 2", key: "e", command: "vim ." },
202
+ ],
203
+ });
204
+
205
+ expect(result.success).toBe(false);
206
+ });
207
+
208
+ test("accepts different keys for different commands", () => {
209
+ const result = GitforestConfigSchema.safeParse({
210
+ ...baseConfig,
211
+ commands: [
212
+ { name: "Editor", key: "e", command: "code ." },
213
+ { name: "Terminal", key: "T", command: "open -a Terminal ." },
214
+ { name: "Build", key: "b", command: "bun run build" },
215
+ ],
216
+ });
217
+
218
+ expect(result.success).toBe(true);
219
+ });
220
+ });
@@ -0,0 +1,179 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { GitforestConfigSchema, DirectoryConfigSchema } from "../../../src/types/index.ts";
3
+
4
+ describe("DirectoryConfigSchema", () => {
5
+ test("validates valid directory config", () => {
6
+ const result = DirectoryConfigSchema.safeParse({
7
+ path: "~/projects",
8
+ maxDepth: 2,
9
+ label: "Projects",
10
+ });
11
+
12
+ expect(result.success).toBe(true);
13
+ if (result.success) {
14
+ expect(result.data.path).toBe("~/projects");
15
+ expect(result.data.maxDepth).toBe(2);
16
+ expect(result.data.label).toBe("Projects");
17
+ }
18
+ });
19
+
20
+ test("uses default maxDepth when not provided", () => {
21
+ const result = DirectoryConfigSchema.safeParse({
22
+ path: "~/projects",
23
+ });
24
+
25
+ expect(result.success).toBe(true);
26
+ if (result.success) {
27
+ expect(result.data.maxDepth).toBe(2);
28
+ }
29
+ });
30
+
31
+ test("rejects empty path", () => {
32
+ const result = DirectoryConfigSchema.safeParse({
33
+ path: "",
34
+ });
35
+
36
+ expect(result.success).toBe(false);
37
+ });
38
+
39
+ test("label is optional", () => {
40
+ const result = DirectoryConfigSchema.safeParse({
41
+ path: "~/projects",
42
+ });
43
+
44
+ expect(result.success).toBe(true);
45
+ if (result.success) {
46
+ expect(result.data.label).toBeUndefined();
47
+ }
48
+ });
49
+ });
50
+
51
+ describe("GitforestConfigSchema", () => {
52
+ test("validates minimal valid config", () => {
53
+ const result = GitforestConfigSchema.safeParse({
54
+ directories: [{ path: "~/projects" }],
55
+ });
56
+
57
+ expect(result.success).toBe(true);
58
+ });
59
+
60
+ test("applies default values", () => {
61
+ const result = GitforestConfigSchema.safeParse({
62
+ directories: [{ path: "~/projects" }],
63
+ });
64
+
65
+ expect(result.success).toBe(true);
66
+ if (result.success) {
67
+ expect(result.data.scan.ignore).toContain("node_modules");
68
+ expect(result.data.scan.includeHidden).toBe(false);
69
+ expect(result.data.scan.concurrency).toBe(5);
70
+ expect(result.data.github.defaultVisibility).toBe("private");
71
+ expect(result.data.display.showSubmodules).toBe(true);
72
+ expect(result.data.display.sortBy).toBe("status");
73
+ expect(result.data.display.sortDirection).toBe("desc");
74
+ expect(result.data.cache.ttlSeconds).toBe(300);
75
+ }
76
+ });
77
+
78
+ test("validates complete config", () => {
79
+ const result = GitforestConfigSchema.safeParse({
80
+ directories: [
81
+ { path: "~/projects", maxDepth: 3, label: "Projects" },
82
+ { path: "~/work", maxDepth: 2 },
83
+ ],
84
+ scan: {
85
+ ignore: ["node_modules", "vendor"],
86
+ includeHidden: true,
87
+ concurrency: 10,
88
+ },
89
+ github: {
90
+ defaultVisibility: "public",
91
+ },
92
+ display: {
93
+ showSubmodules: false,
94
+ sortBy: "name",
95
+ sortDirection: "asc",
96
+ },
97
+ cache: {
98
+ ttlSeconds: 600,
99
+ githubTtlSeconds: 1200,
100
+ enableBackgroundRefresh: true,
101
+ backgroundRefreshIntervalSeconds: 600,
102
+ },
103
+ });
104
+
105
+ expect(result.success).toBe(true);
106
+ if (result.success) {
107
+ expect(result.data.directories).toHaveLength(2);
108
+ expect(result.data.scan.includeHidden).toBe(true);
109
+ expect(result.data.scan.concurrency).toBe(10);
110
+ expect(result.data.github.defaultVisibility).toBe("public");
111
+ expect(result.data.display.showSubmodules).toBe(false);
112
+ expect(result.data.display.sortBy).toBe("name");
113
+ }
114
+ });
115
+
116
+ test("rejects empty directories array", () => {
117
+ const result = GitforestConfigSchema.safeParse({
118
+ directories: [],
119
+ });
120
+
121
+ expect(result.success).toBe(false);
122
+ });
123
+
124
+ test("rejects invalid sortBy value", () => {
125
+ const result = GitforestConfigSchema.safeParse({
126
+ directories: [{ path: "~/projects" }],
127
+ display: {
128
+ sortBy: "invalid",
129
+ },
130
+ });
131
+
132
+ expect(result.success).toBe(false);
133
+ });
134
+
135
+ test("rejects invalid visibility value", () => {
136
+ const result = GitforestConfigSchema.safeParse({
137
+ directories: [{ path: "~/projects" }],
138
+ github: {
139
+ defaultVisibility: "internal",
140
+ },
141
+ });
142
+
143
+ expect(result.success).toBe(false);
144
+ });
145
+
146
+ test("rejects negative maxDepth", () => {
147
+ const result = GitforestConfigSchema.safeParse({
148
+ directories: [{ path: "~/projects", maxDepth: -1 }],
149
+ });
150
+
151
+ expect(result.success).toBe(false);
152
+ });
153
+
154
+ test("rejects maxDepth greater than 10", () => {
155
+ const result = GitforestConfigSchema.safeParse({
156
+ directories: [{ path: "~/projects", maxDepth: 11 }],
157
+ });
158
+
159
+ expect(result.success).toBe(false);
160
+ });
161
+
162
+ test("rejects concurrency less than 1", () => {
163
+ const result = GitforestConfigSchema.safeParse({
164
+ directories: [{ path: "~/projects" }],
165
+ scan: { concurrency: 0 },
166
+ });
167
+
168
+ expect(result.success).toBe(false);
169
+ });
170
+
171
+ test("rejects concurrency greater than 20", () => {
172
+ const result = GitforestConfigSchema.safeParse({
173
+ directories: [{ path: "~/projects" }],
174
+ scan: { concurrency: 21 },
175
+ });
176
+
177
+ expect(result.success).toBe(false);
178
+ });
179
+ });
@@ -0,0 +1,73 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { chunk } from "../../../src/utils/array";
3
+
4
+ describe("chunk", () => {
5
+ test("splits array into correct chunk sizes", () => {
6
+ const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
7
+ const result = chunk(arr, 3);
8
+ expect(result).toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);
9
+ });
10
+
11
+ test("handles empty array", () => {
12
+ const result = chunk([], 3);
13
+ expect(result).toEqual([]);
14
+ });
15
+
16
+ test("handles array smaller than chunk size", () => {
17
+ const arr = [1, 2];
18
+ const result = chunk(arr, 5);
19
+ expect(result).toEqual([[1, 2]]);
20
+ });
21
+
22
+ test("handles exact multiple of chunk size", () => {
23
+ const arr = [1, 2, 3, 4, 5, 6];
24
+ const result = chunk(arr, 2);
25
+ expect(result).toEqual([[1, 2], [3, 4], [5, 6]]);
26
+ });
27
+
28
+ test("handles chunk size of 1", () => {
29
+ const arr = [1, 2, 3];
30
+ const result = chunk(arr, 1);
31
+ expect(result).toEqual([[1], [2], [3]]);
32
+ });
33
+
34
+ test("handles chunk size larger than array", () => {
35
+ const arr = [1, 2, 3];
36
+ const result = chunk(arr, 10);
37
+ expect(result).toEqual([[1, 2, 3]]);
38
+ });
39
+
40
+ test("handles remainder", () => {
41
+ const arr = [1, 2, 3, 4, 5];
42
+ const result = chunk(arr, 2);
43
+ expect(result).toEqual([[1, 2], [3, 4], [5]]);
44
+ });
45
+
46
+ test("handles string arrays", () => {
47
+ const arr = ["a", "b", "c", "d", "e"];
48
+ const result = chunk(arr, 2);
49
+ expect(result).toEqual([["a", "b"], ["c", "d"], ["e"]]);
50
+ });
51
+
52
+ test("handles object arrays", () => {
53
+ const arr = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
54
+ const result = chunk(arr, 2);
55
+ expect(result).toEqual([[{ id: 1 }, { id: 2 }], [{ id: 3 }, { id: 4 }]]);
56
+ });
57
+
58
+ test("does not modify original array", () => {
59
+ const arr = [1, 2, 3, 4, 5];
60
+ const result = chunk(arr, 2);
61
+ expect(arr).toEqual([1, 2, 3, 4, 5]);
62
+ expect(result).toEqual([[1, 2], [3, 4], [5]]);
63
+ });
64
+
65
+ test("does not modify original array with objects", () => {
66
+ const arr = [{ id: 1 }, { id: 2 }, { id: 3 }];
67
+ const result = chunk(arr, 2);
68
+ // The chunk function uses slice(), which creates a shallow copy
69
+ // So modifying objects in the result will affect the original
70
+ result[0]![0]!.id = 99;
71
+ expect(arr[0]!.id).toBe(99); // Shallow copy means original is modified
72
+ });
73
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, test, expect } from "bun:test";
2
+
3
+ describe("debug utilities (basic tests)", () => {
4
+ test("debug module exports required functions", async () => {
5
+ const debugModule = await import("../../../src/utils/debug");
6
+ expect(typeof debugModule.debug).toBe("function");
7
+ expect(typeof debugModule.debugError).toBe("function");
8
+ expect(typeof debugModule.isDebugEnabled).toBe("function");
9
+ });
10
+
11
+ test("debug functions don't throw when called", async () => {
12
+ const debugModule = await import("../../../src/utils/debug");
13
+ // These should not throw even when debug is disabled
14
+ expect(() => debugModule.debug("test", "message")).not.toThrow();
15
+ expect(() => debugModule.debugError("test", "error", new Error("test"))).not.toThrow();
16
+ });
17
+
18
+ test("isDebugEnabled returns boolean", async () => {
19
+ const debugModule = await import("../../../src/utils/debug");
20
+ const result = debugModule.isDebugEnabled();
21
+ expect(typeof result).toBe("boolean");
22
+ });
23
+ });