# GUT TDD Setup Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Install GUT v9.6.0, configure headless test runner, and write tests for the core logic layers (GameState, RoomNavigator, GiftBox FSM) as the TDD foundation for all future sprints.

**Architecture:** GUT addon installed to `addons/gut/`. Tests live in `test/unit/`. Each testable class gets its own `test_<class>.gd` file. Autoloads and components are instantiated fresh per test using `add_child_autofree()`. Headless runner: `godot --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit`. Future sprints write failing tests first, then implement.

**Tech Stack:** Godot 4.6.2, GUT v9.6.0, GDScript static typing, PowerShell (Windows) for download

---

### Task 1: Download and install GUT v9.6.0

**Files:**
- Create: `addons/gut/` (extracted from GitHub zip)
- Modify: `project.godot`

- [ ] **Step 1: Download and extract GUT**

Run in PowerShell from the project root `F:\Development\_gameDev\Cozypaw-Hospital`:

```powershell
$zipUrl = "https://github.com/bitwes/Gut/archive/refs/tags/v9.6.0.zip"
$zipPath = "$env:TEMP\gut_v9.6.0.zip"
$extractPath = "$env:TEMP\gut_extract_v9"
Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath
Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force
Copy-Item -Path "$extractPath\Gut-9.6.0\addons\gut" -Destination "addons\gut" -Recurse
Remove-Item -Path $zipPath, $extractPath -Recurse -Force
```

Expected: `addons/gut/` now contains `plugin.cfg`, `gut.gd`, `gut_cmdln.gd`, and many other files.

Verify:
```powershell
ls addons/gut/ | Select-Object -First 10
```

- [ ] **Step 2: Enable GUT plugin in project.godot**

In `project.godot`, add the `[editor_plugins]` section at the end of the file:

```
[editor_plugins]

enabled=PackedStringArray("res://addons/gut/plugin.cfg")
```

- [ ] **Step 3: Commit**

```bash
cd "F:/Development/_gameDev/Cozypaw-Hospital"
git add addons/gut/ project.godot
git commit -m "chore(tooling): install GUT v9.6.0 test framework"
```

---

### Task 2: Test infrastructure and smoke test

**Files:**
- Create: `test/unit/test_smoke.gd`
- Create: `.gutconfig.json`

- [ ] **Step 1: Create test directory and smoke test**

Create `test/unit/test_smoke.gd`:

```gdscript
## Smoke test — verifies GUT is installed and the test runner works.
extends GutTest


func test_gut_is_working() -> void:
	assert_true(true)


func test_basic_arithmetic() -> void:
	assert_eq(1 + 1, 2)
```

- [ ] **Step 2: Create GUT config file**

Create `.gutconfig.json` in the project root `F:\Development\_gameDev\Cozypaw-Hospital`:

```json
{
  "dirs": ["res://test/"],
  "prefix": "test_",
  "suffix": ".gd",
  "include_subdirs": true,
  "log_level": 1,
  "export_path": ""
}
```

- [ ] **Step 3: Run the smoke test headlessly**

```bash
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit 2>&1
```

Expected: output ends with something like `All tests passed` or `0 failures`. Exit code 0.

If Godot needs to import assets first (first run with new files), run once with `--import` and ignore output, then re-run the test command above:
```bash
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless --import 2>&1
```

- [ ] **Step 4: Commit**

```bash
git add test/unit/test_smoke.gd .gutconfig.json
git commit -m "chore(tooling): add GUT test infrastructure and smoke test"
```

---

### Task 3: GameState tests

**Files:**
- Create: `test/unit/test_game_state.gd`

The implementation (`scripts/autoload/GameState.gd`) already exists. These tests verify it behaves correctly and serve as a regression baseline.

- [ ] **Step 1: Create the test file**

`test/unit/test_game_state.gd`:

