# retoor <retoor@molodetz.nl>
import unittest
from typing import Any, AsyncIterator
from typosaurus_sandbox.research.client import RsearchError
from typosaurus_sandbox.research.config import ResearchConfig
from typosaurus_sandbox.research.envelopes import ChatResponse, DeepReport, DescribeResponse, SearchResponse, SearchResult
from typosaurus_sandbox.research.frontier import QueryFrontier
from typosaurus_sandbox.research.pipeline import (
Extraction,
ResearchPipeline,
WorkItem,
apply_extraction,
extract_response,
)
class StubResearchClient:
def __init__(self) -> None:
self.config = ResearchConfig(max_concurrency=4, default_count=5)
self.cache_hits: dict[tuple[str, str], Any] = {}
self.error_on: set[tuple[str, str]] = set()
self.chat_response = ChatResponse(response="chat answer")
self.describe_response = DescribeResponse(description="described page")
self.calls: list[tuple[str, str]] = []
def search_cached(
self,
query: str,
*,
source: str | None = None,
count: int | None = None,
content: bool = False,
type: str | None = None,
deep: bool = False,
ai: bool = False,
cache: bool = True,
) -> Any:
if type == "images":
return self.cache_hits.get(("images", query))
return self.cache_hits.get(("web", query))
def describe_cached(self, url: str) -> Any:
return self.cache_hits.get(("describe", url))
async def search(
self,
query: str,
*,
source: str | None = None,
count: int | None = None,
content: bool = False,
type: str | None = None,
deep: bool = False,
ai: bool = False,
cache: bool = True,
) -> SearchResponse:
self.calls.append(("search", query))
if ("search", query) in self.error_on:
raise RsearchError("search failed", 503)
return SearchResponse(
query=query,
source="duckduckgo",
count=1,
success=True,
results=[SearchResult(title=query, url=f"https://example.com/{query}", description="details", content="body")],
)
async def chat(self, prompt: str, *, system: str | None = None, json_mode: bool = False, cache: bool = True) -> ChatResponse:
self.calls.append(("chat", prompt))
if ("chat", prompt) in self.error_on:
raise RsearchError("chat failed", 400)
return self.chat_response
async def describe(self, url: str) -> DescribeResponse:
self.calls.append(("describe", url))
if ("describe", url) in self.error_on:
raise RsearchError("describe failed", 500)
return self.describe_response
class TestExtractResponse(unittest.TestCase):
def test_web_results_extract_urls_seeds_and_content(self) -> None:
item = WorkItem("web", "query")
response = SearchResponse(
query="query",
source="duckduckgo",
count=2,
success=True,
results=[
SearchResult(title="First", url="https://a.example/1", description="First details", content="first body"),
SearchResult(title="Second", url="https://b.example/2", description="Second details", content=None),
],
)
extraction = extract_response(item, response)
self.assertEqual(extraction.urls, ("https://a.example/1", "https://b.example/2"))
self.assertEqual(
extraction.query_seeds,
(
("First", "title"),
("First details", "description"),
("Second", "title"),
("Second details", "description"),
),
)
self.assertEqual(extraction.content_texts, ("first body",))
def test_ai_response_adds_content_seed_and_urls(self) -> None:
item = WorkItem("web", "query")
response = SearchResponse(
query="query",
source="ai",
count=0,
success=True,
results=[],
ai_response="Overview at https://docs.example.org/x and https://blog.example.org/y.",
)
extraction = extract_response(item, response)
self.assertEqual(extraction.urls, ("https://docs.example.org/x", "https://blog.example.org/y"))
self.assertEqual(
extraction.query_seeds,
(("Overview at https://docs.example.org/x and https://blog.example.org/y.", "ai_response"),),
)
self.assertEqual(
extraction.content_texts,
("Overview at https://docs.example.org/x and https://blog.example.org/y.",),
)
def test_deep_report_sources_and_markdown_extracted(self) -> None:
item = WorkItem("web", "query")
response = SearchResponse(
query="query",
source="google",
count=1,
success=True,
results=[],
deep=DeepReport(
query="query",
markdown="# Deep\n\nSee https://deep.example.org/report for details.",
sources=[SearchResult(title="Deep source", url="https://deep.example.org/source", description="Deep details")],
),
)
extraction = extract_response(item, response)
self.assertEqual(extraction.urls, ("https://deep.example.org/source", "https://deep.example.org/report"))
self.assertEqual(
extraction.query_seeds,
(("Deep source", "title"), ("Deep details", "description")),
)
self.assertEqual(extraction.content_texts, ("# Deep\n\nSee https://deep.example.org/report for details.",))
def test_chat_response_extracts_content_seed_and_urls(self) -> None:
item = WorkItem("chat", "prompt")
response = ChatResponse(response="Answer at https://chat.example.org/a.")
extraction = extract_response(item, response)
self.assertEqual(extraction.urls, ("https://chat.example.org/a",))
self.assertEqual(extraction.query_seeds, (("Answer at https://chat.example.org/a.", "chat"),))
self.assertEqual(extraction.content_texts, ("Answer at https://chat.example.org/a.",))
def test_describe_response_extracts_description_seed_and_urls(self) -> None:
item = WorkItem("describe", "https://page.example.org/x")
response = DescribeResponse(description="Image shows a cat. More at https://gallery.example.org/cat.")
extraction = extract_response(item, response)
self.assertEqual(extraction.urls, ("https://gallery.example.org/cat",))
self.assertEqual(
extraction.query_seeds,
(("Image shows a cat. More at https://gallery.example.org/cat.", "describe"),),
)
self.assertEqual(extraction.content_texts, ("Image shows a cat. More at https://gallery.example.org/cat.",))
class TestApplyExtraction(unittest.IsolatedAsyncioTestCase):
async def test_registers_each_kind_and_returns_counts(self) -> None:
frontier = QueryFrontier()
extraction = Extraction(
urls=("https://x.example/1", "https://y.example/2"),
query_seeds=(("variant one", "title"), ("variant two", "description")),
content_texts=("body one", "body two"),
)
self.assertEqual(apply_extraction(frontier, extraction), (2, 2, 2))
self.assertEqual(apply_extraction(frontier, extraction), (0, 0, 0))
stats = frontier.snapshot()
self.assertEqual(stats.urls_seen, 2)
self.assertEqual(stats.queries_enqueued, 2)
self.assertEqual(stats.content_seen, 2)
class TestPipelineCacheProbe(unittest.IsolatedAsyncioTestCase):
async def test_web_probe_reflects_search_cache(self) -> None:
client = StubResearchClient()
frontier = QueryFrontier()
pipeline = ResearchPipeline(client, frontier)
self.assertFalse(pipeline._probe_cache(WorkItem("web", "alpha")))
client.cache_hits[("web", "alpha")] = SearchResponse(query="alpha", success=True)
self.assertTrue(pipeline._probe_cache(WorkItem("web", "alpha")))
async def test_images_probe_uses_images_search_cache(self) -> None:
client = StubResearchClient()
frontier = QueryFrontier()
pipeline = ResearchPipeline(client, frontier)
self.assertFalse(pipeline._probe_cache(WorkItem("images", "alpha")))
client.cache_hits[("images", "alpha")] = SearchResponse(query="alpha", success=True)
self.assertTrue(pipeline._probe_cache(WorkItem("images", "alpha")))
async def test_describe_probe_reflects_describe_cache(self) -> None:
client = StubResearchClient()
frontier = QueryFrontier()
pipeline = ResearchPipeline(client, frontier)
self.assertFalse(pipeline._probe_cache(WorkItem("describe", "https://z.example/page")))
client.cache_hits[("describe", "https://z.example/page")] = DescribeResponse(description="cached")
self.assertTrue(pipeline._probe_cache(WorkItem("describe", "https://z.example/page")))
async def test_chat_never_probes_cache(self) -> None:
client = StubResearchClient()
frontier = QueryFrontier()
pipeline = ResearchPipeline(client, frontier)
self.assertFalse(pipeline._probe_cache(WorkItem("chat", "question")))
class TestPipelineOutcomes(unittest.IsolatedAsyncioTestCase):
async def test_chat_cached_response_marks_outcome_cache_hit(self) -> None:
client = StubResearchClient()
client.chat_response = ChatResponse(response="cached answer", cached=True)
frontier = QueryFrontier()
pipeline = ResearchPipeline(client, frontier)
outcome = await pipeline.process(WorkItem("chat", "question"))
self.assertTrue(outcome.success)
self.assertTrue(outcome.cache_hit)
self.assertEqual(outcome.endpoint, "/chat")
async def test_search_error_produces_failure_outcome_and_pool_survives(self) -> None:
client = StubResearchClient()
client.error_on.add(("search", "bad"))
frontier = QueryFrontier()
async def items() -> AsyncIterator[WorkItem]:
yield WorkItem("web", "bad")
yield WorkItem("web", "good")
pipeline = ResearchPipeline(client, frontier)
report = await pipeline.run(items())
self.assertEqual(report.requests_succeeded, 1)
self.assertEqual(report.requests_failed, 1)
self.assertEqual(len(report.outcomes), 2)
failed = next(outcome for outcome in report.outcomes if not outcome.success)
self.assertEqual(failed.endpoint, "/search")
self.assertEqual(failed.status_code, 503)
self.assertEqual(failed.error, "search failed")
async def test_run_drains_all_four_content_types(self) -> None:
client = StubResearchClient()
frontier = QueryFrontier()
async def items() -> AsyncIterator[WorkItem]:
yield WorkItem("web", "alpha")
yield WorkItem("images", "alpha")
yield WorkItem("describe", "https://z.example/page")
yield WorkItem("chat", "question")
pipeline = ResearchPipeline(client, frontier)
report = await pipeline.run(items())
self.assertEqual(report.requests_succeeded, 4)
self.assertEqual(report.requests_failed, 0)
self.assertEqual(len(report.outcomes), 4)
self.assertTrue(all(outcome.success for outcome in report.outcomes))
endpoints = {outcome.item.kind: outcome.endpoint for outcome in report.outcomes}
self.assertEqual(endpoints, {"web": "/search", "images": "/search", "describe": "/describe", "chat": "/chat"})
if __name__ == "__main__":
unittest.main()