I now have all the context I need. Let me compose the section content.

# Section 01: Test Fixtures

## Overview

This section creates a self-contained Godot 4 test fixture project at `tests/fixtures/anim_project/` and adds two new pytest fixtures to the shared conftest. The fixture project exercises all animation data sources that the sprint's two MCP tools (`godotiq_animation_info` and `godotiq_animation_audit`) will query: inline TSCN animations, AnimationLibrary references, AnimationPlayer nodes, AnimationTree state machines, GDScript animation code patterns, and edge cases (no animations, empty player, GLB-only code).

No new production code is written in this section. All files are test data or pytest fixtures.

**Depends on:** Nothing (this is the foundation section).
**Blocks:** All other sections (02 through 07).

---

## Files to Create

| File | Purpose |
|------|---------|
| `tests/fixtures/anim_project/project.godot` | Minimal Godot 4 project file |
| `tests/fixtures/anim_project/scenes/animated_character.tscn` | 4 inline animations, AnimationLibrary, AnimationPlayer, Sprite2D, script attachment |
| `tests/fixtures/anim_project/scenes/animated_scene_tree.tscn` | AnimationTree + StateMachine with 2 states and 2 transitions |
| `tests/fixtures/anim_project/scenes/no_animation_scene.tscn` | Static scene with no animations |
| `tests/fixtures/anim_project/scenes/glb_entity.tscn` | Scene with script but NO inline animations (code-only pattern) |
| `tests/fixtures/anim_project/scenes/empty_player_scene.tscn` | AnimationPlayer with no libraries |
| `tests/fixtures/anim_project/scripts/entities/character.gd` | GDScript with ANIM_MAP, LOOPING_ANIMS, .play() calls |
| `tests/fixtures/anim_project/scripts/entities/glb_animated_entity.gd` | GDScript with NlaTrack-based ANIM_MAP (GLB pattern) |

## File to Modify

| File | Change |
|------|--------|
| `tests/test_tools/conftest.py` | Add `anim_root` and `anim_session` pytest fixtures |

---

## Smoke Tests

These tests validate fixture integrity before any tool code exists. They belong in Section 06/07 test files but can also be verified manually during this section's implementation by running a quick script or adding temporary assertions.

From the TDD plan, the validation checks for this section are:

```python
# Smoke: parse_tscn on each fixture file does not raise
# Smoke: animated_character.tscn has sub_resources of type Animation
# Smoke: animated_scene_tree.tscn has sub_resources of type AnimationNodeStateMachine
```

These can be verified by running the existing `parse_tscn()` function from `godotiq.parsers.tscn_parser` against each fixture file after creation. The goal is to ensure every `.tscn` file parses cleanly with the existing parser before downstream sections rely on them.

---

## Detailed File Specifications

### 1. `tests/fixtures/anim_project/project.godot`

Minimal Godot 4 project file. Must have `config/name="AnimTest"` under the `[application]` section. This name is read by `scan_project()` during `GodotIQSession.load()`.

```
[application]

config/name="AnimTest"
```

No autoloads, no main scene needed. Keep it minimal.

---

### 2. `tests/fixtures/anim_project/scenes/animated_character.tscn`

This is the most complex fixture. It must be a valid Godot 4 `.tscn` that the existing `parse_tscn()` parser handles correctly. The file exercises:

- Inline Animation sub_resources with varying track counts, loop modes, and track types
- An AnimationLibrary sub_resource linking animations via `_data`
- An AnimationPlayer node referencing the library
- A Sprite2D child node (target of animation tracks)
- A script ext_resource attachment
- A deliberately broken track (references `NonExistent:position`) for audit testing
- A "no_loop_suspect" animation (`idle_ground`: short, "idle" in name, loop_mode=0)

**Scene header:** `[gd_scene format=3 uid="uid://anim_character"]`

**ext_resource:** One Script ext_resource pointing to `res://scripts/entities/character.gd`.

**Sub_resources (4 Animations):**

1. **`Animation_idle`**: `resource_name = "idle"`, `length = 2.0`, `loop_mode = 1`, 4 value tracks all targeting `Sprite2D` properties (e.g., `Sprite2D:position`, `Sprite2D:modulate`, `Sprite2D:scale`, `Sprite2D:rotation`).

2. **`Animation_walk`**: `resource_name = "walk"`, `length = 1.0`, `loop_mode = 1`, 3 value tracks + 1 method track (`play_footstep`). The method track path should be `.:play_footstep` or similar.

