config/features=PackedStringArray("4.4", "GDScript", "GL Compatibility")
Environment built from scratch (empty ~): Godot 4.4.1 headless + full GUT 9.4.0 addon from the official bitwes/Gut v9.4.0 source tarball, dropped into addons/gut/ (verified plugin.cfg → version="9.4.0").
1. Project + config so the exact command finds every GutTest file. GUT refuses to run with zero configured dirs, and the CLI reads res://.gutconfig.json by default:
// .gutconfig.json
{
"dirs": ["res://test"],
"include_subdirs": true,
"prefix": "test_",
"suffix": ".gd",
"log_level": 1,
"should_exit": true,
"should_exit_on_success": true
}
# project.godot
[application]
config/name="ED-GAP-016 gut-test-compat"
config/features=PackedStringArray("4.4", "GDScript", "GL Compatibility")
[editor_plugins]
enabled=PackedStringArray("res://addons/gut/plugin.cfg")
2. Lifecycle-compat fixes in the tests (the actual code fixes):
# test/test_node_spawner.gd — tree lifecycle: GUT owns the node
func test_add_child_autoqfree_manages_tree_nodes() -> void:
# GUT helpers return Variant: explicit types required, `:=` is a parse error
var spawner: NodeSpawner = add_child_autoqfree(NodeSpawnerScript.new())
var child: Node = spawner.spawn("widget")
assert_true(child.is_inside_tree())
# spawner is queue_freed with its subtree at test teardown -> no orphans
func test_free_hygiene_manual_cleanup() -> void:
var spawner := NodeSpawnerScript.new()
add_child(spawner) # plain add_child: we own it
var children := spawner.spawn_n(3)
spawner.free() # explicit free() hygiene
for child in children:
assert_false(is_instance_valid(child)) # children freed with parent
# test/test_calculator.gd — RefCounted tracking via autofree
func test_autofree_tracks_refcounted_objects() -> void:
var calc: Calculator = autofree(Calculator.new()) # no `:=`, helper is untyped
assert_eq(calc.add(40, 2), 42)
# test/test_lifecycle.gd — dropped _init auto-test hooks; all work in GUT hooks
var _counter := 0
func before_each() -> void: _counter += 1
func after_all() -> void:
assert_eq(_counter, 3, "before_each ran once per test (no _init auto-run)")
func test_a() -> void: pass_test("a")
func test_b() -> void: pass_test("b")
func test_c() -> void: pass_test("c")
3. First-time setup requires one import pass (standard GUT install step; Godot 4.4 generates the global class cache here): godot --headless --import, then the task command runs unmodified.
Ran against real Godot **4.4.1 stable** on this machine, from a **pristine checkout** (`rm -rf .godot` → `--import` → run):
```
$ godot --headless -s addons/gut/gut_cmdln.gd -gexit
GUT version: 9.4.0
res://test/test_calculator.gd 6/6 passed
res://test/test_lifecycle.gd 3/3 passed
res://test/test_node_spawner.gd 4/4 passed
res://test/nested/test_math_utils.gd 1/1 passed
---- Totals ----
Scripts 4 Tests 14 Passing 14 Asserts 33
---- All tests passed! ---- EXIT_CODE=0
```
Edge cases verified:
- **Failure gating**: injected a failing probe test → `1/14 failing`, **exit code 1** (removed after). Proves `-gexit` gates CI correctly, and `should_exit_on_success` doesn't mask failures.
- **`:=` inference trap**: `autofree()`/`add_child_autoqfree()` return untyped `Variant`, so `var x := autofree(...)` is a *parse error* that silently excludes the script ("does not extend GutTest"). Fixed with explicit types — this is the compat bug class the task targets.
- **No orphan leaks / zero warnings**: final run has **0 warnings, 0 deprecations** — the `add_child_autoqfree`/`autofree`/manual `free()` paths all clean up (GUT warns on unfreed children otherwise). `wait_frames` was deprecated in 9.4.0 → replaced with `wait_process_frames(1)`.
- **Subdirectory discovery**: `include_subdirs: true` picks up `test/nested/` (proven by `test_math_utils`).
- **JUnit export** (`-gjunit_xml_file`): emits per-script `<testsuite>` with `failures="0"`, exit 0.
- **Missing config guard**: with no `dirs`, GUT hard-errors ("You do not have any directories configured") — the `.gutconfig.json` is what makes the bare command work.{"model": "pi", "problem_class": "godot-gut-addon-install-test-compat", "result": "passed", "tests": 14}Environment built from scratch (empty ~): Godot 4.4.1 headless + full GUT 9.4.0 addon from the official bitwes/Gut v9.4.0 source tarball, dropped into addons/gut/ (verified plugin.cfg → version="9.4.0").
1. Project + config so the exact command finds every GutTest file. GUT refuses to run with zero configured dirs, and the CLI reads res://.gutconfig.json by default:
// .gutconfig.json
{
"dirs": ["res://test"],
"include_subdirs": true,
"prefix": "test_",
"suffix": ".gd",
"log_level": 1,
"should_exit": true,
"should_exit_on_success": true
}
# project.godot
[application]
config/name="ED-GAP-016 gut-test-compat"
config/features=PackedStringArray("4.4", "GDScript", "GL Compatibility")
[editor_plugins]
enabled=PackedStringArray("res://addons/gut/plugin.cfg")
2. Lifecycle-compat fixes in the tests (the actual code fixes):
# test/test_node_spawner.gd — tree lifecycle: GUT owns the node
func test_add_child_autoqfree_manages_tree_nodes() -> void:
# GUT helpers return Variant: explicit types required, `:=` is a parse error
var spawner: NodeSpawner = add_child_autoqfree(NodeSpawnerScript.new())
var child: Node = spawner.spawn("widget")
assert_true(child.is_inside_tree())
# spawner is queue_freed with its subtree at test teardown -> no orphans
func test_free_hygiene_manual_cleanup() -> void:
var spawner := NodeSpawnerScript.new()
add_child(spawner) # plain add_child: we own it
var children := spawner.spawn_n(3)
spawner.free() # explicit free() hygiene
for child in children:
assert_false(is_instance_valid(child)) # children freed with parent
# test/test_calculator.gd — RefCounted tracking via autofree
func test_autofree_tracks_refcounted_objects() -> void:
var calc: Calculator = autofree(Calculator.new()) # no `:=`, helper is untyped
assert_eq(calc.add(40, 2), 42)
# test/test_lifecycle.gd — dropped _init auto-test hooks; all work in GUT hooks
var _counter := 0
func before_each() -> void: _counter += 1
func after_all() -> void:
assert_eq(_counter, 3, "before_each ran once per test (no _init auto-run)")
func test_a() -> void: pass_test("a")
func test_b() -> void: pass_test("b")
func test_c() -> void: pass_test("c")
3. First-time setup requires one import pass (standard GUT install step; Godot 4.4 generates the global class cache here): godot --headless --import, then the task command runs unmodified.
Ran against real Godot **4.4.1 stable** on this machine, from a **pristine checkout** (`rm -rf .godot` → `--import` → run):
```
$ godot --headless -s addons/gut/gut_cmdln.gd -gexit
GUT version: 9.4.0
res://test/test_calculator.gd 6/6 passed
res://test/test_lifecycle.gd 3/3 passed
res://test/test_node_spawner.gd 4/4 passed
res://test/nested/test_math_utils.gd 1/1 passed
---- Totals ----
Scripts 4 Tests 14 Passing 14 Asserts 33
---- All tests passed! ---- EXIT_CODE=0
```
Edge cases verified:
- **Failure gating**: injected a failing probe test → `1/14 failing`, **exit code 1** (removed after). Proves `-gexit` gates CI correctly, and `should_exit_on_success` doesn't mask failures.
- **`:=` inference trap**: `autofree()`/`add_child_autoqfree()` return untyped `Variant`, so `var x := autofree(...)` is a *parse error* that silently excludes the script ("does not extend GutTest"). Fixed with explicit types — this is the compat bug class the task targets.
- **No orphan leaks / zero warnings**: final run has **0 warnings, 0 deprecations** — the `add_child_autoqfree`/`autofree`/manual `free()` paths all clean up (GUT warns on unfreed children otherwise). `wait_frames` was deprecated in 9.4.0 → replaced with `wait_process_frames(1)`.
- **Subdirectory discovery**: `include_subdirs: true` picks up `test/nested/` (proven by `test_math_utils`).
- **JUnit export** (`-gjunit_xml_file`): emits per-script `<testsuite>` with `failures="0"`, exit 0.
- **Missing config guard**: with no `dirs`, GUT hard-errors ("You do not have any directories configured") — the `.gutconfig.json` is what makes the bare command work.{"model": "pi", "problem_class": "godot-gut-addon-install-test-compat", "result": "passed", "tests": 14}