Files
thysrael--horizon/tests/test_balanced_digest.py
wehub-resource-sync 67296cb4a7
Deploy Docs / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:26:53 +08:00

191 lines
6.2 KiB
Python

import asyncio
from datetime import datetime, timezone
from types import SimpleNamespace
import pytest
from pydantic import ValidationError
from rich.console import Console
from src.models import (
AIConfig,
CategoryGroupConfig,
Config,
ContentItem,
FilteringConfig,
SourceType,
SourcesConfig,
)
from src.orchestrator import HorizonOrchestrator
def make_item(item_id: str, score: float, category: str | None) -> ContentItem:
metadata = {"category": category} if category is not None else {}
return ContentItem(
id=item_id,
source_type=SourceType.RSS,
title=item_id,
url=f"https://example.com/{item_id}",
published_at=datetime.now(timezone.utc),
ai_score=score,
metadata=metadata,
)
def make_orchestrator(filtering: FilteringConfig) -> HorizonOrchestrator:
orchestrator = HorizonOrchestrator.__new__(HorizonOrchestrator)
orchestrator.config = SimpleNamespace(filtering=filtering)
orchestrator.console = Console(record=True)
return orchestrator
def test_unconfigured_balanced_digest_preserves_old_behavior() -> None:
items = [make_item("lower", 7.0, "ai"), make_item("higher", 9.0, "finance")]
result = make_orchestrator(FilteringConfig()).apply_balanced_digest(items)
assert result.enabled is False
assert result.items is items
def test_category_groups_apply_limits_and_default_group_limit() -> None:
filtering = FilteringConfig(
category_groups={
"ai": CategoryGroupConfig(limit=2, categories=["ai", "ml"]),
"finance": CategoryGroupConfig(limit=1, categories=["finance"]),
},
default_group_limit=1,
)
items = [
make_item("ai-low", 7.0, "ai"),
make_item("finance-low", 6.0, "finance"),
make_item("other-high", 9.5, "world"),
make_item("ai-high", 9.0, "ml"),
make_item("finance-high", 8.5, "finance"),
make_item("ai-mid", 8.0, "ai"),
make_item("other-low", 5.0, None),
]
result = make_orchestrator(filtering).apply_balanced_digest(items)
assert [item.id for item in result.items] == [
"other-high",
"ai-high",
"finance-high",
"ai-mid",
]
assert result.group_counts == {"other": 1, "ai": 2, "finance": 1}
def test_max_items_applies_after_group_limits() -> None:
filtering = FilteringConfig(
max_items=2,
category_groups={
"ai": CategoryGroupConfig(limit=2, categories=["ai"]),
"finance": CategoryGroupConfig(limit=2, categories=["finance"]),
},
)
items = [
make_item("finance", 8.0, "finance"),
make_item("ai-top", 10.0, "ai"),
make_item("ai-second", 9.0, "ai"),
]
result = make_orchestrator(filtering).apply_balanced_digest(items)
assert [item.id for item in result.items] == ["ai-top", "ai-second"]
assert result.group_counts == {"ai": 2}
def test_max_items_works_without_category_groups() -> None:
filtering = FilteringConfig(max_items=1)
items = [make_item("lower", 7.0, None), make_item("higher", 9.0, None)]
result = make_orchestrator(filtering).apply_balanced_digest(items)
assert [item.id for item in result.items] == ["higher"]
def test_duplicate_category_warns_and_first_group_wins() -> None:
filtering = FilteringConfig(
category_groups={
"first": CategoryGroupConfig(limit=1, categories=["shared"]),
"second": CategoryGroupConfig(limit=2, categories=["shared"]),
}
)
orchestrator = make_orchestrator(filtering)
result = orchestrator.apply_balanced_digest(
[make_item("top", 9.0, "shared"), make_item("second", 8.0, "shared")]
)
assert [item.id for item in result.items] == ["top"]
assert result.duplicate_categories == ["shared"]
assert "using 'first'" in orchestrator.console.export_text()
@pytest.mark.parametrize(
"kwargs",
[
{"max_items": 0},
{"default_group_limit": 0},
{"category_groups": {"ai": {"limit": 0, "categories": ["ai"]}}},
{"category_groups": {"ai": {"limit": 1, "categories": []}}},
],
)
def test_balanced_digest_config_rejects_non_positive_or_empty_limits(kwargs) -> None:
with pytest.raises(ValidationError):
FilteringConfig(**kwargs)
def test_run_applies_balanced_digest_before_enrichment(tmp_path, monkeypatch) -> None:
config = Config(
ai=AIConfig(
provider="openai",
model="test",
api_key_env="TEST_API_KEY",
languages=[],
),
sources=SourcesConfig(),
filtering=FilteringConfig(
ai_score_threshold=7.0,
max_items=1,
category_groups={
"ai": CategoryGroupConfig(limit=1, categories=["ai"]),
"finance": CategoryGroupConfig(limit=1, categories=["finance"]),
},
),
)
storage = SimpleNamespace()
orchestrator = HorizonOrchestrator(config, storage)
items = [
make_item("ai", 9.0, "ai"),
make_item("finance", 8.0, "finance"),
make_item("below-threshold", 6.0, "ai"),
]
enriched_ids: list[str] = []
async def fetch_all_sources(since): # type: ignore[no-untyped-def]
return items
async def analyze_content(input_items): # type: ignore[no-untyped-def]
return input_items
async def merge_topic_duplicates(input_items): # type: ignore[no-untyped-def]
return input_items
async def expand_twitter_discussion(input_items): # type: ignore[no-untyped-def]
return None
async def enrich_important_items(input_items): # type: ignore[no-untyped-def]
enriched_ids.extend(item.id for item in input_items)
monkeypatch.setattr(orchestrator, "fetch_all_sources", fetch_all_sources)
monkeypatch.setattr(orchestrator, "_analyze_content", analyze_content)
monkeypatch.setattr(orchestrator, "merge_topic_duplicates", merge_topic_duplicates)
monkeypatch.setattr(orchestrator, "_expand_twitter_discussion", expand_twitter_discussion)
monkeypatch.setattr(orchestrator, "_enrich_important_items", enrich_important_items)
monkeypatch.chdir(tmp_path)
asyncio.run(orchestrator.run())
assert enriched_ids == ["ai"]