# Sprint 1B+1C — TDD Plan

## Testing Strategy

Tests are organized into unit tests (per-function), integration tests (cross-module), and fixture-based tests. The existing pytest + pytest-asyncio infrastructure from Sprint 0/1A is reused.

## Test Files

```
tests/
├── fixtures/
│   └── sample_project/        # NEW minimal project for 1B+1C
├── test_parsers/
│   ├── test_scene_resolver.py  # NEW — 1B unit + integration tests
│   └── test_project_index.py   # NEW — 1C unit + integration tests
└── conftest.py                 # ADD sample_project fixture
```

---

## 1B: Scene Resolver Tests

### File: `tests/test_parsers/test_scene_resolver.py`

### Unit Tests — res:// Path Resolution

**`test_resolve_res_path_basic`** — `res://scenes/main.tscn` + project_root → `project_root/scenes/main.tscn`

**`test_resolve_res_path_nested`** — `res://scenes/equipment/fdm_printer.tscn` → correct nested path

### Unit Tests — Node Cloning

**`test_clone_node_independent`** — Cloned node modifications don't affect original. Verify properties dict is independent, children list is independent.

**`test_clone_node_deep`** — Clone a node with 3 levels of children. Verify all levels are independent copies.

### Unit Tests — Transform Math

**`test_compose_identity`** — Identity * Identity = Identity

**`test_compose_translation`** — Two translations compose by adding origins

**`test_compose_rotation_translation`** — Parent rotated 90° Y + child at (0,0,1) → world pos at (1,0,0)

**`test_basis_multiply_identity`** — Identity basis * any basis = same basis

**`test_basis_xform_point`** — Transform a point by a rotated basis. Verify against known result.

**`test_reconstruct_from_raw`** — Given raw 12 floats, extract columns and verify they match expected basis.

### Integration Tests — Instance Resolution

**`test_resolve_no_instances`** — Parse `worker.tscn` (no instances). Resolved scene has same node count as original. `unresolved` is empty. `instance_sources` is empty.

**`test_resolve_with_instance`** — Parse `sample_project/scenes/main.tscn` which instances `fdm_printer.tscn`. After resolution:
- Instance node has children from fdm_printer
- `instance_sources` contains the mapping
- `total_nodes` > original count

**`test_property_override_transform`** — Main scene overrides transform on instanced node. After resolution, the instance node's position matches the parent's override, not the original.

**`test_instance_type_copied`** — Instance node in parent has `type=""`. After resolution, type is copied from instanced scene's root (e.g., `"Node3D"`).

**`test_unresolved_instances`** — Create a TscnScene with an instance pointing to a non-existent file. Resolver adds path to `unresolved`, doesn't crash. Node still present in tree (without expanded children).

**`test_max_depth_limit`** — Set max_depth=1. Scene with 2-level instance nesting: first level expanded, second level NOT expanded (appears in tree but children not populated from its .tscn).

**`test_non_tscn_instance_noted`** — Instance pointing to `.glb` file. Not expanded but recorded in `instance_sources`.

### Integration Tests — World Transform Calculation

**`test_world_position_root`** — Root node with transform=(identity, origin=(5,0,0)). World position = (5,0,0).

**`test_world_position_child`** — Root at (10,0,0), child at local (0,0,5). World position = (10,0,5) (no rotation).

**`test_world_position_with_rotation`** — Root at origin, rotated 90° around Y. Child at local (0,0,1). World position should be approximately (1,0,0) (Z maps to X under 90° Y rotation).

**`test_world_position_deep_nesting`** — 4 levels of nodes with translations. World position is sum of all parent translations.

**`test_world_position_with_scale`** — Parent scaled 2x. Child at local (1,0,0). World position = parent_pos + (2,0,0).

**`test_world_position_resolved_instance`** — Resolve main→fdm_printer instance. Check that WorkPoint inside FDMPrinter has world position = FDMPrinter world pos + WorkPoint local pos (adjusted for any rotation).

**`test_is_instance_root_populated`** — After resolving and calculating transforms, WorldNode for the instance root has `is_instance_root=True` and correct `instance_source`.