```gdscript
## Tests for GameState — character positions, object states, save/load round-trip.
extends GutTest

const GameStateScript: GDScript = preload("res://scripts/autoload/GameState.gd")

var _state: Node


func before_each() -> void:
	_state = GameStateScript.new()
	add_child_autofree(_state)


func test_get_character_position_default_is_zero() -> void:
	assert_eq(_state.get_character_position("bunny_01"), Vector2.ZERO)


func test_has_character_position_returns_false_for_unknown_id() -> void:
	assert_false(_state.has_character_position("bunny_01"))


func test_set_character_position_stores_value() -> void:
	_state.set_character_position("bunny_01", Vector2(100.0, 200.0))
	assert_eq(_state.get_character_position("bunny_01"), Vector2(100.0, 200.0))


func test_has_character_position_returns_true_after_set() -> void:
	_state.set_character_position("bunny_01", Vector2(50.0, 50.0))
	assert_true(_state.has_character_position("bunny_01"))


func test_set_character_position_emits_character_moved() -> void:
	watch_signals(_state)
	_state.set_character_position("bunny_01", Vector2(100.0, 200.0))
	assert_signal_emitted(_state, "character_moved")


func test_set_character_position_emits_state_changed() -> void:
	watch_signals(_state)
	_state.set_character_position("bunny_01", Vector2(100.0, 200.0))
	assert_signal_emitted(_state, "state_changed")


func test_get_object_state_default_is_idle() -> void:
	assert_eq(_state.get_object_state("gift_box_1"), "idle")


func test_set_object_state_stores_value() -> void:
	_state.set_object_state("gift_box_1", "open")
	assert_eq(_state.get_object_state("gift_box_1"), "open")


func test_set_object_state_emits_state_changed() -> void:
	watch_signals(_state)
	_state.set_object_state("gift_box_1", "open")
	assert_signal_emitted(_state, "state_changed")


func test_save_data_round_trip_character_position() -> void:
	_state.set_character_position("bunny_01", Vector2(100.0, 200.0))
	var data: Dictionary = _state.get_save_data()
	_state.set_character_position("bunny_01", Vector2.ZERO)
	_state.apply_save_data(data)
	assert_eq(_state.get_character_position("bunny_01"), Vector2(100.0, 200.0))


func test_save_data_round_trip_object_state() -> void:
	_state.set_object_state("gift_box_1", "open")
	var data: Dictionary = _state.get_save_data()
	_state.set_object_state("gift_box_1", "idle")
	_state.apply_save_data(data)
	assert_eq(_state.get_object_state("gift_box_1"), "open")


func test_apply_save_data_with_empty_dict_does_not_crash() -> void:
	_state.set_character_position("bunny_01", Vector2(10.0, 20.0))
	_state.apply_save_data({})
	assert_eq(_state.get_character_position("bunny_01"), Vector2.ZERO)
```

- [ ] **Step 2: Run tests**

```bash
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit 2>&1
```

Expected: All 12 GameState tests pass. 0 failures.

- [ ] **Step 3: Commit**

```bash
git add test/unit/test_game_state.gd
git commit -m "test(game-state): add unit tests for character positions, object states, and save round-trip"
```

---

### Task 4: RoomNavigator tests

**Files:**
- Create: `test/unit/test_room_navigator.gd`

`RoomNavigator` extends `Node` (no `class_name`). Each test gets a fresh instance with its own Camera2D. State variables (`_current_floor`, `_current_room`, `_is_at_home`) are set synchronously before tweens start — assertions work immediately without awaiting tween completion.

- [ ] **Step 1: Create the test file**

`test/unit/test_room_navigator.gd`:

