feat: add NORTH_STAR.md and MVP core loop (Telegram bot + LLM + proposal generator)
This commit is contained in:
committed by
GitHub
parent
c071fe6bf7
commit
6862c7d0b8
@@ -0,0 +1,106 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user