# Section 06: Comprehensive Test Suite

## Overview

This section creates the two main test files that exercise the Scene Resolver (Sprint 1B) and Project Index (Sprint 1C) modules, plus an additional test for the value parser `raw` key. All tests are written against the `tests/fixtures/sample_project/` fixture directory and the modules implemented in prior sections.

This section depends on:
- **Section 01 (Value Parser Raw):** The `"raw"` key must be present in `decompose_transform3d()` output.
- **Section 02 (Test Fixtures):** The `tests/fixtures/sample_project/` directory and the `sample_project_root` conftest fixture must exist.
- **Section 03 (Scene Resolver Core):** The `scene_resolver.py` module with `ResolvedScene`, `WorldNode`, `resolve_scene()`, and `_clone_node()` must be implemented.
- **Section 04 (World Transforms):** The `calculate_world_transforms()`, `find_nodes_by_type()`, `find_nodes_by_group()`, and `find_nodes_in_radius()` functions must be implemented in `scene_resolver.py`.
- **Section 05 (Project Index):** The `project_index.py` module with `ProjectIndex`, `scan_project()`, `rescan_changed()`, `find_asset_usage()`, `find_unused_assets()`, and `get_scene_dependency_chain()` must be implemented.

This section blocks nothing -- it is the final section in the implementation order.

---

## Files to Create

| File | Action |
|---|---|
| `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_parsers/test_scene_resolver.py` | CREATE |
| `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_parsers/test_project_index.py` | CREATE |

## File to Modify

| File | Action |
|---|---|
| `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_parsers/test_value_parser.py` | ADD test to existing `TestTransform3DDecomposition` class |

---

## Conftest Additions

**File:** `/Users/salvatorefarruggio/Desktop/godotiq/tests/conftest.py`

Add two new fixtures to the existing conftest (which already has `project_root` and `tmp_config`):

```python
@pytest.fixture
def sample_project_root() -> Path:
    """Return the Path to the sample_project fixture directory."""
    return Path(__file__).resolve().parent / "fixtures" / "sample_project"


@pytest.fixture(scope="module")
def sample_project_index(sample_project_root: Path):
    """Scan the sample project and return a cached ProjectIndex.

    Module-scoped so all tests in a module share the same scan result.
    """
    from godotiq.parsers.project_index import scan_project
    return scan_project(sample_project_root)
```

Note: `sample_project_index` is module-scoped. If pytest complains about mixing function-scoped and module-scoped fixtures, use `sample_project_root` as a direct `Path` construction inside the fixture body rather than injecting it.

---

## Test 1: Value Parser Raw Key

### File: `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_parsers/test_value_parser.py`

Add the following test to the existing `TestTransform3DDecomposition` class (currently at line 118 of the file):

```python
def test_decompose_transform3d_has_raw(self) -> None:
    """Verify that decompose_transform3d() returns the original 12-float tuple under 'raw'."""
    values = (1, 0, 0, 0, 1, 0, 0, 0, 1, -36.117, 0, -1.947)
    result = decompose_transform3d(values)
    assert "raw" in result
    assert result["raw"] == values
```

This test verifies that the `"raw"` key exists in the decomposed transform dict and contains the exact 12-float tuple that was passed in. This key is essential for world-transform composition (section 04) which needs raw basis column vectors rather than decomposed Euler angles.

---

## Test 2: Scene Resolver Tests

### File: `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_parsers/test_scene_resolver.py`

This file tests all scene resolver functionality: res:// path resolution, node cloning, transform math, instance resolution, world transform calculation, and spatial query functions.

### Imports and Setup

The file needs these imports:

```python
"""Tests for the scene resolver module -- instance resolution, transforms, and queries."""

import math
from pathlib import Path

import pytest

from godotiq.parsers.tscn_parser import parse_tscn, TscnNode, TscnScene
from godotiq.parsers.scene_resolver import (
    ResolvedScene,
    WorldNode,
    resolve_scene,
    calculate_world_transforms,
    find_nodes_by_type,
    find_nodes_by_group,
    find_nodes_in_radius,
    _resolve_res_path,
    _clone_node,
)
```

A module-level constant for the fixtures path:

```python
FIXTURES = Path(__file__).resolve().parent.parent / "fixtures"
SAMPLE_PROJECT = FIXTURES / "sample_project"
```

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

```python
class TestResPathResolution:
    """Tests for _resolve_res_path helper."""

    def test_resolve_res_path_basic(self) -> None:
        """res://scenes/main.tscn + project_root -> project_root/scenes/main.tscn"""
        result = _resolve_res_path("res://scenes/main.tscn", SAMPLE_PROJECT)
        assert result == SAMPLE_PROJECT / "scenes" / "main.tscn"

    def test_resolve_res_path_nested(self) -> None:
        """res://scenes/equipment/fdm_printer.tscn -> correct nested path."""
        result = _resolve_res_path("res://scenes/equipment/fdm_printer.tscn", SAMPLE_PROJECT)
        assert result == SAMPLE_PROJECT / "scenes" / "equipment" / "fdm_printer.tscn"
```

