Files
moeka-project/apps/server/scripts/e2e-llm-router.ts
T
RainbowBird c627bce9c9 refactor(server): split services into domain/adapter layers, drop dead code
Why
- src/services/ was an unordered mix of single-file services and module
  directories with no shared classification axis, plus several long-dead
  admin batch helpers that survived the move to the simpler synchronous
  admin-flux-grants flow.

What
- services/ now has two top-level layers:
    domain/   — DB state + business rules (billing, characters, chats,
                flux, flux-transaction, llm-router, providers, request-log,
                stripe, user-deletion, admin/{flux-grants,router-config})
    adapters/ — thin wrappers over external SDKs / infra (config-kv, email,
                posthog, tts/)
- admin/* moved under domain/admin/ with consistent plural names
  (flux-grants, router-config).
- tts-adapters/ collapsed to adapters/tts/ (no redundant -adapters suffix
  once nested under adapters/).
- 63 src files + scripts/e2e-llm-router.ts + tests/verifications/_harness.ts
  had relative imports rewritten; git mv preserves blame.
- apps/server/CLAUDE.md and docs/ai-context/*.md updated to match new paths.

Dead code removed
- services/admin-flux-grant-batches/ (service + worker + tests, 1090 LOC) —
  superseded by admin-flux-grants and never wired into app.ts.
- routes/admin/flux-grant-batches/ — same.
- utils/redis-compressed.ts + test — zero production call sites.
- llm-router/index.ts re-exports trimmed from 26 to 6; only symbols with
  external consumers are kept.

Intentionally kept
- schemas/flux-grant-batch.ts and its schemas/index.ts export remain so the
  drizzle-kit generate diff stays empty. Removing them is a separate PR
  that owns the drop-table migration for flux_grant_batch /
  flux_grant_batch_recipient.

Verification
- pnpm -F @proj-airi/server typecheck: passes.
- pnpm exec eslint apps/server: 49 errors, identical to main baseline
  (all are pre-existing node/prefer-global/buffer in envelope-crypto and
  scripts/e2e-llm-router; untouched by this change).
- Vitest passes per-file; the 6 mockDB hook timeouts under full-parallel
  run are the known pushSchema-per-worker infra cost, not a regression.
2026-05-18 23:36:45 +08:00

133 lines
4.3 KiB
TypeScript

#!/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.log(` 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.log(` auth = ${auth ? '<set>' : '<none>'}`)
}
if (init?.body) {
console.log(` 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.log(` ← ${res.status} body: ${text.slice(0, 300)}`)
}
return res
}
const router = createLlmRouterService({
configKV,
envelopeCrypto: envelope,
gatewayMetrics: null,
fetchImpl: debugFetch,
})
console.log('→ 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.log(`← 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.log()
console.log('Assistant response:')
console.log(` model: ${payload.model ?? '<unknown>'}`)
console.log(` text: ${JSON.stringify(content)}`)
console.log(` 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.log()
console.log('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)
})