0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-11 02:59:07 +08:00
discourse/plugins/discourse-ai/spec/system/admin_ai_features_spec.rb
Régis Hanol 2641aac7c2
DEV: Add a reusable SettingDefinitionField for FormKit setting fields (#41099)
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.
2026-06-25 09:30:38 +02:00

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