# Test Hygiene & Assertion Hardening Analysis Report

**Author**: `explorer_rem_2` (Teamwork Explorer Subagent)  
**Parent Agent**: `orchestrator_22` (`34037784-62e1-41f8-bfe6-912696fdec14`)  
**Target File**: `tests/e2e_cocos/test_vfx_texture_atlas_pipeline_e2e.py`  
**Date**: 2026-10-04T13:10:00Z  
**Classification**: Read-Only Architecture & Remediation Strategy  

---

## 1. Executive Summary & Root Cause Analysis

### 1.1 The Problems
1. **Hygiene Limit Breach**: `tests/e2e_cocos/test_vfx_texture_atlas_pipeline_e2e.py` is currently **575 lines** (574 non-empty lines), violating the 500-line Hard Cap enforced by `tools/lint/check_code_and_doc_hygiene.py --strict` (`CODE_HARD_CAP = 500`).
2. **Shallow Assertion Defect**: `test_t1_monster_and_character_scaffolding_structure` (lines 231–250) used a disjunctive existence check (`assert has_scaffold_script or has_resource_dirs`). This allowed a major regression to pass undetected: **4,000 animation frames** across 10 monster manifests ($V \le 3.75$) and 6 character manifests ($V \le 7.50$) overflowed the normalized UV unit interval $[0.0, 1.0]$.

### 1.2 Core Findings
- **Why the file exceeds 500 lines**: It monolithically bundles 4 test tiers (Tiers 1–4, 20 test cases), extensive inline data tables (`CANONICAL_ACTIVE_SKILLS`, `CANONICAL_MOBILITY_SKILL`, `CANONICAL_SUPPORT_SIGILS` = 29 lines), inline fixtures/helpers (41 lines), and all boundary/workload tests.
- **Why merely splitting tests is insufficient**: Moving only the scaffolding test (20 lines) and parity test (22 lines) leaves `test_vfx_texture_atlas_pipeline_e2e.py` at **532 lines**, which still breaches the 500-line Hard Cap.
- **The Solution**: A **Three-Component Modular Architecture**:
  1. Extract shared tables, path constants, fixtures, and validators into `tests/e2e_cocos/vfx_test_helpers.py` (~110 lines).
  2. Streamline `tests/e2e_cocos/test_vfx_texture_atlas_pipeline_e2e.py` (~340 lines, well under 500-line Hard Cap) with a hardened, comprehensive scaffolding check.
  3. Create companion suite `tests/e2e_cocos/test_vfx_scaffolding_and_parity_e2e.py` (~190 lines, well under 350-line Soft Cap) with granular, parameterized tests for all 10 monsters and 6 characters.

---

## 2. Structural Inspection of `test_vfx_texture_atlas_pipeline_e2e.py`

### 2.1 Line Breakdown (575 Lines)

| Section | Lines in File | Line Count | Contents & Responsibilities |
|---|---|---|---|
| **Module Header & Imports** | 1–30 | 30 | Docstring, future annotations, stdlib, numpy, PIL, pytest |
| **Path Constants** | 31–43 | 13 | `REPO_ROOT`, `COCOS_VFX_DIR`, `COCOS_MANIFEST_PATH`, etc. |
| **Canonical Data Tables** | 44–72 | 29 | `CANONICAL_ACTIVE_SKILLS` (12), `CANONICAL_MOBILITY_SKILL`, `CANONICAL_SUPPORT_SIGILS` (5) |
| **Helpers & Fixtures** | 73–114 | 42 | `match_clip_identifier`, `loaded_manifest`, `loaded_albedo_image`, `loaded_normal_image` |
| **Tier 1: Feature Coverage** | 115–251 | 137 | 6 test methods: Skills, Sigils, PoT, Albedo/Normal, ASTC, Scaffolding |
| **Tier 2: Boundary Cases** | 252–387 | 136 | 5 test methods: Dilation padding, Blue mean > 128, UV bounds, Frame dimensions, Pivot |
| **Tier 3: Combinations** | 388–498 | 111 | 3 test methods: Skill+Sigil pairwise (5 pairs), Normal vector length, WebApp parity |
| **Tier 4: Workload Scenarios** | 499–575 | 77 | 2 test methods: Combat loadout rotation, Cocos TypeScript compilation |
| **Total** | 1–575 | **575** | **Breaches CODE_HARD_CAP (575 > 500)** |

