@@ -6,35 +6,30 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
66
77const {
88 mockCalculateCost,
9- mockCheckActorUsageLimits ,
10- mockCheckAndBillOverageThreshold ,
9+ mockCheckAttributedUsageLimits ,
10+ mockCheckAndBillPayerOverageThreshold ,
1111 mockGenerateEmbeddings,
1212 mockGetBoundWorkspaceFileSecretProvenanceByMetadata,
1313 mockGetFileMetadataByKeys,
1414 mockProcessDocument,
1515 mockRecordUsage,
1616} = vi . hoisted ( ( ) => ( {
1717 mockCalculateCost : vi . fn ( ) ,
18- mockCheckActorUsageLimits : vi . fn ( ) ,
19- mockCheckAndBillOverageThreshold : vi . fn ( ) ,
18+ mockCheckAttributedUsageLimits : vi . fn ( ) ,
19+ mockCheckAndBillPayerOverageThreshold : vi . fn ( ) ,
2020 mockGenerateEmbeddings : vi . fn ( ) ,
2121 mockGetBoundWorkspaceFileSecretProvenanceByMetadata : vi . fn ( ) ,
2222 mockGetFileMetadataByKeys : vi . fn ( ) ,
2323 mockProcessDocument : vi . fn ( ) ,
2424 mockRecordUsage : vi . fn ( ) ,
2525} ) )
2626
27- vi . mock ( '@/lib/billing/calculations/usage-monitor' , ( ) => ( {
28- checkActorUsageLimits : mockCheckActorUsageLimits ,
29- } ) )
30-
3127vi . mock ( '@/lib/billing/core/usage-log' , ( ) => ( {
3228 recordUsage : mockRecordUsage ,
3329} ) )
3430
3531vi . mock ( '@/lib/billing/threshold-billing' , ( ) => ( {
36- checkAndBillOverageThreshold : mockCheckAndBillOverageThreshold ,
37- checkAndBillPayerOverageThreshold : vi . fn ( ) ,
32+ checkAndBillPayerOverageThreshold : mockCheckAndBillPayerOverageThreshold ,
3833} ) )
3934
4035vi . mock ( '@/lib/knowledge/documents/document-processor' , ( ) => ( {
@@ -69,11 +64,15 @@ vi.mock('@/providers/utils', () => ({
6964 calculateCost : mockCalculateCost ,
7065} ) )
7166
67+ import * as billingAttribution from '@/lib/billing/core/billing-attribution'
7268import * as embeddingClient from '@/lib/embeddings/client'
7369import { processDocumentAsync } from '@/lib/knowledge/documents/service'
7470
7571const mockEmbeddingCapacity = vi . fn < typeof embeddingClient . assertKnowledgeEmbeddingCapacity > ( )
7672beforeEach ( ( ) => {
73+ vi . spyOn ( billingAttribution , 'checkAttributedUsageLimits' ) . mockImplementation (
74+ mockCheckAttributedUsageLimits
75+ )
7776 mockEmbeddingCapacity . mockReset ( ) . mockResolvedValue ( undefined )
7877 vi . spyOn ( embeddingClient , 'assertKnowledgeEmbeddingCapacity' ) . mockImplementation (
7978 mockEmbeddingCapacity
@@ -86,13 +85,26 @@ const PERSISTED_KEY = 'workspace/workspace-1/persisted.pdf'
8685const PERSISTED_URL = `/api/files/serve/${ encodeURIComponent ( PERSISTED_KEY ) } ?context=workspace`
8786const CONTENT_UPDATED_AT = new Date ( '2026-08-05T12:00:00.000Z' )
8887
88+ const BILLING_ATTRIBUTION : billingAttribution . BillingAttributionSnapshot = {
89+ actorUserId : 'uploader-1' ,
90+ workspaceId : 'workspace-1' ,
91+ organizationId : null ,
92+ billedAccountUserId : 'workspace-owner' ,
93+ billingEntity : { type : 'user' , id : 'workspace-owner' } ,
94+ billingPeriod : {
95+ start : '2026-08-01T00:00:00.000Z' ,
96+ end : '2026-09-01T00:00:00.000Z' ,
97+ source : 'default' ,
98+ } ,
99+ payerSubscription : null ,
100+ }
101+
89102const PERSISTED_CONTEXT = {
90- workspaceId : null ,
91- knowledgeBaseUserId : 'knowledge-owner' ,
103+ workspaceId : 'workspace-1' ,
104+ organizationId : null ,
92105 chunkingConfig : null ,
93106 embeddingModel : 'text-embedding-3-small' ,
94107 embeddingDimension : 1536 ,
95- billedAccountUserId : null ,
96108 uploadedBy : 'uploader-1' ,
97109 filename : 'persisted.pdf' ,
98110 fileUrl : PERSISTED_URL ,
@@ -194,7 +206,7 @@ describe('knowledge document indexing usage', () => {
194206 // The processing claim is guarded and returns the row it claimed; without a
195207 // stub every worker would read as 'already completed' and return early.
196208 dbChainMockFns . returning . mockResolvedValue ( [ { id : 'document-1' } ] )
197- mockCheckActorUsageLimits . mockResolvedValue ( { isExceeded : false } )
209+ mockCheckAttributedUsageLimits . mockResolvedValue ( { isExceeded : false } )
198210 mockGetFileMetadataByKeys . mockImplementation ( async ( _keys : string [ ] , context : string ) =>
199211 context === 'workspace' ? [ SOURCE_BINDING ] : [ ]
200212 )
@@ -233,7 +245,7 @@ describe('knowledge document indexing usage', () => {
233245 DOCUMENT_ID ,
234246 DOC_DATA ,
235247 { } ,
236- undefined ,
248+ BILLING_ATTRIBUTION ,
237249 'timeout-pass'
238250 )
239251 const rejected = expect ( pending ) . rejects . toThrow ( 'Document processing timed out' )
@@ -263,7 +275,7 @@ describe('knowledge document indexing usage', () => {
263275 DOCUMENT_ID ,
264276 DOC_DATA ,
265277 { } ,
266- undefined ,
278+ BILLING_ATTRIBUTION ,
267279 'timeout-pass'
268280 )
269281 const rejected = expect ( pending ) . rejects . toThrow ( 'Document processing timed out' )
@@ -278,22 +290,54 @@ describe('knowledge document indexing usage', () => {
278290
279291 it ( 'records one embedding charge per indexing pass' , async ( ) => {
280292 armDocumentReads ( )
281- await processDocumentAsync ( KNOWLEDGE_BASE_ID , DOCUMENT_ID , DOC_DATA , { } , undefined , 'pass-1' )
293+ await processDocumentAsync (
294+ KNOWLEDGE_BASE_ID ,
295+ DOCUMENT_ID ,
296+ DOC_DATA ,
297+ { } ,
298+ BILLING_ATTRIBUTION ,
299+ 'pass-1'
300+ )
282301
283302 expect ( mockRecordUsage ) . toHaveBeenCalledTimes ( 1 )
284303 expect ( recordedSourceReference ( 0 ) ) . toBe ( `knowledge-document:${ DOCUMENT_ID } :pass-1` )
304+ expect ( mockCheckAttributedUsageLimits ) . toHaveBeenCalledWith ( BILLING_ATTRIBUTION )
305+ expect ( mockRecordUsage ) . toHaveBeenCalledWith (
306+ expect . objectContaining ( {
307+ userId : BILLING_ATTRIBUTION . actorUserId ,
308+ workspaceId : BILLING_ATTRIBUTION . workspaceId ,
309+ billingEntity : BILLING_ATTRIBUTION . billingEntity ,
310+ } )
311+ )
312+ expect ( mockCheckAndBillPayerOverageThreshold ) . toHaveBeenCalledWith (
313+ BILLING_ATTRIBUTION . billingEntity
314+ )
285315 } )
286316
287317 it ( 'reuses the same usage source reference across attempts of one indexing pass' , async ( ) => {
288318 const nowSpy = vi . spyOn ( Date , 'now' )
289319
290320 nowSpy . mockReturnValue ( 1_000 )
291321 armDocumentReads ( )
292- await processDocumentAsync ( KNOWLEDGE_BASE_ID , DOCUMENT_ID , DOC_DATA , { } , undefined , 'pass-1' )
322+ await processDocumentAsync (
323+ KNOWLEDGE_BASE_ID ,
324+ DOCUMENT_ID ,
325+ DOC_DATA ,
326+ { } ,
327+ BILLING_ATTRIBUTION ,
328+ 'pass-1'
329+ )
293330
294331 nowSpy . mockReturnValue ( 9_000 )
295332 armDocumentReads ( )
296- await processDocumentAsync ( KNOWLEDGE_BASE_ID , DOCUMENT_ID , DOC_DATA , { } , undefined , 'pass-1' )
333+ await processDocumentAsync (
334+ KNOWLEDGE_BASE_ID ,
335+ DOCUMENT_ID ,
336+ DOC_DATA ,
337+ { } ,
338+ BILLING_ATTRIBUTION ,
339+ 'pass-1'
340+ )
297341
298342 nowSpy . mockRestore ( )
299343
@@ -303,10 +347,24 @@ describe('knowledge document indexing usage', () => {
303347
304348 it ( 'uses a distinct usage source reference for a genuinely new indexing pass' , async ( ) => {
305349 armDocumentReads ( )
306- await processDocumentAsync ( KNOWLEDGE_BASE_ID , DOCUMENT_ID , DOC_DATA , { } , undefined , 'pass-1' )
350+ await processDocumentAsync (
351+ KNOWLEDGE_BASE_ID ,
352+ DOCUMENT_ID ,
353+ DOC_DATA ,
354+ { } ,
355+ BILLING_ATTRIBUTION ,
356+ 'pass-1'
357+ )
307358
308359 armDocumentReads ( )
309- await processDocumentAsync ( KNOWLEDGE_BASE_ID , DOCUMENT_ID , DOC_DATA , { } , undefined , 'pass-2' )
360+ await processDocumentAsync (
361+ KNOWLEDGE_BASE_ID ,
362+ DOCUMENT_ID ,
363+ DOC_DATA ,
364+ { } ,
365+ BILLING_ATTRIBUTION ,
366+ 'pass-2'
367+ )
310368
311369 expect ( mockRecordUsage ) . toHaveBeenCalledTimes ( 2 )
312370 expect ( recordedSourceReference ( 1 ) ) . not . toBe ( recordedSourceReference ( 0 ) )
@@ -317,11 +375,11 @@ describe('knowledge document indexing usage', () => {
317375
318376 nowSpy . mockReturnValue ( 1_000 )
319377 armDocumentReads ( )
320- await processDocumentAsync ( KNOWLEDGE_BASE_ID , DOCUMENT_ID , DOC_DATA , { } )
378+ await processDocumentAsync ( KNOWLEDGE_BASE_ID , DOCUMENT_ID , DOC_DATA , { } , BILLING_ATTRIBUTION )
321379
322380 nowSpy . mockReturnValue ( 9_000 )
323381 armDocumentReads ( )
324- await processDocumentAsync ( KNOWLEDGE_BASE_ID , DOCUMENT_ID , DOC_DATA , { } )
382+ await processDocumentAsync ( KNOWLEDGE_BASE_ID , DOCUMENT_ID , DOC_DATA , { } , BILLING_ATTRIBUTION )
325383
326384 nowSpy . mockRestore ( )
327385
@@ -333,7 +391,7 @@ describe('knowledge document indexing usage', () => {
333391
334392 it ( 're-bills the fallback reference when the embedding model changes' , async ( ) => {
335393 armDocumentReads ( )
336- await processDocumentAsync ( KNOWLEDGE_BASE_ID , DOCUMENT_ID , DOC_DATA , { } )
394+ await processDocumentAsync ( KNOWLEDGE_BASE_ID , DOCUMENT_ID , DOC_DATA , { } , BILLING_ATTRIBUTION )
337395
338396 mockGenerateEmbeddings . mockResolvedValue ( {
339397 embeddings : [ [ 0.3 , 0.4 ] ] ,
@@ -342,7 +400,7 @@ describe('knowledge document indexing usage', () => {
342400 pricingId : 'text-embedding-3-large' ,
343401 } )
344402 armDocumentReads ( )
345- await processDocumentAsync ( KNOWLEDGE_BASE_ID , DOCUMENT_ID , DOC_DATA , { } )
403+ await processDocumentAsync ( KNOWLEDGE_BASE_ID , DOCUMENT_ID , DOC_DATA , { } , BILLING_ATTRIBUTION )
346404
347405 expect ( recordedSourceReference ( 1 ) ) . not . toBe ( recordedSourceReference ( 0 ) )
348406 } )
0 commit comments