discourse/plugins/discourse-ai/spec/requests/admin/ai_translations_controller_spec.rb
Penar Musaraj 90baea1ea7
FEATURE: Switch from opt-in to opt-out for categories in AI translations (#40169)
This PR changes Discourse AI translations from an opt-in category model
to an opt-out model: instead of translating only selected
`ai_translation_target_categories`, it introduces
`ai_translation_excluded_categories`, updates the admin UI copy and save
flow, changes topic/post/category candidate selection and detection jobs
to translate all non-excluded categories by default, and adds a
migration that converts existing target-category settings into the
equivalent excluded-category list for existing sites.

It also updates all related specs.

---------

Co-authored-by: discourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-05-26 14:51:04 -04:00

393 lines
12 KiB
Ruby
Vendored

# frozen_string_literal: true
describe DiscourseAi::Admin::AiTranslationsController do
fab!(:admin)
fab!(:user)
before do
enable_current_plugin
assign_fake_provider_to(:ai_default_llm_model)
end
describe "#show" do
context "when logged in as admin" do
fab!(:excluded_category, :category)
before do
sign_in(admin)
SiteSetting.discourse_ai_enabled = true
SiteSetting.ai_translation_enabled = true
SiteSetting.content_localization_supported_locales = "en|fr|es"
SiteSetting.ai_translation_excluded_categories = excluded_category.id.to_s
end
it "returns base configuration data without progress" do
SiteSetting.ai_translation_backfill_max_age_days = 30
SiteSetting.ai_translation_backfill_hourly_rate = 100
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["translation_id"]).to eq(DiscourseAi::Configuration::Module::TRANSLATION_ID)
expect(json["enabled"]).to eq(true)
expect(json["translation_enabled"]).to eq(true)
expect(json["hourly_rate"]).to eq(100)
expect(json["backfill_enabled"]).to be_in([true, false])
expect(json).not_to have_key("translation_progress")
expect(json).not_to have_key("total")
expect(json).not_to have_key("posts_with_detected_locale")
end
it "returns no_locales_configured when no locales are supported" do
SiteSetting.content_localization_supported_locales = ""
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["no_locales_configured"]).to eq(true)
end
it "does not include no_locales_configured when locales are set" do
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json).not_to have_key("no_locales_configured")
end
it "returns translation_enabled field" do
SiteSetting.ai_translation_backfill_max_age_days = 30
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["translation_enabled"]).to eq(true)
SiteSetting.ai_translation_enabled = false
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["translation_enabled"]).to eq(false)
end
it "correctly indicates if backfill is enabled" do
SiteSetting.ai_translation_backfill_hourly_rate = 30
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(200)
expect(response.parsed_body["backfill_enabled"]).to eq(true)
SiteSetting.ai_translation_backfill_hourly_rate = 0
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(200)
expect(response.parsed_body["backfill_enabled"]).to eq(false)
end
it "returns excluded_category_ids from ai_translation_excluded_categories" do
other_category = Fabricate(:category)
SiteSetting.ai_translation_excluded_categories =
"#{excluded_category.id}|#{other_category.id}"
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(200)
expect(response.parsed_body["excluded_category_ids"]).to contain_exactly(
excluded_category.id,
other_category.id,
)
end
it "returns an empty array when no excluded categories are configured" do
SiteSetting.ai_translation_excluded_categories = ""
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(200)
expect(response.parsed_body["excluded_category_ids"]).to eq([])
end
it "correctly indicates if feature is enabled" do
SiteSetting.ai_translation_backfill_max_age_days = 30
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(200)
expect(response.parsed_body["enabled"]).to eq(true)
SiteSetting.ai_translation_backfill_max_age_days = 0
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(200)
expect(response.parsed_body["enabled"]).to eq(false)
end
end
context "when not logged in as admin" do
it "returns 404 for anonymous users" do
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(404)
end
it "returns 404 for regular users" do
sign_in(user)
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(404)
end
end
context "when plugin is disabled" do
before do
sign_in(admin)
SiteSetting.discourse_ai_enabled = false
end
it "returns 404" do
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(404)
end
end
context "when AI translation is disabled" do
before do
sign_in(admin)
SiteSetting.discourse_ai_enabled = true
SiteSetting.ai_translation_enabled = false
end
it "still returns data but with enabled flag set to false" do
get "/admin/plugins/discourse-ai/ai-translations.json"
expect(response.status).to eq(200)
expect(response.parsed_body["enabled"]).to eq(false)
end
end
end
describe "#progress" do
context "when logged in as admin" do
fab!(:target_category, :category)
before do
Discourse.cache.clear
sign_in(admin)
SiteSetting.discourse_ai_enabled = true
SiteSetting.ai_translation_enabled = true
SiteSetting.content_localization_supported_locales = "en|fr|es"
SiteSetting.ai_translation_excluded_categories = ""
end
it "returns translation progress data" do
SiteSetting.ai_translation_backfill_max_age_days = 30
SiteSetting.ai_translation_personal_messages = "group"
SiteSetting.ai_translation_backfill_hourly_rate = 100
english_posts =
Fabricate.times(
14,
:post,
locale: "en",
topic: Fabricate(:topic, category: target_category),
)
french_post =
Fabricate(:post, locale: "fr", topic: Fabricate(:topic, category: target_category))
Fabricate.times(4, :post, topic: Fabricate(:topic, category: target_category))
PostLocalization.create!(
post: french_post,
locale: "en",
raw: "Translated to English",
cooked: "<p>Translated to English</p>",
post_version: french_post.version,
localizer_user_id: admin.id,
)
get "/admin/plugins/discourse-ai/ai-translations/progress.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["translation_progress"]).to be_an(Array)
expect(json["translation_progress"].length).to eq(3)
expect(json["total"]).to eq(19)
expect(json["posts_with_detected_locale"]).to eq(15)
locale_data = json["translation_progress"].first
expect(locale_data["locale"]).to eq("en")
expect(locale_data["total"]).to eq(1)
expect(locale_data["done"]).to eq(1)
end
it "shows only posts requiring translation for all locales (consistent behavior)" do
SiteSetting.ai_translation_backfill_max_age_days = 30
SiteSetting.ai_translation_personal_messages = "group"
SiteSetting.default_locale = "en"
english_posts =
Fabricate.times(
100,
:post,
locale: "en",
topic: Fabricate(:topic, category: target_category),
)
french_posts =
Fabricate.times(
10,
:post,
locale: "fr",
topic: Fabricate(:topic, category: target_category),
)
spanish_posts =
Fabricate.times(
5,
:post,
locale: "es",
topic: Fabricate(:topic, category: target_category),
)
french_posts
.take(8)
.each do |post|
PostLocalization.create!(
post: post,
locale: "en",
raw: "Translated to English",
cooked: "<p>Translated to English</p>",
post_version: post.version,
localizer_user_id: admin.id,
)
end
spanish_posts
.take(3)
.each do |post|
PostLocalization.create!(
post: post,
locale: "en",
raw: "Translated to English",
cooked: "<p>Translated to English</p>",
post_version: post.version,
localizer_user_id: admin.id,
)
end
english_posts
.take(50)
.each do |post|
PostLocalization.create!(
post: post,
locale: "fr",
raw: "Translated to French",
cooked: "<p>Translated to French</p>",
post_version: post.version,
localizer_user_id: admin.id,
)
end
english_posts
.drop(50)
.take(30)
.each do |post|
PostLocalization.create!(
post: post,
locale: "es",
raw: "Translated to Spanish",
cooked: "<p>Translated to Spanish</p>",
post_version: post.version,
localizer_user_id: admin.id,
)
end
get "/admin/plugins/discourse-ai/ai-translations/progress.json"
expect(response.status).to eq(200)
json = response.parsed_body
progress = json["translation_progress"]
en_data = progress.find { |p| p["locale"] == "en" }
fr_data = progress.find { |p| p["locale"] == "fr" }
es_data = progress.find { |p| p["locale"] == "es" }
expect(en_data["total"]).to eq(15)
expect(en_data["done"]).to eq(11)
expect(fr_data["total"]).to eq(105)
expect(fr_data["done"]).to eq(50)
expect(es_data["total"]).to eq(110)
expect(es_data["done"]).to eq(30)
end
it "returns empty when no locales are supported" do
SiteSetting.content_localization_supported_locales = ""
get "/admin/plugins/discourse-ai/ai-translations/progress.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["translation_progress"]).to eq([])
expect(json["total"]).to eq(0)
expect(json["posts_with_detected_locale"]).to eq(0)
end
end
context "when not logged in as admin" do
it "returns 404 for anonymous users" do
get "/admin/plugins/discourse-ai/ai-translations/progress.json"
expect(response.status).to eq(404)
end
it "returns 404 for regular users" do
sign_in(user)
get "/admin/plugins/discourse-ai/ai-translations/progress.json"
expect(response.status).to eq(404)
end
end
context "when plugin is disabled" do
before do
sign_in(admin)
SiteSetting.discourse_ai_enabled = false
end
it "returns 404" do
get "/admin/plugins/discourse-ai/ai-translations/progress.json"
expect(response.status).to eq(404)
end
end
context "when translation is not fully enabled" do
before do
sign_in(admin)
SiteSetting.discourse_ai_enabled = true
SiteSetting.ai_translation_enabled = false
SiteSetting.content_localization_supported_locales = "en|fr"
end
it "returns empty progress" do
get "/admin/plugins/discourse-ai/ai-translations/progress.json"
expect(response.status).to eq(200)
json = response.parsed_body
expect(json["translation_progress"]).to eq([])
expect(json["total"]).to eq(0)
expect(json["posts_with_detected_locale"]).to eq(0)
end
end
end
end