### Unit Tests -- Node Cloning

```python
class TestNodeCloning:
    """Tests for _clone_node deep copy behavior."""

    def test_clone_node_independent(self) -> None:
        """Cloned node modifications don't affect original."""
        original = TscnNode(
            name="Test",
            type="Node3D",
            properties={"key": "value"},
            groups=["group1"],
        )
        cloned = _clone_node(original)

        cloned.properties["key"] = "changed"
        cloned.groups.append("group2")

        assert original.properties["key"] == "value"
        assert "group2" not in original.groups

    def test_clone_node_deep(self) -> None:
        """Clone a node with 3 levels of children. All levels are independent copies."""
        grandchild = TscnNode(name="GrandChild", type="Node3D")
        child = TscnNode(name="Child", type="Node3D", children=[grandchild])
        root = TscnNode(name="Root", type="Node3D", children=[child])

        cloned = _clone_node(root)

        # Modify cloned grandchild
        cloned.children[0].children[0].name = "Modified"
        assert grandchild.name == "GrandChild"
```

### Unit Tests -- Transform Math

These tests exercise the internal transform composition functions. The scene resolver works with raw column-major 3x3 basis matrices + origin vectors during world transform calculation. Identity basis is `((1,0,0),(0,1,0),(0,0,1))` with origin `(0,0,0)`.

```python
class TestTransformMath:
    """Tests for basis multiplication and transform composition."""

    def test_compose_identity(self) -> None:
        """Identity * Identity = Identity. World position stays at origin."""
        root = TscnNode(name="Root", type="Node3D")
        child = TscnNode(name="Child", type="Node3D")
        root.children = [child]

        resolved = ResolvedScene(
            source_path="test",
            root=root,
            total_nodes=2,
            instance_sources={},
            unresolved=[],
        )
        world_nodes = calculate_world_transforms(resolved)

        child_wn = [w for w in world_nodes if w.full_path == "Child"][0]
        assert child_wn.world_position == pytest.approx((0.0, 0.0, 0.0))

    def test_compose_translation(self) -> None:
        """Two translations compose by adding origins."""
        root = TscnNode(
            name="Root",
            type="Node3D",
            properties={
                "transform": {
                    "position": (10.0, 0.0, 0.0),
                    "rotation": (0.0, 0.0, 0.0),
                    "scale": (1.0, 1.0, 1.0),
                    "raw": (1, 0, 0, 0, 1, 0, 0, 0, 1, 10, 0, 0),
                }
            },
            position=(10.0, 0.0, 0.0),
        )
        child = TscnNode(
            name="Child",
            type="Node3D",
            properties={
                "transform": {
                    "position": (0.0, 0.0, 5.0),
                    "rotation": (0.0, 0.0, 0.0),
                    "scale": (1.0, 1.0, 1.0),
                    "raw": (1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 5),
                }
            },
            position=(0.0, 0.0, 5.0),
        )
        root.children = [child]

        resolved = ResolvedScene(
            source_path="test",
            root=root,
            total_nodes=2,
            instance_sources={},
            unresolved=[],
        )
        world_nodes = calculate_world_transforms(resolved)

        child_wn = [w for w in world_nodes if w.full_path == "Child"][0]
        assert child_wn.world_position == pytest.approx((10.0, 0.0, 5.0))

    def test_compose_rotation_translation(self) -> None:
        """Parent rotated 90 degrees Y + child at (0,0,1) -> world pos at (1,0,0).

        A 90-degree Y rotation maps the Z axis to the X axis:
        Basis for 90-degree Y rotation (column-major):
          col0 (X): (0, 0, -1)
          col1 (Y): (0, 1, 0)
          col2 (Z): (1, 0, 0)
        Raw: (0, 0, -1, 0, 1, 0, 1, 0, 0, 0, 0, 0)
        """
        raw_90y = (0, 0, -1, 0, 1, 0, 1, 0, 0, 0, 0, 0)
        from godotiq.parsers.value_parser import decompose_transform3d
        decomposed = decompose_transform3d(raw_90y)

        root = TscnNode(
            name="Root",
            type="Node3D",
            properties={"transform": decomposed},
            position=decomposed["position"],
            rotation=decomposed["rotation"],
            scale=decomposed["scale"],
        )
        child = TscnNode(
            name="Child",
            type="Node3D",
            properties={
                "transform": {
                    "position": (0.0, 0.0, 1.0),
                    "rotation": (0.0, 0.0, 0.0),
                    "scale": (1.0, 1.0, 1.0),
                    "raw": (1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1),
                }
            },
            position=(0.0, 0.0, 1.0),
        )
        root.children = [child]

        resolved = ResolvedScene(
            source_path="test",
            root=root,
            total_nodes=2,
            instance_sources={},
            unresolved=[],
        )
        world_nodes = calculate_world_transforms(resolved)

        child_wn = [w for w in world_nodes if w.full_path == "Child"][0]
        assert child_wn.world_position == pytest.approx((1.0, 0.0, 0.0), abs=1e-6)

    def test_basis_multiply_identity(self) -> None:
        """Identity basis * any basis = same basis. Verified through transform composition."""
        raw_90y = (0, 0, -1, 0, 1, 0, 1, 0, 0, 0, 0, 0)
        from godotiq.parsers.value_parser import decompose_transform3d
        decomposed = decompose_transform3d(raw_90y)

        root = TscnNode(name="Root", type="Node3D")
        child = TscnNode(
            name="Child",
            type="Node3D",
            properties={"transform": decomposed},
            position=decomposed["position"],
            rotation=decomposed["rotation"],
            scale=decomposed["scale"],
        )
        root.children = [child]

        resolved = ResolvedScene(
            source_path="test",
            root=root,
            total_nodes=2,
            instance_sources={},
            unresolved=[],
        )
        world_nodes = calculate_world_transforms(resolved)

        child_wn = [w for w in world_nodes if w.full_path == "Child"][0]
        assert child_wn.world_rotation[1] == pytest.approx(math.pi / 2, abs=0.01)

    def test_basis_xform_point(self) -> None:
        """Transform a point by a rotated basis. Verify against known result."""
        raw_90y = (0, 0, -1, 0, 1, 0, 1, 0, 0, 5, 0, 0)
        from godotiq.parsers.value_parser import decompose_transform3d
        decomposed = decompose_transform3d(raw_90y)

        root = TscnNode(
            name="Root",
            type="Node3D",
            properties={"transform": decomposed},
            position=decomposed["position"],
            rotation=decomposed["rotation"],
            scale=decomposed["scale"],
        )
        child = TscnNode(
            name="Child",
            type="Node3D",
            properties={
                "transform": {
                    "position": (0.0, 0.0, 1.0),
                    "rotation": (0.0, 0.0, 0.0),
                    "scale": (1.0, 1.0, 1.0),
                    "raw": (1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1),
                }
            },
            position=(0.0, 0.0, 1.0),
        )
        root.children = [child]

        resolved = ResolvedScene(
            source_path="test",
            root=root,
            total_nodes=2,
            instance_sources={},
            unresolved=[],
        )
        world_nodes = calculate_world_transforms(resolved)

        child_wn = [w for w in world_nodes if w.full_path == "Child"][0]
        assert child_wn.world_position == pytest.approx((6.0, 0.0, 0.0), abs=1e-6)

    def test_reconstruct_from_raw(self) -> None:
        """Given raw 12 floats, extract columns and verify they match expected basis."""
        raw = (0, 0, -1, 0, 1, 0, 1, 0, 0, 5, 3, 2)
        col0 = (raw[0], raw[1], raw[2])
        assert col0 == (0, 0, -1)
        col1 = (raw[3], raw[4], raw[5])
        assert col1 == (0, 1, 0)
        col2 = (raw[6], raw[7], raw[8])
        assert col2 == (1, 0, 0)
        origin = (raw[9], raw[10], raw[11])
        assert origin == (5, 3, 2)
```

