Files
gamedev-the-steward/tests/unit/test_world_event_retention.gd
2026-08-12 22:59:59 +02:00

300 lines
11 KiB
GDScript

extends GutTest
func test_daily_rollups_group_by_day_location_item_and_type_deterministically() -> void:
var events: Array[WorldEventRecord] = [
_transaction(9, &"storage_deposited", 5, &"village_pantry", &"food", 2.0),
_transaction(2, &"storage_deposited", 4, &"village_pantry", &"food", 1.0),
_transaction(4, &"storage_deposited", 8, &"village_pantry", &"wood", 3.0),
_transaction(5, &"storage_deposited", 15, &"village_pantry", &"food", 4.0),
_transaction(6, &"storage_deposited", 15, &"lower_town", &"food", 5.0),
_transaction(7, &"resource_extracted", 15, &"forest_edge", &"food", 6.0),
_transaction(8, &"storage_deposited", 25, &"village_pantry", &"food", 7.0),
]
var source := _store_from_events(events)
var source_before := source.to_dictionary()
var planner := WorldEventRetentionPlanner.new()
var retention_plan := planner.plan(source, 35, 10, 1, [&"storage_deposited"])
assert_not_null(retention_plan)
assert_eq(retention_plan.get_retained_event_ids(), [7, 8])
assert_eq(retention_plan.get_rolled_up_event_ids(), [2, 4, 5, 6, 9])
assert_eq(source.to_dictionary(), source_before)
var rollups := retention_plan.get_rollup_store()
assert_eq(rollups.size(), 4)
var day_zero_food := rollups.get_by_key(
0, &"world_core", &"village_pantry", &"storage_deposited", &"food"
)
assert_not_null(day_zero_food)
assert_eq(day_zero_food.get_event_count(), 2)
assert_eq(day_zero_food.get_total_amount(), 3.0)
assert_eq(day_zero_food.get_minimum_tick(), 4)
assert_eq(day_zero_food.get_maximum_tick(), 5)
assert_eq(day_zero_food.get_minimum_event_id(), 2)
assert_eq(day_zero_food.get_maximum_event_id(), 9)
assert_eq(day_zero_food.get_source_event_ids_checksum(), JSON.stringify([2, 9]).sha256_text())
assert_eq(rollups.get_for_day(0).size(), 2)
assert_eq(rollups.get_for_location(&"world_core", &"village_pantry").size(), 3)
assert_eq(rollups.get_for_item(&"food").size(), 3)
assert_eq(rollups.get_for_event_type(&"storage_deposited").size(), 4)
assert_eq(retention_plan.get_reason_trace(7)["reasons"], ["not_rollup_eligible"])
assert_eq(retention_plan.get_reason_trace(8)["reasons"], ["within_raw_retention"])
var telemetry := retention_plan.get_telemetry()
assert_eq(telemetry["input_event_count"], 7)
assert_eq(telemetry["retained_event_count"], 2)
assert_eq(telemetry["rolled_up_event_count"], 5)
assert_eq(telemetry["rollup_record_count"], 4)
assert_eq(telemetry["rollup_contribution_count"], 5)
assert_true(telemetry["source_unchanged"])
assert_eq(planner.get_last_telemetry(), telemetry)
var reversed_events := events.duplicate()
reversed_events.reverse()
var reversed_plan := WorldEventRetentionPlanner.new().plan(
_store_from_events(reversed_events), 35, 10, 1, [&"storage_deposited"]
)
assert_not_null(reversed_plan)
assert_eq(reversed_plan.to_dictionary(), retention_plan.to_dictionary())
assert_eq(reversed_plan.checksum(), retention_plan.checksum())
func test_pinned_external_and_payload_causal_facts_remain_exact_raw_events() -> void:
var source := WorldEventStore.new()
assert_true(source.append(_causal_transaction(1, 1, -1)))
assert_true(source.append(_causal_transaction(2, 2, 1)))
assert_true(
source.append(
WorldEventRecord.create(
3, &"situation_opened", 3, {}, _address(&"village_pantry"), {"cause_event_id": 2}
)
)
)
assert_true(source.append(_causal_transaction(4, 4, -1)))
assert_true(source.append(_causal_transaction(5, 5, -1)))
assert_true(source.append(_causal_transaction(6, 6, -1)))
var planner := WorldEventRetentionPlanner.new()
var retention_plan := planner.plan(source, 50, 10, 0, [&"storage_deposited"], [4], [5])
assert_not_null(retention_plan)
assert_eq(retention_plan.get_retained_event_ids(), [1, 2, 3, 4, 5])
assert_eq(retention_plan.get_rolled_up_event_ids(), [6])
assert_has(retention_plan.get_reason_trace(1)["reasons"], "causal_ancestor")
assert_eq(retention_plan.get_reason_trace(1)["required_by_event_ids"], [2])
assert_has(retention_plan.get_reason_trace(2)["reasons"], "causal_ancestor")
assert_eq(retention_plan.get_reason_trace(2)["required_by_event_ids"], [3])
assert_has(retention_plan.get_reason_trace(3)["reasons"], "not_rollup_eligible")
assert_has(retention_plan.get_reason_trace(4)["reasons"], "pinned_fact")
assert_has(retention_plan.get_reason_trace(5)["reasons"], "explicit_causal_fact")
assert_eq(retention_plan.get_reason_trace(6)["decision"], "rolled_up")
assert_eq(retention_plan.get_telemetry()["causal_ancestor_count"], 2)
var missing_causal_trace := retention_plan.to_dictionary()
missing_causal_trace["reason_traces"][0]["reasons"].erase("causal_ancestor")
assert_null(WorldEventRetentionPlan.from_dictionary(missing_causal_trace))
var missing_pin_trace := retention_plan.to_dictionary()
missing_pin_trace["reason_traces"][3]["reasons"].erase("pinned_fact")
assert_null(WorldEventRetentionPlan.from_dictionary(missing_pin_trace))
var before_missing_pin := source.to_dictionary()
assert_null(planner.plan(source, 50, 10, 0, [&"storage_deposited"], [404]))
assert_string_contains(planner.get_last_error(), "pinned fact ID 404 is missing")
assert_false(planner.get_last_telemetry()["succeeded"])
assert_eq(source.to_dictionary(), before_missing_pin)
func test_cargo_ledger_rolls_up_each_item_and_invalid_transactions_stay_raw() -> void:
var source := WorldEventStore.new()
assert_true(
source.append(
WorldEventRecord.create(
10,
&"caravan_cargo_deposited",
1,
{},
_address(&"river_port"),
{"cargo_ledger": {"wood": 1.0, "food": 2.5}, "cause_event_id": -1}
)
)
)
assert_true(
source.append(
WorldEventRecord.create(
11,
&"caravan_cargo_deposited",
2,
{},
_address(&"river_port"),
{"cargo_ledger": {"food": 1.0}, "item_id": "food", "amount": 1.0}
)
)
)
assert_true(
source.append(
WorldEventRecord.create(
12,
&"caravan_cargo_deposited",
2,
{},
_address(&"river_port"),
{"cargo_ledger": {"food": 0.0}}
)
)
)
assert_true(
source.append(
WorldEventRecord.create(
13, &"caravan_cargo_deposited", 2, {}, null, {"cargo_ledger": {"food": 1.0}}
)
)
)
var retention_plan := WorldEventRetentionPlanner.new().plan(
source, 30, 10, 0, [&"caravan_cargo_deposited"]
)
assert_not_null(retention_plan)
assert_eq(retention_plan.get_rolled_up_event_ids(), [10])
assert_eq(retention_plan.get_retained_event_ids(), [11, 12, 13])
var rollups := retention_plan.get_rollup_store()
assert_eq(rollups.size(), 2)
assert_eq(
(
rollups
. get_by_key(0, &"world_core", &"river_port", &"caravan_cargo_deposited", &"food")
. get_total_amount()
),
2.5
)
assert_eq(
(
rollups
. get_by_key(0, &"world_core", &"river_port", &"caravan_cargo_deposited", &"wood")
. get_total_amount()
),
1.0
)
assert_eq(retention_plan.get_reason_trace(10)["rollup_keys"].size(), 2)
for event_id in [11, 12, 13]:
assert_has(retention_plan.get_reason_trace(event_id)["reasons"], "rollup_payload_invalid")
assert_eq(retention_plan.get_telemetry()["invalid_rollup_event_count"], 3)
assert_eq(retention_plan.get_telemetry()["rollup_contribution_count"], 2)
func test_record_store_and_plan_are_canonical_primitive_and_round_trippable() -> void:
var source := _store_from_events(
[
_transaction(3, &"storage_deposited", 1, &"pantry", &"food", 2.0),
_transaction(4, &"storage_deposited", 2, &"pantry", &"food", 3.0),
]
)
var retention_plan := WorldEventRetentionPlanner.new().plan(
source, 20, 10, 0, [&"storage_deposited"]
)
var serialized := retention_plan.to_dictionary()
var restored := WorldEventRetentionPlan.from_dictionary(serialized)
assert_not_null(restored)
assert_true(_is_primitive_tree(serialized))
assert_eq(restored.to_dictionary(), serialized)
assert_eq(JSON.stringify(restored.to_dictionary()), JSON.stringify(serialized))
assert_eq(restored.checksum(), retention_plan.checksum())
var rollup := restored.get_rollup_store().get_all()[0]
assert_has(rollup.get_reason_trace(), &"daily_transaction_rollup")
assert_true(String(rollup.get_reason_trace()[-1]).begins_with("source_event_ids_sha256_"))
assert_eq(
WorldEventDailyRollupRecord.from_dictionary(rollup.to_dictionary()).to_dictionary(),
rollup.to_dictionary()
)
var store := restored.get_rollup_store()
var store_before := store.to_dictionary()
var duplicated_store := store_before.duplicate(true)
duplicated_store["rollups"].append(store_before["rollups"][0].duplicate(true))
assert_false(store.restore_from_dictionary(duplicated_store))
assert_eq(store.to_dictionary(), store_before)
var noncanonical_plan := serialized.duplicate(true)
noncanonical_plan["reason_traces"].reverse()
assert_null(WorldEventRetentionPlan.from_dictionary(noncanonical_plan))
var invalid_record := rollup.to_dictionary()
invalid_record["total_amount"] = -1.0
assert_null(WorldEventDailyRollupRecord.from_dictionary(invalid_record))
var false_source_checksum := serialized.duplicate(true)
false_source_checksum["rollup_store"]["rollups"][0]["source_event_ids_checksum"] = (
JSON.stringify([3]).sha256_text()
)
assert_null(WorldEventRetentionPlan.from_dictionary(false_source_checksum))
func test_planner_rejects_dangling_and_non_prior_causal_facts_without_mutation() -> void:
var dangling := WorldEventStore.new()
assert_true(dangling.append(_causal_transaction(1, 1, 99)))
var dangling_before := dangling.to_dictionary()
var planner := WorldEventRetentionPlanner.new()
assert_null(planner.plan(dangling, 20, 10, 0, [&"storage_deposited"] as Array))
assert_string_contains(planner.get_last_error(), "missing causal fact 99")
assert_eq(dangling.to_dictionary(), dangling_before)
var forward := WorldEventStore.new()
assert_true(forward.append(_causal_transaction(1, 5, 2)))
assert_true(forward.append(_causal_transaction(2, 5, -1)))
var forward_before := forward.to_dictionary()
assert_null(planner.plan(forward, 20, 10, 0, [&"storage_deposited"] as Array))
assert_string_contains(planner.get_last_error(), "non-prior causal fact 2")
assert_eq(forward.to_dictionary(), forward_before)
func _transaction(
event_id: int,
event_type: StringName,
tick: int,
location_id: StringName,
item_id: StringName,
amount: float
) -> WorldEventRecord:
return WorldEventRecord.create(
event_id,
event_type,
tick,
{},
_address(location_id),
{"item_id": String(item_id), "amount": amount}
)
func _causal_transaction(event_id: int, tick: int, cause_event_id: int) -> WorldEventRecord:
return WorldEventRecord.create(
event_id,
&"storage_deposited",
tick,
{},
_address(&"village_pantry"),
{"item_id": "food", "amount": 1.0, "cause_event_id": cause_event_id}
)
func _address(location_id: StringName) -> SpatialAddress:
return SpatialAddress.create(&"world_core", location_id, Vector3.ZERO)
func _store_from_events(events: Array[WorldEventRecord]) -> WorldEventStore:
var store := WorldEventStore.new()
for event: WorldEventRecord in events:
assert_true(store.append(event))
return store
func _is_primitive_tree(value: Variant) -> bool:
if value == null or value is bool or value is int or value is String:
return true
if value is float:
return is_finite(value)
if value is Array:
for item: Variant in value:
if not _is_primitive_tree(item):
return false
return true
if value is Dictionary:
for key: Variant in value:
if not key is String or not _is_primitive_tree(value[key]):
return false
return true
return false