diablo2-web/scripts/verify-challenger-m1.ts

730 lines
30 KiB
TypeScript

/**
* Challenger & Empirical Stress Testing Suite for Milestone M1 (Requirement R1).
*
* Comprehensive empirical verification against `src/sim/input.ts` and `src/ui/hud-manager.ts`:
* 1. Unit & Boundary Assertions on KeyboardInput (WASD, Arrows, Diagonals, Opposites, Shift, Digits, Space, Talk, Waypoint)
* 2. Differential Fuzzing: 1,000 randomized keyboard states against an independent oracle
* 3. In-Memory HUD Manager State Transitions (Space/Escape dismissal, KeyS/KeyT Skill Tree, KeyW Weapon Swap, 1..4 Potions, Input Element Suppression)
* 4. Headless Chrome CDP In-Engine Live Gameplay Verification (`acts.html?act=1`):
* - KeyW, KeyA, KeyS, KeyD zero player movement verification
* - Real keyboard hotkey panel toggling & dismissal
* - Weapon swap verification
* - Accessibility arrow key navigation
* - Visual screenshot capture
*/
import { spawn } from 'node:child_process'
import { mkdirSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { createServer } from 'vite'
import { KeyboardInput, directionOf, DIRECTION, type Movement } from '../src/sim/input.ts'
import { HudManager } from '../src/ui/hud-manager.ts'
const HANDOFF_DIR = '/usr/local/google/home/taodao/d2w-wg1-main/.agents/teamwork_preview_challenger_m1_1'
const sleep = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
interface TestResult {
name: string
passed: boolean
error?: string
}
const results: TestResult[] = []
function assert(condition: boolean, message: string): void {
if (!condition) {
throw new Error(`Assertion failed: ${message}`)
}
}
function assertClose(a: number, b: number, epsilon = 1e-4, msg = ''): void {
if (Math.abs(a - b) > epsilon) {
throw new Error(`Assertion failed: expected ${a} to be close to ${b} (diff=${Math.abs(a - b)} > ${epsilon}). ${msg}`)
}
}
// ============================================================================
// PART 1: IN-MEMORY KEYBOARDINPUT UNIT & ADVERSARIAL STRESS
// ============================================================================
function runKeyboardInputTests(): void {
console.log('\n--- PART 1: In-Memory KeyboardInput Empirical Verification ---')
const listeners = new Map<string, (e: any) => void>()
const mockWindow = {
addEventListener: (type: string, fn: any) => listeners.set(type, fn),
removeEventListener: (type: string, fn: any) => listeners.delete(type),
}
const input = new KeyboardInput(mockWindow as unknown as Window)
input.attach()
const press = (code: string) => {
let prevented = false
const evt = {
code,
preventDefault: () => { prevented = true },
}
listeners.get('keydown')?.(evt)
return prevented
}
const release = (code: string) => {
listeners.get('keyup')?.({ code })
}
const blur = () => {
listeners.get('blur')?.({})
}
// 1.1 WASD keys produce {x: 0, y: 0} and active === false
console.log(' 1.1 Testing WASD keys individual and combined...')
for (const code of ['KeyW', 'KeyA', 'KeyS', 'KeyD']) {
press(code)
const m = input.movement()
assert(m.x === 0 && m.y === 0, `${code} produced non-zero movement: ${JSON.stringify(m)}`)
assert(input.active === false, `${code} set active to true`)
assert(directionOf(m) === null, `${code} produced direction: ${directionOf(m)}`)
release(code)
}
// Combinations of WASD
press('KeyW')
press('KeyA')
assert(input.movement().x === 0 && input.movement().y === 0, 'WA produced movement')
assert(input.active === false, 'WA produced active: true')
press('KeyS')
press('KeyD')
assert(input.movement().x === 0 && input.movement().y === 0, 'WASD produced movement')
assert(input.active === false, 'WASD produced active: true')
release('KeyW')
release('KeyA')
release('KeyS')
release('KeyD')
// 1.2 Arrow keys produce correct unit vectors and active === true
console.log(' 1.2 Testing Arrow keys unit vectors and active === true...')
// Up
press('ArrowUp')
assert(input.active === true, 'ArrowUp active is false')
assertClose(input.movement().x, 0, 1e-4, 'ArrowUp x')
assertClose(input.movement().y, -1, 1e-4, 'ArrowUp y')
assert(directionOf(input.movement()) === DIRECTION.North, 'ArrowUp direction not North')
release('ArrowUp')
assert(input.active === false, 'ArrowUp release active still true')
// Down
press('ArrowDown')
assert(input.active === true, 'ArrowDown active is false')
assertClose(input.movement().x, 0, 1e-4, 'ArrowDown x')
assertClose(input.movement().y, 1, 1e-4, 'ArrowDown y')
assert(directionOf(input.movement()) === DIRECTION.South, 'ArrowDown direction not South')
release('ArrowDown')
// Left
press('ArrowLeft')
assert(input.active === true, 'ArrowLeft active is false')
assertClose(input.movement().x, -1, 1e-4, 'ArrowLeft x')
assertClose(input.movement().y, 0, 1e-4, 'ArrowLeft y')
assert(directionOf(input.movement()) === DIRECTION.West, 'ArrowLeft direction not West')
release('ArrowLeft')
// Right
press('ArrowRight')
assert(input.active === true, 'ArrowRight active is false')
assertClose(input.movement().x, 1, 1e-4, 'ArrowRight x')
assertClose(input.movement().y, 0, 1e-4, 'ArrowRight y')
assert(directionOf(input.movement()) === DIRECTION.East, 'ArrowRight direction not East')
release('ArrowRight')
// 1.3 Diagonal normalisation (hypot === 1.0, not √2)
console.log(' 1.3 Testing Diagonal arrows normalisation...')
press('ArrowUp')
press('ArrowRight')
const diagNE = input.movement()
assert(input.active === true, 'Diag active is false')
assertClose(Math.hypot(diagNE.x, diagNE.y), 1.0, 1e-4, 'DiagNE magnitude not 1.0')
assertClose(diagNE.x, Math.SQRT1_2, 1e-4, 'DiagNE x')
assertClose(diagNE.y, -Math.SQRT1_2, 1e-4, 'DiagNE y')
assert(directionOf(diagNE) === DIRECTION.NorthEast, 'DiagNE direction')
release('ArrowUp')
release('ArrowRight')
press('ArrowDown')
press('ArrowLeft')
const diagSW = input.movement()
assertClose(Math.hypot(diagSW.x, diagSW.y), 1.0, 1e-4, 'DiagSW magnitude')
assertClose(diagSW.x, -Math.SQRT1_2, 1e-4, 'DiagSW x')
assertClose(diagSW.y, Math.SQRT1_2, 1e-4, 'DiagSW y')
assert(directionOf(diagSW) === DIRECTION.SouthWest, 'DiagSW direction')
release('ArrowDown')
release('ArrowLeft')
// 1.4 Opposing arrows cancellation
console.log(' 1.4 Testing Opposing arrows cancellation...')
press('ArrowUp')
press('ArrowDown')
assert(input.active === true, 'Opposing up/down active should be true')
assert(input.movement().x === 0 && input.movement().y === 0, 'Opposing up/down non-zero vector')
assert(directionOf(input.movement()) === null, 'Opposing up/down direction not null')
release('ArrowUp')
release('ArrowDown')
// 1.5 ShiftLeft / ShiftRight tracking
console.log(' 1.5 Testing ShiftLeft / ShiftRight state...')
assert(input.shiftHeld === false, 'Shift initially held')
press('ShiftLeft')
assert(input.shiftHeld === true, 'ShiftLeft down did not set shiftHeld')
release('ShiftLeft')
assert(input.shiftHeld === false, 'ShiftLeft up did not clear shiftHeld')
press('ShiftRight')
assert(input.shiftHeld === true, 'ShiftRight down did not set shiftHeld')
release('ShiftRight')
assert(input.shiftHeld === false, 'ShiftRight up did not clear shiftHeld')
press('ShiftLeft')
press('ShiftRight')
assert(input.shiftHeld === true, 'Both shifts down')
release('ShiftLeft')
assert(input.shiftHeld === true, 'One shift released, other still down')
release('ShiftRight')
assert(input.shiftHeld === false, 'Both shifts released')
// 1.6 Digit1..4 do not appear in digits
console.log(' 1.6 Testing Digit1..4 exclusion from digits...')
for (const digit of ['Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5']) {
press(digit)
assert(input.digits.length === 0, `${digit} appeared in digits: ${JSON.stringify(input.digits)}`)
release(digit)
}
// 1.7 Space does not set attacking === true
console.log(' 1.7 Testing Space key attacking suppression...')
press('Space')
assert(input.attacking === false, 'Space key set attacking to true!')
release('Space')
assert(input.attacking === false, 'After Space release')
// Verify KeyJ and Enter do set attacking === true
press('KeyJ')
assert(input.attacking === true, 'KeyJ failed to set attacking: true')
release('KeyJ')
assert(input.attacking === false, 'KeyJ release failed to clear attacking')
press('Enter')
assert(input.attacking === true, 'Enter failed to set attacking: true')
release('Enter')
assert(input.attacking === false, 'Enter release failed to clear attacking')
// 1.8 Ghost interaction decoupling (KeyT and KeyB)
console.log(' 1.8 Testing Ghost interaction keys decoupling (KeyT, KeyB)...')
press('KeyT')
assert(input.talking === false, 'KeyT set talking: true')
release('KeyT')
press('KeyB')
assert(input.takeWaypoint() === false, 'KeyB triggered takeWaypoint()')
release('KeyB')
// 1.9 Blur resets all held state
console.log(' 1.9 Testing blur event resets all held state...')
press('ArrowUp')
press('ShiftLeft')
press('KeyJ')
assert(input.active === true, 'Pre-blur active')
assert(input.shiftHeld === true, 'Pre-blur shiftHeld')
assert(input.attacking === true, 'Pre-blur attacking')
blur()
assert(input.active === false, 'Post-blur active not false')
assert(input.shiftHeld === false, 'Post-blur shiftHeld not false')
assert(input.attacking === false, 'Post-blur attacking not false')
assert(input.movement().x === 0 && input.movement().y === 0, 'Post-blur movement not zero')
input.detach()
console.log(' ✅ All In-Memory KeyboardInput tests passed!')
}
// ============================================================================
// PART 2: DIFFERENTIAL FUZZING (ORACLE VS IMPLEMENTATION)
// ============================================================================
function runDifferentialFuzzing(iterations = 1000): void {
console.log(`\n--- PART 2: Differential Fuzzing (${iterations} iterations) ---`)
const ARROWS: Record<string, [number, number]> = {
ArrowUp: [0, -1],
ArrowDown: [0, 1],
ArrowLeft: [-1, 0],
ArrowRight: [1, 0],
}
const ALL_TEST_KEYS = [
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
'KeyW', 'KeyA', 'KeyS', 'KeyD',
'Digit1', 'Digit2', 'Digit3', 'Digit4',
'Space', 'ShiftLeft', 'ShiftRight', 'KeyJ', 'Enter', 'KeyT', 'KeyB', 'KeyE', 'Tab',
'KeyQ', 'KeyC', 'KeyV', 'KeyI', 'KeyR', 'KeyG', 'KeyP', 'KeyK', 'KeyL',
]
const listeners = new Map<string, (e: any) => void>()
const mockWindow = {
addEventListener: (type: string, fn: any) => listeners.set(type, fn),
removeEventListener: (type: string, fn: any) => listeners.delete(type),
}
const input = new KeyboardInput(mockWindow as unknown as Window)
input.attach()
// Simple, obviously correct Oracle implementation
function oracle(heldKeys: Set<string>) {
let ox = 0
let oy = 0
let active = false
for (const k of heldKeys) {
const v = ARROWS[k]
if (v) {
ox += v[0]
oy += v[1]
active = true
}
}
const mag = Math.hypot(ox, oy)
const movement: Movement = mag === 0 ? { x: 0, y: 0 } : { x: ox / mag, y: oy / mag }
const shiftHeld = heldKeys.has('ShiftLeft') || heldKeys.has('ShiftRight')
const attacking = heldKeys.has('KeyJ') || heldKeys.has('Enter')
const digits: number[] = [] // Invariant: must be empty
const talking = false // Invariant: must be false
return { movement, active, shiftHeld, attacking, digits, talking }
}
// Seeded pseudo-random generator
let seed = 1337
const rand = () => {
seed = (seed * 1664525 + 1013904223) % 4294967296
return seed / 4294967296
}
const currentlyHeld = new Set<string>()
for (let iter = 0; iter < iterations; iter++) {
// Pick 1-5 keys to toggle
const numToggles = Math.floor(rand() * 5) + 1
for (let t = 0; t < numToggles; t++) {
const key = ALL_TEST_KEYS[Math.floor(rand() * ALL_TEST_KEYS.length)]!
if (currentlyHeld.has(key)) {
currentlyHeld.delete(key)
listeners.get('keyup')?.({ code: key })
} else {
currentlyHeld.add(key)
listeners.get('keydown')?.({ code: key, preventDefault: () => {} })
}
}
const exp = oracle(currentlyHeld)
const actMov = input.movement()
assertClose(actMov.x, exp.movement.x, 1e-4, `Iter ${iter} Movement X mismatch on keys: ${Array.from(currentlyHeld).join(',')}`)
assertClose(actMov.y, exp.movement.y, 1e-4, `Iter ${iter} Movement Y mismatch on keys: ${Array.from(currentlyHeld).join(',')}`)
assert(input.active === exp.active, `Iter ${iter} Active mismatch: actual ${input.active} vs oracle ${exp.active}`)
assert(input.shiftHeld === exp.shiftHeld, `Iter ${iter} ShiftHeld mismatch`)
assert(input.attacking === exp.attacking, `Iter ${iter} Attacking mismatch`)
assert(input.digits.length === 0, `Iter ${iter} Digits must be empty`)
assert(input.talking === false, `Iter ${iter} Talking must be false`)
}
// Clear all
listeners.get('blur')?.({})
input.detach()
console.log(` ✅ Differential fuzzing passed all ${iterations} iterations with 0 discrepancies!`)
}
// ============================================================================
// PART 3: IN-MEMORY HUD MANAGER KEY HANDLING & STATE TRANSITIONS
// ============================================================================
function runHudManagerKeyTests(): void {
console.log('\n--- PART 3: In-Memory HudManager Key Handling & State Transitions ---')
const windowListeners = new Map<string, (e: any) => void>()
const mockWindow = {
addEventListener: (type: string, fn: any) => windowListeners.set(type, fn),
removeEventListener: (type: string, fn: any) => windowListeners.delete(type),
devicePixelRatio: 1.0,
innerWidth: 1920,
innerHeight: 1080,
}
// Mock global DOM constructors if running in pure Node
if (typeof (globalThis as any).window === 'undefined') {
;(globalThis as any).window = mockWindow
}
if (typeof (globalThis as any).HTMLInputElement === 'undefined') {
;(globalThis as any).HTMLInputElement = class {}
}
if (typeof (globalThis as any).HTMLSelectElement === 'undefined') {
;(globalThis as any).HTMLSelectElement = class {}
}
const mockCanvas = {
width: 1920,
height: 1080,
clientWidth: 1920,
clientHeight: 1080,
style: { pointerEvents: 'none' },
getBoundingClientRect: () => ({ left: 0, top: 0, width: 1920, height: 1080 }),
getContext: () => null,
addEventListener: () => {},
} as unknown as HTMLCanvasElement
const hud = new HudManager(mockCanvas, {
onToggleAutomap: () => {},
onWaypointTeleport: () => {},
})
// Wire events via private bindEvents()
;(hud as any).bindEvents()
const fireKey = (key: string, code: string, target?: any) => {
let prevented = false
const evt = {
key,
code,
target: target ?? {},
preventDefault: () => { prevented = true },
}
windowListeners.get('keydown')?.(evt)
return prevented
}
// 3.1 KeyS and KeyT toggle Skill Tree
console.log(' 3.1 Testing KeyS and KeyT toggle Skill Tree...')
assert(hud.rightPanel === 'none', 'Initial right panel')
fireKey('s', 'KeyS')
assert(hud.rightPanel === 'skill', 'KeyS did not open skill tree')
assert(hud.skillTree.visible === true, 'Skill tree not visible')
fireKey('s', 'KeyS')
assert(hud.rightPanel === 'none', 'KeyS did not close skill tree')
assert(hud.skillTree.visible === false, 'Skill tree still visible')
fireKey('t', 'KeyT')
assert(hud.rightPanel === 'skill', 'KeyT did not open skill tree')
fireKey('t', 'KeyT')
assert(hud.rightPanel === 'none', 'KeyT did not close skill tree')
// Alternate: KeyS opens, KeyT closes
fireKey('s', 'KeyS')
assert(hud.rightPanel === 'skill', 'KeyS open')
fireKey('t', 'KeyT')
assert(hud.rightPanel === 'none', 'KeyT close')
// 3.2 KeyW toggles Weapon Swap
console.log(' 3.2 Testing KeyW toggles Weapon Swap...')
assert(hud.inventory.weaponSwapSet === 0, 'Initial weaponSwapSet is not 0')
fireKey('w', 'KeyW')
assert(hud.inventory.weaponSwapSet === 1, 'KeyW did not switch to weapon set II (1)')
fireKey('w', 'KeyW')
assert(hud.inventory.weaponSwapSet === 0, 'KeyW did not switch back to weapon set I (0)')
// 3.3 Space and Escape call closeAllPanels()
console.log(' 3.3 Testing Space and Escape panel closing...')
// Test Character Sheet dismissal via Space
fireKey('a', 'KeyA')
assert(hud.leftPanel === 'char', 'KeyA open char sheet')
const preventedSpace = fireKey(' ', 'Space')
assert(hud.leftPanel === 'none', 'Space failed to close char sheet')
assert(preventedSpace === true, 'Space should preventDefault when closing panel')
// Test Skill Tree dismissal via Escape
fireKey('s', 'KeyS')
assert(hud.rightPanel === 'skill', 'KeyS open skill tree')
const preventedEscape = fireKey('Escape', 'Escape')
assert(hud.rightPanel === 'none', 'Escape failed to close skill tree')
assert(preventedEscape === true, 'Escape should preventDefault when closing panel')
// Test Multi-Panel dismissal via Space
fireKey('c', 'KeyC')
fireKey('i', 'KeyI')
assert(hud.leftPanel === 'char', 'Char open')
assert(hud.rightPanel === 'inv', 'Inv open')
fireKey(' ', 'Space')
assert(hud.leftPanel === 'none', 'Space failed to close left panel')
assert(hud.rightPanel === 'none', 'Space failed to close right panel')
// Test Space when no panels open returns false
const preventedNoOp = fireKey(' ', 'Space')
assert(preventedNoOp === false, 'Space when no panels open should not preventDefault')
// 3.4 Number keys 1..4 drink belt potions
console.log(' 3.4 Testing Number keys 1..4 drink belt potions without altering skills...')
hud.hp = 500
hud.mana = 400
const initialLeftSkill = hud.hotkeys.leftSkillId
const initialRightSkill = hud.hotkeys.rightSkillId
const initialPotions = hud.belt.countTotalPotions()
fireKey('1', 'Digit1')
assert(hud.belt.countTotalPotions() === initialPotions - 1, 'Potion 1 not consumed')
assert(hud.hp > 500, 'HP did not increase after drinking potion 1')
assert(hud.hotkeys.leftSkillId === initialLeftSkill, 'leftSkillId changed!')
assert(hud.hotkeys.rightSkillId === initialRightSkill, 'rightSkillId changed!')
fireKey('2', 'Digit2')
assert(hud.belt.countTotalPotions() === initialPotions - 2, 'Potion 2 not consumed')
// 3.5 Target element filtering (input / select)
console.log(' 3.5 Testing Input / Select element key event suppression...')
const inputEl = new (globalThis as any).HTMLInputElement()
fireKey('s', 'KeyS', inputEl)
assert(hud.rightPanel === 'none', 'KeyS inside input element toggled skill tree!')
fireKey('w', 'KeyW', inputEl)
assert(hud.inventory.weaponSwapSet === 0, 'KeyW inside input element toggled weapon swap!')
console.log(' ✅ All In-Memory HudManager tests passed!')
}
// ============================================================================
// PART 4: HEADLESS CHROME CDP IN-ENGINE LIVE GAMEPLAY VERIFICATION
// ============================================================================
async function runChromeCdpVerification(): Promise<void> {
console.log('\n--- PART 4: Headless Chrome CDP Live Gameplay Invariant Verification ---')
mkdirSync(HANDOFF_DIR, { recursive: true })
const server = await createServer({
root: process.cwd(),
server: { port: 5199, host: '127.0.0.1', strictPort: false },
})
await server.listen()
const port = (server.httpServer?.address() as any)?.port ?? 5199
const baseUrl = `http://127.0.0.1:${port}`
console.log(` Vite server running at: ${baseUrl}`)
const debugPort = 9255
const chromeProc = spawn('/usr/bin/google-chrome', [
'--headless=new',
`--remote-debugging-port=${debugPort}`,
'--no-sandbox',
'--disable-dev-shm-usage',
'--enable-webgl',
'--ignore-gpu-blocklist',
'--use-gl=angle',
'--use-angle=swiftshader',
'--window-size=1280,840',
'about:blank',
])
try {
let wsUrl: string | null = null
for (let i = 0; i < 60; i++) {
await sleep(200)
try {
const res = await fetch(`http://127.0.0.1:${debugPort}/json/list`)
const pages = (await res.json()) as any[]
const p = pages.find(x => x.type === 'page')
if (p?.webSocketDebuggerUrl) {
wsUrl = p.webSocketDebuggerUrl
break
}
} catch {}
}
if (!wsUrl) throw new Error('Could not connect to Chrome CDP')
console.log(' Connected to Chrome CDP WebSocket.')
const ws = new WebSocket(wsUrl)
await new Promise<void>(res => { ws.onopen = () => res() })
let idCounter = 1
const send = <T = any>(method: string, params: Record<string, unknown> = {}): Promise<T> =>
new Promise((resolve, reject) => {
const id = idCounter++
const handler = (event: MessageEvent) => {
const msg = JSON.parse(String(event.data))
if (msg.id === id) {
ws.removeEventListener('message', handler)
if (msg.error) reject(new Error(`CDP ${method} failed: ${JSON.stringify(msg.error)}`))
else resolve(msg.result as T)
}
}
ws.addEventListener('message', handler)
ws.send(JSON.stringify({ id, method, params }))
})
await send('Page.enable')
await send('Runtime.enable')
const evalJs = async <T = any>(expr: string): Promise<T> => {
const res = await send<any>('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true })
if (res.exceptionDetails) throw new Error(`JS error: ${JSON.stringify(res.exceptionDetails)}`)
return res.result.value
}
const saveScreenshot = async (name: string): Promise<string> => {
const shot = await send<any>('Page.captureScreenshot', { format: 'png' })
const p = join(HANDOFF_DIR, name)
writeFileSync(p, Buffer.from(shot.data, 'base64'))
console.log(` 📸 Saved screenshot: ${name}`)
return p
}
const pressKeyCdp = async (key: string, code: string, holdMs = 50) => {
await send('Input.dispatchKeyEvent', { type: 'keyDown', key, code, windowsVirtualKeyCode: key.charCodeAt(0) })
if (holdMs > 0) await sleep(holdMs)
await send('Input.dispatchKeyEvent', { type: 'keyUp', key, code, windowsVirtualKeyCode: key.charCodeAt(0) })
}
await send('Page.navigate', { url: `${baseUrl}/acts.html?act=1` })
// Wait for engine and HUD readiness
console.log(' Waiting for __d2webAct and __d2webHud...')
for (let i = 0; i < 100; i++) {
await sleep(250)
const ready = await evalJs<boolean>(`Boolean(window.__d2webAct?.ready && window.__d2webHud?.ready)`)
if (ready) break
}
await sleep(500)
const ready = await evalJs<boolean>(`Boolean(window.__d2webAct?.ready && window.__d2webHud?.ready)`)
assert(ready === true, 'Timed out waiting for scene readiness')
console.log(' ✅ Engine & HUD ready.')
// 4.1 Test WASD produces ZERO player movement
console.log(' 4.1 Testing WASD keys produce 0 player displacement in live engine...')
const posInitial = await evalJs<{ x: number; y: number }>(`({ x: window.__d2webAct.x, y: window.__d2webAct.y })`)
// Press W, A, S, D repeatedly
for (let i = 0; i < 5; i++) {
await pressKeyCdp('w', 'KeyW', 50)
await pressKeyCdp('a', 'KeyA', 50)
await pressKeyCdp('s', 'KeyS', 50)
await pressKeyCdp('d', 'KeyD', 50)
await sleep(50)
}
await sleep(300)
const posAfterWasd = await evalJs<{ x: number; y: number }>(`({ x: window.__d2webAct.x, y: window.__d2webAct.y })`)
console.log(` Player coordinates before WASD: (${posInitial.x}, ${posInitial.y}), after: (${posAfterWasd.x}, ${posAfterWasd.y})`)
assert(posInitial.x === posAfterWasd.x && posInitial.y === posAfterWasd.y, `Player moved during WASD! Initial: ${JSON.stringify(posInitial)}, After: ${JSON.stringify(posAfterWasd)}`)
console.log(' ✅ VERIFIED: WASD keys produce exactly 0 player displacement.')
// Close any panels opened during WASD test (e.g. A opened char, S opened skill)
await pressKeyCdp(' ', 'Space', 50)
await sleep(100)
// 4.2 Test KeyA opens Character Sheet and Space closes it
console.log(' 4.2 Testing KeyA opens Character Sheet and Space closes it...')
await pressKeyCdp('a', 'KeyA')
await sleep(150)
const leftPanelChar = await evalJs<string>(`window.__d2webHud.leftPanel`)
assert(leftPanelChar === 'char', `KeyA failed to open Character Sheet (got: ${leftPanelChar})`)
await saveScreenshot('challenger_m1_char_sheet_opened.png')
await pressKeyCdp(' ', 'Space')
await sleep(150)
const leftPanelClosed = await evalJs<string>(`window.__d2webHud.leftPanel`)
assert(leftPanelClosed === 'none', `Space failed to close Character Sheet (got: ${leftPanelClosed})`)
// 4.3 Test KeyS and KeyT toggle Skill Tree and Escape closes it
console.log(' 4.3 Testing KeyS / KeyT toggles Skill Tree and Escape closes it...')
await pressKeyCdp('s', 'KeyS')
await sleep(150)
const rightPanelSkillS = await evalJs<string>(`window.__d2webHud.rightPanel`)
assert(rightPanelSkillS === 'skill', `KeyS failed to open Skill Tree (got: ${rightPanelSkillS})`)
await saveScreenshot('challenger_m1_skill_tree_opened_by_s.png')
await pressKeyCdp('s', 'KeyS')
await sleep(150)
const rightPanelClosedS = await evalJs<string>(`window.__d2webHud.rightPanel`)
assert(rightPanelClosedS === 'none', `KeyS failed to close Skill Tree (got: ${rightPanelClosedS})`)
await pressKeyCdp('t', 'KeyT')
await sleep(150)
const rightPanelSkillT = await evalJs<string>(`window.__d2webHud.rightPanel`)
assert(rightPanelSkillT === 'skill', `KeyT failed to open Skill Tree (got: ${rightPanelSkillT})`)
await pressKeyCdp('Escape', 'Escape')
await sleep(150)
const rightPanelClosedEsc = await evalJs<string>(`window.__d2webHud.rightPanel`)
assert(rightPanelClosedEsc === 'none', `Escape failed to close Skill Tree (got: ${rightPanelClosedEsc})`)
// 4.4 Test KeyW toggles Weapon Swap
console.log(' 4.4 Testing KeyW toggles Weapon Swap...')
const swap0 = await evalJs<number>(`window.__d2webHud.weaponSwapSet`)
await pressKeyCdp('w', 'KeyW')
await sleep(150)
const swap1 = await evalJs<number>(`window.__d2webHud.weaponSwapSet`)
assert(swap1 === (swap0 === 0 ? 1 : 0), `KeyW failed to toggle weapon swap set (from ${swap0} to ${swap1})`)
await saveScreenshot('challenger_m1_weapon_swap_set_toggled.png')
await pressKeyCdp('w', 'KeyW')
await sleep(150)
const swapBack = await evalJs<number>(`window.__d2webHud.weaponSwapSet`)
assert(swapBack === swap0, `KeyW failed to toggle weapon swap set back (from ${swap1} to ${swapBack})`)
// 4.5 Test Arrow keys move the player smoothly
console.log(' 4.5 Testing Arrow keys accessibility player movement...')
const posBeforeArrow = await evalJs<{ x: number; y: number }>(`({ x: window.__d2webAct.x, y: window.__d2webAct.y })`)
// Hold ArrowRight for 350ms
await send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'ArrowRight', code: 'ArrowRight', windowsVirtualKeyCode: 39 })
await sleep(350)
await send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'ArrowRight', code: 'ArrowRight', windowsVirtualKeyCode: 39 })
await sleep(250)
const posAfterArrow = await evalJs<{ x: number; y: number }>(`({ x: window.__d2webAct.x, y: window.__d2webAct.y })`)
console.log(` Player coordinates before Arrow: (${posBeforeArrow.x}, ${posBeforeArrow.y}), after ArrowRight: (${posAfterArrow.x}, ${posAfterArrow.y})`)
assert(posAfterArrow.x > posBeforeArrow.x, `ArrowRight failed to move player east! (dx = ${posAfterArrow.x - posBeforeArrow.x})`)
console.log(' ✅ VERIFIED: Arrow keys move player successfully.')
await saveScreenshot('challenger_m1_arrow_movement.png')
// 4.6 Test Digit 1..4 drinks belt potions without altering active skills
console.log(' 4.6 Testing Digit 1..4 drinks belt potions in live browser...')
const beforePotions = await evalJs<number>(`window.__d2webHud.beltPotionsCount`)
const beforeLeftSkill = await evalJs<number>(`window.__d2webHud.leftSkillId`)
const beforeRightSkill = await evalJs<number>(`window.__d2webHud.rightSkillId`)
await pressKeyCdp('1', 'Digit1')
await sleep(150)
const afterPotions = await evalJs<number>(`window.__d2webHud.beltPotionsCount`)
const afterLeftSkill = await evalJs<number>(`window.__d2webHud.leftSkillId`)
const afterRightSkill = await evalJs<number>(`window.__d2webHud.rightSkillId`)
assert(afterPotions === beforePotions - 1, `Digit1 failed to consume potion (before: ${beforePotions}, after: ${afterPotions})`)
assert(afterLeftSkill === beforeLeftSkill, `Digit1 changed leftSkillId`)
assert(afterRightSkill === beforeRightSkill, `Digit1 changed rightSkillId`)
console.log(' ✅ VERIFIED: Digit1 drinks belt potion without modifying skill selection.')
ws.close()
} finally {
chromeProc.kill()
await server.close()
}
}
// ============================================================================
// MAIN RUNNER
// ============================================================================
async function main(): Promise<void> {
console.log('======================================================================')
console.log('🛡️ CHALLENGER EMPIRICAL VERIFICATION SUITE — MILESTONE 1 (R1)')
console.log('======================================================================')
try {
runKeyboardInputTests()
runDifferentialFuzzing(1000)
runHudManagerKeyTests()
await runChromeCdpVerification()
console.log('\n======================================================================')
console.log('🎉 ALL EMPIRICAL CHALLENGER TESTS PASSED (100% SUCCESS, VERDICT: APPROVE)')
console.log('======================================================================')
process.exit(0)
} catch (err: any) {
console.error('\n❌ CHALLENGER TEST FAILED WITH AN ERROR:')
console.error(err)
process.exit(1)
}
}
main()