import { describe, it, expect } from 'vitest'; import { embedRoutes } from '../src/routes/embed'; import type { Bindings } from '../src/types'; // ── Minimal in-memory fakes (no Workers runtime) ───────────────────────────── // POST /embed/query is a thin shell over semanticSearch (rule 07). We drive the real // route through the real semanticSearch by faking only AI + VECTORIZE bindings. // - module off = no AI/VECTORIZE bindings → semanticSearch returns null → route 409. // - module on = fake AI (query→vector) + fake VECTORIZE.query (returns matches). function makeEnv(withBindings: boolean, matches: unknown[] = []): Bindings { const env = { ENVIRONMENT: 'test', ...(withBindings ? { AI: { async run(_m: string, i: { text: string[] }) { return { data: i.text.map(() => [0.1, 0.2, 0.3]) }; } }, VECTORIZE: { async query(_v: number[], _o: unknown) { return { matches }; } }, } : {}), } as unknown as Bindings; return env; } async function post(env: Bindings, body: unknown): Promise<{ status: number; json: any }> { // embedRoutes is the standalone sub-app; parent mounts it at '/embed', so here the path is '/query'. const req = new Request('http://kbdb/query', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }); const res = await embedRoutes.fetch(req, env); return { status: res.status, json: await res.json() }; } describe('POST /embed/query', () => { it('module on → returns semantic hits', async () => { const matches = [ { id: 'e1', score: 0.9, metadata: { owner_id: 'leo', entry_type: 'workflow', source: 'wiki' } }, { id: 'e2', score: 0.7, metadata: { owner_id: 'leo', entry_type: 'workflow', source: 'wiki' } }, ]; const env = makeEnv(true, matches); const { status, json } = await post(env, { q: 'doorbell', owner_id: 'leo', topK: 5 }); expect(status).toBe(200); expect(json.success).toBe(true); expect(json.query).toBe('doorbell'); expect(json.hits.map((h: { id: string }) => h.id)).toEqual(['e1', 'e2']); expect(json.hits[0]).toMatchObject({ id: 'e1', score: 0.9, owner_id: 'leo', entry_type: 'workflow', source: 'wiki' }); }); it('module off → 409 + capability_hint (誠實不假綠)', async () => { const env = makeEnv(false); const { status, json } = await post(env, { q: 'doorbell' }); expect(status).toBe(409); expect(json.success).toBe(false); expect(typeof json.capability_hint).toBe('string'); }); it('missing q → 400', async () => { const env = makeEnv(true); const { status, json } = await post(env, { owner_id: 'leo' }); expect(status).toBe(400); expect(json.success).toBe(false); expect(json.error).toContain('q'); }); it('empty/blank q → 400', async () => { const env = makeEnv(true); const { status, json } = await post(env, { q: ' ' }); expect(status).toBe(400); expect(json.success).toBe(false); }); });