feat: add bounded npc memory retention
This commit is contained in:
@@ -17,6 +17,7 @@ func _run() -> void:
|
||||
_test_previous_world_relationship_migration()
|
||||
_test_previous_world_knowledge_migration()
|
||||
_test_previous_world_provenance_migration()
|
||||
_test_previous_world_retention_migration()
|
||||
_test_relationship_schema_rejection()
|
||||
_test_schema_rejection()
|
||||
|
||||
@@ -138,6 +139,15 @@ func _test_schema_rejection() -> void:
|
||||
)
|
||||
_check(SimulationStateRecord.from_json("[]") == null, "Non-record JSON should be rejected")
|
||||
|
||||
var manager := _create_manager(41)
|
||||
var invalid_clock_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||
invalid_clock_data["simulation"]["tick_interval"] = 0.0
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(invalid_clock_data) == null,
|
||||
"Saved simulation clocks with a zero tick interval should be rejected"
|
||||
)
|
||||
manager.free()
|
||||
|
||||
|
||||
func _test_legacy_resource_migration() -> void:
|
||||
var migrated := ResourceStateRecord.from_dictionary(
|
||||
@@ -272,7 +282,7 @@ func _test_previous_world_knowledge_migration() -> void:
|
||||
var deposit_event := EconomicEventRecord.create(
|
||||
0,
|
||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||
12,
|
||||
0,
|
||||
1,
|
||||
SimulationIds.npc_inventory_id(1),
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
@@ -305,11 +315,11 @@ func _test_previous_world_knowledge_migration() -> void:
|
||||
func _test_previous_world_provenance_migration() -> void:
|
||||
var manager := _create_manager(60)
|
||||
var previous_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||
previous_data["schema_version"] = SimulationStateRecord.PREVIOUS_SCHEMA_VERSION
|
||||
previous_data["schema_version"] = SimulationStateRecord.PROVENANCE_LEGACY_SCHEMA_VERSION
|
||||
var deposit_event := EconomicEventRecord.create(
|
||||
0,
|
||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||
12,
|
||||
0,
|
||||
1,
|
||||
SimulationIds.npc_inventory_id(1),
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
@@ -358,6 +368,77 @@ func _test_previous_world_provenance_migration() -> void:
|
||||
manager.free()
|
||||
|
||||
|
||||
func _test_previous_world_retention_migration() -> void:
|
||||
var manager := _create_manager(61)
|
||||
var previous_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||
previous_data["schema_version"] = SimulationStateRecord.PREVIOUS_SCHEMA_VERSION
|
||||
previous_data["simulation"]["tick_count"] = 50
|
||||
var deposit_event := EconomicEventRecord.create(
|
||||
0,
|
||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||
0,
|
||||
0,
|
||||
SimulationIds.npc_inventory_id(0),
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
2.0
|
||||
)
|
||||
previous_data["economic_events"] = [deposit_event.to_dictionary()]
|
||||
previous_data["simulation"]["next_event_id"] = 1
|
||||
var actor_knowledge := (
|
||||
KnownEventStateRecord
|
||||
. create(0, 0, SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED)
|
||||
. to_dictionary()
|
||||
)
|
||||
var witness_knowledge := (
|
||||
KnownEventStateRecord
|
||||
. create(2, 0, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED)
|
||||
. to_dictionary()
|
||||
)
|
||||
var communicated_knowledge := (
|
||||
KnownEventStateRecord
|
||||
. create(
|
||||
1,
|
||||
0,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||
2,
|
||||
0,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||
)
|
||||
. to_dictionary()
|
||||
)
|
||||
for old_record in [actor_knowledge, witness_knowledge, communicated_knowledge]:
|
||||
old_record["schema_version"] = 2
|
||||
old_record.erase("acquired_tick")
|
||||
old_record.erase("source_acquisition_method")
|
||||
previous_data["event_knowledge"] = [communicated_knowledge, actor_knowledge, witness_knowledge]
|
||||
previous_data["relationships"] = []
|
||||
var migrated := SimulationStateRecord.from_dictionary(previous_data)
|
||||
_check(migrated != null, "World schema v6 should add deterministic retention metadata")
|
||||
if migrated != null:
|
||||
var records_by_knower := {}
|
||||
for record in migrated.event_knowledge:
|
||||
records_by_knower[record.get_knower_id()] = record
|
||||
var migrated_actor := records_by_knower.get(0) as KnownEventStateRecord
|
||||
var migrated_listener := records_by_knower.get(1) as KnownEventStateRecord
|
||||
_check(
|
||||
migrated_actor != null and migrated_actor.get_acquired_tick() == 0,
|
||||
"Direct v6 knowledge should retain the objective event tick"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
migrated_listener != null
|
||||
and migrated_listener.get_acquired_tick() == 50
|
||||
and (
|
||||
migrated_listener.get_source_acquisition_method()
|
||||
== SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||
)
|
||||
),
|
||||
"Communicated v6 knowledge should receive a fresh lease and source snapshot"
|
||||
)
|
||||
manager.free()
|
||||
|
||||
|
||||
func _test_relationship_schema_rejection() -> void:
|
||||
var manager := _create_manager(58)
|
||||
var missing_cause_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||
@@ -389,7 +470,7 @@ func _test_relationship_schema_rejection() -> void:
|
||||
var event := EconomicEventRecord.create(
|
||||
0,
|
||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
SimulationIds.npc_inventory_id(0),
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
@@ -411,7 +492,7 @@ func _test_relationship_schema_rejection() -> void:
|
||||
var unrelated_deposit := EconomicEventRecord.create(
|
||||
0,
|
||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
SimulationIds.npc_inventory_id(2),
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
@@ -435,7 +516,7 @@ func _test_relationship_schema_rejection() -> void:
|
||||
var nonpositive_deposit := EconomicEventRecord.create(
|
||||
0,
|
||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
SimulationIds.npc_inventory_id(1),
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
@@ -456,26 +537,40 @@ func _test_relationship_schema_rejection() -> void:
|
||||
var provenance_event := EconomicEventRecord.create(
|
||||
0,
|
||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
SimulationIds.npc_inventory_id(0),
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
1.0
|
||||
)
|
||||
var missing_source_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||
missing_source_data["economic_events"] = [provenance_event.to_dictionary()]
|
||||
missing_source_data["simulation"]["next_event_id"] = 1
|
||||
missing_source_data["event_knowledge"] = [
|
||||
var historical_source_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||
historical_source_data["economic_events"] = [provenance_event.to_dictionary()]
|
||||
historical_source_data["simulation"]["next_event_id"] = 1
|
||||
historical_source_data["event_knowledge"] = [
|
||||
(
|
||||
KnownEventStateRecord
|
||||
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 2)
|
||||
. create(
|
||||
1,
|
||||
0,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||
2,
|
||||
0,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||
)
|
||||
. to_dictionary()
|
||||
)
|
||||
]
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(missing_source_data) == null,
|
||||
"Communicated knowledge must reference a source who knows the same event"
|
||||
SimulationStateRecord.from_dictionary(historical_source_data) != null,
|
||||
"A listener's provenance should remain valid after the direct source forgets"
|
||||
)
|
||||
|
||||
var unknown_source_data: Dictionary = historical_source_data.duplicate(true)
|
||||
unknown_source_data["event_knowledge"][0]["source_npc_id"] = 999
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(unknown_source_data) == null,
|
||||
"Communicated knowledge must reference an existing historical source"
|
||||
)
|
||||
|
||||
var wrong_actor_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||
@@ -499,12 +594,26 @@ func _test_relationship_schema_rejection() -> void:
|
||||
relayed_source_data["event_knowledge"] = [
|
||||
(
|
||||
KnownEventStateRecord
|
||||
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 2)
|
||||
. create(
|
||||
1,
|
||||
0,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||
2,
|
||||
0,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED
|
||||
)
|
||||
. to_dictionary()
|
||||
),
|
||||
(
|
||||
KnownEventStateRecord
|
||||
. create(2, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 0)
|
||||
. create(
|
||||
2,
|
||||
0,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||
0,
|
||||
0,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED
|
||||
)
|
||||
. to_dictionary()
|
||||
),
|
||||
(
|
||||
@@ -517,6 +626,14 @@ func _test_relationship_schema_rejection() -> void:
|
||||
SimulationStateRecord.from_dictionary(relayed_source_data) == null,
|
||||
"Communicated facts must not form a second relay hop in this phase"
|
||||
)
|
||||
var reacquired_source_data: Dictionary = relayed_source_data.duplicate(true)
|
||||
reacquired_source_data["event_knowledge"][0]["source_acquisition_method"] = String(
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||
)
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(reacquired_source_data) != null,
|
||||
"Historical direct provenance should survive a source reacquiring the fact differently"
|
||||
)
|
||||
|
||||
var order_independent_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||
order_independent_data["economic_events"] = [provenance_event.to_dictionary()]
|
||||
@@ -524,7 +641,14 @@ func _test_relationship_schema_rejection() -> void:
|
||||
order_independent_data["event_knowledge"] = [
|
||||
(
|
||||
KnownEventStateRecord
|
||||
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 2)
|
||||
. create(
|
||||
1,
|
||||
0,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||
2,
|
||||
0,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||
)
|
||||
. to_dictionary()
|
||||
),
|
||||
(
|
||||
@@ -542,6 +666,22 @@ func _test_relationship_schema_rejection() -> void:
|
||||
SimulationStateRecord.from_dictionary(order_independent_data) != null,
|
||||
"Provenance validation should not depend on knowledge array ordering"
|
||||
)
|
||||
|
||||
var invalid_direct_time_data: Dictionary = manager.create_state_record().to_dictionary()
|
||||
invalid_direct_time_data["simulation"]["tick_count"] = 2
|
||||
invalid_direct_time_data["economic_events"] = [provenance_event.to_dictionary()]
|
||||
invalid_direct_time_data["simulation"]["next_event_id"] = 1
|
||||
invalid_direct_time_data["event_knowledge"] = [
|
||||
(
|
||||
KnownEventStateRecord
|
||||
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 1)
|
||||
. to_dictionary()
|
||||
)
|
||||
]
|
||||
_check(
|
||||
SimulationStateRecord.from_dictionary(invalid_direct_time_data) == null,
|
||||
"Direct witness acquisition must use the immutable objective event tick"
|
||||
)
|
||||
manager.free()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user