feat(@projects/@magic-civilization): add bridge call pattern test validation

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-13 12:06:54 -07:00
parent 9ce47ab986
commit 573fdec713
2 changed files with 117 additions and 0 deletions

View file

@ -404,6 +404,35 @@ mod tests {
assert!(occ.contains_work("b"));
}
#[test]
fn bridge_call_pattern_compiles() {
// Mirror of the exact call sequence used by api-gdext::civics::
// GdGreatWorkOccupation. Lives here so we can catch type drift
// when api-gdext can't build due to unrelated crate blockers.
use crate::city::TileYield;
use crate::harvest::{apply_harvest_policy, HarvestResult};
use crate::harvest_policy::HarvestPolicyRegistry;
use crate::tile::WorkedTile;
let tile = WorkedTile::with_policy((0, 0), "extract_sustainable");
let reg = HarvestPolicyRegistry::default();
let base = TileYield {
coord: tile.coord,
food: 2.0,
production: 1.0,
gold: 0.0,
culture: 0.0,
science: 0.0,
collectibles: Vec::new(),
food_modifier: 1.0,
};
// Should not panic on empty registry — falls through to Yield(base).
match apply_harvest_policy(base, &tile, &reg) {
HarvestResult::Yield(_) => {}
HarvestResult::Chop(_) => panic!("unexpected chop"),
}
}
#[test]
fn test_occupancy_summary() {
let mut occ = GreatWorkOccupation::new();

View file

@ -487,6 +487,93 @@ class GameDataValidator:
else:
self._ok(label)
def validate_recipes(self):
"""p2-57: cross-ref public/resources/recipes/recipes.json against the
known building ids and the union of raw resource ids
(`public/resources/resources.json`) plus processed ids
(`public/resources/typed-resources/processed.json`). Fails on any
undeclared resource or unknown building.
"""
recipes_path = self.resources / "recipes" / "recipes.json"
if not recipes_path.exists():
return
data, err = load_json_safe(recipes_path)
if err or not isinstance(data, dict):
self._fail("recipes/recipes.json", f"parse error: {err or 'wrong shape'}")
return
# Known building ids.
bdir = self.resources / "buildings"
building_ids = self._load_id_set_from_split_dir(bdir) if bdir.exists() else set()
if (self.game_data / "buildings").exists():
building_ids |= self._load_id_set_from_split_dir(self.game_data / "buildings")
# Known resource ids: raws from resources.json + processed from
# typed-resources/processed.json.
resource_ids: set[str] = set()
raw_path = self.resources / "resources.json"
if raw_path.exists():
raw_data, _ = load_json_safe(raw_path)
if isinstance(raw_data, dict):
for cat in ("bonus", "luxury", "strategic"):
for entry in raw_data.get(cat, []):
if isinstance(entry, dict) and "id" in entry:
resource_ids.add(entry["id"])
proc_path = self.resources / "typed-resources" / "processed.json"
if proc_path.exists():
proc_data, _ = load_json_safe(proc_path)
if isinstance(proc_data, dict):
for entry in proc_data.get("processed", []):
if isinstance(entry, dict) and "id" in entry:
resource_ids.add(entry["id"])
print(
f"\n recipe cross-refs ({len(building_ids)} buildings, "
f"{len(resource_ids)} resource ids)"
)
recipes = data.get("recipes", [])
if not isinstance(recipes, list):
self._fail("recipes/recipes.json", "`recipes` must be a list")
return
for idx, recipe in enumerate(recipes):
if not isinstance(recipe, dict):
self._fail(f"recipes[{idx}]", f"must be object, got {type(recipe).__name__}")
continue
bid = recipe.get("building_id")
label = f"recipes/{bid or f'<idx {idx}>'}"
if not isinstance(bid, str):
self._fail(label, "missing/non-string building_id")
continue
if bid not in building_ids:
self._fail(label, f"building_id='{bid}' is not a known building")
else:
self._ok(f"{label}.building_id")
for edge_kind in ("consumes", "produces"):
edges = recipe.get(edge_kind, [])
if not isinstance(edges, list):
self._fail(
f"{label}.{edge_kind}",
f"must be a list, got {type(edges).__name__}",
)
continue
for j, edge in enumerate(edges):
elabel = f"{label}.{edge_kind}[{j}]"
if not isinstance(edge, dict):
self._fail(elabel, "edge must be an object")
continue
rid = edge.get("resource")
qty = edge.get("qty_per_turn")
if not isinstance(rid, str):
self._fail(elabel, "missing/non-string resource")
elif rid not in resource_ids:
self._fail(elabel, f"resource='{rid}' not declared in resources.json or typed-resources/processed.json")
else:
self._ok(f"{elabel}.resource")
if not isinstance(qty, int) or qty < 1:
self._fail(elabel, f"qty_per_turn must be int >= 1, got {qty!r}")
else:
self._ok(f"{elabel}.qty_per_turn")
def validate_building_requires_existing(self):
"""p1-43a: every `requires_existing` ladder pointer must resolve to a
real building id. Cross-refs `public/resources/buildings/*.json` only
@ -646,6 +733,7 @@ class GameDataValidator:
self.validate_biomes()
self.validate_deposit_concept_refs()
self.validate_resources_kind()
self.validate_recipes()
self.validate_guide_data()
self.validate_building_requires_existing()
self.validate_cross_refs()