@@ -3,6 +3,10 @@ import { createSessionPrincipal } from '@sim/testing/factories/principal.factory
33import { jsonResponse } from '@sim/testing/helpers/http'
44import { apiKeyByokMock , apiKeyByokMockFns } from '@sim/testing/mocks/api-key-byok.mock'
55import { authInternalMock , authInternalMockFns } from '@sim/testing/mocks/auth-internal.mock'
6+ import {
7+ billingAttributionMock ,
8+ billingAttributionMockFns ,
9+ } from '@sim/testing/mocks/billing-attribution.mock'
610import { billingUsageLogMock } from '@sim/testing/mocks/billing-usage-log.mock'
711import {
812 executorPrincipalMock ,
@@ -179,6 +183,8 @@ vi.mock('@/lib/billing/core/chat-access', () => ({
179183 payerHasPaidAccess : mockPayerHasPaidAccess ,
180184} ) )
181185
186+ vi . mock ( '@/lib/billing/core/attribution' , ( ) => billingAttributionMock )
187+
182188vi . mock ( '@/lib/uploads/contexts/workspace/file-manager' , ( ) => workspaceFileManagerMock )
183189
184190vi . mock (
@@ -448,6 +454,7 @@ import { createToolConfig, getTool } from '@/tools/utils'
448454import { getToolAsync } from '@/tools/utils.server'
449455
450456const mockGetBYOKKey = apiKeyByokMockFns . mockGetBYOKKey
457+ const mockResolveWorkspaceBillingPayer = billingAttributionMockFns . mockResolveWorkspaceBillingPayer
451458const mockGenerateInternalToken = authInternalMockFns . mockGenerateInternalToken
452459const mockCreateExecutorPrincipalFromExecutionContext =
453460 executorPrincipalMockFns . mockCreateExecutorPrincipalFromExecutionContext
@@ -6619,7 +6626,9 @@ describe('Hosted Key Injection', () => {
66196626
66206627 expect ( result . success ) . toBe ( false )
66216628 expect ( result . error ) . toContain ( 'available on paid plans' )
6622- expect ( mockPayerHasPaidAccess ) . toHaveBeenCalledWith ( TEST_BILLING_ATTRIBUTION )
6629+ expect ( mockPayerHasPaidAccess ) . toHaveBeenCalledWith ( TEST_BILLING_ATTRIBUTION , {
6630+ onBalanceUnavailable : 'deny' ,
6631+ } )
66236632 expect ( mockRateLimiterFns . acquireKey ) . not . toHaveBeenCalled ( )
66246633 expect ( request ) . not . toHaveBeenCalled ( )
66256634 } )
@@ -6635,6 +6644,60 @@ describe('Hosted Key Injection', () => {
66356644 expect ( request ) . toHaveBeenCalledTimes ( 1 )
66366645 } )
66376646
6647+ async function runPaidOnlyToolWithoutAttribution ( ) {
6648+ mockGetBYOKKey . mockResolvedValue ( null )
6649+ mockValidateUrlWithDNS . mockResolvedValue ( { isValid : true , resolvedIP : '93.184.216.34' } )
6650+ mockSecureFetchWithPinnedIP . mockResolvedValue ( {
6651+ ok : true ,
6652+ status : 200 ,
6653+ statusText : 'OK' ,
6654+ headers : { get : ( ) => null , toRecord : ( ) => ( { } ) } ,
6655+ json : async ( ) => ( { ok : true } ) ,
6656+ text : async ( ) => '{"ok":true}' ,
6657+ } )
6658+ const result = await executeTool ( PAID_ONLY_TOOL_ID , {
6659+ _context : { workspaceId : 'workspace-without-run' , userId : 'user-1' } ,
6660+ } )
6661+ return { result, request : mockSecureFetchWithPinnedIP }
6662+ }
6663+
6664+ it ( 'judges the workspace payer when the run carries no attribution' , async ( ) => {
6665+ mockResolveWorkspaceBillingPayer . mockResolvedValue ( {
6666+ billedAccountUserId : 'payer-1' ,
6667+ organizationId : 'org-1' ,
6668+ payerSubscription : { plan : 'team' } ,
6669+ } )
6670+ mockPayerHasPaidAccess . mockResolvedValue ( true )
6671+
6672+ const { result, request } = await runPaidOnlyToolWithoutAttribution ( )
6673+
6674+ expect ( result . success ) . toBe ( true )
6675+ expect ( mockResolveWorkspaceBillingPayer ) . toHaveBeenCalledWith ( 'workspace-without-run' , {
6676+ onMissing : 'return-null' ,
6677+ } )
6678+ expect ( mockPayerHasPaidAccess ) . toHaveBeenCalledWith (
6679+ {
6680+ billingEntity : { type : 'organization' , id : 'org-1' } ,
6681+ payerSubscription : { plan : 'team' } ,
6682+ } ,
6683+ { onBalanceUnavailable : 'deny' }
6684+ )
6685+ expect ( request ) . toHaveBeenCalledTimes ( 1 )
6686+ } )
6687+
6688+ it ( 'refuses the hosted key when the workspace has no payer' , async ( ) => {
6689+ mockResolveWorkspaceBillingPayer . mockResolvedValue ( null )
6690+ mockPayerHasPaidAccess . mockResolvedValue ( true )
6691+
6692+ const { result, request } = await runPaidOnlyToolWithoutAttribution ( )
6693+
6694+ expect ( result . success ) . toBe ( false )
6695+ expect ( result . error ) . toContain ( 'available on paid plans' )
6696+ expect ( mockPayerHasPaidAccess ) . not . toHaveBeenCalled ( )
6697+ expect ( mockRateLimiterFns . acquireKey ) . not . toHaveBeenCalled ( )
6698+ expect ( request ) . not . toHaveBeenCalled ( )
6699+ } )
6700+
66386701 it ( "runs a free payer's own workspace key without consulting the plan" , async ( ) => {
66396702 mockGetBYOKKey . mockResolvedValue ( { apiKey : 'workspace-own-key' } )
66406703 mockPayerHasPaidAccess . mockResolvedValue ( false )
0 commit comments