--- a/tests/unit/test_wilderness_map_generator.py +++ b/tests/unit/test_wilderness_map_generator.py @@ -14,7 +14,8 @@ Tests: 9. Binary serializer and deserializer round-trip match (16-byte header + tables + Uint8Array). 10. ProceduralMapEngine facade delegation. -11. Robust error handling for invalid/truncated binary buffers. -12. Client tile_grid_loader.js compatibility check via Node.js runtime. +11. Robust error handling for invalid/truncated binary buffers (Python + JS). +12. Client tile_grid_loader.js compatibility check, OOB/NaN guards, and mutation via Node.js runtime. """ from __future__ import annotations @@ -227,6 +228,19 @@ class TestMapBinarySerializer: with pytest.raises(ValueError, match="Invalid map magic"): deserialize_map_grid(corrupt_magic) + def test_binary_truncation_across_all_sections(self) -> None: + gen = WildernessMapGenerator.for_zone("zone_tang_kiem_nhai") + valid_buf = serialize_map_grid(gen.generate(seed=42)) + total_len = len(valid_buf) + for cut in [16, 20, 25, total_len - 1, total_len - 50]: + with pytest.raises(ValueError, match="Buffer truncated"): + deserialize_map_grid(valid_buf[:cut]) + + def test_unknown_tile_value_fallback_to_wall(self) -> None: + gen = WildernessMapGenerator.for_zone("zone_tang_kiem_nhai") + raw = bytearray(serialize_map_grid(gen.generate(seed=7))) + raw[-1] = 99 # Out of range tile code + restored = deserialize_map_grid(bytes(raw)) + assert restored.tiles[-1][-1].tile_type == TileType.WALL + def test_facade_and_to_binary_integration(self) -> None: map_data = generate_wilderness_map("zone_tang_kiem_nhai", seed=123) @@ -237,6 +251,15 @@ class TestMapBinarySerializer: class TestClientTileGridLoaderNodeIntegration: - """Verifies client-side JS loader decodes binary stream.""" + """Verifies client-side JS loader decodes binary stream, handles OOB/NaN queries, and in-place mutations.""" + + @staticmethod + def _run_node(script: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["node", "-e", script], + capture_output=True, + text=True, + cwd=r"c:\Projects\FreeExile", + ) def test_node_tile_grid_loader_roundtrip(self, tmp_path) -> None: @@ -251,3 +274,84 @@ class TestClientTileGridLoaderNodeIntegration: const fs = require('fs'); - global.window = {}; const { TileGridLoader } = require('./client/webapp/js/engine/tile_grid_loader.js'); @@ -264,4 +286,75 @@ class TestClientTileGridLoaderNodeIntegration: """ - res = subprocess.run(["node", "-e", js_script], capture_output=True, text=True) + res = self._run_node(js_script) assert res.returncode == 0, f"Node decoding failed: {res.stderr}" + + def test_node_loader_clean_import_without_window(self) -> None: + if not shutil.which("node"): + pytest.skip("Node.js runtime not installed in environment") + script = """ + const { TileGridLoader, BIOME_CODES } = require('./client/webapp/js/engine/tile_grid_loader.js'); + if (!TileGridLoader || !BIOME_CODES) process.exit(1); + """ + res = self._run_node(script) + assert res.returncode == 0, f"Clean Node require failed (window ReferenceError): {res.stderr}" + + def test_node_get_tile_at_non_finite_and_oob(self) -> None: + if not shutil.which("node"): + pytest.skip("Node.js runtime not installed in environment") + script = """ + const { TileGridLoader } = require('./client/webapp/js/engine/tile_grid_loader.js'); + TileGridLoader.loadFallbackGrid(60, 45, 1); + const badCoords = [NaN, undefined, -1, 60, Infinity, -Infinity, "invalid"]; + for (const c of badCoords) { + if (TileGridLoader.getTileAt(c, 0) !== 2 || TileGridLoader.getTileAt(0, c) !== 2) { + console.error("Failed for coord:", c, "got:", TileGridLoader.getTileAt(c, 0)); + process.exit(1); + } + } + if (TileGridLoader.getTileAt(NaN, NaN) !== 2) process.exit(2); + if (TileGridLoader.getTileAt(0.9, 0.9) !== 1) process.exit(3); + process.exit(0); + """ + res = self._run_node(script) + assert res.returncode == 0, f"getTileAt non-finite/OOB failed: {res.stderr}" + + def test_node_set_tile_at_mutation_and_guards(self) -> None: + if not shutil.which("node"): + pytest.skip("Node.js runtime not installed in environment") + script = """ + const { TileGridLoader } = require('./client/webapp/js/engine/tile_grid_loader.js'); + TileGridLoader.loadFallbackGrid(10, 10, 1); + if (!TileGridLoader.setTileAt(5, 5, 10) || TileGridLoader.getTileAt(5, 5) !== 10) process.exit(1); + if (!TileGridLoader.setTileAt(5, 5, 1) || TileGridLoader.getTileAt(5, 5) !== 1) process.exit(2); + for (const bad of [-1, 10, NaN, Infinity]) { + if (TileGridLoader.setTileAt(bad, 0, 1) !== false) process.exit(3); + } + process.exit(0); + """ + res = self._run_node(script) + assert res.returncode == 0, f"setTileAt mutation or guard failed: {res.stderr}" + + def test_node_loader_truncated_and_corrupt_buffers(self, tmp_path) -> None: + if not shutil.which("node"): + pytest.skip("Node.js runtime not installed in environment") + gen = WildernessMapGenerator.for_zone("zone_tang_kiem_nhai") + bin_path = tmp_path / "valid.bin" + bin_path.write_bytes(serialize_map_grid(gen.generate(seed=42))) + bin_str = str(bin_path).replace("\\", "/") + + script = f""" + const fs = require('fs'); + const {{ TileGridLoader }} = require('./client/webapp/js/engine/tile_grid_loader.js'); + if (TileGridLoader.loadBinaryMap(null) !== null) process.exit(1); + if (TileGridLoader.loadBinaryMap(Buffer.alloc(0)) !== null) process.exit(2); + if (TileGridLoader.loadBinaryMap(Buffer.alloc(15)) !== null) process.exit(3); + + const valid = fs.readFileSync('{bin_str}'); + const corrupt = Buffer.from(valid); corrupt[0] = 88; corrupt[1] = 88; + if (TileGridLoader.loadBinaryMap(corrupt) !== null) process.exit(4); + + for (const cut of [16, 20, valid.length - 1]) {{ + if (TileGridLoader.loadBinaryMap(valid.subarray(0, cut)) !== null) process.exit(5); + }} + + const fake = Buffer.alloc(16); + fake.write("FE", 0, 2, 'ascii'); fake.writeUInt8(1, 2); fake.writeUInt8(1, 3); + fake.writeUInt16LE(0, 4); fake.writeUInt16LE(0, 6); fake.writeUInt8(10, 12); + if (TileGridLoader.loadBinaryMap(fake) !== null) process.exit(6); + + process.exit(0); + """ + res = self._run_node(script) + assert res.returncode == 0, f"Node buffer rejection failed: {res.stderr}"