|
1 | 1 | /** |
2 | 2 | * @vitest-environment node |
3 | 3 | */ |
| 4 | +import { readFileSync } from 'node:fs' |
4 | 5 | import { createMockRequest, resetEnvFlagsMock, setEnvFlags } from '@sim/testing' |
5 | 6 | import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' |
6 | 7 |
|
@@ -80,6 +81,16 @@ vi.mock('@/lib/billing/threshold-billing', () => ({ |
80 | 81 | })) |
81 | 82 |
|
82 | 83 | import { billingUpdateCostBodySchema } from '@/lib/api/contracts/subscription' |
| 84 | +import { |
| 85 | + BillingCallbackBody, |
| 86 | + BillingCallbackHeaders, |
| 87 | + BillingProtocol, |
| 88 | + BillingProtocolHeaders, |
| 89 | +} from '@/lib/mothership/generated/billing' |
| 90 | +import { |
| 91 | + BILLING_PROTOCOL_HEADERS, |
| 92 | + COPILOT_BILLING_PROTOCOL, |
| 93 | +} from '@/lib/mothership/generated/billing-protocol-v1' |
83 | 94 | import { POST } from '@/app/api/billing/update-cost/route' |
84 | 95 |
|
85 | 96 | afterAll(resetEnvFlagsMock) |
@@ -167,6 +178,45 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => { |
167 | 178 | }) |
168 | 179 | }) |
169 | 180 |
|
| 181 | + it('keeps the worker callback protocol aligned with the Go-produced wire constants', () => { |
| 182 | + expect(Object.values(BillingProtocol).sort()).toEqual( |
| 183 | + Object.values(COPILOT_BILLING_PROTOCOL).sort() |
| 184 | + ) |
| 185 | + expect(BillingProtocolHeaders).toEqual(BILLING_PROTOCOL_HEADERS) |
| 186 | + }) |
| 187 | + |
| 188 | + it.skipIf(!process.env.BILLING_WIRE_FIXTURE)( |
| 189 | + 'accepts the actual worker HTTP callbacks through the Sim handler', |
| 190 | + async () => { |
| 191 | + const path = process.env.BILLING_WIRE_FIXTURE |
| 192 | + if (!path) throw new Error('Missing worker callback fixture') |
| 193 | + const receipts: { body: unknown; headers: Record<string, string> }[] = JSON.parse( |
| 194 | + readFileSync(path, 'utf8') |
| 195 | + ) |
| 196 | + expect(receipts.length).toBeGreaterThan(5) |
| 197 | + setEnvFlags({ isBillingEnabled: true, isHosted: true }) |
| 198 | + for (const receipt of receipts) { |
| 199 | + const body = BillingCallbackBody.parse(receipt.body) |
| 200 | + BillingCallbackHeaders.parse(receipt.headers) |
| 201 | + mockRequireBillingAttributionHeader.mockReturnValue({ |
| 202 | + ...ATTRIBUTION, |
| 203 | + workspaceId: body.workspaceId, |
| 204 | + }) |
| 205 | + const result = await POST(createMockRequest('POST', body, receipt.headers)) |
| 206 | + expect(result.status, JSON.stringify(await result.clone().json())).toBe(200) |
| 207 | + expect(mockRecordCumulativeUsage).toHaveBeenLastCalledWith( |
| 208 | + expect.objectContaining({ |
| 209 | + userId: body.userId, |
| 210 | + cost: body.cost, |
| 211 | + model: body.model, |
| 212 | + eventKey: `update-cost:${body.idempotencyKey}`, |
| 213 | + metadata: { inputTokens: body.inputTokens, outputTokens: body.outputTokens }, |
| 214 | + }) |
| 215 | + ) |
| 216 | + } |
| 217 | + } |
| 218 | + ) |
| 219 | + |
170 | 220 | it('returns 401 for a billing-disabled request without valid internal auth', async () => { |
171 | 221 | setEnvFlags({ isBillingEnabled: false }) |
172 | 222 | mockCheckInternalApiKey.mockReturnValue({ success: false, error: 'Invalid internal API key' }) |
|
0 commit comments