```gdscript
## Tests for RoomNavigator — floor/room state, home/hospital navigation logic.
extends GutTest

const RoomNavigatorScript: GDScript = preload("res://scripts/systems/room_navigator.gd")

var _nav: Node
var _camera: Camera2D


func before_each() -> void:
	_nav = RoomNavigatorScript.new()
	add_child_autofree(_nav)
	_camera = Camera2D.new()
	add_child_autofree(_camera)
	_nav.initialize(_camera)


func test_initial_floor_is_zero() -> void:
	assert_eq(_nav.get_current_floor(), 0)


func test_initial_room_is_zero() -> void:
	assert_eq(_nav.get_current_room(), 0)


func test_is_at_home_starts_false() -> void:
	assert_false(_nav.is_at_home())


func test_go_to_room_updates_floor() -> void:
	_nav.go_to_room(1, 2)
	assert_eq(_nav.get_current_floor(), 1)


func test_go_to_room_updates_room() -> void:
	_nav.go_to_room(1, 2)
	assert_eq(_nav.get_current_room(), 2)


func test_go_to_room_same_position_when_not_at_home_is_noop() -> void:
	_nav.go_to_room(1, 2)
	var camera_pos: Vector2 = _camera.position
	_nav.go_to_room(1, 2)
	assert_eq(_camera.position, camera_pos)


func test_go_to_floor_navigates_to_room_zero() -> void:
	_nav.go_to_floor(2)
	assert_eq(_nav.get_current_floor(), 2)
	assert_eq(_nav.get_current_room(), 0)


func test_go_to_home_sets_is_at_home_true() -> void:
	_nav.go_to_home()
	assert_true(_nav.is_at_home())


func test_go_to_home_twice_is_noop_on_second_call() -> void:
	_nav.go_to_home()
	var camera_pos: Vector2 = _camera.position
	_nav.go_to_home()
	assert_eq(_camera.position, camera_pos)


func test_go_to_hospital_clears_is_at_home() -> void:
	_nav.go_to_home()
	_nav.go_to_hospital()
	assert_false(_nav.is_at_home())


func test_go_to_hospital_restores_last_floor() -> void:
	_nav.go_to_room(1, 2)
	_nav.go_to_home()
	_nav.go_to_hospital()
	assert_eq(_nav.get_current_floor(), 1)


func test_go_to_hospital_restores_last_room() -> void:
	_nav.go_to_room(1, 2)
	_nav.go_to_home()
	_nav.go_to_hospital()
	assert_eq(_nav.get_current_room(), 2)


func test_go_to_hospital_when_not_at_home_is_noop() -> void:
	_nav.go_to_room(1, 2)
	var camera_pos: Vector2 = _camera.position
	_nav.go_to_hospital()
	assert_eq(_camera.position, camera_pos)


func test_go_to_room_after_home_clears_is_at_home() -> void:
	_nav.go_to_home()
	_nav.go_to_room(0, 0)
	assert_false(_nav.is_at_home())
```

- [ ] **Step 2: Run tests**

```bash
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit 2>&1
```

Expected: All RoomNavigator + GameState + smoke tests pass. 0 failures.

- [ ] **Step 3: Commit**

```bash
git add test/unit/test_room_navigator.gd
git commit -m "test(room-navigator): add unit tests for floor/room state and home/hospital navigation"
```

---

### Task 5: GiftBox FSM tests

**Files:**
- Create: `test/unit/test_gift_box.gd`

GiftBox is instantiated from its scene (so `_ready()` runs and hides the Gift node). State transitions are tested synchronously. Tween visual outcomes (alpha values mid-animation) are not tested — only state machine transitions.

- [ ] **Step 1: Create the test file**

`test/unit/test_gift_box.gd`:

