@@ -9,17 +9,27 @@ import {
99import type { ExecutionContext } from '@/executor/types'
1010import type { SerializedBlock } from '@/serializer/types'
1111
12- const { mockExecutorExecute, mockCreateSnapshot } = vi . hoisted ( ( ) => ( {
13- mockExecutorExecute : vi . fn ( ) ,
14- mockCreateSnapshot : vi . fn ( ) ,
15- } ) )
12+ const { mockExecutorExecute, mockCreateSnapshot, mockResolveBillingAttribution, executorOptions } =
13+ vi . hoisted ( ( ) => ( {
14+ mockExecutorExecute : vi . fn ( ) ,
15+ mockCreateSnapshot : vi . fn ( ) ,
16+ mockResolveBillingAttribution : vi . fn ( ) ,
17+ executorOptions : [ ] as Array < Record < string , any > > ,
18+ } ) )
1619
1720vi . mock ( '@/executor' , ( ) => ( {
1821 Executor : class {
22+ constructor ( options : Record < string , any > ) {
23+ executorOptions . push ( options )
24+ }
1925 execute = mockExecutorExecute
2026 } ,
2127} ) )
2228
29+ vi . mock ( '@/lib/billing/core/billing-attribution' , ( ) => ( {
30+ resolveBillingAttribution : mockResolveBillingAttribution ,
31+ } ) )
32+
2333vi . mock ( '@/lib/logs/execution/snapshot/service' , ( ) => ( {
2434 snapshotService : { createSnapshotWithDeduplication : mockCreateSnapshot } ,
2535} ) )
@@ -92,6 +102,7 @@ describe('WorkflowBlockHandler', () => {
92102
93103 // Reset all mocks
94104 vi . clearAllMocks ( )
105+ executorOptions . length = 0
95106
96107 // Setup default fetch mock
97108 mockFetch . mockResolvedValue ( {
@@ -273,6 +284,43 @@ describe('WorkflowBlockHandler', () => {
273284 expect ( mockExecutorExecute ) . toHaveBeenCalledWith ( 'child-workflow-id' )
274285 } )
275286
287+ it ( 'threads the parent billing attribution into the child execution context' , async ( ) => {
288+ const billingAttribution = {
289+ actorUserId : 'actor-1' ,
290+ workspaceId : 'workspace-parent' ,
291+ organizationId : 'org-1' ,
292+ billedAccountUserId : 'owner-1' ,
293+ billingEntity : { type : 'organization' , id : 'org-1' } ,
294+ billingPeriod : { start : '2026-07-01T00:00:00.000Z' , end : '2026-08-01T00:00:00.000Z' } ,
295+ payerSubscription : null ,
296+ }
297+ const ctx = {
298+ ...mockContext ,
299+ workspaceId : 'workspace-parent' ,
300+ metadata : { ...mockContext . metadata , billingAttribution } ,
301+ } as ExecutionContext
302+
303+ mockFetch . mockResolvedValueOnce ( {
304+ ok : true ,
305+ json : ( ) =>
306+ Promise . resolve ( {
307+ data : {
308+ name : 'Child Workflow' ,
309+ workspaceId : 'workspace-parent' ,
310+ state : { blocks : { } , edges : [ ] , loops : { } , parallels : { } } ,
311+ } ,
312+ } ) ,
313+ } )
314+ mockCreateSnapshot . mockResolvedValue ( { snapshot : { id : 'snapshot-1' } } )
315+ mockExecutorExecute . mockResolvedValue ( { success : true , output : { data : 'ok' } } )
316+
317+ await handler . execute ( ctx , mockBlock , inputs )
318+
319+ expect ( executorOptions ) . toHaveLength ( 1 )
320+ expect ( executorOptions [ 0 ] . contextExtensions . billingAttribution ) . toBe ( billingAttribution )
321+ expect ( mockResolveBillingAttribution ) . not . toHaveBeenCalled ( )
322+ } )
323+
276324 it ( 'should fail closed when the executing context has no workspace' , async ( ) => {
277325 mockFetch . mockResolvedValueOnce ( {
278326 ok : true ,
0 commit comments