107 lines
3.3 KiB
Python
107 lines
3.3 KiB
Python
"""Tests for steward.proposals.generator."""
|
|
|
|
from datetime import UTC
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import httpx
|
|
import pytest
|
|
import respx
|
|
|
|
from steward.config import Settings
|
|
from steward.llm.client import LLMClient
|
|
from steward.proposals.generator import Proposal, ProposalGenerator
|
|
|
|
|
|
def _make_settings(**kwargs) -> Settings:
|
|
defaults = dict(
|
|
openai_api_key="test-key",
|
|
analysis_target_url="https://example.com/api/status",
|
|
analysis_target_api_key="secret",
|
|
)
|
|
defaults.update(kwargs)
|
|
return Settings(**defaults)
|
|
|
|
|
|
@pytest.fixture
|
|
def settings():
|
|
return _make_settings()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_llm():
|
|
llm = MagicMock(spec=LLMClient)
|
|
llm.chat = AsyncMock(
|
|
return_value="**Summary:** Everything is healthy.\n**Why:** No issues detected."
|
|
)
|
|
return llm
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_generates_proposal(settings, mock_llm):
|
|
"""run() should fetch the URL and return a Proposal."""
|
|
with respx.mock:
|
|
respx.get("https://example.com/api/status").mock(
|
|
return_value=httpx.Response(200, text='{"status": "ok"}')
|
|
)
|
|
generator = ProposalGenerator(settings, mock_llm)
|
|
proposal = await generator.run()
|
|
|
|
assert proposal is not None
|
|
assert isinstance(proposal, Proposal)
|
|
assert proposal.source_url == "https://example.com/api/status"
|
|
assert proposal.raw_data == '{"status": "ok"}'
|
|
mock_llm.chat.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_no_url_returns_none(mock_llm):
|
|
"""run() should return None when analysis_target_url is not configured."""
|
|
settings = _make_settings(analysis_target_url="")
|
|
generator = ProposalGenerator(settings, mock_llm)
|
|
result = await generator.run()
|
|
assert result is None
|
|
mock_llm.chat.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_http_error_returns_none(settings, mock_llm):
|
|
"""run() should return None when the HTTP request fails."""
|
|
with respx.mock:
|
|
respx.get("https://example.com/api/status").mock(
|
|
return_value=httpx.Response(500, text="Internal Server Error")
|
|
)
|
|
generator = ProposalGenerator(settings, mock_llm)
|
|
proposal = await generator.run()
|
|
|
|
assert proposal is None
|
|
mock_llm.chat.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fetch_sends_bearer_token(settings, mock_llm):
|
|
"""_fetch() should include ****** when api_key is configured."""
|
|
with respx.mock:
|
|
route = respx.get("https://example.com/api/status").mock(
|
|
return_value=httpx.Response(200, text="data")
|
|
)
|
|
generator = ProposalGenerator(settings, mock_llm)
|
|
await generator._fetch("https://example.com/api/status")
|
|
|
|
request = route.calls.last.request
|
|
assert request.headers["Authorization"].startswith("Bearer ")
|
|
|
|
|
|
def test_proposal_format_for_telegram():
|
|
"""format_for_telegram() should contain the source URL and body."""
|
|
from datetime import datetime
|
|
p = Proposal(
|
|
title="Test",
|
|
body="**Summary:** All good.",
|
|
source_url="https://example.com",
|
|
generated_at=datetime(2026, 1, 1, 8, 0, 0, tzinfo=UTC),
|
|
)
|
|
text = p.format_for_telegram()
|
|
assert "https://example.com" in text
|
|
assert "All good." in text
|
|
assert "2026-01-01" in text
|