### Integration Tests -- Instance Resolution

These tests use the `sample_project` fixture directory and exercise the full `resolve_scene()` pipeline.

```python
class TestInstanceResolution:
    """Integration tests for resolve_scene() with the sample project fixtures."""

    def test_resolve_no_instances(self) -> None:
        """Parse a scene with no instance references. Resolved scene matches original."""
        scene = parse_tscn(str(FIXTURES / "worker.tscn"))
        resolved = resolve_scene(scene, FIXTURES)
        assert resolved.total_nodes == len(scene.nodes)
        assert resolved.unresolved == []
        assert resolved.instance_sources == {}

    def test_resolve_with_instance(self) -> None:
        """Parse 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
        """
        scene = parse_tscn(str(SAMPLE_PROJECT / "scenes" / "main.tscn"))
        resolved = resolve_scene(scene, SAMPLE_PROJECT)

        assert resolved.total_nodes > len(scene.nodes)
        assert len(resolved.instance_sources) > 0
        assert "res://scenes/equipment/fdm_printer.tscn" in resolved.instance_sources.values()

    def test_property_override_transform(self) -> None:
        """Main scene overrides transform on instanced node.
        After resolution, the instance node's position matches the parent's override (-36, 0, -2),
        not the original fdm_printer root position (0, 0, 0).
        """
        scene = parse_tscn(str(SAMPLE_PROJECT / "scenes" / "main.tscn"))
        resolved = resolve_scene(scene, SAMPLE_PROJECT)

        equipment = resolved.root.children[0]
        fdm_printer = equipment.children[0]

        assert fdm_printer.position == pytest.approx((-36.0, 0.0, -2.0))

    def test_instance_type_copied(self) -> None:
        """Instance node in parent has type=''. After resolution, type is copied
        from instanced scene's root (Node3D).
        """
        scene = parse_tscn(str(SAMPLE_PROJECT / "scenes" / "main.tscn"))
        resolved = resolve_scene(scene, SAMPLE_PROJECT)

        equipment = resolved.root.children[0]
        fdm_printer = equipment.children[0]

        assert fdm_printer.type == "Node3D"

    def test_unresolved_instances(self) -> None:
        """Create a TscnScene with an instance pointing to a non-existent file.
        Resolver adds path to unresolved, doesn't crash.
        """
        from godotiq.parsers.tscn_parser import ExtResource, SceneHeader

        scene = TscnScene(
            header=SceneHeader(format=3),
            ext_resources={
                "fake_id": ExtResource(
                    id="fake_id",
                    type="PackedScene",
                    path="res://nonexistent/missing.tscn",
                )
            },
        )
        root = TscnNode(name="Root", type="Node3D")
        instance_node = TscnNode(
            name="MissingInstance", type="", parent=".", instance="fake_id"
        )
        root.children = [instance_node]
        scene.root = root
        scene.nodes = [root, instance_node]

        resolved = resolve_scene(scene, SAMPLE_PROJECT)

        assert "res://nonexistent/missing.tscn" in resolved.unresolved
        assert resolved.root.children[0].name == "MissingInstance"

    def test_max_depth_limit(self) -> None:
        """Set max_depth=1. Scene with instance nesting: first level expanded,
        deeper levels NOT expanded.
        """
        scene = parse_tscn(str(SAMPLE_PROJECT / "scenes" / "main.tscn"))
        resolved = resolve_scene(scene, SAMPLE_PROJECT, max_depth=1)

        assert resolved.total_nodes >= 3

    def test_non_tscn_instance_noted(self) -> None:
        """Instance pointing to .glb file. Not expanded but recorded in instance_sources."""
        scene = parse_tscn(str(SAMPLE_PROJECT / "scenes" / "equipment" / "fdm_printer.tscn"))
        resolved = resolve_scene(scene, SAMPLE_PROJECT)

        instance_values = list(resolved.instance_sources.values())
        glb_instances = [v for v in instance_values if v.endswith(".glb")]
        assert len(glb_instances) > 0
```

