Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions apps/sim/lib/logs/execution/logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,28 @@ import type { SerializableExecutionState } from '@/executor/execution/types'

afterAll(resetDbChainMock)

/** Flat logger whose withMetadata() children share one spy set, so log level is assertable. */
const { mockLogger } = vi.hoisted(() => {
const mockLogger: Record<string, ReturnType<typeof vi.fn>> = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
trace: vi.fn(),
fatal: vi.fn(),
}
mockLogger.child = vi.fn(() => mockLogger)
mockLogger.withMetadata = vi.fn(() => mockLogger)
return { mockLogger }
})

vi.mock('@sim/logger', () => ({
createLogger: vi.fn(() => mockLogger),
logger: mockLogger,
runWithRequestContext: vi.fn(<T>(_ctx: unknown, fn: () => T): T => fn()),
getRequestContext: vi.fn(() => undefined),
}))

// Mock billing modules
vi.mock('@/lib/billing/core/subscription', () => ({
getHighestPriorityPersonalSubscription: vi.fn(() => Promise.resolve(null)),
Expand Down Expand Up @@ -1013,6 +1035,36 @@ describe('recordExecutionUsage boundary-delta reconciliation', () => {
expect(recordUsage).not.toHaveBeenCalled()
})

const unbilledErrorCalls = () =>
mockLogger.error.mock.calls.filter((call) =>
String(call[0]).includes('Failed to record execution usage to usage_log ledger')
)

test('a structurally zero-cost run without billing context logs no unbilled-charge error', async () => {
mockDb([])

const recorded = await logger.recordExecutionUsage(
'workflow-1',
costSummary({ baseExecutionCharge: 0 }),
'api',
'exec-1',
'user-1'
)

expect(recorded).toBe(0)
expect(recordUsage).not.toHaveBeenCalled()
expect(unbilledErrorCalls()).toHaveLength(0)
})

test('a genuine ledger write failure still logs the unbilled-charge error', async () => {
vi.mocked(recordUsage).mockRejectedValueOnce(new Error('ledger insert failed'))

const recorded = await run(costSummary(), [])

expect(recorded).toBe(0)
expect(unbilledErrorCalls()).toHaveLength(1)
})

test('retry with everything already billed records nothing (idempotent)', async () => {
await run(
costSummary({
Expand Down
16 changes: 10 additions & 6 deletions apps/sim/lib/logs/execution/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1547,12 +1547,6 @@ export class ExecutionLogger implements IExecutionLoggerService {
return 0
}

if (workflowRecord.workspaceId && !billingContext) {
throw new Error('Billing attribution is required for workspace execution usage')
}
const resolvedBillingContext =
billingContext ?? deriveBillingContext(userId, await getHighestPrioritySubscription(userId))

// Build the run's *cumulative* target ledger lines from the cost summary.
// The usage_log is then reconciled to these targets: at each completion
// boundary (pause or terminal) we record only the increment versus what
Expand Down Expand Up @@ -1615,11 +1609,21 @@ export class ExecutionLogger implements IExecutionLoggerService {
}
}

// Bail before requiring billing attribution: a run with no billable target
// (e.g. a preprocessing-gated run that never executed) writes no ledger row
// either way, so demanding attribution here would raise a lost-revenue
// error for a charge that does not exist.
if (targets.length === 0) {
statsLog.debug('No cost to record')
return 0
}

if (workflowRecord.workspaceId && !billingContext) {
throw new Error('Billing attribution is required for workspace execution usage')
}
const resolvedBillingContext =
billingContext ?? deriveBillingContext(userId, await getHighestPrioritySubscription(userId))

// Matches the billedBefore key resolution (toFixed(8)): a delta below this
// is finer than the idempotency key can distinguish across boundaries, so
// ignoring it keeps the key and the gate consistent.
Expand Down