- 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
110 lines
3.3 KiB
Python
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
|