### Integration Tests -- World Transform Calculation

```python
class TestWorldTransforms:
    """Integration tests for calculate_world_transforms()."""

    def test_world_position_root(self) -> None:
        """Root node with transform at (5,0,0). World position = (5,0,0)."""
        root = TscnNode(
            name="Root",
            type="Node3D",
            properties={
                "transform": {
                    "position": (5.0, 0.0, 0.0),
                    "rotation": (0.0, 0.0, 0.0),
                    "scale": (1.0, 1.0, 1.0),
                    "raw": (1, 0, 0, 0, 1, 0, 0, 0, 1, 5, 0, 0),
                }
            },
            position=(5.0, 0.0, 0.0),
        )
        resolved = ResolvedScene(
            source_path="test",
            root=root,
            total_nodes=1,
            instance_sources={},
            unresolved=[],
        )
        world_nodes = calculate_world_transforms(resolved)

        root_wn = [w for w in world_nodes if w.full_path == ""][0]
        assert root_wn.world_position == pytest.approx((5.0, 0.0, 0.0))

    def test_world_position_child(self) -> None:
        """Root at (10,0,0), child at local (0,0,5). World position = (10,0,5) (no rotation)."""
        root = TscnNode(
            name="Root",
            type="Node3D",
            properties={
                "transform": {
                    "position": (10.0, 0.0, 0.0),
                    "rotation": (0.0, 0.0, 0.0),
                    "scale": (1.0, 1.0, 1.0),
                    "raw": (1, 0, 0, 0, 1, 0, 0, 0, 1, 10, 0, 0),
                }
            },
            position=(10.0, 0.0, 0.0),
        )
        child = TscnNode(
            name="Child",
            type="Node3D",
            properties={
                "transform": {
                    "position": (0.0, 0.0, 5.0),
                    "rotation": (0.0, 0.0, 0.0),
                    "scale": (1.0, 1.0, 1.0),
                    "raw": (1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 5),
                }
            },
            position=(0.0, 0.0, 5.0),
        )
        root.children = [child]

        resolved = ResolvedScene(
            source_path="test",
            root=root,
            total_nodes=2,
            instance_sources={},
            unresolved=[],
        )
        world_nodes = calculate_world_transforms(resolved)

        child_wn = [w for w in world_nodes if w.full_path == "Child"][0]
        assert child_wn.world_position == pytest.approx((10.0, 0.0, 5.0))

    def test_world_position_with_rotation(self) -> None:
        """Root at origin, rotated 90 degrees around Y. Child at local (0,0,1).
        World position should be approximately (1,0,0).
        """
        raw_90y = (0, 0, -1, 0, 1, 0, 1, 0, 0, 0, 0, 0)
        from godotiq.parsers.value_parser import decompose_transform3d
        decomposed = decompose_transform3d(raw_90y)

        root = TscnNode(
            name="Root",
            type="Node3D",
            properties={"transform": decomposed},
            position=decomposed["position"],
            rotation=decomposed["rotation"],
            scale=decomposed["scale"],
        )
        child = TscnNode(
            name="Child",
            type="Node3D",
            properties={
                "transform": {
                    "position": (0.0, 0.0, 1.0),
                    "rotation": (0.0, 0.0, 0.0),
                    "scale": (1.0, 1.0, 1.0),
                    "raw": (1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1),
                }
            },
            position=(0.0, 0.0, 1.0),
        )
        root.children = [child]

        resolved = ResolvedScene(
            source_path="test",
            root=root,
            total_nodes=2,
            instance_sources={},
            unresolved=[],
        )
        world_nodes = calculate_world_transforms(resolved)

        child_wn = [w for w in world_nodes if w.full_path == "Child"][0]
        assert child_wn.world_position == pytest.approx((1.0, 0.0, 0.0), abs=1e-6)

    def test_world_position_deep_nesting(self) -> None:
        """4 levels of nodes with translations. World position is sum of all parent translations."""
        def make_node(name, tx, ty, tz):
            raw = (1, 0, 0, 0, 1, 0, 0, 0, 1, tx, ty, tz)
            return TscnNode(
                name=name,
                type="Node3D",
                properties={
                    "transform": {
                        "position": (float(tx), float(ty), float(tz)),
                        "rotation": (0.0, 0.0, 0.0),
                        "scale": (1.0, 1.0, 1.0),
                        "raw": raw,
                    }
                },
                position=(float(tx), float(ty), float(tz)),
            )

        level0 = make_node("L0", 1, 0, 0)
        level1 = make_node("L1", 0, 2, 0)
        level2 = make_node("L2", 0, 0, 3)
        level3 = make_node("L3", 4, 0, 0)

        level0.children = [level1]
        level1.children = [level2]
        level2.children = [level3]

        resolved = ResolvedScene(
            source_path="test",
            root=level0,
            total_nodes=4,
            instance_sources={},
            unresolved=[],
        )
        world_nodes = calculate_world_transforms(resolved)

        l3_wn = [w for w in world_nodes if "L3" in w.full_path][0]
        assert l3_wn.world_position == pytest.approx((5.0, 2.0, 3.0))

    def test_world_position_with_scale(self) -> None:
        """Parent scaled 2x. Child at local (1,0,0). World position = parent_pos + (2,0,0)."""
        root = TscnNode(
            name="Root",
            type="Node3D",
            properties={
                "transform": {
                    "position": (0.0, 0.0, 0.0),
                    "rotation": (0.0, 0.0, 0.0),
                    "scale": (2.0, 2.0, 2.0),
                    "raw": (2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0),
                }
            },
            position=(0.0, 0.0, 0.0),
            scale=(2.0, 2.0, 2.0),
        )
        child = TscnNode(
            name="Child",
            type="Node3D",
            properties={
                "transform": {
                    "position": (1.0, 0.0, 0.0),
                    "rotation": (0.0, 0.0, 0.0),
                    "scale": (1.0, 1.0, 1.0),
                    "raw": (1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0),
                }
            },
            position=(1.0, 0.0, 0.0),
        )
        root.children = [child]

        resolved = ResolvedScene(
            source_path="test",
            root=root,
            total_nodes=2,
            instance_sources={},
            unresolved=[],
        )
        world_nodes = calculate_world_transforms(resolved)

        child_wn = [w for w in world_nodes if w.full_path == "Child"][0]
        assert child_wn.world_position == pytest.approx((2.0, 0.0, 0.0))

    def test_world_position_resolved_instance(self) -> None:
        """Resolve main->fdm_printer instance. WorkPoint inside FDMPrinter
        has world position = FDMPrinter world pos + WorkPoint local pos.

        FDMPrinter_1 override position: (-36, 0, -2)
        WorkPoint local position: (0, 0, 1.2)
        Expected world position: (-36, 0, -0.8)
        """
        scene = parse_tscn(str(SAMPLE_PROJECT / "scenes" / "main.tscn"))
        resolved = resolve_scene(scene, SAMPLE_PROJECT)
        world_nodes = calculate_world_transforms(resolved)

        work_point = [w for w in world_nodes if "WorkPoint" in w.full_path]
        assert len(work_point) > 0
        wp = work_point[0]
        assert wp.world_position == pytest.approx((-36.0, 0.0, -0.8), abs=0.01)

    def test_is_instance_root_populated(self) -> None:
        """After resolving and calculating transforms, WorldNode for the instance root
        has is_instance_root=True and correct instance_source.
        """
        scene = parse_tscn(str(SAMPLE_PROJECT / "scenes" / "main.tscn"))
        resolved = resolve_scene(scene, SAMPLE_PROJECT)
        world_nodes = calculate_world_transforms(resolved)

        fdm_nodes = [w for w in world_nodes if "FDMPrinter_1" in w.full_path and w.is_instance_root]
        assert len(fdm_nodes) > 0
        assert fdm_nodes[0].instance_source == "res://scenes/equipment/fdm_printer.tscn"
```

