Now I have all the context I need. Let me produce the section content.

# Section 04 -- Unit Tests for GDScript Parser

## Overview

This section creates the unit test file `tests/test_parsers/test_gd_parser.py` with class-based test groups that exercise every feature of the GDScript parser via inline strings passed to `_parse_gd_content()`. These tests do NOT use fixture files (that is section 05 -- integration tests). Every test constructs its GDScript input as a Python string literal.

**Depends on:** section-02 (state machine core) and section-03 (body patterns) must be implemented before these tests can pass. Section-01 (dataclasses and helpers) is a transitive dependency.

**Blocks:** section-05 (integration tests).

**File to create:** `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_parsers/test_gd_parser.py`

---

## Existing Test Compatibility

The existing Sprint 0 test `tests/test_server.py::TestParserStubs::test_parse_gd_stub_raises` expects `parse_gd("")` to raise `NotImplementedError`. Once the parser is implemented (sections 01-03), this test will break because `parse_gd` will change to accept a path, not content. That is expected and documented in the implementation plan. The existing test in `tests/test_scaffolding_int.py` imports `GdSignal`, `GdFunction`, `GdScript`, and `parse_gd` -- all of these must remain importable from `godotiq.parsers.gd_parser`.

When implementing this section, the Sprint 0 test `test_parse_gd_stub_raises` must be updated to match the new `parse_gd(path)` signature. It should be changed to expect `FileNotFoundError` for a nonexistent path, matching how `parse_tscn` works:

```python
def test_parse_gd_raises_file_not_found(self) -> None:
    with pytest.raises(FileNotFoundError):
        parse_gd("/nonexistent/path/script.gd")
```

---

## Import Setup

The test file imports `_parse_gd_content` (the internal function that accepts raw string content) plus all dataclasses. The convention follows the existing test file structure in the project (e.g., `tests/test_parsers/test_tscn_parser.py`).

```python
"""Unit tests for the GDScript parser — inline string tests."""

from godotiq.parsers.gd_parser import (
    _parse_gd_content,
    parse_gd,
    GdScript,
    GdSignal,
    GdFunction,
    GdVariable,
    GdEnum,
    GdSignalConnection,
    GdSignalEmission,
    GdAutoloadRef,
    GdPreloadRef,
)

import pytest
```

All test functions use `_parse_gd_content(content_string)` or `_parse_gd_content(content_string, known_autoloads=["..."])` to parse inline GDScript snippets. No temporary files are created for unit tests.

---

## Test Classes and Cases

### TestClassDeclarations

Tests class_name and extends parsing at the top level.

**File:** `tests/test_parsers/test_gd_parser.py`

```python
class TestClassDeclarations:
    """Class-level declarations: class_name, extends."""

    # Test: parse class_name from "class_name EconomyManagerClass"
    #   result.class_name == "EconomyManagerClass"

    # Test: parse extends from "extends Node"
    #   result.extends == "Node"

    # Test: combined "class_name Item extends Node"
    #   result.class_name == "Item", result.extends == "Node"

    # Test: extends with file path 'extends "res://path.gd"'
    #   result.extends == '"res://path.gd"'

    # Test: minimal file "extends Node3D" only
    #   result.extends == "Node3D", everything else empty lists/empty strings

    # Test: empty string content
    #   Returns default GdScript(), no exceptions raised

    # Test: parse_gd with nonexistent file path raises FileNotFoundError
    #   pytest.raises(FileNotFoundError) around parse_gd("/nonexistent/file.gd")
```

Each test constructs a small GDScript string (one or two lines), passes it to `_parse_gd_content()`, and asserts the expected `class_name` and/or `extends` fields. The `parse_gd` test uses the public API with a bad path.

---

### TestSignalParsing

Tests signal declarations with varying argument patterns.

