730 lines
30 KiB
TypeScript
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()
|