@@ -408,6 +408,63 @@ func TestManager_StopDuringFanOutDoesNotDeadlock(t *testing.T) {
408408 require .NoError (t , manager .Stop ())
409409}
410410
411+ // TestManager_PromoteOrphanOnNoSuchTransactionID verifies that the
412+ // "no such transaction ID" error message returned by the Fabric-X ledger
413+ // (fabric-smart-client/platform/fabricx/core/ledger/ledger.go) is recognised
414+ // as a NotFound error, so the manager promotes the tx to storage.Orphan after
415+ // the NotFoundGracePeriod has elapsed.
416+ func TestManager_PromoteOrphanOnNoSuchTransactionID (t * testing.T ) {
417+ logger := logging .MustGetLogger ()
418+ mockDB := & mock2.Storage {}
419+ mockHandler := & mock2.Handler {}
420+ config := recovery2.Config {
421+ Enabled : true ,
422+ TTL : 100 * time .Millisecond ,
423+ ScanInterval : 100 * time .Millisecond ,
424+ BatchSize : 100 ,
425+ WorkerCount : 1 ,
426+ LeaseDuration : time .Second ,
427+ AdvisoryLockID : 1 ,
428+ InstanceID : "test-instance" ,
429+ NotFoundGracePeriod : 10 * time .Millisecond ,
430+ }
431+
432+ // stored_at well beyond the 10ms grace period so the promotion fires.
433+ txRecord := & ttxdb.RecoveryClaim {
434+ TxID : "txNoSuchTx" ,
435+ StoredAt : time .Now ().Add (- time .Hour ),
436+ }
437+
438+ leadership1 := & mock2.Leadership {}
439+ leadership1 .CloseReturns (nil )
440+ leadership2 := & mock2.Leadership {}
441+ leadership2 .CloseReturns (nil )
442+
443+ mockDB .AcquireRecoveryLeadershipReturnsOnCall (0 , leadership1 , true , nil )
444+ mockDB .AcquireRecoveryLeadershipReturns (leadership2 , true , nil )
445+ mockDB .ClaimPendingTransactionsReturnsOnCall (0 , []* ttxdb.RecoveryClaim {txRecord }, nil )
446+ mockDB .ClaimPendingTransactionsReturns ([]* ttxdb.RecoveryClaim {}, nil )
447+ // Use the Fabric-X ledger sentinel substring directly.
448+ mockHandler .RecoverReturns (errors .New ("Failed to get transaction with id d48c4, error no such transaction ID [d48c] in index" ))
449+ mockDB .ReleaseRecoveryClaimReturns (nil )
450+ mockDB .SetStatusReturns (nil )
451+
452+ manager := recovery2 .NewManager (logger , mockDB , mockHandler , config )
453+
454+ require .NoError (t , manager .Start ())
455+ // Wait for the initial sweep (jitter up to 1s + handler invocation).
456+ time .Sleep (1300 * time .Millisecond )
457+ _ = manager .Stop ()
458+
459+ require .GreaterOrEqual (t , mockHandler .RecoverCallCount (), 1 )
460+ require .Equal (t , 1 , mockDB .SetStatusCallCount (), "expected exactly one SetStatus call for the orphan promotion" )
461+
462+ _ , gotTxID , gotStatus , gotMsg := mockDB .SetStatusArgsForCall (0 )
463+ assert .Equal (t , "txNoSuchTx" , gotTxID )
464+ assert .Equal (t , storage .Orphan , gotStatus , "orphan path must promote to storage.Orphan, not storage.Deleted" )
465+ assert .Contains (t , gotMsg , "tx never reached ledger" )
466+ }
467+
411468func TestDefaultConfig (t * testing.T ) {
412469 config := recovery2 .DefaultConfig ()
413470
0 commit comments