Files
gamedev-the-steward/tests/unit/test_simulation_save_manifest.gd
T
2026-08-23 17:20:41 +02:00

281 lines
10 KiB
GDScript

extends GutTest
const MANAGER_SCRIPT := preload("res://simulation/SimulationManager.gd")
var manager: Node
func before_each() -> void:
manager = MANAGER_SCRIPT.new()
add_child_autofree(manager)
func test_canonical_manifest_round_trips_defensively() -> void:
var local_state: SimulationStateRecord = manager.create_state_record()
var regional_facade := RegionalSimulationFacade.create_jajce_route()
var manifest := SimulationSaveManifest.create(local_state, regional_facade)
assert_not_null(manifest)
var saved := manifest.to_dictionary()
var expected_fields := [
"local_checksum",
"local_state",
"regional_checksum",
"regional_state",
"schema",
"schema_version",
]
var saved_fields := saved.keys()
saved_fields.sort()
assert_eq(saved_fields, expected_fields)
assert_eq(saved["schema"], SimulationSaveManifest.SCHEMA_NAME)
assert_eq(saved["schema_version"], SimulationSaveManifest.SCHEMA_VERSION)
assert_eq(
saved["local_checksum"],
SimulationSaveManifest._checksum_dictionary(saved["local_state"]),
)
assert_eq(
saved["regional_checksum"],
SimulationSaveManifest._checksum_dictionary(saved["regional_state"]),
)
var restored := SimulationSaveManifest.from_dictionary(saved)
var restored_json := SimulationSaveManifest.from_json(manifest.to_json())
assert_not_null(restored)
assert_not_null(restored_json)
assert_eq(restored.to_dictionary(), saved)
assert_eq(restored.checksum(), manifest.checksum())
assert_eq(restored_json.checksum(), manifest.checksum())
assert_not_same(manifest.get_local_state(), local_state)
assert_not_same(manifest.get_regional_facade(), regional_facade)
local_state.simulation["tick_count"] = 99
var copied_local := manifest.get_local_state()
copied_local.simulation["tick_count"] = 77
assert_eq(int(manifest.get_local_state().simulation["tick_count"]), 0)
var copied_regional := manifest.get_regional_facade().to_dictionary()
copied_regional["last_processed_tick"] = 88
assert_eq(manifest.get_regional_facade().get_last_processed_tick(), 0)
func test_parser_rejects_missing_extra_and_wrong_manifest_fields() -> void:
var valid := _valid_manifest_dictionary()
for field: String in SimulationSaveManifest.MANIFEST_FIELDS:
var missing := valid.duplicate(true)
missing.erase(field)
assert_null(SimulationSaveManifest.from_dictionary(missing), "Missing %s" % field)
var extra := valid.duplicate(true)
extra["unexpected"] = true
assert_null(SimulationSaveManifest.from_dictionary(extra))
var wrong_schema := valid.duplicate(true)
wrong_schema["schema"] = &"the_steward.save_manifest"
assert_null(SimulationSaveManifest.from_dictionary(wrong_schema))
var wrong_version := valid.duplicate(true)
wrong_version["schema_version"] = "1"
assert_null(SimulationSaveManifest.from_dictionary(wrong_version))
var wrong_float_version := valid.duplicate(true)
wrong_float_version["schema_version"] = 1.0
assert_null(SimulationSaveManifest.from_dictionary(wrong_float_version))
var wrong_local := valid.duplicate(true)
wrong_local["local_state"] = []
assert_null(SimulationSaveManifest.from_dictionary(wrong_local))
var wrong_regional := valid.duplicate(true)
wrong_regional["regional_state"] = "regional"
assert_null(SimulationSaveManifest.from_dictionary(wrong_regional))
var wrong_local_checksum := valid.duplicate(true)
wrong_local_checksum["local_checksum"] = 1
assert_null(SimulationSaveManifest.from_dictionary(wrong_local_checksum))
var wrong_regional_checksum := valid.duplicate(true)
wrong_regional_checksum["regional_checksum"] = []
assert_null(SimulationSaveManifest.from_dictionary(wrong_regional_checksum))
assert_null(SimulationSaveManifest.from_json("[]"))
assert_null(SimulationSaveManifest.from_json("not json"))
func test_nested_tampering_and_checksum_mismatches_fail_closed() -> void:
var valid := _valid_manifest_dictionary()
var local_checksum_mismatch := valid.duplicate(true)
local_checksum_mismatch["local_checksum"] = "0".repeat(64)
assert_null(SimulationSaveManifest.from_dictionary(local_checksum_mismatch))
var regional_checksum_mismatch := valid.duplicate(true)
regional_checksum_mismatch["regional_checksum"] = "f".repeat(64)
assert_null(SimulationSaveManifest.from_dictionary(regional_checksum_mismatch))
var tampered_local := valid.duplicate(true)
tampered_local["local_state"]["simulation"]["seed"] = 4004
assert_null(SimulationSaveManifest.from_dictionary(tampered_local))
var tampered_regional := valid.duplicate(true)
tampered_regional["regional_state"]["last_processed_tick"] = 1
assert_null(SimulationSaveManifest.from_dictionary(tampered_regional))
var nested_extra := valid.duplicate(true)
nested_extra["local_state"]["unexpected"] = true
nested_extra["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
nested_extra["local_state"]
)
assert_null(SimulationSaveManifest.from_dictionary(nested_extra))
assert_null(SimulationSaveManifest.from_json(JSON.stringify(nested_extra)))
func test_local_seed_and_tick_require_exact_integer_types() -> void:
var valid := _valid_manifest_dictionary()
var invalid_boundaries: Array[Dictionary] = [
{"field": "seed", "value": "1337"},
{"field": "seed", "value": 1337.5},
{"field": "tick_count", "value": "0"},
{"field": "tick_count", "value": 0.5},
]
for invalid_boundary: Dictionary in invalid_boundaries:
var field := String(invalid_boundary["field"])
var value: Variant = invalid_boundary["value"]
var malformed := valid.duplicate(true)
malformed["local_state"]["simulation"][field] = value
malformed["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
malformed["local_state"]
)
assert_null(
SimulationSaveManifest.from_dictionary(malformed),
"Dictionary parser should reject %s=%s" % [field, value],
)
assert_null(
SimulationSaveManifest.from_json(JSON.stringify(malformed)),
"JSON parser should reject %s=%s" % [field, value],
)
var local_state: SimulationStateRecord = manager.create_state_record()
local_state.simulation[field] = value
assert_null(
SimulationSaveManifest.create(
local_state, RegionalSimulationFacade.create_jajce_route()
),
"Manifest creation should reject %s=%s" % [field, value],
)
func test_json_parser_rejects_nonnumeric_regional_ledger_values() -> void:
var string_ledger := _valid_manifest_dictionary()
_set_regional_stockpile_value(
string_ledger["regional_state"],
RegionalSimulationFacade.SETTLEMENT_TRAVNIK,
"4",
)
assert_null(SimulationSaveManifest.from_json(JSON.stringify(string_ledger)))
var boolean_ledger := _valid_manifest_dictionary()
_set_regional_stockpile_value(
boolean_ledger["regional_state"],
RegionalSimulationFacade.SETTLEMENT_JAJCE,
false,
)
var canonical_regional_state: Dictionary = boolean_ledger["regional_state"].duplicate(true)
_set_regional_stockpile_value(
canonical_regional_state,
RegionalSimulationFacade.SETTLEMENT_JAJCE,
0.0,
)
boolean_ledger["regional_checksum"] = SimulationSaveManifest._checksum_dictionary(
canonical_regional_state
)
assert_null(SimulationSaveManifest.from_json(JSON.stringify(boolean_ledger)))
func test_world_location_and_tick_must_match_the_regional_boundary() -> void:
var valid := _valid_manifest_dictionary()
var wrong_world := valid.duplicate(true)
wrong_world["local_state"]["simulation"]["world_id"] = "regional_elsewhere"
wrong_world["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
wrong_world["local_state"]
)
assert_null(SimulationSaveManifest.from_dictionary(wrong_world))
var wrong_location := valid.duplicate(true)
wrong_location["local_state"]["simulation"]["location_id"] = String(
RegionalSimulationFacade.LOCATION_TRAVNIK
)
wrong_location["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
wrong_location["local_state"]
)
assert_null(SimulationSaveManifest.from_dictionary(wrong_location))
var wrong_seed := valid.duplicate(true)
wrong_seed["local_state"]["simulation"]["seed"] = (
RegionalSimulationFacade.DEFAULT_WORLD_SEED + 1
)
wrong_seed["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
wrong_seed["local_state"]
)
assert_null(SimulationSaveManifest.from_dictionary(wrong_seed))
var wrong_regional_seed := valid.duplicate(true)
wrong_regional_seed["regional_state"]["world_seed"] = (
RegionalSimulationFacade.DEFAULT_WORLD_SEED + 1
)
wrong_regional_seed["regional_checksum"] = SimulationSaveManifest._checksum_dictionary(
wrong_regional_seed["regional_state"]
)
assert_null(SimulationSaveManifest.from_dictionary(wrong_regional_seed))
var wrong_tick := valid.duplicate(true)
wrong_tick["local_state"]["simulation"]["tick_count"] = 1
wrong_tick["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
wrong_tick["local_state"]
)
assert_null(SimulationSaveManifest.from_dictionary(wrong_tick))
assert_null(
SimulationSaveManifest.create(
manager.create_state_record(),
RegionalSimulationFacade.create_jajce_route(
RegionalSimulationFacade.DEFAULT_WORLD_SEED, 1
)
)
)
assert_null(
SimulationSaveManifest.create(
manager.create_state_record(),
RegionalSimulationFacade.create_jajce_route(
RegionalSimulationFacade.DEFAULT_WORLD_SEED + 1, 0
)
)
)
var negative_seed_local: SimulationStateRecord = manager.create_state_record()
negative_seed_local.simulation["seed"] = -42
assert_not_null(
SimulationSaveManifest.create(
negative_seed_local, RegionalSimulationFacade.create_jajce_route(-42, 0)
)
)
manager.simulate_tick()
var synchronized := SimulationSaveManifest.create(
manager.create_state_record(),
RegionalSimulationFacade.create_jajce_route(RegionalSimulationFacade.DEFAULT_WORLD_SEED, 1)
)
assert_not_null(synchronized)
assert_eq(synchronized.get_regional_facade().get_last_processed_tick(), 1)
assert_eq(int(synchronized.get_local_state().simulation["tick_count"]), 1)
func _valid_manifest_dictionary() -> Dictionary:
var manifest := SimulationSaveManifest.create(
manager.create_state_record(), RegionalSimulationFacade.create_jajce_route()
)
assert_not_null(manifest)
return manifest.to_dictionary()
func _set_regional_stockpile_value(
regional_state: Dictionary, settlement_id: StringName, value: Variant
) -> void:
for settlement: Dictionary in regional_state["caravan_service"]["world_state"]["settlements"]:
if StringName(settlement["settlement_id"]) == settlement_id:
settlement["stockpile_ledger"][String(RegionalSimulationFacade.TRADE_GOODS)] = value
return
assert_true(false, "Expected regional settlement '%s'" % settlement_id)