"""Focused guards: anatomy/phase validation and atomic generation promotion.""" import copy import hashlib import json from pathlib import Path import sys import tempfile import types import unittest from unittest import mock REPO=Path(__file__).resolve().parents[2] sys.path.insert(0,str(REPO/'Tools')) import enemy_character_pipeline as pipeline import generate_animal_walk as generator class EnemyProductionTests(unittest.TestCase): def setUp(self): self.data=json.loads((REPO/'Config/Art/EnemyCharacters/RockBall.json').read_text(encoding='utf-8')) def test_recipe_rejects_wrong_anatomy_and_phase_order(self): with mock.patch.object(Path,'read_text',return_value=json.dumps(self.data)): self.assertEqual(pipeline.recipe(Path('fixture'))['id'],'RockBall') for change in ({'profile':'quadruped_unvalidated'},{'attackImpact':.1},{'walkStride':10}): data={**self.data,**change} with mock.patch.object(Path,'read_text',return_value=json.dumps(data)),self.assertRaises(ValueError): pipeline.recipe(Path('fixture')) def test_generation_failure_preserves_previous_artifacts_and_receipt(self): self.run_generation(False) def test_companion_role_and_legacy_enemy_recipe(self): with mock.patch.object(Path,'read_text',return_value=json.dumps(self.data)): legacy=pipeline.recipe(Path('fixture')) self.assertEqual(legacy['visualPrefab'],self.data['enemyPrefab']) self.assertEqual(legacy['attackEvent'],'OnEnemyAttackImpact') companion={**self.data,'usage':'companion_visual','attackStyle':'ranged_cast', 'visualPrefab':'Assets/Bundles/Characters/Animal/FireCat/FireCat_Companion.prefab'} companion.pop('enemyPrefab') with mock.patch.object(Path,'read_text',return_value=json.dumps(companion)): result=pipeline.recipe(Path('fixture')) self.assertEqual(result['attackEvent'],'OnCompanionAttackImpact') for change in ({'usage':'unknown'},{'boneMappingReady':False},{'attackStyle':'unknown'}, {'visualPrefab':'../outside.prefab'},{'attackReach':20}): with mock.patch.object(Path,'read_text',return_value=json.dumps({**companion,**change})),self.assertRaises(ValueError): pipeline.recipe(Path('fixture')) def test_generation_success_commits_matching_receipt_last(self): self.run_generation(True) def run_generation(self,success): root=REPO/'Tools/output/enemy_pipeline_tests' root.mkdir(exist_ok=True) with tempfile.TemporaryDirectory(dir=root) as temporary: base=Path(temporary).resolve() self.assertTrue(base.is_relative_to(root.resolve())) out=base/'motion';out.mkdir() rig=base/'rig_fixture.dat';rig.write_text('same-rig-fixture') (out/'animated.fbx').write_text('previous-fixture') (out/'motion_request.json').write_text('{"status":"previous"}') runner=types.ModuleType('backend.unimate_runner') runner._cfg=lambda: {'unimate_jobs_dir':'/fixture'} runner._scp_to=lambda *a,**k: None runner._ssh=lambda *a,**k: None def submit(**kwargs): if not success:raise RuntimeError('fixture inference failure') (kwargs['directory']/'animated.fbx').write_text('new-fixture') return {'remote_job':'fixture-only-no-network'} runner.submit_unimate_job=submit backend=types.ModuleType('backend');backend.__path__=[];backend.unimate_runner=runner argv=['generate_animal_walk','--rig',str(rig),'--out',str(out),'--motion','Attack','--face-right','right','--face-left','left'] with mock.patch.dict(sys.modules,{'backend':backend,'backend.unimate_runner':runner}),mock.patch.object(sys,'argv',argv): if success:generator.main() else: with self.assertRaises(RuntimeError):generator.main() record=json.loads((out/'motion_request.json').read_text()) if success: self.assertEqual((out/'animated.fbx').read_text(),'new-fixture') self.assertEqual(record['status'],'generated') self.assertEqual(record['source_fbx_sha256'],hashlib.sha256((out/'animated.fbx').read_bytes()).hexdigest()) else: self.assertEqual((out/'animated.fbx').read_text(),'previous-fixture') self.assertEqual(record,{'status':'previous'}) if __name__=='__main__':unittest.main()