3. **`Animation_attack`**: `resource_name = "attack"`, `length = 0.5`, `loop_mode = 0`, 2 tracks -- one targeting `Sprite2D:position` (valid) and one targeting `NonExistent:position` (broken, for audit test).

4. **`Animation_idle_ground`**: `resource_name = "idle_ground"`, `length = 1.5`, `loop_mode = 0`, 1 value track. This animation has "idle" in its name but does NOT loop, and is under 3s, which triggers the `no_loop_suspect` audit check.

**Animation sub_resource format** (repeat for each animation, adjusting values):

```
[sub_resource type="Animation" id="Animation_idle"]
resource_name = "idle"
length = 2.0
loop_mode = 1
tracks/0/type = "value"
tracks/0/path = NodePath("Sprite2D:position")
tracks/0/interp = 1
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Vector2(0, 0)]
}
tracks/1/type = "value"
tracks/1/path = NodePath("Sprite2D:modulate")
...
```

Each track needs at minimum: `tracks/N/type` and `tracks/N/path` properties. The extraction helpers count tracks by scanning for `tracks/N/type` keys and extract paths from `tracks/N/path`. The `keys` property can be a simplified placeholder (the parser stores multiline values as raw strings or parsed dicts -- either works).

For the method track in `Animation_walk`:
```
tracks/3/type = "method"
tracks/3/path = NodePath(".:play_footstep")
```

**AnimationLibrary sub_resource:**

```
[sub_resource type="AnimationLibrary" id="AnimationLibrary_main"]
_data = { "idle": SubResource("Animation_idle"), "walk": SubResource("Animation_walk"), "attack": SubResource("Animation_attack"), "idle_ground": SubResource("Animation_idle_ground") }
```

Critical detail: The `_data` property starts with underscore. According to the TSCN parser's multiline handling (lines 316-321 in `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/parsers/tscn_parser.py`), properties starting with `_` are stored as raw strings, NOT parsed through `parse_value()`. This means if `_data` fits on a single line and has no unbalanced brackets, it may be stored as a raw string. The extraction helper (Section 02) must handle both formats -- parsed dict and raw string. Keep `_data` on a single line so it is stored as a raw string by the parser.

**AnimationPlayer node:**

```
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
libraries = { "": SubResource("AnimationLibrary_main") }
```

The `libraries` property is a single-line dict. The parser sees `libraries` (does NOT start with `_`, not in the blocklist) so it goes through `parse_value()`. The value parser will parse this as a Python dict: `{"": {"type": "sub_resource", "id": "AnimationLibrary_main"}}`.

**Sprite2D node:**

```
[node name="Sprite2D" type="Sprite2D" parent="."]
```

Minimal -- just needs to exist as a valid track target.

**Root node with script:**

```
[node name="Character" type="Node2D"]
script = ExtResource("1_script")
```

The root has the script attached. The `get_all_scripts()` method on TscnScene resolves `node.script` (the ext_resource id) through `scene.ext_resources` to get the `res://` path.

---

### 3. `tests/fixtures/anim_project/scenes/animated_scene_tree.tscn`

A 3D scene with AnimationTree + StateMachine. Uses real Godot serialization format.

**Scene header:** `[gd_scene format=3 uid="uid://anim_tree_scene"]`

**Sub_resources needed:**

1. **2 Animation sub_resources** (`Animation_idle` and `Animation_walk`) with basic tracks (1 track each is sufficient).

2. **1 AnimationLibrary** wrapping the two animations via `_data`.

3. **2 AnimationNodeAnimation sub_resources** with `animation` property:
   ```
   [sub_resource type="AnimationNodeAnimation" id="AnimNode_idle"]
   animation = &"idle"

   [sub_resource type="AnimationNodeAnimation" id="AnimNode_walk"]
   animation = &"walk"
   ```
   The `&"idle"` syntax is a Godot StringName literal. The value parser's `_STRING_NAME_RE` pattern matches `StringName(&"...")` but NOT bare `&"..."`. However, looking at the parser, bare `&"idle"` without the `StringName()` wrapper will fall through to the fallback and be stored as the raw string `&"idle"`. The extraction helper `_strip_string_name()` must handle this format.

4. **2 AnimationNodeStateMachineTransition sub_resources:**
   ```
   [sub_resource type="AnimationNodeStateMachineTransition" id="Trans_idle_walk"]
   [sub_resource type="AnimationNodeStateMachineTransition" id="Trans_walk_idle"]
   ```
   Minimal -- no extra properties needed.