### Integration Tests -- Query Functions

```python
class TestQueryFunctions:
    """Tests for find_nodes_by_type, find_nodes_by_group, find_nodes_in_radius."""

    def _get_sample_world_nodes(self):
        """Helper: resolve sample project main scene and return world nodes."""
        scene = parse_tscn(str(SAMPLE_PROJECT / "scenes" / "main.tscn"))
        resolved = resolve_scene(scene, SAMPLE_PROJECT)
        return calculate_world_transforms(resolved)

    def test_find_nodes_by_type(self) -> None:
        """Filter by type. All results have matching type."""
        world_nodes = self._get_sample_world_nodes()
        results = find_nodes_by_type(world_nodes, "Node3D")
        assert len(results) > 0
        assert all(w.node.type == "Node3D" for w in results)

    def test_find_nodes_by_group(self) -> None:
        """Filter by group. Verify results contain nodes with the group."""
        world_nodes = self._get_sample_world_nodes()
        results = find_nodes_by_group(world_nodes, "interactable")
        assert len(results) > 0
        assert all("interactable" in w.node.groups for w in results)

    def test_find_nodes_in_radius_includes_nearby(self) -> None:
        """Place center near FDMPrinter_1 position (-36, 0, -2). Verify it is included."""
        world_nodes = self._get_sample_world_nodes()
        results = find_nodes_in_radius(world_nodes, center=(-36.0, 0.0, -2.0), radius=2.0)
        assert len(results) > 0
        paths = [w.full_path for w in results]
        assert any("FDMPrinter_1" in p for p in paths)

    def test_find_nodes_in_radius_excludes_far(self) -> None:
        """Place center far from all nodes with small radius. Verify empty result."""
        world_nodes = self._get_sample_world_nodes()
        results = find_nodes_in_radius(world_nodes, center=(9999.0, 9999.0, 9999.0), radius=1.0)
        assert len(results) == 0
```

