@@ -32,6 +32,7 @@ describe('EsSyncProcessor', () => {
3232 bulk : jest . fn ( ) ,
3333 updateByQuery : jest . fn ( ) ,
3434 deleteByQuery : jest . fn ( ) ,
35+ get : jest . fn ( ) ,
3536 } ;
3637
3738 const mock_user_follows_repository = {
@@ -184,6 +185,7 @@ describe('EsSyncProcessor', () => {
184185 const mock_tweet = {
185186 tweet_id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
186187 content : 'Reply tweet' ,
188+ type : TweetType . REPLY ,
187189 user_id : '1a2b3c4d-5e6f-7g8h-9i0j-k1l2m3n4o5p6' ,
188190 user : {
189191 name : 'Test User' ,
@@ -213,6 +215,260 @@ describe('EsSyncProcessor', () => {
213215 } ) ,
214216 } ) ;
215217 } ) ;
218+
219+ it ( 'should use existing parent_id from ES when not provided in job data' , async ( ) => {
220+ const mock_tweet = {
221+ tweet_id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
222+ content : 'Reply tweet' ,
223+ type : TweetType . REPLY ,
224+ user_id : '1a2b3c4d-5e6f-7g8h-9i0j-k1l2m3n4o5p6' ,
225+ user : {
226+ name : 'Test User' ,
227+ username : 'testuser' ,
228+ } as User ,
229+ } as Tweet ;
230+
231+ const job = {
232+ data : {
233+ tweet_id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
234+ parent_id : undefined ,
235+ conversation_id : '4fa1b0f4-a059-4b6f-ab1f-137217d33d3c' ,
236+ } ,
237+ } as Job ;
238+
239+ const existing_es_doc = {
240+ _source : {
241+ parent_id : '6ba9c7cf-302b-433f-8642-50de81ef0372' ,
242+ conversation_id : '4fa1b0f4-a059-4b6f-ab1f-137217d33d3c' ,
243+ } ,
244+ } ;
245+
246+ mock_tweets_repository . findOne . mockResolvedValue ( mock_tweet ) ;
247+ mock_elasticsearch_service . get . mockResolvedValue ( existing_es_doc as any ) ;
248+ mock_elasticsearch_service . index . mockResolvedValue ( { } as any ) ;
249+
250+ await processor . handleIndexTweet ( job ) ;
251+
252+ expect ( mock_elasticsearch_service . get ) . toHaveBeenCalledWith ( {
253+ index : ELASTICSEARCH_INDICES . TWEETS ,
254+ id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
255+ } ) ;
256+ expect ( mock_elasticsearch_service . index ) . toHaveBeenCalledWith ( {
257+ index : ELASTICSEARCH_INDICES . TWEETS ,
258+ id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
259+ document : expect . objectContaining ( {
260+ parent_id : '6ba9c7cf-302b-433f-8642-50de81ef0372' ,
261+ conversation_id : '4fa1b0f4-a059-4b6f-ab1f-137217d33d3c' ,
262+ } ) ,
263+ } ) ;
264+ } ) ;
265+
266+ it ( 'should use existing conversation_id from ES when not provided in job data' , async ( ) => {
267+ const mock_tweet = {
268+ tweet_id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
269+ content : 'Reply tweet' ,
270+ type : TweetType . REPLY ,
271+ user_id : '1a2b3c4d-5e6f-7g8h-9i0j-k1l2m3n4o5p6' ,
272+ user : {
273+ name : 'Test User' ,
274+ username : 'testuser' ,
275+ } as User ,
276+ } as Tweet ;
277+
278+ const job = {
279+ data : {
280+ tweet_id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
281+ parent_id : '6ba9c7cf-302b-433f-8642-50de81ef0372' ,
282+ conversation_id : undefined ,
283+ } ,
284+ } as Job ;
285+
286+ const existing_es_doc = {
287+ _source : {
288+ parent_id : '6ba9c7cf-302b-433f-8642-50de81ef0372' ,
289+ conversation_id : '4fa1b0f4-a059-4b6f-ab1f-137217d33d3c' ,
290+ } ,
291+ } ;
292+
293+ mock_tweets_repository . findOne . mockResolvedValue ( mock_tweet ) ;
294+ mock_elasticsearch_service . get . mockResolvedValue ( existing_es_doc as any ) ;
295+ mock_elasticsearch_service . index . mockResolvedValue ( { } as any ) ;
296+
297+ await processor . handleIndexTweet ( job ) ;
298+
299+ expect ( mock_elasticsearch_service . get ) . toHaveBeenCalledWith ( {
300+ index : ELASTICSEARCH_INDICES . TWEETS ,
301+ id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
302+ } ) ;
303+ expect ( mock_elasticsearch_service . index ) . toHaveBeenCalledWith ( {
304+ index : ELASTICSEARCH_INDICES . TWEETS ,
305+ id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
306+ document : expect . objectContaining ( {
307+ parent_id : '6ba9c7cf-302b-433f-8642-50de81ef0372' ,
308+ conversation_id : '4fa1b0f4-a059-4b6f-ab1f-137217d33d3c' ,
309+ } ) ,
310+ } ) ;
311+ } ) ;
312+
313+ it ( 'should use existing parent_id and conversation_id from ES when both not provided' , async ( ) => {
314+ const mock_tweet = {
315+ tweet_id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
316+ content : 'Reply tweet' ,
317+ type : TweetType . REPLY ,
318+ user_id : '1a2b3c4d-5e6f-7g8h-9i0j-k1l2m3n4o5p6' ,
319+ user : {
320+ name : 'Test User' ,
321+ username : 'testuser' ,
322+ } as User ,
323+ } as Tweet ;
324+
325+ const job = {
326+ data : {
327+ tweet_id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
328+ parent_id : undefined ,
329+ conversation_id : undefined ,
330+ } ,
331+ } as Job ;
332+
333+ const existing_es_doc = {
334+ _source : {
335+ parent_id : '6ba9c7cf-302b-433f-8642-50de81ef0372' ,
336+ conversation_id : '4fa1b0f4-a059-4b6f-ab1f-137217d33d3c' ,
337+ } ,
338+ } ;
339+
340+ mock_tweets_repository . findOne . mockResolvedValue ( mock_tweet ) ;
341+ mock_elasticsearch_service . get . mockResolvedValue ( existing_es_doc as any ) ;
342+ mock_elasticsearch_service . index . mockResolvedValue ( { } as any ) ;
343+
344+ await processor . handleIndexTweet ( job ) ;
345+
346+ expect ( mock_elasticsearch_service . get ) . toHaveBeenCalledWith ( {
347+ index : ELASTICSEARCH_INDICES . TWEETS ,
348+ id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
349+ } ) ;
350+ expect ( mock_elasticsearch_service . index ) . toHaveBeenCalledWith ( {
351+ index : ELASTICSEARCH_INDICES . TWEETS ,
352+ id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
353+ document : expect . objectContaining ( {
354+ parent_id : '6ba9c7cf-302b-433f-8642-50de81ef0372' ,
355+ conversation_id : '4fa1b0f4-a059-4b6f-ab1f-137217d33d3c' ,
356+ } ) ,
357+ } ) ;
358+ } ) ;
359+
360+ it ( 'should skip ES lookup when tweet type is TWEET even if IDs not provided' , async ( ) => {
361+ const mock_tweet = {
362+ tweet_id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
363+ content : 'Regular tweet' ,
364+ type : TweetType . TWEET ,
365+ user_id : '1a2b3c4d-5e6f-7g8h-9i0j-k1l2m3n4o5p6' ,
366+ user : {
367+ name : 'Test User' ,
368+ username : 'testuser' ,
369+ } as User ,
370+ } as Tweet ;
371+
372+ const job = {
373+ data : {
374+ tweet_id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
375+ parent_id : undefined ,
376+ conversation_id : undefined ,
377+ } ,
378+ } as Job ;
379+
380+ mock_tweets_repository . findOne . mockResolvedValue ( mock_tweet ) ;
381+ mock_elasticsearch_service . index . mockResolvedValue ( { } as any ) ;
382+
383+ await processor . handleIndexTweet ( job ) ;
384+
385+ expect ( mock_elasticsearch_service . get ) . not . toHaveBeenCalled ( ) ;
386+ expect ( mock_elasticsearch_service . index ) . toHaveBeenCalled ( ) ;
387+ } ) ;
388+
389+ it ( 'should handle ES get error gracefully and continue with indexing' , async ( ) => {
390+ const mock_tweet = {
391+ tweet_id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
392+ content : 'Reply tweet' ,
393+ type : TweetType . REPLY ,
394+ user_id : '1a2b3c4d-5e6f-7g8h-9i0j-k1l2m3n4o5p6' ,
395+ user : {
396+ name : 'Test User' ,
397+ username : 'testuser' ,
398+ } as User ,
399+ } as Tweet ;
400+
401+ const job = {
402+ data : {
403+ tweet_id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
404+ parent_id : undefined ,
405+ conversation_id : undefined ,
406+ } ,
407+ } as Job ;
408+
409+ mock_tweets_repository . findOne . mockResolvedValue ( mock_tweet ) ;
410+ mock_elasticsearch_service . get . mockRejectedValue ( new Error ( 'Document not found' ) ) ;
411+ mock_elasticsearch_service . index . mockResolvedValue ( { } as any ) ;
412+
413+ const logger_spy = jest . spyOn ( Logger . prototype , 'debug' ) ;
414+
415+ await processor . handleIndexTweet ( job ) ;
416+
417+ expect ( logger_spy ) . toHaveBeenCalledWith (
418+ 'No existing ES document for tweet 0c059899-f706-4c8f-97d7-ba2e9fc22d6d'
419+ ) ;
420+ expect ( mock_elasticsearch_service . index ) . toHaveBeenCalledWith ( {
421+ index : ELASTICSEARCH_INDICES . TWEETS ,
422+ id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
423+ document : expect . objectContaining ( {
424+ tweet_id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
425+ } ) ,
426+ } ) ;
427+ } ) ;
428+
429+ it ( 'should prefer job data IDs over existing ES document IDs' , async ( ) => {
430+ const mock_tweet = {
431+ tweet_id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
432+ content : 'Reply tweet' ,
433+ type : TweetType . REPLY ,
434+ user_id : '1a2b3c4d-5e6f-7g8h-9i0j-k1l2m3n4o5p6' ,
435+ user : {
436+ name : 'Test User' ,
437+ username : 'testuser' ,
438+ } as User ,
439+ } as Tweet ;
440+
441+ const job = {
442+ data : {
443+ tweet_id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
444+ parent_id : 'new-parent-id' ,
445+ conversation_id : 'new-conversation-id' ,
446+ } ,
447+ } as Job ;
448+
449+ const existing_es_doc = {
450+ _source : {
451+ parent_id : 'old-parent-id' ,
452+ conversation_id : 'old-conversation-id' ,
453+ } ,
454+ } ;
455+
456+ mock_tweets_repository . findOne . mockResolvedValue ( mock_tweet ) ;
457+ mock_elasticsearch_service . get . mockResolvedValue ( existing_es_doc as any ) ;
458+ mock_elasticsearch_service . index . mockResolvedValue ( { } as any ) ;
459+
460+ await processor . handleIndexTweet ( job ) ;
461+
462+ expect ( mock_elasticsearch_service . get ) . not . toHaveBeenCalled ( ) ;
463+ expect ( mock_elasticsearch_service . index ) . toHaveBeenCalledWith ( {
464+ index : ELASTICSEARCH_INDICES . TWEETS ,
465+ id : '0c059899-f706-4c8f-97d7-ba2e9fc22d6d' ,
466+ document : expect . objectContaining ( {
467+ parent_id : 'new-parent-id' ,
468+ conversation_id : 'new-conversation-id' ,
469+ } ) ,
470+ } ) ;
471+ } ) ;
216472 } ) ;
217473
218474 describe ( 'handleDeleteTweet' , ( ) => {
0 commit comments