### 2.2 Mathematical Demonstration of the Line Overflow
If an engineer attempts to harden `test_t1_monster_and_character_scaffolding_structure` in place (adding loops over 10 monsters and 6 characters with UV validation), the test expands by ~45 lines, swelling the file to **620 lines**.

If an engineer only removes `test_t1_monster_and_character_scaffolding_structure` (20 lines) and `test_t3_cocos_and_webapp_manifest_cross_platform_parity` (22 lines) to a separate file, the remaining lines are:
$$575 - 20 - 22 = 533 \text{ lines} > 500 \text{ lines (Hard Cap Failure!)}$$

Therefore, **extracting shared data tables and fixtures into `vfx_test_helpers.py` is mandatory** to bring `test_vfx_texture_atlas_pipeline_e2e.py` safely under 500 lines.

---

## 3. Analysis of the Shallow Assertion Defect

### 3.1 Defective Code (`test_vfx_texture_atlas_pipeline_e2e.py:231–250`)
```python
def test_t1_monster_and_character_scaffolding_structure(self) -> None:
    scaffold_script_candidates = [
        REPO_ROOT / "tools" / "asset_pipeline" / "monster_character_pipeline_scaffold.py",
        REPO_ROOT / "tools" / "asset_pipeline" / "generate_monster_and_character_pipelines.py",
    ]
    has_scaffold_script = any(p.exists() for p in scaffold_script_candidates)

    monsters_dir = REPO_ROOT / "client" / "cocos" / "assets" / "resources" / "monsters"
    characters_dir = REPO_ROOT / "client" / "cocos" / "assets" / "resources" / "characters"
    has_resource_dirs = monsters_dir.exists() and characters_dir.exists()

    assert has_scaffold_script or has_resource_dirs, (
        "Extensible scaffolding missing: Expected scaffolding script in tools/asset_pipeline/ "
        "or monster/character resource directory scaffolding in client/cocos/assets/resources/."
    )
```

### 3.2 Four Vulnerabilities in the Existing Assertion
1. **Short-Circuit Logic (`or`)**: `has_scaffold_script` is `True` because the generator script exists in the repository. Python short-circuits, completely bypassing `has_resource_dirs`. The directories could be empty or absent and the test would still pass.
2. **Parent-Only Granularity**: `has_resource_dirs` only tested `monsters` and `characters` top-level directory existence. It never checked individual archetypes or classes.
3. **Missing File Verification**: Did not verify existence or readability of `{genus}_anim_manifest.json`, `pipeline_config.json`, `{class_id}_anim_manifest.json`, or `song_binh_config.json`.
4. **Zero UV Integrity Auditing**: Never parsed manifest contents or checked UV boundedness. This allowed a generator bug in `monster_character_pipeline_scaffold.py:107, 178` to go undetected, where:
   - 10 monster archetypes had $160 / 216$ frames ($74.1\%$) with $V > 1.0$ (up to $V = 3.75$).
   - 6 character classes had $400 / 448$ frames ($89.3\%$) with $V > 1.0$ (up to $V = 7.50$).
   - Total of **4,000 invalid frames** on disk.

---

## 4. Rigorous Assertion Design Specification

### 4.1 Invariants Required for Acceptance

