From ef25e2cf8b85fd3484d2336791ab3a67c4954d8b Mon Sep 17 00:00:00 2001 From: Natalie Date: Tue, 9 Jun 2026 20:44:17 -0700 Subject: [PATCH] =?UTF-8?q?feat(api):=20=E2=9C=A8=20add=20round-trip=20ser?= =?UTF-8?q?ialization=20tests=20for=20city=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- src/simulator/api-gdext/src/city_slot.rs | 75 ++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/simulator/api-gdext/src/city_slot.rs b/src/simulator/api-gdext/src/city_slot.rs index 618fbc7d..01889b43 100644 --- a/src/simulator/api-gdext/src/city_slot.rs +++ b/src/simulator/api-gdext/src/city_slot.rs @@ -1040,3 +1040,78 @@ mod load_items_tests { assert_eq!(cost_of(&defs, "axe"), Some(10)); } } + +#[cfg(test)] +mod save_round_trip_tests { + use super::*; + use mc_city::{BuildingQueue, CityYields, Queueable, QueueEntry, CITY_CENTER_QUEUE_ID}; + + /// Punch-list item 4: a city carrying non-default `building_yields`, a + /// non-empty per-building production queue, and both `captured_turn` / + /// `last_attacked_turn` set must survive the gdext save path + /// (`to_json` → `load_from_json` → `to_json`) byte-identically. + /// + /// Byte-identity is asserted on the *re-serialized* form (json1 == json2) + /// rather than on the original input string. `building_yields` is a + /// `HashMap`, whose serialization order is only stable when serializing a + /// single map instance; a fresh deserialize could reorder a multi-entry + /// map. A single `building_yields` entry keeps the comparison deterministic + /// while still exercising the non-default-map path. `queues` is a + /// `BTreeMap`, so multi-entry queue state is order-stable regardless. + #[test] + fn city_with_yields_queue_and_capture_turns_round_trips_byte_identical() { + let mut city = City::new("city_smithgate"); + city.city_name = "Smithgate".to_string(); + city.population = 4; + city.food_stored = 12.5; + city.production_progress = 7.0; + + // Non-default building_yields (single entry → deterministic serde order). + city.register_building_yields( + "smithy", + CityYields { food: 0.0, production: 3.0, gold: 1.0, culture: 0.0, science: 0.5 }, + ); + + // Non-empty per-building production queue (BTreeMap → order-stable). + let mut q = BuildingQueue::new(); + q.push(QueueEntry { + queueable: Queueable::Item { item_id: "bronze_sword".to_string() }, + production_cost: 40, + production_invested: 12, + origin: mc_core::ProductionOrigin::default(), + }); + city.queues_mut().insert(CITY_CENTER_QUEUE_ID.to_string(), q); + + // Both occupation/siege timestamps set. + city.mark_captured(7); + city.mark_attacked(9); + + let slot: Vec> = vec![vec![city]]; + + let json1 = to_json(&slot, 0, 0); + assert!(json1.contains("smithy"), "building_yields must serialize"); + assert!(json1.contains("bronze_sword"), "queue entry must serialize"); + assert!(json1.contains("\"captured_turn\":7"), "captured_turn must serialize"); + assert!(json1.contains("\"last_attacked_turn\":9"), "last_attacked_turn must serialize"); + + // Restore into a fresh slot via the real load path, then re-serialize. + let mut slot2: Vec> = vec![vec![City::new("placeholder")]]; + assert!(load_from_json(&mut slot2, 0, 0, &json1), "load_from_json must succeed"); + let json2 = to_json(&slot2, 0, 0); + + assert_eq!(json1, json2, "city save round-trip must be byte-identical"); + + // Field-level proof the restored city kept the four target fields. + let restored = at(&slot2, 0, 0).expect("restored city present"); + assert_eq!(restored.captured_turn, Some(7)); + assert_eq!(restored.last_attacked_turn, Some(9)); + assert_eq!(restored.queues().get(CITY_CENTER_QUEUE_ID).map(|q| q.len()), Some(1)); + assert!( + restored.queues().get(CITY_CENTER_QUEUE_ID) + .and_then(|q| q.entries().first()) + .map(|e| e.production_invested == 12) + .unwrap_or(false), + "queued entry progress must survive" + ); + } +}