5. **1 AnimationNodeStateMachine sub_resource** with states and transitions:
   ```
   [sub_resource type="AnimationNodeStateMachine" id="StateMachine_main"]
   states/idle/node = SubResource("AnimNode_idle")
   states/idle/position = Vector2(300, 100)
   states/walk/node = SubResource("AnimNode_walk")
   states/walk/position = Vector2(500, 100)
   transitions = [&"idle", &"walk", SubResource("Trans_idle_walk"), &"walk", &"idle", SubResource("Trans_walk_idle")]
   ```
   The `transitions` property is an array. The value parser will parse `[&"idle", &"walk", SubResource("Trans_idle_walk"), ...]` as a Python list. Each `&"idle"` element will be stored as the raw string `&"idle"` (since bare `&"..."` is not matched by `_STRING_NAME_RE`). Each `SubResource("...")` will be parsed as `{"type": "sub_resource", "id": "..."}`.

**Nodes:**

- Root: `[node name="Scene3D" type="Node3D"]`
- AnimationPlayer: `[node name="AnimationPlayer" type="AnimationPlayer" parent="."]` with `libraries = { "": SubResource("AnimationLibrary_tree") }`
- AnimationTree: `[node name="AnimationTree" type="AnimationTree" parent="."]` with:
  ```
  tree_root = SubResource("StateMachine_main")
  anim_player = NodePath("../AnimationPlayer")
  ```

---

### 4. `tests/fixtures/anim_project/scenes/no_animation_scene.tscn`

Minimal static scene. Used to test that animation tools return empty results gracefully.

```
[gd_scene format=3 uid="uid://no_anim_scene"]

[node name="StaticScene" type="Node3D"]

[node name="Mesh" type="MeshInstance3D" parent="."]

[node name="Body" type="StaticBody3D" parent="."]
```

No AnimationPlayer, no AnimationTree, no scripts.

---

### 5. `tests/fixtures/anim_project/scenes/glb_entity.tscn`

Scene with a script attachment but NO inline animations. Used to test code-only animation detection (the script references NlaTrack animation names from a GLB file).

```
[gd_scene format=3 uid="uid://glb_entity"]

[ext_resource type="Script" path="res://scripts/entities/glb_animated_entity.gd" id="1_glb_script"]

[node name="GLBEntity" type="Node3D"]
script = ExtResource("1_glb_script")
```

---

### 6. `tests/fixtures/anim_project/scenes/empty_player_scene.tscn`

Scene with an AnimationPlayer node that has NO `libraries` property (or empty libraries). Used for the `empty_player` audit check.

```
[gd_scene format=3 uid="uid://empty_player"]

[node name="EmptyScene" type="Node3D"]

[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
```

The AnimationPlayer has no `libraries` property at all. When the extraction helper checks for the `libraries` property on this node, it will find nothing -- triggering the empty_player detection.

---

### 7. `tests/fixtures/anim_project/scripts/entities/character.gd`

GDScript with animation-related code patterns that the `_extract_code_animations()` helper will detect via regex.

Required patterns:

- **ANIM_MAP constant dict** with 4 entries including a "special" key mapping to "special_move":
  ```gdscript
  const ANIM_MAP := {
      "idle": "idle",
      "walk": "walk",
      "attack": "attack",
      "special": "special_move",
  }
  ```

- **LOOPING_ANIMS constant array** with 2 entries:
  ```gdscript
  const LOOPING_ANIMS := ["idle", "walk"]
  ```

- **AnimationPlayer variable declaration:**
  ```gdscript
  var anim_player: AnimationPlayer
  ```

- **`.play()` calls** for "idle", "attack", and "special_move":
  ```gdscript
  anim_player.play("idle")
  anim_player.play("attack")
  anim_player.play("special_move")
  ```

The script should extend `Node2D` (matching the scene root type) and have enough structure to look realistic while containing all the patterns the regex scanner needs to find.

---

### 8. `tests/fixtures/anim_project/scripts/entities/glb_animated_entity.gd`

GDScript demonstrating the GLB/PFT worker pattern where animation names come from GLB NlaTrack names (code-only animations, not in any `.tscn`).

Required patterns:

- **ANIM_MAP constant dict** with NlaTrack entries:
  ```gdscript
  const ANIM_MAP := {
      "idle": "NlaTrack_001",
      "walk": "NlaTrack_002",
      "run": "NlaTrack_003",
  }
  ```

- **LOOPING_ANIMS constant array:**
  ```gdscript
  const LOOPING_ANIMS := ["idle", "walk"]
  ```

