11/**
22 * @vitest -environment node
33 */
4- import { resetEnvFlagsMock , setEnvFlags } from '@sim/testing'
4+ import {
5+ dbChainMockFns ,
6+ queueTableRows ,
7+ resetDbChainMock ,
8+ resetEnvFlagsMock ,
9+ schemaMock ,
10+ setEnvFlags ,
11+ } from '@sim/testing'
512import { afterAll , beforeAll , beforeEach , describe , expect , it , vi } from 'vitest'
13+ import { TableRowNotFoundError } from '@/lib/table/rows/errors'
614import type {
715 RowExecutionMetadata ,
816 TableDefinition ,
@@ -15,11 +23,25 @@ const {
1523 mockResolveSystemBillingAttribution,
1624 mockRunsCancel,
1725 mockRunsList,
26+ mockGetJobQueue,
27+ mockGetTableById,
28+ mockListActiveDispatches,
29+ mockMarkActiveDispatchesCancelled,
30+ mockQueueCancelByKey,
31+ mockQueueCancelJob,
32+ mockUpdateRow,
1833} = vi . hoisted ( ( ) => ( {
1934 mockResolveBillingAttribution : vi . fn ( ) ,
2035 mockResolveSystemBillingAttribution : vi . fn ( ) ,
2136 mockRunsCancel : vi . fn ( ) ,
2237 mockRunsList : vi . fn ( ) ,
38+ mockGetJobQueue : vi . fn ( ) ,
39+ mockGetTableById : vi . fn ( ) ,
40+ mockListActiveDispatches : vi . fn ( ) ,
41+ mockMarkActiveDispatchesCancelled : vi . fn ( ) ,
42+ mockQueueCancelByKey : vi . fn ( ) ,
43+ mockQueueCancelJob : vi . fn ( ) ,
44+ mockUpdateRow : vi . fn ( ) ,
2345} ) )
2446
2547const SYSTEM_BILLING_ATTRIBUTION = {
@@ -48,15 +70,40 @@ vi.mock('@trigger.dev/sdk', () => ({
4870 } ,
4971} ) )
5072
73+ vi . mock ( '@/lib/core/async-jobs/config' , ( ) => ( {
74+ getJobQueue : mockGetJobQueue ,
75+ } ) )
76+
77+ vi . mock ( '@/lib/table/dispatcher' , ( ) => ( {
78+ listActiveDispatches : mockListActiveDispatches ,
79+ markActiveDispatchesCancelled : mockMarkActiveDispatchesCancelled ,
80+ } ) )
81+
82+ vi . mock ( '@/lib/table/rows/service' , ( ) => ( {
83+ updateRow : mockUpdateRow ,
84+ } ) )
85+
86+ vi . mock ( '@/lib/table/service' , ( ) => ( {
87+ getTableById : mockGetTableById ,
88+ } ) )
89+
5190import {
5291 buildEnqueueItems ,
5392 cancelCellRunsByTags ,
93+ cancelWorkflowGroupRuns ,
5494 pickNextEligibleGroupForRow ,
5595 type WorkflowGroupCellPayload ,
5696} from '@/lib/table/workflow-columns'
5797
5898beforeEach ( ( ) => {
5999 vi . clearAllMocks ( )
100+ resetDbChainMock ( )
101+ mockGetJobQueue . mockResolvedValue ( {
102+ cancelByKey : mockQueueCancelByKey ,
103+ cancelJob : mockQueueCancelJob ,
104+ } )
105+ mockListActiveDispatches . mockResolvedValue ( [ ] )
106+ mockMarkActiveDispatchesCancelled . mockResolvedValue ( [ ] )
60107 mockResolveBillingAttribution . mockImplementation (
61108 ( { actorUserId, workspaceId } : { actorUserId : string ; workspaceId : string } ) =>
62109 Promise . resolve ( {
@@ -271,3 +318,69 @@ describe('cancelCellRunsByTags', () => {
271318 )
272319 } )
273320} )
321+
322+ describe ( 'cancelWorkflowGroupRuns deletion races' , ( ) => {
323+ const group = makeGroup ( { id : 'g1' } )
324+ const table = makeTable ( [ group ] )
325+ const inFlightExecution = {
326+ tableId : table . id ,
327+ rowId : 'row1' ,
328+ groupId : group . id ,
329+ status : 'running' ,
330+ executionId : 'execution-1' ,
331+ jobId : null ,
332+ workflowId : group . workflowId ,
333+ error : null ,
334+ runningBlockIds : [ ] ,
335+ blockErrors : { } ,
336+ cancelledAt : null ,
337+ }
338+
339+ beforeEach ( ( ) => {
340+ setEnvFlags ( { isTriggerDevEnabled : false , isBillingEnabled : true } )
341+ mockGetTableById . mockResolvedValue ( table )
342+ } )
343+
344+ it ( 'ignores a row deleted after its in-flight execution was selected' , async ( ) => {
345+ queueTableRows ( schemaMock . tableRowExecutions , [ inFlightExecution ] )
346+ mockUpdateRow . mockRejectedValueOnce ( new TableRowNotFoundError ( ) )
347+
348+ await expect ( cancelWorkflowGroupRuns ( table . id ) ) . resolves . toBe ( 1 )
349+ expect ( mockUpdateRow ) . toHaveBeenCalledOnce ( )
350+ } )
351+
352+ it ( 'rethrows unrelated cancellation write failures' , async ( ) => {
353+ const error = new Error ( 'database unavailable' )
354+ queueTableRows ( schemaMock . tableRowExecutions , [ inFlightExecution ] )
355+ mockUpdateRow . mockRejectedValueOnce ( error )
356+
357+ await expect ( cancelWorkflowGroupRuns ( table . id ) ) . rejects . toBe ( error )
358+ } )
359+
360+ it ( 'ignores a tombstone foreign-key failure caused by a deleted row' , async ( ) => {
361+ mockListActiveDispatches . mockResolvedValueOnce ( [
362+ { id : 'dispatch-1' , scope : { groupIds : [ group . id ] , rowIds : [ 'row1' ] } } ,
363+ ] )
364+ const cause = Object . assign ( new Error ( 'foreign key violation' ) , {
365+ code : '23503' ,
366+ constraint_name : 'table_row_executions_row_id_user_table_rows_id_fk' ,
367+ } )
368+ dbChainMockFns . onConflictDoNothing . mockRejectedValueOnce ( new Error ( 'Failed query' , { cause } ) )
369+
370+ await expect ( cancelWorkflowGroupRuns ( table . id , 'row1' ) ) . resolves . toBe ( 0 )
371+ } )
372+
373+ it ( 'rethrows tombstone failures from any other constraint' , async ( ) => {
374+ mockListActiveDispatches . mockResolvedValueOnce ( [
375+ { id : 'dispatch-1' , scope : { groupIds : [ group . id ] , rowIds : [ 'row1' ] } } ,
376+ ] )
377+ const cause = Object . assign ( new Error ( 'foreign key violation' ) , {
378+ code : '23503' ,
379+ constraint_name : 'table_row_executions_table_id_user_table_definitions_id_fk' ,
380+ } )
381+ const error = new Error ( 'Failed query' , { cause } )
382+ dbChainMockFns . onConflictDoNothing . mockRejectedValueOnce ( error )
383+
384+ await expect ( cancelWorkflowGroupRuns ( table . id , 'row1' ) ) . rejects . toBe ( error )
385+ } )
386+ } )
0 commit comments