"""Artwork registration must preserve custom images and unrelated accounts.""" import hashlib import importlib.util import json import os import sys from pathlib import Path import pytest RESOURCES = Path(__file__).resolve().parents[1] / "resources" @pytest.fixture def steam(monkeypatch): spec = importlib.util.spec_from_file_location("steam_artwork_test", RESOURCES / "steam_shortcut.py") module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) monkeypatch.setattr(module, "steam_running", lambda: False) return module def make_steam_home(tmp_path): home = tmp_path / "home" steam = home / ".local/share/Steam" (steam / "config").mkdir(parents=True) (steam / "userdata/10/config").mkdir(parents=True) active = steam / "userdata/20/config" active.mkdir(parents=True) (steam / "config/loginusers.vdf").write_text( '"users" { "76561197960265738" { "MostRecent" "0" } ' '"76561197960265748" { "MostRecent" "1" } }') return home, active, steam def test_bundled_artwork_integrity_and_png_dimensions(steam): for _, _, name, expected in steam.ARTWORK: assert hashlib.sha256((RESOURCES / "artwork" / name).read_bytes()).hexdigest() == expected import struct wide = (RESOURCES / "artwork/halo-ce-landscape.png").read_bytes() thumbnail = (RESOURCES / "artwork/halo-ce-thumbnail.png").read_bytes() logo = (RESOURCES / "artwork/halo-ce-logo.png").read_bytes() icon = (RESOURCES / "artwork" / steam.ICON_NAME).read_bytes() assert struct.unpack(">II", wide[16:24]) == (920, 430) assert struct.unpack(">II", thumbnail[16:24]) == (180, 270) assert struct.unpack(">II", logo[16:24]) == (1000, 431) assert logo[25] == 6 # RGBA, preserving the logo's transparent background. assert struct.unpack(">II", icon[16:24]) == (256, 256) assert hashlib.sha256(icon).hexdigest() == steam.ICON_SHA256 assert steam.ARTWORK_TYPES == {"p": 0, "_hero": 1, "_logo": 2, "": 3} def test_new_shortcut_gets_box_art_in_only_active_account(steam, tmp_path): home, active, directory = make_steam_home(tmp_path) result = steam.add_native_shortcut(home, home / "Games/HaloCENativeVR") appid = result["appid"] assert result["status"] == "added" assert result["artwork"]["status"] == "added" assert (active / "grid" / f"{appid}p.jpg").read_bytes() == (RESOURCES / "artwork/halo-ce-cover.jpg").read_bytes() assert (active / "grid" / f"{appid}.png").read_bytes() == (RESOURCES / "artwork/halo-ce-landscape.png").read_bytes() assert not (directory / "userdata/10/config/grid").exists() assert (active / "grid" / f"{appid}_hero.jpg").read_bytes() == (RESOURCES / "artwork/halo-ce-hero.jpg").read_bytes() assert (active / "grid" / f"{appid}_logo.png").read_bytes() == (RESOURCES / "artwork/halo-ce-logo.png").read_bytes() assert steam.loads((active / "shortcuts.vdf").read_bytes())["shortcuts"].value["0"].value["appid"].value == appid def test_retry_fills_missing_art_for_unchanged_existing_shortcut(steam, tmp_path): home, active, _ = make_steam_home(tmp_path) game = home / "Games/HaloCENativeVR" raw, appid = steam.update_shortcut(None, str(game / "halo"), str(game)) (active / "shortcuts.vdf").write_bytes(raw) result = steam.add_native_shortcut(home, game) assert result["unchanged"] and result["appid"] == appid assert result["artwork"]["status"] == "added" assert (active / "shortcuts.vdf").read_bytes() == raw again = steam.add_native_shortcut(home, game) assert again["artwork"]["status"] == "unchanged" assert sorted(path.name for path in (active / "grid").iterdir()) == sorted( str(appid) + suffix + extension for suffix, extension, _, _ in steam.ARTWORK) def test_existing_unsigned_shortcut_id_is_used_even_if_crc_differs(steam, tmp_path): home, active, _ = make_steam_home(tmp_path) game = home / "Games/HaloCENativeVR" raw, original = steam.update_shortcut(None, str(game / "halo"), str(game)) root = steam.loads(raw) selected = 0xF1234567 assert selected != original root["shortcuts"].value["0"].value["appid"].value = selected (active / "shortcuts.vdf").write_bytes(steam.dumps(root)) result = steam.add_native_shortcut(home, game) assert result["appid"] == selected assert (active / "grid" / f"{selected}p.jpg").is_file() assert not (active / "grid" / f"{original}p.jpg").exists() def test_custom_art_with_other_extensions_and_other_games_is_kept(steam, tmp_path): config = tmp_path / "config" grid = config / "grid" grid.mkdir(parents=True) appid = 3732925724 contents = {str(appid) + suffix + ".jpeg": b"custom " + suffix.encode() for suffix, _, _, _ in steam.ARTWORK} contents["10p.png"] = b"other game" for name, content in contents.items(): (grid / name).write_bytes(content) result = steam.install_artwork(config, appid) assert result["status"] == "preserved" assert result["installed"] == [] assert {path.name: path.read_bytes() for path in grid.iterdir()} == contents def test_custom_portrait_is_kept_and_missing_wide_tile_is_added(steam, tmp_path): config = tmp_path / "config" grid = config / "grid" grid.mkdir(parents=True) (grid / "3732925724p.jpg").write_bytes(b"custom portrait") result = steam.install_artwork(config, 3732925724) assert result["installed"] == ["3732925724" + suffix + extension for suffix, extension, _, _ in steam.ARTWORK if suffix != "p"] assert result["preserved"] == ["3732925724p.jpg"] assert (grid / "3732925724p.jpg").read_bytes() == b"custom portrait" def test_corrupt_bundle_adds_no_art_but_reports_shortcut_separately(steam, tmp_path, monkeypatch): home, active, _ = make_steam_home(tmp_path) real = steam.install_artwork broken = tmp_path / "broken" broken.mkdir() (broken / "halo-ce-cover.jpg").write_bytes(b"bad") monkeypatch.setattr(steam, "install_artwork", lambda config, appid: real(config, appid, broken)) result = steam.add_native_shortcut(home, home / "Games/HaloCENativeVR") assert result["status"] == "added" and (active / "shortcuts.vdf").is_file() assert result["artwork"]["status"] == "manual" assert "integrity" in result["artwork"]["reason"] assert not (active / "grid").exists() def test_second_publication_failure_rolls_back_only_new_art(steam, tmp_path, monkeypatch): config = tmp_path / "config" config.mkdir() real_link = os.link count = 0 def link(source, destination): nonlocal count count += 1 if count == 2: raise OSError("simulated filesystem failure") real_link(source, destination) monkeypatch.setattr(steam.os, "link", link) with pytest.raises(OSError, match="simulated"): steam.install_artwork(config, 3732925724) assert list((config / "grid").iterdir()) == [] def test_concurrent_custom_art_is_preserved_and_first_new_file_rolled_back(steam, tmp_path, monkeypatch): config = tmp_path / "config" grid = config / "grid" grid.mkdir(parents=True) real_link = os.link def link(source, destination): real_link(source, destination) (grid / "3732925724.jpeg").write_bytes(b"new user art") monkeypatch.setattr(steam.os, "link", link) with pytest.raises(ValueError, match="changed during"): steam.install_artwork(config, 3732925724) assert {path.name: path.read_bytes() for path in grid.iterdir()} == {"3732925724.jpeg": b"new user art"} def test_artwork_is_left_untouched_if_steam_reopens(steam, tmp_path, monkeypatch): config = tmp_path / "config" config.mkdir() states = iter([False, False, True]) monkeypatch.setattr(steam, "steam_running", lambda: next(states)) with pytest.raises(ValueError, match="reopened"): steam.install_artwork(config, 3732925724) assert list((config / "grid").iterdir()) == [] def test_symlink_artwork_folder_is_rejected_without_touching_target(steam, tmp_path): config = tmp_path / "config" outside = tmp_path / "outside" config.mkdir() outside.mkdir() try: (config / "grid").symlink_to(outside, target_is_directory=True) except OSError: pytest.skip("Creating symbolic links requires Windows privileges.") with pytest.raises(ValueError, match="ordinary directory"): steam.install_artwork(config, 3732925724) assert list(outside.iterdir()) == [] def test_hardlinked_custom_art_is_rejected_before_writing_other_tile(steam, tmp_path): config = tmp_path / "config" grid = config / "grid" grid.mkdir(parents=True) foreign = tmp_path / "foreign.jpg" foreign.write_bytes(b"keep") os.link(foreign, grid / "3732925724p.jpg") with pytest.raises(ValueError, match="hard-linked"): steam.install_artwork(config, 3732925724) assert foreign.read_bytes() == b"keep" assert sorted(path.name for path in grid.iterdir()) == ["3732925724p.jpg"] def owned_game(tmp_path): game = tmp_path / "Games/HaloCENativeVR" game.mkdir(parents=True) program = bytearray(64) program[:6] = b"\x7fELF\x02\x01" program[18:20] = (183).to_bytes(2, "little") (game / "halo").write_bytes(program) (game / ".halo-frame-installer.json").write_text(json.dumps({ "owner": "halo-frame-installer", "sourceCommit": "88142798513ebd99fc7c6224023e8b44c05d0106", "files": {"halo": hashlib.sha256(program).hexdigest()}})) return game def test_closed_shortcut_icon_uses_persistent_owned_sidecar(steam, tmp_path): game = owned_game(tmp_path) raw, appid = steam.update_shortcut(None, str(game / "halo"), str(game)) updated, result = steam._closed_shortcut_icon(raw, game, appid) target = game / ".installer-artwork" / steam.ICON_NAME assert result == {"status": "added", "path": str(target)} assert target.read_bytes() == (RESOURCES / "artwork" / steam.ICON_NAME).read_bytes() _, _, selected = steam._shortcut_icon_field(updated, appid, str(game / "halo")) assert selected == str(target) unchanged, retry = steam._closed_shortcut_icon(updated, game, appid) assert unchanged == updated and retry["status"] == "unchanged" def test_closed_shortcut_custom_icon_path_is_preserved_without_sidecar(steam, tmp_path): game = owned_game(tmp_path) raw, appid = steam.update_shortcut(None, str(game / "halo"), str(game)) root = steam.loads(raw) root["shortcuts"].value["0"].value["icon"] = steam.Value(1, "/custom/chief.png") custom = steam.dumps(root) unchanged, result = steam._closed_shortcut_icon(custom, game, appid) assert unchanged == custom and result["status"] == "preserved" assert not (game / ".installer-artwork").exists() def test_changed_managed_icon_is_kept_in_both_empty_and_selected_shortcut(steam, tmp_path): game = owned_game(tmp_path) raw, appid = steam.update_shortcut(None, str(game / "halo"), str(game)) updated, result = steam._closed_shortcut_icon(raw, game, appid) icon = Path(result["path"]) icon.write_bytes(b"my custom Chief icon") for content in (raw, updated): unchanged, retry = steam._closed_shortcut_icon(content, game, appid) assert unchanged == content and retry["status"] == "preserved" assert icon.read_bytes() == b"my custom Chief icon" def test_icon_publication_requires_installer_owned_game(steam, tmp_path): game = owned_game(tmp_path) (game / ".halo-frame-installer.json").write_text('{"owner":"another-application"}') with pytest.raises(ValueError, match="ownership"): steam._publish_shortcut_icon(game) assert not (game / ".installer-artwork").exists() def test_icon_source_integrity_failure_does_not_create_sidecar(steam, tmp_path): game = owned_game(tmp_path) broken = tmp_path / "artwork" broken.mkdir() (broken / steam.ICON_NAME).write_bytes(b"changed bundle") with pytest.raises(ValueError, match="integrity"): steam._publish_shortcut_icon(game, broken) assert not (game / ".installer-artwork").exists() def test_later_corrupt_logo_prevents_all_artwork_writes(steam, tmp_path): sources = tmp_path / "artwork" sources.mkdir() for _, _, name, _ in steam.ARTWORK: (sources / name).write_bytes((RESOURCES / "artwork" / name).read_bytes()) (sources / "halo-ce-logo.png").write_bytes(b"corrupted last category") config = tmp_path / "config" config.mkdir() with pytest.raises(ValueError, match="integrity"): steam.install_artwork(config, 3000000001, sources) assert not (config / "grid").exists() def test_corrupt_optional_icon_prevents_any_artwork_publication(steam, tmp_path): source = tmp_path / "corrupt-icon" source.mkdir() for _, _, filename, _ in steam.ARTWORK: (source / filename).write_bytes((RESOURCES / "artwork" / filename).read_bytes()) (source / steam.ICON_NAME).write_bytes(b"corrupted optional icon") config = tmp_path / "config" config.mkdir() with pytest.raises(ValueError, match="icon.*integrity"): steam.install_artwork(config, 3000000001, source) assert not (config / "grid").exists() def test_original_guided_native_install_gets_icon_without_marker_adoption(steam, tmp_path): game = owned_game(tmp_path) metadata = json.loads((game / ".halo-frame-installer.json").read_text()) (game / ".halo-frame-installer.json").unlink() marker = game / ".codex-halo-native-install.json" metadata["owner"] = "codex-halo-native-frame-20261005" metadata["build"] = {"sourceCommit": metadata.pop("sourceCommit")} before = json.dumps(metadata).encode() marker.write_bytes(before) program_before = (game / "halo").read_bytes() result = steam._publish_shortcut_icon(game) assert result["status"] == "added" assert marker.read_bytes() == before and not (game / ".halo-frame-installer.json").exists() assert (game / "halo").read_bytes() == program_before assert hashlib.sha256(Path(result["path"]).read_bytes()).hexdigest() == steam.ICON_SHA256 @pytest.mark.parametrize("change", ["source", "program", "architecture"]) def test_icon_sidecar_needs_pinned_source_and_verified_native_identity(steam, tmp_path, change): game = owned_game(tmp_path) marker = game / ".halo-frame-installer.json" metadata = json.loads(marker.read_text()) if change == "source": metadata["sourceCommit"] = "f" * 40 else: program = bytearray((game / "halo").read_bytes()) program[18:20] = (62).to_bytes(2, "little") if change == "architecture" else program[18:20] program.extend(b"modified") (game / "halo").write_bytes(program) if change == "architecture": metadata["files"]["halo"] = hashlib.sha256(program).hexdigest() marker.write_text(json.dumps(metadata)) with pytest.raises(ValueError, match="verified|revision"): steam._publish_shortcut_icon(game) assert not (game / ".installer-artwork").exists()