# Báo Cáo Phân Tích Test Suite Alignment & Thiết Kế Unit Test Cho Milestone M2

> **Tác giả**: `explorer_m2_progression_3` (teamwork_preview_explorer)  
> **Cột mốc**: Milestone M2 (`LevelProgressionService` & Death Penalty Engine)  
> **Tài liệu tham chiếu**: [ORIGINAL_REQUEST.md](file:///c:/Projects/FreeExile/.agents/teamwork/ORIGINAL_REQUEST.md), [PROJECT.md](file:///c:/Projects/FreeExile/.agents/teamwork/orchestrator_4/PROJECT.md), [GEMINI.md](file:///c:/Projects/FreeExile/GEMINI.md)

---

## 1. TỔNG QUAN PHÂN TÍCH HIỆN TRẠNG TEST SUITE (EXECUTIVE SUMMARY)

Đội ngũ kiểm thử đã hoàn thành việc rà soát toàn diện test suite hiện có tại [tests/e2e/test_level_progression_e2e.py](file:///c:/Projects/FreeExile/tests/e2e/test_level_progression_e2e.py) và cấu trúc dữ liệu nền tảng [server/world/level_progression_curve.py](file:///c:/Projects/FreeExile/server/world/level_progression_curve.py).

### Kết Quả Kiểm Thử Thực Tế Hiện Tại
Chạy lệnh kiểm thử E2E hiện tại:
```bash
pytest tests/e2e/test_level_progression_e2e.py -v
```
- **Tổng số test cases**: 50 tests.
- **Kết quả**: **44 PASSED**, **6 XFAILED** (0 FAILED).
- **Phân bổ 6 ca `@pytest.mark.xfail`**:
  1. `test_f05_award_monster_exp_contract`: XFAIL do chưa có `LevelProgressionService`.
  2. `test_f05_level_up_awards_stats_and_passives`: XFAIL do chưa có `LevelProgressionService`.
  3. `test_f05_apply_death_penalty_contract`: XFAIL do chưa có `LevelProgressionService`.
  4. `test_f06_trial_10_rejected_at_level_99`: XFAIL do thuộc Milestone M3 (`AscendancyEngine` Lv 100 gate).
  5. `test_f06_trial_10_accepted_at_level_100`: XFAIL do thuộc Milestone M3 (`AscendancyEngine` Lv 100 gate).
  6. `test_f07_godhood_keystone_metamorphosis_flag`: XFAIL do thuộc Milestone M3 (`AscendancyAppliedEffects`).

---

## 2. ĐỐI SOÁT CHI TIẾT CÁC FEATURE F03, F04, F05, F08 TRONG E2E TEST SUITE

| Feature | Tên Test Case trong E2E Suite | Trạng Thái Hiện Tại | Đối Tượng Kiểm Thử Hiện Tại | Kế Hoạch Chuyển Tiếp Sang `LevelProgressionService` |
| :--- | :--- | :--- | :--- | :--- |
| **F03 (Level Gap)** | `test_f03_level_gap_safe_range_full_exp` | **PASSED** | Local Oracle `calc_level_gap_decay` | Kiểm thử hàm `calculate_level_gap_multiplier()` của Service |
| **F03 (Level Gap)** | `test_f03_level_gap_decay_starts_at_diff_6` | **PASSED** | Local Oracle `calc_level_gap_decay` | Kiểm thử biên $\Delta = 6$ đạt tỷ lệ $\exp(-0.60) \approx 0.5488$ |
| **F03 (Level Gap)** | `test_f03_level_gap_decay_diff_10_under_5_percent` | **PASSED** | Local Oracle `calc_level_gap_decay` | Kiểm thử $\Delta = 10 \rightarrow \exp(-3.0) \approx 0.04978 \le 0.05$ |
| **F03 (Level Gap)** | `test_f03_level_gap_extreme_gap_floor` | **PASSED** | Local Oracle `calc_level_gap_decay` | Kiểm thử clamp sàn tối thiểu $0.01$ (1%) |
| **F03 (Level Gap)** | `test_f03_level_gap_underleveled_anti_boosting` | **PASSED** | Local Oracle `calc_level_gap_decay` | Kiểm thử chống cày kéo (underleveled decay clamp $0.05$) |
| **F04 (Death Penalty)** | `test_f04_death_penalty_tiered_ratios` (Param 13 mốc) | **PASSED** | Local Oracle `calc_death_penalty_ratio` | Kiểm thử `get_death_penalty_ratio(level)` từ benchmarks |
| **F04 (Death Penalty)** | `test_f04_death_penalty_safe_floor_at_zero` | **PASSED** | Local Oracle | Kiểm thử `apply_death_penalty()` sàn 0%, không tụt cấp |
| **F05 (Service Engine)** | `test_f05_award_monster_exp_contract` | **XFAIL** (M2) | `LevelProgressionService` | **Sẽ PASS 100% khi M2 triển khai Service** |
| **F05 (Service Engine)** | `test_f05_level_up_awards_stats_and_passives` | **XFAIL** (M2) | `LevelProgressionService` | **Sẽ PASS 100% khi M2 triển khai Service** |
| **F05 (Service Engine)** | `test_f05_apply_death_penalty_contract` | **XFAIL** (M2) | `LevelProgressionService` | **Sẽ PASS 100% khi M2 triển khai Service** |
| **F08 (Simulation)** | `test_f08_simulation_zero_death_100_percent_success` | **PASSED** | Math validation | Phục vụ kiểm chứng Milestone M4 |
| **F08 (Simulation)** | `test_f08_simulation_high_death_rate_catastrophic_setback` | **PASSED** | Math validation | Phục vụ kiểm chứng Milestone M4 |

---

## 3. PHÁT HIỆN TRỌNG YẾU: BẤT ĐỒNG BỘ CONTRACT & GIẢI PHÁP ĐỒNG HÓA

Qua rà soát mã nguồn đối chiếu giữa [PROJECT.md](file:///c:/Projects/FreeExile/.agents/teamwork/orchestrator_4/PROJECT.md), [tests/e2e/test_level_progression_e2e.py](file:///c:/Projects/FreeExile/tests/e2e/test_level_progression_e2e.py), và nhiệm vụ của Explorer 1, phát hiện **3 điểm bất đồng bộ cực kỳ quan trọng**:

### 3.1. Thứ Tự Tham Số Positional của `award_monster_exp`
- **Trong `PROJECT.md` dòng 47**: `award_monster_exp(player_id, monster_level, base_exp, zone_level)`
- **Trong `test_level_progression_e2e.py` dòng 222 & 231**:
  - Dòng 222: `res = service.award_monster_exp("p1", 80, 80, 1000)`
  - Dòng 231: `res = service.award_monster_exp("p2", 1, 1, d1 + 10)`
  *Phân tích*: Tham số thứ 3 là `zone_level` (80 hoặc 1), tham số thứ 4 là `base_exp` (1000 hoặc $d1+10$). Nếu gán cứng tham số thứ 3 là `base_exp`, player chỉ nhận 80 EXP hoặc 1 EXP, khiến `assert res.effective_exp == 1000` và `assert res.new_level == 2` **sẽ FAIL ngay lập tức**!
- **Giải Pháp Đồng Hóa Bắt Buộc Cho Worker M2**:
  Hàm trong `LevelProgressionService` cần hỗ trợ linh hoạt cả 2 trường hợp gọi positional và keyword:
  ```python
  def award_monster_exp(
      self,
      player_id: str,
      monster_level: int,
      arg3: Optional[int] = None,
      arg4: Optional[int] = None,
      *,
      base_exp: Optional[int] = None,
      zone_level: Optional[int] = None,
  ) -> ExpAwardResult:
      # Nếu keyword được truyền, ưu tiên sử dụng keyword
      final_base_exp = base_exp
      final_zone_level = zone_level
      if arg3 is not None and arg4 is not None:
          # Kiểm tra nếu arg4 > 100 và arg3 <= 100: arg3 là zone_level, arg4 là base_exp (chuẩn E2E test)
          if final_base_exp is None and final_zone_level is None:
              if arg4 > 100 >= arg3:
                  final_zone_level, final_base_exp = arg3, arg4
              else:
                  # Hoặc fallback theo thứ tự (zone_level, base_exp)
                  final_zone_level, final_base_exp = arg3, arg4
      elif arg3 is not None and final_base_exp is None:
          final_base_exp = arg3
  ```

### 3.2. Thuộc Tính DTO & Alias Naming
- Trong `test_level_progression_e2e.py`:
  - `res.effective_exp == 1000` (dòng 223)
  - `res.level_up_occurred == False` (dòng 223)
  - `res.new_exp >= 0` (dòng 240 trên `DeathPenaltyResult`)
- Trong `PROJECT.md` và dispatch ban đầu:
  - `exp_awarded`, `leveled_up`, `current_exp_after`.
- **Giải Pháp Đồng Hóa**: Sử dụng `@dataclass(slots=True, frozen=True)` kết hợp `@property` alias để hỗ trợ cả 2 chuẩn:
  - `ExpAwardResult`: `effective_exp` $\leftrightarrow$ `exp_awarded`, `level_up_occurred` $\leftrightarrow$ `leveled_up`.
  - `DeathPenaltyResult`: `new_exp` $\leftrightarrow$ `current_exp_after`.

### 3.3. Cơ Chế Tính Phạt Chết (Death Penalty Base)
- **Quy định chuẩn tại ORIGINAL_REQUEST §R2**: Phạt phần trăm trên thanh EXP của cấp hiện tại (`delta_exp * ratio`).
- Ví dụ: Cấp 99 bị phạt 25% thanh EXP. Nếu người chơi đang có 10% thanh EXP, lượng trừ là 25% của thanh, nhưng do sàn an toàn tối thiểu 0%, người chơi mất toàn bộ 10% EXP hiện có và về đúng 0%, **không bao giờ tụt cấp**.
- Công thức chuẩn:
  ```python
  nominal_penalty = int(math.floor(delta_exp * penalty_ratio))
  exp_lost = min(current_exp, nominal_penalty)
  current_exp_after = max(0, current_exp - nominal_penalty)
  ```

---

## 4. THIẾT KẾ BỘ UNIT TEST CHUYÊN TRÁCH: `tests/unit/test_level_progression_service.py`

Bộ Unit Test chuyên trách được chia làm 5 nhóm lớp kiểm thử với tổng cộng 25+ test cases:

```
tests/unit/test_level_progression_service.py
├── TestLevelGapDecayFormula
│   ├── test_level_gap_exact_match_full_exp
│   ├── test_level_gap_safe_range_within_5_levels
│   ├── test_level_gap_decay_boundary_6_levels
│   ├── test_level_gap_decay_10_levels_lower_under_five_percent
│   ├── test_level_gap_extreme_overlevel_clamp
│   └── test_level_gap_underleveled_anti_boosting
├── TestTieredDeathPenalty
│   ├── test_death_penalty_grace_period_level_1_to_60
│   ├── test_death_penalty_mid_tier_61_to_80
│   ├── test_death_penalty_high_tier_81_to_89
│   ├── test_death_penalty_endgame_tier_90_to_98
│   ├── test_death_penalty_pinnacle_tier_99
│   ├── test_death_penalty_godhood_tier_100
│   ├── test_death_penalty_safe_floor_at_zero_exp
│   ├── test_death_penalty_safe_floor_clamping
│   └── test_death_penalty_consecutive_death_streak
├── TestLevelUpTransitions
│   ├── test_level_up_1_to_2_exact_exp
│   ├── test_level_up_1_to_2_with_rollover
│   ├── test_level_up_stats_and_talent_points
│   ├── test_multi_level_jump
│   └── test_pinnacle_transition_99_to_100
├── TestLevelCapAndTerminalState
│   ├── test_level_100_cap_no_further_exp_gain
│   ├── test_level_100_cap_flags_and_metrics
│   └── test_level_100_death_penalty_immunity
└── TestServiceStateAndPersistence
    ├── test_player_initialization_defaults
    ├── test_get_level_info_dto_immutability
    └── test_interface_polymorphism_and_aliases
```

---

## 5. MÃ NGUỒN CHI TIẾT BỘ TEST (PROPOSED UNIT TEST SUITE CODE)

Dưới đây là thiết kế hoàn chỉnh file [tests/unit/test_level_progression_service.py](file:///c:/Projects/FreeExile/tests/unit/test_level_progression_service.py) để Worker M2 áp dụng:

```python
"""
Unit Test Suite for FreeExile Level Progression Service & Death Penalty Engine.
Verifies LevelProgressionService, Piecewise EXP Curve, Level Gap Decay,
and Tiered Death Penalty Safe Floor adhering to PoE2 2026 Standards.
"""

from __future__ import annotations
import math
import pytest
from server.world.game_design_matrix_service import GameDesignMatrixService
from server.world.level_progression_service import LevelProgressionService


@pytest.fixture
def progression_service():
    """Provides a fresh LevelProgressionService with in-memory matrix."""
    matrix = GameDesignMatrixService(db_path=":memory:")
    matrix.seed_canonical_data(force=True)
    service = LevelProgressionService(matrix_service=matrix)
    yield service
    matrix.close()


class TestLevelGapDecayFormula:
    """Verifies exponential decay formula: eta(Delta) = exp(-0.60 * (Delta - 5))."""

    def test_level_gap_exact_match_full_exp(self, progression_service: LevelProgressionService):
        """Player level == Monster level yields 100% (1.0) multiplier."""
        assert progression_service.calculate_level_gap_multiplier(80, 80) == 1.0

    def test_level_gap_safe_range_within_5_levels(self, progression_service: LevelProgressionService):
        """Level differences within [-5, +5] receive 100% full EXP."""
        for diff in range(-5, 6):
            mult = progression_service.calculate_level_gap_multiplier(80, 80 + diff)
            assert mult == 1.0

    def test_level_gap_decay_boundary_6_levels(self, progression_service: LevelProgressionService):
        """Gap of 6 levels triggers exponential decay: exp(-0.60) ~= 0.5488."""
        mult = progression_service.calculate_level_gap_multiplier(86, 80)
        assert abs(mult - math.exp(-0.60)) < 1e-4

    def test_level_gap_decay_10_levels_lower_under_five_percent(self, progression_service: LevelProgressionService):
        """Monster 10 levels lower yields <= 5% EXP (exp(-3.0) ~= 0.04978 <= 0.05)."""
        mult = progression_service.calculate_level_gap_multiplier(90, 80)
        assert mult <= 0.05
        assert abs(mult - math.exp(-3.0)) < 1e-4

    def test_level_gap_extreme_overlevel_clamp(self, progression_service: LevelProgressionService):
        """Extreme level gap clamps to absolute floor 0.01 (1%)."""
        mult = progression_service.calculate_level_gap_multiplier(95, 20)
        assert mult == 0.01

    def test_level_gap_underleveled_anti_boosting(self, progression_service: LevelProgressionService):
        """Underleveled player decay prevents power-leveling (clamped to 0.05)."""
        mult = progression_service.calculate_level_gap_multiplier(20, 80)
        assert mult <= 0.10 and mult >= 0.05


class TestTieredDeathPenalty:
    """Verifies tiered percentage penalties and absolute safe floor (never de-level)."""

    @pytest.mark.parametrize("level", [1, 20, 45, 60])
    def test_death_penalty_grace_period_level_1_to_60(self, progression_service: LevelProgressionService, level: int):
        """Levels 1 to 60 have 0% death penalty (grace period)."""
        assert progression_service.get_death_penalty_ratio(level) == 0.0

    @pytest.mark.parametrize("level", [61, 70, 80])
    def test_death_penalty_mid_tier_61_to_80(self, progression_service: LevelProgressionService, level: int):
        """Levels 61 to 80 lose 5% of current level EXP bar."""
        assert progression_service.get_death_penalty_ratio(level) == 0.05

    @pytest.mark.parametrize("level", [81, 85, 89])
    def test_death_penalty_high_tier_81_to_89(self, progression_service: LevelProgressionService, level: int):
        """Levels 81 to 89 lose 10% of current level EXP bar."""
        assert progression_service.get_death_penalty_ratio(level) == 0.10

    @pytest.mark.parametrize("level", [90, 95, 98])
    def test_death_penalty_endgame_tier_90_to_98(self, progression_service: LevelProgressionService, level: int):
        """Levels 90 to 98 lose 15% of current level EXP bar."""
        assert progression_service.get_death_penalty_ratio(level) == 0.15

    def test_death_penalty_pinnacle_tier_99(self, progression_service: LevelProgressionService):
        """Level 99 loses 25% of current level EXP bar."""
        assert progression_service.get_death_penalty_ratio(99) == 0.25

    def test_death_penalty_godhood_tier_100(self, progression_service: LevelProgressionService):
        """Level 100 has 0% death penalty (immortal cap)."""
        assert progression_service.get_death_penalty_ratio(100) == 0.0

    def test_death_penalty_safe_floor_at_zero_exp(self, progression_service: LevelProgressionService):
        """Dying at 0% EXP results in 0 lost EXP; player never drops below level."""
        pid = "player_zero_exp"
        progression_service.set_player_state(pid, level=95, current_exp=0)
        res = progression_service.apply_death_penalty(pid)
        assert res.exp_lost == 0
        assert res.new_exp == 0
        assert res.de_leveled is False
        assert progression_service.get_level_info(pid).level == 95

    def test_death_penalty_safe_floor_clamping(self, progression_service: LevelProgressionService):
        """Dying at 10% EXP at Level 99 (25% penalty) clamps to 0% without de-leveling."""
        pid = "player_clamp_test"
        d99 = progression_service.get_delta_exp(99)
        initial_exp = int(d99 * 0.10)
        progression_service.set_player_state(pid, level=99, current_exp=initial_exp)
        res = progression_service.apply_death_penalty(pid)
        assert res.new_exp == 0
        assert res.exp_lost == initial_exp
        assert res.de_leveled is False
        assert progression_service.get_level_info(pid).level == 99

    def test_death_penalty_consecutive_death_streak(self, progression_service: LevelProgressionService):
        """5 consecutive deaths at Level 99 clamp at 0% and track deaths_count."""
        pid = "player_streak"
        d99 = progression_service.get_delta_exp(99)
        progression_service.set_player_state(pid, level=99, current_exp=int(d99 * 0.50))
        for _ in range(5):
            progression_service.apply_death_penalty(pid)
        state = progression_service.get_level_info(pid)
        assert state.level == 99
        assert state.current_exp == 0
        assert state.deaths_count == 5


class TestLevelUpTransitions:
    """Verifies level advancement, rollover EXP, and passive/stat grants."""

    def test_level_up_1_to_2_exact_exp(self, progression_service: LevelProgressionService):
        """Awarding exactly 600 EXP advances Level 1 to Level 2 with 0 rollover."""
        pid = "player_exact"
        res = progression_service.award_monster_exp(pid, 1, 1, 600)
        assert res.level_up_occurred is True
        assert res.new_level == 2
        assert res.current_exp == 0

    def test_level_up_1_to_2_with_rollover(self, progression_service: LevelProgressionService):
        """Awarding 650 EXP advances Level 1 to Level 2 with 50 rollover EXP."""
        pid = "player_rollover"
        res = progression_service.award_monster_exp(pid, 1, 1, 650)
        assert res.level_up_occurred is True
        assert res.new_level == 2
        assert res.current_exp == 50

    def test_level_up_stats_and_talent_points(self, progression_service: LevelProgressionService):
        """Level up grants +5 stat points and +1 unspent talent point."""
        pid = "player_stats"
        progression_service.award_monster_exp(pid, 1, 1, 600)
        state = progression_service.get_level_info(pid)
        assert state.unspent_talent_points == 1
        assert state.total_talent_points == 1

    def test_multi_level_jump(self, progression_service: LevelProgressionService):
        """Large EXP award cascades through multiple levels accurately."""
        pid = "player_multi"
        d1 = progression_service.get_delta_exp(1)  # 600
        d2 = progression_service.get_delta_exp(2)  # 2662
        res = progression_service.award_monster_exp(pid, 1, 1, d1 + d2 + 100)
        assert res.new_level == 3
        assert res.current_exp == 100
        assert res.levels_gained == 2


class TestLevelCapAndTerminalState:
    """Verifies strict Level 100 cap and zero overflow."""

    def test_level_100_cap_no_further_exp_gain(self, progression_service: LevelProgressionService):
        """Kills at Level 100 award 0 effective EXP."""
        pid = "player_lv100"
        progression_service.set_player_state(pid, level=100, current_exp=0)
        res = progression_service.award_monster_exp(pid, 100, 100, 100000)
        assert res.effective_exp == 0
        assert res.level_up_occurred is False
        assert res.new_level == 100

    def test_level_100_death_penalty_immunity(self, progression_service: LevelProgressionService):
        """Level 100 player suffers 0 penalty upon death."""
        pid = "player_lv100_death"
        progression_service.set_player_state(pid, level=100, current_exp=0)
        res = progression_service.apply_death_penalty(pid)
        assert res.exp_lost == 0
        assert res.new_exp == 0
        assert progression_service.get_level_info(pid).level == 100


class TestServiceStateAndPersistence:
    """Verifies DTO properties, aliases, and polymorphic call signatures."""

    def test_interface_polymorphism_and_aliases(self, progression_service: LevelProgressionService):
        """Verifies compatibility with E2E signatures and alias properties."""
        pid = "player_alias"
        res = progression_service.award_monster_exp(pid, 80, 80, 1000)
        assert res.effective_exp == res.exp_awarded == 1000
        assert res.level_up_occurred == res.leveled_up is False

        death_res = progression_service.apply_death_penalty(pid)
        assert death_res.new_exp == death_res.current_exp_after
```

---

## 6. LỆNH THỰC THI KIỂM CHỨNG TOÀN DIỆN CHO WORKER M2 & REVIEWERS

Sau khi Worker M2 hoàn tất cài đặt `server/world/level_progression_types.py` và `server/world/level_progression_service.py`, quy trình nghiệm thu gồm 6 bước độc lập:

### Bước 1: Kiểm thử Unit Test Chuyên Trách
```bash
pytest tests/unit/test_level_progression_service.py -v
```
*Tiêu chí*: 100% tests PASS (0 Failed).

### Bước 2: Kiểm thử Hài Hòa E2E F03, F04, F05
```bash
pytest tests/e2e/test_level_progression_e2e.py -v -k "f03 or f04 or f05"
```
*Tiêu chí*: 24/24 tests PASS (hoặc 3 XPASS/PASSED cho F05, 0 Failed).

### Bước 3: Kiểm thử Toàn Bộ E2E Suite
```bash
pytest tests/e2e/test_level_progression_e2e.py -v
```
*Tiêu chí*: 47 PASSED, 3 XFAIL (F06 Trial 10 & F07 Keystone thuộc Milestone M3).

### Bước 4: Kiểm Soát Kiểu Dữ Liệu Ngặt Nghèo (Mypy Strict)
```bash
mypy --strict server/world/level_progression_types.py server/world/level_progression_service.py
```
*Tiêu chí*: `Success: no issues found`.

### Bước 5: Kiểm Toán Vệ Sinh Mã Nguồn (Code & Doc Hygiene Gate)
```bash
python tools/lint/check_code_and_doc_hygiene.py --strict
```
*Tiêu chí*: 0 Hard Cap violations (`level_progression_types.py` $\le 350$, `level_progression_service.py` $\le 350-500$, docs $\le 400-600$).

### Bước 6: Cổng Kiểm Toán An Ninh Độc Lập (Security Veto Gate)
```bash
python tools/security/run_independent_security_audit.py --build-id "M2_PROGRESSION_RELEASE" --env STAGING
```
*Tiêu chí*: 0 Critical, 0 High vulnerabilities, Exit Code 0.
