Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion tests/test_colony_stance.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
Expand Down
100 changes: 73 additions & 27 deletions tests/test_food_upkeep.gd
Original file line number Diff line number Diff line change
@@ -1,61 +1,107 @@
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 ──────────────────────────────────
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%")


Expand All @@ -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) ────────────
Expand Down
4 changes: 2 additions & 2 deletions tests/test_recruit_worker.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")

Expand Down
Loading