diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 38cbe5e..cc88747 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -108,7 +108,59 @@ jobs: run: | set -euo pipefail ./.tools/Godot_v${{ steps.godot-config.outputs.version }}-${{ steps.godot-config.outputs.status }}_linux.x86_64 --headless --path . --script res://tests/test_reservations.gd > reservation-tests.log 2>&1 + RESV_EXIT=$? cat reservation-tests.log + if [ $RESV_EXIT -ne 0 ]; then + echo "::error::Reservation tests failed (exit code $RESV_EXIT)" + exit 1 + fi + - name: Run resource trend tests (issue #137) + shell: bash + run: | + set -euo pipefail + ./.tools/Godot_v${{ steps.godot-config.outputs.version }}-${{ steps.godot-config.outputs.status }}_linux.x86_64 --headless --path . --script res://tests/test_resource_trends.gd > trend-tests.log 2>&1 + TREND_EXIT=$? + cat trend-tests.log + if [ $TREND_EXIT -ne 0 ]; then + echo "::error::Resource trend tests failed (exit code $TREND_EXIT)" + exit 1 + fi + + - name: Run food upkeep tests (issue #147) + shell: bash + run: | + set -euo pipefail + ./.tools/Godot_v${{ steps.godot-config.outputs.version }}-${{ steps.godot-config.outputs.status }}_linux.x86_64 --headless --path . --script res://tests/test_food_upkeep.gd > food-tests.log 2>&1 + FOOD_EXIT=$? + cat food-tests.log + if [ $FOOD_EXIT -ne 0 ]; then + echo "::error::Food upkeep tests failed (exit code $FOOD_EXIT)" + exit 1 + fi + + - name: Run recruit worker tests (issue #149) + shell: bash + run: | + set -euo pipefail + ./.tools/Godot_v${{ steps.godot-config.outputs.version }}-${{ steps.godot-config.outputs.status }}_linux.x86_64 --headless --path . --script res://tests/test_recruit_worker.gd > recruit-tests.log 2>&1 + RECRUIT_EXIT=$? + cat recruit-tests.log + if [ $RECRUIT_EXIT -ne 0 ]; then + echo "::error::Recruit worker tests failed (exit code $RECRUIT_EXIT)" + exit 1 + fi + + - name: Run worker cap tests (issue #146) + shell: bash + run: | + set -euo pipefail + ./.tools/Godot_v${{ steps.godot-config.outputs.version }}-${{ steps.godot-config.outputs.status }}_linux.x86_64 --headless --path . --script res://tests/test_worker_cap.gd > cap-tests.log 2>&1 + CAP_EXIT=$? + cat cap-tests.log + if [ $CAP_EXIT -ne 0 ]; then + echo "::error::Worker cap tests failed (exit code $CAP_EXIT)" + exit 1 + fi macos-validation: name: macOS validation @@ -213,3 +265,62 @@ jobs: echo "::error::Layout regression tests failed (exit code $LAYOUT_EXIT)" exit 1 fi + - name: Run reservation tests (issue #143) + shell: bash + run: | + set -euo pipefail + "$GODOT_MAC_APP/Contents/MacOS/Godot" --headless --path . --script res://tests/test_reservations.gd > reservation-tests.log 2>&1 + RESV_EXIT=$? + cat reservation-tests.log + if [ $RESV_EXIT -ne 0 ]; then + echo "::error::Reservation tests failed (exit code $RESV_EXIT)" + exit 1 + fi + + - name: Run resource trend tests (issue #137) + shell: bash + run: | + set -euo pipefail + "$GODOT_MAC_APP/Contents/MacOS/Godot" --headless --path . --script res://tests/test_resource_trends.gd > trend-tests.log 2>&1 + TREND_EXIT=$? + cat trend-tests.log + if [ $TREND_EXIT -ne 0 ]; then + echo "::error::Resource trend tests failed (exit code $TREND_EXIT)" + exit 1 + fi + + - name: Run food upkeep tests (issue #147) + shell: bash + run: | + set -euo pipefail + "$GODOT_MAC_APP/Contents/MacOS/Godot" --headless --path . --script res://tests/test_food_upkeep.gd > food-tests.log 2>&1 + FOOD_EXIT=$? + cat food-tests.log + if [ $FOOD_EXIT -ne 0 ]; then + echo "::error::Food upkeep tests failed (exit code $FOOD_EXIT)" + exit 1 + fi + + - name: Run recruit worker tests (issue #149) + shell: bash + run: | + set -euo pipefail + "$GODOT_MAC_APP/Contents/MacOS/Godot" --headless --path . --script res://tests/test_recruit_worker.gd > recruit-tests.log 2>&1 + RECRUIT_EXIT=$? + cat recruit-tests.log + if [ $RECRUIT_EXIT -ne 0 ]; then + echo "::error::Recruit worker tests failed (exit code $RECRUIT_EXIT)" + exit 1 + fi + + - name: Run worker cap tests (issue #146) + shell: bash + run: | + set -euo pipefail + "$GODOT_MAC_APP/Contents/MacOS/Godot" --headless --path . --script res://tests/test_worker_cap.gd > cap-tests.log 2>&1 + CAP_EXIT=$? + cat cap-tests.log + if [ $CAP_EXIT -ne 0 ]; then + echo "::error::Worker cap tests failed (exit code $CAP_EXIT)" + exit 1 + fi diff --git a/tests/test_colony_stance.gd b/tests/test_colony_stance.gd index 99eaa1f..4b072b0 100644 --- a/tests/test_colony_stance.gd +++ b/tests/test_colony_stance.gd @@ -9,7 +9,7 @@ var test_fail := 0 func _initialize() -> void: # Preload the scripts we need var stance_script: GDScript = preload("res://scripts/colony_stance.gd") - var main_script: GDScript = preload("res://scripts/main.gd") + var main_script: GDScript = load("res://scripts/main.gd") # ── Test 1: get_effective_priority_order for balanced stance ── print("") diff --git a/tests/test_food_upkeep.gd b/tests/test_food_upkeep.gd index e59460a..bd3345f 100644 --- a/tests/test_food_upkeep.gd +++ b/tests/test_food_upkeep.gd @@ -1,33 +1,79 @@ -extends TestSuite +extends SceneTree # Tests for food upkeep model (issue #147, links to #133). # Validates: base workers no pressure, extra workers create pressure, # low-food slowdown, starvation pause, and food-gathering bias. const Constants := preload("res://scripts/constants.gd") +var test_pass := 0 +var test_fail := 0 + + +func _initialize() -> void: + test_base_workers_no_upkeep() + test_extra_workers_create_pressure() + test_one_extra_worker_cost() + test_upkeep_never_negative() + test_no_slowdown_when_food_ok() + test_low_food_slowdown_at_threshold() + test_starvation_pause() + test_linear_interpolation() + test_food_level_classification() + test_bias_to_food_when_low() + test_upkeep_interval() + test_base_workers_constant() + test_food_per_extra_worker() + test_constants_consistency() + + # Summary + print("") + print("=== test_food_upkeep summary: %d passed, %d failed ===" % [test_pass, test_fail]) + if test_fail > 0: + print("FAILURES DETECTED") + quit(1) + else: + print("test_food_upkeep: ok") + quit(0) + + +func _assert(condition: Variant, name: String, detail: String = "") -> void: + if not condition: + test_fail += 1 + if not detail.is_empty(): + print("TEST %s: FAIL — %s" % [name, detail]) + else: + print("TEST %s: FAIL" % name) + else: + test_pass += 1 + print("TEST %s: PASS" % name) + + +func _assert_eq(actual: Variant, expected: Variant, name: String) -> void: + _assert(actual == expected, name, "expected %s, got %s" % [str(expected), str(actual)]) + # ── Test: base workers do not create food pressure ─────────────────────────── func test_base_workers_no_upkeep() -> void: var extra := get_extra_workers_count(2) - assert_eq(extra, 0, "Base 2 workers should produce 0 extra") + _assert_eq(extra, 0, "Base 2 workers should produce 0 extra") var food_cost := get_food_cost(2, Constants.FOOD_PER_EXTRA_WORKER) - assert_eq(food_cost, 0, "Base workers should cost 0 food per upkeep cycle") + _assert_eq(food_cost, 0, "Base workers should cost 0 food per upkeep cycle") # ── Test: extra workers create clear food pressure ─────────────────────────── func test_extra_workers_create_pressure() -> void: var extra := get_extra_workers_count(4) - assert_eq(extra, 2, "4 workers should produce 2 extra") + _assert_eq(extra, 2, "4 workers should produce 2 extra") var food_cost := get_food_cost(4, Constants.FOOD_PER_EXTRA_WORKER) - assert_eq(food_cost, 2, "4 workers should cost 2 food per upkeep cycle (1 per extra)") + _assert_eq(food_cost, 2, "4 workers should cost 2 food per upkeep cycle (1 per extra)") # ── Test: one extra worker costs exactly one food per interval ─────────────── func test_one_extra_worker_cost() -> void: var food_cost := get_food_cost(3, Constants.FOOD_PER_EXTRA_WORKER) - assert_eq(food_cost, 1, "3 workers (1 extra) should cost 1 food") + _assert_eq(food_cost, 1, "3 workers (1 extra) should cost 1 food") # ── Test: upkeep never drives food negative ────────────────────────────────── @@ -35,27 +81,27 @@ func test_upkeep_never_negative() -> void: var current_food := 2 var cost := get_food_cost(5, Constants.FOOD_PER_EXTRA_WORKER) # 4 extra * 1 = 4 var remaining := apply_upkeep(current_food, cost) - assert_eq(remaining, 0, "Upkeep should clamp to 0, not go negative") + _assert_eq(remaining, 0, "Upkeep should clamp to 0, not go negative") # ── Test: no slowdown when food is above threshold ─────────────────────────── func test_no_slowdown_when_food_ok() -> void: var factor := get_slowdown_factor(10) - assert_eq(factor, 1.0, "High food should give full speed (1.0)") + _assert_eq(factor, 1.0, "High food should give full speed (1.0)") # ── Test: low-food slowdown at threshold ───────────────────────────────────── func test_low_food_slowdown_at_threshold() -> void: # At exactly LOW_FOOD_THRESHOLD (3), should be at LOW_FOOD_SPEED_FACTOR (0.5) var factor := get_slowdown_factor(Constants.LOW_FOOD_THRESHOLD) - assert_eq(factor, Constants.LOW_FOOD_SPEED_FACTOR, + _assert_eq(factor, Constants.LOW_FOOD_SPEED_FACTOR, "At low food threshold, speed should be 50%") # ── Test: starvation pause at starvation threshold ─────────────────────────── func test_starvation_pause() -> void: var factor := get_slowdown_factor(Constants.STARVATION_FOOD_THRESHOLD) - assert_eq(factor, Constants.STARVATION_SPEED_FACTOR, + _assert_eq(factor, Constants.STARVATION_SPEED_FACTOR, "At starvation threshold, speed should be 0%") @@ -64,60 +110,60 @@ func test_linear_interpolation() -> void: # STARVATION=1, LOW=3, so food=2 is exactly in the middle var factor := get_slowdown_factor(2) var expected = lerp(Constants.STARVATION_SPEED_FACTOR, Constants.LOW_FOOD_SPEED_FACTOR, 0.5) - assert_eq(factor, expected, "Food at midpoint should give interpolated slowdown") + _assert_eq(factor, expected, "Food at midpoint should give interpolated slowdown") # ── Test: food level classification ────────────────────────────────────────── func test_food_level_classification() -> void: - assert_eq(get_food_level(0), "starving", "Zero food is starving") - assert_eq(get_food_level(Constants.STARVATION_FOOD_THRESHOLD), "starving", + _assert_eq(get_food_level(0), "starving", "Zero food is starving") + _assert_eq(get_food_level(Constants.STARVATION_FOOD_THRESHOLD), "starving", "At starvation threshold, still starving") - assert_eq(get_food_level(Constants.STARVATION_FOOD_THRESHOLD + 1), "low", + _assert_eq(get_food_level(Constants.STARVATION_FOOD_THRESHOLD + 1), "low", "One above starvation is low") - assert_eq(get_food_level(Constants.LOW_FOOD_THRESHOLD), "low", + _assert_eq(get_food_level(Constants.LOW_FOOD_THRESHOLD), "low", "At low threshold, still low") - assert_eq(get_food_level(Constants.LOW_FOOD_THRESHOLD + 1), "ok", + _assert_eq(get_food_level(Constants.LOW_FOOD_THRESHOLD + 1), "ok", "One above low threshold is ok") # ── Test: bias to food gathering when low ──────────────────────────────────── func test_bias_to_food_when_low() -> void: - assert_eq(should_bias_to_food(Constants.STARVATION_FOOD_THRESHOLD), true, + _assert_eq(should_bias_to_food(Constants.STARVATION_FOOD_THRESHOLD), true, "Should bias when starving") - assert_eq(should_bias_to_food(Constants.LOW_FOOD_THRESHOLD), true, + _assert_eq(should_bias_to_food(Constants.LOW_FOOD_THRESHOLD), true, "Should bias when low") - assert_eq(should_bias_to_food(Constants.LOW_FOOD_THRESHOLD + 1), false, + _assert_eq(should_bias_to_food(Constants.LOW_FOOD_THRESHOLD + 1), false, "Should not bias when ok") # ── Test: upkeep interval is 10 ticks ──────────────────────────────────────── func test_upkeep_interval() -> void: - assert_eq(Constants.FOOD_UPKEEP_INTERVAL_TICKS, 10, + _assert_eq(Constants.FOOD_UPKEEP_INTERVAL_TICKS, 10, "Upkeep should trigger every 10 ticks") # ── Test: base workers constant ────────────────────────────────────────────── func test_base_workers_constant() -> void: - assert_eq(Constants.BASE_WORKERS_NO_UPKEEP, 2, + _assert_eq(Constants.BASE_WORKERS_NO_UPKEEP, 2, "Base workers without upkeep should be 2") # ── Test: food per extra worker constant ───────────────────────────────────── func test_food_per_extra_worker() -> void: - assert_eq(Constants.FOOD_PER_EXTRA_WORKER, 1, + _assert_eq(Constants.FOOD_PER_EXTRA_WORKER, 1, "Each extra worker consumes 1 food per interval") # ── Test: constants are consistent with acceptance criteria ────────────────── func test_constants_consistency() -> void: # STARVATION < LOW ensures interpolation range exists - assert_lt(Constants.STARVATION_FOOD_THRESHOLD, Constants.LOW_FOOD_THRESHOLD, + _assert(Constants.STARVATION_FOOD_THRESHOLD < Constants.LOW_FOOD_THRESHOLD, "Starvation threshold must be below low threshold") # Speed factors are in [0, 1] - assert_gte(Constants.STARVATION_SPEED_FACTOR, 0.0, "Starvation factor >= 0") - assert_lte(Constants.STARVATION_SPEED_FACTOR, 1.0, "Starvation factor <= 1") - assert_gte(Constants.LOW_FOOD_SPEED_FACTOR, 0.0, "Low food factor >= 0") - assert_lte(Constants.LOW_FOOD_SPEED_FACTOR, 1.0, "Low food factor <= 1") + _assert(Constants.STARVATION_SPEED_FACTOR >= 0.0, "Starvation factor >= 0") + _assert(Constants.STARVATION_SPEED_FACTOR <= 1.0, "Starvation factor <= 1") + _assert(Constants.LOW_FOOD_SPEED_FACTOR >= 0.0, "Low food factor >= 0") + _assert(Constants.LOW_FOOD_SPEED_FACTOR <= 1.0, "Low food factor <= 1") # ── Helper functions (mirroring main.gd logic for test isolation) ──────────── diff --git a/tests/test_recruit_worker.gd b/tests/test_recruit_worker.gd index 9bcdfd8..606674a 100644 --- a/tests/test_recruit_worker.gd +++ b/tests/test_recruit_worker.gd @@ -8,7 +8,7 @@ var test_fail := 0 func _initialize() -> void: # Load main.gd and create an instance (no UI nodes needed for logic tests) - var main_script: GDScript = preload("res://scripts/main.gd") + var main_script: GDScript = load("res://scripts/main.gd") var main: Control = main_script.new() test_can_recruit_with_capacity(main) @@ -80,7 +80,7 @@ func test_recruit_adds_worker_to_state(main: Control) -> void: {"name": "Jun", "task": {"kind": "", "data": {}}}, ]) _assert(main.can_recruit_worker(), "precondition: can recruit") - var initial_count := main.state.workers.size() + var initial_count: int = main.state.workers.size() main.recruit_worker() _assert_eq(main.state.workers.size(), initial_count + 1, "recruit: state workers count increases by 1") diff --git a/tests/test_resource_trends.gd b/tests/test_resource_trends.gd index 58ffff2..224a835 100644 --- a/tests/test_resource_trends.gd +++ b/tests/test_resource_trends.gd @@ -10,6 +10,12 @@ extends SceneTree const C := preload("res://scripts/constants.gd") +const MainScript := load("res://scripts/main.gd") + + +# --- Helper: create a fresh Main instance (no autoloads needed) --- +static func _make_main() -> Control: + return MainScript.new() # --- Layout/clipping tests for HUD row labels (issue #135) --- @@ -184,17 +190,15 @@ func _test_trend_stable_value() -> bool: # --- _get_trend logic tests --- -## Since _get_trend is a method of Main (an autoload singleton), we call it -## directly through the autoload reference. The function signature is: +## Since _get_trend is a method of Main, we create a fresh instance +## via preload + new() instead of relying on autoloads. The function signature is: ## func _get_trend(resource_name: String) -> String ## It reads from `state.resources` and `prev_resources`, so we set those up ## before each call. func _get_trend_mock(resource_name: String, current_val: int, previous_val: int = -1) -> String: """Simulate _get_trend by setting state.resources and prev_resources then calling the method.""" - var main := Globals.get_node("/root/Main") as Node - if main == null: - return {"ok": false, "msg": "Main autoload not found"} + var main := _make_main() as Control # Set up state.resources main.state = { @@ -251,9 +255,7 @@ func _test_get_trend_unknown_resource() -> bool: # --- stockpile_summary_text arrow embedding tests --- func _test_summary_contains_rising_arrow() -> bool: - var main := Globals.get_node("/root/Main") as Node - if main == null: - return {"ok": false, "msg": "Main autoload not found"} + var main := _make_main() as Control main.state = { "resources": {"wood": 10, "stone": 4, "food": 3}, @@ -272,9 +274,7 @@ func _test_summary_contains_rising_arrow() -> bool: return summary.find(rising_arrow) >= 0 func _test_summary_contains_stable_arrow() -> bool: - var main := Globals.get_node("/root/Main") as Node - if main == null: - return {"ok": false, "msg": "Main autoload not found"} + var main := _make_main() as Control main.state = { "resources": {"wood": 8, "stone": 4, "food": 2}, @@ -300,9 +300,7 @@ func _test_summary_contains_stable_arrow() -> bool: ## Safe upper bound: ~35 characters for 280px sidebar, ~40 for 320px sidebar. func _test_compact_summary_fits_bottom_dock() -> bool: - var main := Globals.get_node("/root/Main") as Node - if main == null: - return {"ok": false, "msg": "Main autoload not found"} + var main := _make_main() as Control main.state = { "resources": {"wood": 100, "stone": 50, "food": 75}, @@ -327,9 +325,7 @@ func _test_compact_summary_fits_bottom_dock() -> bool: return true func _test_compact_summary_fits_side_dock() -> bool: - var main := Globals.get_node("/root/Main") as Node - if main == null: - return {"ok": false, "msg": "Main autoload not found"} + var main := _make_main() as Control main.state = { "resources": {"wood": 100, "stone": 50, "food": 75}, @@ -353,9 +349,7 @@ func _test_compact_summary_fits_side_dock() -> bool: return true func _test_noncompact_first_line_fits() -> bool: - var main := Globals.get_node("/root/Main") as Node - if main == null: - return {"ok": false, "msg": "Main autoload not found"} + var main := _make_main() as Control main.state = { "resources": {"wood": 100, "stone": 50, "food": 75}, @@ -384,9 +378,7 @@ func _test_noncompact_first_line_fits() -> bool: return true func _test_all_arrows_in_compact() -> bool: - var main := Globals.get_node("/root/Main") as Node - if main == null: - return {"ok": false, "msg": "Main autoload not found"} + var main := _make_main() as Control # Set up a scenario where all three resources have different trends main.state = { @@ -421,9 +413,7 @@ func _test_all_arrows_in_compact() -> bool: return true func _test_all_arrows_in_noncompact() -> bool: - var main := Globals.get_node("/root/Main") as Node - if main == null: - return {"ok": false, "msg": "Main autoload not found"} + var main := _make_main() as Control # Set up a scenario where all three resources have different trends main.state = { @@ -458,9 +448,7 @@ func _test_all_arrows_in_noncompact() -> bool: return true func _test_extreme_values_fit_compact() -> bool: - var main := Globals.get_node("/root/Main") as Node - if main == null: - return {"ok": false, "msg": "Main autoload not found"} + var main := _make_main() as Control # Test with large resource values to ensure no clipping from wider numbers main.state = { diff --git a/tests/test_worker_cap.gd b/tests/test_worker_cap.gd index 113f23f..255b7bc 100644 --- a/tests/test_worker_cap.gd +++ b/tests/test_worker_cap.gd @@ -8,7 +8,7 @@ var test_fail := 0 func _initialize() -> void: # Load main.gd and create an instance (no UI nodes needed for logic tests) - var main_script: GDScript = preload("res://scripts/main.gd") + var main_script: GDScript = load("res://scripts/main.gd") var main: Control = main_script.new() test_base_cap_no_structures(main) diff --git a/tests/test_worker_intent.gd b/tests/test_worker_intent.gd index eee450d..957a0ea 100644 --- a/tests/test_worker_intent.gd +++ b/tests/test_worker_intent.gd @@ -7,7 +7,7 @@ var test_pass := 0 var test_fail := 0 func _initialize() -> void: - var main_script: GDScript = preload("res://scripts/main.gd") + var main_script: GDScript = load("res://scripts/main.gd") var main: Control = main_script.new() test_worker_intent_icon_gather_wood(main)