6715402bcc
舊版把向量刪除包成 fire-and-forget(waitUntil(...).catch(()=>{}))——失敗被靜默
吞掉,呼叫端(rag_takedown_direct workflow/未來的 portal 刪除 UI)永遠不知道
向量沒清乾淨。與同檔 /entries/deprecate-by-library(同步 await + 回報
vectors_deleted)的做法不一致,補齊同款誠實回報:回應加 vector_deleted
(true/false/null),D1 本體刪除不因向量失敗而被擋下。
補上這支端點原本零覆蓋的測試(entries-delete.test.ts,3 案:模組未開/
向量刪除成功/向量刪除失敗)。
隨附 live e2e 驗證(youlin stage,4 筆測試知識,含批次 3 筆):刪除後
keyword/semantic 混合檢索與 rag_chat AI 問答皆不再讀到已刪內容,控制組
(未刪的既有知識)不受影響——確認端到端「刪除即在所有查詢路徑消失」成立,
細節見 Leo/arcrun-rag#46 留言。
85 lines
4.2 KiB
TypeScript
85 lines
4.2 KiB
TypeScript
// arcrun-rag#46「刪掉的知識搜尋還撈得到」第 4 點:中途失敗要看得出來。
|
||
//
|
||
// DELETE /entries/:id 舊版把向量刪除包成 fire-and-forget(waitUntil(...).catch(()=>{}))——
|
||
// 呼叫端完全看不到向量清除是否成功。本測試鎖住新行為:同步 await+回應帶 vector_deleted,
|
||
// 且無論向量刪除成功或失敗,D1 本體都要真的被刪掉(不因向量失敗而擋下本體刪除)。
|
||
//
|
||
// 測試手法同 search-deprecated-filter.test.ts:fake D1 捕捉 SQL 形狀;mock VECTORIZE 可控
|
||
// deleteByIds 成功/失敗,驗證 route 層如何把結果誠實透傳給呼叫端。
|
||
import { describe, it, expect } from 'vitest';
|
||
import { Hono } from 'hono';
|
||
import { entryRoutes } from '../src/routes/entries';
|
||
import type { Bindings } from '../src/types';
|
||
|
||
interface Captured { sql: string; params: unknown[] }
|
||
|
||
function makeCaptureDB(captured: Captured[]) {
|
||
const prepare = (sql: string) => {
|
||
const rec: Captured = { sql, params: [] };
|
||
captured.push(rec);
|
||
const stmt = {
|
||
bind(...args: unknown[]) { rec.params = args; return stmt; },
|
||
async all<T>() { return { results: [] as T[] }; },
|
||
async first<T>() { return null as unknown as T; },
|
||
async run() { return { success: true }; },
|
||
};
|
||
return stmt;
|
||
};
|
||
return { prepare } as unknown as D1Database;
|
||
}
|
||
|
||
function makeApp(captured: Captured[], extraEnv: Record<string, unknown> = {}) {
|
||
const app = new Hono<{ Bindings: Bindings }>();
|
||
app.route('/entries', entryRoutes);
|
||
const env = { DB: makeCaptureDB(captured), ENVIRONMENT: 'test', ...extraEnv } as unknown as Bindings;
|
||
return { app, env };
|
||
}
|
||
|
||
describe('arcrun-rag#46 — DELETE /entries/:id 失敗可見性', () => {
|
||
it('embed 模組未開 → vector_deleted:null(不適用,非「清成功了」的謊)+ D1 仍照刪', async () => {
|
||
const captured: Captured[] = [];
|
||
const { app, env } = makeApp(captured); // 無 VECTORIZE/AI
|
||
const res = await app.request('/entries/e123', { method: 'DELETE' }, env);
|
||
expect(res.status).toBe(200);
|
||
const body = (await res.json()) as { success: boolean; vector_deleted: boolean | null };
|
||
expect(body.success).toBe(true);
|
||
expect(body.vector_deleted).toBe(null);
|
||
expect(captured.some((c) => c.sql.includes('DELETE FROM entries WHERE id = ?'))).toBe(true);
|
||
});
|
||
|
||
it('模組開+向量刪除成功 → vector_deleted:true(同步等到結果才回應,不是猜的)', async () => {
|
||
const captured: Captured[] = [];
|
||
const deletedIds: string[][] = [];
|
||
const { app, env } = makeApp(captured, {
|
||
VECTORIZE: {
|
||
async deleteByIds(ids: string[]) { deletedIds.push(ids); return { count: ids.length }; },
|
||
},
|
||
AI: { async run() { return { data: [[0.1]] }; } },
|
||
});
|
||
const res = await app.request('/entries/e123', { method: 'DELETE' }, env);
|
||
const body = (await res.json()) as { success: boolean; vector_deleted: boolean | null };
|
||
expect(body.success).toBe(true);
|
||
expect(body.vector_deleted).toBe(true);
|
||
expect(deletedIds).toEqual([['e123']]); // 真的呼叫了、帶對 id,不是没做就回真
|
||
});
|
||
|
||
it('🔴 模組開+向量刪除失敗 → vector_deleted:false 誠實回報,且 D1 本體仍真的刪掉', async () => {
|
||
const captured: Captured[] = [];
|
||
const { app, env } = makeApp(captured, {
|
||
VECTORIZE: {
|
||
async deleteByIds() { throw new Error('Vectorize 503(模擬故障)'); },
|
||
},
|
||
AI: { async run() { return { data: [[0.1]] }; } },
|
||
});
|
||
const res = await app.request('/entries/e123', { method: 'DELETE' }, env);
|
||
// 舊版這裡的失敗會被 waitUntil(...).catch(()=>{}) 吞掉、caller 永遠看不到;
|
||
// 新版:HTTP 仍是 200(D1 本體真的刪了,這件事沒有失敗),但誠實標出向量那一步失敗了。
|
||
expect(res.status).toBe(200);
|
||
const body = (await res.json()) as { success: boolean; vector_deleted: boolean | null };
|
||
expect(body.success).toBe(true);
|
||
expect(body.vector_deleted).toBe(false);
|
||
// D1 本體不因向量失敗而被擋下——刪除的「本體一定會消失」承諾不打折扣。
|
||
expect(captured.some((c) => c.sql.includes('DELETE FROM entries WHERE id = ?'))).toBe(true);
|
||
});
|
||
});
|