diablo2-web/TEST_INFRA.md

86 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# E2E Test Infrastructure & Philosophy (`feat/netproto`)
## Test Philosophy
The `feat/netproto` E2E test suite validates the complete Diablo II: Lord of Destruction v1.13c online protocol stack (`src/netproto/`), client world/map/motion state engine (`src/client/`), and browser UI/asset integration (`src/ui/`, `src/i18n/`, `scripts/`) under strict deterministic, hermetic execution guarantees:
1. **1.13c Binary Ground-Truth Parity**: Every protocol frame, bitfield, hash algorithm, compression table, and DRLG collision mask is verified against the canonical 1.13c binary tables (`D2Common.dll`, `D2Client.dll`, `D2Net.dll`, `D2MCPClient.dll`, `Bnclient.dll`, `Fog.dll`, and MPQ data tables `ItemStatCost.txt`, `Armor.txt`, `Weapons.txt`, `Misc.txt`, `Levels.txt`, `LvlPrest.txt`, `LvlTypes.txt`, `LvlWarp.txt`, `MonStats.txt`, `Objects.txt`, `SuperUniques.txt`, `Sounds.txt`, `SoundEnviron.txt`).
2. **Zero Live Network Dependency in Automated Runs**: All Vitest suites (`tests/e2e-netproto/tier1..4.test.ts`) execute 100% hermetically in-process using `createMemoryStreamPair`, `FakeClock`, `InMemoryPacketTap`, and `.d2cap` JSONL wire captures without opening any TCP or WebSocket connections to `101.37.117.183` or `www.laiseek.xyz`.
3. **Fail-Fast Protocol & Asset Validation**: Malformed packet headers, unknown opcodes, payload length mismatches, bitstream overruns, invalid item qualities/stat IDs, and unwalkable spawn coordinates throw structured `ProtocolError` or explicit validation errors rather than silently succeeding or returning permissive fallbacks.
4. **Protocol-Neutral Architectural Boundary**: Client and UI modules (`src/client/**`, `src/ui/**`) consume only the public domain boundary (`src/netproto/index.ts`: `ServerEvent`, `ClientCommand`, `GameServerAdapter`, `D2OnlineFlow`) and never import wire-level protocol internals (`src/netproto/{bncs,mcp,d2gs,crypto}/**`).
---
## Feature Inventory (F1–F20)
| ID | Feature Name | Primary Modules | 1.13c Ground Truth Reference |
|---|---|---|---|
| **F1** | Core Binary & Bit Primitives (`ByteReader`, `ByteWriter`, `BitReader`, `BitWriter`, `Clock`, `TextCodec`, `PacketTap`) | `src/netproto/core/{byte-reader,byte-writer,bit-reader,bit-writer,clock,text-codec,packet-tap,errors}.ts` | LSB-first bitstreams (`D2Common.dll` `0x6fd7c470`), LE/BE integer framing, GBK/UTF-8/Latin1 codecs, `.d2cap` JSONL format |
| **F2** | Transport & Endpoint Resolution (`ByteStream`, `EndpointResolver`, `MemoryStream`, `WsBridge`) | `src/netproto/transport/{byte-stream,endpoint,memory-stream,ws-stream,node-tcp-stream}.ts` | `bnet:6112`, `realm:6113`, `game:4000`, `ts:4002`, `maxConcurrentBnetWss = 2` rate limit |
| **F3** | Cryptographic & Auth Primitives (`XSHA-1`, `BnetHash`, `CheckRevision`, `CDKey`) | `src/netproto/crypto/{xsha1,bnet-hash,check-revision,cdkey}.ts` | Broken SHA-1 (`leftRotate(1 << n)` without word rotation), `hashBnetPassword`, `doubleHash`, `hashRealmPassword`, 16-char D2DV/D2XP CD-key shuffle & lookup table, 1.13c `0x7686beca` |
| **F4** | BNCS (`bnetd` Port 6112) Framing, Packets & Session | `src/netproto/bncs/{framing,packets,session}.ts` | `0xFF` magic header, `SID_AUTH_INFO (0x50)`, `SID_AUTH_CHECK (0x51)`, `SID_LOGONRESPONSE2 (0x3A)`, `SID_CREATEACCOUNT2 (0x3D)`, `SID_QUERYREALMS2 (0x40)`, `SID_LOGONREALMEX (0x3E)`, `SID_ENTERCHAT (0x0A)`, `SID_CHATEVENT (0x0F)`, `SID_PING (0x25)` |
| **F5** | MCP (`d2cs` Port 6113) Framing, Packets & Session | `src/netproto/mcp/{framing,packets,session}.ts` | `[u16LE len][u8 id]` framing, `MCP_STARTUP (0x01)`, `MCP_CHARCREATE (0x02)`, `MCP_CREATEGAME (0x03)`, `MCP_JOINGAME (0x04)`, `MCP_GAMELIST (0x05)`, `MCP_GAMEINFO (0x06)`, `MCP_CHARLOGON (0x07)`, `MCP_CHARDELETE (0x0A)`, `MCP_CHARUPGRADE (0x18)`, `MCP_CHARLIST2 (0x19)`, 33B `statString` |
| **F6** | D2GS (`d2gs` Port 4000) Huffman Compression & Block Framing | `src/netproto/d2gs/{compression,framing-s2c,framing-c2s}.ts` | `Fog.10229` canonical 256-symbol Huffman tree, 1/2-byte block header (`D2Net.dll` `0x6fbf6b60`), `0xAF` compression negotiation (`[0xAF, 0x01]` & 130B custom Huffman table `[0xAF, 0x81, ...128B]`), 179-entry S->C size table (`0x6fbfb090`), 111-entry C->S size table (`0x6fbfabd8`) |
| **F7** | D2GS S->C Packet Decoder Registry (`0x00..0xB4`) & C->S Command Encoders (`0x01..0x6D`) | `src/netproto/d2gs/registry.ts`, `src/netproto/d2gs/s2c/*.ts`, `src/netproto/d2gs/c2s/*.ts` | `D2Client.dll` handler table (`0x6fb8de60` & `0x6fb5d450`), 0 unknown/opaque packets across all 1.13c opcodes |
| **F8** | D2GS `0x9C` / `0x9D` Item Bitstream Decoder & Data Table Pipeline | `src/netproto/d2gs/items/{item-bitstream,item-types}.ts`, `scripts/extract-d2net-tables.ts` | `D2Common.11145` (`0x6fd7c470`), compact (`0x00200000`) vs extended items (`0x6fd7c0c0` / `0x6fd7a600`), qualities 1..9, chained stats (`17/18`, `48/49`, `50/51`, `52/53`, `54..56`, `57..59`), `0x1FF` terminator |
| **F9** | D2GS Session State Machine & `D2gsAdapter` (`GameServerAdapter`) | `src/netproto/d2gs/{session,adapter}.ts` | `connecting -> logonSent -> entering -> ingame -> closed`, `0x68` (37B) -> `0x01`/`0x02` -> `0x6B` (1B), 5000ms `0x6D` Ping heartbeat, `0x8F` Pong RTT tracking, early event buffering |
| **F10** | Full Online Flow (`D2OnlineFlow`: BNCS -> MCP -> D2GS -> Leave -> Rejoin) | `src/netproto/flow/{online-flow,config}.ts` | End-to-end state machine (`idle -> connecting_bnet -> authenticated -> connecting_realm -> realm_ready -> connecting_game -> ingame -> closed`), `loginOrRegister`, `enterRealm`, `createCharacter`, `selectCharacter`, `createGame`, `joinGame`, `leaveToLobby` |
| **F11** | Client World State Store (`ClientWorld`) | `src/client/world/{client-world,index}.ts` | Applies all 22 `ServerEvent` variants into reactive world state (`act`, `mapSeed`, `areaId`, `self`, `units`, `containers`, `revealedRooms`, `waypoints`, `quests`, `party`, `trade`, `merc`, `chatLog`, `soundQueue`, `portals`, `lastRttMs`) |
| **F12** | DRLG Map Generation, Collision Grid, Tile Atlas & `MapService` | `src/client/map/{map-service,level-view,tile-atlas}.ts`, `scripts/pack-tiles.ts` | 136 levels across Acts 1–5, `DRLGROOM_LoadDt1Files` (`0x6fdb8400`) universal DT1s (`Blank.dt1`, `InvisWal.dt1`, `Warp.dt1`), SplitMix64 `pickVariant`, `COLLIDE_WALL` (`0x0001`), `COLLIDE_BLOCK_PLAYER` (`0x0004`), `COLLIDE_BLANK` (`0x8000`) |
| **F13** | Sub-Tile A* Pathfinding, Bresenham LOS, Movement Predictor & Interpolator | `src/client/motion/{pathfind,predictor,interpolate}.ts` | 8-directional octile A* (`10` cardinal / `14` diagonal) with corner-cutting prevention, Bresenham raycast LOS, `LocalMovementPredictor` (`0x96` WalkVerify + `0x15` ReassignPlayer snap), `RemoteEntityInterpolator` |
| **F14** | Offline `.d2s` Waypoint & Difficulty Unlock Tool (`d2s-unlock`) | `scripts/d2s-unlock.ts` | v96 (`0x60`) `.d2s` header (`0xAA55AA55`), `WS` waypoint block (`0x02, 0x01, 0x00` + 5B `0x7f ff ff ff 7f` = 39 waypoints), `Woo!` quest block, rotate-left-1 + byte-add checksum (`0x0C`) |
| **F15** | Client Settings Store (`SettingsStore`, `ClientSettings`, Realm WS Derivation) | `src/client/settings/{client-settings,settings-store}.ts` | `localStorage` persistence (`d2lod_client_settings_v1`), host validation (`validateServerHost`), `deriveRealmWsEndpoints` (`wss://<host>/ws/{bnet,realm,game}` vs `ws://<host>:8081..8083`) |
| **F16** | Unified Top Toolbar (`Toolbar`, Language Switcher, Settings Modal) | `src/client/toolbar/{toolbar,index}.ts`, `src/i18n/lang.ts` | `offline-local` vs `online-realm` mode selector, `zh` / `en` instant locale toggle without reload, `800x600` / `1024x768` viewport toggle, Automap & Packet Inspector toggles, Settings modal |
| **F17** | Dual Viewport (`800x600` & `1024x768`) & Scene Drawables (`ClientWorldSceneSource`) | `src/client/view/{viewport-profile,scene-source,unit-drawables}.ts`, `src/ui/hud-manager.ts` | `VIEWPORT_800x600` (`barOffsetX: 0`, `rightDockX: 400`) vs `VIEWPORT_1024x768` (`barOffsetX: 112`, `rightDockX: 624`, 224px central corridor), isometric `worldToScreen` / `screenToWorld`, `ClientWorldSceneSource` |
| **F18** | Automap Overlay (`AutomapView`) | `src/client/automap/{automap-view,index}.ts` | Toggle (`Tab`), mode (`overlay`/`minimap`), zoom (`0.5..4.0`), pan (`panBy`, `resetPan`), fog-of-war (`RoomReveal`/`RoomHide`), cross-shape player marker, party/monster/merc/portal/waypoint markers |
| **F19** | Spatial Audio (`SoundService`) & OPFS/HTTP-Range MPQ Asset Streaming (`OpfsMpqManager`) | `src/client/audio/{sound-service,index}.ts`, `src/client/assets/{opfs-mpq,index}.ts` | Euclidean sub-tile distance attenuation & stereo pan (`computeSpatialAudioMetrics`), `resolveSoundEnvironmentForLevel`, `HttpRangeMpqSource` (`206 Partial Content`), `InMemoryChunkCache` LRU, `OpfsMpqManager` |
| **F20** | Outbound Rate Limiter (`OutboundRateLimiter`) & Live Packet Inspector (`PacketInspector`) | `src/client/session/{rate-limiter,index}.ts`, `src/client/inspector/{packet-inspector,index}.ts` | Per-category cooldowns (`move` 80ms, `skill` 120ms, `interact` 150ms, `chat` 500ms, `item` 120ms, `npc` 200ms) + token bucket (20 tokens/s, burst 8), `PacketInspector` ring buffer, filtering, hex dump, `.d2cap` export/import |
---
## Test Architecture
All E2E tests reside under `tests/e2e-netproto/` and are organized into 4 progressive tiers:
1. **`tests/e2e-netproto/tier1-feature-coverage.test.ts`**:
- **Scope**: Positive happy-path verification for all 20 features (`F1`–`F20`).
- **Density**: $\ge 5$ distinct `it(...)` test cases per feature ($\ge 100$ test cases total).
2. **`tests/e2e-netproto/tier2-boundary-corner.test.ts`**:
- **Scope**: Negative, boundary, malformed, truncated, overflow, and adversarial input verification for all 20 features (`F1`–`F20`).
- **Density**: $\ge 5$ distinct `it(...)` test cases per feature ($\ge 100$ test cases total).
3. **`tests/e2e-netproto/tier3-cross-feature.test.ts`**:
- **Scope**: Pairwise and multi-module integration tests verifying contracts across module boundaries (e.g., `D2gsS2cFramer` + `decodeD2gsS2cPacket` + `ClientWorld`, `MapService` + `findPathWorld` + `LocalMovementPredictor`, `SettingsStore` + `Toolbar` + `ViewportProfile`, `PacketInspector` + `InMemoryPacketTap` + `D2gsSession`).
- **Density**: $\ge 20$ distinct `it(...)` test cases.
4. **`tests/e2e-netproto/tier4-real-world-scenarios.test.ts`**:
- **Scope**: Full end-to-end multi-step user and bot workflows simulating real gameplay sessions from connection handshake through town navigation, combat, item loot/identification, party/trade, act transitions, and `.d2cap` capture replay.
- **Density**: $\ge 10$ distinct `it(...)` end-to-end workload scenarios.
---
## Real-World Application Scenarios (Tier 4)
1. **Scenario 1 — New Account Registration, Realm Logon, Character Creation & First Game Entry**: Full `D2OnlineFlow` lifecycle with `loginOrRegister(..., { register: true })`, `SID_QUERYREALMS2`, `SID_LOGONREALMEX`, `MCP_STARTUP`, `MCP_CHARCREATE`, `MCP_CHARLOGON`, `MCP_CREATEGAME`, `MCP_JOINGAME`, and `D2GS` `0xAF -> 0x68 -> 0x01/0x02 -> 0x6B -> 0x03/0x04` transition into `ingame`.
2. **Scenario 2 — Act 1 Rogue Encampment Spawn, Room Reveal, Town Navigation & Akara Vendor Transaction**: Loading Act 1 Level 1 via `MapService`, applying `LoadAct`, `RoomReveal`, `UnitAssign` (self + Akara + Stash + Waypoint), computing an A* path around obstacles, walking with `LocalMovementPredictor` + `WalkVerify (0x96)`, opening Akara's shop (`NpcInit` -> `0x9C` shop items), and buying a tome (`NpcBuy` -> `0x2A` transaction + `0x9D` inventory item).
3. **Scenario 3 — Blood Moor Combat, Skill Casting, Monster Kill, Item Drop, Pickup, Identification & Equip**: Spawning a monster pack (`0xAC` AssignNPC with boss/champion flags), casting Charged Bolt (`CastRightSkillOnTarget` -> `0x4C` SkillCast + `0x95` mana update), monster taking damage (`0x0C` NPCGetHit) and dying (`0x11` ReportKill + `0x1A` ExpByte), dropping an unidentified Magic Ring (`0x9C` AddToGround), picking it up (`ItemPickup`), identifying with a scroll (`IdentifyItem` -> identified `0x9D` with prefix/suffix/stats), and equipping it (`ItemEquip`).
4. **Scenario 4 — Waypoint Teleportation & Act Transition (Act 1 Rogue Encampment -> Act 2 Lut Gholein)**: Interacting with the Rogue Encampment waypoint (`Interact` -> `0x63` WaypointMenu bitmask), selecting Lut Gholein (`WaypointGo`), receiving `UnloadComplete (0x05)`, `LoadAct (0x03)` for Act 2 (`areaId=40`), `RoomReveal (0x07)`, and `LoadComplete (0x04)`, and verifying `MapService` + `AutomapView` + `SoundService` transition cleanly to Act 2 town.
5. **Scenario 5 — Multiplayer Party Formation, Remote Entity Interpolation, Hostility & Corpse Recovery**: Receiving `0x5B` PlayerInGame for a second player, exchanging party invites (`Party` -> `0x8D` AssignPlayerToParty), tracking party member HP/area (`0x7F` PartyMemberState) and map coordinates (`0x90` PartyMemberMapPos) on `AutomapView`, interpolating remote movement (`0x0F` PlayerMove -> `RemoteEntityInterpolator`), toggling hostility (`Hostile` -> `0x8C`), and handling player corpse assignment (`0x74` PlayerCorpseAssign + `Resurrect`).
6. **Scenario 6 — Player-to-Player Trade Session & Horadric Cube / Stash Container Management**: Initiating trade (`0x77` ButtonActions), placing gold (`0x79` GoldInTrade) and items into the trade window (`container='trade'`), accepting trade (`0x78` TradeAccepted), and moving items between Inventory (`storePage=1`), Horadric Cube (`storePage=4`), and Stash (`storePage=5`).
7. **Scenario 7 — Mercenary Hiring, Stat/Experience Progression, Equipment & Resurrection**: Opening Kashya's hire list (`0x4F` MercForHireListStart + `0x4E` MercForHire), hiring an Act 1 Rogue scout (`HireMerc` -> `0x81` AssignMerc), applying merc attributes and experience (`0x9E..0xA2`), equipping a bow on the mercenary (`0x9D` with `ownerType=1` -> `container='merc'`), and resurrecting the merc (`NpcResurrectMerc` + `0x9B` MercReviveCost).
8. **Scenario 8 — Leave Game to Lobby, Character Switch, Game List Discovery & Rejoin**: Leaving an active D2GS session via `flow.leaveToLobby()` (`0x69` LeaveGame), verifying BNCS/MCP remain alive in `realm_ready`, switching characters (`selectCharacter`), querying active games (`listGames` + `getGameInfo`), and joining a second game without reconnecting BNCS.
9. **Scenario 9 — Full UI & Client Integration Workflow (Settings, Toolbar, Viewport Toggle, Automap, Audio, OPFS Cache & Rate Limiter)**: Configuring `SettingsStore` (`online-realm`, `1024x768`, `zh`/`en`), driving `Toolbar` callbacks, switching `ViewportProfile` between `800x600` and `1024x768`, rendering `ClientWorldSceneSource` drawables and `AutomapView` markers, computing `SoundService` spatial metrics, caching MPQ chunks via `OpfsMpqManager` + `HttpRangeMpqSource`, and throttling rapid clicks through `OutboundRateLimiter`.
10. **Scenario 10 — `.d2cap` Wire Capture Recording, Export, Import, Inspection & Deterministic Replay**: Recording a multi-connection (`bnet`, `realm`, `game`) session through `InMemoryPacketTap` and `PacketInspector`, exporting to `.d2cap` JSONL (`d2cap-v1`), re-importing into a fresh `PacketInspector` + `D2gsS2cFramer` + `ClientWorld`, and verifying 100% state equivalence and formatted hex dumps.
---
## Coverage Thresholds
For $N = 20$ features (`F1`–`F20`):
| Tier | File | Minimum Formula | Required Minimum |
|---|---|---|---|
| **Tier 1** | `tests/e2e-netproto/tier1-feature-coverage.test.ts` | $5 \times N$ | $\ge 100$ tests ($\ge 5$ per feature) |
| **Tier 2** | `tests/e2e-netproto/tier2-boundary-corner.test.ts` | $5 \times N$ | $\ge 100$ tests ($\ge 5$ per feature) |
| **Tier 3** | `tests/e2e-netproto/tier3-cross-feature.test.ts` | $1 \times N$ | $\ge 20$ pairwise interaction tests |
| **Tier 4** | `tests/e2e-netproto/tier4-real-world-scenarios.test.ts` | $\ge 10$ | $\ge 10$ end-to-end scenario tests |
| **Total** | `tests/e2e-netproto/tier*.test.ts` | $11N + 10$ | **$\ge 230$ tests** |