Skip to content

Commit c347a60

Browse files
authored
Merge pull request #51 from zackify/copilot/fix-50
Add comprehensive tests for routes and update CI workflow
2 parents c8e5e58 + d0a77e1 commit c347a60

File tree

10 files changed

+646
-117
lines changed

10 files changed

+646
-117
lines changed

******

88 KB
Binary file not shown.

.github/workflows/docker-publish.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ jobs:
2121
- name: Set up Docker Buildx
2222
uses: docker/setup-buildx-action@v3
2323

24+
- name: Setup Bun
25+
uses: oven-sh/setup-bun@v1
26+
with:
27+
bun-version: latest
28+
29+
- name: Install dependencies
30+
run: bun install
31+
32+
- name: Run tests
33+
env:
34+
DATABASE_PATH: ":memory:"
35+
run: bun test
36+
2437
- name: Log in to Docker Hub
2538
uses: docker/login-action@v3
2639
with:

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,8 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
3333

3434
# Finder (MacOS) folder config
3535
.DS_Store
36+
37+
# SQLite database files
38+
*.sqlite
39+
*.sqlite-*
40+
.aider*

src/database/database.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,41 @@
11
import * as sqliteVec from "sqlite-vec";
22
import Database from "bun:sqlite";
33

4-
//macos needs custom path for extension loading
4+
// macos needs custom path for extension loading
55
if (process.env.SQLITE_PATH) {
66
Database.setCustomSQLite(process.env.SQLITE_PATH);
77
}
88

9-
export const db = new Database(process.env.DATABASE_PATH || "./data/db.sqlite");
10-
db.exec("PRAGMA journal_mode = WAL;");
9+
/**
10+
* Get the database instance
11+
* This function always returns the current database,
12+
* checking for a test database first, then falling back to the singleton
13+
*/
14+
export function getDb(): Database {
15+
// For tests, we support injecting a test database instance
16+
const testDb = (globalThis as any).testDb;
17+
if (testDb) {
18+
return testDb;
19+
}
20+
21+
// For normal operation, create a singleton
22+
if (!(globalThis as any)._dbSingleton) {
23+
const dbPath = process.env.DATABASE_PATH || "./data/db.sqlite";
24+
console.log(`Creating database connection to ${dbPath}`);
25+
26+
(globalThis as any)._dbSingleton = new Database(dbPath);
27+
(globalThis as any)._dbSingleton.exec("PRAGMA journal_mode = WAL;");
28+
sqliteVec.load((globalThis as any)._dbSingleton);
29+
}
30+
31+
return (globalThis as any)._dbSingleton;
32+
}
1133

12-
sqliteVec.load(db);
34+
// Create and export a db proxy that always returns the current database instance
35+
// This ensures that even code using the imported db directly will get the test db when appropriate
36+
export const db = new Proxy({} as Database, {
37+
get: function(target, prop) {
38+
const currentDb = getDb();
39+
return currentDb[prop as keyof Database];
40+
}
41+
});

tests/documentRoute.test.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {
2+
expect,
3+
describe,
4+
test,
5+
beforeAll,
6+
beforeEach,
7+
afterAll,
8+
mock,
9+
spyOn,
10+
} from "bun:test";
11+
import Database from "bun:sqlite";
12+
import * as sqliteVec from "sqlite-vec";
13+
import { createDocumentsTableSQL } from "../src/database/migrations";
14+
15+
// Set test environment variables
16+
process.env.DATABASE_PATH = "******"; // In-memory database for tests
17+
process.env.AI_API_KEY = "test-key";
18+
process.env.AI_EMBEDDING_MODEL = "test-model";
19+
20+
// Mock for generateEmbeddings
21+
const mockEmbeddings = Array(1536).fill(0.1);
22+
const generateEmbeddingsMock = mock(async (text, config) => {
23+
return mockEmbeddings;
24+
});
25+
26+
// Mock the generateEmbeddings module
27+
mock.module("../src/shared/generateEmbeddings", () => {
28+
return {
29+
generateEmbeddings: generateEmbeddingsMock,
30+
};
31+
});
32+
33+
// Override the database module before other modules are imported
34+
describe("Document Route", () => {
35+
let db: Database;
36+
let documentId: number;
37+
38+
// Create a fresh test setup before tests
39+
beforeAll(async () => {
40+
// Create fresh database
41+
db = new Database("******");
42+
43+
// Configure database
44+
db.exec("PRAGMA journal_mode = WAL;");
45+
sqliteVec.load(db);
46+
db.exec(createDocumentsTableSQL("1536"));
47+
48+
// Spy on database module to return our test db
49+
mock.module("../src/database/database", () => ({
50+
db: db,
51+
getDb: () => db
52+
}));
53+
});
54+
55+
// Insert test data before each test
56+
beforeEach(() => {
57+
// Clear any existing data
58+
db.exec("DELETE FROM documents");
59+
60+
// Insert test data
61+
const sampleText = "This is a test document for document endpoint";
62+
const sampleSource = "test-source";
63+
const sampleMetadata = JSON.stringify({ testKey: "testValue" });
64+
const embeddingsStr = `[${mockEmbeddings.join(",")}]`;
65+
66+
db.query(`
67+
INSERT INTO documents (text, metadata, embeddings, source)
68+
VALUES (?, ?, ?, ?)
69+
`).run(sampleText, sampleMetadata, embeddingsStr, sampleSource);
70+
71+
// Get the document id
72+
const document = db.query("SELECT id FROM documents LIMIT 1").get() as { id: number } | null;
73+
documentId = document?.id || 0;
74+
});
75+
76+
// Clean up after all tests
77+
afterAll(() => {
78+
db.close();
79+
});
80+
81+
test("should retrieve document by id", async () => {
82+
// Import the module only after our mock is set up
83+
const { document } = await import("../src/routes/document/document");
84+
85+
// Get document by ID using the handler function directly
86+
const result = await document({ id: documentId });
87+
88+
// Verify document properties
89+
expect(result).toHaveProperty("document");
90+
91+
// Type guard to ensure we're checking the success case
92+
if ("document" in result) {
93+
const doc = result.document;
94+
expect(doc).toHaveProperty("id", documentId);
95+
expect(doc).toHaveProperty("text", "This is a test document for document endpoint");
96+
expect(doc).toHaveProperty("source", "test-source");
97+
expect(doc).toHaveProperty("metadata");
98+
expect(doc.metadata).toHaveProperty("testKey", "testValue");
99+
} else {
100+
// This should not happen in this test, but helps TypeScript
101+
throw new Error("Expected document in result but got error");
102+
}
103+
});
104+
105+
test("should handle non-existent document id", async () => {
106+
// Import the module only after our mock is set up
107+
const { document } = await import("../src/routes/document/document");
108+
109+
const result = await document({ id: 9999 });
110+
111+
// Verify error response
112+
expect(result).toHaveProperty("error", "Document not found");
113+
expect(result).not.toHaveProperty("document");
114+
});
115+
116+
test("should handle validation errors", async () => {
117+
// Import the module only after our mock is set up
118+
const { documentRoute } = await import("../src/routes/document/document");
119+
120+
// Create request with missing ID
121+
const request = new Request("http://localhost/document", {
122+
method: "POST",
123+
headers: {
124+
"Content-Type": "application/json",
125+
},
126+
body: JSON.stringify({}),
127+
});
128+
129+
// Process the request
130+
const response = await documentRoute(request);
131+
const responseData = await response.json();
132+
133+
// Verify validation error
134+
expect(response.status).toBe(400);
135+
expect(responseData).toHaveProperty("error", "Validation failed");
136+
expect(responseData).toHaveProperty("issues");
137+
});
138+
});

