"""Tests for app.services.chunk_codec -- the Python port of world.md §3.6. Pure unit tests: no database, no app, no network. Golden fixtures live in tests/fixtures/chunks/ (see generate_fixtures.py there for how/why). """ from pathlib import Path import pytest from app.services import chunk_codec as cc FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures" / "chunks" def _read_fixture(name: str) -> bytes: return (FIXTURES_DIR / name).read_bytes() class TestGoldenFixtures: """Byte-stability: decode a committed fixture, re-encode it, and the bytes must match exactly (world.md §3.6.3).""" @pytest.mark.parametrize("filename", [ "empty_chunk.bin", "simple_tile_chunk.bin", "chunk_with_objects.bin", ]) def test_round_trip_is_byte_identical(self, filename): original = _read_fixture(filename) decoded = cc.decode_binary(original) re_encoded = cc.encode_binary(decoded) assert re_encoded == original def test_simple_tile_chunk_logical_values(self): decoded = cc.decode_binary(_read_fixture("simple_tile_chunk.bin")) assert decoded.world_id == "12" assert decoded.room_id == "main" assert decoded.coord == (3, -1, 0) assert decoded.bounds.min == (96.0, -32.0, 0.0) assert decoded.bounds.size == (32.0, 32.0, 1.0) assert len(decoded.tile_layers) == 1 layer = decoded.tile_layers[0] assert layer.name == "ground" assert (layer.width, layer.height, layer.depth) == (4, 4, 1) assert len(layer.palette) == 3 assert layer.tiles == [0, 1, 1, 0, 1, 2, 2, 1, 1, 2, 2, 1, 0, 1, 1, 0] assert decoded.revision == 2 assert decoded.author == "fixture-gen" def test_chunk_with_objects_logical_values(self): decoded = cc.decode_binary(_read_fixture("chunk_with_objects.bin")) assert len(decoded.objects) == 2 obj1, obj2 = decoded.objects assert obj1.id == "obj_0001" assert obj1.type == "interactable" assert obj1.pos == (101.5, -110.25, 0.5) assert obj1.properties == {"locked": False, "loot_table": "chest_basic"} assert obj2.id == "obj_0002" assert obj2.dynamic is True assert obj2.flags.violence == 1 assert obj2.flags.sources == ["asset:props/chest"] assert decoded.flags.violence == 1 assert decoded.flags.sources == ["asset:props/chest"] def test_empty_chunk_has_no_layers_or_objects(self): decoded = cc.decode_binary(_read_fixture("empty_chunk.bin")) assert decoded.tile_layers == [] assert decoded.objects == [] class TestChecksum: def test_checksum_is_sha256_hex(self): import hashlib assert cc.checksum(b"") == hashlib.sha256(b"").hexdigest() assert len(cc.checksum(b"")) == 64 def test_header_checksum_matches_recomputed_for_valid_fixture(self): data = _read_fixture("simple_tile_chunk.bin") header_checksum_hex = data[16:48].hex() body = data[cc.HEADER_SIZE:] assert header_checksum_hex == cc.checksum(body) class TestBuildKey: def test_matches_world_persistence_impl_4_5_format(self): assert cc.build_key(12, "main", 3, -1, 0) == "12:main:3:-1:0" assert cc.build_key("12", "main", 3, -1, 0) == "12:main:3:-1:0" class TestFloat32Fidelity: """This is the concrete regression test for the cross-codec divergence documented at the bottom of chunk_codec.py: this module must encode real IEEE-754 float32, including fractional and negative values, round-tripping exactly (up to float32 precision) -- unlike scripts/chunk_data_decoder.gd's current int-truncating implementation.""" def test_fractional_and_negative_floats_survive_round_trip(self): payload = cc.ChunkPayload( world_id="1", room_id="main", coord=(0, 0, 0), bounds=cc.ChunkBounds(min=(-17.25, 0.125, -3.0), size=(32.0, 32.0, 1.0)), objects=[cc.WorldObject( id="o1", type="prop", asset_id="a", pos=(1.5, -2.75, 0.001), rot=(0.0, 0.7071068, 0.0, 0.7071068), scale=(1.0, 1.0, 1.0), )], ) encoded = cc.encode_binary(payload) decoded = cc.decode_binary(encoded) assert decoded.bounds.min == pytest.approx((-17.25, 0.125, -3.0), abs=1e-6) assert decoded.objects[0].pos == pytest.approx((1.5, -2.75, 0.001), abs=1e-6) assert decoded.objects[0].rot == pytest.approx( (0.0, 0.7071068, 0.0, 0.7071068), abs=1e-6 ) class TestMalformedInput: def test_bad_magic_raises_codec_error(self): data = bytearray(_read_fixture("empty_chunk.bin")) data[0:4] = b"XXXX" with pytest.raises(cc.ChunkCodecError): cc.decode_binary(bytes(data)) def test_too_short_raises_codec_error(self): with pytest.raises(cc.ChunkCodecError): cc.decode_binary(b"AGPC\x01\x00") def test_unsupported_version_raises_codec_error(self): data = bytearray(_read_fixture("empty_chunk.bin")) data[4:6] = (99).to_bytes(2, "little") with pytest.raises(cc.ChunkCodecError): cc.decode_binary(bytes(data)) def test_payload_len_mismatch_raises_codec_error(self): data = bytearray(_read_fixture("empty_chunk.bin")) # Corrupt the declared payload_len (offset 12..16). data[12:16] = (99999).to_bytes(4, "little") with pytest.raises(cc.ChunkCodecError): cc.decode_binary(bytes(data)) class TestValidation: """decode_and_validate: the server-side "don't trust the client" boundary (world_persistence_impl.md §5.5).""" def _valid_args(self): return { "expected_key": "12:main:3:-1:0", "grid_size": (4, 4, 1), "max_objects": 512, } def test_valid_payload_passes(self): data = _read_fixture("simple_tile_chunk.bin") payload = cc.decode_and_validate(data, **self._valid_args()) assert payload.coord == (3, -1, 0) def test_checksum_mismatch_detected(self): data = bytearray(_read_fixture("simple_tile_chunk.bin")) # Corrupt one byte of the header's advisory checksum field # (offset 16..48) only -- magic/version/payload_len and the whole # payload body stay untouched, so this exercises exactly the # "recomputed checksum disagrees with the header" path rather than # also breaking structural decoding. data[16] ^= 0xFF with pytest.raises(cc.ChunkValidationError) as exc_info: cc.decode_and_validate(bytes(data), **self._valid_args()) assert exc_info.value.reason == "checksum_mismatch" def test_chunk_key_mismatch_detected(self): data = _read_fixture("simple_tile_chunk.bin") args = self._valid_args() args["expected_key"] = "12:main:99:99:0" # doesn't match the payload's own coord with pytest.raises(cc.ChunkValidationError) as exc_info: cc.decode_and_validate(data, **args) assert exc_info.value.reason == "chunk_key_mismatch" def test_layer_dims_mismatch_detected(self): data = _read_fixture("simple_tile_chunk.bin") args = self._valid_args() args["grid_size"] = (8, 8, 1) # fixture's layer is 4x4x1 with pytest.raises(cc.ChunkValidationError) as exc_info: cc.decode_and_validate(data, **args) assert exc_info.value.reason == "layer_dims" def test_too_many_objects_detected(self): data = _read_fixture("chunk_with_objects.bin") # has 2 objects args = self._valid_args() args["expected_key"] = "12:main:3:-4:0" args["grid_size"] = (2, 2, 1) args["max_objects"] = 1 with pytest.raises(cc.ChunkValidationError) as exc_info: cc.decode_and_validate(data, **args) assert exc_info.value.reason == "too_many_objects" def test_object_out_of_bounds_detected(self): payload = cc.ChunkPayload( world_id="1", room_id="main", coord=(0, 0, 0), bounds=cc.ChunkBounds(min=(0.0, 0.0, 0.0), size=(32.0, 32.0, 1.0)), objects=[cc.WorldObject(id="o1", type="prop", asset_id="a", pos=(9999.0, 0.0, 0.0))], ) data = cc.encode_binary(payload) with pytest.raises(cc.ChunkValidationError) as exc_info: cc.decode_and_validate( data, expected_key="1:main:0:0:0", grid_size=(32, 32, 1), max_objects=10 ) assert exc_info.value.reason == "object_out_of_bounds" def test_tile_out_of_palette_detected(self): layer = cc.TileLayer( name="ground", width=2, height=2, depth=1, palette=[cc.TileDef(id=1)], # tile 5 below isn't in this palette tiles=[1, 5, 0, 1], ) payload = cc.ChunkPayload(world_id="1", room_id="main", coord=(0, 0, 0), tile_layers=[layer]) data = cc.encode_binary(payload) with pytest.raises(cc.ChunkValidationError) as exc_info: cc.decode_and_validate( data, expected_key="1:main:0:0:0", grid_size=(2, 2, 1), max_objects=10 ) assert exc_info.value.reason == "tile_out_of_palette"