steward_mirror/tests/test_proposals.py
Daniel Wagner 070e967833 test: update test suite for OmegaConf config system
- Add tests/conftest.py with make_settings() helper that maps legacy config
  parameter names to new nested OmegaConf structure
- Update all test files to use the conftest fixture
- All 73 tests now pass with new config system
- Maintains backward compatibility via Settings class properties

Also add AGENTS.md with comprehensive AI agent guidelines:
- Project overview and key technologies
- Directory structure reference
- Development workflow and common tasks
- Testing strategy and patterns
- CI/CD pipeline overview
- Common pitfalls and best practices
- Debugging guide for agents working on the project
2026-07-26 12:48:40 +10:00

110 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
from tests.conftest import make_settings
def _make_settings(**kwargs) -> Settings:
"""Wrapper for test compatibility."""
defaults = dict(
openai_api_key="test-key",
analysis_target_url="https://example.com/api/status",
analysis_target_api_key="secret",
)
defaults.update(kwargs)
return make_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 an Authorization header 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 p.source_url in text
assert "All good." in text
assert "2026-01-01" in text