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.
- package/.bunignore +7 -0
- package/.github/workflows/ci.yml +73 -0
- package/CLAUDE.md +111 -0
- package/CONTRIBUTING.md +145 -0
- package/README.md +168 -0
- package/bun.lock +267 -0
- package/bunfig.toml +15 -0
- package/cli +0 -0
- package/config/gitforest.example.yaml +94 -0
- package/docs/ai/IMPROVEMENT_PLAN.md +341 -0
- package/docs/ai/VERIFICATION_REPORT.md +87 -0
- package/docs/ai/architecture.md +169 -0
- package/docs/ai/checks/check-2025-12-02-tests.md +40 -0
- package/docs/ai/checks/check-2025-12-02.md +55 -0
- package/docs/ai/checks/test-verification-report.md +85 -0
- package/docs/ai/implementation-guide.md +776 -0
- package/docs/ai/research/gitty-codebase-analysis.md +221 -0
- package/docs/ai/tickets/GENERAL-sitrep.md +30 -0
- package/docs/ai/tickets/TASK-database-tests-sitrep.md +25 -0
- package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-detail-modal-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +24 -0
- package/docs/ai/tickets/TASK-github-service-sitrep.md +32 -0
- package/docs/ai/tickets/TASK-github-token-sitrep.md +51 -0
- package/docs/ai/tickets/TASK-hascommits-sitrep.md +35 -0
- package/docs/ai/tickets/TASK-keybindings-sitrep.md +26 -0
- package/docs/ai/tickets/TASK-layout-sitrep.md +25 -0
- package/docs/ai/tickets/TASK-markdown-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-project-item-sitrep.md +79 -0
- package/docs/ai/tickets/TASK-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-state-sitrep.md +26 -0
- package/docs/ai/tickets/TASK-types-sitrep.md +25 -0
- package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-001-sitrep.md +24 -0
- package/docs/ai/tickets/TKT-002-sitrep.md +25 -0
- package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +46 -0
- package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +135 -0
- package/docs/ai/tickets/TKT-003-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-004-sitrep.md +27 -0
- package/docs/ai/tickets/TKT-005-sitrep.md +25 -0
- package/docs/ai/tickets/TKT-006-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-007-sitrep.md +30 -0
- package/docs/ai/tickets/TKT-008-sitrep.md +32 -0
- package/docs/ai/tickets/TKT-009-sitrep.md +27 -0
- package/docs/ai/tickets/TKT-010-sitrep.md +27 -0
- package/docs/ai/tickets/TKT-011-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-012-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +28 -0
- package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +28 -0
- package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +95 -0
- package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +61 -0
- package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +28 -0
- package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +32 -0
- package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +75 -0
- package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +32 -0
- package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +64 -0
- package/docs/ai/tkt-001-fix-database-error.md +217 -0
- package/docs/ai/ui-enhancement-plan.md +562 -0
- package/package.json +50 -0
- package/src/app.tsx +43 -0
- package/src/cli/config.ts +94 -0
- package/src/cli/formatters.ts +632 -0
- package/src/cli/index.ts +583 -0
- package/src/components/CloneDialog.tsx +137 -0
- package/src/components/ColumnHeader.tsx +128 -0
- package/src/components/CommandPalette.tsx +120 -0
- package/src/components/ConfirmDialog.tsx +105 -0
- package/src/components/ErrorBoundary.tsx +128 -0
- package/src/components/FilterBar.tsx +71 -0
- package/src/components/FilterOptionsOverlay.tsx +131 -0
- package/src/components/HelpOverlay.tsx +120 -0
- package/src/components/Layout.tsx +379 -0
- package/src/components/MarkdownRenderer.tsx +127 -0
- package/src/components/ProgressBar.tsx +53 -0
- package/src/components/ProjectItem.tsx +143 -0
- package/src/components/ProjectList.tsx +90 -0
- package/src/components/RepoDetailModal.tsx +367 -0
- package/src/components/StatusBar.tsx +188 -0
- package/src/components/UnifiedProjectItem.tsx +436 -0
- package/src/components/ViewModeIndicator.tsx +37 -0
- package/src/components/onboarding/CompleteStep.tsx +82 -0
- package/src/components/onboarding/DirectoriesStep.test.tsx +52 -0
- package/src/components/onboarding/DirectoriesStep.tsx +847 -0
- package/src/components/onboarding/DirectoriesStep.unit.test.ts +345 -0
- package/src/components/onboarding/GitHubAuthStep.tsx +268 -0
- package/src/components/onboarding/OnboardingWizard.tsx +130 -0
- package/src/components/onboarding/WelcomeStep.tsx +69 -0
- package/src/config/loader.ts +263 -0
- package/src/config/onboarding.ts +67 -0
- package/src/constants.ts +96 -0
- package/src/db/index.ts +147 -0
- package/src/db/schema.ts +70 -0
- package/src/git/commands.ts +283 -0
- package/src/git/index.ts +2 -0
- package/src/git/operations.ts +93 -0
- package/src/git/service.ts +539 -0
- package/src/git/status.ts +84 -0
- package/src/git/types.ts +5 -0
- package/src/github/auth.ts +311 -0
- package/src/github/cache.ts +231 -0
- package/src/github/cli.ts +22 -0
- package/src/github/unified.ts +415 -0
- package/src/hooks/useBackgroundFetch.ts +76 -0
- package/src/hooks/useConfirmDialogActions.ts +120 -0
- package/src/hooks/useKeyBindings.ts +656 -0
- package/src/hooks/useProjects.ts +47 -0
- package/src/hooks/useUnifiedRepos.ts +317 -0
- package/src/index.tsx +494 -0
- package/src/operations/batch.ts +280 -0
- package/src/operations/commands.ts +140 -0
- package/src/operations/index.ts +37 -0
- package/src/scanner/index.ts +424 -0
- package/src/scanner/markers.ts +43 -0
- package/src/scanner/submodules.ts +61 -0
- package/src/services/git.ts +484 -0
- package/src/services/github.ts +676 -0
- package/src/services/index.ts +28 -0
- package/src/services/types.ts +99 -0
- package/src/state/actions.ts +175 -0
- package/src/state/reducer.ts +294 -0
- package/src/state/store.tsx +216 -0
- package/src/state/types.ts +8 -0
- package/src/types/index.ts +383 -0
- package/src/ui/theme.ts +44 -0
- package/src/utils/array.ts +14 -0
- package/src/utils/debug.ts +38 -0
- package/src/utils/errors.ts +17 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/markdown.ts +230 -0
- package/src/utils/project-utils.ts +129 -0
- package/src/utils/rate-limiter.ts +134 -0
- package/src/utils/retry.ts +147 -0
- package/src/utils/timeout.ts +56 -0
- package/test/integration/app.isolated.tsx +240 -0
- package/test/integration/cli-commands.test.ts +287 -0
- package/test/integration/cli-validation.test.ts +264 -0
- package/test/integration/git-operations.test.ts +218 -0
- package/test/integration/scanner.test.ts +228 -0
- package/test/preload.ts +18 -0
- package/test/unit/cli/commands.test.ts +13 -0
- package/test/unit/cli/formatters.test.ts +1116 -0
- package/test/unit/cli/github-commands.test.ts +12 -0
- package/test/unit/components/CloneDialog.test.tsx +240 -0
- package/test/unit/components/ColumnHeader.test.tsx +128 -0
- package/test/unit/components/CommandPalette.test.tsx +355 -0
- package/test/unit/components/ConfirmDialog.test.tsx +111 -0
- package/test/unit/components/ErrorBoundary.test.tsx +139 -0
- package/test/unit/components/FilterBar.test.tsx +43 -0
- package/test/unit/components/FilterOptionsOverlay.test.tsx +197 -0
- package/test/unit/components/HelpOverlay.test.tsx +90 -0
- package/test/unit/components/Layout.test.tsx +328 -0
- package/test/unit/components/MarkdownRenderer.test.tsx +45 -0
- package/test/unit/components/ProgressBar.test.tsx +138 -0
- package/test/unit/components/ProjectItem.test.tsx +182 -0
- package/test/unit/components/ProjectList.test.tsx +311 -0
- package/test/unit/components/RepoDetailModal.test.tsx +445 -0
- package/test/unit/components/StatusBar.test.tsx +112 -0
- package/test/unit/components/UnifiedProjectItem.test.tsx +618 -0
- package/test/unit/components/ViewModeIndicator.test.tsx +137 -0
- package/test/unit/components/test-utils.tsx +63 -0
- package/test/unit/config/loader.test.ts +692 -0
- package/test/unit/db/database.test.ts +978 -0
- package/test/unit/db/index.test.ts +314 -0
- package/test/unit/fixtures/setup.ts +186 -0
- package/test/unit/git/commands-untested.test.ts +205 -0
- package/test/unit/git/commands.test.ts +269 -0
- package/test/unit/git/operations.test.ts +322 -0
- package/test/unit/git/status.test.ts +219 -0
- package/test/unit/github/auth.test.ts +317 -0
- package/test/unit/github/cache.test.ts +1028 -0
- package/test/unit/github/cli.test.ts +135 -0
- package/test/unit/github/unified.test.ts +1201 -0
- package/test/unit/graceful-shutdown.test.ts +83 -0
- package/test/unit/hooks/useBackgroundFetch.test.tsx +239 -0
- package/test/unit/hooks/useConfirmDialogActions.test.tsx +81 -0
- package/test/unit/hooks/useKeyBindings.isolated.ts +715 -0
- package/test/unit/hooks/useProjects.test.tsx +186 -0
- package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +115 -0
- package/test/unit/hooks/useUnifiedRepos.test.tsx +177 -0
- package/test/unit/mocks/config.ts +109 -0
- package/test/unit/mocks/git-service.ts +274 -0
- package/test/unit/mocks/github-service.ts +250 -0
- package/test/unit/mocks/index.ts +72 -0
- package/test/unit/mocks/project.ts +148 -0
- package/test/unit/mocks/state-mocks.ts +187 -0
- package/test/unit/mocks/unified.ts +169 -0
- package/test/unit/operations/batch.test.ts +216 -0
- package/test/unit/operations/commands.test.ts +550 -0
- package/test/unit/scanner/errors.test.ts +297 -0
- package/test/unit/scanner/index.test.ts +1011 -0
- package/test/unit/scanner/markers.test.ts +150 -0
- package/test/unit/scanner/submodules.test.ts +99 -0
- package/test/unit/services/git-errors.test.ts +190 -0
- package/test/unit/services/git.test.ts +442 -0
- package/test/unit/services/github-errors.test.ts +293 -0
- package/test/unit/services/github.test.ts +200 -0
- package/test/unit/state/actions.test.ts +217 -0
- package/test/unit/state/reducer.test.ts +745 -0
- package/test/unit/state/store.test.tsx +711 -0
- package/test/unit/types/commands.test.ts +220 -0
- package/test/unit/types/schema.test.ts +179 -0
- package/test/unit/utils/array.test.ts +73 -0
- package/test/unit/utils/debug.test.ts +23 -0
- package/test/unit/utils/errors.test.ts +295 -0
- package/test/unit/utils/markdown.test.ts +163 -0
- package/test/unit/utils/project-utils.test.ts +756 -0
- package/test/unit/utils/rate-limiter.test.ts +256 -0
- package/test/unit/utils/retry.test.ts +165 -0
- package/test/unit/utils/strip-ansi.ts +13 -0
- package/test/unit/utils/timeout.test.ts +93 -0
- 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
|
+
});
|