From b072737193d8d71e7b713dc558e571024b8cbfda Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 22 Mar 2026 23:48:32 -0700 Subject: [PATCH] fix: expand tilde (~) in vision_analyze local file paths (#2585) Path('~/.hermes/image.png').is_file() returns False because Path doesn't expand tilde. This caused the tool to fall through to URL validation, which also failed, producing a confusing error: 'Invalid image source. Provide an HTTP/HTTPS URL or a valid local file path.' Fix: use os.path.expanduser() before constructing the Path object. Added two tests for tilde expansion (success and nonexistent file). --- tests/tools/test_vision_tools.py | 56 ++++++++++++++++++++++++++++++++ tools/vision_tools.py | 2 +- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_vision_tools.py b/tests/tools/test_vision_tools.py index 8beb6a0c..14febac0 100644 --- a/tests/tools/test_vision_tools.py +++ b/tests/tools/test_vision_tools.py @@ -378,6 +378,62 @@ class TestVisionRequirements: # --------------------------------------------------------------------------- +# --------------------------------------------------------------------------- +# Tilde expansion in local file paths +# --------------------------------------------------------------------------- + + +class TestTildeExpansion: + """Verify that ~/path style paths are expanded correctly.""" + + @pytest.mark.asyncio + async def test_tilde_path_expanded_to_local_file(self, tmp_path, monkeypatch): + """vision_analyze_tool should expand ~ in file paths.""" + # Create a fake image file under a fake home directory + fake_home = tmp_path / "fakehome" + fake_home.mkdir() + img = fake_home / "test_image.png" + img.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 8) + + monkeypatch.setenv("HOME", str(fake_home)) + + mock_response = MagicMock() + mock_choice = MagicMock() + mock_choice.message.content = "A test image" + mock_response.choices = [mock_choice] + + with ( + patch( + "tools.vision_tools._image_to_base64_data_url", + return_value="data:image/png;base64,abc", + ), + patch( + "tools.vision_tools.async_call_llm", + new_callable=AsyncMock, + return_value=mock_response, + ), + ): + result = await vision_analyze_tool( + "~/test_image.png", "describe this", "test/model" + ) + data = json.loads(result) + assert data["success"] is True + assert data["analysis"] == "A test image" + + @pytest.mark.asyncio + async def test_tilde_path_nonexistent_file_gives_error(self, tmp_path, monkeypatch): + """A tilde path that doesn't resolve to a real file should fail gracefully.""" + fake_home = tmp_path / "fakehome" + fake_home.mkdir() + monkeypatch.setenv("HOME", str(fake_home)) + + result = await vision_analyze_tool( + "~/nonexistent.png", "describe this", "test/model" + ) + data = json.loads(result) + assert data["success"] is False + + class TestVisionRegistration: def test_vision_analyze_registered(self): from tools.registry import registry diff --git a/tools/vision_tools.py b/tools/vision_tools.py index 657cd479..867d9ef3 100644 --- a/tools/vision_tools.py +++ b/tools/vision_tools.py @@ -242,7 +242,7 @@ async def vision_analyze_tool( logger.info("User prompt: %s", user_prompt[:100]) # Determine if this is a local file path or a remote URL - local_path = Path(image_url) + local_path = Path(os.path.expanduser(image_url)) if local_path.is_file(): # Local file path (e.g. from platform image cache) -- skip download logger.info("Using local image file: %s", image_url)