fix: update test_llm_router to use topic parameter and valid JSON mocks

This commit is contained in:
Nicholas Riegel 2026-05-30 00:27:48 -04:00
parent f26f896fe4
commit 02318799bb

View file

@ -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. RED PHASE all tests here FAIL until llm_router and provider services exist.
""" """
import json
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
from odoo.tests import TransactionCase, tagged from odoo.tests import TransactionCase, tagged
from odoo.exceptions import UserError from odoo.exceptions import UserError
from .factories import BlogPublisherFactory 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": "<h1>Test</h1><p>Content here.</p>",
"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') @tagged('post_install', '-at_install', 'itsulu_blog_publisher', 'llm_router')
class TestLLMRouterProviderDispatch(TransactionCase): class TestLLMRouterProviderDispatch(TransactionCase):
"""Verify that the LLM router dispatches to the correct backend provider.""" """Verify that the LLM router dispatches to the correct backend provider."""
@ -46,9 +72,7 @@ class TestLLMRouterProviderDispatch(TransactionCase):
""" """
# ARRANGE # ARRANGE
from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter
mock_response = MagicMock() mock_response = _make_mock_llm_response(tokens_used=800)
mock_response.text = '<h1>AI Blog Post</h1><p>Content here...</p>'
mock_response.tokens_used = 800
with patch( with patch(
'odoo.addons.itsulu_blog_publisher.services.anthropic_provider' 'odoo.addons.itsulu_blog_publisher.services.anthropic_provider'
@ -57,18 +81,16 @@ class TestLLMRouterProviderDispatch(TransactionCase):
) as mock_generate: ) as mock_generate:
# ACT # ACT
router = LLMRouter(self.env, provider='anthropic', model='claude-sonnet-4-20250514') 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 # ASSERT
mock_generate.assert_called_once() 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): def test_openai_provider_calls_openai_endpoint(self):
"""Router with provider='openai' delegates to OpenAIProvider.generate().""" """Router with provider='openai' delegates to OpenAIProvider.generate()."""
from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter
mock_response = MagicMock() mock_response = _make_mock_llm_response(tokens_used=600)
mock_response.text = '<p>OpenAI generated content</p>'
mock_response.tokens_used = 600
with patch( with patch(
'odoo.addons.itsulu_blog_publisher.services.openai_provider' 'odoo.addons.itsulu_blog_publisher.services.openai_provider'
@ -76,17 +98,15 @@ class TestLLMRouterProviderDispatch(TransactionCase):
return_value=mock_response, return_value=mock_response,
) as mock_generate: ) as mock_generate:
router = LLMRouter(self.env, provider='openai', model='gpt-4o') 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() mock_generate.assert_called_once()
self.assertTrue(result.text) self.assertTrue(result.body_html)
def test_gemini_provider_calls_gemini_endpoint(self): def test_gemini_provider_calls_gemini_endpoint(self):
"""Router with provider='gemini' delegates to GeminiProvider.generate().""" """Router with provider='gemini' delegates to GeminiProvider.generate()."""
from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter
mock_response = MagicMock() mock_response = _make_mock_llm_response(tokens_used=700)
mock_response.text = '<p>Gemini generated content</p>'
mock_response.tokens_used = 700
with patch( with patch(
'odoo.addons.itsulu_blog_publisher.services.gemini_provider' 'odoo.addons.itsulu_blog_publisher.services.gemini_provider'
@ -94,17 +114,15 @@ class TestLLMRouterProviderDispatch(TransactionCase):
return_value=mock_response, return_value=mock_response,
) as mock_generate: ) as mock_generate:
router = LLMRouter(self.env, provider='gemini', model='gemini-2.0-flash') 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() mock_generate.assert_called_once()
self.assertTrue(result.text) self.assertTrue(result.body_html)
def test_ollama_provider_calls_local_api_endpoint(self): def test_ollama_provider_calls_local_api_endpoint(self):
"""Router with provider='ollama' delegates to OllamaProvider.generate().""" """Router with provider='ollama' delegates to OllamaProvider.generate()."""
from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter
mock_response = MagicMock() mock_response = _make_mock_llm_response(tokens_used=500)
mock_response.text = '<p>Mistral generated content</p>'
mock_response.tokens_used = 500
with patch( with patch(
'odoo.addons.itsulu_blog_publisher.services.ollama_provider' 'odoo.addons.itsulu_blog_publisher.services.ollama_provider'
@ -112,10 +130,10 @@ class TestLLMRouterProviderDispatch(TransactionCase):
return_value=mock_response, return_value=mock_response,
) as mock_generate: ) as mock_generate:
router = LLMRouter(self.env, provider='ollama', model='mistral') 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() mock_generate.assert_called_once()
self.assertTrue(result.text) self.assertTrue(result.body_html)
def test_unknown_provider_raises_user_error(self): def test_unknown_provider_raises_user_error(self):
""" """
@ -126,9 +144,9 @@ class TestLLMRouterProviderDispatch(TransactionCase):
with self.assertRaises(UserError) as ctx: with self.assertRaises(UserError) as ctx:
router = LLMRouter(self.env, provider='unknown_provider', model='some-model') 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): def test_missing_api_key_raises_user_error(self):
"""Router raises UserError when required API key param is absent.""" """Router raises UserError when required API key param is absent."""
@ -139,7 +157,7 @@ class TestLLMRouterProviderDispatch(TransactionCase):
) )
with self.assertRaises(UserError) as ctx: with self.assertRaises(UserError) as ctx:
router = LLMRouter(self.env, provider='anthropic', model='claude-sonnet-4-20250514') 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()) self.assertIn('api key', str(ctx.exception).lower())
# Restore for other tests # Restore for other tests
@ -166,9 +184,7 @@ class TestLLMRouterTokenLogging(TransactionCase):
tokens_used as a positive integer when the API returns usage data. tokens_used as a positive integer when the API returns usage data.
""" """
from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter
mock_response = MagicMock() mock_response = _make_mock_llm_response(tokens_used=1234)
mock_response.text = '<p>Content</p>'
mock_response.tokens_used = 1234
with patch( with patch(
'odoo.addons.itsulu_blog_publisher.services.anthropic_provider' 'odoo.addons.itsulu_blog_publisher.services.anthropic_provider'
@ -176,7 +192,7 @@ class TestLLMRouterTokenLogging(TransactionCase):
return_value=mock_response, return_value=mock_response,
): ):
router = LLMRouter(self.env, provider='anthropic', model='claude-sonnet-4-20250514') 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.assertGreater(result.tokens_used, 0)
self.assertEqual(result.tokens_used, 1234) self.assertEqual(result.tokens_used, 1234)