---

## Test 3: Project Index Tests

### File: `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_parsers/test_project_index.py`

This file tests the project scanning, cross-reference map construction, query functions, and incremental rescan capabilities.

### Imports and Setup

```python
"""Tests for the project index module -- scanning, cross-references, and queries."""

import os
import shutil
from pathlib import Path

import pytest

from godotiq.parsers.project_index import (
    ProjectIndex,
    scan_project,
    rescan_changed,
    find_asset_usage,
    find_unused_assets,
    get_scene_dependency_chain,
    _parse_project_godot,
    _to_res_path,
)
from godotiq.cache.file_cache import FileCache

FIXTURES = Path(__file__).resolve().parent.parent / "fixtures"
SAMPLE_PROJECT = FIXTURES / "sample_project"
```

A module-scoped fixture for the scanned project index:

```python
@pytest.fixture(scope="module")
def sample_index():
    """Scan the sample project once and cache for all tests in the module."""
    return scan_project(SAMPLE_PROJECT)
```

### Unit Tests -- project.godot Parsing

```python
class TestProjectGodotParsing:
    """Tests for _parse_project_godot helper."""

    def test_parse_project_name(self) -> None:
        """Parse fixture project.godot -> project_name = 'Sample Project'."""
        name, _ = _parse_project_godot(SAMPLE_PROJECT / "project.godot")
        assert name == "Sample Project"

    def test_parse_autoloads(self) -> None:
        """Parse fixture -> autoloads dict with correct mappings. '*' prefix stripped."""
        _, autoloads = _parse_project_godot(SAMPLE_PROJECT / "project.godot")
        assert "GameManager" in autoloads
        assert autoloads["GameManager"] == "res://scripts/entities/printer.gd"

    def test_parse_empty_autoloads(self, tmp_path) -> None:
        """project.godot without [autoload] section -> empty dict."""
        godot_file = tmp_path / "project.godot"
        godot_file.write_text('[application]\n\nconfig/name="No Autoloads"\n')
        _, autoloads = _parse_project_godot(godot_file)
        assert autoloads == {}

    def test_parse_project_godot_missing(self, tmp_path) -> None:
        """Non-existent file -> FileNotFoundError."""
        with pytest.raises(FileNotFoundError):
            _parse_project_godot(tmp_path / "nonexistent" / "project.godot")
```

### Unit Tests -- Path Conversion