```python
class TestSignalParsing:
    """Signal declarations with typed and untyped arguments."""

    # Test: signal without args "signal health_depleted"
    #   len(result.signals) == 1, result.signals[0].args == []

    # Test: signal with untyped args "signal foo(a, b)"
    #   args == [("a", ""), ("b", "")]

    # Test: signal with typed args "signal cash_changed(amount: float, delta: float)"
    #   args == [("amount", "float"), ("delta", "float")]

    # Test: signal with custom type "signal any_state_changed(id: String, old: Printer.State, new: Printer.State)"
    #   type stored as "Printer.State"

    # Test: multiple signals in one file (3 signal lines)
    #   len(result.signals) == 3

    # Test: signal line numbers match source
    #   Input with signal on line 3 -> result.signals[0].line == 3
```

Input strings should contain `extends Node` plus signal declarations. Line numbers are 1-indexed (matching GDScript convention where the first line of the file is line 1).

---

### TestFunctionParsing

Tests function declarations, argument parsing, return types, static/virtual flags, and end_line tracking.

```python
class TestFunctionParsing:
    """Function declarations, arguments, return types, and end_line."""

    # Test: basic func "func _ready() -> void:"
    #   name="_ready", return_type="void", args==[]

    # Test: func with typed params and default
    #   "func add_cash(amount: float, description: String = \"\") -> void:"
    #   args == [("amount", "float", ""), ("description", "String", '""')]

    # Test: func returning bool "func can_afford(amount: float) -> bool:"
    #   return_type == "bool"

    # Test: static func "static func sum2(a, b):"
    #   is_static == True

    # Test: virtual func "_ready"
    #   is_virtual == True, is_private == True

    # Test: non-virtual func "add_cash"
    #   is_virtual == False, is_private == False

    # Test: func with no return type "func foo():"
    #   return_type == ""

    # Test: func with complex return type "func get_idle_printers() -> Array[Printer]:"
    #   return_type == "Array[Printer]"

    # Test: func arg with parens in default "func f(v: Vector2 = Vector2(1, 2)):"
    #   Depth-aware splitting produces correct arg triple:
    #   args == [("v", "Vector2", "Vector2(1, 2)")]

    # Test: end_line for function with body
    #   func on line 2 with 3 body lines -> end_line >= line, end_line is last non-blank body line

    # Test: end_line for last function in file
    #   Last func's end_line is the last non-blank line of file

    # Test: one-liner func "func square(a): return a * a"
    #   end_line == line
```

For end_line tests, construct multi-line GDScript content with tabs for indentation (GDScript convention). Example for end_line tracking:

```python
content = """\
extends Node
func _ready() -> void:
\tprint("hello")
\tprint("world")
"""
```

The function starts on line 2; the last non-blank body line is line 4, so `end_line` should be 4.

---

### TestVariableParsing

Tests variable/constant declarations including types, defaults, annotations, multiline values, and property getter/setter syntax.

```python
class TestVariableParsing:
    """Variable and constant declarations, annotations, multiline values."""

    # Test: typed var "var cash: float = 100.0"
    #   type_hint == "float", default_value == "100.0"

    # Test: untyped var "var printers: Dictionary = {}"
    #   type_hint == "Dictionary", default_value == "{}"

    # Test: const "const ANSWER = 42"
    #   is_const == True, default_value == "42"

    # Test: typed const "const LEVEL_XP: Array[int] = [0, 10, 20]"
    #   is_const == True, type_hint == "Array[int]"

    # Test: @export same-line "@export var speed: float = 5.0"
    #   annotation == "@export"

    # Test: @export_range same-line '@export_range(0, 100) var hp: int'
    #   annotation == "@export_range(0, 100)"

    # Test: @onready same-line "@onready var sprite := $Sprite2D"
    #   annotation == "@onready"

    # Test: static var "static var balance: int = 0"
    #   is_static == True

    # Test: scope tracking -- class-level var has scope == "class"

    # Test: scope tracking -- var inside function body is NOT captured
    #   Content has a func with a local var; result.variables only has class-level vars

    # Test: multiline const array -- bracket depth tracking
    #   const X: Array[int] = [\n\t0,\n\t10,\n] -> single var with complete default_value

    # Test: multiline var dict (session_stats pattern)
    #   var stats: Dictionary = {\n\t"key": 0,\n} -> IN_MULTILINE_VALUE triggers for var too

    # Test: property getter/setter "var health: int:" (trailing colon, no =)
    #   Captured as variable, getter/setter body skipped

    # Test: inline comment stripped
    #   "var speed: float = 5.0  # fast" -> default_value == "5.0"

    # Test: @export_group standalone -- recognized and discarded
    #   An @export_group line followed by a var does NOT attach @export_group as annotation
```

