14 integration tests covering full fight lifecycle with real in-memory
DB: SSE event ordering, polling bot challenge/response flow, human
player response submission, and 3 concurrent fights without interference.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8 tests covering: complete fight lifecycle, HP progression, ELO
updates, concurrent fight prevention, round data validity, ELO
conservation, status transitions, and win streak tracking.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critical finding: when all bots answer correctly, even a 50ms speed
advantage wins 95.4% of fights. At 100ms+ gap it's 100% deterministic.
ELO separation reaches 450+ after just 50 fights. Speed completely
dominates the "all correct" meta.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Simulated 1000 fights with 80% accuracy and equal speed. First-to-lead
wins 60.4% of decided fights, confirming combo system is balanced.
No decay or comeback mechanics needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When both bots answer wrong, the one with higher checkAnswer confidence
(closer to correct) gets +1 point advantage. Rewards trying over
timing out. Equally wrong remains a pure draw.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When both bots are correct, the one with higher checkAnswer confidence
(exact match 1.0 vs fuzzy match 0.8) gets up to +1.0 bonus points.
This rewards precise answers over approximate ones, adding another
competitive dimension beyond pure speed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both-correct fights could almost never produce critical hits because
max margin at typical speed differences was ~2.2, far below threshold
of 4. Lowering to 3 yields ~15% critical hit rate (target 10-20%),
making speed differences produce more exciting fight dynamics.
Research findings: simulated 1000 fights with both bots answering
correctly. Critical rate went from 0% to 15.3% with new threshold.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Multi-turn simulation, authority impersonation, encoding tricks
(Base64, ROT13), fake JSON system prompts, red team framing,
reward manipulation, inverted instruction logic. All difficulty: hard.
Zero audit failures after addition.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Test roundToDifficulty: rounds 1-2 easy, 3-4 medium, 5+ hard
- Test pickChallenge difficulty filtering works with round numbers
- Audit prompt difficulty tags across all 16 challenge types
- 8 types lack hard prompts, 3 lack medium prompts
- 82.7% of prompts are untagged (no difficulty attribute)
- Report written to loop/difficulty-distribution.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- checkAnswer now returns highest score across all accepted answers
instead of first match, fixing 95 false-low-confidence results
- Skip string containment for purely numeric strings to prevent
false positives like "1000" matching inside "10000"
- Preserve decimal points in normalize() (42.0 no longer becomes 420)
- Use word-boundary regex for number matching in responses
- Fix 47 wrong choices scoring too high (comma-formatted numbers,
verbose choices matching terse answers)
- Fix 17 prompts where no choice matched any accepted answer
- Challenge audit now reports zero failures across all 1472 prompts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fetchNostrProfile now uses Promise.allSettled to query all relays
concurrently. Aggregates results with latest-created_at-wins strategy
instead of stopping at the first relay that responds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Module re-evaluation during HMR reset autoRestoreRan to false, causing
duplicate auth-restore API calls. Now persists flag on globalThis.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
useNostr (9), useFightCache (5), useOnlineStatus (4) composable tests.
Added fake-indexeddb dev dependency for IDB tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Only invalidates __alltime__ and current season cache keys on fight completion,
preserving historical season caches. Test verifies selective behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Global app.onError handler already in app.ts catches all unhandled route
exceptions. Production mode returns "Internal server error" only.
Tests verify no stack traces or file paths leak in responses.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Both bots timing out every round: verified draw with zero damage
- Perfect victory (10-0): verified correct ELO calculation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
blacklistJwt() adds token to in-memory blacklist until its natural expiry.
verifyJwt() checks blacklist before signature verification.
Cleanup interval removes expired entries every 10 minutes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents finding: no replay protection in NIP-98 verification.
Token replay within 120s window succeeds (mitigated by JWT issuance being idempotent).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests the actual rateLimit middleware with production mode via dynamic import.
Covers: under-limit allows, over-limit returns 429, window reset, per-IP
isolation, and poll endpoint config (30 req/1s window).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Catches runtime errors in child components, displays user-friendly
error message with reload button. Wired into App.vue wrapping
router-view. Tests verify error capture and button rendering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Timeout and WebSocket errors reject with proper Error objects
- Caller catches and falls through to poll-based confirmation
- Preimage undefined check prevents calling confirm with no preimage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feedbackTimer, timerHandle, and pollHandle are all cleared in
onUnmounted. Test confirms cleanup pattern works correctly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
13 silent catch handlers replaced with descriptive console.warn logging
across 6 frontend files. No silent error swallowing remains.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SSE now always attempts reconnection when fight isn't finished,
regardless of isLive.value. Uses exponential backoff (1s, 2s, 4s,
max 8s). Moved sseRetries to outer scope to persist across reconnects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Validates challenge type against the full CHALLENGE_TYPES enum before
processing. Invalid types now return 400 instead of silently falling
back to speed_blitz.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Production now requires JWT_SECRET env var. Added comprehensive JWT
tests: creation, verification, expiry, tampered payload, tampered
signature, and malformed token rejection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added getDecodedToken validation that rejects malformed tokens with
400 before any DB lookups. Tests cover empty, non-base64, truncated,
and random base64 tokens.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add rateLimit(1_000, 30) middleware to GET /poll endpoint
- Fix Challenge type errors in human-responses test files (missing baseDamage)
- Add rate-limit unit test verifying 429 after exceeding limit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
better-sqlite3 driver is synchronous — .get() does NOT need await.
Added tests for unknown pubkey (404) and missing pubkey (400) on join.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Frontend: vitest.config.ts with vue plugin + jsdom, dummy component test
- Server: in-memory SQLite test DB factory + Hono testClient helper + smoke test
- CI: add pnpm audit and server coverage threshold steps
- Root: vitest workspace config for multi-project test discovery
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scoring: both-wrong is now a DRAW — equal scores (3/3), no winner,
symmetric minimal damage. Garbage answers no longer beat reasonable
ones just by being faster. Both-wrong narrations reflect the draw.
Entrance: removed duplicate announceDeepIntro() call from FightViewer
(was already called inside playEntrance). Removed _resetPositions()
after entrance (entrance already places fighters at home positions,
the extra reset caused a visible snap/reset).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>