```python
class TestPathConversion:
    """Tests for _to_res_path helper."""

    def test_to_res_path(self) -> None:
        """Absolute path + project_root -> correct res:// path with forward slashes."""
        abs_path = Path("/some/project/scenes/main.tscn")
        project_root = Path("/some/project")
        result = _to_res_path(abs_path, project_root)
        assert result == "res://scenes/main.tscn"

    def test_to_res_path_nested(self) -> None:
        """Deeply nested file -> correct res:// path."""
        abs_path = Path("/project/assets/models/printers/printer_standard.glb")
        project_root = Path("/project")
        result = _to_res_path(abs_path, project_root)
        assert result == "res://assets/models/printers/printer_standard.glb"
```

### Integration Tests -- Project Scanning

```python
class TestProjectScanning:
    """Integration tests for scan_project()."""

    def test_scan_finds_all_tscn(self, sample_index) -> None:
        """All .tscn files discovered and parsed. total_scenes matches expected count."""
        assert sample_index.total_scenes == 2
        assert "res://scenes/main.tscn" in sample_index.scenes
        assert "res://scenes/equipment/fdm_printer.tscn" in sample_index.scenes

    def test_scan_finds_all_scripts(self, sample_index) -> None:
        """.gd files listed in scripts. Paths are res:// format. total_scripts correct."""
        assert sample_index.total_scripts == 2
        assert "res://scripts/entities/printer.gd" in sample_index.scripts
        assert "res://scripts/entities/worker.gd" in sample_index.scripts

    def test_scan_finds_assets(self, sample_index) -> None:
        """.glb files in assets dict with correct type and size_bytes >= 0."""
        assert sample_index.total_assets >= 2
        glb_path = "res://assets/models/printers/printer_standard.glb"
        assert glb_path in sample_index.assets
        assert sample_index.assets[glb_path]["type"] == "model"
        assert isinstance(sample_index.assets[glb_path]["size_bytes"], int)

    def test_scan_skips_godot_dir(self, tmp_path) -> None:
        """A .godot/ dir with a .tscn inside is NOT included in scan results."""
        project = tmp_path / "project"
        shutil.copytree(SAMPLE_PROJECT, project)

        godot_dir = project / ".godot"
        godot_dir.mkdir()
        (godot_dir / "hidden_scene.tscn").write_text(
            '[gd_scene format=3]\n[node name="Hidden" type="Node3D"]\n'
        )

        index = scan_project(project)
        assert "res://.godot/hidden_scene.tscn" not in index.scenes

    def test_scan_skips_addons(self, tmp_path) -> None:
        """An addons/ dir with a .tscn inside is NOT included in scan results."""
        project = tmp_path / "project"
        shutil.copytree(SAMPLE_PROJECT, project)

        addons_dir = project / "addons"
        addons_dir.mkdir()
        (addons_dir / "plugin_scene.tscn").write_text(
            '[gd_scene format=3]\n[node name="Plugin" type="Node3D"]\n'
        )

        index = scan_project(project)
        assert "res://addons/plugin_scene.tscn" not in index.scenes

    def test_scan_project_not_found(self, tmp_path) -> None:
        """Non-existent project_root -> FileNotFoundError."""
        with pytest.raises(FileNotFoundError):
            scan_project(tmp_path / "nonexistent")
```

### Integration Tests -- Cross-Reference Maps

```python
class TestCrossReferences:
    """Tests for cross-reference map construction."""

    def test_scene_instances_map(self, sample_index) -> None:
        """scene_instances['res://scenes/main.tscn'] contains fdm_printer path."""
        instances = sample_index.scene_instances.get("res://scenes/main.tscn", [])
        assert "res://scenes/equipment/fdm_printer.tscn" in instances

    def test_scene_instance_of_map(self, sample_index) -> None:
        """Inverse: scene_instance_of['fdm_printer'] contains main.tscn."""
        instance_of = sample_index.scene_instance_of.get(
            "res://scenes/equipment/fdm_printer.tscn", []
        )
        assert "res://scenes/main.tscn" in instance_of

    def test_script_to_scenes_map(self, sample_index) -> None:
        """script_to_scenes['printer.gd'] contains fdm_printer.tscn."""
        scenes = sample_index.script_to_scenes.get("res://scripts/entities/printer.gd", [])
        assert "res://scenes/equipment/fdm_printer.tscn" in scenes

    def test_resource_usage(self, sample_index) -> None:
        """resource_usage['printer_standard.glb'] contains fdm_printer.tscn."""
        usage = sample_index.resource_usage.get(
            "res://assets/models/printers/printer_standard.glb", []
        )
        assert "res://scenes/equipment/fdm_printer.tscn" in usage
```

### Integration Tests -- Query Functions

