chore(server): cleanup unused scripts

This commit is contained in:
RainbowBird
2026-08-02 17:34:31 +08:00
parent 61e5470801
commit 9aa27af108
5 changed files with 0 additions and 779 deletions
-132
View File
@@ -1,132 +0,0 @@
#!/usr/bin/env tsx
/**
* End-to-end test for U1-U7: hits a real OpenRouter API via the router
* service to prove envelope decrypt + config load + key rotation + upstream
* fetch all work together.
*
* Use when:
* - Verifying the gateway end-to-end after a fresh seed, without going
* through the HTTP auth chain.
*
* Expects:
* - `.env.local` provides REDIS_URL, LLM_ROUTER_MASTER_KEY.
* - `LLM_ROUTER_CONFIG` already seeded via
* `POST /api/admin/config/router` (see
* `docs/ai-context/verifications/llm-router.md` for the curl invocation).
*
* Returns: exit 0 with the assistant response printed; exit 1 on failure.
*/
import { env, exit } from 'node:process'
import Redis from 'ioredis'
import { parseEnv } from '../src/libs/env'
import { createConfigKVService } from '../src/services/adapters/config-kv'
import { createLlmRouterService } from '../src/services/domain/llm-router'
import { createEnvelopeCrypto } from '../src/utils/envelope-crypto'
async function main() {
const parsedEnv = parseEnv(env)
if (!parsedEnv.LLM_ROUTER_MASTER_KEY) {
console.error('error: LLM_ROUTER_MASTER_KEY env var is required')
exit(1)
}
const redis = new Redis(parsedEnv.REDIS_URL)
const configKV = createConfigKVService(redis)
const envelope = createEnvelopeCrypto({
masterKey: parsedEnv.LLM_ROUTER_MASTER_KEY,
previousMasterKey: parsedEnv.LLM_ROUTER_MASTER_KEY_PREVIOUS,
})
// Debug wrapper: log every upstream request + response so we can see what
// the router is actually sending when E2E fails. Remove after E2E passes.
const debugFetch: typeof fetch = async (input, init) => {
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
console.info(` fetch → POST ${url}`)
if (init?.headers) {
const hdrs = init.headers as Record<string, string>
const auth = hdrs.authorization || hdrs.Authorization
// NOTICE:
// Never log credential substrings: a 30-char prefix of an OpenRouter
// key (`sk-or-v1-bb1a38505a7309...`) is enough to identify the account.
// Print presence only. Source: codex review 2026-05-15 #10.
console.info(` auth = ${auth ? '<set>' : '<none>'}`)
}
if (init?.body) {
console.info(` body = ${String(init.body).slice(0, 200)}`)
}
const res = await fetch(input as any, init as any)
if (!res.ok) {
const clone = res.clone()
const text = await clone.text().catch(() => '<unreadable>')
console.info(`${res.status} body: ${text.slice(0, 300)}`)
}
return res
}
const router = createLlmRouterService({
configKV,
envelopeCrypto: envelope,
gatewayMetrics: null,
fetchImpl: debugFetch,
})
console.info('→ calling router.route() with model=chat-default')
const start = Date.now()
let response: Response
try {
response = await router.route({
modelName: 'chat-default',
body: {
messages: [
{ role: 'user', content: 'Say "hello world" in exactly 3 words, no period.' },
],
max_tokens: 20,
},
headers: {},
})
}
catch (err) {
console.error('router.route threw:', err)
await redis.quit()
exit(1)
}
const elapsed = Date.now() - start
console.info(`← status ${response.status} (${elapsed}ms)`)
if (!response.ok) {
const text = await response.text()
console.error('upstream non-2xx body:', text.slice(0, 500))
await redis.quit()
exit(1)
}
const payload = await response.json() as {
choices?: Array<{ message?: { content?: string } }>
usage?: { prompt_tokens?: number, completion_tokens?: number }
model?: string
}
const content = payload.choices?.[0]?.message?.content
console.info()
console.info('Assistant response:')
console.info(` model: ${payload.model ?? '<unknown>'}`)
console.info(` text: ${JSON.stringify(content)}`)
console.info(` tokens: prompt=${payload.usage?.prompt_tokens ?? '?'} completion=${payload.usage?.completion_tokens ?? '?'}`)
if (!content) {
console.error('error: response.choices[0].message.content was empty')
await redis.quit()
exit(1)
}
console.info()
console.info('E2E PASS — router service successfully called OpenRouter and returned a usable response.')
await redis.quit()
}
main().catch((err) => {
console.error('e2e failed:', err)
exit(1)
})
-260
View File
@@ -1,260 +0,0 @@
import type { Database } from '../../src/libs/db'
import { Buffer } from 'node:buffer'
import { vi } from 'vitest'
import { buildApp } from '../../src/app'
import { mockDB } from '../../src/libs/mock-db'
import { createAdminFluxGrantsService } from '../../src/services/domain/admin/flux-grants'
import { createBillingService } from '../../src/services/domain/billing/billing-service'
import { createFluxService } from '../../src/services/domain/flux'
import { createUserDeletionService } from '../../src/services/domain/user-deletion'
import { userFluxRedisKey } from '../../src/utils/redis-keys'
import * as schema from '../../src/schemas'
// NOTICE:
// drizzle-kit's `pushSchema` (called by `mockDB`) takes ~500ms per invocation.
// Vitest spawns a fresh worker per test file, so a module-level promise scopes
// the cache correctly: schema push runs once per file, every
// `startVerificationContext()` after that reuses the in-memory PGlite and
// only truncates rows. See `docs/ai/context/verification-automation.md` for
// the broader rationale on test boot cost.
let sharedDbPromise: Promise<Database> | null = null
async function getSharedDb(): Promise<Database> {
sharedDbPromise ??= mockDB(schema)
return sharedDbPromise
}
async function resetDataRows(db: Database): Promise<void> {
// Delete in FK-safe order. better-auth's session / account / verification
// tables reference user with `onDelete: cascade`, so deleting `user` last
// implicitly clears them — we still call them out for clarity and so a
// future test that seeds sessions directly does not silently leak rows.
await db.delete(schema.fluxTransaction)
await db.delete(schema.userFlux)
await db.delete(schema.session)
await db.delete(schema.account)
await db.delete(schema.user)
}
interface SeedUserOptions {
id: string
email?: string
balance: number
}
interface SessionUser {
id: string
email: string
emailVerified?: boolean
name?: string
/** better-auth `admin` plugin role. Set `'admin'` to pass `adminGuard`. */
role?: string | null
}
export type Harness = Awaited<ReturnType<typeof startVerificationContext>>
/**
* Boots a Hono app with the same wiring as production for a verification
* scenario.
*
* Use when:
* - You need to assert a full user path (HTTP -> route -> service -> DB / ledger)
* rather than a unit-level code path
* - You want real `createFluxService` + `createBillingService` against a real
* in-memory Postgres (PGlite), with auth / OIDC / WebSocket / OTel stubbed
*
* Expects:
* - No external network. The mock router never opens sockets so
* pre-flight-rejecting cases never reach it; tests that actually need an
* upstream LLM response must stub `fetch` themselves
*
* Returns:
* - A `Harness` value with the mounted app, drizzle handle, and helpers to
* set a session user, seed flux balance, override config keys, and inspect
* the in-memory Redis store
*/
export async function startVerificationContext() {
const db = await getSharedDb()
await resetDataRows(db)
let activeSession: { user: any, session: any } | null = null
const auth: any = {
api: {
getSession: vi.fn(async () => activeSession),
getOAuthServerConfig: vi.fn(async () => ({})),
getOpenIdConfig: vi.fn(async () => ({})),
},
handler: vi.fn(async () => new Response('not-found', { status: 404 })),
}
const configStore: Record<string, any> = {
FLUX_PER_REQUEST: 1,
INITIAL_USER_FLUX: 0,
AUTH_RATE_LIMIT_MAX: 1000,
AUTH_RATE_LIMIT_WINDOW_SEC: 60,
FLUX_PER_1K_CHARS_TTS: 2,
TTS_DEBT_TTL_SECONDS: 86400,
}
const configKV: any = {
get: vi.fn(async (key: string) => configStore[key]),
getOrThrow: vi.fn(async (key: string) => {
if (configStore[key] === undefined)
throw new Error(`Config key "${key}" is not set`)
return configStore[key]
}),
getOptional: vi.fn(async (key: string) => (configStore[key] ?? null)),
set: vi.fn(async (key: string, value: any) => {
configStore[key] = value
}),
}
const redisStore = new Map<string, string>()
const redisSubscriber = {
on: vi.fn(),
subscribe: vi.fn(async () => 1),
unsubscribe: vi.fn(async () => 0),
quit: vi.fn(async () => 'OK'),
}
const redis: any = {
get: vi.fn(async (key: string) => redisStore.get(key) ?? null),
getBuffer: vi.fn(async (key: string) => {
const v = redisStore.get(key)
return v ? Buffer.from(v, 'utf8') : null
}),
set: vi.fn(async (key: string, value: any) => {
redisStore.set(key, String(value))
return 'OK'
}),
del: vi.fn(async (key: string) => (redisStore.delete(key) ? 1 : 0)),
incrby: vi.fn(async (key: string, by: number) => {
const next = (Number.parseInt(redisStore.get(key) ?? '0', 10) || 0) + by
redisStore.set(key, String(next))
return next
}),
expire: vi.fn(async () => 1),
duplicate: vi.fn(() => redisSubscriber),
publish: vi.fn(async () => 0),
}
const fluxService = createFluxService(db, redis, configKV)
const billingService = createBillingService(db, redis, configKV)
const adminFluxGrantsService = createAdminFluxGrantsService({ db, billingService })
// NOTICE:
// Production wires 5 soft-delete handlers (stripe / flux / providers /
// characters / chats). The harness only wires `flux` so the verification
// for "balance soft-deleted + ledger preserved" can run without dragging
// in stripe SDK, character / chat / provider services. Tests covering the
// other 4 handlers should opt in via a future option flag rather than
// widening the default wiring.
const userDeletionService = createUserDeletionService()
userDeletionService.register({
name: 'flux',
priority: 20,
softDelete: ({ userId }) => fluxService.deleteAllForUser(userId),
})
// NOTICE:
// The Proxy returns a fresh vi.fn() for every property access. Stand-in for
// services this verification doesn't touch (chat, characters, providers,
// stripe, admin-flux-grants, user-deletion, ttsMeter). If a test exercises
// one of these and starts getting `undefined is not a function` errors,
// wire in a real instance instead of widening this stub.
const stub: any = new Proxy({}, { get: () => vi.fn(async () => undefined) })
const env: any = {
API_SERVER_URL: 'http://localhost:3000',
OTEL_SERVICE_NAME: 'airi-server-test',
ADDITIONAL_TRUSTED_ORIGINS: '',
HOST: '127.0.0.1',
PORT: 0,
}
const { app } = await buildApp({
auth,
db,
characterService: stub,
chatService: stub,
providerService: stub,
fluxService,
fluxTransactionService: stub,
stripeService: stub,
billingService,
adminFluxGrantsService,
adminUsersService: stub,
ttsMeter: stub,
requestLogService: { logRequest: vi.fn(async () => undefined) } as any,
configKV,
redis,
env,
otel: null,
userDeletionService,
llmRouter: {
route: vi.fn(async () => new Response('{}', { status: 200 })),
invalidateConfig: vi.fn(),
} as any,
})
return {
app,
db,
schema,
redisStore,
configStore,
userDeletionService,
fluxService,
setSessionUser(user: SessionUser | null) {
activeSession = user
? {
user: {
id: user.id,
email: user.email,
name: user.name ?? user.id,
emailVerified: user.emailVerified ?? true,
role: user.role ?? null,
banned: false,
banExpires: null,
createdAt: new Date(),
updatedAt: new Date(),
},
session: {
id: `sess-${user.id}`,
userId: user.id,
token: `tok-${user.id}`,
createdAt: new Date(),
updatedAt: new Date(),
expiresAt: new Date(Date.now() + 3600_000),
ipAddress: null,
userAgent: null,
},
}
: null
},
async seedUser(opts: SeedUserOptions) {
await db.insert(schema.user).values({
id: opts.id,
name: opts.id,
email: opts.email ?? `${opts.id}@example.com`,
emailVerified: true,
}).onConflictDoNothing()
await db.insert(schema.userFlux).values({
userId: opts.id,
flux: opts.balance,
}).onConflictDoNothing()
// NOTICE:
// Prime the Redis cache so `fluxService.getFlux()` reads the seeded
// balance directly instead of touching the DB-init path (which would
// create an `initial` flux_transaction row and skew ledger assertions).
redisStore.set(userFluxRedisKey(opts.id), String(opts.balance))
},
setConfig(kv: Record<string, any>) {
Object.assign(configStore, kv)
},
}
}
@@ -1,116 +0,0 @@
// Verification: docs/ai/context/verification-automation.md
// Source doc: apps/server/docs/ai-context/verifications/account-deletion.md
//
// Covers one slice of the verification doc — the part that pins down
// "Flux balance soft-deleted, cache invalidated, ledger preserved".
//
// What this test does NOT cover (intentional, out of harness scope):
// - The Better Auth `/api/auth/delete-user/callback` HTTP entry point. The
// prod trigger lives inside better-auth's plugin internals; testing it
// end-to-end would mean mounting the real `createAuth(...)` against
// better-auth's verification-token flow, which the harness does not do.
// We call `userDeletionService.softDeleteAll(...)` directly — the same
// function better-auth's `beforeDelete` hook calls — so the orchestration
// contract is exercised even though the HTTP boundary is skipped.
// - The stripe / providers / characters / chats handlers. The harness wires
// only the `flux` handler (see `_harness.ts`). Verifying the other four
// needs widening the default wiring or a per-test opt-in.
import type { Harness } from './_harness'
import { eq } from 'drizzle-orm'
import { afterAll, beforeEach, describe, expect, it } from 'vitest'
import { userFluxRedisKey } from '../../src/utils/redis-keys'
import { startVerificationContext } from './_harness'
describe('verification: account-deletion (flux slice)', () => {
let ctx: Harness
beforeEach(async () => {
ctx = await startVerificationContext()
})
afterAll(async () => {
// PGlite is module-cached and lives for the worker's lifetime.
})
it('soft-deletes user_flux, invalidates redis cache, leaves flux_transaction ledger intact', async () => {
const userId = 'user-doomed'
// Seed: balance 500 + one historical debit ledger row. The ledger row is
// the load-bearing assertion — flux_transaction has no `deleted_at`
// column on purpose, and the bare userId (no FK) is the mechanism that
// lets the ledger outlive the user row. See `schemas/flux-transaction.ts`
// NOTICE comment.
await ctx.seedUser({ id: userId, balance: 500 })
await ctx.db.insert(ctx.schema.fluxTransaction).values({
userId,
type: 'debit',
amount: 50,
balanceBefore: 550,
balanceAfter: 500,
description: 'prior llm call',
})
// Sanity precondition: balance is 500 and ledger has 1 row.
const balanceBefore = await ctx.fluxService.getFlux(userId)
expect(balanceBefore.flux).toBe(500)
expect(ctx.redisStore.get(userFluxRedisKey(userId))).toBe('500')
const ledgerBefore = await ctx.db.query.fluxTransaction.findMany({
where: eq(ctx.schema.fluxTransaction.userId, userId),
})
expect(ledgerBefore).toHaveLength(1)
// Act: same call better-auth makes inside the delete-user callback hook.
await ctx.userDeletionService.softDeleteAll({ userId, reason: 'user-requested' })
// Assert 1: user_flux row is stamped with deletedAt (soft-delete).
const rawRow = await ctx.db
.select()
.from(ctx.schema.userFlux)
.where(eq(ctx.schema.userFlux.userId, userId))
expect(rawRow).toHaveLength(1)
expect(rawRow[0].deletedAt).not.toBeNull()
expect(rawRow[0].flux).toBe(500) // balance value is NOT zeroed; soft-delete only stamps deletedAt
// Assert 2: redis cache for the user's balance is dropped.
expect(ctx.redisStore.get(userFluxRedisKey(userId))).toBeUndefined()
// Assert 3: ledger rows persist. The audit trail outlives the user row.
const ledgerAfter = await ctx.db.query.fluxTransaction.findMany({
where: eq(ctx.schema.fluxTransaction.userId, userId),
})
expect(ledgerAfter).toHaveLength(1)
expect(ledgerAfter[0].description).toBe('prior llm call')
// Assert 4: `fluxService.getFlux` rejects after soft-delete. The read
// path filters on `deletedAt IS NULL`, the init-path's
// `onConflictDoNothing` no-ops against the soft-deleted row, and the
// re-read still finds nothing — so the service throws. In production
// this scenario should not arise because better-auth hard-deletes the
// user row after `softDeleteAll`. The throw is the defense-in-depth
// failure mode described in the `fluxService` NOTICE for any request
// that bypasses `sessionMiddleware` with a soft-deleted user.
await expect(ctx.fluxService.getFlux(userId)).rejects.toThrow(/Failed to initialize flux/)
})
it('softDeleteAll is idempotent — calling it twice does not throw and leaves state unchanged', async () => {
// The handler uses `WHERE deletedAt IS NULL` so the second call finds 0
// rows and is a no-op. `redis.del` is also a no-op when the key is
// absent. Pinning this prevents a future refactor that tightens the
// where-clause and accidentally requires the row to be live.
const userId = 'user-doomed-twice'
await ctx.seedUser({ id: userId, balance: 100 })
await ctx.userDeletionService.softDeleteAll({ userId, reason: 'user-requested' })
await ctx.userDeletionService.softDeleteAll({ userId, reason: 'user-requested' })
const rawRow = await ctx.db
.select()
.from(ctx.schema.userFlux)
.where(eq(ctx.schema.userFlux.userId, userId))
expect(rawRow).toHaveLength(1)
expect(rawRow[0].deletedAt).not.toBeNull()
})
})
@@ -1,154 +0,0 @@
// Verification: docs/ai/context/verification-automation.md
// Source doc: apps/server/docs/ai-context/verifications/admin-flux-grants.md
//
// Covers the three user paths from the verification doc:
// Path 1: admin POST /api/admin/flux-grants → 200 + granted array, ledger row written
// Path 2: ?dryRun=true → preview returned, ledger unchanged
// Path 3: adminGuard rejects (401 no session / 403 non-admin role)
import type { Harness } from './_harness'
import { eq } from 'drizzle-orm'
import { afterAll, beforeEach, describe, expect, it } from 'vitest'
import { startVerificationContext } from './_harness'
const ADMIN_EMAIL = 'admin@example.com'
describe('verification: admin-flux-grants', () => {
let ctx: Harness
beforeEach(async () => {
ctx = await startVerificationContext()
ctx.setConfig({ INITIAL_USER_FLUX: 0 })
// Admin user (caller). Admin access is role-based (`role === 'admin'`).
await ctx.seedUser({ id: 'admin-1', email: ADMIN_EMAIL, balance: 0 })
})
afterAll(async () => {
// PGlite is per-context and per-test — letting it drop out of scope is enough.
})
describe('path 1: admin synchronously grants flux', () => {
it('credits 100 flux to each existing recipient and returns the per-email outcome buckets', async () => {
await ctx.seedUser({ id: 'recipient-1', email: 'rec1@example.com', balance: 0 })
await ctx.seedUser({ id: 'recipient-2', email: 'rec2@example.com', balance: 25 })
ctx.setSessionUser({ id: 'admin-1', email: ADMIN_EMAIL, role: 'admin' })
const res = await ctx.app.request('/api/admin/flux-grants', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
description: 'integration-test grant',
amount: 100,
emails: ['rec1@example.com', 'rec2@example.com'],
}),
})
expect(res.status).toBe(200)
const body = await res.json() as {
summary: { willGrant: number, totalFluxToIssue: number }
result: {
granted: { email: string, userId: string, fluxTransactionId: string, balanceAfter: number }[]
skipped: unknown[]
failed: unknown[]
}
}
expect(body.summary.willGrant).toBe(2)
expect(body.summary.totalFluxToIssue).toBe(200)
expect(body.result.failed).toEqual([])
expect(body.result.skipped).toEqual([])
expect(body.result.granted).toHaveLength(2)
const granted1 = body.result.granted.find(g => g.email === 'rec1@example.com')
const granted2 = body.result.granted.find(g => g.email === 'rec2@example.com')
expect(granted1?.balanceAfter).toBe(100)
expect(granted2?.balanceAfter).toBe(125)
expect(granted1?.fluxTransactionId).toBeTruthy()
// Ledger writes per recipient with type='promo' and the operator id in metadata.
const rec1Ledger = await ctx.db.query.fluxTransaction.findMany({
where: eq(ctx.schema.fluxTransaction.userId, 'recipient-1'),
})
expect(rec1Ledger).toHaveLength(1)
expect(rec1Ledger[0].type).toBe('promo')
expect(rec1Ledger[0].amount).toBe(100)
expect(rec1Ledger[0].balanceBefore).toBe(0)
expect(rec1Ledger[0].balanceAfter).toBe(100)
expect(rec1Ledger[0].description).toBe('integration-test grant')
const meta = rec1Ledger[0].metadata as { issuedByUserId?: string, description?: string }
expect(meta?.issuedByUserId).toBe('admin-1')
})
})
describe('path 2: dry-run preview', () => {
it('reports willGrant / notFound / duplicateInInput without writing a ledger row', async () => {
await ctx.seedUser({ id: 'recipient-1', email: 'rec1@example.com', balance: 0 })
ctx.setSessionUser({ id: 'admin-1', email: ADMIN_EMAIL, role: 'admin' })
const res = await ctx.app.request('/api/admin/flux-grants?dryRun=true', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
description: 'smoke',
amount: 100,
emails: [
'rec1@example.com',
'REC1@example.com', // case-variant duplicate
'ghost@nope.example', // not_found
'rec1@example.com', // exact duplicate
],
}),
})
expect(res.status).toBe(200)
const body = await res.json() as {
preview: {
totalEmails: number
willGrant: number
willSkip: { notFound: number, userDeleted: number, duplicateInInput: number }
totalFluxToIssue: number
}
}
expect(body.preview.totalEmails).toBe(4)
expect(body.preview.willGrant).toBe(1)
expect(body.preview.willSkip.notFound).toBe(1)
expect(body.preview.willSkip.duplicateInInput).toBe(2)
expect(body.preview.totalFluxToIssue).toBe(100)
// Ledger must be untouched.
const ledger = await ctx.db.query.fluxTransaction.findMany({
where: eq(ctx.schema.fluxTransaction.userId, 'recipient-1'),
})
expect(ledger).toEqual([])
})
})
describe('path 3: adminGuard rejects unauthorized callers', () => {
it('returns 401 when no session is attached', async () => {
ctx.setSessionUser(null)
const res = await ctx.app.request('/api/admin/flux-grants', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description: 'x', amount: 1, emails: ['a@b.com'] }),
})
expect(res.status).toBe(401)
})
it('returns 403 when the session user has no admin role', async () => {
await ctx.seedUser({ id: 'normie', email: 'normie@example.com', balance: 0 })
ctx.setSessionUser({ id: 'normie', email: 'normie@example.com', role: 'user' })
const res = await ctx.app.request('/api/admin/flux-grants', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description: 'x', amount: 1, emails: ['a@b.com'] }),
})
expect(res.status).toBe(403)
})
})
})
@@ -1,117 +0,0 @@
// Verification: docs/ai/context/verification-automation.md
// Source doc: apps/server/docs/ai-context/verifications/flux-unbilled-exploit-fix.md
//
// Covers the "user path" section of the verification doc:
// Scenario: user with 0 < balance < fallbackRate fires N concurrent LLM
// completion requests.
// Expected (post-patch): all N are rejected at pre-flight with 402; the
// upstream router is never invoked; flux_transaction ledger gains
// no 'debit' row; user_flux.flux stays at the seeded value.
import type { Harness } from './_harness'
import { and, eq } from 'drizzle-orm'
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'
import { startVerificationContext } from './_harness'
describe('verification: flux-unbilled-exploit-fix', () => {
let ctx: Harness
beforeAll(async () => {
ctx = await startVerificationContext()
})
beforeEach(async () => {
// Clean ledger between cases so balanceBefore / balanceAfter assertions
// don't carry state across tests.
await ctx.db.delete(ctx.schema.fluxTransaction)
await ctx.db.delete(ctx.schema.userFlux)
ctx.redisStore.clear()
})
afterAll(async () => {
// PGlite is in-memory — letting it fall out of scope is enough.
})
it('rejects N concurrent partial-balance requests at pre-flight, never writes a debit ledger row', async () => {
// ROOT CAUSE:
//
// Before commit 7267b0d6b the pre-flight gate read `if (flux.flux <= 0)`,
// so users with `0 < balance < FLUX_PER_REQUEST` could spawn N concurrent
// requests, each pass pre-flight, complete the upstream LLM call, and
// race on a post-billing debit that almost always failed (insufficient
// funds for the requested amount, full-amount rollback). Result: N free
// LLM responses.
//
// After 7267b0d6b: pre-flight rejects when `flux.flux < fallbackRate`
// before the upstream call. This test pins the after-patch behavior so
// a regression that loosens the gate back to `<= 0` will fail here.
const userId = 'user-partial-balance'
const N = 5
ctx.setConfig({ FLUX_PER_REQUEST: 100, FLUX_PER_1K_TOKENS: 50 })
await ctx.seedUser({ id: userId, balance: 5 })
ctx.setSessionUser({ id: userId, email: `${userId}@example.com` })
const responses = await Promise.all(
Array.from({ length: N }, () => ctx.app.request('/api/v1/openai/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'auto', messages: [{ role: 'user', content: 'hi' }] }),
})),
)
// All N must be rejected before the gateway is touched.
expect(responses).toHaveLength(N)
for (const res of responses) {
expect(res.status).toBe(402)
}
const bodies = await Promise.all(responses.map(r => r.json() as Promise<{ error: string, message: string }>))
for (const body of bodies) {
expect(body.error).toBe('PAYMENT_REQUIRED')
expect(body.message).toMatch(/Insufficient flux/i)
}
// Ledger must be untouched. The pre-flight reject path never reaches
// `consumeFluxForLLM`, so no `debit` rows should exist.
const ledger = await ctx.db.query.fluxTransaction.findMany({
where: eq(ctx.schema.fluxTransaction.userId, userId),
})
expect(ledger).toEqual([])
// Balance must be exactly the seeded value. No drain, no rollback.
const fluxRow = await ctx.db.query.userFlux.findFirst({
where: and(
eq(ctx.schema.userFlux.userId, userId),
// Active rows only — deletedAt IS NULL is fluxService's read guard
),
})
expect(fluxRow?.flux).toBe(5)
})
it('allows a request when balance >= fallbackRate (pre-flight pass-through smoke check)', async () => {
// Companion case: with sufficient balance the pre-flight gate must NOT
// block. We don't run the request to completion (no upstream gateway
// available in this harness), only assert that the failure mode here is
// upstream-fetch error (not 402). This protects against a regression
// that flips the comparison and rejects everyone.
const userId = 'user-funded'
ctx.setConfig({ FLUX_PER_REQUEST: 100, FLUX_PER_1K_TOKENS: 50 })
await ctx.seedUser({ id: userId, balance: 1000 })
ctx.setSessionUser({ id: userId, email: `${userId}@example.com` })
const res = await ctx.app.request('/api/v1/openai/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'auto', messages: [{ role: 'user', content: 'hi' }] }),
})
// Pre-flight passed -> route delegated to the mock router. The harness
// wires `llmRouter.route` to a vi.fn that returns 200, so we only assert
// the pre-flight gate did NOT short-circuit with 402.
expect(res.status).not.toBe(402)
})
})