300 lines
11 KiB
GDScript
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
|