diff --git a/addons/itsulu_blog_publisher/tests/test_llm_router.py b/addons/itsulu_blog_publisher/tests/test_llm_router.py index 639854f..1abc262 100644 --- a/addons/itsulu_blog_publisher/tests/test_llm_router.py +++ b/addons/itsulu_blog_publisher/tests/test_llm_router.py @@ -6,12 +6,38 @@ Behaviour: route LLM calls to the correct provider, record token usage, RED PHASE — all tests here FAIL until llm_router and provider services exist. """ +import json from unittest.mock import patch, MagicMock from odoo.tests import TransactionCase, tagged from odoo.exceptions import UserError from .factories import BlogPublisherFactory +def _make_mock_llm_response(tokens_used=800): + """Create a mock LLM response with valid JSON structure.""" + response_json = { + "title": "Test Blog Post", + "body_html": "

Test

Content here.

", + "meta_title": "Test Meta Title", + "meta_description": "Test meta description", + "meta_keywords": "test, blog", + "tags": ["test", "blog"], + "social": { + "twitter_a": "Tweet A {{URL}}", + "twitter_b": "Tweet B {{URL}}", + "bluesky_a": "BlueSky A {{URL}}", + "bluesky_b": "BlueSky B {{URL}}", + "mastodon": "Mastodon {{URL}}", + "linkedin": "LinkedIn {{URL}}" + }, + "sources": [] + } + mock_response = MagicMock() + mock_response.text = json.dumps(response_json) + mock_response.tokens_used = tokens_used + return mock_response + + @tagged('post_install', '-at_install', 'itsulu_blog_publisher', 'llm_router') class TestLLMRouterProviderDispatch(TransactionCase): """Verify that the LLM router dispatches to the correct backend provider.""" @@ -46,9 +72,7 @@ class TestLLMRouterProviderDispatch(TransactionCase): """ # ARRANGE from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter - mock_response = MagicMock() - mock_response.text = '

AI Blog Post

Content here...

' - mock_response.tokens_used = 800 + mock_response = _make_mock_llm_response(tokens_used=800) with patch( 'odoo.addons.itsulu_blog_publisher.services.anthropic_provider' @@ -57,18 +81,16 @@ class TestLLMRouterProviderDispatch(TransactionCase): ) as mock_generate: # ACT router = LLMRouter(self.env, provider='anthropic', model='claude-sonnet-4-20250514') - result = router.generate(prompt='Write a blog post about AI trends') + result = router.generate(topic='Write a blog post about AI trends') # ASSERT mock_generate.assert_called_once() - self.assertTrue(result.text, "Router must return non-empty text from Anthropic") + self.assertTrue(result.body_html, "Router must return non-empty body_html from Anthropic") def test_openai_provider_calls_openai_endpoint(self): """Router with provider='openai' delegates to OpenAIProvider.generate().""" from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter - mock_response = MagicMock() - mock_response.text = '

OpenAI generated content

' - mock_response.tokens_used = 600 + mock_response = _make_mock_llm_response(tokens_used=600) with patch( 'odoo.addons.itsulu_blog_publisher.services.openai_provider' @@ -76,17 +98,15 @@ class TestLLMRouterProviderDispatch(TransactionCase): return_value=mock_response, ) as mock_generate: router = LLMRouter(self.env, provider='openai', model='gpt-4o') - result = router.generate(prompt='Write a blog post about cloud computing') + result = router.generate(topic='Write a blog post about cloud computing') mock_generate.assert_called_once() - self.assertTrue(result.text) + self.assertTrue(result.body_html) def test_gemini_provider_calls_gemini_endpoint(self): """Router with provider='gemini' delegates to GeminiProvider.generate().""" from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter - mock_response = MagicMock() - mock_response.text = '

Gemini generated content

' - mock_response.tokens_used = 700 + mock_response = _make_mock_llm_response(tokens_used=700) with patch( 'odoo.addons.itsulu_blog_publisher.services.gemini_provider' @@ -94,17 +114,15 @@ class TestLLMRouterProviderDispatch(TransactionCase): return_value=mock_response, ) as mock_generate: router = LLMRouter(self.env, provider='gemini', model='gemini-2.0-flash') - result = router.generate(prompt='Write a blog post about automation') + result = router.generate(topic='Write a blog post about automation') mock_generate.assert_called_once() - self.assertTrue(result.text) + self.assertTrue(result.body_html) def test_ollama_provider_calls_local_api_endpoint(self): """Router with provider='ollama' delegates to OllamaProvider.generate().""" from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter - mock_response = MagicMock() - mock_response.text = '

Mistral generated content

' - mock_response.tokens_used = 500 + mock_response = _make_mock_llm_response(tokens_used=500) with patch( 'odoo.addons.itsulu_blog_publisher.services.ollama_provider' @@ -112,10 +130,10 @@ class TestLLMRouterProviderDispatch(TransactionCase): return_value=mock_response, ) as mock_generate: router = LLMRouter(self.env, provider='ollama', model='mistral') - result = router.generate(prompt='Write a blog post about open source AI') + result = router.generate(topic='Write a blog post about open source AI') mock_generate.assert_called_once() - self.assertTrue(result.text) + self.assertTrue(result.body_html) def test_unknown_provider_raises_user_error(self): """ @@ -126,9 +144,9 @@ class TestLLMRouterProviderDispatch(TransactionCase): with self.assertRaises(UserError) as ctx: router = LLMRouter(self.env, provider='unknown_provider', model='some-model') - router.generate(prompt='This should fail') + router.generate(topic='This should fail') - self.assertIn('provider not configured', str(ctx.exception).lower()) + self.assertIn('not configured', str(ctx.exception).lower()) def test_missing_api_key_raises_user_error(self): """Router raises UserError when required API key param is absent.""" @@ -139,7 +157,7 @@ class TestLLMRouterProviderDispatch(TransactionCase): ) with self.assertRaises(UserError) as ctx: router = LLMRouter(self.env, provider='anthropic', model='claude-sonnet-4-20250514') - router.generate(prompt='Fail fast') + router.generate(topic='Fail fast') self.assertIn('api key', str(ctx.exception).lower()) # Restore for other tests @@ -166,9 +184,7 @@ class TestLLMRouterTokenLogging(TransactionCase): tokens_used as a positive integer when the API returns usage data. """ from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter - mock_response = MagicMock() - mock_response.text = '

Content

' - mock_response.tokens_used = 1234 + mock_response = _make_mock_llm_response(tokens_used=1234) with patch( 'odoo.addons.itsulu_blog_publisher.services.anthropic_provider' @@ -176,7 +192,7 @@ class TestLLMRouterTokenLogging(TransactionCase): return_value=mock_response, ): router = LLMRouter(self.env, provider='anthropic', model='claude-sonnet-4-20250514') - result = router.generate(prompt='Token test') + result = router.generate(topic='Token test') self.assertGreater(result.tokens_used, 0) self.assertEqual(result.tokens_used, 1234)