fix: migrate email template to Odoo 17 syntax; fix BDD LLM mock JSON
PRODUCTION BUG: email template used pre-Odoo-14 Mako syntax (${} and
% for/% if), which Odoo 17 does not render — real notification emails
would show literal '${object.blog_post_id.name}' in the subject.
- subject/email_from: ${...} → {{ ... }} (inline_template engine)
- body_html: add type="qweb"; ${x} → <t t-out="x"/>;
% for → <t t-foreach t-as>; % if → <t t-if>
BDD: when_llm_router_called mocked provider.generate() returning raw
HTML in .text, but LLMRouter._parse_response expects JSON and raised
UserError before ctx['mock_generate'] was set. Now returns valid JSON
with all required fields, and records the mock before generate() runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
687b1bfa0f
commit
2fd8d7fa3b
2 changed files with 52 additions and 30 deletions
|
|
@ -5,42 +5,42 @@
|
|||
<record id="email_template_blog_published" model="mail.template">
|
||||
<field name="name">Blog Publisher — Post Published Notification</field>
|
||||
<field name="model_id" ref="model_itsulu_blog_generation_log"/>
|
||||
<field name="subject">[ITSulu Insights] Blog Post Published: ${object.blog_post_id.name}</field>
|
||||
<field name="email_from">${user.email_formatted}</field>
|
||||
<field name="subject">[ITSulu Insights] Blog Post Published: {{ object.blog_post_id.name }}</field>
|
||||
<field name="email_from">{{ user.email_formatted }}</field>
|
||||
<field name="auto_delete">True</field>
|
||||
<field name="body_html"><![CDATA[
|
||||
<field name="body_html" type="qweb"><![CDATA[
|
||||
<div style="font-family: Arial, sans-serif; line-height: 1.6; max-width: 700px;">
|
||||
<h2>Blog Post Published</h2>
|
||||
<p><strong>Title:</strong> ${object.blog_post_id.name}</p>
|
||||
<p><strong>Blog:</strong> ${object.blog_post_id.blog_id.name}</p>
|
||||
<p><strong>URL:</strong> <a href="https://itsulu.com${object.blog_post_id.website_url}">https://itsulu.com${object.blog_post_id.website_url}</a></p>
|
||||
<p><strong>Title:</strong> <t t-out="object.blog_post_id.name"/></p>
|
||||
<p><strong>Blog:</strong> <t t-out="object.blog_post_id.blog_id.name"/></p>
|
||||
<p><strong>URL:</strong> <a t-attf-href="https://itsulu.com{{ object.blog_post_id.website_url }}">https://itsulu.com<t t-out="object.blog_post_id.website_url"/></a></p>
|
||||
<h3>Social Media Posts — Ready to Post</h3>
|
||||
% for social in object.blog_post_id.itsulu_social_id:
|
||||
% if social.twitter_post_a:
|
||||
<t t-foreach="object.blog_post_id.itsulu_social_id" t-as="social">
|
||||
<t t-if="social.twitter_post_a">
|
||||
<h4>Twitter</h4>
|
||||
<p>${social.twitter_post_a}</p>
|
||||
% endif
|
||||
% if social.twitter_post_b:
|
||||
<p><t t-out="social.twitter_post_a"/></p>
|
||||
</t>
|
||||
<t t-if="social.twitter_post_b">
|
||||
<h4>Twitter</h4>
|
||||
<p>${social.twitter_post_b}</p>
|
||||
% endif
|
||||
% if social.bluesky_post_a:
|
||||
<p><t t-out="social.twitter_post_b"/></p>
|
||||
</t>
|
||||
<t t-if="social.bluesky_post_a">
|
||||
<h4>BlueSky</h4>
|
||||
<p>${social.bluesky_post_a}</p>
|
||||
% endif
|
||||
% if social.bluesky_post_b:
|
||||
<p><t t-out="social.bluesky_post_a"/></p>
|
||||
</t>
|
||||
<t t-if="social.bluesky_post_b">
|
||||
<h4>BlueSky</h4>
|
||||
<p>${social.bluesky_post_b}</p>
|
||||
% endif
|
||||
% if social.mastodon_post:
|
||||
<p><t t-out="social.bluesky_post_b"/></p>
|
||||
</t>
|
||||
<t t-if="social.mastodon_post">
|
||||
<h4>Mastodon</h4>
|
||||
<p>${social.mastodon_post}</p>
|
||||
% endif
|
||||
% if social.linkedin_post:
|
||||
<p><t t-out="social.mastodon_post"/></p>
|
||||
</t>
|
||||
<t t-if="social.linkedin_post">
|
||||
<h4>LinkedIn</h4>
|
||||
<p>${social.linkedin_post}</p>
|
||||
% endif
|
||||
% endfor
|
||||
<p><t t-out="social.linkedin_post"/></p>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
]]></field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -226,10 +226,31 @@ def given_ollama_base_url(odoo_env, url):
|
|||
)
|
||||
|
||||
|
||||
def _valid_llm_json():
|
||||
"""Return a JSON string with all fields LLMRouter._parse_response requires."""
|
||||
import json
|
||||
return json.dumps({
|
||||
'title': 'Generated Blog Post',
|
||||
'body_html': '<p>' + ('Generated content. ' * 30) + '</p>',
|
||||
'meta_title': 'Generated SEO Title',
|
||||
'meta_description': 'Generated meta description for the post.',
|
||||
'meta_keywords': 'ai, testing, blog',
|
||||
'tags': ['AI', 'Testing'],
|
||||
'social': {
|
||||
'twitter_a': 'Tweet A', 'twitter_b': 'Tweet B',
|
||||
'bluesky_a': 'BlueSky A', 'bluesky_b': 'BlueSky B',
|
||||
'mastodon': 'Mastodon post', 'linkedin': 'LinkedIn post',
|
||||
},
|
||||
'sources': [],
|
||||
})
|
||||
|
||||
|
||||
@when('the LLM router is called with a prompt')
|
||||
def when_llm_router_called(odoo_env, ctx):
|
||||
from odoo.addons.itsulu_blog_publisher.services.llm_router import LLMRouter
|
||||
mock_resp = MagicMock(text='<p>Generated</p>', tokens_used=500)
|
||||
# The router parses provider_response.text as JSON, so the mock must
|
||||
# return a valid JSON blob with all required fields — not raw HTML.
|
||||
mock_resp = MagicMock(text=_valid_llm_json(), tokens_used=500)
|
||||
|
||||
provider_map = {
|
||||
'anthropic': 'odoo.addons.itsulu_blog_publisher.services.anthropic_provider.AnthropicProvider.generate',
|
||||
|
|
@ -243,10 +264,11 @@ def when_llm_router_called(odoo_env, ctx):
|
|||
try:
|
||||
if patch_target:
|
||||
with patch(patch_target, return_value=mock_resp) as mock_gen:
|
||||
router = LLMRouter(odoo_env, provider=provider, model=ctx.get('model', ''))
|
||||
result = router.generate(topic='Write a blog post')
|
||||
ctx['result'] = result
|
||||
# Record the mock before generate() so assertions can see it
|
||||
# even if generate() raises.
|
||||
ctx['mock_generate'] = mock_gen
|
||||
router = LLMRouter(odoo_env, provider=provider, model=ctx.get('model', ''))
|
||||
ctx['result'] = router.generate(topic='Write a blog post')
|
||||
else:
|
||||
router = LLMRouter(odoo_env, provider=provider, model=ctx.get('model', ''))
|
||||
ctx['result'] = router.generate(topic='Write a blog post')
|
||||
|
|
|
|||
Loading…
Reference in a new issue