From 2960f0360add11e7df60984fd031fc4267ddda63 Mon Sep 17 00:00:00 2001 From: Koho Zheng Date: Fri, 10 Jul 2026 06:07:45 +0800 Subject: [PATCH] fix(image-routing): detect windows image paths --- agent/image_routing.py | 23 +++++++++++-------- tests/agent/test_image_routing.py | 38 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/agent/image_routing.py b/agent/image_routing.py index 1fe52d9565b2..d12889945bbc 100644 --- a/agent/image_routing.py +++ b/agent/image_routing.py @@ -63,10 +63,13 @@ _IMAGE_EXT_PATTERN = "|".join(e.lstrip(".") for e in _IMAGE_EXTS) # Absolute / home-relative local image path. Matches the same shape gateway's -# extract_local_files() uses: anchors to ``~/`` or ``/``, ignores matches inside -# URLs (the ``(? Tuple[List[str], List[str]]: Returns ``(local_paths, urls)``: - * ``local_paths`` — absolute (``/``) or home-relative (``~/``) paths - whose suffix is an image extension AND whose expanded form exists - on disk as a file. Order-preserving, deduplicated. + * ``local_paths`` — absolute (``/`` or Windows drive-letter) or + home-relative (``~/``) paths whose suffix is an image extension AND + whose expanded form exists on disk as a file. Order-preserving, + deduplicated. * ``urls`` — ``http(s)://…`` URLs whose path ends in an image extension (a ``?query`` is allowed after the extension). Order-preserving, deduplicated. @@ -119,16 +123,17 @@ def _in_code(pos: int) -> bool: if _in_code(match.start()): continue raw = match.group(0) - expanded = os.path.expanduser(raw) + expanded = os.path.normpath(os.path.expanduser(raw)) try: if not os.path.isfile(expanded): continue except OSError: # ENAMETOOLONG / EINVAL on pathological inputs — skip rather than crash. continue - if expanded in seen_paths: + dedupe_key = os.path.normcase(expanded) + if dedupe_key in seen_paths: continue - seen_paths.add(expanded) + seen_paths.add(dedupe_key) local_paths.append(expanded) urls: list[str] = [] diff --git a/tests/agent/test_image_routing.py b/tests/agent/test_image_routing.py index 671e6aaa18dd..308c83323c06 100644 --- a/tests/agent/test_image_routing.py +++ b/tests/agent/test_image_routing.py @@ -539,12 +539,50 @@ def test_finds_absolute_path(self, tmp_path: Path): def test_finds_home_relative_path(self, tmp_path: Path, monkeypatch): # Simulate ~/foo.png by pointing HOME at tmp_path and creating the file monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setenv("USERPROFILE", str(tmp_path)) img = tmp_path / "foo.png" img.write_bytes(_png_bytes()) paths, urls = extract_image_refs("see ~/foo.png please") assert paths == [str(img)] assert urls == [] + def test_finds_windows_drive_letter_paths(self): + first = "C:\\Users\\Hiro\\shot.png" + second = "D:/captures/current.JPG" + + with patch("agent.image_routing.os.path.isfile", return_value=True): + paths, urls = extract_image_refs(f"Compare {first} against {second}.") + + assert paths == [first, second] + assert urls == [] + + def test_windows_drive_letter_paths_are_case_insensitive(self): + path = "c:/captures/current.PNG" + + with patch("agent.image_routing.os.path.isfile", return_value=True): + paths, urls = extract_image_refs(f"Check {path}") + + assert paths == [path] + assert urls == [] + + def test_ignores_relative_windows_drive_paths(self): + with patch("agent.image_routing.os.path.isfile", return_value=True): + paths, urls = extract_image_refs( + "Do not attach C:relative.png or C:folder\\nested.png" + ) + + assert paths == [] + assert urls == [] + + def test_does_not_match_windows_path_inside_url(self): + body = "Only the URL: https://example.com/files/C:/captures/current.png" + + with patch("agent.image_routing.os.path.isfile", return_value=True): + paths, urls = extract_image_refs(body) + + assert paths == [] + assert urls == ["https://example.com/files/C:/captures/current.png"] + def test_skips_nonexistent_paths(self, tmp_path: Path): # Path-shaped but no file on disk → skipped. body = f"What's at {tmp_path}/never_created.png ?"