@@ -7,13 +7,18 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
77const mocks = vi . hoisted ( ( ) => ( {
88 getKnowledgeDocument : vi . fn ( ) ,
99 processDocumentsWithQueue : vi . fn ( ) ,
10+ reclaimStaleDocumentProcessingClaim : vi . fn ( ) ,
1011} ) )
1112
1213vi . mock ( '@/lib/knowledge/documents/service' , ( ) => ( {
1314 getKnowledgeDocument : mocks . getKnowledgeDocument ,
1415 processDocumentsWithQueue : mocks . processDocumentsWithQueue ,
1516} ) )
1617
18+ vi . mock ( '@/lib/knowledge/documents/processing-claim' , ( ) => ( {
19+ reclaimStaleDocumentProcessingClaim : mocks . reclaimStaleDocumentProcessingClaim ,
20+ } ) )
21+
1722import type { BillingAttributionSnapshot } from '@/lib/billing/core/billing-attribution'
1823import type { OutboxEventContext } from '@/lib/core/outbox/service'
1924import { KNOWLEDGE_DOCUMENT_PROCESSING_OUTBOX_EVENT } from '@/lib/knowledge/documents/processing-outbox-event'
@@ -71,6 +76,7 @@ describe('knowledge document processing outbox handler', () => {
7176 vi . clearAllMocks ( )
7277 mocks . getKnowledgeDocument . mockResolvedValue ( DOCUMENT )
7378 mocks . processDocumentsWithQueue . mockResolvedValue ( undefined )
79+ mocks . reclaimStaleDocumentProcessingClaim . mockResolvedValue ( false )
7480 } )
7581
7682 it ( 'dispatches the authoritative document with the stable outbox event id' , async ( ) => {
@@ -106,18 +112,58 @@ describe('knowledge document processing outbox handler', () => {
106112 )
107113
108114 it ( 'keeps the event retryable while an earlier processing attempt is active' , async ( ) => {
115+ const processingStartedAt = new Date ( )
109116 mocks . getKnowledgeDocument . mockResolvedValueOnce ( {
110117 ...DOCUMENT ,
111118 processingStatus : 'processing' ,
119+ processingStartedAt,
112120 } )
113121
114122 await expect ( handler ( ) ( PAYLOAD , createContext ( ) ) ) . rejects . toThrow (
115123 'Knowledge document document-1 is already being processed'
116124 )
117125
126+ expect ( mocks . reclaimStaleDocumentProcessingClaim ) . toHaveBeenCalledWith ( {
127+ knowledgeBaseId : 'knowledge-base-1' ,
128+ documentId : 'document-1' ,
129+ processingStartedAt,
130+ } )
118131 expect ( mocks . processDocumentsWithQueue ) . not . toHaveBeenCalled ( )
119132 } )
120133
134+ it ( 'reclaims and redispatches an abandoned processing attempt' , async ( ) => {
135+ const processingStartedAt = new Date ( '2026-08-11T11:00:00.000Z' )
136+ mocks . getKnowledgeDocument . mockResolvedValueOnce ( {
137+ ...DOCUMENT ,
138+ processingStatus : 'processing' ,
139+ processingStartedAt,
140+ } )
141+ mocks . reclaimStaleDocumentProcessingClaim . mockResolvedValueOnce ( true )
142+
143+ await handler ( ) ( PAYLOAD , createContext ( 'outbox-event-retry' ) )
144+
145+ expect ( mocks . reclaimStaleDocumentProcessingClaim ) . toHaveBeenCalledWith ( {
146+ knowledgeBaseId : 'knowledge-base-1' ,
147+ documentId : 'document-1' ,
148+ processingStartedAt,
149+ } )
150+ expect ( mocks . processDocumentsWithQueue ) . toHaveBeenCalledWith (
151+ [
152+ {
153+ documentId : 'document-1' ,
154+ filename : 'guide.pdf' ,
155+ fileUrl : '/api/files/serve/kb%2Fguide.pdf?context=knowledge-base' ,
156+ fileSize : 128 ,
157+ mimeType : 'application/pdf' ,
158+ } ,
159+ ] ,
160+ 'knowledge-base-1' ,
161+ { recipe : 'default' , lang : 'en' } ,
162+ 'outbox-event-retry' ,
163+ BILLING_ATTRIBUTION
164+ )
165+ } )
166+
121167 it ( 'propagates dispatch failures so the outbox schedules a retry' , async ( ) => {
122168 const failure = new Error ( 'queue unavailable' )
123169 mocks . processDocumentsWithQueue . mockRejectedValueOnce ( failure )
0 commit comments