mirror of
https://github.com/discourse/discourse.git
synced 2026-08-11 02:59:07 +08:00
Part of the effort to convert site setting components to FormKit. Several places now need to render and edit a typed setting (or setting-like field) inside a FormKit form — the category type setup form, discourse-ai feature settings, workflows — and each does it with its own ad-hoc `type → control` chain plus duplicated `|`-delimited list (de)serialization. This introduces one shared, plugin-extensible component for that, and migrates the first two consumers onto it. ### `de8be6cc` — Add a reusable FormKit field for typed settings - A `registerSettingFieldType` registry + `resolveSettingFieldType` (resolution: `subtype` → `list`+`list_type` collapse → `type` → string fallback), generalizing the pattern from the workflows node configurator. - `SettingDefinitionField` owns the `<form.Field>` and renders the registry entry's control; built-in renderers for `bool`, `integer`, `enum`, `string`, `group_list`, `category_list`, `compact_list` and `duration`. - `settingFieldValidation` derives a field's FormKit validation from its definition. - Migrates the **category type setup** form off its inline `if/else` chain onto the shared component (−158 lines there). - Registry unit test + component integration test. ### `212e1cd` — Convert discourse-ai feature settings to SettingDefinitionField - The AI features edit page delegated to a bespoke `AiFeatureSettingField` + a hand-written control chain duplicated across two template branches. It now uses `SettingDefinitionField`, normalizing each `SiteSetting` into a field definition with a small adapter (`settingToDefinition`). - Deletes `AiFeatureSettingField`, the duplicated chain, and the controller's `getValidationFor` (validation now lives in core). **+24 / −336.** ### Behaviour notes - AI `group_list` settings now render with the shared **group chooser** (matching the rest of the admin) instead of the generic list-setting widget; value-compatible (`"1|2"`). - AI `bool`/`integer` fields inherit the registry's per-type widths. ### Testing - Unit + integration tests in core. - System specs pass together: discourse-ai AI features, plus the solved / events / ideas category-type setup specs. ### Follow-ups (deliberately out of scope here) - Canonicalize the `choices` vs `valid_values` descriptor shape. - Thread `allow_any` through free-form list settings; restore the category-list async race-guard. - Add a `float` registry entry; promote the `SiteSetting → definition` adapter to core when a second core consumer (workflows / the all-settings page) adopts the component.
128 lines
4 KiB
Ruby
Vendored
128 lines
4 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
RSpec.describe "Admin AI features configuration" do
|
|
fab!(:admin)
|
|
fab!(:llm_model)
|
|
fab!(:summarization_agent, :ai_agent)
|
|
fab!(:group_1, :group)
|
|
fab!(:group_2, :group)
|
|
let(:page_header) { PageObjects::Components::DPageHeader.new }
|
|
let(:form) { PageObjects::Components::FormKit.new("form") }
|
|
let(:ai_features_page) { PageObjects::Pages::AdminAiFeatures.new }
|
|
|
|
before do
|
|
enable_current_plugin
|
|
summarization_agent.allowed_group_ids = [group_1.id, group_2.id]
|
|
summarization_agent.save!
|
|
assign_fake_provider_to(:ai_default_llm_model)
|
|
SiteSetting.ai_summarization_enabled = true
|
|
SiteSetting.ai_summarization_agent = summarization_agent.id
|
|
sign_in(admin)
|
|
end
|
|
|
|
it "lists all persona backed AI features separated by enabled/not enabled" do
|
|
all_modules = DiscourseAi::Configuration::Module.all.select(&:visible?)
|
|
configured_count = all_modules.count(&:enabled?)
|
|
ai_features_page.visit
|
|
ai_features_page.toggle_enabled
|
|
|
|
expect(ai_features_page).to have_listed_modules(configured_count)
|
|
|
|
ai_features_page.toggle_not_enabled
|
|
|
|
expect(ai_features_page).to have_listed_modules(all_modules.size - configured_count)
|
|
|
|
screenshot_marker(label: "ai-admin-features")
|
|
end
|
|
|
|
it "lists the agent used for the corresponding AI feature" do
|
|
ai_features_page.visit
|
|
|
|
ai_features_page.toggle_enabled
|
|
|
|
expect(ai_features_page).to have_feature_agent("topic_summaries", summarization_agent.name)
|
|
end
|
|
|
|
it "lists the groups allowed to use the AI feature" do
|
|
ai_features_page.visit
|
|
|
|
ai_features_page.toggle_enabled
|
|
|
|
expect(ai_features_page).to have_feature_groups("topic_summaries", [group_1.name, group_2.name])
|
|
end
|
|
|
|
it "shows edit page with grouped settings" do
|
|
ai_features_page.visit
|
|
|
|
ai_features_page.click_edit_module("summarization")
|
|
|
|
expect(page).to have_current_path("/admin/plugins/discourse-ai/ai-features/1/edit")
|
|
|
|
expect(page).to have_css(".ai-feature-editor")
|
|
expect(page).to have_css(".form-kit__section")
|
|
expect(page).to have_css(".form-kit__field")
|
|
end
|
|
|
|
it "renders group_list settings as group selectors" do
|
|
SiteSetting.ai_bot_enabled = true
|
|
SiteSetting.ai_bot_allowed_groups = "#{group_1.id}|#{group_2.id}"
|
|
|
|
page.visit(
|
|
"/admin/plugins/discourse-ai/ai-features/#{DiscourseAi::Configuration::Module::BOT_ID}/edit",
|
|
)
|
|
|
|
expect(page).to have_css(".ai-feature-editor")
|
|
|
|
field = form.field("ai_bot_allowed_groups")
|
|
expect(field.component).to have_css(".group-chooser")
|
|
expect(field.component).to have_content(group_1.name)
|
|
expect(field.component).to have_content(group_2.name)
|
|
end
|
|
|
|
it "displays LLM names in compact_list settings" do
|
|
llm1 = Fabricate(:llm_model, display_name: "Test LLM Alpha")
|
|
llm2 = Fabricate(:llm_model, display_name: "Test LLM Beta")
|
|
|
|
SiteSetting.ai_bot_enabled = true
|
|
SiteSetting.ai_bot_enabled_llms = "#{llm1.id}|#{llm2.id}"
|
|
|
|
page.visit(
|
|
"/admin/plugins/discourse-ai/ai-features/#{DiscourseAi::Configuration::Module::BOT_ID}/edit",
|
|
)
|
|
|
|
expect(page).to have_css(".ai-feature-editor")
|
|
|
|
field = form.field("ai_bot_enabled_llms")
|
|
expect(field.component).to have_content("Test LLM Alpha")
|
|
expect(field.component).to have_content("Test LLM Beta")
|
|
end
|
|
|
|
context "with external AI features" do
|
|
let(:fake_plugin) do
|
|
plugin = Plugin::Instance.new
|
|
plugin.path = "#{Rails.root.join("spec/fixtures/plugins/my_plugin/plugin.rb")}"
|
|
plugin
|
|
end
|
|
|
|
before do
|
|
DiscourseAi.register_feature(
|
|
module_name: :test_external,
|
|
feature: :test_feature,
|
|
agent_klass: DiscourseAi::TestHelpers::FakeExternalAgent,
|
|
plugin: fake_plugin,
|
|
)
|
|
end
|
|
|
|
after do
|
|
DiscoursePluginRegistry._raw_external_ai_features.reject! do |entry|
|
|
entry[:value][:module_name] == :test_external
|
|
end
|
|
end
|
|
|
|
it "shows external modules from the registry" do
|
|
ai_features_page.visit
|
|
expect(page).to have_content("test_external")
|
|
expect(page).to have_content("test_feature")
|
|
end
|
|
end
|
|
end
|