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:
co-authored by
Claude Opus 4.6
parent
2c0323d5fb
commit
4d8b18a58a
@@ -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}` }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user