gitforest 0.1.0 → 1.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/LICENSE +21 -0
- package/package.json +24 -4
- package/src/components/onboarding/DirectoriesStep.tsx +19 -19
- package/src/github/auth.ts +3 -3
- package/src/utils/debug.ts +4 -4
- package/.bunignore +0 -7
- package/.github/workflows/ci.yml +0 -73
- package/CLAUDE.md +0 -111
- package/CONTRIBUTING.md +0 -145
- package/bun.lock +0 -267
- package/bunfig.toml +0 -15
- package/cli +0 -0
- package/docs/ai/IMPROVEMENT_PLAN.md +0 -341
- package/docs/ai/VERIFICATION_REPORT.md +0 -87
- package/docs/ai/architecture.md +0 -169
- package/docs/ai/checks/check-2025-12-02-tests.md +0 -40
- package/docs/ai/checks/check-2025-12-02.md +0 -55
- package/docs/ai/checks/test-verification-report.md +0 -85
- package/docs/ai/implementation-guide.md +0 -776
- package/docs/ai/research/gitty-codebase-analysis.md +0 -221
- package/docs/ai/tickets/GENERAL-sitrep.md +0 -30
- package/docs/ai/tickets/TASK-database-tests-sitrep.md +0 -25
- package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +0 -28
- package/docs/ai/tickets/TASK-detail-modal-sitrep.md +0 -28
- package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +0 -24
- package/docs/ai/tickets/TASK-github-service-sitrep.md +0 -32
- package/docs/ai/tickets/TASK-github-token-sitrep.md +0 -51
- package/docs/ai/tickets/TASK-hascommits-sitrep.md +0 -35
- package/docs/ai/tickets/TASK-keybindings-sitrep.md +0 -26
- package/docs/ai/tickets/TASK-layout-sitrep.md +0 -25
- package/docs/ai/tickets/TASK-markdown-sitrep.md +0 -28
- package/docs/ai/tickets/TASK-project-item-sitrep.md +0 -79
- package/docs/ai/tickets/TASK-sitrep.md +0 -28
- package/docs/ai/tickets/TASK-state-sitrep.md +0 -26
- package/docs/ai/tickets/TASK-types-sitrep.md +0 -25
- package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +0 -26
- package/docs/ai/tickets/TKT-001-sitrep.md +0 -24
- package/docs/ai/tickets/TKT-002-sitrep.md +0 -25
- package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +0 -46
- package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +0 -135
- package/docs/ai/tickets/TKT-003-sitrep.md +0 -26
- package/docs/ai/tickets/TKT-004-sitrep.md +0 -27
- package/docs/ai/tickets/TKT-005-sitrep.md +0 -25
- package/docs/ai/tickets/TKT-006-sitrep.md +0 -26
- package/docs/ai/tickets/TKT-007-sitrep.md +0 -30
- package/docs/ai/tickets/TKT-008-sitrep.md +0 -32
- package/docs/ai/tickets/TKT-009-sitrep.md +0 -27
- package/docs/ai/tickets/TKT-010-sitrep.md +0 -27
- package/docs/ai/tickets/TKT-011-sitrep.md +0 -26
- package/docs/ai/tickets/TKT-012-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +0 -28
- package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +0 -24
- package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +0 -29
- package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +0 -29
- package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +0 -26
- package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +0 -30
- package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +0 -28
- package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +0 -26
- package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +0 -30
- package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +0 -29
- package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +0 -95
- package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +0 -61
- package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +0 -30
- package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +0 -27
- package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +0 -27
- package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +0 -28
- package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +0 -26
- package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +0 -26
- package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +0 -24
- package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +0 -29
- package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +0 -24
- package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +0 -24
- package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +0 -27
- package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +0 -27
- package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +0 -32
- package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +0 -27
- package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +0 -30
- package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +0 -29
- package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +0 -27
- package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +0 -26
- package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +0 -27
- package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +0 -75
- package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +0 -29
- package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +0 -29
- package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +0 -25
- package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +0 -32
- package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +0 -64
- package/docs/ai/tkt-001-fix-database-error.md +0 -217
- package/docs/ai/ui-enhancement-plan.md +0 -562
- package/test/integration/app.isolated.tsx +0 -240
- package/test/integration/cli-commands.test.ts +0 -287
- package/test/integration/cli-validation.test.ts +0 -264
- package/test/integration/git-operations.test.ts +0 -218
- package/test/integration/scanner.test.ts +0 -228
- package/test/preload.ts +0 -18
- package/test/unit/cli/commands.test.ts +0 -13
- package/test/unit/cli/formatters.test.ts +0 -1116
- package/test/unit/cli/github-commands.test.ts +0 -12
- package/test/unit/components/CloneDialog.test.tsx +0 -240
- package/test/unit/components/ColumnHeader.test.tsx +0 -128
- package/test/unit/components/CommandPalette.test.tsx +0 -355
- package/test/unit/components/ConfirmDialog.test.tsx +0 -111
- package/test/unit/components/ErrorBoundary.test.tsx +0 -139
- package/test/unit/components/FilterBar.test.tsx +0 -43
- package/test/unit/components/FilterOptionsOverlay.test.tsx +0 -197
- package/test/unit/components/HelpOverlay.test.tsx +0 -90
- package/test/unit/components/Layout.test.tsx +0 -328
- package/test/unit/components/MarkdownRenderer.test.tsx +0 -45
- package/test/unit/components/ProgressBar.test.tsx +0 -138
- package/test/unit/components/ProjectItem.test.tsx +0 -182
- package/test/unit/components/ProjectList.test.tsx +0 -311
- package/test/unit/components/RepoDetailModal.test.tsx +0 -445
- package/test/unit/components/StatusBar.test.tsx +0 -112
- package/test/unit/components/UnifiedProjectItem.test.tsx +0 -618
- package/test/unit/components/ViewModeIndicator.test.tsx +0 -137
- package/test/unit/components/test-utils.tsx +0 -63
- package/test/unit/config/loader.test.ts +0 -692
- package/test/unit/db/database.test.ts +0 -978
- package/test/unit/db/index.test.ts +0 -314
- package/test/unit/fixtures/setup.ts +0 -186
- package/test/unit/git/commands-untested.test.ts +0 -205
- package/test/unit/git/commands.test.ts +0 -269
- package/test/unit/git/operations.test.ts +0 -322
- package/test/unit/git/status.test.ts +0 -219
- package/test/unit/github/auth.test.ts +0 -317
- package/test/unit/github/cache.test.ts +0 -1028
- package/test/unit/github/cli.test.ts +0 -135
- package/test/unit/github/unified.test.ts +0 -1201
- package/test/unit/graceful-shutdown.test.ts +0 -83
- package/test/unit/hooks/useBackgroundFetch.test.tsx +0 -239
- package/test/unit/hooks/useConfirmDialogActions.test.tsx +0 -81
- package/test/unit/hooks/useKeyBindings.isolated.ts +0 -715
- package/test/unit/hooks/useProjects.test.tsx +0 -186
- package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +0 -115
- package/test/unit/hooks/useUnifiedRepos.test.tsx +0 -177
- package/test/unit/mocks/config.ts +0 -109
- package/test/unit/mocks/git-service.ts +0 -274
- package/test/unit/mocks/github-service.ts +0 -250
- package/test/unit/mocks/index.ts +0 -72
- package/test/unit/mocks/project.ts +0 -148
- package/test/unit/mocks/state-mocks.ts +0 -187
- package/test/unit/mocks/unified.ts +0 -169
- package/test/unit/operations/batch.test.ts +0 -216
- package/test/unit/operations/commands.test.ts +0 -550
- package/test/unit/scanner/errors.test.ts +0 -297
- package/test/unit/scanner/index.test.ts +0 -1011
- package/test/unit/scanner/markers.test.ts +0 -150
- package/test/unit/scanner/submodules.test.ts +0 -99
- package/test/unit/services/git-errors.test.ts +0 -190
- package/test/unit/services/git.test.ts +0 -442
- package/test/unit/services/github-errors.test.ts +0 -293
- package/test/unit/services/github.test.ts +0 -200
- package/test/unit/state/actions.test.ts +0 -217
- package/test/unit/state/reducer.test.ts +0 -745
- package/test/unit/state/store.test.tsx +0 -711
- package/test/unit/types/commands.test.ts +0 -220
- package/test/unit/types/schema.test.ts +0 -179
- package/test/unit/utils/array.test.ts +0 -73
- package/test/unit/utils/debug.test.ts +0 -23
- package/test/unit/utils/errors.test.ts +0 -295
- package/test/unit/utils/markdown.test.ts +0 -163
- package/test/unit/utils/project-utils.test.ts +0 -756
- package/test/unit/utils/rate-limiter.test.ts +0 -256
- package/test/unit/utils/retry.test.ts +0 -165
- package/test/unit/utils/strip-ansi.ts +0 -13
- package/test/unit/utils/timeout.test.ts +0 -93
- package/tsconfig.json +0 -29
|
@@ -1,978 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for database module
|
|
3
|
-
* Uses in-memory SQLite for testing
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
7
|
-
import { Database } from "bun:sqlite";
|
|
8
|
-
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
9
|
-
import { eq, sql } from "drizzle-orm";
|
|
10
|
-
import * as schema from "../../../src/db/schema.ts";
|
|
11
|
-
|
|
12
|
-
describe("Database", () => {
|
|
13
|
-
let sqliteDb: Database;
|
|
14
|
-
let db: ReturnType<typeof drizzle>;
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
// Use in-memory database for testing
|
|
18
|
-
sqliteDb = new Database(":memory:");
|
|
19
|
-
|
|
20
|
-
// Enable foreign key constraints
|
|
21
|
-
sqliteDb.run("PRAGMA foreign_keys = ON");
|
|
22
|
-
|
|
23
|
-
// Create tables
|
|
24
|
-
sqliteDb.run(`
|
|
25
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
26
|
-
id TEXT PRIMARY KEY,
|
|
27
|
-
name TEXT NOT NULL,
|
|
28
|
-
path TEXT NOT NULL UNIQUE,
|
|
29
|
-
type TEXT NOT NULL,
|
|
30
|
-
project_marker TEXT,
|
|
31
|
-
status_json TEXT,
|
|
32
|
-
submodule_json TEXT,
|
|
33
|
-
last_scanned INTEGER
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
CREATE TABLE IF NOT EXISTS remote_status (
|
|
37
|
-
project_id TEXT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
|
|
38
|
-
last_fetched INTEGER,
|
|
39
|
-
unpulled_commits INTEGER,
|
|
40
|
-
remote_last_activity INTEGER,
|
|
41
|
-
remote_url TEXT
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
CREATE TABLE IF NOT EXISTS github_repos (
|
|
45
|
-
id INTEGER PRIMARY KEY,
|
|
46
|
-
name TEXT NOT NULL,
|
|
47
|
-
full_name TEXT NOT NULL UNIQUE,
|
|
48
|
-
owner TEXT NOT NULL,
|
|
49
|
-
description TEXT,
|
|
50
|
-
html_url TEXT,
|
|
51
|
-
ssh_url TEXT,
|
|
52
|
-
clone_url TEXT,
|
|
53
|
-
is_private INTEGER,
|
|
54
|
-
is_archived INTEGER,
|
|
55
|
-
is_fork INTEGER,
|
|
56
|
-
pushed_at INTEGER,
|
|
57
|
-
updated_at INTEGER,
|
|
58
|
-
default_branch TEXT,
|
|
59
|
-
language TEXT,
|
|
60
|
-
size INTEGER,
|
|
61
|
-
stargazers_count INTEGER,
|
|
62
|
-
forks_count INTEGER,
|
|
63
|
-
open_issues_count INTEGER,
|
|
64
|
-
watchers_count INTEGER,
|
|
65
|
-
topics TEXT,
|
|
66
|
-
license TEXT,
|
|
67
|
-
has_issues INTEGER,
|
|
68
|
-
has_wiki INTEGER,
|
|
69
|
-
has_discussions INTEGER,
|
|
70
|
-
last_fetched INTEGER
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
CREATE TABLE IF NOT EXISTS config_cache (
|
|
74
|
-
key TEXT PRIMARY KEY,
|
|
75
|
-
value TEXT,
|
|
76
|
-
updated_at INTEGER
|
|
77
|
-
);
|
|
78
|
-
`);
|
|
79
|
-
|
|
80
|
-
db = drizzle(sqliteDb, { schema });
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
afterEach(() => {
|
|
84
|
-
sqliteDb.close();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe("projects table", () => {
|
|
88
|
-
// Existing tests...
|
|
89
|
-
test("inserts a project", () => {
|
|
90
|
-
const project = {
|
|
91
|
-
id: "test-123",
|
|
92
|
-
name: "my-project",
|
|
93
|
-
path: "/home/user/my-project",
|
|
94
|
-
type: "git",
|
|
95
|
-
projectMarker: null,
|
|
96
|
-
statusJson: JSON.stringify({ isDirty: false }),
|
|
97
|
-
submoduleJson: null,
|
|
98
|
-
lastScanned: new Date(),
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
db.insert(schema.projects).values(project).run();
|
|
102
|
-
|
|
103
|
-
const result = db.select().from(schema.projects).all();
|
|
104
|
-
expect(result).toHaveLength(1);
|
|
105
|
-
expect(result[0]!.name).toBe("my-project");
|
|
106
|
-
expect(result[0]!.path).toBe("/home/user/my-project");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("enforces unique path constraint", () => {
|
|
110
|
-
const project1 = {
|
|
111
|
-
id: "test-1",
|
|
112
|
-
name: "project-1",
|
|
113
|
-
path: "/home/user/project",
|
|
114
|
-
type: "git",
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const project2 = {
|
|
118
|
-
id: "test-2",
|
|
119
|
-
name: "project-2",
|
|
120
|
-
path: "/home/user/project", // Same path
|
|
121
|
-
type: "git",
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
db.insert(schema.projects).values(project1).run();
|
|
125
|
-
|
|
126
|
-
// Should throw on duplicate path
|
|
127
|
-
expect(() => {
|
|
128
|
-
db.insert(schema.projects).values(project2).run();
|
|
129
|
-
}).toThrow();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("updates a project", () => {
|
|
133
|
-
const project = {
|
|
134
|
-
id: "update-test",
|
|
135
|
-
name: "original-name",
|
|
136
|
-
path: "/home/user/project",
|
|
137
|
-
type: "git",
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
db.insert(schema.projects).values(project).run();
|
|
141
|
-
|
|
142
|
-
db.update(schema.projects)
|
|
143
|
-
.set({ name: "updated-name" })
|
|
144
|
-
.where(eq(schema.projects.id, "update-test"))
|
|
145
|
-
.run();
|
|
146
|
-
|
|
147
|
-
const result = db.select().from(schema.projects).where(eq(schema.projects.id, "update-test")).all();
|
|
148
|
-
expect(result[0]!.name).toBe("updated-name");
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("deletes a project", () => {
|
|
152
|
-
const project = {
|
|
153
|
-
id: "delete-test",
|
|
154
|
-
name: "to-delete",
|
|
155
|
-
path: "/home/user/to-delete",
|
|
156
|
-
type: "git",
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
db.insert(schema.projects).values(project).run();
|
|
160
|
-
|
|
161
|
-
// Verify it exists
|
|
162
|
-
let result = db.select().from(schema.projects).all();
|
|
163
|
-
expect(result).toHaveLength(1);
|
|
164
|
-
|
|
165
|
-
// Delete it
|
|
166
|
-
db.delete(schema.projects).where(eq(schema.projects.id, "delete-test")).run();
|
|
167
|
-
|
|
168
|
-
// Verify it's gone
|
|
169
|
-
result = db.select().from(schema.projects).all();
|
|
170
|
-
expect(result).toHaveLength(0);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("stores different project types", () => {
|
|
174
|
-
const projects = [
|
|
175
|
-
{ id: "git-1", name: "git-project", path: "/path/git", type: "git" },
|
|
176
|
-
{ id: "sub-1", name: "submodule", path: "/path/sub", type: "git-submodule" },
|
|
177
|
-
{ id: "non-1", name: "npm-project", path: "/path/npm", type: "non-git", projectMarker: "package.json" },
|
|
178
|
-
];
|
|
179
|
-
|
|
180
|
-
for (const project of projects) {
|
|
181
|
-
db.insert(schema.projects).values(project).run();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const gitProjects = db.select().from(schema.projects).where(eq(schema.projects.type, "git")).all();
|
|
185
|
-
expect(gitProjects).toHaveLength(1);
|
|
186
|
-
|
|
187
|
-
const nonGitProjects = db.select().from(schema.projects).where(eq(schema.projects.type, "non-git")).all();
|
|
188
|
-
expect(nonGitProjects).toHaveLength(1);
|
|
189
|
-
expect(nonGitProjects[0]!.projectMarker).toBe("package.json");
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("stores and retrieves status JSON", () => {
|
|
193
|
-
const status = {
|
|
194
|
-
isDirty: true,
|
|
195
|
-
hasUnstagedChanges: true,
|
|
196
|
-
modifiedCount: 5,
|
|
197
|
-
currentBranch: "main",
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const project = {
|
|
201
|
-
id: "status-test",
|
|
202
|
-
name: "status-project",
|
|
203
|
-
path: "/path/status",
|
|
204
|
-
type: "git",
|
|
205
|
-
statusJson: JSON.stringify(status),
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
db.insert(schema.projects).values(project).run();
|
|
209
|
-
|
|
210
|
-
const result = db.select().from(schema.projects).where(eq(schema.projects.id, "status-test")).all();
|
|
211
|
-
const retrievedStatus = JSON.parse(result[0]!.statusJson!);
|
|
212
|
-
|
|
213
|
-
expect(retrievedStatus.isDirty).toBe(true);
|
|
214
|
-
expect(retrievedStatus.modifiedCount).toBe(5);
|
|
215
|
-
expect(retrievedStatus.currentBranch).toBe("main");
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// New tests for project cache functionality
|
|
219
|
-
describe("project cache operations", () => {
|
|
220
|
-
test("saves projects to cache", () => {
|
|
221
|
-
const projects = [
|
|
222
|
-
{ id: "cache-1", name: "cached-1", path: "/cache/1", type: "git" },
|
|
223
|
-
{ id: "cache-2", name: "cached-2", path: "/cache/2", type: "git" },
|
|
224
|
-
];
|
|
225
|
-
|
|
226
|
-
// Save multiple projects
|
|
227
|
-
for (const project of projects) {
|
|
228
|
-
db.insert(schema.projects).values(project).run();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const allProjects = db.select().from(schema.projects).all();
|
|
232
|
-
expect(allProjects).toHaveLength(2);
|
|
233
|
-
expect(allProjects.map(p => p.name)).toEqual(["cached-1", "cached-2"]);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
test("loads projects from cache", () => {
|
|
237
|
-
// Insert test data
|
|
238
|
-
db.insert(schema.projects).values([
|
|
239
|
-
{ id: "load-1", name: "load-test-1", path: "/load/1", type: "git" },
|
|
240
|
-
{ id: "load-2", name: "load-test-2", path: "/load/2", type: "non-git" },
|
|
241
|
-
]).run();
|
|
242
|
-
|
|
243
|
-
// Load all projects
|
|
244
|
-
const allProjects = db.select().from(schema.projects).all();
|
|
245
|
-
expect(allProjects).toHaveLength(2);
|
|
246
|
-
|
|
247
|
-
// Load specific project by ID
|
|
248
|
-
const projectById = db.select().from(schema.projects)
|
|
249
|
-
.where(eq(schema.projects.id, "load-1"))
|
|
250
|
-
.all();
|
|
251
|
-
expect(projectById).toHaveLength(1);
|
|
252
|
-
expect(projectById[0]!.name).toBe("load-test-1");
|
|
253
|
-
|
|
254
|
-
// Load by type
|
|
255
|
-
const gitProjects = db.select().from(schema.projects)
|
|
256
|
-
.where(eq(schema.projects.type, "git"))
|
|
257
|
-
.all();
|
|
258
|
-
expect(gitProjects).toHaveLength(1);
|
|
259
|
-
expect(gitProjects[0]!.id).toBe("load-1");
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
test("updates existing projects (upsert)", () => {
|
|
263
|
-
const initialProject = {
|
|
264
|
-
id: "upsert-test",
|
|
265
|
-
name: "initial-name",
|
|
266
|
-
path: "/upsert/test",
|
|
267
|
-
type: "git",
|
|
268
|
-
lastScanned: new Date("2023-01-01"),
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
// Insert initial project
|
|
272
|
-
db.insert(schema.projects).values(initialProject).run();
|
|
273
|
-
|
|
274
|
-
// Update with upsert pattern
|
|
275
|
-
const updatedProject = {
|
|
276
|
-
id: "upsert-test",
|
|
277
|
-
name: "updated-name",
|
|
278
|
-
path: "/upsert/test",
|
|
279
|
-
type: "git",
|
|
280
|
-
lastScanned: new Date("2023-12-31"),
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
// SQLite doesn't have native upsert, so we delete then insert
|
|
284
|
-
db.delete(schema.projects).where(eq(schema.projects.id, "upsert-test")).run();
|
|
285
|
-
db.insert(schema.projects).values(updatedProject).run();
|
|
286
|
-
|
|
287
|
-
const result = db.select().from(schema.projects)
|
|
288
|
-
.where(eq(schema.projects.id, "upsert-test"))
|
|
289
|
-
.all();
|
|
290
|
-
|
|
291
|
-
expect(result).toHaveLength(1);
|
|
292
|
-
expect(result[0]!.name).toBe("updated-name");
|
|
293
|
-
expect(result[0]!.lastScanned).toEqual(updatedProject.lastScanned);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
test("cache TTL expiration logic", () => {
|
|
297
|
-
// Insert projects with different lastScanned times
|
|
298
|
-
const now = new Date();
|
|
299
|
-
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
|
300
|
-
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
|
|
301
|
-
|
|
302
|
-
db.insert(schema.projects).values([
|
|
303
|
-
{ id: "fresh", name: "fresh", path: "/fresh", type: "git", lastScanned: now },
|
|
304
|
-
{ id: "hour-old", name: "hour-old", path: "/hour", type: "git", lastScanned: oneHourAgo },
|
|
305
|
-
{ id: "expired", name: "expired", path: "/expired", type: "git", lastScanned: twoDaysAgo },
|
|
306
|
-
]).run();
|
|
307
|
-
|
|
308
|
-
// Get all projects and filter manually (simulating TTL check)
|
|
309
|
-
const allProjects = db.select().from(schema.projects).all();
|
|
310
|
-
const oneDayAgo = now.getTime() - (24 * 60 * 60 * 1000);
|
|
311
|
-
const expiredProjects = allProjects.filter(p => {
|
|
312
|
-
if (!p.lastScanned) return false;
|
|
313
|
-
return new Date(p.lastScanned).getTime() < oneDayAgo;
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
expect(expiredProjects).toHaveLength(1);
|
|
317
|
-
expect(expiredProjects[0]!.id).toBe("expired");
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
describe("remote_status table", () => {
|
|
323
|
-
test("inserts remote status for project", () => {
|
|
324
|
-
// First create a project
|
|
325
|
-
db.insert(schema.projects).values({
|
|
326
|
-
id: "project-with-remote",
|
|
327
|
-
name: "remote-project",
|
|
328
|
-
path: "/path/remote",
|
|
329
|
-
type: "git",
|
|
330
|
-
}).run();
|
|
331
|
-
|
|
332
|
-
// Then add remote status
|
|
333
|
-
db.insert(schema.remoteStatus).values({
|
|
334
|
-
projectId: "project-with-remote",
|
|
335
|
-
lastFetched: new Date(),
|
|
336
|
-
unpulledCommits: 3,
|
|
337
|
-
remoteUrl: "git@github.com:user/repo.git",
|
|
338
|
-
}).run();
|
|
339
|
-
|
|
340
|
-
const result = db.select().from(schema.remoteStatus).all();
|
|
341
|
-
expect(result).toHaveLength(1);
|
|
342
|
-
expect(result[0]!.unpulledCommits).toBe(3);
|
|
343
|
-
expect(result[0]!.remoteUrl).toBe("git@github.com:user/repo.git");
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
test("updates remote status", () => {
|
|
347
|
-
db.insert(schema.projects).values({
|
|
348
|
-
id: "update-remote",
|
|
349
|
-
name: "project",
|
|
350
|
-
path: "/path/update-remote",
|
|
351
|
-
type: "git",
|
|
352
|
-
}).run();
|
|
353
|
-
|
|
354
|
-
db.insert(schema.remoteStatus).values({
|
|
355
|
-
projectId: "update-remote",
|
|
356
|
-
unpulledCommits: 1,
|
|
357
|
-
}).run();
|
|
358
|
-
|
|
359
|
-
db.update(schema.remoteStatus)
|
|
360
|
-
.set({ unpulledCommits: 5 })
|
|
361
|
-
.where(eq(schema.remoteStatus.projectId, "update-remote"))
|
|
362
|
-
.run();
|
|
363
|
-
|
|
364
|
-
const result = db.select().from(schema.remoteStatus).where(eq(schema.remoteStatus.projectId, "update-remote")).all();
|
|
365
|
-
expect(result[0]!.unpulledCommits).toBe(5);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
// New tests for remote status
|
|
369
|
-
describe("remote status operations", () => {
|
|
370
|
-
test("saves remote status", () => {
|
|
371
|
-
// Create project first
|
|
372
|
-
db.insert(schema.projects).values({
|
|
373
|
-
id: "save-remote",
|
|
374
|
-
name: "save-remote-project",
|
|
375
|
-
path: "/save/remote",
|
|
376
|
-
type: "git",
|
|
377
|
-
}).run();
|
|
378
|
-
|
|
379
|
-
const remoteStatus = {
|
|
380
|
-
projectId: "save-remote",
|
|
381
|
-
lastFetched: new Date(),
|
|
382
|
-
unpulledCommits: 5,
|
|
383
|
-
remoteLastActivity: new Date(Date.now() - 3600000), // 1 hour ago
|
|
384
|
-
remoteUrl: "https://github.com/test/repo.git",
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
db.insert(schema.remoteStatus).values(remoteStatus).run();
|
|
388
|
-
|
|
389
|
-
const saved = db.select().from(schema.remoteStatus)
|
|
390
|
-
.where(eq(schema.remoteStatus.projectId, "save-remote"))
|
|
391
|
-
.all();
|
|
392
|
-
|
|
393
|
-
expect(saved).toHaveLength(1);
|
|
394
|
-
expect(saved[0]!.unpulledCommits).toBe(5);
|
|
395
|
-
expect(saved[0]!.remoteUrl).toBe("https://github.com/test/repo.git");
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
test("loads remote status", () => {
|
|
399
|
-
// Create project and remote status
|
|
400
|
-
db.insert(schema.projects).values({
|
|
401
|
-
id: "load-remote",
|
|
402
|
-
name: "load-remote-project",
|
|
403
|
-
path: "/load/remote",
|
|
404
|
-
type: "git",
|
|
405
|
-
}).run();
|
|
406
|
-
|
|
407
|
-
db.insert(schema.remoteStatus).values({
|
|
408
|
-
projectId: "load-remote",
|
|
409
|
-
unpulledCommits: 10,
|
|
410
|
-
remoteUrl: "git@github.com:user/repo.git",
|
|
411
|
-
}).run();
|
|
412
|
-
|
|
413
|
-
// Load by project ID
|
|
414
|
-
const result = db.select().from(schema.remoteStatus)
|
|
415
|
-
.where(eq(schema.remoteStatus.projectId, "load-remote"))
|
|
416
|
-
.all();
|
|
417
|
-
|
|
418
|
-
expect(result).toHaveLength(1);
|
|
419
|
-
expect(result[0]!.unpulledCommits).toBe(10);
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
test("cascade delete when project deleted", () => {
|
|
423
|
-
// Create project
|
|
424
|
-
db.insert(schema.projects).values({
|
|
425
|
-
id: "cascade-test",
|
|
426
|
-
name: "cascade-project",
|
|
427
|
-
path: "/cascade/test",
|
|
428
|
-
type: "git",
|
|
429
|
-
}).run();
|
|
430
|
-
|
|
431
|
-
// Create remote status
|
|
432
|
-
db.insert(schema.remoteStatus).values({
|
|
433
|
-
projectId: "cascade-test",
|
|
434
|
-
unpulledCommits: 2,
|
|
435
|
-
}).run();
|
|
436
|
-
|
|
437
|
-
// Verify both exist
|
|
438
|
-
expect(db.select().from(schema.projects).all()).toHaveLength(1);
|
|
439
|
-
expect(db.select().from(schema.remoteStatus).all()).toHaveLength(1);
|
|
440
|
-
|
|
441
|
-
// Delete project
|
|
442
|
-
db.delete(schema.projects).where(eq(schema.projects.id, "cascade-test")).run();
|
|
443
|
-
|
|
444
|
-
// Verify remote status was also deleted (cascade)
|
|
445
|
-
expect(db.select().from(schema.projects).all()).toHaveLength(0);
|
|
446
|
-
expect(db.select().from(schema.remoteStatus).all()).toHaveLength(0);
|
|
447
|
-
});
|
|
448
|
-
});
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
describe("config_cache table", () => {
|
|
452
|
-
test("stores config values", () => {
|
|
453
|
-
db.insert(schema.configCache).values({
|
|
454
|
-
key: "last_config_hash",
|
|
455
|
-
value: "abc123",
|
|
456
|
-
updatedAt: new Date(),
|
|
457
|
-
}).run();
|
|
458
|
-
|
|
459
|
-
const result = db.select().from(schema.configCache).where(eq(schema.configCache.key, "last_config_hash")).all();
|
|
460
|
-
expect(result).toHaveLength(1);
|
|
461
|
-
expect(result[0]!.value).toBe("abc123");
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
test("updates config values with upsert pattern", () => {
|
|
465
|
-
const key = "test_key";
|
|
466
|
-
|
|
467
|
-
// Insert initial value
|
|
468
|
-
db.insert(schema.configCache).values({
|
|
469
|
-
key,
|
|
470
|
-
value: "initial",
|
|
471
|
-
updatedAt: new Date(),
|
|
472
|
-
}).run();
|
|
473
|
-
|
|
474
|
-
// Update with new value
|
|
475
|
-
db.update(schema.configCache)
|
|
476
|
-
.set({ value: "updated", updatedAt: new Date() })
|
|
477
|
-
.where(eq(schema.configCache.key, key))
|
|
478
|
-
.run();
|
|
479
|
-
|
|
480
|
-
const result = db.select().from(schema.configCache).where(eq(schema.configCache.key, key)).all();
|
|
481
|
-
expect(result[0]!.value).toBe("updated");
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
// New tests for config cache
|
|
485
|
-
describe("config cache operations", () => {
|
|
486
|
-
test("saves config values", () => {
|
|
487
|
-
const configValues = [
|
|
488
|
-
{ key: "github_token", value: "ghp_123456", updatedAt: new Date() },
|
|
489
|
-
{ key: "theme", value: "dark", updatedAt: new Date() },
|
|
490
|
-
{ key: "auto_refresh", value: "true", updatedAt: new Date() },
|
|
491
|
-
];
|
|
492
|
-
|
|
493
|
-
for (const config of configValues) {
|
|
494
|
-
db.insert(schema.configCache).values(config).run();
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const allConfigs = db.select().from(schema.configCache).all();
|
|
498
|
-
expect(allConfigs).toHaveLength(3);
|
|
499
|
-
expect(allConfigs.map(c => c.key).sort()).toEqual(["auto_refresh", "github_token", "theme"]);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
test("loads config values", () => {
|
|
503
|
-
// Insert test configs
|
|
504
|
-
db.insert(schema.configCache).values([
|
|
505
|
-
{ key: "load_test_1", value: "value1", updatedAt: new Date("2023-01-01") },
|
|
506
|
-
{ key: "load_test_2", value: "value2", updatedAt: new Date("2023-12-31") },
|
|
507
|
-
]).run();
|
|
508
|
-
|
|
509
|
-
// Load single config
|
|
510
|
-
const config1 = db.select().from(schema.configCache)
|
|
511
|
-
.where(eq(schema.configCache.key, "load_test_1"))
|
|
512
|
-
.all();
|
|
513
|
-
expect(config1).toHaveLength(1);
|
|
514
|
-
expect(config1[0]!.value).toBe("value1");
|
|
515
|
-
|
|
516
|
-
// Load all configs
|
|
517
|
-
const allConfigs = db.select().from(schema.configCache).all();
|
|
518
|
-
expect(allConfigs).toHaveLength(2);
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
test("updates config values", () => {
|
|
522
|
-
const key = "update_test";
|
|
523
|
-
const initialDate = new Date("2023-01-01");
|
|
524
|
-
const updatedDate = new Date("2023-12-31");
|
|
525
|
-
|
|
526
|
-
// Initial insert
|
|
527
|
-
db.insert(schema.configCache).values({
|
|
528
|
-
key,
|
|
529
|
-
value: "initial_value",
|
|
530
|
-
updatedAt: initialDate,
|
|
531
|
-
}).run();
|
|
532
|
-
|
|
533
|
-
// Update
|
|
534
|
-
db.update(schema.configCache)
|
|
535
|
-
.set({
|
|
536
|
-
value: "updated_value",
|
|
537
|
-
updatedAt: updatedDate,
|
|
538
|
-
})
|
|
539
|
-
.where(eq(schema.configCache.key, key))
|
|
540
|
-
.run();
|
|
541
|
-
|
|
542
|
-
const result = db.select().from(schema.configCache)
|
|
543
|
-
.where(eq(schema.configCache.key, key))
|
|
544
|
-
.all();
|
|
545
|
-
|
|
546
|
-
expect(result).toHaveLength(1);
|
|
547
|
-
expect(result[0]!.value).toBe("updated_value");
|
|
548
|
-
expect(new Date(result[0]!.updatedAt!)).toEqual(updatedDate);
|
|
549
|
-
});
|
|
550
|
-
});
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
describe("github_repos table", () => {
|
|
554
|
-
describe("GitHub repos cache operations", () => {
|
|
555
|
-
test("saves github repos", () => {
|
|
556
|
-
const repos = [
|
|
557
|
-
{
|
|
558
|
-
id: 1,
|
|
559
|
-
name: "repo1",
|
|
560
|
-
fullName: "user1/repo1",
|
|
561
|
-
owner: "user1",
|
|
562
|
-
description: "Test repo 1",
|
|
563
|
-
htmlUrl: "https://github.com/user1/repo1",
|
|
564
|
-
isPrivate: false,
|
|
565
|
-
isArchived: false,
|
|
566
|
-
isFork: false,
|
|
567
|
-
pushedAt: new Date("2023-12-01"),
|
|
568
|
-
updatedAt: new Date("2023-12-15"),
|
|
569
|
-
defaultBranch: "main",
|
|
570
|
-
language: "TypeScript",
|
|
571
|
-
size: 1024,
|
|
572
|
-
stargazersCount: 100,
|
|
573
|
-
forksCount: 20,
|
|
574
|
-
openIssuesCount: 5,
|
|
575
|
-
watchersCount: 50,
|
|
576
|
-
topics: JSON.stringify(["typescript", "react"]),
|
|
577
|
-
license: "MIT",
|
|
578
|
-
hasIssues: true,
|
|
579
|
-
hasWiki: true,
|
|
580
|
-
hasDiscussions: false,
|
|
581
|
-
lastFetched: new Date(),
|
|
582
|
-
},
|
|
583
|
-
{
|
|
584
|
-
id: 2,
|
|
585
|
-
name: "repo2",
|
|
586
|
-
fullName: "user2/repo2",
|
|
587
|
-
owner: "user2",
|
|
588
|
-
description: "Test repo 2",
|
|
589
|
-
htmlUrl: "https://github.com/user2/repo2",
|
|
590
|
-
isPrivate: true,
|
|
591
|
-
isArchived: false,
|
|
592
|
-
isFork: true,
|
|
593
|
-
pushedAt: new Date("2023-11-15"),
|
|
594
|
-
updatedAt: new Date("2023-11-20"),
|
|
595
|
-
defaultBranch: "master",
|
|
596
|
-
language: "JavaScript",
|
|
597
|
-
size: 2048,
|
|
598
|
-
stargazersCount: 50,
|
|
599
|
-
forksCount: 10,
|
|
600
|
-
openIssuesCount: 3,
|
|
601
|
-
watchersCount: 25,
|
|
602
|
-
topics: JSON.stringify(["javascript", "node"]),
|
|
603
|
-
license: "Apache-2.0",
|
|
604
|
-
hasIssues: true,
|
|
605
|
-
hasWiki: false,
|
|
606
|
-
hasDiscussions: true,
|
|
607
|
-
lastFetched: new Date(),
|
|
608
|
-
},
|
|
609
|
-
];
|
|
610
|
-
|
|
611
|
-
for (const repo of repos) {
|
|
612
|
-
db.insert(schema.githubRepos).values(repo).run();
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const allRepos = db.select().from(schema.githubRepos).all();
|
|
616
|
-
expect(allRepos).toHaveLength(2);
|
|
617
|
-
expect(allRepos[0]!.fullName).toBe("user1/repo1");
|
|
618
|
-
expect(allRepos[1]!.fullName).toBe("user2/repo2");
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
test("loads github repos", () => {
|
|
622
|
-
// Insert test repos
|
|
623
|
-
db.insert(schema.githubRepos).values([
|
|
624
|
-
{
|
|
625
|
-
id: 101,
|
|
626
|
-
name: "load-test",
|
|
627
|
-
fullName: "testuser/load-test",
|
|
628
|
-
owner: "testuser",
|
|
629
|
-
description: "Load test repo",
|
|
630
|
-
lastFetched: new Date(),
|
|
631
|
-
},
|
|
632
|
-
{
|
|
633
|
-
id: 102,
|
|
634
|
-
name: "another-test",
|
|
635
|
-
fullName: "anotheruser/another-test",
|
|
636
|
-
owner: "anotheruser",
|
|
637
|
-
description: "Another test repo",
|
|
638
|
-
lastFetched: new Date(),
|
|
639
|
-
},
|
|
640
|
-
]).run();
|
|
641
|
-
|
|
642
|
-
// Load all repos
|
|
643
|
-
const allRepos = db.select().from(schema.githubRepos).all();
|
|
644
|
-
expect(allRepos).toHaveLength(2);
|
|
645
|
-
|
|
646
|
-
// Load by ID
|
|
647
|
-
const repoById = db.select().from(schema.githubRepos)
|
|
648
|
-
.where(eq(schema.githubRepos.id, 101))
|
|
649
|
-
.all();
|
|
650
|
-
expect(repoById).toHaveLength(1);
|
|
651
|
-
expect(repoById[0]!.name).toBe("load-test");
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
test("filters by owner", () => {
|
|
655
|
-
// Insert repos with different owners
|
|
656
|
-
db.insert(schema.githubRepos).values([
|
|
657
|
-
{
|
|
658
|
-
id: 201,
|
|
659
|
-
name: "repo1",
|
|
660
|
-
fullName: "owner1/repo1",
|
|
661
|
-
owner: "owner1",
|
|
662
|
-
lastFetched: new Date(),
|
|
663
|
-
},
|
|
664
|
-
{
|
|
665
|
-
id: 202,
|
|
666
|
-
name: "repo2",
|
|
667
|
-
fullName: "owner1/repo2",
|
|
668
|
-
owner: "owner1",
|
|
669
|
-
lastFetched: new Date(),
|
|
670
|
-
},
|
|
671
|
-
{
|
|
672
|
-
id: 203,
|
|
673
|
-
name: "repo3",
|
|
674
|
-
fullName: "owner2/repo3",
|
|
675
|
-
owner: "owner2",
|
|
676
|
-
lastFetched: new Date(),
|
|
677
|
-
},
|
|
678
|
-
]).run();
|
|
679
|
-
|
|
680
|
-
// Filter by owner1
|
|
681
|
-
const owner1Repos = db.select().from(schema.githubRepos)
|
|
682
|
-
.where(eq(schema.githubRepos.owner, "owner1"))
|
|
683
|
-
.all();
|
|
684
|
-
|
|
685
|
-
expect(owner1Repos).toHaveLength(2);
|
|
686
|
-
expect(owner1Repos.every(r => r.owner === "owner1")).toBe(true);
|
|
687
|
-
|
|
688
|
-
// Filter by owner2
|
|
689
|
-
const owner2Repos = db.select().from(schema.githubRepos)
|
|
690
|
-
.where(eq(schema.githubRepos.owner, "owner2"))
|
|
691
|
-
.all();
|
|
692
|
-
|
|
693
|
-
expect(owner2Repos).toHaveLength(1);
|
|
694
|
-
expect(owner2Repos[0]!.name).toBe("repo3");
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
test("last_fetched timestamp", () => {
|
|
698
|
-
// Insert repos with different fetch times
|
|
699
|
-
const now = new Date();
|
|
700
|
-
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
|
701
|
-
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
702
|
-
|
|
703
|
-
db.insert(schema.githubRepos).values([
|
|
704
|
-
{
|
|
705
|
-
id: 301,
|
|
706
|
-
name: "fresh",
|
|
707
|
-
fullName: "test/fresh",
|
|
708
|
-
owner: "test",
|
|
709
|
-
lastFetched: now,
|
|
710
|
-
},
|
|
711
|
-
{
|
|
712
|
-
id: 302,
|
|
713
|
-
name: "hour-old",
|
|
714
|
-
fullName: "test/hour-old",
|
|
715
|
-
owner: "test",
|
|
716
|
-
lastFetched: oneHourAgo,
|
|
717
|
-
},
|
|
718
|
-
{
|
|
719
|
-
id: 303,
|
|
720
|
-
name: "stale",
|
|
721
|
-
fullName: "test/stale",
|
|
722
|
-
owner: "test",
|
|
723
|
-
lastFetched: yesterday,
|
|
724
|
-
},
|
|
725
|
-
]).run();
|
|
726
|
-
|
|
727
|
-
// Get all repos and filter manually (simulating stale check)
|
|
728
|
-
const allRepos = db.select().from(schema.githubRepos).all();
|
|
729
|
-
const twoHoursAgo = now.getTime() - (2 * 60 * 60 * 1000);
|
|
730
|
-
const staleRepos = allRepos.filter(r => {
|
|
731
|
-
if (!r.lastFetched) return false;
|
|
732
|
-
return new Date(r.lastFetched).getTime() < twoHoursAgo;
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
expect(staleRepos).toHaveLength(1);
|
|
736
|
-
expect(staleRepos[0]!.name).toBe("stale");
|
|
737
|
-
});
|
|
738
|
-
});
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
describe("database operations", () => {
|
|
742
|
-
test("clears all projects", () => {
|
|
743
|
-
// Insert some projects
|
|
744
|
-
db.insert(schema.projects).values([
|
|
745
|
-
{ id: "p1", name: "project-1", path: "/path/1", type: "git" },
|
|
746
|
-
{ id: "p2", name: "project-2", path: "/path/2", type: "git" },
|
|
747
|
-
{ id: "p3", name: "project-3", path: "/path/3", type: "non-git" },
|
|
748
|
-
]).run();
|
|
749
|
-
|
|
750
|
-
// Verify they exist
|
|
751
|
-
let result = db.select().from(schema.projects).all();
|
|
752
|
-
expect(result).toHaveLength(3);
|
|
753
|
-
|
|
754
|
-
// Clear all
|
|
755
|
-
db.delete(schema.projects).run();
|
|
756
|
-
|
|
757
|
-
// Verify empty
|
|
758
|
-
result = db.select().from(schema.projects).all();
|
|
759
|
-
expect(result).toHaveLength(0);
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
test("filters by type", () => {
|
|
763
|
-
db.insert(schema.projects).values([
|
|
764
|
-
{ id: "g1", name: "git-1", path: "/git/1", type: "git" },
|
|
765
|
-
{ id: "g2", name: "git-2", path: "/git/2", type: "git" },
|
|
766
|
-
{ id: "n1", name: "non-1", path: "/non/1", type: "non-git" },
|
|
767
|
-
{ id: "s1", name: "sub-1", path: "/sub/1", type: "git-submodule" },
|
|
768
|
-
]).run();
|
|
769
|
-
|
|
770
|
-
const gitOnly = db.select()
|
|
771
|
-
.from(schema.projects)
|
|
772
|
-
.where(eq(schema.projects.type, "git"))
|
|
773
|
-
.all();
|
|
774
|
-
|
|
775
|
-
expect(gitOnly).toHaveLength(2);
|
|
776
|
-
expect(gitOnly.every(p => p.type === "git")).toBe(true);
|
|
777
|
-
});
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
describe("error handling", () => {
|
|
781
|
-
test("handles database initialization errors", () => {
|
|
782
|
-
// This would be tested in the actual initDb function
|
|
783
|
-
// For now, we can simulate with a bad query
|
|
784
|
-
expect(() => {
|
|
785
|
-
sqliteDb.run("INVALID SQL");
|
|
786
|
-
}).toThrow();
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
test("handles write errors", () => {
|
|
790
|
-
// Try to insert duplicate primary key
|
|
791
|
-
db.insert(schema.projects).values({
|
|
792
|
-
id: "duplicate",
|
|
793
|
-
name: "project-1",
|
|
794
|
-
path: "/path/1",
|
|
795
|
-
type: "git",
|
|
796
|
-
}).run();
|
|
797
|
-
|
|
798
|
-
expect(() => {
|
|
799
|
-
db.insert(schema.projects).values({
|
|
800
|
-
id: "duplicate", // Same ID
|
|
801
|
-
name: "project-2",
|
|
802
|
-
path: "/path/2",
|
|
803
|
-
type: "git",
|
|
804
|
-
}).run();
|
|
805
|
-
}).toThrow();
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
test("handles read errors on non-existent data", () => {
|
|
809
|
-
// Query non-existent data
|
|
810
|
-
const result = db.select().from(schema.projects)
|
|
811
|
-
.where(eq(schema.projects.id, "non-existent"))
|
|
812
|
-
.all();
|
|
813
|
-
|
|
814
|
-
expect(result).toHaveLength(0);
|
|
815
|
-
});
|
|
816
|
-
|
|
817
|
-
test("handles database cleanup", () => {
|
|
818
|
-
// Insert some data
|
|
819
|
-
db.insert(schema.projects).values({
|
|
820
|
-
id: "cleanup-test",
|
|
821
|
-
name: "cleanup",
|
|
822
|
-
path: "/cleanup",
|
|
823
|
-
type: "git",
|
|
824
|
-
}).run();
|
|
825
|
-
|
|
826
|
-
// Verify data exists
|
|
827
|
-
expect(db.select().from(schema.projects).all()).toHaveLength(1);
|
|
828
|
-
|
|
829
|
-
// Close database (simulating cleanup)
|
|
830
|
-
sqliteDb.close();
|
|
831
|
-
|
|
832
|
-
// Attempting to use closed database should throw
|
|
833
|
-
expect(() => {
|
|
834
|
-
sqliteDb.run("SELECT 1");
|
|
835
|
-
}).toThrow();
|
|
836
|
-
});
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
describe("edge cases", () => {
|
|
840
|
-
test("handles empty database operations", () => {
|
|
841
|
-
// Verify empty database
|
|
842
|
-
expect(db.select().from(schema.projects).all()).toHaveLength(0);
|
|
843
|
-
expect(db.select().from(schema.remoteStatus).all()).toHaveLength(0);
|
|
844
|
-
expect(db.select().from(schema.githubRepos).all()).toHaveLength(0);
|
|
845
|
-
expect(db.select().from(schema.configCache).all()).toHaveLength(0);
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
test("handles large batch inserts", () => {
|
|
849
|
-
const batchSize = 100;
|
|
850
|
-
const projects = Array.from({ length: batchSize }, (_, i) => ({
|
|
851
|
-
id: `batch-${i}`,
|
|
852
|
-
name: `Batch Project ${i}`,
|
|
853
|
-
path: `/batch/${i}`,
|
|
854
|
-
type: i % 3 === 0 ? "git" : i % 3 === 1 ? "non-git" : "git-submodule",
|
|
855
|
-
}));
|
|
856
|
-
|
|
857
|
-
// Insert large batch
|
|
858
|
-
const start = Date.now();
|
|
859
|
-
for (const project of projects) {
|
|
860
|
-
db.insert(schema.projects).values(project).run();
|
|
861
|
-
}
|
|
862
|
-
const duration = Date.now() - start;
|
|
863
|
-
|
|
864
|
-
// Verify all inserted
|
|
865
|
-
const allProjects = db.select().from(schema.projects).all();
|
|
866
|
-
expect(allProjects).toHaveLength(batchSize);
|
|
867
|
-
|
|
868
|
-
// Performance check - should complete reasonably fast
|
|
869
|
-
expect(duration).toBeLessThan(5000); // 5 seconds max
|
|
870
|
-
});
|
|
871
|
-
|
|
872
|
-
test("handles special characters in data", () => {
|
|
873
|
-
const specialProject = {
|
|
874
|
-
id: "special-chars",
|
|
875
|
-
name: "Project with 'quotes' and \"double quotes\"",
|
|
876
|
-
path: "/path/with spaces and 'quotes'",
|
|
877
|
-
type: "git",
|
|
878
|
-
statusJson: JSON.stringify({
|
|
879
|
-
message: "Status with \"quotes\" and 'apostrophes'",
|
|
880
|
-
path: "/path/with/unicode/字符/🔥",
|
|
881
|
-
}),
|
|
882
|
-
};
|
|
883
|
-
|
|
884
|
-
db.insert(schema.projects).values(specialProject).run();
|
|
885
|
-
|
|
886
|
-
const result = db.select().from(schema.projects)
|
|
887
|
-
.where(eq(schema.projects.id, "special-chars"))
|
|
888
|
-
.all();
|
|
889
|
-
|
|
890
|
-
expect(result).toHaveLength(1);
|
|
891
|
-
const retrievedStatus = JSON.parse(result[0]!.statusJson!);
|
|
892
|
-
expect(retrievedStatus.message).toBe("Status with \"quotes\" and 'apostrophes'");
|
|
893
|
-
expect(retrievedStatus.path).toBe("/path/with/unicode/字符/🔥");
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
test("handles concurrent-like access patterns", () => {
|
|
897
|
-
// Simulate rapid inserts and updates
|
|
898
|
-
const operations = Array.from({ length: 50 }, (_, i) => {
|
|
899
|
-
const id = `concurrent-${i}`;
|
|
900
|
-
return {
|
|
901
|
-
id,
|
|
902
|
-
name: `Concurrent ${i}`,
|
|
903
|
-
path: `/concurrent/${i}`,
|
|
904
|
-
type: "git",
|
|
905
|
-
};
|
|
906
|
-
});
|
|
907
|
-
|
|
908
|
-
// Perform all inserts
|
|
909
|
-
for (const op of operations) {
|
|
910
|
-
db.insert(schema.projects).values(op).run();
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// Perform updates
|
|
914
|
-
for (let i = 0; i < 25; i++) {
|
|
915
|
-
db.update(schema.projects)
|
|
916
|
-
.set({ name: `Updated ${i}` })
|
|
917
|
-
.where(eq(schema.projects.id, `concurrent-${i}`))
|
|
918
|
-
.run();
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// Verify results
|
|
922
|
-
const allProjects = db.select().from(schema.projects).all();
|
|
923
|
-
expect(allProjects).toHaveLength(50);
|
|
924
|
-
|
|
925
|
-
const updatedProjects = db.select().from(schema.projects)
|
|
926
|
-
.where(sql`${schema.projects.name} LIKE 'Updated %'`)
|
|
927
|
-
.all();
|
|
928
|
-
expect(updatedProjects).toHaveLength(25);
|
|
929
|
-
});
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
describe("clearCache function", () => {
|
|
933
|
-
test("clears all cached data", () => {
|
|
934
|
-
// Insert data into all tables
|
|
935
|
-
db.insert(schema.projects).values([
|
|
936
|
-
{ id: "clear-1", name: "Clear 1", path: "/clear/1", type: "git" },
|
|
937
|
-
{ id: "clear-2", name: "Clear 2", path: "/clear/2", type: "non-git" },
|
|
938
|
-
]).run();
|
|
939
|
-
|
|
940
|
-
db.insert(schema.remoteStatus).values({
|
|
941
|
-
projectId: "clear-1",
|
|
942
|
-
unpulledCommits: 5,
|
|
943
|
-
}).run();
|
|
944
|
-
|
|
945
|
-
db.insert(schema.githubRepos).values({
|
|
946
|
-
id: 401,
|
|
947
|
-
name: "clear-repo",
|
|
948
|
-
fullName: "clear/clear-repo",
|
|
949
|
-
owner: "clear",
|
|
950
|
-
lastFetched: new Date(),
|
|
951
|
-
}).run();
|
|
952
|
-
|
|
953
|
-
db.insert(schema.configCache).values({
|
|
954
|
-
key: "clear_test",
|
|
955
|
-
value: "test_value",
|
|
956
|
-
updatedAt: new Date(),
|
|
957
|
-
}).run();
|
|
958
|
-
|
|
959
|
-
// Verify data exists
|
|
960
|
-
expect(db.select().from(schema.projects).all()).toHaveLength(2);
|
|
961
|
-
expect(db.select().from(schema.remoteStatus).all()).toHaveLength(1);
|
|
962
|
-
expect(db.select().from(schema.githubRepos).all()).toHaveLength(1);
|
|
963
|
-
expect(db.select().from(schema.configCache).all()).toHaveLength(1);
|
|
964
|
-
|
|
965
|
-
// Clear cache
|
|
966
|
-
db.delete(schema.projects).run();
|
|
967
|
-
db.delete(schema.remoteStatus).run();
|
|
968
|
-
|
|
969
|
-
// Verify projects and remote status are cleared
|
|
970
|
-
expect(db.select().from(schema.projects).all()).toHaveLength(0);
|
|
971
|
-
expect(db.select().from(schema.remoteStatus).all()).toHaveLength(0);
|
|
972
|
-
|
|
973
|
-
// Other tables remain intact
|
|
974
|
-
expect(db.select().from(schema.githubRepos).all()).toHaveLength(1);
|
|
975
|
-
expect(db.select().from(schema.configCache).all()).toHaveLength(1);
|
|
976
|
-
});
|
|
977
|
-
});
|
|
978
|
-
});
|