For multiline tests, use tab-indented content spanning multiple lines with brackets:

```python
content = """\
extends Node
const LEVEL_XP: Array[int] = [
\t0,
\t40,
\t100,
]
"""
```

The parser should accumulate the multiline value into a single `default_value` string.

For the property getter/setter test:

```python
content = """\
extends Node
var health: int:
\tget:
\t\treturn _health
\tset(value):
\t\t_health = value
var name: String = "test"
"""
```

The variable `health` should be captured with no default_value, and the getter/setter body should be skipped. The `name` variable on the next class-level line should also be captured.

---

### TestEnumParsing

Tests enum declarations in both single-line and multi-line forms, named and anonymous, with optional explicit values.

```python
class TestEnumParsing:
    """Enum definitions: named, anonymous, explicit values, multiline."""

    # Test: named enum "enum GameState { PLAYING, PAUSED, DAILY_SUMMARY }"
    #   name == "GameState", values == {"PLAYING": None, "PAUSED": None, "DAILY_SUMMARY": None}

    # Test: enum with explicit values "enum { A, B = 5, C }"
    #   values["B"] == 5, values["C"] == None

    # Test: enum with negative values "enum { X = -1 }"
    #   values["X"] == -1

    # Test: anonymous enum "enum { A, B }"
    #   name == ""

    # Test: multiline enum
    #   enum State {\n\tIDLE,\n\tRUNNING,\n} -> accumulated correctly

    # Test: enum line number
    #   Enum on line 2 -> result.enums[0].line == 2
```

---

### TestSignalConnections

Tests `.connect()` call detection inside function bodies, including depth-aware parsing, bind arguments, lambdas, and `$Node` sources.

```python
class TestSignalConnections:
    """Signal .connect() detection in function bodies."""

    # Test: direct connect "signal_name.connect(_handler)"
    #   target == "_handler", has_bind == False

    # Test: connect with bind "signal_name.connect(_handler.bind(arg))"
    #   has_bind == True, bind_args == "arg"

    # Test: connect with $Node "$Timer.timeout.connect(_on_timeout)"
    #   signal_source contains "$Timer.timeout"

    # Test: connect with lambda
    #   "level_up.connect(func(_l, _u): fired[0] = true)"
    #   target starts with "func"

    # Test: depth-aware parsing -- connect call with trailing code on same line
    #   The correct closing paren is found even with extra code after

    # Test: signal_source captures full chain "printer.print_started"
    #   signal_source == "printer.print_started"
```

All connect calls must be inside a function body (indented under a `func` declaration). Example input:

```python
content = """\
extends Node
func _ready() -> void:
\tprinter.print_started.connect(_on_print_started)
"""
```

---

### TestSignalEmissions

Tests `.emit()` call detection and argument counting.

```python
class TestSignalEmissions:
    """Signal .emit() detection and arg counting."""

    # Test: emit no args "health_depleted.emit()"
    #   arg_count == 0

    # Test: emit 2 args "cash_changed.emit(cash, amount)"
    #   arg_count == 2

    # Test: emit 3 args "any_print_failed.emit(id, product, reason)"
    #   arg_count == 3

    # Test: emit with nested parens in args
    #   "foo.emit(bar(1, 2), baz)" -> arg_count == 2 (depth-aware counting)
```

