diablo2-web/TEST_INFRA.md

82 lines
14 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.

# Test Infrastructure & 4-Tier E2E Test Plan: D2GS v1.13c Online Gameplay Loops & Server Authority
## 1. Test Philosophy & Ground Truth Invariants
The `tests/e2e-d2gs-online/` suite is an opaque-box, requirement-driven 4-tier E2E test suite verifying all 16 features (`F1`–`F16`) defined in `PROJECT.md` and `ORIGINAL_REQUEST.md` (`2026-10-01T13:08:09Z`).
### Core Verification Principles
1. **Diablo II v1.13c Binary & Data-Table Ground Truth**:
- Every C2S binary packet emitted by `encodeD2gsClientCommand` / `clientCommandToC2sPackets` is verified against `C2S_PACKET_SIZES[opcode]` (`src/netproto/d2gs/tables/s2c-sizes.ts`) and exact little-endian wire layout (`u8`, `u16LE`, `u32LE`).
- Every online item tooltip (`onlineItemDataToUiInventoryItem` and `HudModel.syncFromWorld()`) is verified against canonical 1.13c item tables (`getEmbeddedDropTables()`, `Armor.txt`, `Weapons.txt`, `Misc.txt`, `MagicPrefix.txt`, `MagicSuffix.txt`, `RarePrefix.txt`, `RareSuffix.txt`, `SetItems.txt`, `UniqueItems.txt`, `Runes.txt`, `ItemStatCost.txt`) — never raw `Stat #N: V` fallbacks.
- Every Town and Quest NPC across Acts 1–5 is verified against `TOWN_NPC_DESCRIPTORS` and `NPC_HCIDX_TO_DESCRIPTOR_ID` (`src/common/units/npc-table.ts`).
2. **Strict Server Authority (Zero Optimistic Client Mutation)**:
- When `HudManager` / `WorldPanelsHud` has a `HudCommandSink` (`HudModel`) attached (`hasCommandSink === true`), UI interactions emit typed `ClientCommand` payloads and C2S wire bytes **without** mutating local inventory, equipment, belt, stash, cube, mercenary, party, or trade state ahead of the server.
- Only authoritative `ServerEvent` updates applied to `ClientWorld` (`src/client/world/client-world.ts`) and synchronized via `HudModel.syncFromWorld()` (`src/client/ui-model/hud-model.ts`) mutate the visible UI state.
3. **Opaque-Box Contract Testing**:
- All tests interact through public module boundaries (`src/netproto/index.ts`, `src/common/items/item-bridge.ts`, `src/common/items/ground-items.ts`, `src/common/units/npc-table.ts`, `src/client/world/client-world.ts`, `src/client/ui-model/hud-model.ts`, `src/client/ui/hud-manager.ts`, `src/client/ui/world-panels.ts`, `src/client/view/unit-drawables.ts`, `src/client/view/scene-source.ts`, `src/client/input/command-mapper.ts`).
---
## 2. Complete 16-Feature Inventory (`F1`–`F16`)
| ID | Feature | Requirement | Primary Public Contracts Under Test |
|---|---|---|---|
| **F1** | Canonical 1.13c Online Item Tooltip Bridge | R1.1 | `onlineItemDataToUiInventoryItem`, `HudModel.syncFromWorld()`: CHI/ENG base names, Magic/Rare/Set/Unique/Runeword names, 1H/2H damage, defense, durability, `reqLevel`/`reqStr`/`reqDex`/`reqClass`, sorted `ItemStatCost` lines (`stats`/`statsEn`), `sockets`/`usedSockets`/`socketedItems`. |
| **F2** | C2S Container, Belt, NPC, Merc, Warp, Party & Trade Packet Encoders | R1.2, R2–R5 | `encodeD2gsClientCommand` & `clientCommandToC2sPackets` matching `C2S_PACKET_SIZES`: `0x09 WarpTo` (9B), `0x1D SwapCursorBodyItem` (`ItemSwap`, 9B), `0x1F SwapContainerItem` (17B), `0x23 BeltPut` (9B), `0x24 BeltRemove` (5B), `0x25 BeltSwap` (9B), `0x27 IdentifyItem` (9B), `0x28 SocketItem` (9B), `0x29 ScrollToBook` (9B), `0x31 QuestMessage` (9B), `0x36 HireMerc` (`MercHire`, 9B), `0x38 EntityAction` (`NpcEntityAction`, 13B), `0x41 Resurrect` (1B), `0x4F Trade` (`ClickButton`, 7B), `0x5D Hostile` (7B, `button`=1/2), `0x5E Party` (`PartyAction`, 6B), `0x61 MercItem` (3B), `0x62 NpcResurrectMerc` (`MercResurrect`, 5B). |
| **F3** | Server-Authoritative Container, Belt, Identify, Socket & Tome UI Wiring | R1.2 | `HudModel` & `HudManager`: equip slot swap (`0x1D`) vs socket (`0x28`); container grid scroll-to-book (`0x29`), socket (`0x28`), swap (`0x1F`); belt put (`0x23`), remove (`0x24`), swap (`0x25`), shift-use on merc (`0x26` `useOnMerc=1`); right-click `isc`/`ibk` identify targeting (`0x27`). |
| **F4** | Weapon Set II (`bodyLoc` 11 & 12) Sync | R1.3 | `HudModel.syncFromWorld()`: `bodyLoc` 11 (`rarm2`) -> `hud.inventory.swapWeapons.weapon1`, `bodyLoc` 12 (`larm2`) -> `hud.inventory.swapWeapons.weapon2`, preserved across `0x60`/`0x97` weapon switch. |
| **F5** | Act 1–5 Town & Quest NPC Descriptors | R2.1 | `TOWN_NPC_DESCRIPTORS`, `NPC_HCIDX_TO_DESCRIPTOR_ID`, `getTownNpcDescriptorByHcIdx`: Cain (`146, 244, 245, 246, 520, 265`), Warriv (`155, 175`), Meshif (`210, 258`), Kaelan (`203`), Tyrael (`251, 367`), Hadriel (`259`), Qual-Kehk (`515`), Kashya (`150`), Greiz (`198`), Asheara (`252`), Charsi (`154`), Larzuk (`511`), Anya (`512`). |
| **F6** | NPC Info (`0x27`), SetNPCTalk (`0x91`), Talk Submenu & `0x31 QuestMessage` | R2.2 | `ClientWorld.apply({ type: 'Npc', kind: 'info' \| 'menu' \| 'heal' })`, synchronous `NpcInit` menu opening, `Talk` submenu (`Introduction`, `Gossip`, Quest speech), `.tbl` speech dialog box, and `0x31 QuestMessage`. |
| **F7** | Act Travel Menu Options & C2S `0x38 EntityAction` | R2.3 | `buildNpcMenuOptions` & `HudModel.handleNpcMenuAction`: `travel_east` (Warriv 155, Meshif 210), `travel_west` (Warriv 175, Meshif 258), `travel_harrogath` (Tyrael 367) emitting `NpcEntityAction` (`0x38`, 13B). |
| **F8** | Mercenary World State (`0x4E`, `0x4F`, `0x9B` & `lifePct`) | R3.1 | `ClientWorld.apply({ type: 'Merc' })`: `hireListStart` (`0x4F`), `forHire` (`0x4E` `{ nameId, seed }`), `reviveCost` (`0x9B` `{ reviveCost, deadNameId }`), `lifePct` updates (`0x0C`, `0xAB`, `0x69`, `0x81`, `0x9E..0xA0`), and `world.containers.merc` unequip-to-cursor (`0x08`). |
| **F9** | Mercenary Hire (`0x36`) & Resurrect (`0x62`) UI | R3.2 | `hire` option on mercenary captains (`150, 198, 252, 515`) -> `0x38` (`action=3`) -> Hire panel populated from `world.mercHireList` -> `MercHire` (`0x36`, 9B); dead merc (`reviveCost > 0`) exposes `resurrect` -> `MercResurrect` (`0x62`, 5B). |
| **F10** | Mercenary HUD Portrait & Paperdoll (`O` Hotkey, `0x61 MercItem`) | R3.3 | Top-left mercenary portrait + life bar in `HudManager`; `'O'` hotkey toggles `'merc'` panel; `world.containers.merc` syncs to merc equipment slots (`helm`, `armor`, `weapon1`, `weapon2`); clicking slot emits `MercItem` (`0x61`, 3B). |
| **F11** | Dungeon/Stair Warp Clicking (`0x09 AssignWarp` → C2S `0x09 WarpTo`) | R4.1 | `0x09 AssignWarp` preserves `warpId` on `UnitSnapshot` & `ClientUnit`; `buildSceneDrawables` & `ClientWorldSceneSource.pickUnitAtScreen` expose `'warp'`/`'tile'` units with localized destination labels; clicking emits `MoveToUnit` (`0x04`) + `TakeWarp`/`WarpTo` (`0x09`, 9B). |
| **F12** | Portal Ownership Hover (`0x60`/`0x82`) & Ground Item Labels (`0x16`) | R4.2 | Town Portal (`classId` 59/60) enriched with `world.portals` (`<ownerName>'s Town Portal`, destination `areaId`); ground gold piles propagate `goldAmount` (`<N> Gold` / `<N> 金币`, never hardcoded `'1 Gold'`); `Alt` ground labels layout without overlap and click-to-pickup emits `ItemPickup` (`0x16`). |
| **F13** | `SkillCast` (`0x4C`/`0x4D`/`0x6C`/`0x99`/`0x9A`) Animation | R4.3 | `ClientWorld.apply({ type: 'SkillCast' })` updates caster unit `castState`, facing `direction` toward target, and attack/cast animation mode (`'A1'`/`'SC'`, mode `7`/`10` for player, `4`/`7` for monster). |
| **F14** | Player Death Overlay & C2S `0x41 Resurrect` | R4.4 | `world.self.life <= 0` (when `maxLife > 0`) or death mode sets `isDead` / `isSelfDead`, renders classic death overlay (`"You Have Died. Press ESC to continue."` / `"你挂了,请按 ESC 键继续"`), and pressing `Escape` or clicking overlay emits `{ type: 'Resurrect' }` (`0x41`, 1B). |
| **F15** | Multiplayer Party State & Party Panel (`P` Hotkey, `0x5D`/`0x5E`) | R5.1 | `ClientWorld.apply({ type: 'Party' })` handles `roster` (`0x5B`/`0x5C`), `relation`, `memberState`, `memberPos`; `'P'` hotkey toggles `'party'` panel; Invite/Cancel/Accept/Leave emit `PartyAction` (`0x5E`, 6B); Hostile (`button=1`) and Loot (`button=2`) emit `Hostile` (`0x5D`, 7B); party portraits & automap markers. |
| **F16** | Player-to-Player Trade UI (`0x77`–`0x79` & C2S `0x4F`) & Quest Reward Item Panels | R5.2, R5.3 | `0x77 ButtonActions`, `0x78 TradeAccepted` (`partnerName`), `0x79 GoldInTrade` (`selfGold`, `partnerGold`), `tradePartner` & `tradeSelf` 10×4 grids, Trade Accept/Cancel (`0x4F ClickButton`, 7B); Quest Reward panels (`imbue` on Charsi 154, `add_sockets` on Larzuk 511, `personalize` on Anya 512). |
---
## 3. 4-Tier Suite Architecture & Coverage Thresholds
```text
tests/e2e-d2gs-online/
├── helpers.ts # Shared fixtures, mock Canvas/DOM harness, packet/item builders, runtime wrappers
├── tier1-feature-coverage.test.ts # Tier 1: Core feature happy-path coverage (80 tests: 5 per F1–F16)
├── tier2-boundary-corner.test.ts # Tier 2: Boundary, corner, negative & anti-regression coverage (80 tests: 5 per F1–F16)
├── tier3-cross-feature.test.ts # Tier 3: Pairwise cross-feature integration coverage (16 tests across F1–F16 pairs)
└── tier4-real-world-scenarios.test.ts # Tier 4: End-to-end multi-step online gameplay session scenarios (8 scenarios)
```
| Tier | File | Minimum Required | Implemented | Scope |
|---|---|---|---|---|
| **Tier 1** | `tests/e2e-d2gs-online/tier1-feature-coverage.test.ts` | `>= 80` (`>= 5` / feature) | **80** (`5 × 16`) | Core happy-path behavior for `F1`–`F16` |
| **Tier 2** | `tests/e2e-d2gs-online/tier2-boundary-corner.test.ts` | `>= 80` (`>= 5` / feature) | **80** (`5 × 16`) | Boundary values, malformed/stale packets, server rejection, zero optimistic mutation |
| **Tier 3** | `tests/e2e-d2gs-online/tier3-cross-feature.test.ts` | `>= 16` | **16** | Pairwise cross-feature workflows (`F1×F3`, `F2×F3`, `F1×F4`, `F5×F6`, `F5×F7`, `F8×F9`, `F8×F10`, `F1×F10`, `F11×F12`, `F12×F1`, `F13×F14`, `F14×F8`, `F15×F12`, `F15×F16`, `F5×F16`, `F1×F16`) |
| **Tier 4** | `tests/e2e-d2gs-online/tier4-real-world-scenarios.test.ts` | `>= 8` | **8** | Full multi-step online gameplay session loops |
| **Total** | `tests/e2e-d2gs-online/*.test.ts` | `>= 184` | **184** | All 16 features (`F1`–`F16`) |
---
## 4. Tier 4 Real-World Online Gameplay Session Scenarios
1. **Scenario 1 — Town Identification, Tome Packing, Rune Socketing & Weapon Swap Loop (`F1 + F2 + F3 + F4`)**:
- Player picks up an unidentified Unique item, right-clicks Tome of Identify (`ibk`) to arm identify targeting, left-clicks the item (`0x27 IdentifyItem`), receives server `0x9C` identified update with canonical 1.13c stats, packs loose Scroll of Identify (`isc`) into `ibk` (`0x29 ScrollToBook`), sockets a rune into a 2-socket helm (`0x28 SocketItem`), and verifies Weapon Set II (`bodyLoc` 11/12) persistence across weapon switch (`0x97`).
2. **Scenario 2 — Belt Potion Management & Shift-Click Mercenary Feeding (`F2 + F3 + F8 + F10`)**:
- Player places potions into belt (`0x23 BeltPut`), swaps a belt slot with a Super Healing Potion on cursor (`0x25 BeltSwap`), removes a belt potion (`0x24 BeltRemove`), and Shift-uses a belt slot (`0x26 UseBeltItem` with `useOnMerc = 1`) to heal a damaged Act 2 mercenary whose `lifePct` subsequently updates via S2C `0x0C`/`0x9E`.
3. **Scenario 3 — Multi-Act Town NPC Dialogue, Cain Identification & Act Travel (`F2 + F5 + F6 + F7`)**:
- Player interacts with Deckard Cain (`hcIdx` 244), opens `Talk` submenu (`Introduction`/`Gossip`/Quest speech -> `0x31 QuestMessage`), triggers Cain Identify Items (`0x38` `action=4`), then interacts with Warriv (`155` -> `travel_east` to Act 2), Meshif (`210` -> `travel_east` to Act 3), and Tyrael (`367` -> `travel_harrogath` to Act 5).
4. **Scenario 4 — Mercenary Captain Hiring, Equipment Paperdoll (`O`) & Post-Combat Resurrection (`F1 + F2 + F8 + F9 + F10`)**:
- Player opens Kashya (`150`) / Greiz (`198`) menu, clicks `Hire` (`0x38` `action=3`), receives `0x4F` + `0x4E` hire candidates, hires a mercenary (`0x36 HireMerc`), opens `'O'` Mercenary Paperdoll, equips/swaps merc gear (`0x61 MercItem`), takes lethal merc damage (`0x9B MercReviveCost`), and resurrects the mercenary (`0x62 NpcResurrectMerc`).
5. **Scenario 5 — Dungeon Warp Traversal, Town Portal Ownership Hover & Ground Loot Pickup (`F2 + F11 + F12`)**:
- Server assigns a dungeon warp tile (`0x09 AssignWarp`), player hovers/clicks warp (`0x04 MoveToUnit` + `0x09 WarpTo`), enters dungeon, hovers an allied Town Portal (`0x60`/`0x82` -> `"Sorceress's Town Portal"`), holds `Alt` to inspect non-overlapping ground labels (`750 Gold` + Magic item), and clicks to pick up (`0x16 ItemPickup`).
6. **Scenario 6 — Combat SkillCast Animation, Player Death Overlay & Town Corpse Resurrection (`F2 + F13 + F14`)**:
- Monster and player exchange `SkillCast` packets (`0x4C`/`0x4D`/`0x6C`) updating facing octants and `'A1'`/`'SC'` animation modes; lethal damage reduces `world.self.life` to `0`, activating `isDead` and rendering `"You Have Died. Press ESC to continue."`; pressing `Escape` dispatches `0x41 Resurrect` (1B), and server respawn restores town state.
7. **Scenario 7 — Multiplayer Party Roster, Invite/Accept, Hostile & Corpse Loot Permissions (`F2 + F15`)**:
- Second player joins (`0x5B` with `partyId=0xffff` and non-empty `charName`), player opens `'P'` Party panel, invites (`0x5E` `action=0x06`) and accepts (`0x5E` `action=0x08`), receives party `memberState` (`0x7F`) and `memberPos` (`0x90`) for top-left party portrait and automap marker, toggles Corpse Loot (`0x5D` `button=2`) and Hostile (`0x5D` `button=1`), and cleans up when peer leaves (`0x5C`).
8. **Scenario 8 — Player-to-Player Trade Session & Act 1/5 Quest Reward Item Upgrading (`F1 + F2 + F5 + F16`)**:
- Player initiates trade (`0x77` `action=0x01`), receives `tradePartner` item (`container: 'tradePartner'`) and `0x79 GoldInTrade`, places `tradeSelf` item (`container: 'trade'`), receives `0x78 TradeAccepted` with `partnerName`, clicks Trade Accept (`0x4F` `buttonId=0x04`), completes trade (`0x77` `action=0x06`), and uses Charsi `imbue` (`154`), Larzuk `add_sockets` (`511`), and Anya `personalize` (`512`) quest reward panels.