```python
class TestQueryFunctions:
    """Tests for find_asset_usage, find_unused_assets, get_scene_dependency_chain."""

    def test_find_asset_usage(self, sample_index) -> None:
        """find_asset_usage for printer_standard.glb returns non-empty list."""
        usage = find_asset_usage(sample_index, "res://assets/models/printers/printer_standard.glb")
        assert len(usage) > 0
        assert "res://scenes/equipment/fdm_printer.tscn" in usage

    def test_find_unused_assets(self, sample_index) -> None:
        """unused_model.glb appears in unused assets. printer_standard.glb does NOT."""
        unused = find_unused_assets(sample_index)
        assert "res://assets/models/printers/unused_model.glb" in unused
        assert "res://assets/models/printers/printer_standard.glb" not in unused

    def test_scene_dependency_chain(self, sample_index) -> None:
        """Dependency chain from main.tscn contains fdm_printer.tscn."""
        chain = get_scene_dependency_chain(sample_index, "res://scenes/main.tscn")
        assert "res://scenes/equipment/fdm_printer.tscn" in chain
```

### Integration Tests -- FileCache

```python
class TestFileCache:
    """Tests for FileCache integration with scan_project."""

    def test_scan_with_cache(self) -> None:
        """Scan with FileCache. Second scan uses cached results."""
        cache = FileCache()
        index1 = scan_project(SAMPLE_PROJECT, cache=cache)
        index2 = scan_project(SAMPLE_PROJECT, cache=cache)

        assert index1.total_scenes == index2.total_scenes
        assert index1.total_scripts == index2.total_scripts
        assert set(index1.scenes.keys()) == set(index2.scenes.keys())

    def test_incremental_rescan(self, tmp_path) -> None:
        """Scan with cache. Touch one .tscn file. rescan_changed() re-parses only that file."""
        project = tmp_path / "project"
        shutil.copytree(SAMPLE_PROJECT, project)

        cache = FileCache()
        index1 = scan_project(project, cache=cache)

        main_tscn = project / "scenes" / "main.tscn"
        os.utime(str(main_tscn), None)

        index2 = rescan_changed(index1, cache)

        assert index2.total_scenes == index1.total_scenes
        assert "res://scenes/main.tscn" in index2.scenes
```

---

## Key Implementation Notes for Test Authors

### Data Structures Referenced

The tests import and construct instances of these dataclasses (all from `scene_resolver.py` unless noted):

- **`ResolvedScene`**: Fields are `source_path: str`, `root: TscnNode`, `total_nodes: int`, `instance_sources: dict[str, str]`, `unresolved: list[str]`
- **`WorldNode`**: Fields are `node: TscnNode`, `world_position: tuple[float, float, float]`, `world_rotation: tuple[float, float, float]`, `world_scale: tuple[float, float, float]`, `full_path: str`, `depth: int`, `is_instance_root: bool`, `instance_source: str | None`
- **`ProjectIndex`** (from `project_index.py`): Fields listed in section 05
- **`TscnNode`**, **`TscnScene`**, **`ExtResource`** (from `tscn_parser.py`): Existing Sprint 1A dataclasses

### Path Construction for WorldNode

The `full_path` field in `WorldNode` uses these rules:
- Root node: `full_path = ""` (empty string)
- Direct child of root: `full_path = child.name` (e.g., `"Equipment"`)
- Deeper nodes: `full_path = parent_path + "/" + child.name`
- Instance-expanded children continue from the instance node's path, not from the instanced scene's root

### Transform Property Format

When constructing test `TscnNode` objects with transforms, the `properties["transform"]` value must be a dict with keys `"position"`, `"rotation"`, `"scale"`, and `"raw"` (the format returned by `decompose_transform3d()` after section 01's modification). If no transform property exists, the implementation should use identity transform.

### Column-Major Basis Matrix Layout

The 12 raw floats from Godot's `Transform3D` are in column-major order:
- Column 0 (X axis): `raw[0], raw[1], raw[2]`
- Column 1 (Y axis): `raw[3], raw[4], raw[5]`
- Column 2 (Z axis): `raw[6], raw[7], raw[8]`
- Origin: `raw[9], raw[10], raw[11]`

### Private Function Imports

Several tests import private helper functions (`_resolve_res_path`, `_clone_node`, `_parse_project_godot`, `_to_res_path`). These are prefixed with underscore by convention but need to be importable for unit testing. If the implementation makes them truly private (e.g., nested functions), the tests should be restructured to test through the public API instead. The TDD plan assumes they are module-level functions accessible by import.

---

## Verification

After all sections (01-06) are implemented, run the full test suite:

```bash
cd /Users/salvatorefarruggio/Desktop/godotiq && python -m pytest tests/test_parsers/test_value_parser.py tests/test_parsers/test_scene_resolver.py tests/test_parsers/test_project_index.py -v
```

Expected results:
- All existing value parser tests pass (backward compatible)
- The new `test_decompose_transform3d_has_raw` test passes
- All scene resolver tests pass (28 tests across 6 test classes)
- All project index tests pass (20 tests across 6 test classes)