tests/embedding-model-change.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ import { reembedAllDocuments } from "../src/database/reembedding";
66
// Store original environment variables
77
const originalEnv = { ...process.env };
88

9+
// Mock for generateEmbeddings
10+
const mockEmbeddings = Array(1536).fill(0.1);
11+
const generateEmbeddingsMock = mock(async (text, config) => {
12+
return mockEmbeddings;
13+
});
14+
15+
// Mock the generateEmbeddings module
16+
mock.module("../src/shared/generateEmbeddings", () => {
17+
return {
18+
generateEmbeddings: generateEmbeddingsMock,
19+
};
20+
});
21+
922
// Helper function to setup mocks for a test
1023
const setupMocks = (storedModel: string, storedSize: string) => {
1124
// Create fresh mock for reembedding

tests/helpers/mockDb.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import Database from "bun:sqlite";
2+
import * as sqliteVec from "sqlite-vec";
3+
import { createDocumentsTableSQL, createDocumentChunksTableSQL } from "../../src/database/migrations";
4+
5+
/**
6+
* Creates a fresh in-memory database instance for testing
7+
* Each test file should create its own instance to avoid connection issues
8+
*/
9+
export function createTestDb() {
10+
// Create a new in-memory database
11+
const testDb = new Database("******");
12+
13+
// Enable WAL mode
14+
testDb.exec("PRAGMA journal_mode = WAL;");
15+
16+
// Load SQLite vector extension
17+
sqliteVec.load(testDb);
18+
19+
// Run migrations manually (can't use migration functions directly as they use the global db)
20+
testDb.exec(`
21+
CREATE TABLE IF NOT EXISTS migrations (
22+
id INTEGER PRIMARY KEY AUTOINCREMENT,
23+
name TEXT UNIQUE NOT NULL,
24+
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
25+
);
26+
`);
27+
28+
// Create documents table with 1536 dimensions for OpenAI embeddings
29+
testDb.exec(createDocumentsTableSQL("1536"));
30+
31+
// Create document chunks table
32+
testDb.exec(createDocumentChunksTableSQL("1536"));
33+
34+
// Create metadata table
35+
testDb.exec(`
36+
CREATE TABLE IF NOT EXISTS metadata (
37+
id INTEGER PRIMARY KEY AUTOINCREMENT,
38+
key TEXT UNIQUE NOT NULL,
39+
value TEXT,
40+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
41+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
42+
);
43+
`);
44+
45+
// Insert some initial metadata values that are expected
46+
testDb.exec(`
47+
INSERT INTO metadata (key, value) VALUES ('AI_EMBEDDING_MODEL', 'text-embedding-ada-002');
48+
INSERT INTO metadata (key, value) VALUES ('AI_EMBEDDING_SIZE', '1536');
49+
`);
50+
51+
return testDb;
52+
}
53+
54+
/**
55+
* Set up the test database as a global for the duration of test execution
56+
*/
57+
export function setupTestDb() {
58+
const db = createTestDb();
59+
(globalThis as any).testDb = db;
60+
return db;
61+
}
62+
63+
/**
64+
* Clean up the test database after tests are complete
65+
*/
66+
export function teardownTestDb(db: Database) {
67+
db.close();
68+
delete (globalThis as any).testDb;
69+
}

0 commit comments

Comments
 (0)