0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 10:47:18 +08:00
discourse/plugins/discourse-ai/spec/configuration/feature_spec.rb
Natalie Tay 86680f6b0d
FEATURE: Dedicated image caption AI feature and agent validations (#41729)
Follow up to https://github.com/discourse/discourse/pull/41586


1. **Bulk of the PR diff** -- there's a mix of usages for "description"
and "caption", we'll stick to V1 terminology and keep "AI caption"
instead of bringing in "description".

2. Move image caption out of AI helper, into its own dedicated "AI
Feature" for consolidated settings

3. Update default values for the existing image caption agent 
- vision_enabled was default off, now on
- update description

4. Validations and problem checks
- via Site Setting validation - agent / llm
  - when enabling the feature
  - or when selecting invalid agent

- inform admin when they've enabled the setting but the agent is
misconfigured via ProblemCheck
2026-07-16 11:29:39 +08:00

298 lines
9.9 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe DiscourseAi::Configuration::Feature do
fab!(:llm_model) { Fabricate(:llm_model, vision_enabled: true) }
fab!(:ai_agent) { Fabricate(:ai_agent, default_llm_id: llm_model.id, vision_enabled: true) }
before { assign_fake_provider_to(:ai_default_llm_model) }
def allow_configuring_setting(&block)
DiscourseAi::Completions::Llm.with_prepared_responses(["OK"]) { block.call }
end
before { enable_current_plugin }
describe "#llm_model" do
context "when agent is not found" do
it "returns nil when agent_id is invalid" do
ai_feature =
described_class.new(
"topic_summaries",
"ai_summarization_agent",
DiscourseAi::Configuration::Module::SUMMARIZATION_ID,
DiscourseAi::Configuration::Module::SUMMARIZATION,
)
SiteSetting.ai_summarization_agent = 999_999
expect(ai_feature.llm_models).to eq([])
end
end
context "with summarization module" do
let(:ai_feature) do
described_class.new(
"topic_summaries",
"ai_summarization_agent",
DiscourseAi::Configuration::Module::SUMMARIZATION_ID,
DiscourseAi::Configuration::Module::SUMMARIZATION,
)
end
it "returns the configured llm model" do
SiteSetting.ai_summarization_agent = ai_agent.id
expect(ai_feature.llm_models).to eq([llm_model])
end
end
context "with AI helper module" do
let(:ai_feature) do
described_class.new(
"proofread",
"ai_helper_proofreader_agent",
DiscourseAi::Configuration::Module::AI_HELPER_ID,
DiscourseAi::Configuration::Module::AI_HELPER,
)
end
it "returns the agent's default llm when no specific helper model is set" do
SiteSetting.ai_helper_proofreader_agent = ai_agent.id
expect(ai_feature.llm_models).to eq([llm_model])
end
end
context "with translation module" do
fab!(:translation_model, :llm_model)
let(:ai_feature) do
described_class.new(
"locale_detector",
"ai_translation_locale_detector_agent",
DiscourseAi::Configuration::Module::TRANSLATION_ID,
DiscourseAi::Configuration::Module::TRANSLATION,
)
end
it "uses translation model when configured" do
SiteSetting.ai_translation_locale_detector_agent = ai_agent.id
ai_agent.update!(default_llm_id: translation_model.id)
expect(ai_feature.llm_models).to eq([translation_model])
end
end
end
describe "#enabled?" do
it "returns true when no enabled_by_setting is specified" do
ai_feature =
described_class.new(
"topic_summaries",
"ai_summarization_agent",
DiscourseAi::Configuration::Module::SUMMARIZATION_ID,
DiscourseAi::Configuration::Module::SUMMARIZATION,
)
expect(ai_feature.enabled?).to be true
end
it "respects the enabled_by_setting when specified" do
ai_feature =
described_class.new(
"gists",
"ai_summary_gists_agent",
DiscourseAi::Configuration::Module::SUMMARIZATION_ID,
DiscourseAi::Configuration::Module::SUMMARIZATION,
enabled_by_setting: "ai_summary_gists_enabled",
)
SiteSetting.ai_summary_gists_enabled = false
expect(ai_feature.enabled?).to be false
SiteSetting.ai_summary_gists_enabled = true
expect(ai_feature.enabled?).to be true
end
end
describe ".bot_features" do
fab!(:bot_llm, :llm_model)
fab!(:non_bot_llm, :llm_model)
before { SiteSetting.ai_bot_enabled_llms = bot_llm.id.to_s }
fab!(:chat_agent) do
Fabricate(
:ai_agent,
default_llm_id: bot_llm.id,
allow_chat_channel_mentions: true,
allow_chat_direct_messages: false,
)
end
fab!(:dm_agent) do
Fabricate(
:ai_agent,
default_llm_id: bot_llm.id,
allow_chat_channel_mentions: false,
allow_chat_direct_messages: true,
)
end
fab!(:topic_agent) do
Fabricate(
:ai_agent,
default_llm_id: bot_llm.id,
allow_topic_mentions: true,
allow_personal_messages: false,
)
end
fab!(:pm_agent) do
Fabricate(:ai_agent, allow_topic_mentions: false, allow_personal_messages: true)
end
fab!(:inactive_agent) do
Fabricate(
:ai_agent,
enabled: false,
allow_chat_channel_mentions: false,
allow_chat_direct_messages: false,
allow_topic_mentions: false,
allow_personal_messages: true,
)
end
let(:bot_feature) { described_class.bot_features.first }
it "returns bot features with correct configuration" do
expect(bot_feature.name).to eq("bot")
expect(bot_feature.agent_setting).to be_nil
expect(bot_feature.module_id).to eq(DiscourseAi::Configuration::Module::BOT_ID)
expect(bot_feature.module_name).to eq(DiscourseAi::Configuration::Module::BOT)
end
it "returns only LLMs enabled in ai_bot_enabled_llms setting" do
# Disable all other agents to ensure only test agents are active
expected_agent_ids = [chat_agent.id, dm_agent.id, topic_agent.id, pm_agent.id]
AiAgent.where.not(id: expected_agent_ids).update_all(enabled: false)
expect(bot_feature.llm_models).to contain_exactly(bot_llm)
expect(bot_feature.llm_models).not_to include(non_bot_llm)
end
it "returns only agents with at least one bot permission enabled" do
expected_ids = [chat_agent.id, dm_agent.id, topic_agent.id, pm_agent.id]
AiAgent.where.not(id: expected_ids).update_all(enabled: false)
expect(bot_feature.agent_ids).to match_array(expected_ids)
expect(bot_feature.agent_ids).not_to include(inactive_agent.id)
end
it "includes agents with multiple permissions enabled" do
multi_permission_agent =
Fabricate(
:ai_agent,
enabled: true,
default_llm_id: bot_llm.id,
allow_chat_channel_mentions: true,
allow_chat_direct_messages: true,
allow_topic_mentions: true,
allow_personal_messages: true,
)
expect(bot_feature.agent_ids).to include(multi_permission_agent.id)
end
it "does not include the image caption agent" do
pm_agent.update!(default_llm_id: bot_llm.id, vision_enabled: true)
bot_llm.update!(vision_enabled: true)
SiteSetting.ai_image_caption_agent = pm_agent.id
expect(bot_feature.agent_ids).not_to include(pm_agent.id)
end
end
describe ".image_caption_features" do
let(:image_caption_feature) { described_class.image_caption_features.first }
it "returns the post image captions feature with the image caption module configuration" do
expect(image_caption_feature.name).to eq("post_image_captions")
expect(image_caption_feature.agent_setting).to eq("ai_image_caption_agent")
expect(image_caption_feature.module_id).to eq(
DiscourseAi::Configuration::Module::IMAGE_CAPTION_ID,
)
expect(image_caption_feature.module_name).to eq(
DiscourseAi::Configuration::Module::IMAGE_CAPTION,
)
end
it "uses the selected image caption agent" do
SiteSetting.ai_image_caption_agent = ai_agent.id
expect(image_caption_feature.agent_ids).to eq([ai_agent.id])
end
end
describe "#agent_ids" do
it "returns the agent id from site settings" do
ai_feature =
described_class.new(
"topic_summaries",
"ai_summarization_agent",
DiscourseAi::Configuration::Module::SUMMARIZATION_ID,
DiscourseAi::Configuration::Module::SUMMARIZATION,
)
SiteSetting.ai_summarization_agent = ai_agent.id
expect(ai_feature.agent_ids).to eq([ai_agent.id])
end
end
describe ".admin_dashboard_features" do
it "returns the first-party admin dashboard highlights feature" do
feature = described_class.admin_dashboard_features.first
expect(feature.name).to eq("highlights")
expect(feature.agent_setting).to eq("ai_admin_dashboard_highlights_agent")
expect(feature.module_id).to eq(DiscourseAi::Configuration::Module::ADMIN_DASHBOARD_ID)
expect(feature.module_name).to eq(DiscourseAi::Configuration::Module::ADMIN_DASHBOARD)
end
it "is enabled only when its selected agent is enabled" do
SiteSetting.ai_admin_dashboard_enabled = true
agent = AiAgent.find_by(id: -38) || Fabricate(:ai_agent, id: -38)
agent.update!(enabled: true)
feature = described_class.admin_dashboard_features.first
expect(feature).to be_enabled
agent.update!(enabled: false)
expect(feature).not_to be_enabled
end
it "is disabled when the admin dashboard module is disabled" do
SiteSetting.ai_admin_dashboard_enabled = false
agent = AiAgent.find_by(id: -38) || Fabricate(:ai_agent, id: -38)
agent.update!(enabled: true)
expect(described_class.admin_dashboard_features.first).not_to be_enabled
end
end
describe "admin dashboard module" do
it "is hidden from the AI features page" do
admin_dashboard_module =
DiscourseAi::Configuration::Module.all.find do |mod|
mod.name == DiscourseAi::Configuration::Module::ADMIN_DASHBOARD
end
expect(admin_dashboard_module).not_to be_visible
end
end
describe ".find_features_using" do
it "returns all features using a specific agent" do
SiteSetting.ai_summarization_agent = ai_agent.id
SiteSetting.ai_helper_proofreader_agent = ai_agent.id
SiteSetting.ai_image_caption_agent = ai_agent.id
SiteSetting.ai_translation_locale_detector_agent = 999
features = described_class.find_features_using(agent_id: ai_agent.id)
expect(features.map(&:name)).to include("topic_summaries", "proofread", "post_image_captions")
expect(features.map(&:name)).not_to include("locale_detector")
end
end
end