import json from pathlib import Path import shutil from typer.testing import CliRunner from spriteforge.backend_registry import register_backend_contract from spriteforge.backends.base import Backend,BackendContext,RawAsset from spriteforge.backends.registry import register_backend from spriteforge.cache import input_hash from spriteforge.cli import app from spriteforge.manifest import AssetEntry,AssetSpec runner=CliRunner() def manifest_file(tmp_path): path=tmp_path/"assets.yaml" path.write_text("""\ a_floor: backend: procedural generator: floor_tile params: {width: 24, height: 12, color: '#605040'} b_decal: backend: procedural generator: decal params: {width: 20, height: 10, color: '#904030'} """) return path def build(path,output,cache,*extra): return runner.invoke(app,["build",str(path),"--output",str(output),"--cache-dir",str(cache),*extra]) def test_second_build_hits_artifact_cache(tmp_path): path=manifest_file(tmp_path);output=tmp_path/"out";cache=tmp_path/"cache" first=build(path,output,cache,"--jobs","2") assert first.exit_code==0 and "generated=2" in first.stdout second=build(path,output,cache,"--jobs","2") assert second.exit_code==0 and "artifact=2" in second.stdout and "generated=0" in second.stdout def test_raw_cache_survives_artifact_eviction(tmp_path): path=manifest_file(tmp_path);output=tmp_path/"out";cache=tmp_path/"cache" assert build(path,output,cache).exit_code==0 shutil.rmtree(cache/"artifacts") repeated=build(path,output,cache) assert repeated.exit_code==0 and "raw=2" in repeated.stdout and "artifact=0" in repeated.stdout def test_metadata_tags_do_not_invalidate_render_or_artifact(tmp_path): path=manifest_file(tmp_path);output=tmp_path/"out";cache=tmp_path/"cache" assert build(path,output,cache).exit_code==0 path.write_text(path.read_text().replace("a_floor:\n", "a_floor:\n tags: [environment]\n")) repeated=build(path,output,cache) assert repeated.exit_code==0 and "generated=0" in repeated.stdout and "artifact=2" in repeated.stdout index=json.loads((output/"index.json").read_text()) assert next(item for item in index["assets"] if item["id"]=="a_floor")["tags"]==["environment"] def test_one_asset_change_preserves_other_and_palette(tmp_path): path=manifest_file(tmp_path);output=tmp_path/"out";cache=tmp_path/"cache" assert build(path,output,cache).exit_code==0 old=json.loads((output/"index.json").read_text());old_items={x["id"]:x for x in old["assets"]};palette=old["palette"]["hash"] path.write_text(path.read_text().replace("#605040","#405870")) changed=build(path,output,cache,"--asset","a_floor") assert changed.exit_code==0 and "generated=1" in changed.stdout new=json.loads((output/"index.json").read_text());new_items={x["id"]:x for x in new["assets"]} assert new_items["b_decal"]==old_items["b_decal"] assert new_items["a_floor"]["hash"]!=old_items["a_floor"]["hash"] assert new["palette"]["hash"]==palette class DependencyBackend(Backend): name="dependency_test";version="1" def dependencies(self,spec,context):return (Path(spec.params["path"]),) def generate(self,spec,context)->RawAsset:raise AssertionError("not used") def test_dependency_bytes_invalidate_input_hash(tmp_path): register_backend_contract("dependency_test",lambda spec:None,replace=True) register_backend("dependency_test",DependencyBackend,replace=True) source=tmp_path/"source.bin";source.write_bytes(b"one") spec=AssetSpec(backend="dependency_test",params={"path":str(source)}) entry=AssetEntry("thing",spec,tmp_path/"assets.yaml",1) before=input_hash(entry)[0];source.write_bytes(b"two");after=input_hash(entry)[0] assert before!=after def test_filtered_palette_rebuild_is_rejected(tmp_path): path=manifest_file(tmp_path) result=build(path,tmp_path/"out",tmp_path/"cache","--asset","a_floor","--rebuild-palette") assert result.exit_code==2 and "unfiltered full build" in result.stderr