feat: v4 — TUI fight loop, rate limiting, webhook tooling, expanded choreographies

- Fight loop CLI with TUI renderer (ink-style terminal UI)
- Rate limiting middleware for API routes
- Queue cooldowns wired into orchestrator after fights
- Webhook test utility for bot debugging
- API docs route
- Expanded FightScene choreographies and weapon props
- Fix Drizzle transaction execution in orchestrator
- Schema additions, scoring/challenge/mock expansions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 00:14:46 +00:00
co-authored by Claude Opus 4.6
parent 2c0323d5fb
commit 4d8b18a58a
24 changed files with 2973 additions and 721 deletions
+72
View File
@@ -0,0 +1,72 @@
import { isAllowedWebhookUrl } from './orchestrator.js'
export interface WebhookTestResult {
reachable: boolean
validResponse: boolean
latencyMs: number
error?: string
}
export async function testWebhook(webhookUrl: string): Promise<WebhookTestResult> {
if (!isAllowedWebhookUrl(webhookUrl)) {
return { reachable: false, validResponse: false, latencyMs: 0, error: 'URL blocked: private/internal addresses are not allowed.' }
}
const testPayload = JSON.stringify({
fight_id: 'test_000000',
round: 0,
type: 'webhook_test',
challenge: 'WEBHOOK TEST: respond with {"answer": "pong"} to verify your setup.',
constraints: { timeout_ms: 5000, max_tokens: 500 },
opponent: { name: 'test_bot', wins: 0, losses: 0 },
arena: 'localhost',
arena_modifier: null,
})
const start = Date.now()
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000)
const res = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: testPayload,
signal: controller.signal,
})
clearTimeout(timeout)
const latencyMs = Date.now() - start
if (!res.ok) {
return { reachable: true, validResponse: false, latencyMs, error: `Webhook returned HTTP ${res.status}. Expected 200.` }
}
const text = await res.text()
if (text.length > 10240) {
return { reachable: true, validResponse: false, latencyMs, error: 'Response too large (>10KB).' }
}
let data: Record<string, unknown>
try {
data = JSON.parse(text)
} catch {
return { reachable: true, validResponse: false, latencyMs, error: 'Response is not valid JSON. Expected {"answer": "..."}.' }
}
if (typeof data.answer !== 'string') {
return { reachable: true, validResponse: false, latencyMs, error: 'Response JSON missing "answer" field. Expected {"answer": "pong"}.' }
}
return { reachable: true, validResponse: true, latencyMs }
} catch (err: unknown) {
const latencyMs = Date.now() - start
const isAbort = err instanceof Error && err.name === 'AbortError'
if (isAbort) {
return { reachable: false, validResponse: false, latencyMs, error: 'Webhook timed out (5s). Is your server running?' }
}
const msg = err instanceof Error ? err.message : String(err)
return { reachable: false, validResponse: false, latencyMs, error: `Connection failed: ${msg}` }
}
}