Emit calls must also be inside function bodies:

```python
content = """\
extends Node
func do_thing() -> void:
\thealth_depleted.emit()
"""
```

---

### TestAutoloadRefs

Tests autoload reference detection for all four patterns: direct, has_node, get_node, and get_node_or_null.

```python
class TestAutoloadRefs:
    """Autoload singleton reference detection."""

    # Test: direct autoload "EconomyManager.spend_cash(...)"
    #   with known_autoloads=["EconomyManager"]
    #   pattern == "direct", autoload_name == "EconomyManager"

    # Test: no detection without known_autoloads
    #   Direct refs not captured when known_autoloads is None

    # Test: has_node pattern 'has_node("/root/ProgressionManager")'
    #   pattern == "has_node"

    # Test: get_node pattern 'get_node("/root/ProgressionManager")'
    #   pattern == "get_node"

    # Test: get_node_or_null pattern 'get_node_or_null("Main/Systems/SlotManager")'
    #   pattern == "get_node_or_null"

    # Test: has_node and get_node detected even without known_autoloads
    #   These are structurally unambiguous, no autoload list needed

    # Test: multiple autoload refs on different lines each create separate entries
    #   2 lines with different autoload calls -> len(result.autoload_refs) == 2
```

For direct autoload detection, the `known_autoloads` parameter must be passed:

```python
content = """\
extends Node
func _end_day() -> void:
\tEconomyManager.spend_cash(100, "costs")
"""
result = _parse_gd_content(content, known_autoloads=["EconomyManager"])
```

For has_node/get_node/get_node_or_null, these are detected even without the autoloads list because they use explicit `/root/` paths:

```python
content = """\
extends Node
func get_level() -> int:
\tif has_node("/root/ProgressionManager"):
\t\treturn get_node("/root/ProgressionManager").player_level
\treturn 1
"""
result = _parse_gd_content(content)
```

---

### TestPreloadLoad

Tests preload/load reference detection in both top-level const declarations and inside function bodies.

```python
class TestPreloadLoad:
    """preload() and load() reference detection."""

    # Test: preload 'preload("res://scenes/player.tscn")'
    #   is_preload == True, path == "res://scenes/player.tscn"

    # Test: load 'load("res://data/items.tres")'
    #   is_preload == False

    # Test: preload inside const creates BOTH GdVariable and GdPreloadRef
    #   'const MyScene = preload("res://scenes/player.tscn")'
    #   len(result.variables) >= 1 and len(result.preload_refs) >= 1

    # Test: preload in function body also creates GdPreloadRef
    #   preload inside func body -> still detected
```

preload/load detection works in ALL states (top-level and inside function bodies), so both contexts must be tested. Example for top-level preload in a const:

```python
content = """\
extends Node
const MyScene = preload("res://scenes/player.tscn")
"""
```

---

### TestIsInstanceValid

Tests `is_instance_valid()` detection and line number recording.

```python
class TestIsInstanceValid:
    """is_instance_valid() detection."""

    # Test: is_instance_valid detected and line recorded
    #   Content with is_instance_valid call -> line in result.is_instance_valid_lines

    # Test: multiple is_instance_valid calls -> multiple line entries
    #   2 calls on different lines -> len(result.is_instance_valid_lines) == 2
```

---

### TestInnerClasses

Tests inner class name detection and body skipping.

```python
class TestInnerClasses:
    """Inner class detection and body skipping."""

    # Test: inner class "class Circle extends Shape:" -> "Circle" in inner_classes

    # Test: inner class body skipped -- vars/funcs inside NOT added to outer class
    #   Content has inner class with var and func; only outer-level vars/funcs captured

    # Test: inner class without extends "class Something:" -> name captured
```

Example for body skipping test:

```python
content = """\
extends Node
var outer_var: int = 1
class Inner extends RefCounted:
\tvar inner_var: int = 2
\tfunc inner_func() -> void:
\t\tpass
func outer_func() -> void:
\tpass
"""
```

