Skip to content

Commit d8a1cdf

Browse files
committed
Make final billing best-effort
1 parent 753896a commit d8a1cdf

File tree

2 files changed

+41
-5
lines changed

2 files changed

+41
-5
lines changed

apps/api/src/routes/sandboxes.test.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { QuotaService } from '../services/quota.js'
2222
import { createInMemoryQuotaApi } from '../services/quota.memory.js'
2323
import { BillingService } from '../services/billing.js'
2424
import { createInMemoryBillingApi } from '../services/billing.memory.js'
25+
import type { BillingApi } from '../services/billing.js'
2526
import { AuditLog } from '../services/audit-log.js'
2627
import { createInMemoryAuditLog } from '../services/audit-log.memory.js'
2728
import { NodeRepo } from '../services/node-repo.js'
@@ -38,7 +39,7 @@ import type { NodeClientApi } from '../services/node-client.js'
3839
const TEST_ORG = 'org_test_123'
3940
const TEST_USER = 'user_test_456'
4041

41-
function createTestEnv(overrides?: { nodeClient?: NodeClientApi }) {
42+
function createTestEnv(overrides?: { nodeClient?: NodeClientApi; billingApi?: BillingApi }) {
4243
const sandboxRepo = createInMemorySandboxRepo()
4344
const execRepo = createInMemoryExecRepo()
4445
const sessionRepo = createInMemorySessionRepo()
@@ -47,7 +48,7 @@ function createTestEnv(overrides?: { nodeClient?: NodeClientApi }) {
4748
const redis = createInMemoryRedisApi()
4849
const artifactRepo = createInMemoryArtifactRepo()
4950
const quotaApi = createInMemoryQuotaApi()
50-
const billingApi = createInMemoryBillingApi()
51+
const billingApi = overrides?.billingApi ?? createInMemoryBillingApi()
5152
const auditLog = createInMemoryAuditLog()
5253

5354
const TestLayer = AppLive.pipe(
@@ -560,6 +561,36 @@ describe.skipIf(!RUN_API_INTEGRATION_TESTS)('POST /v1/sandboxes — node daemon
560561
})
561562
})
562563

564+
describe.skipIf(!RUN_API_INTEGRATION_TESTS)('POST /v1/sandboxes/:id/stop', () => {
565+
test('returns 202 even if final billing metering fails', async () => {
566+
const baseBillingApi = createInMemoryBillingApi()
567+
const env = createTestEnv({
568+
billingApi: {
569+
...baseBillingApi,
570+
getBillingTier: () => Effect.die(new Error('billing unavailable')),
571+
trackCompute: () => Effect.die(new Error('billing unavailable')),
572+
},
573+
})
574+
575+
const sandboxId = await createRunningSandbox(env)
576+
577+
const result = await env.runTest(
578+
Effect.gen(function* () {
579+
const client = yield* HttpClient.HttpClient
580+
const response = yield* client.execute(
581+
HttpClientRequest.post(`/v1/sandboxes/${sandboxId}/stop`),
582+
)
583+
const body = yield* response.json
584+
return { status: response.status, body: body as { sandbox_id: string; status: string } }
585+
}),
586+
)
587+
588+
expect(result.status).toBe(202)
589+
expect(result.body.sandbox_id).toBe(sandboxId)
590+
expect(result.body.status).toBe('stopping')
591+
})
592+
})
593+
563594
// ---------------------------------------------------------------------------
564595
// Quota enforcement — create sandbox
565596
// ---------------------------------------------------------------------------

apps/api/src/routes/sandboxes.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,14 @@ const finalizeSandboxStop = (
375375
actorId: string,
376376
) =>
377377
Effect.gen(function* () {
378+
const repo = yield* SandboxRepo
379+
const row = yield* repo.findById(idBytes, orgId)
380+
381+
if (row) {
382+
// Final credit meter before termination (best-effort).
383+
yield* meterSandbox(row, new Date()).pipe(Effect.catchAll(() => Effect.void))
384+
}
385+
378386
// Collect artifacts before fully stopping (best-effort)
379387
yield* collectArtifactsOnStop(sandboxId, idBytes, orgId)
380388

@@ -448,9 +456,6 @@ const stopSandbox = Effect.gen(function* () {
448456
return HttpServerResponse.unsafeJson(response, { status: 202 })
449457
}
450458

451-
// Final credit meter before termination (best-effort)
452-
yield* meterSandbox(row, new Date()).pipe(Effect.catchAll(() => Effect.void))
453-
454459
// Transition to stopping
455460
yield* repo.updateStatus(idBytes, auth.orgId, 'stopping', {
456461
failureReason: 'sandbox_stopped',

0 commit comments

Comments
 (0)