From 7b89bb9a01eef94a080def8256d3f347fb8dffd5 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 12 Oct 2026 21:12:46 -0400 Subject: [PATCH] fix(agent): recover usable trip images and isolate malformed metadata --- apps/agent/src/trip_images.py | 26 +++++++--- .../test_trip_image_resilience_regression.py | 52 +++++++++++++++++++ 2 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 apps/agent/tests/test_trip_image_resilience_regression.py diff --git a/apps/agent/src/trip_images.py b/apps/agent/src/trip_images.py index 103826a..32f66f6 100644 --- a/apps/agent/src/trip_images.py +++ b/apps/agent/src/trip_images.py @@ -14,9 +14,16 @@ def fetch_card(title: str, client: httpx.Client) -> dict: summary = client.get("https://en.wikipedia.org/api/rest_v1/page/summary/" + quote(title, safe="")) summary.raise_for_status() - data = summary.json() - image = data.get("thumbnail", {}).get("source", "") - original = data.get("originalimage", {}).get("source", "") + def mapping(value): + if not isinstance(value, dict): + raise ValueError("The photo source returned invalid metadata.") + return value + + data = mapping(summary.json()) + original = mapping(data.get("originalimage") or {}).get("source", "") + image = mapping(data.get("thumbnail") or {}).get("source") or original + if not isinstance(image, str) or not isinstance(original, str): + raise ValueError("The photo source returned invalid image URLs.") if any(urlsplit(url).scheme != "https" or urlsplit(url).hostname not in IMAGE_HOSTS for url in [image, original]): raise ValueError("No supported destination image was available.") @@ -33,10 +40,17 @@ def commons_filename(url): "iiprop": "extmetadata", "titles": "File:" + filename, }) metadata.raise_for_status() - pages = metadata.json().get("query", {}).get("pages", {}) - info = next(iter(pages.values()), {}).get("imageinfo", [{}])[0].get("extmetadata", {}) + pages = mapping(mapping(mapping(metadata.json()).get("query")).get("pages")) + page = mapping(next(iter(pages.values()), None)) + image_info = page.get("imageinfo") + if not isinstance(image_info, list) or not image_info: + raise ValueError("The photo source returned invalid image metadata.") + info = mapping(mapping(image_info[0]).get("extmetadata")) def plain(key): - return unescape(re.sub(r"<[^>]*>", "", info.get(key, {}).get("value", ""))).strip()[:500] + value = mapping(info.get(key) or {}).get("value", "") + if not isinstance(value, str): + raise ValueError("The photo source returned invalid attribution.") + return unescape(re.sub(r"<[^>]*>", "", value)).strip()[:500] license_name = plain("LicenseShortName") artist = plain("Artist") if not artist or not license_name or not (license_name.startswith("CC") or license_name == "Public domain"): diff --git a/apps/agent/tests/test_trip_image_resilience_regression.py b/apps/agent/tests/test_trip_image_resilience_regression.py new file mode 100644 index 0000000..fa2c4ea --- /dev/null +++ b/apps/agent/tests/test_trip_image_resilience_regression.py @@ -0,0 +1,52 @@ +import httpx +import pytest + +import src.trip_images as images + + +IMAGE = 'https://upload.wikimedia.org/wikipedia/commons/a/ab/Test.jpg' +METADATA = {'query': {'pages': {'1': {'imageinfo': [{'extmetadata': { + 'Artist': {'value': 'A Photographer'}, 'LicenseShortName': {'value': 'CC BY-SA 4.0'}, +}}]}}}} + + +def test_original_image_is_usable_when_summary_has_no_thumbnail(): + def handle(request): + payload = {'originalimage': {'source': IMAGE}} if 'summary' in request.url.path else METADATA + return httpx.Response(200, json=payload) + + with httpx.Client(transport=httpx.MockTransport(handle)) as client: + card = images.fetch_card('Coast', client) + assert card['status'] == 'available' + assert card['image_url'] == IMAGE + assert card['credit_url'].endswith('File:Test.jpg') + + +@pytest.mark.parametrize(('summary', 'metadata'), [ + ([], METADATA), + ({'thumbnail': {'source': IMAGE}, 'originalimage': {'source': IMAGE}}, {'query': []}), + ({'thumbnail': {'source': IMAGE}, 'originalimage': {'source': IMAGE}}, {'query': {'pages': []}}), + ({'thumbnail': {'source': IMAGE}, 'originalimage': {'source': IMAGE}}, {'query': {'pages': {'1': None}}}), + ({'thumbnail': {'source': IMAGE}, 'originalimage': {'source': IMAGE}}, {'query': {'pages': {'1': {'imageinfo': [{'extmetadata': None}]}}}}), +]) +def test_malformed_stop_does_not_abort_other_destination_cards(monkeypatch, summary, metadata): + real_client = httpx.Client + def make_client(**kwargs): + current_title = None + + def handle(request): + nonlocal current_title + if 'summary' in request.url.path: + current_title = request.url.path.rsplit('/', 1)[-1] + return httpx.Response(200, json=summary if current_title == 'Broken' else { + 'thumbnail': {'source': IMAGE}, 'originalimage': {'source': IMAGE}, + }) + return httpx.Response(200, json=metadata if current_title == 'Broken' else METADATA) + + return real_client(transport=httpx.MockTransport(handle)) + + monkeypatch.setattr(images.httpx, 'Client', make_client) + result = images.get_trip_stop_images.invoke({'wikipedia_titles': ['Broken', 'Healthy']}) + assert [stop['status'] for stop in result['stops']] == ['unavailable', 'available'] + assert result['stops'][0]['place'] == 'Broken' + assert result['stops'][1]['image_url'] == IMAGE