Only `outer_var` should appear in `result.variables`, only `outer_func` in `result.functions`. `"Inner"` should appear in `result.inner_classes`. `inner_var` and `inner_func` should NOT appear.

---

### TestEdgeCases

Tests robustness for empty/malformed input and annotation edge cases.

```python
class TestEdgeCases:
    """Edge cases: empty files, comments-only, malformed input, annotations."""

    # Test: empty string content -> empty GdScript, no exceptions

    # Test: file with only comments -> empty GdScript
    #   "# This is a comment\n# Another comment\n"

    # Test: malformed line does not raise -- silently skipped
    #   Content with gibberish lines interspersed with valid declarations

    # Test: unclosed multiline construct at EOF -> closes with accumulated content
    #   "extends Node\nconst X = [\n\t1,\n\t2," (no closing bracket)
    #   No exception; the variable should be captured with whatever was accumulated

    # Test: annotation buffer not corrupted by @rpc or @warning_ignore before func
    #   "@rpc\nfunc network_sync():" -> function captured, no annotation leaks to next var

    # Test: @export_group does not corrupt annotation buffer
    #   "@export_group(\"Movement\")\nvar speed: float = 5.0"
    #   speed's annotation should be "" (not "@export_group(...)")
```

For the annotation corruption test, the key behavior is that `@rpc`, `@warning_ignore`, `@tool`, `@icon`, `@abstract`, and `@static_unload` are recognized and silently discarded -- they do NOT get buffered and applied to the next variable. Only `@export*` and `@onready` are buffered.

Similarly, `@export_group`, `@export_subgroup`, and `@export_category` are standalone grouping annotations that are recognized and discarded without buffering.

---

## Implementation Notes

1. **All tests call `_parse_gd_content()` directly** with inline string content. No temp files are needed. The only test that uses `parse_gd()` (the public file-reading wrapper) is the `FileNotFoundError` test.

2. **Line numbering is 1-indexed.** The first line of the content string is line 1.

3. **GDScript uses tab indentation.** All inline test strings should use `\t` for indentation inside function bodies, inner classes, and multiline constructs to match real GDScript conventions.

4. **The `_parse_gd_content` signature** is `_parse_gd_content(content: str, known_autoloads: list[str] | None = None) -> GdScript`. When testing autoload detection, pass the `known_autoloads` parameter.

5. **Test class naming follows the project convention**: `class TestFeatureName:` with `def test_scenario_description(self) -> None:` methods. All test functions have type hints (`-> None`).

6. **The test file must NOT import from `src.godotiq`** -- use `from godotiq.parsers.gd_parser import ...` (the package is installed in editable mode via `pip install -e ".[dev]"`).

7. **No pytest-asyncio markers needed** for these tests -- all tests are synchronous since `_parse_gd_content` is a synchronous function.

8. **Approximately 40-50 test functions** across the 12 test classes, providing thorough coverage of the parser's features without testing integration with real fixture files (that is section 05).

## Actual Implementation Notes

### No-op Section
All 79 unit tests specified by this section were already created during sections 02 and 03 as part of TDD cycles:
- Section 02 created: TestClassDeclarations (7), TestSignalParsing (6), TestFunctionParsing (12), TestVariableParsing (14), TestEnumParsing (6), TestInnerClasses (3), TestEdgeCases (6) = 54 tests
- Section 03 added: TestSignalConnections (7), TestSignalEmissions (4), TestAutoloadRefs (7), TestPreloadLoad (4), TestIsInstanceValid (2) = 24 tests + 1 missing_file test = 79 total

The Sprint 0 test update (`test_parse_gd_stub_raises` -> `test_parse_gd_missing_file_raises`) was also done in section 02.

### Test Coverage Summary
- 12 test classes across all parser features
- 79 unit test functions (exceeds the planned 40-50)
- All tests use `_parse_gd_content()` with inline strings
- 206 total tests pass (all Sprint 0/1/1BC tests unaffected)