@@ -510,8 +510,8 @@ func TestTriggerWorkflowIdempotency_ReclaimDoesNotUpdateLastDeniedAt(t *testing.
510510 assert .False (t , lastDeniedAt .Valid )
511511}
512512
513- func TestTriggerWorkflowIdempotency_MixedKeysAllowsPartial (t * testing.T ) {
514- // Mixed batch: one reclaimable, one running. We should allow the reclaimable run and skip the duplicate .
513+ func TestTriggerWorkflowIdempotency_MixedKeysRejectsPartialByDefault (t * testing.T ) {
514+ // Public trigger paths should remain atomic: if any key is still duplicate, reject the whole batch .
515515 pool , cleanup := setupPostgresWithMigration (t )
516516 defer cleanup ()
517517
@@ -562,6 +562,77 @@ func TestTriggerWorkflowIdempotency_MixedKeysAllowsPartial(t *testing.T) {
562562 IdempotencyKey : & keyRunning ,
563563 }
564564
565+ _ , _ , err = repo .Triggers ().TriggerFromWorkflowNames (ctx , tenantId , []* WorkflowNameTriggerOpts {terminalSecond , runningSecond })
566+ require .Error (t , err )
567+ assert .ErrorIs (t , err , ErrIdempotencyKeyAlreadyClaimed )
568+
569+ claimedTerminal := getClaimedByExternalId (t , pool , tenantId , keyTerminal )
570+ require .NotNil (t , claimedTerminal )
571+ assert .Equal (t , terminalFirst .ExternalId , * claimedTerminal )
572+
573+ claimedRunning := getClaimedByExternalId (t , pool , tenantId , keyRunning )
574+ require .NotNil (t , claimedRunning )
575+ assert .Equal (t , runningFirst .ExternalId , * claimedRunning )
576+
577+ lastDeniedAt := getLastDeniedAt (t , pool , tenantId , keyRunning )
578+ assert .True (t , lastDeniedAt .Valid )
579+ assert .WithinDuration (t , time .Now (), lastDeniedAt .Time , 2 * time .Minute )
580+ }
581+
582+ func TestTriggerWorkflowIdempotency_MixedKeysAllowsPartialWhenEnabled (t * testing.T ) {
583+ // Internal queued trigger processing may skip duplicates while still creating non-duplicate runs.
584+ pool , cleanup := setupPostgresWithMigration (t )
585+ defer cleanup ()
586+
587+ repo , cleanupRepo := setupRepositoryWithTTL (t , pool , 30 * time .Minute , 1 * time .Minute )
588+ defer cleanupRepo ()
589+
590+ tenantId := setupTenant (t , repo , pool )
591+ wf := createMinimalWorkflow (t , pool , tenantId , "test-workflow" )
592+
593+ ctx := context .Background ()
594+ keyTerminal := IdempotencyKey ("idem-key-terminal-allow-partial" )
595+ keyRunning := IdempotencyKey ("idem-key-running-allow-partial" )
596+
597+ terminalFirst := & WorkflowNameTriggerOpts {
598+ TriggerTaskData : & TriggerTaskData {
599+ WorkflowName : "test-workflow" ,
600+ },
601+ ExternalId : uuid .New (),
602+ IdempotencyKey : & keyTerminal ,
603+ }
604+ _ , _ , err := repo .Triggers ().TriggerFromWorkflowNames (ctx , tenantId , []* WorkflowNameTriggerOpts {terminalFirst })
605+ require .NoError (t , err )
606+ insertRunStatus (t , pool , tenantId , wf , terminalFirst .ExternalId , sqlcv1 .V1ReadableStatusOlapFAILED )
607+
608+ runningFirst := & WorkflowNameTriggerOpts {
609+ TriggerTaskData : & TriggerTaskData {
610+ WorkflowName : "test-workflow" ,
611+ },
612+ ExternalId : uuid .New (),
613+ IdempotencyKey : & keyRunning ,
614+ }
615+ _ , _ , err = repo .Triggers ().TriggerFromWorkflowNames (ctx , tenantId , []* WorkflowNameTriggerOpts {runningFirst })
616+ require .NoError (t , err )
617+ insertRunStatus (t , pool , tenantId , wf , runningFirst .ExternalId , sqlcv1 .V1ReadableStatusOlapRUNNING )
618+
619+ terminalSecond := & WorkflowNameTriggerOpts {
620+ TriggerTaskData : & TriggerTaskData {
621+ WorkflowName : "test-workflow" ,
622+ },
623+ ExternalId : uuid .New (),
624+ IdempotencyKey : & keyTerminal ,
625+ AllowPartialIdempotency : true ,
626+ }
627+ runningSecond := & WorkflowNameTriggerOpts {
628+ TriggerTaskData : & TriggerTaskData {
629+ WorkflowName : "test-workflow" ,
630+ },
631+ ExternalId : uuid .New (),
632+ IdempotencyKey : & keyRunning ,
633+ AllowPartialIdempotency : true ,
634+ }
635+
565636 _ , _ , err = repo .Triggers ().TriggerFromWorkflowNames (ctx , tenantId , []* WorkflowNameTriggerOpts {terminalSecond , runningSecond })
566637 require .NoError (t , err )
567638
0 commit comments