From c175a1d60ff243257d476b68e20ad62db0437cba Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 13:37:50 +0000 Subject: [PATCH] fix(fonts): resolve relative asset paths from the install root, not the cwd resolve_asset_path checked os.path.exists(relative_path) unconditionally, so a relative asset path was still resolved against the process cwd first -- exactly the dependency this module exists to remove. An unrelated working directory that happens to contain assets/fonts/4x6-font.ttf (a stale checkout, a copied assets folder, another project) would shadow the real bundled font instead of the install root ever being consulted. Only an absolute path is now returned as-is; a relative path always resolves against _INSTALL_ROOT first, matching the docstring's stated contract. FontManager._resolve_asset_path delegates to this function, so it's covered by the same fix. Co-Authored-By: Claude Opus 5 --- src/common/font_layout.py | 2 +- test/test_font_pixel_grid.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/common/font_layout.py b/src/common/font_layout.py index 7844ef38..097e604a 100644 --- a/src/common/font_layout.py +++ b/src/common/font_layout.py @@ -77,7 +77,7 @@ def resolve_asset_path(relative_path: str) -> str: file written without ``WorkingDirectory``) silently loses every font and degrades to PIL's default face. """ - if os.path.exists(relative_path): + if os.path.isabs(relative_path) and os.path.exists(relative_path): return relative_path candidate = _INSTALL_ROOT / relative_path if candidate.exists(): diff --git a/test/test_font_pixel_grid.py b/test/test_font_pixel_grid.py index 2835c574..3889d91a 100644 --- a/test/test_font_pixel_grid.py +++ b/test/test_font_pixel_grid.py @@ -98,6 +98,30 @@ class TestAssetPathsIgnoreTheWorkingDirectory: ) assert out.stdout.strip() == "True", out.stderr + def test_a_same_named_file_in_the_cwd_does_not_shadow_the_bundled_asset( + self, tmp_path, + ): + # A relative path is a repo-relative asset name, not a cwd-relative + # one: an unrelated decoy that happens to sit at the same relative + # path in whatever directory the process was started from must not + # be preferred over the real bundled font. + decoy_dir = tmp_path / "assets" / "fonts" + decoy_dir.mkdir(parents=True) + decoy = decoy_dir / FOUR_BY_SIX + decoy.write_bytes(b"not a real font") + code = ( + "from src.common.font_layout import resolve_asset_path;" + "print(resolve_asset_path('assets/fonts/4x6-font.ttf'))" + ) + env = dict(os.environ, PYTHONPATH=str(PROJECT_ROOT)) + out = subprocess.run( # nosec B603 - fixed argv, no shell + [sys.executable, "-c", code], + cwd=tmp_path, env=env, capture_output=True, text=True, timeout=120, + ) + resolved = out.stdout.strip() + assert resolved != str(decoy), out.stderr + assert resolved == str(PROJECT_ROOT / "assets" / "fonts" / FOUR_BY_SIX) + def test_an_absolute_path_is_returned_untouched(self): absolute = str(PROJECT_ROOT / "assets" / "fonts" / FOUR_BY_SIX) assert resolve_asset_path(absolute) == absolute