#### Invariant 1: Structural & Directory Completeness (AND Logic)
- Generator script `tools/asset_pipeline/monster_character_pipeline_scaffold.py` must exist and be $\ge 1,024$ bytes.
- All 10 monster archetypes must exist under `client/cocos/assets/resources/monsters/archetypes/`:
  1. `mob_risen_skeleton`
  2. `mob_feral_hellhound`
  3. `mob_blood_crawler`
  4. `mob_bramble_treant`
  5. `mob_corrupted_raptor`
  6. `mob_flesh_abomination`
  7. `mob_ironhide_behemoth`
  8. `mob_primal_cannibal`
  9. `mob_shadow_wraith`
  10. `mob_tomb_lord`
  Each must contain `{genus}_anim_manifest.json` and `pipeline_config.json`.
- All 6 exile characters must exist under `client/cocos/assets/resources/characters/`:
  1. `char_sword_master`
  2. `char_sword_maiden`
  3. `char_feral_berserker`
  4. `char_wild_archer`
  5. `char_glacial_lancer`
  6. `char_shadow_assassin`
  Each must contain `{class_id}_anim_manifest.json` and `song_binh_config.json`.

#### Invariant 2: Mathematical UV Boundedness ($\forall \text{frame} \in \text{manifest}$)
For every frame descriptor across all 16 manifests ($5,520$ total frames):
- **Unit interval bounds**:
  $$0.0 \le u_0 \le 1.0, \quad 0.0 \le v_0 \le 1.0, \quad 0.0 \le u_1 \le 1.0, \quad 0.0 \le v_1 \le 1.0$$
- **Non-degeneracy**:
  $$u_1 > u_0, \quad v_1 > v_0$$
- **Pixel bounds**:
  $$x \ge 0, \quad y \ge 0, \quad x + w \le \text{textureWidth}, \quad y + h \le \text{textureHeight}$$
- **Isometric bottom-center pivot**:
  $$f[\text{"pivot"}] = [0.5, 0.90] \pm 0.001$$
- **8-direction kinematic coverage**:
  All clips must implement canonical directions `["S", "SW", "W", "NW", "N", "NE", "E", "SE"]`.

### 4.2 Reusable UV Validation Helper (`validate_manifest_uv_bounds`)
```python
def validate_manifest_uv_bounds(manifest_path: Path) -> List[str]:
    """Validates UV coordinates and pixel boundaries for all frames in a manifest."""
    if not manifest_path.exists():
        return [f"File missing: {manifest_path}"]
    try:
        with open(manifest_path, "r", encoding="utf-8") as f:
            data = json.load(f)
    except Exception as e:
        return [f"Invalid JSON in {manifest_path}: {e}"]

    tex_w = float(data.get("textureWidth", data.get("texture_width", 2048)))
    tex_h = float(data.get("textureHeight", data.get("texture_height", 2048)))
    frame_w = float(data.get("frameWidth", data.get("frame_width", 160)))
    frame_h = float(data.get("frameHeight", data.get("frame_height", 160)))
    cols = int(data.get("cols", max(1, int(tex_w // frame_w))))

    clips = data.get("clips", {})
    if not clips:
        return [f"{manifest_path.name}: No clips defined in manifest"]

    errors: List[str] = []
    global_idx = 0

    for clip_name, clip_data in clips.items():
        frames = clip_data.get("frames", [])
        if not frames:
            errors.append(f"{manifest_path.name} -> {clip_name}: No frames")
            continue

        for local_idx, f_desc in enumerate(frames):
            if isinstance(f_desc, dict) and "uv" in f_desc:
                u0, v0, u1, v1 = f_desc["uv"]
            elif isinstance(f_desc, dict) and "x" in f_desc:
                x, y = f_desc["x"], f_desc["y"]
                w, h = f_desc.get("w", frame_w), f_desc.get("h", frame_h)
                u0, v0 = x / tex_w, y / tex_h
                u1, v1 = (x + w) / tex_w, (y + h) / tex_h
            else:
                col = (global_idx + local_idx) % cols
                row = (global_idx + local_idx) // cols
                u0 = (col * frame_w) / tex_w
                v0 = (row * frame_h) / tex_h
                u1 = ((col + 1) * frame_w) / tex_w
                v1 = ((row + 1) * frame_h) / tex_h

            if not (0.0 <= u0 <= 1.0 and 0.0 <= v0 <= 1.0 and 0.0 <= u1 <= 1.0 and 0.0 <= v1 <= 1.0):
                errors.append(
                    f"{manifest_path.name} -> {clip_name}[{local_idx}]: UV out of bounds [0.0, 1.0]: "
                    f"({u0:.4f}, {v0:.4f}, {u1:.4f}, {v1:.4f}) [atlas: {tex_w:.0f}x{tex_h:.0f}]"
                )
            if u1 <= u0 or v1 <= v0:
                errors.append(f"{manifest_path.name} -> {clip_name}[{local_idx}]: Degenerate UV bounds")

        global_idx += len(frames)
    return errors
```