### Integration Tests — Query Functions

**`test_find_nodes_by_type`** — After resolving sample scene, filter by type. Verify correct count and all results have matching type.

**`test_find_nodes_by_group`** — After resolving, filter by group. Verify results.

**`test_find_nodes_in_radius_includes_nearby`** — Place center near a known node. Verify that node is included.

**`test_find_nodes_in_radius_excludes_far`** — Place center far from all nodes with small radius. Verify empty result.

---

## 1C: Project Index Tests

### File: `tests/test_parsers/test_project_index.py`

### Unit Tests — project.godot Parsing

**`test_parse_project_name`** — Parse fixture project.godot → project_name = "Sample Project"

**`test_parse_autoloads`** — Parse fixture → autoloads dict with correct name→path mappings. `*` prefix stripped.

**`test_parse_empty_autoloads`** — project.godot without [autoload] section → empty dict.

**`test_parse_project_godot_missing`** — Non-existent file → FileNotFoundError or empty defaults (depending on caller).

### Unit Tests — Path Conversion

**`test_to_res_path`** — Absolute path + project_root → correct res:// path with forward slashes.

**`test_to_res_path_nested`** — Deeply nested file → correct res:// path.

### Integration Tests — Project Scanning

**`test_scan_finds_all_tscn`** — Scan `sample_project/`. All .tscn files discovered and parsed. `total_scenes` matches expected count.

**`test_scan_finds_all_scripts`** — `.gd` files listed in `scripts`. Paths are res:// format. `total_scripts` correct.

**`test_scan_finds_assets`** — `.glb` files in assets dict with correct type="model" and size_bytes > 0. `total_assets` correct.

**`test_scan_skips_godot_dir`** — Create a `.godot/` dir in fixture with a .tscn inside. Verify it's NOT in scan results.

**`test_scan_skips_addons`** — Create an `addons/` dir in fixture with a .tscn inside. Verify it's NOT in scan results.

**`test_scan_project_not_found`** — Non-existent project_root → FileNotFoundError.

### Integration Tests — Cross-Reference Maps

**`test_scene_instances_map`** — After scan, `scene_instances["res://scenes/main.tscn"]` contains `"res://scenes/equipment/fdm_printer.tscn"`.

**`test_scene_instance_of_map`** — Inverse: `scene_instance_of["res://scenes/equipment/fdm_printer.tscn"]` contains `"res://scenes/main.tscn"`.

**`test_script_to_scenes_map`** — `script_to_scenes["res://scripts/entities/printer.gd"]` contains `"res://scenes/equipment/fdm_printer.tscn"`.

**`test_resource_usage`** — `resource_usage["res://assets/models/printers/printer_standard.glb"]` contains `"res://scenes/equipment/fdm_printer.tscn"`.

### Integration Tests — Query Functions

**`test_find_asset_usage`** — `find_asset_usage(index, "res://assets/models/printers/printer_standard.glb")` → non-empty list.

**`test_find_unused_assets`** — `unused_model.glb` appears in `find_unused_assets()` result. `printer_standard.glb` does NOT.

**`test_scene_dependency_chain`** — `get_scene_dependency_chain(index, "res://scenes/main.tscn")` contains `fdm_printer.tscn`.

### Integration Tests — FileCache

**`test_scan_with_cache`** — Scan with FileCache. Scan again. Second scan uses cached results (verify by checking cache hits or by mocking).

**`test_incremental_rescan`** — Scan with cache. Touch one .tscn file (change mtime). Call `rescan_changed()`. Verify only the touched file is re-parsed. (Test by checking that the touched scene's data reflects changes.)

---

## Conftest Additions

Add to `tests/conftest.py`:

**`sample_project_root`** fixture — Returns `Path` to `tests/fixtures/sample_project/`

**`sample_project_index`** fixture (module-scoped) — Calls `scan_project(sample_project_root)` and caches the result.

---

## Value Parser Modification Test

### File: `tests/test_parsers/test_value_parser.py` (ADD to existing)

**`test_decompose_transform3d_has_raw`** — Verify that `decompose_transform3d()` return dict contains `"raw"` key with the original 12-float tuple.
