@@ -0,0 +1,84 @@
// 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 ) ;
} ) ;
} ) ;