- **AnimationPlayer variable declaration:**
  ```gdscript
  var anim_player: AnimationPlayer
  ```

- **`.play()` calls** via helper function (demonstrating indirect play):
  ```gdscript
  func play_animation(anim_name: String) -> void:
      var track_name = ANIM_MAP.get(anim_name, "")
      anim_player.play(track_name)
  ```
  Note: The regex `r'\.play\(\s*(?:&)?"([^"]+)"'` only matches literal string arguments, NOT variable arguments. So `anim_player.play(track_name)` will NOT be detected. Add explicit `.play()` calls with string literals if you want them in `play_calls`:
  ```gdscript
  anim_player.play("NlaTrack_001")
  ```

The script should extend `Node3D`.

---

### 9. Modify `tests/test_tools/conftest.py`

Add two new pytest fixtures after the existing `session` fixture. Follow the exact same pattern as the existing `sample_root`/`session` fixtures.

```python
@pytest.fixture
def anim_root() -> Path:
    """Path to the animation test fixture project."""
    return Path(__file__).resolve().parent.parent / "fixtures" / "anim_project"


@pytest.fixture
def anim_session(anim_root: Path) -> GodotIQSession:
    """A loaded GodotIQSession for the animation test project."""
    s = GodotIQSession(anim_root)
    s.load()
    return s
```

The `anim_session` fixture calls `s.load()` which triggers `scan_project()` to index all `.tscn`, `.gd`, and asset files under `anim_project/`. After loading:

- `anim_session._index.scenes` will contain res:// paths for all 5 `.tscn` files, each mapped to a parsed `TscnScene` object
- `anim_session._index.scripts` will contain res:// paths for both `.gd` files
- `anim_session._gd_scripts` will contain parsed `GdScript` objects for both scripts
- `anim_session._index.project_name` will be `"AnimTest"`

---

## Important Parser Behavior Notes

These details are critical for writing correct fixture files:

1. **`_data` property storage**: The TSCN parser stores properties starting with `_` as raw strings (line 318 of `tscn_parser.py`). So `_data = { "idle": SubResource("Animation_idle") }` will be stored as the raw string `{ "idle": SubResource("Animation_idle") }`, NOT as a parsed dict. The extraction helper in Section 02 must regex-parse this string.

2. **`libraries` property storage**: Since `libraries` does NOT start with `_` and is NOT in the underscore/blocklist, it goes through `parse_value()`. A value like `{ "": SubResource("AnimationLibrary_main") }` will be parsed as: `{"": {"type": "sub_resource", "id": "AnimationLibrary_main"}}`.

3. **`transitions` array storage**: The value `[&"idle", &"walk", SubResource("Trans_idle_walk"), ...]` will be parsed as a Python list. Bare `&"idle"` values fall through the value parser to the raw string fallback, so they appear as the string `&"idle"` in the list. `SubResource(...)` elements are parsed as `{"type": "sub_resource", "id": "..."}` dicts.

4. **`animation = &"idle"` storage**: Bare `&"idle"` as a property value is stored as the raw string `&"idle"` (the value parser's `_STRING_NAME_RE` only matches `StringName(&"...")` wrapper syntax, not bare `&"..."`).

5. **NodePath unwrapping**: `NodePath("Sprite2D:position")` is matched by `_NODE_PATH_RE` and stored as the plain string `"Sprite2D:position"`. The `NodePath()` wrapper is stripped by the value parser.

6. **Track property keys**: Animation tracks use keys like `tracks/0/type`, `tracks/0/path`, `tracks/0/keys`. These are stored as regular properties on the `SubResource.properties` dict. The `keys` property often contains a multiline dict -- if the opening brace `{` has unbalanced depth, the parser accumulates lines until brackets balance. The final value is passed through `parse_value()` (since `keys` does not start with `_`).

---

## Verification Steps

After creating all fixture files, verify correctness:

1. Run `parse_tscn()` on each `.tscn` fixture file and confirm no exceptions
2. Confirm `animated_character.tscn` has 4 sub_resources of type `"Animation"` and 1 of type `"AnimationLibrary"`
3. Confirm `animated_scene_tree.tscn` has sub_resources of type `"AnimationNodeStateMachine"` and `"AnimationNodeAnimation"`
4. Confirm `GodotIQSession(anim_root).load()` completes without errors
5. Confirm `anim_session._index.scenes` contains 5 scene entries
6. Confirm `anim_session._gd_scripts` contains 2 parsed scripts