|
1 | | -import { DatabaseSync } from 'node:sqlite'; |
2 | | -import { drizzle } from 'drizzle-orm/d1'; |
3 | 1 | import { beforeEach, describe, expect, it, vi } from 'vitest'; |
4 | 2 | import * as schema from '../db/schema.js'; |
| 3 | +import { createD1TestDb } from './fake-d1.js'; |
5 | 4 | import { |
6 | 5 | createDirectoryAgent, |
7 | 6 | listDirectoryAgents, |
8 | 7 | listDirectoryRatings, |
9 | 8 | searchDirectory, |
10 | 9 | upsertDirectoryRating, |
11 | 10 | } from '../engine/directory.js'; |
12 | | -import { buildFtsQuery } from '../engine/searchQuery.js'; |
13 | 11 |
|
14 | 12 | const TEST_SCHEMA_SQL = ` |
15 | 13 | PRAGMA foreign_keys = ON; |
@@ -168,71 +166,8 @@ AFTER DELETE ON directory_skills BEGIN |
168 | 166 | END; |
169 | 167 | `; |
170 | 168 |
|
171 | | -class FakeD1PreparedStatement { |
172 | | - private readonly sqlite: DatabaseSync; |
173 | | - private readonly query: string; |
174 | | - private readonly params: unknown[]; |
175 | | - |
176 | | - constructor(sqlite: DatabaseSync, query: string, params: unknown[] = []) { |
177 | | - this.sqlite = sqlite; |
178 | | - this.query = query; |
179 | | - this.params = params; |
180 | | - } |
181 | | - |
182 | | - bind(...params: unknown[]) { |
183 | | - return new FakeD1PreparedStatement(this.sqlite, this.query, params); |
184 | | - } |
185 | | - |
186 | | - async run() { |
187 | | - this.sqlite.prepare(this.query).run(...this.params); |
188 | | - return { success: true, meta: {}, results: [] }; |
189 | | - } |
190 | | - |
191 | | - async all() { |
192 | | - return { |
193 | | - success: true, |
194 | | - meta: {}, |
195 | | - results: this.sqlite.prepare(this.query).all(...this.params), |
196 | | - }; |
197 | | - } |
198 | | - |
199 | | - async raw() { |
200 | | - const statement = this.sqlite.prepare(this.query); |
201 | | - statement.setReturnArrays(true); |
202 | | - return statement.all(...this.params); |
203 | | - } |
204 | | - |
205 | | - async first() { |
206 | | - return this.sqlite.prepare(this.query).get(...this.params); |
207 | | - } |
208 | | -} |
209 | | - |
210 | | -class FakeD1Database { |
211 | | - readonly sqlite = new DatabaseSync(':memory:'); |
212 | | - |
213 | | - constructor() { |
214 | | - this.sqlite.exec(TEST_SCHEMA_SQL); |
215 | | - } |
216 | | - |
217 | | - prepare(query: string) { |
218 | | - return new FakeD1PreparedStatement(this.sqlite, query); |
219 | | - } |
220 | | - |
221 | | - async batch(statements: Array<FakeD1PreparedStatement>) { |
222 | | - return Promise.all(statements.map((statement) => statement.all())); |
223 | | - } |
224 | | - |
225 | | - async exec(query: string) { |
226 | | - this.sqlite.exec(query); |
227 | | - } |
228 | | -} |
229 | | - |
230 | 169 | function createTestDb() { |
231 | | - const d1 = new FakeD1Database(); |
232 | | - return { |
233 | | - db: drizzle(d1 as unknown as D1Database, { schema }), |
234 | | - sqlite: d1.sqlite, |
235 | | - }; |
| 170 | + return createD1TestDb(TEST_SCHEMA_SQL); |
236 | 171 | } |
237 | 172 |
|
238 | 173 | async function seedWorkspace(db: ReturnType<typeof createTestDb>['db']) { |
@@ -343,8 +278,8 @@ describe('directory engine', () => { |
343 | 278 | expect(ratedEntries[0]?.rating_avg).toBe(4); |
344 | 279 | }); |
345 | 280 |
|
346 | | - it('matches FTS5 queries across skills and agent descriptions', async () => { |
347 | | - const { db, sqlite } = createTestDb(); |
| 281 | + it('matches search queries across skills and agent descriptions', async () => { |
| 282 | + const { db } = createTestDb(); |
348 | 283 | await seedWorkspace(db); |
349 | 284 | await seedAgent(db, { id: 'agent_research', name: 'ResearchBot' }); |
350 | 285 | await seedAgent(db, { id: 'agent_support', name: 'SupportBot' }); |
@@ -381,25 +316,14 @@ describe('directory engine', () => { |
381 | 316 | ], |
382 | 317 | }); |
383 | 318 |
|
384 | | - const skillQuery = buildFtsQuery('entity extraction pipelines'); |
385 | | - const skillMatchRows = sqlite.prepare(` |
386 | | - SELECT da.slug |
387 | | - FROM directory_skills_fts |
388 | | - JOIN directory_skills ds ON ds.rowid = directory_skills_fts.rowid |
389 | | - JOIN directory_agents da ON da.id = ds.directory_agent_id |
390 | | - WHERE directory_skills_fts MATCH ? |
391 | | - ORDER BY da.slug ASC |
392 | | - `).all(skillQuery) as Array<{ slug: string }>; |
393 | | - expect(skillMatchRows.map((row) => row.slug)).toContain('researchbot'); |
394 | | - |
395 | | - const descriptionQuery = buildFtsQuery('subscription billing specialist'); |
396 | | - const descriptionMatchRows = sqlite.prepare(` |
397 | | - SELECT da.slug |
398 | | - FROM directory_agents_fts |
399 | | - JOIN directory_agents da ON da.rowid = directory_agents_fts.rowid |
400 | | - WHERE directory_agents_fts MATCH ? |
401 | | - ORDER BY da.slug ASC |
402 | | - `).all(descriptionQuery) as Array<{ slug: string }>; |
403 | | - expect(descriptionMatchRows.map((row) => row.slug)).toContain('supportbot'); |
| 319 | + const skillResults = await searchDirectory(db, 'ws_test', { |
| 320 | + q: 'entity extraction pipelines', |
| 321 | + }); |
| 322 | + expect(skillResults.map((row) => row.slug)).toContain('researchbot'); |
| 323 | + |
| 324 | + const descriptionResults = await searchDirectory(db, 'ws_test', { |
| 325 | + q: 'subscription billing specialist', |
| 326 | + }); |
| 327 | + expect(descriptionResults.map((row) => row.slug)).toContain('supportbot'); |
404 | 328 | }); |
405 | 329 | }); |
0 commit comments