---

## 5. Modularization Blueprint

```
tests/e2e_cocos/
├── vfx_test_helpers.py                     # ~110 lines (Shared constants, tables, fixtures, UV validator)
├── test_vfx_texture_atlas_pipeline_e2e.py  # ~340 lines (Tiers 1-4 Core VFX, Hardened Scaffolding Check)
└── test_vfx_scaffolding_and_parity_e2e.py  # ~190 lines (Deep Scaffolding Invariants & WebApp Parity)
```

### 5.1 Component 1: `tests/e2e_cocos/vfx_test_helpers.py` (~110 lines)
- **Exports**:
  - `CANONICAL_ACTIVE_SKILLS` (12 skills)
  - `CANONICAL_MOBILITY_SKILL` (1099)
  - `CANONICAL_SUPPORT_SIGILS` (5 sigils)
  - `CANONICAL_MONSTER_GENERA` (10 genera)
  - `CANONICAL_EXILE_CLASSES` (6 classes)
  - `CANONICAL_DIRECTIONS` (8 directions)
  - Path constants: `COCOS_VFX_DIR`, `COCOS_MANIFEST_PATH`, `COCOS_ALBEDO_PATH`, `COCOS_NORMAL_PATH`, `COCOS_ASTC_PATH`, `COCOS_MONSTERS_DIR`, `COCOS_CHARACTERS_DIR`, `WEBAPP_VFX_DIR`
  - Helpers: `match_clip_identifier`, `validate_manifest_uv_bounds`
  - Fixtures: `loaded_manifest`, `loaded_albedo_image`, `loaded_normal_image`

### 5.2 Component 2: `tests/e2e_cocos/test_vfx_texture_atlas_pipeline_e2e.py` (~340 lines)
- **Preserved Interfaces**: Maintains the exact module path and class names referenced in `TEST_READY.md` and `TEST_INFRA.md`.
- **Test Inventory (19 Tests)**:
  - `TestTier1FeatureCoverage`:
    - `test_t1_all_twelve_active_skills_present_in_manifest`
    - `test_t1_all_five_support_sigils_present_in_manifest`
    - `test_t1_atlas_dimensions_power_of_two`
    - `test_t1_albedo_and_tangent_normal_maps_exist`
    - `test_t1_astc_texture_container_generation_and_header`
    - `test_t1_monster_and_character_scaffolding_structure` *(Hardened: audits all 10 monsters & 6 characters for existence + 0 UV errors)*
  - `TestTier2BoundaryAndCornerCases`:
    - `test_t2_edge_texels_dilation_padding_nonzero_rgb_with_zero_alpha`
    - `test_t2_tangent_normal_map_blue_channel_mean_above_threshold`
    - `test_t2_uv_coordinates_bounded_within_unit_interval` *(Skills/Sigils VFX)*
    - `test_t2_frame_dimensions_match_atlas_grid_metrics`
    - `test_t2_bottom_center_pivot_coordinate_precision`
  - `TestTier3CrossFeatureCombinations`:
    - `test_t3_active_skill_and_support_sigil_sequence_compatibility` *(Parametrized 5 pairs)*
    - `test_t3_normal_map_tangent_vector_unit_length_normalization`
  - `TestTier4RealWorldWorkloadScenarios`:
    - `test_t4_full_combat_loadout_sequence_simulation`
    - `test_t4_cocos_creator_typescript_compilation`

