@@ -326,22 +326,38 @@ describe('index-documents job handler', () => {
326326 ) . toHaveLength ( 0 )
327327 } )
328328
329- it ( 'warns when an item yields no indexable text, and still completes it with an empty material ' , async ( ) => {
329+ it ( 'rejects a file when its reader yields no indexable text' , async ( ) => {
330330 const handler = createIndexDocumentsJobHandler ( knowledgeLockManager as never )
331- knowledgeItemGetByIdMock . mockReturnValue ( createNoteItem ( NOTE_ITEM_ID ) )
332- knowledgeItemUpdateStatusMock . mockReturnValue ( createNoteItem ( NOTE_ITEM_ID ) )
331+ knowledgeItemGetByIdMock . mockReturnValue ( createFileItem ( FILE_ITEM_ID ) )
333332 loadKnowledgeItemDocumentsMock . mockResolvedValueOnce ( [ ] )
333+ const failureReason =
334+ 'No indexable text was extracted. Check the source, OCR, or document processing settings, then reindex.'
334335
335- await handler . execute ( createCtx ( { baseId : 'kb-1' , itemId : NOTE_ITEM_ID } ) )
336+ await expect ( handler . execute ( createCtx ( { baseId : 'kb-1' , itemId : FILE_ITEM_ID } ) ) ) . rejects . toThrow ( failureReason )
337+
338+ // JobManager forwards the thrown reason to onSettled; verify that the existing
339+ // settlement seam makes the row visibly failed (and therefore reindexable).
340+ await handler . onSettled ?.( {
341+ jobId : 'index-job' ,
342+ type : 'knowledge.index-documents' ,
343+ scheduleId : null ,
344+ parentId : null ,
345+ status : 'failed' ,
346+ input : { baseId : 'kb-1' , itemId : FILE_ITEM_ID } ,
347+ error : { code : 'FAILED' , message : failureReason , retryable : false } ,
348+ attempt : 1 ,
349+ metadata : { }
350+ } )
336351
337- // An image-only PDF or failed extraction must leave a diagnosable trace —
338- // without the warn it would look indexed while matching nothing.
352+ expect ( embedKnowledgeTextsMock ) . not . toHaveBeenCalled ( )
353+ expect ( rebuildMaterialMock ) . not . toHaveBeenCalled ( )
354+ expect ( knowledgeItemUpdateStatusMock ) . not . toHaveBeenCalledWith ( FILE_ITEM_ID , 'embedding' )
355+ expect ( knowledgeItemUpdateStatusMock ) . not . toHaveBeenCalledWith ( FILE_ITEM_ID , 'completed' )
356+ expect ( knowledgeItemUpdateStatusMock ) . toHaveBeenCalledWith ( FILE_ITEM_ID , 'failed' , { error : failureReason } )
339357 expect ( loggerWarnMock ) . toHaveBeenCalledWith (
340- 'Knowledge item produced no indexable text; it will complete with an empty index ' ,
341- expect . objectContaining ( { baseId : 'kb-1' , itemId : NOTE_ITEM_ID } )
358+ 'Knowledge item produced no indexable text; failing indexing ' ,
359+ expect . objectContaining ( { baseId : 'kb-1' , itemId : FILE_ITEM_ID } )
342360 )
343- expect ( lastRebuildInput ( ) . units ) . toEqual ( [ ] )
344- expect ( knowledgeItemUpdateStatusMock ) . toHaveBeenCalledWith ( NOTE_ITEM_ID , 'completed' )
345361 } )
346362
347363 it ( 'uses the processed-artifact path (indexedRelativePath) as the material relative path' , async ( ) => {
@@ -365,21 +381,17 @@ describe('index-documents job handler', () => {
365381 expect ( loadKnowledgeItemDocumentsMock ) . toHaveBeenCalledWith ( expect . objectContaining ( { id : FILE_ITEM_ID } ) )
366382 } )
367383
368- it ( 'completes with empty vectors when the reader returns no documents ' , async ( ) => {
384+ it ( 'rejects whitespace-only reader output before embedding or material replacement ' , async ( ) => {
369385 const handler = createIndexDocumentsJobHandler ( knowledgeLockManager as never )
370- knowledgeItemGetByIdMock . mockReturnValue ( createNoteItem ( NOTE_ITEM_ID ) )
371- knowledgeItemUpdateStatusMock . mockReturnValue ( createNoteItem ( NOTE_ITEM_ID ) )
372- loadKnowledgeItemDocumentsMock . mockResolvedValueOnce ( [ ] )
373-
374- await handler . execute ( createCtx ( { baseId : 'kb-1' , itemId : NOTE_ITEM_ID } ) )
386+ knowledgeItemGetByIdMock . mockReturnValue ( createFileItem ( FILE_ITEM_ID ) )
387+ loadKnowledgeItemDocumentsMock . mockResolvedValueOnce ( [ { text : ' \n\t ' , metadata : { source : FILE_ITEM_ID } } ] )
375388
376- expect ( knowledgeItemUpdateStatusMock ) . toHaveBeenCalledWith ( NOTE_ITEM_ID , 'reading' )
377- expect ( knowledgeItemUpdateStatusMock ) . toHaveBeenCalledWith ( NOTE_ITEM_ID , 'embedding' )
378- expect ( rebuildMaterialMock ) . toHaveBeenCalledWith (
379- NOTE_ITEM_ID ,
380- expect . objectContaining ( { content : expect . objectContaining ( { text : '' } ) , units : [ ] , embeddings : [ ] } )
389+ await expect ( handler . execute ( createCtx ( { baseId : 'kb-1' , itemId : FILE_ITEM_ID } ) ) ) . rejects . toThrow (
390+ 'No indexable text was extracted'
381391 )
382- expect ( knowledgeItemUpdateStatusMock ) . toHaveBeenCalledWith ( NOTE_ITEM_ID , 'completed' )
392+
393+ expect ( embedKnowledgeTextsMock ) . not . toHaveBeenCalled ( )
394+ expect ( rebuildMaterialMock ) . not . toHaveBeenCalled ( )
383395 } )
384396
385397 it ( 'skips vector write when the item becomes deleting inside the mutation lock' , async ( ) => {
0 commit comments