a digital entity named phi that roams bsky phi.zzstoatzz.io
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix search_network for real semble API response shape

semble returns {urls: [...], pagination: {...}} not a flat array.
fields are metadata.title, metadata.description, urlLibraryCount.
also remove unnecessary @pytest.mark.asyncio (asyncio_mode=auto).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+43 -31
+9 -6
src/bot/agent.py
··· 297 297 params={"query": query, "limit": 10}, 298 298 ) 299 299 r.raise_for_status() 300 - results = r.json() 300 + data = r.json() 301 301 302 - if not results: 302 + # response is {urls: [...], pagination: {...}} 303 + items = data.get("urls") if isinstance(data, dict) else data 304 + if not items: 303 305 return f"no network results for '{query}'" 304 306 305 307 lines = [] 306 - for item in results: 307 - title = item.get("title") or item.get("text") or "untitled" 308 + for item in items: 309 + meta = item.get("metadata", {}) 310 + title = meta.get("title") or item.get("title") or "untitled" 308 311 url = item.get("url", "") 309 - saves = item.get("saveCount") or item.get("saves") or 0 310 - desc = item.get("description") or "" 312 + saves = item.get("urlLibraryCount") or 0 313 + desc = meta.get("description") or "" 311 314 line = f"{title}" 312 315 if url: 313 316 line += f" — {url}"
+34 -25
tests/test_search_network.py
··· 3 3 from unittest.mock import AsyncMock, MagicMock, patch 4 4 5 5 import httpx 6 - import pytest 7 6 8 7 9 8 async def _search_network(query: str) -> str: ··· 15 14 params={"query": query, "limit": 10}, 16 15 ) 17 16 r.raise_for_status() 18 - results = r.json() 17 + data = r.json() 19 18 20 - if not results: 19 + # response is {urls: [...], pagination: {...}} 20 + items = data.get("urls") if isinstance(data, dict) else data 21 + if not items: 21 22 return f"no network results for '{query}'" 22 23 23 24 lines = [] 24 - for item in results: 25 - title = item.get("title") or item.get("text") or "untitled" 25 + for item in items: 26 + meta = item.get("metadata", {}) 27 + title = meta.get("title") or item.get("title") or "untitled" 26 28 url = item.get("url", "") 27 - saves = item.get("saveCount") or item.get("saves") or 0 28 - desc = item.get("description") or "" 29 + saves = item.get("urlLibraryCount") or 0 30 + desc = meta.get("description") or "" 29 31 line = f"{title}" 30 32 if url: 31 33 line += f" — {url}" ··· 42 44 def _mock_response(status_code: int, json_data=None): 43 45 resp = MagicMock(spec=httpx.Response) 44 46 resp.status_code = status_code 45 - resp.json.return_value = json_data or [] 47 + resp.json.return_value = json_data if json_data is not None else {"urls": []} 46 48 resp.raise_for_status = MagicMock() 47 49 if status_code >= 400: 48 50 resp.raise_for_status.side_effect = httpx.HTTPStatusError( 49 51 "error", request=MagicMock(), response=resp 50 52 ) 51 53 return resp 54 + 55 + 56 + def _semble_response(items): 57 + """Wrap items in semble's {urls: [...], pagination: {...}} envelope.""" 58 + return {"urls": items, "pagination": {"currentPage": 1, "totalPages": 1, "totalCount": len(items), "hasMore": False, "limit": 10}} 52 59 53 60 54 61 class TestSearchNetworkFormatting: 55 - @pytest.mark.asyncio 56 62 async def test_formats_results_with_all_fields(self): 57 - resp = _mock_response(200, [ 63 + resp = _mock_response(200, _semble_response([ 58 64 { 59 - "title": "AT Protocol", 60 65 "url": "https://atproto.com", 61 - "saveCount": 5, 62 - "description": "Federated social networking protocol", 66 + "metadata": { 67 + "title": "AT Protocol", 68 + "description": "Federated social networking protocol", 69 + }, 70 + "urlLibraryCount": 5, 63 71 }, 64 72 { 65 - "title": "Bluesky Docs", 66 73 "url": "https://docs.bsky.app", 67 - "saveCount": 3, 68 - "description": "Documentation for Bluesky", 74 + "metadata": { 75 + "title": "Bluesky Docs", 76 + "description": "Documentation for Bluesky", 77 + }, 78 + "urlLibraryCount": 3, 69 79 }, 70 - ]) 80 + ])) 71 81 with patch("httpx.AsyncClient.get", new_callable=AsyncMock, return_value=resp): 72 82 result = await _search_network("atproto") 73 83 assert "AT Protocol — https://atproto.com (5 saves)" in result 74 84 assert "Federated social networking protocol" in result 75 85 assert "Bluesky Docs — https://docs.bsky.app (3 saves)" in result 76 86 77 - @pytest.mark.asyncio 78 87 async def test_formats_results_with_minimal_fields(self): 79 - resp = _mock_response(200, [{"text": "some note about music"}]) 88 + resp = _mock_response(200, _semble_response([ 89 + {"url": "https://example.com/music", "metadata": {"title": "some note about music"}}, 90 + ])) 80 91 with patch("httpx.AsyncClient.get", new_callable=AsyncMock, return_value=resp): 81 92 result = await _search_network("music") 82 93 assert "some note about music" in result 83 94 assert "saves" not in result 84 95 85 - @pytest.mark.asyncio 86 96 async def test_empty_results(self): 87 - resp = _mock_response(200, []) 97 + resp = _mock_response(200, _semble_response([])) 88 98 with patch("httpx.AsyncClient.get", new_callable=AsyncMock, return_value=resp): 89 99 result = await _search_network("nonexistent") 90 100 assert result == "no network results for 'nonexistent'" 91 101 92 - @pytest.mark.asyncio 93 102 async def test_api_failure(self): 94 103 resp = _mock_response(500) 95 104 with patch("httpx.AsyncClient.get", new_callable=AsyncMock, return_value=resp): 96 105 result = await _search_network("anything") 97 106 assert result.startswith("network search failed:") 98 107 99 - @pytest.mark.asyncio 100 108 async def test_network_error(self): 101 109 with patch( 102 110 "httpx.AsyncClient.get", ··· 106 114 result = await _search_network("anything") 107 115 assert result.startswith("network search failed:") 108 116 109 - @pytest.mark.asyncio 110 117 async def test_untitled_fallback(self): 111 - resp = _mock_response(200, [{"url": "https://example.com"}]) 118 + resp = _mock_response(200, _semble_response([ 119 + {"url": "https://example.com", "metadata": {}}, 120 + ])) 112 121 with patch("httpx.AsyncClient.get", new_callable=AsyncMock, return_value=resp): 113 122 result = await _search_network("test") 114 123 assert "untitled — https://example.com" in result