- 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>
73 lines
2.4 KiB
TypeScript
73 lines
2.4 KiB
TypeScript
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}` }
|
|
}
|
|
}
|