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_world_v14_migrates_to_empty_emergent_collections() -> void: var previous: Dictionary = manager.create_state_record().to_dictionary() previous["schema_version"] = SimulationStateRecord.PRE_EMERGENT_SCHEMA_VERSION for field in ["situations", "quest_journal", "commitments", "conversation_history"]: previous.erase(field) for field in [ "next_situation_id", "next_journal_entry_id", "next_commitment_id", "next_conversation_act_id", ]: previous["simulation"].erase(field) var migrated: SimulationStateRecord = SimulationStateRecord.from_dictionary(previous) assert_not_null(migrated) assert_true(migrated.situations.is_empty()) assert_true(migrated.quest_journal.is_empty()) assert_true(migrated.commitments.is_empty()) assert_true(migrated.conversation_history.is_empty()) assert_eq(int(migrated.simulation["next_situation_id"]), 0) assert_eq(int(migrated.simulation["next_conversation_act_id"]), 0) func test_world_v15_migrates_to_the_existing_explicit_scope() -> void: var previous: Dictionary = manager.create_state_record().to_dictionary() previous["schema_version"] = SimulationStateRecord.EMERGENT_SCHEMA_VERSION previous["simulation"].erase("world_id") previous["simulation"].erase("location_id") var migrated := SimulationStateRecord.from_dictionary(previous) assert_not_null(migrated) assert_eq(StringName(migrated.simulation["world_id"]), SimulationIds.REGIONAL_WORLD_BOSNIA) assert_eq(StringName(migrated.simulation["location_id"]), SimulationIds.REGIONAL_LOCATION_JAJCE) var npc: SimNPC = manager.npcs[0] manager.record_narrative_event( SimulationIds.EVENT_TASK_BLOCKED, npc.id, SimulationIds.npc_inventory_id(npc.id), "Feed animal: needs 1 food", SimulationIds.ACTION_FEED_ANIMAL, SimulationIds.RESOURCE_FOOD, 1.0 ) var previous_with_blocked_fact: Dictionary = manager.create_state_record().to_dictionary() previous_with_blocked_fact["schema_version"] = SimulationStateRecord.EMERGENT_SCHEMA_VERSION previous_with_blocked_fact["simulation"].erase("world_id") previous_with_blocked_fact["simulation"].erase("location_id") for field in ["blocked_contract_kind", "world_id", "location_id"]: previous_with_blocked_fact["economic_events"][0].erase(field) assert_not_null( SimulationStateRecord.from_dictionary(previous_with_blocked_fact), "A legacy compound-action inventory failure must remain loadable under v16" ) func test_legacy_unstructured_blocked_fact_is_preserved_but_not_made_knowable() -> void: var legacy_state: Dictionary = manager.create_state_record().to_dictionary() legacy_state["schema_version"] = SimulationStateRecord.PRE_EMERGENT_SCHEMA_VERSION var legacy_event: Dictionary = ( EconomicEventRecord . create_narrative( 0, SimulationIds.EVENT_TASK_BLOCKED, 0, manager.npcs[0].id, &"legacy_unknown_source", "Legacy work was blocked" ) . to_dictionary() ) legacy_event["schema_version"] = EconomicEventRecord.POSITION_LEGACY_SCHEMA_VERSION legacy_event.erase("action_id") legacy_event.erase("required_amount") legacy_state["economic_events"] = [legacy_event] legacy_state["simulation"]["next_event_id"] = 1 var migrated := SimulationStateRecord.from_dictionary(legacy_state) assert_not_null(migrated) var preserved: EconomicEventRecord = migrated.economic_events[0] assert_true(ActivityEventValidator.is_legacy_unstructured_blocked(preserved)) assert_false(EventKnowledgeSystem.is_knowable_event(preserved)) assert_not_null( SimulationStateRecord.from_dictionary(migrated.to_dictionary()), "A grandfathered fact must survive its next current-schema save" ) func test_orphan_journal_and_situation_evidence_are_rejected() -> void: var orphan_journal: Dictionary = manager.create_state_record().to_dictionary() orphan_journal["quest_journal"] = [ ( QuestJournalEntryStateRecord . create(0, 42, SituationSystem.DEFINITION_PANTRY_SHORTAGE, 0) . to_dictionary() ) ] orphan_journal["simulation"]["next_journal_entry_id"] = 1 assert_null(SimulationStateRecord.from_dictionary(orphan_journal)) var missing_evidence: Dictionary = manager.create_state_record().to_dictionary() missing_evidence["situations"] = [ ( SituationStateRecord . create( 0, SituationSystem.DEFINITION_PANTRY_SHORTAGE, "missing-evidence", 0, 999, {"interested_npc_id": "0"} ) . to_dictionary() ) ] missing_evidence["simulation"]["next_situation_id"] = 1 assert_null(SimulationStateRecord.from_dictionary(missing_evidence)) func test_conversation_history_requires_valid_actors_and_monotonic_id() -> void: var state: Dictionary = manager.create_state_record().to_dictionary() var npc_ref := WorldEntityRef.create(WorldEventRecord.ENTITY_TYPE_NPC, &"0") var player_ref := WorldEntityRef.create(WorldEventRecord.ENTITY_TYPE_PLAYER, &"-1") state["conversation_history"] = [ ( ConversationActStateRecord . create( 0, 0, npc_ref, player_ref, ConversationIntentIds.GREET, [&"topic_pantry_shortage"] ) . to_dictionary() ) ] assert_null( SimulationStateRecord.from_dictionary(state), "The next stable act ID cannot collide with retained history" ) state["simulation"]["next_conversation_act_id"] = 1 assert_not_null(SimulationStateRecord.from_dictionary(state)) state["conversation_history"][0]["speaker"]["entity_id"] = "missing_npc" assert_null(SimulationStateRecord.from_dictionary(state)) func test_activity_facts_validate_actor_target_effect_and_exact_cost_without_knowledge() -> void: var npc: SimNPC = manager.npcs[0] _assign_activity(npc, SimulationIds.ACTION_STUDY, SimulationIds.ACTIVITY_STUDY_DESK) var result: ActionResult = manager.execute_activity_action( WorldEntityRef.create(SimulationIds.ENTITY_PERSON, StringName(str(npc.id))), SimulationIds.ACTION_STUDY, SimulationIds.ACTIVITY_STUDY_DESK, manager.get_action_state_revision() ) assert_true(result.did_succeed()) var state: Dictionary = manager.create_state_record().to_dictionary() state["event_knowledge"] = [] assert_not_null(SimulationStateRecord.from_dictionary(state)) var invalid_actor := state.duplicate(true) invalid_actor["economic_events"][0]["actor_id"] = 999 assert_null(SimulationStateRecord.from_dictionary(invalid_actor)) var invalid_target := state.duplicate(true) invalid_target["economic_events"][0]["target_kind"] = "storage" assert_null(SimulationStateRecord.from_dictionary(invalid_target)) var invalid_metric := state.duplicate(true) invalid_metric["economic_events"][0]["metric_id"] = "safety" assert_null(SimulationStateRecord.from_dictionary(invalid_metric)) var invalid_cost := state.duplicate(true) invalid_cost["economic_events"][0]["cost_amount"] = 2.0 assert_null(SimulationStateRecord.from_dictionary(invalid_cost)) var invalid_scope := state.duplicate(true) invalid_scope["economic_events"][0]["location_id"] = "location_elsewhere" assert_null(SimulationStateRecord.from_dictionary(invalid_scope)) var impossible_delta := state.duplicate(true) impossible_delta["economic_events"][0]["metric_delta"] = 99.0 assert_null(SimulationStateRecord.from_dictionary(impossible_delta)) func test_capped_activity_never_consumes_cost_or_claims_a_false_delta() -> void: manager.village.safety = 100.0 manager.village.update_modifiers() manager.village.update_priorities() var woodpile: StorageStateRecord = manager.get_woodpile() var wood_before := woodpile.get_amount(SimulationIds.RESOURCE_WOOD) var events_before: int = manager.economic_events.size() var npc: SimNPC = manager.npcs[0] _assign_activity(npc, SimulationIds.ACTION_PATROL, SimulationIds.ACTIVITY_GUARD_POST) var capped: ActionResult = manager.execute_activity_action( WorldEntityRef.create(SimulationIds.ENTITY_PERSON, StringName(str(npc.id))), SimulationIds.ACTION_PATROL, SimulationIds.ACTIVITY_GUARD_POST, manager.get_action_state_revision() ) assert_false(capped.did_succeed()) assert_eq(capped.get_reason_code(), &"no_effect") assert_eq(woodpile.get_amount(SimulationIds.RESOURCE_WOOD), wood_before) assert_eq(manager.economic_events.size(), events_before) manager.village.safety = 99.0 var partial: ActionResult = manager.execute_activity_action( WorldEntityRef.create(SimulationIds.ENTITY_PERSON, StringName(str(npc.id))), SimulationIds.ACTION_PATROL, SimulationIds.ACTIVITY_GUARD_POST, manager.get_action_state_revision() ) assert_true(partial.did_succeed()) assert_eq(manager.village.safety, 100.0) assert_eq(float(partial.get_payload()["amount"]), 1.0) assert_eq(float(manager.economic_events.back().data["metric_delta"]), 1.0) func test_rejected_activity_exposes_its_exact_failure_fact_and_revision() -> void: var woodpile: StorageStateRecord = manager.get_woodpile() var available := woodpile.get_amount(SimulationIds.RESOURCE_WOOD) assert_eq(woodpile.withdraw(SimulationIds.RESOURCE_WOOD, available), available) manager.economy.sync_resource(SimulationIds.RESOURCE_WOOD) var revision_before: int = manager.get_action_state_revision() var npc: SimNPC = manager.npcs[0] _assign_activity(npc, SimulationIds.ACTION_PATROL, SimulationIds.ACTIVITY_GUARD_POST) var result: ActionResult = manager.execute_activity_action( WorldEntityRef.create(SimulationIds.ENTITY_PERSON, StringName(str(npc.id))), SimulationIds.ACTION_PATROL, SimulationIds.ACTIVITY_GUARD_POST, revision_before ) assert_false(result.did_succeed()) assert_eq(result.get_reason_code(), &"insufficient_cost") var payload := result.get_payload() assert_eq(int(payload["event_id"]), revision_before) assert_eq(int(payload["state_revision"]), revision_before + 1) assert_has(payload["reason_trace"], "authoritative_failure_fact_recorded") var failure_fact: EconomicEventRecord = manager.event_log.get_by_id(int(payload["event_id"])) assert_not_null(failure_fact) assert_eq(StringName(failure_fact.data["event_type"]), SimulationIds.EVENT_TASK_BLOCKED) assert_eq(StringName(failure_fact.data["action_id"]), SimulationIds.ACTION_PATROL) assert_eq(int(failure_fact.data["actor_id"]), npc.id) var state: Dictionary = manager.create_state_record().to_dictionary() assert_not_null( SimulationStateRecord.from_dictionary(state), "Original blocked state must parse" ) state["event_knowledge"] = [] state["opportunities"] = [] state["situations"] = [] state["quest_journal"] = [] state["commitments"] = [] var npc_ids := {} for saved_npc in state["npcs"]: npc_ids[int(saved_npc["id"])] = true var storage_records := {} for saved_storage in state["storages"]: var storage := StorageStateRecord.from_dictionary(saved_storage) storage_records[storage.get_storage_id()] = storage assert_true(ActivityEventValidator.is_valid_blocked(failure_fact)) assert_true( ActivityEventValidator.is_valid_blocked_for_state( failure_fact, npc_ids, storage_records, int(state["simulation"]["tick_count"]), StringName(state["simulation"]["world_id"]), StringName(state["simulation"]["location_id"]) ) ) assert_not_null(SimulationStateRecord.from_dictionary(state)) for mutation in [ ["actor_id", 999], ["action_id", String(SimulationIds.ACTION_STUDY)], ["source_id", String(SimulationIds.STORAGE_VILLAGE_PANTRY)], ["item_id", String(SimulationIds.RESOURCE_FOOD)], ["required_amount", 2.0], ["world_id", "wrong_world"], ["location_id", "wrong_location"], ]: var tampered: Dictionary = state.duplicate(true) tampered["economic_events"][0][mutation[0]] = mutation[1] assert_null( SimulationStateRecord.from_dictionary(tampered), "Tampered task_blocked field '%s' must be rejected" % mutation[0] ) func test_unloaded_activity_requires_a_registered_capability_target() -> void: var npc: SimNPC = manager.npcs[0] var actor := WorldEntityRef.create(SimulationIds.ENTITY_PERSON, StringName(str(npc.id))) _assign_activity(npc, SimulationIds.ACTION_STUDY, &"invented_library") var woodpile: StorageStateRecord = manager.get_woodpile() var wood_before := woodpile.get_amount(SimulationIds.RESOURCE_WOOD) var rejected: ActionResult = manager.execute_activity_action( actor, SimulationIds.ACTION_STUDY, &"invented_library", manager.get_action_state_revision() ) assert_false(rejected.did_succeed()) assert_eq(woodpile.get_amount(SimulationIds.RESOURCE_WOOD), wood_before) assert_true(manager.economic_events.is_empty()) assert_true( manager.register_abstract_activity_target( &"library_annex", SimulationIds.ACTION_STUDY, npc.position ) ) npc.target_id = &"library_annex" var completed: ActionResult = manager.execute_activity_action( actor, SimulationIds.ACTION_STUDY, &"library_annex", manager.get_action_state_revision() ) assert_true(completed.did_succeed()) assert_eq(StringName(manager.economic_events.back().data["source_id"]), &"library_annex") assert_not_null( SimulationStateRecord.from_dictionary(manager.create_state_record().to_dictionary()) ) func test_dead_npc_cannot_complete_an_abstract_activity() -> void: var npc: SimNPC = manager.npcs[0] npc.die_from_combat() var actor := WorldEntityRef.create(SimulationIds.ENTITY_PERSON, StringName(str(npc.id))) var woodpile: StorageStateRecord = manager.get_woodpile() var wood_before := woodpile.get_amount(SimulationIds.RESOURCE_WOOD) var safety_before: float = manager.village.safety var events_before: int = manager.economic_events.size() var result: ActionResult = manager.execute_activity_action( actor, SimulationIds.ACTION_PATROL, SimulationIds.ACTIVITY_GUARD_POST, manager.get_action_state_revision() ) assert_false(result.did_succeed()) assert_eq(result.get_reason_code(), ActivityActionCommandService.REASON_UNSUPPORTED_ACTOR) assert_eq(woodpile.get_amount(SimulationIds.RESOURCE_WOOD), wood_before) assert_eq(manager.village.safety, safety_before) assert_eq(manager.economic_events.size(), events_before) func test_npc_activity_requires_the_exact_authoritative_work_assignment() -> void: var npc: SimNPC = manager.npcs[0] var actor := WorldEntityRef.create(SimulationIds.ENTITY_PERSON, StringName(str(npc.id))) var woodpile: StorageStateRecord = manager.get_woodpile() var wood_before := woodpile.get_amount(SimulationIds.RESOURCE_WOOD) var safety_before: float = manager.village.safety var events_before: int = manager.economic_events.size() var idle_result: ActionResult = manager.execute_activity_action( actor, SimulationIds.ACTION_PATROL, SimulationIds.ACTIVITY_GUARD_POST, manager.get_action_state_revision() ) assert_false(idle_result.did_succeed()) var omitted_revision: ActionResult = manager.execute_activity_action( actor, SimulationIds.ACTION_PATROL, SimulationIds.ACTIVITY_GUARD_POST ) assert_false(omitted_revision.did_succeed()) var player_bypass: ActionResult = manager.execute_activity_action( WorldEntityRef.create( SimulationIds.ENTITY_PLAYER, StringName(str(SimulationIds.PLAYER_ACTOR_ID)) ), SimulationIds.ACTION_PATROL, SimulationIds.ACTIVITY_GUARD_POST, manager.get_action_state_revision() ) assert_false(player_bypass.did_succeed()) npc.set_task(SimulationIds.ACTION_STUDY) npc.target_id = SimulationIds.ACTIVITY_STUDY_DESK npc.travel_target_position = npc.position npc.has_travel_target = false npc.start_working() var early_result: ActionResult = manager.execute_activity_action( actor, SimulationIds.ACTION_STUDY, SimulationIds.ACTIVITY_STUDY_DESK, manager.get_action_state_revision() ) assert_false(early_result.did_succeed()) npc.task_progress = npc.task_duration npc.task_complete = true npc.task_state = SimNPC.TASK_STATE_COMPLETE var wrong_task: ActionResult = manager.execute_activity_action( actor, SimulationIds.ACTION_PATROL, SimulationIds.ACTIVITY_GUARD_POST, manager.get_action_state_revision() ) assert_false(wrong_task.did_succeed()) npc.target_id = SimulationIds.ACTIVITY_GUARD_POST var wrong_target: ActionResult = manager.execute_activity_action( actor, SimulationIds.ACTION_STUDY, SimulationIds.ACTIVITY_STUDY_DESK, manager.get_action_state_revision() ) assert_false(wrong_target.did_succeed()) assert_eq(woodpile.get_amount(SimulationIds.RESOURCE_WOOD), wood_before) assert_eq(manager.village.safety, safety_before) assert_eq(manager.economic_events.size(), events_before) func test_custom_activity_scope_round_trips_and_rebuilds_the_world_event_index() -> void: var adapter := ActiveWorldAdapter.new() assert_true( adapter.configure_context_identity(&"test_context", &"test_world", &"test_location") ) add_child_autofree(adapter) var scoped_manager := MANAGER_SCRIPT.new() scoped_manager.active_world_adapter = adapter add_child_autofree(scoped_manager) var npc: SimNPC = scoped_manager.npcs[0] _assign_activity(npc, SimulationIds.ACTION_STUDY, SimulationIds.ACTIVITY_STUDY_DESK) var result: ActionResult = scoped_manager.execute_activity_action( WorldEntityRef.create(SimulationIds.ENTITY_PERSON, StringName(str(npc.id))), SimulationIds.ACTION_STUDY, SimulationIds.ACTIVITY_STUDY_DESK, scoped_manager.get_action_state_revision() ) assert_true(result.did_succeed()) var snapshot := scoped_manager.serialize_state() var checksum := scoped_manager.get_state_checksum() var parsed := SimulationStateRecord.from_json(snapshot) assert_not_null(parsed) assert_eq(StringName(parsed.simulation["world_id"]), &"test_world") assert_eq(StringName(parsed.simulation["location_id"]), &"test_location") assert_true(scoped_manager.restore_state_from_json(snapshot)) assert_eq(scoped_manager.get_state_checksum(), checksum) var location := SpatialAddress.create(&"test_world", &"test_location", Vector3.ZERO) assert_eq(scoped_manager.event_log.world_events.get_for_location(location).size(), 1) func test_restore_rejects_adapter_scope_mismatch_before_mutating_empty_history() -> void: var source_adapter := ActiveWorldAdapter.new() assert_true(source_adapter.configure_context_identity(&"source", &"world_a", &"location_a")) add_child_autofree(source_adapter) var source_manager := MANAGER_SCRIPT.new() source_manager.active_world_adapter = source_adapter add_child_autofree(source_manager) var source_record: SimulationStateRecord = source_manager.create_state_record() assert_true(source_record.economic_events.is_empty()) var target_adapter := ActiveWorldAdapter.new() assert_true(target_adapter.configure_context_identity(&"target", &"world_b", &"location_b")) add_child_autofree(target_adapter) var target_manager := MANAGER_SCRIPT.new() target_manager.active_world_adapter = target_adapter add_child_autofree(target_manager) var checksum_before: String = target_manager.get_state_checksum() var scope_before: Dictionary = target_manager.event_log.get_scope() assert_false(target_manager.restore_state(source_record)) assert_eq(target_manager.get_state_checksum(), checksum_before) assert_eq(target_manager.event_log.get_scope(), scope_before) source_manager.record_narrative_event( SimulationIds.EVENT_TASK_STARTED, source_manager.npcs[0].id, &"", "Started scoped work" ) var populated_source_record: SimulationStateRecord = source_manager.create_state_record() assert_false(populated_source_record.economic_events.is_empty()) assert_false(target_manager.restore_state(populated_source_record)) assert_eq(target_manager.get_state_checksum(), checksum_before) assert_eq(target_manager.event_log.get_scope(), scope_before) func _assign_activity(npc: SimNPC, action_id: StringName, target_id: StringName) -> void: npc.set_task(action_id) npc.target_id = target_id npc.travel_target_position = npc.position npc.has_travel_target = false npc.start_working() npc.task_progress = npc.task_duration npc.task_complete = true npc.task_state = SimNPC.TASK_STATE_COMPLETE