# addons/itsulu_blog_publisher/tests/test_blog_generation_log.py """ Tests for models/blog_generation_log.py Behaviour: every generation attempt (success or failure) is recorded with full metadata; failed logs expose a Retry action. RED PHASE — all tests FAIL until itsulu.blog.generation.log model exists. """ from odoo.tests import TransactionCase, tagged from .factories import BlogPublisherFactory @tagged('post_install', '-at_install', 'itsulu_blog_publisher', 'generation_log') class TestBlogGenerationLogCreation(TransactionCase): """Verify that generation log records capture the correct metadata.""" def setUp(self): super().setUp() self.factory = BlogPublisherFactory(self.env) self.blog = self.factory.blog(name='ITSulu Insights') def test_successful_log_record_is_created_with_correct_fields(self): """ 1. Behaviour: a success log captures provider, model, tokens, duration, post link 2. ARRANGE: a blog post and a log factory call 3. ACT: create the log 4. ASSERT: all fields populated, state='success' 5. FAIL reason: model does not exist """ # ARRANGE blog_post = self.factory.blog_post(blog=self.blog) # ACT log = self.factory.generation_log( blog_post=blog_post, state='success', llm_provider='anthropic', llm_model='claude-sonnet-4-20250514', tokens_used=1500, duration_seconds=11.7, trigger_source='manual', ) # ASSERT self.assertEqual(log.state, 'success') self.assertEqual(log.llm_provider, 'anthropic') self.assertEqual(log.llm_model, 'claude-sonnet-4-20250514') self.assertEqual(log.tokens_used, 1500) self.assertAlmostEqual(log.duration_seconds, 11.7, places=1) self.assertEqual(log.blog_post_id.id, blog_post.id) self.assertEqual(log.trigger_source, 'manual') def test_error_log_record_stores_human_readable_error_message(self): """Error logs must store the full error message for operator diagnosis.""" # ARRANGE + ACT log = self.factory.generation_log( state='error', error_message='Anthropic API returned 401 Unauthorized — check API key.', blog_post_id=False, ) # ASSERT self.assertEqual(log.state, 'error') self.assertIn('401', log.error_message) def test_error_log_has_no_linked_blog_post(self): """When generation fails, no blog post is created, so log.blog_post_id is False.""" # ACT log = self.factory.generation_log(state='error', blog_post_id=False) # ASSERT self.assertFalse(log.blog_post_id) def test_log_trigger_source_can_be_scheduled(self): """Logs triggered by a cron job record trigger_source='scheduled'.""" # ACT log = self.factory.generation_log(trigger_source='scheduled', state='success') # ASSERT self.assertEqual(log.trigger_source, 'scheduled') def test_log_trigger_source_records_slot_name_for_scheduled_runs(self): """Scheduled logs record which slot triggered them (morning/afternoon/evening).""" # ACT log = self.factory.generation_log( trigger_source='scheduled', schedule_slot='morning', state='success', ) # ASSERT self.assertEqual(log.schedule_slot, 'morning') def test_tokens_used_defaults_to_zero_for_error_logs(self): """Error logs that never reached the LLM should have tokens_used=0.""" # ACT log = self.factory.generation_log(state='error', tokens_used=0) # ASSERT self.assertEqual(log.tokens_used, 0) @tagged('post_install', '-at_install', 'itsulu_blog_publisher', 'generation_log') class TestBlogGenerationLogRetry(TransactionCase): """Verify that failed logs expose a working Retry action.""" def setUp(self): super().setUp() self.factory = BlogPublisherFactory(self.env) def test_error_log_action_retry_returns_wizard_action(self): """ Calling action_retry() on an error log must return an ir.actions.act_window or similar action dict so the UI can open the Generate Now wizard. """ # ARRANGE log = self.factory.generation_log( state='error', error_message='Timeout', ) # ACT result = log.action_retry() # ASSERT self.assertIsInstance(result, dict, "action_retry() must return an action dict") self.assertIn(result.get('type', ''), [ 'ir.actions.act_window', 'ir.actions.client', 'ir.actions.server', ]) def test_success_log_does_not_expose_retry_button(self): """ A success log should NOT have action_retry() callable (or it should raise, or return False — not open a wizard). This prevents accidental duplicate generation. """ # ARRANGE blog_post = self.factory.blog_post() log = self.factory.generation_log(state='success', blog_post=blog_post) # ASSERT — retry on success log should either not exist or be a no-op if hasattr(log, 'action_retry'): result = log.action_retry() self.assertFalse(result, "action_retry() on a success log must return False or no-op")