### 5.3 Component 3: `tests/e2e_cocos/test_vfx_scaffolding_and_parity_e2e.py` (~190 lines)
- **Test Inventory (9 Tests)**:
  - `TestScaffoldingStructureAndConfig`:
    - `test_scaffolding_generator_scripts_exist`
    - `test_monster_archetypes_directory_and_manifest_completeness`
    - `test_exile_characters_directory_and_manifest_completeness`
    - `test_monster_pipeline_configs_apple_metal_conformance`
    - `test_character_song_binh_configs_weapon_swap_conformance`
  - `TestScaffoldingManifestUvInvariants`:
    - `test_monster_manifests_all_frames_uv_strictly_bounded`
    - `test_character_manifests_all_frames_uv_strictly_bounded`
    - `test_scaffolding_frame_pivots_and_directions_integrity`
  - `TestCrossPlatformManifestParity`:
    - `test_cocos_and_webapp_manifest_cross_platform_parity`

### 5.4 Line Count and Hygiene Compliance Comparison

| File | Before Refactor | After Refactor | Status vs Soft Cap (350) | Status vs Hard Cap (500) |
|---|---|---|---|---|
| `test_vfx_texture_atlas_pipeline_e2e.py` | 575 lines | **~340 lines** | ✅ PASS (Under 350) | ✅ PASS (Strictly < 500) |
| `test_vfx_scaffolding_and_parity_e2e.py` | — (new) | **~190 lines** | ✅ PASS (Under 350) | ✅ PASS (Strictly < 500) |
| `vfx_test_helpers.py` | — (new) | **~110 lines** | ✅ PASS (Under 350) | ✅ PASS (Strictly < 500) |
| **Total** | 575 lines | 640 lines | All $\le 340$ lines | **Zero Hard Cap Violations** |

---

## 6. Implementation Notes for Worker Agent

When remediating the underlying generator defect in `tools/asset_pipeline/monster_character_pipeline_scaffold.py`:
1. In `scaffold_monsters`:
   - 216 frames of 160×160 cannot fit in a 2048px tall texture if incrementing a row per direction.
   - Set `"textureWidth": 2048, "textureHeight": 8192` (Power of Two) or pack frames into a 2D grid.
   - Change UV calculation: `y / float(manifest["textureHeight"])` instead of `y / 2048.0`.
2. In `scaffold_characters`:
   - 448–560 frames of 160×192 require `"textureWidth": 2048, "textureHeight": 16384` (Power of Two) or multi-column packing.
   - Change UV calculation: `y / float(manifest["textureHeight"])` instead of `y / 2048.0`.
3. Run `python tools/asset_pipeline/monster_character_pipeline_scaffold.py` to regenerate all 16 manifest files.
4. Run the hardened test suites to verify that 100% of the 5,520 animation frames satisfy $0.0 \le u_0, v_0, u_1, v_1 \le 1.0$.

---

## 7. Quality Gate Verification Commands

```bash
# 1. Verify Code & Doc Hygiene (must have exit code 0 and 0 Hard Cap violations)
python tools/lint/check_code_and_doc_hygiene.py --strict

# 2. Run Primary VFX E2E Test Suite
pytest tests/e2e_cocos/test_vfx_texture_atlas_pipeline_e2e.py -v

# 3. Run Scaffolding & Parity E2E Test Suite
pytest tests/e2e_cocos/test_vfx_scaffolding_and_parity_e2e.py -v

# 4. Run entire Cocos E2E Test Suite
pytest tests/e2e_cocos/ -v
```
