diff --git a/tests/test_tools/test_animation_info.py b/tests/test_tools/test_animation_info.py
new file mode 100644
index 0000000..e0e7b19
--- /dev/null
+++ b/tests/test_tools/test_animation_info.py
@@ -0,0 +1,187 @@
+"""Tests for godotiq_animation_info tool."""
+
+from __future__ import annotations
+
+import pytest
+
+from godotiq.tools.animation import _build_animation_info, _extract_code_animations
+
+
+class TestAnimationInfoInlineScene:
+    """Test 1: animated_character.tscn has 4 animations with correct metadata."""
+
+    def test_animation_info_inline_scene(self, anim_session):
+        """animated_character.tscn should return 4 animations via AnimationPlayer."""
+        result = _build_animation_info(anim_session, "res://scenes/animated_character.tscn")
+        assert "error" not in result
+        players = result["animation_players"]
+        assert len(players) == 1
+        anims = players[0]["animations"]
+        names = {a["name"] for a in anims}
+        assert names == {"idle", "walk", "attack", "idle_ground"}
+
+        # Verify idle metadata
+        idle = next(a for a in anims if a["name"] == "idle")
+        assert idle["length"] == 2.0
+        assert idle["loop_mode"] == "linear"
+        assert idle["track_count"] == 4
+
+
+class TestAnimationInfoDetailBrief:
+    """Test 2: brief detail returns names only, no track counts."""
+
+    def test_animation_info_detail_brief(self, anim_session):
+        """Brief detail should return animation names only."""
+        result = _build_animation_info(
+            anim_session, "res://scenes/animated_character.tscn", detail="brief"
+        )
+        anims = result["animation_players"][0]["animations"]
+        for a in anims:
+            assert "name" in a
+            assert "track_count" not in a
+            assert "length" not in a
+
+
+class TestAnimationInfoDetailFull:
+    """Test 3: full detail includes track_paths."""
+
+    def test_animation_info_detail_full(self, anim_session):
+        """Full detail should include track_paths."""
+        result = _build_animation_info(
+            anim_session, "res://scenes/animated_character.tscn", detail="full"
+        )
+        anims = result["animation_players"][0]["animations"]
+        idle = next(a for a in anims if a["name"] == "idle")
+        assert "track_paths" in idle
+        assert len(idle["track_paths"]) > 0
+
+
+class TestAnimationInfoCodeAnimations:
+    """Test 4: character.gd play_calls, ANIM_MAP, LOOPING_ANIMS detected."""
+
+    def test_animation_info_code_animations(self, anim_session):
+        """Code animations from character.gd should be detected."""
+        result = _build_animation_info(anim_session, "res://scenes/animated_character.tscn")
+        code = result["code_animations"]
+        assert code  # not empty
+        assert "idle" in code["play_calls"]
+        assert "attack" in code["play_calls"]
+        assert "special_move" in code["play_calls"]
+        assert len(code["anim_maps"]) > 0
+        assert len(code["looping_lists"]) > 0
+
+
+class TestAnimationInfoCodeOnlyGlb:
+    """Test 5: glb_animated_entity.gd code_animations with NlaTrack entries."""
+
+    def test_animation_info_code_only_glb(self, anim_session):
+        """GLB entity scene should have code animations with NlaTrack references."""
+        result = _build_animation_info(anim_session, "res://scenes/glb_entity.tscn")
+        code = result["code_animations"]
+        assert code
+        assert len(code["anim_maps"]) > 0
+        entries = code["anim_maps"][0]["entries"]
+        assert any("NlaTrack" in v for v in entries.values())
+
+
+class TestAnimationInfoAnimationTree:
+    """Test 6: animated_scene_tree.tscn has 2 states, 2 transitions."""
+
+    def test_animation_info_animation_tree(self, anim_session):
+        """AnimationTree scene should expose states and transitions."""
+        result = _build_animation_info(anim_session, "res://scenes/animated_scene_tree.tscn")
+        trees = result["animation_trees"]
+        assert len(trees) == 1
+        tree = trees[0]
+        assert set(tree["states"]) == {"idle", "walk"}
+        assert len(tree["transitions"]) == 2
+        pairs = {(t["from"], t["to"]) for t in tree["transitions"]}
+        assert ("idle", "walk") in pairs
+        assert ("walk", "idle") in pairs
+
+
+class TestAnimationInfoNoAnimations:
+    """Test 7: no_animation_scene.tscn returns empty results."""
+
+    def test_animation_info_no_animations(self, anim_session):
+        """Scene with no animations should return empty lists and zero counts."""
+        result = _build_animation_info(anim_session, "res://scenes/no_animation_scene.tscn")
+        assert result["animation_players"] == []
+        assert result["animation_trees"] == []
+        assert result["summary"]["total_animations"] == 0
+        assert result["summary"]["total_players"] == 0
+        assert result["summary"]["total_trees"] == 0
+
+
+class TestAnimationInfoNodeFilter:
+    """Test 8: node filter correctly filters to matching player."""
+
+    def test_animation_info_node_filter(self, anim_session):
+        """Filtering by node='AnimationPlayer' should return that player."""
+        result = _build_animation_info(
+            anim_session, "res://scenes/animated_character.tscn", node="AnimationPlayer"
+        )
+        assert len(result["animation_players"]) == 1
+        assert result["animation_players"][0]["name"] == "AnimationPlayer"
+
+
+class TestAnimationInfoNodeFilterNotFound:
+    """Test 9: non-existent node returns error with alternatives."""
+
+    def test_animation_info_node_filter_not_found(self, anim_session):
+        """Filtering by non-existent node should return error with alternatives."""
+        result = _build_animation_info(
+            anim_session, "res://scenes/animated_character.tscn", node="NonExistent"
+        )
+        assert "error" in result
+        assert "alternatives" in result
+        assert "AnimationPlayer" in result["alternatives"]
+
+
+class TestAnimationInfoSummaryCounts:
+    """Test 10: summary counts unique animation names correctly."""
+
+    def test_animation_info_summary_counts(self, anim_session):
+        """Summary should count unique animations across inline + code sources."""
+        result = _build_animation_info(anim_session, "res://scenes/animated_character.tscn")
+        summary = result["summary"]
+        # 4 inline + "special_move" from code = 5 unique
+        assert summary["total_animations"] >= 5
+        assert summary["total_players"] == 1
+        assert summary["inline_count"] == 4
+        assert summary["code_only_count"] >= 1
+        assert "tscn_inline" in summary["sources"]
+        assert "code" in summary["sources"]
+
+
+class TestAnimationInfoMissingScene:
+    """Test 11: non-existent scene path returns error."""
+
+    def test_animation_info_missing_scene(self, anim_session):
+        """Non-existent scene should return error dict."""
+        result = _build_animation_info(anim_session, "res://scenes/does_not_exist.tscn")
+        assert "error" in result
+
+
+class TestAnimationInfoCodePlayDetection:
+    """Test 12: unit test _extract_code_animations with synthetic input."""
+
+    def test_animation_info_code_play_detection(self):
+        """Direct unit test for play/queue/travel detection."""
+        code = '''
+var anim_player: AnimationPlayer
+var tree: AnimationTree
+
+func _ready():
+    anim_player.play("idle")
+    anim_player.play(&"walk")
+    anim_player.queue("attack")
+    tree.get("parameters/playback").travel("run_state")
+'''
+        result = _extract_code_animations(code)
+        assert "idle" in result["play_calls"]
+        assert "walk" in result["play_calls"]
+        assert "attack" in result["play_calls"]
+        assert "run_state" in result["travel_calls"]
+        assert result["has_anim_player"] is True
+        assert result["has_anim_tree"] is True
