c9e44bb9a3
- routes/embed.ts 加 /embed/query:q 必填、owner_id/source/entry_type/topK 通用 filter - 模組未開回 409+capability_hint(不假綠)、缺q回400;能力不重寫(rule07 薄殼) - tests/embed-query.test.ts 4例、tasks 12.6 標x;tsc 0 error、vitest 9/9
72 lines
3.0 KiB
TypeScript
72 lines
3.0 KiB
TypeScript
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);
|
|
});
|
|
});
|