```gdscript
## Tests for GiftBox — CLOSED/OPENING/OPEN state machine transitions.
extends GutTest

var _box: GiftBox


func before_each() -> void:
	_box = preload("res://scenes/objects/GiftBox.tscn").instantiate() as GiftBox
	add_child_autofree(_box)


func test_initial_state_is_closed() -> void:
	assert_eq(_box._state, GiftBox.State.CLOSED)


func test_ready_hides_gift_node() -> void:
	var gift: Node2D = _box.get_node("Gift") as Node2D
	assert_eq(gift.modulate.a, 0.0)


func test_start_opening_transitions_to_opening() -> void:
	_box._start_opening()
	assert_eq(_box._state, GiftBox.State.OPENING)


func test_on_lid_opened_transitions_to_open() -> void:
	_box._start_opening()
	_box._on_lid_opened()
	assert_eq(_box._state, GiftBox.State.OPEN)


func test_input_ignored_when_state_is_opening() -> void:
	_box._state = GiftBox.State.OPENING
	var event: InputEventScreenTouch = InputEventScreenTouch.new()
	event.pressed = true
	event.position = _box.global_position
	_box._input(event)
	assert_eq(_box._state, GiftBox.State.OPENING)


func test_input_ignored_when_state_is_open() -> void:
	_box._state = GiftBox.State.OPEN
	var event: InputEventScreenTouch = InputEventScreenTouch.new()
	event.pressed = true
	event.position = _box.global_position
	_box._input(event)
	assert_eq(_box._state, GiftBox.State.OPEN)


func test_tap_outside_hitbox_does_not_open() -> void:
	var event: InputEventScreenTouch = InputEventScreenTouch.new()
	event.pressed = true
	event.position = _box.global_position + Vector2(200.0, 200.0)
	_box._input(event)
	assert_eq(_box._state, GiftBox.State.CLOSED)


func test_release_event_does_not_open() -> void:
	var event: InputEventScreenTouch = InputEventScreenTouch.new()
	event.pressed = false
	event.position = _box.global_position
	_box._input(event)
	assert_eq(_box._state, GiftBox.State.CLOSED)
```

- [ ] **Step 2: Run tests**

```bash
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit 2>&1
```

Expected: All GiftBox + earlier tests pass. 0 failures.

- [ ] **Step 3: Commit**

```bash
git add test/unit/test_gift_box.gd
git commit -m "test(gift-box): add unit tests for CLOSED/OPENING/OPEN state machine"
```

---

### Task 6: Update CLAUDE.md — TDD conventions

**Files:**
- Modify: `CLAUDE.md`

- [ ] **Step 1: Replace the Tests section in CLAUDE.md**

Find the block starting with `### Tests` and ending before `### Tests` or the next `##` section (whichever comes first). Replace the entire block with:

~~~markdown
### Tests

**Framework:** GUT v9.6.0 (`addons/gut/`)

**Headless runner:**

    "F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit

**TDD-Workflow (ab Sprint 15):**
1. Failing test schreiben für die neue Logik
2. Test ausführen → muss FAIL sein
3. Minimalimplementierung schreiben
4. Test ausführen → muss PASS sein
5. Committen

**Was wird getestet:**
- ✅ Autoloads: `GameState`, `RoomNavigator`, `SaveManager`
- ✅ State-Machines: `GiftBox`, `TeaPot`, `Cradle`, `DeliveryBed`, etc.
- ✅ Reine Logik-Berechnungen (Koordinaten, Serialisierung)
- ❌ Tween-Animationen (visuell, kein Assert möglich)
- ❌ Touch-Input (manuelle Tests auf Tablet)

**Test-Dateien:** `test/unit/test_<class_name>.gd`, extends `GutTest`

**Device-Tests:**
- Scene-Tests manuell auf echtem Android-Tablet pro Sprint
- Kinder als UAT — wöchentliches Zeigen der Builds
~~~

- [ ] **Step 2: Run all tests to confirm baseline passes**

```bash
"F:/Development/_tools/Godot_v4.6.2-stable_win64/Godot_v4.6.2-stable_win64.exe" --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://test/ -gexit 2>&1
```

Expected: All tests pass (smoke + GameState + RoomNavigator + GiftBox). 0 failures.

- [ ] **Step 3: Commit**

```bash
git add CLAUDE.md
git commit -m "docs(claude): update testing conventions to reflect GUT TDD workflow"
```
