0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-07 13:19:19 +08:00
discourse/plugins/discourse-ai/spec/jobs/regular/detect_translate_post_spec.rb
Natalie Tay 9349e06001
FEATURE: Add category scope for AI translations (#41585)
We have switched AI translations settings first from public categories,
to "include" categories, then to "exclude" categories.

This is not great. Some admins prefer one, and some prefer others. 

This PR allows admins to do either. This also updates default categories
to be "public" only, when the original default was "all" (public +
private).

The migration covers moving existing users to 
- if has selected categories -> "exclude strict" + their selected
categories
- if no selected categories + feature enabled -> "all" to maintain
status quo



https://github.com/user-attachments/assets/607f2340-8fa6-4059-b13d-07d1938ad74f
2026-07-10 09:39:20 +08:00

267 lines
8.8 KiB
Ruby
Vendored

# frozen_string_literal: true
describe Jobs::DetectTranslatePost do
subject(:job) { described_class.new }
fab!(:post)
let(:locales) { %w[en ja] }
before do
assign_fake_provider_to(:ai_default_llm_model)
# fake provider (Completions::Endpoints::Fake) returns translated text that includes this svg
stub_request(:get, "https://meta.discourse.org/images/discourse-logo.svg").to_return(
status: 200,
body: "",
)
enable_current_plugin
SiteSetting.ai_translation_enabled = true
SiteSetting.content_localization_supported_locales = locales.join("|")
SiteSetting.ai_translation_category_scope = "all"
SiteSetting.ai_translation_categories = ""
end
it "does nothing when translator is disabled" do
SiteSetting.discourse_ai_enabled = false
DiscourseAi::Translation::PostLocaleDetector.expects(:detect_locale).never
DiscourseAi::Translation::PostLocalizer.expects(:localize).never
job.execute({ post_id: post.id })
end
it "does nothing when content translation is disabled" do
SiteSetting.ai_translation_enabled = false
DiscourseAi::Translation::PostLocaleDetector.expects(:detect_locale).never
DiscourseAi::Translation::PostLocalizer.expects(:localize).never
job.execute({ post_id: post.id })
end
it "skips translation when credits are unavailable" do
DiscourseAi::Translation.expects(:credits_available_for_post_detection?).returns(false)
DiscourseAi::Translation::PostLocaleDetector.expects(:detect_locale).never
DiscourseAi::Translation::PostLocalizer.expects(:localize).never
job.execute({ post_id: post.id })
end
it "detects locale" do
allow(DiscourseAi::Translation::PostLocaleDetector).to receive(:detect_locale).with(
post,
).and_return("zh_CN")
job.execute({ post_id: post.id })
end
it "skips locale detection when post has a locale" do
post.update!(locale: "en")
DiscourseAi::Translation::PostLocaleDetector.expects(:detect_locale).with(post).never
job.execute({ post_id: post.id })
end
it "skips bot posts by default" do
post.update!(user: Discourse.system_user)
DiscourseAi::Translation::PostLocaleDetector.expects(:detect_locale).never
DiscourseAi::Translation::PostLocalizer.expects(:localize).never
job.execute({ post_id: post.id })
end
it "translates bot posts when force is true" do
post.update!(user: Discourse.system_user)
DiscourseAi::Translation::PostLocaleDetector.expects(:detect_locale).once
job.execute({ post_id: post.id, force: true })
end
it "translates bot posts when ai_translation_include_bot_content is true" do
SiteSetting.ai_translation_include_bot_content = true
post.update!(user: Discourse.system_user)
DiscourseAi::Translation::PostLocaleDetector.expects(:detect_locale).once
job.execute({ post_id: post.id })
end
it "skips locale detection when no target languages are configured" do
SiteSetting.content_localization_supported_locales = ""
DiscourseAi::Translation::PostLocaleDetector.expects(:detect_locale).never
DiscourseAi::Translation::PostLocalizer.expects(:localize).never
job.execute({ post_id: post.id })
end
it "skips translating to the post's language" do
post.update(locale: "en")
DiscourseAi::Translation::PostLocalizer.expects(:localize).with(post, "en").never
DiscourseAi::Translation::PostLocalizer.expects(:localize).with(post, "ja").once
job.execute({ post_id: post.id })
end
context "when translation exists and retranslation quota hit" do
before do
DiscourseAi::Translation::PostLocalizer
.expects(:has_relocalize_quota?)
.with(post, "ja")
.returns(false)
end
it "skips translating if the post is already localized" do
post.update(locale: "en")
Fabricate(:post_localization, post:, locale: "ja")
DiscourseAi::Translation::PostLocalizer.expects(:localize).never
job.execute({ post_id: post.id })
end
it "does not translate to language of similar variant" do
post.update(locale: "en_GB")
Fabricate(:post_localization, post: post, locale: "ja_JP")
DiscourseAi::Translation::PostLocalizer.expects(:localize).never
job.execute({ post_id: post.id })
end
it "translates if force is true" do
post.update(locale: "en")
Fabricate(:post_localization, post:, locale: "ja")
DiscourseAi::Translation::PostLocalizer.expects(:localize).with(post, "ja").once
job.execute({ post_id: post.id, force: true })
end
end
it "handles translation errors gracefully" do
post.update(locale: "en")
DiscourseAi::Translation::PostLocalizer.expects(:localize).raises(
StandardError.new("API error"),
)
expect { job.execute({ post_id: post.id }) }.not_to raise_error
end
describe "with category scope and PM scope" do
fab!(:included_category, :category)
fab!(:excluded_category, :category)
fab!(:included_topic) { Fabricate(:topic, category: included_category) }
fab!(:excluded_topic) { Fabricate(:topic, category: excluded_category) }
fab!(:included_post) { Fabricate(:post, topic: included_topic) }
fab!(:excluded_post) { Fabricate(:post, topic: excluded_topic) }
fab!(:personal_pm_topic, :private_message_topic)
fab!(:personal_pm_post) { Fabricate(:post, topic: personal_pm_topic) }
fab!(:group_pm_topic) do
Fabricate(:group_private_message_topic, recipient_group: Fabricate(:group))
end
fab!(:group_pm_post) { Fabricate(:post, topic: group_pm_topic) }
before do
SiteSetting.ai_translation_category_scope = "exclude"
SiteSetting.ai_translation_categories = excluded_category.id.to_s
end
it "skips posts outside the category scope" do
DiscourseAi::Translation::PostLocaleDetector.expects(:detect_locale).with(excluded_post).never
job.execute({ post_id: excluded_post.id })
end
it "processes posts in included categories" do
DiscourseAi::Translation::PostLocaleDetector.expects(:detect_locale).with(included_post).once
job.execute({ post_id: included_post.id })
end
it "processes posts from selected subcategories" do
subcategory = Fabricate(:category, parent_category: included_category)
subcategory_post = Fabricate(:post, topic: Fabricate(:topic, category: subcategory))
SiteSetting.ai_translation_category_scope = "include"
SiteSetting.ai_translation_categories = included_category.id.to_s
DiscourseAi::Translation::PostLocaleDetector
.expects(:detect_locale)
.with(subcategory_post)
.once
job.execute({ post_id: subcategory_post.id })
end
context "when pm_translation_scope is none" do
before { SiteSetting.ai_translation_personal_messages = "none" }
it "skips all PMs" do
DiscourseAi::Translation::PostLocaleDetector
.expects(:detect_locale)
.with(personal_pm_post)
.never
job.execute({ post_id: personal_pm_post.id })
DiscourseAi::Translation::PostLocaleDetector
.expects(:detect_locale)
.with(group_pm_post)
.never
job.execute({ post_id: group_pm_post.id })
end
end
context "when pm_translation_scope is group" do
before { SiteSetting.ai_translation_personal_messages = "group" }
it "processes group PMs but skips personal PMs" do
DiscourseAi::Translation::PostLocaleDetector
.expects(:detect_locale)
.with(group_pm_post)
.once
job.execute({ post_id: group_pm_post.id })
DiscourseAi::Translation::PostLocaleDetector
.expects(:detect_locale)
.with(personal_pm_post)
.never
job.execute({ post_id: personal_pm_post.id })
end
end
context "when pm_translation_scope is all" do
before { SiteSetting.ai_translation_personal_messages = "all" }
it "processes all PMs" do
DiscourseAi::Translation::PostLocaleDetector
.expects(:detect_locale)
.with(group_pm_post)
.once
job.execute({ post_id: group_pm_post.id })
DiscourseAi::Translation::PostLocaleDetector
.expects(:detect_locale)
.with(personal_pm_post)
.once
job.execute({ post_id: personal_pm_post.id })
end
end
describe "force arg" do
it "processes private content when force is true" do
DiscourseAi::Translation::PostLocaleDetector
.expects(:detect_locale)
.with(group_pm_post)
.once
job.execute({ post_id: group_pm_post.id, force: true })
end
it "processes PM content when force is true" do
DiscourseAi::Translation::PostLocaleDetector
.expects(:detect_locale)
.with(personal_pm_post)
.once
job.execute({ post_id: personal_pm_post.id, force: true })
end
end
end
end