0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-09 21:45:25 +08:00
discourse/plugins/discourse-ai/spec/requests/ai_bot/bot_controller_spec.rb
Mark VanLandingham c2a47a6675
FIX: Authorize Retried AI Bot Replies Against Topic Creator (#42325)
## Summary

Previously, AI bot replies checked agent access against the author of
the latest post, which meant a more privileged participant could
unintentionally unlock a restricted agent for a conversation; retries
could also lose the original authorization context, especially after a
failed response.

This change keeps topic-selected agents tied to the topic creator’s
permissions, records the exact authorizing user before response
generation begins, and reuses that information for retries. Explicit
agent mentions still use the mentioning user’s permissions, while legacy
conversations and missing users fail closed rather than guessing or
inheriting access from another participant.
## Source

- Patch Triage: https://patch.discourse.org/patch-triage/1180
2026-08-05 14:28:53 -05:00

697 lines
25 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::BotController do
fab!(:user)
fab!(:pm_topic, :private_message_topic)
fab!(:pm_post) { Fabricate(:post, topic: pm_topic) }
fab!(:pm_post2) { Fabricate(:post, topic: pm_topic) }
fab!(:pm_post3) { Fabricate(:post, topic: pm_topic) }
before do
enable_current_plugin
sign_in(user)
end
describe "#show_debug_info" do
fab!(:debug_bot_model) { Fabricate(:llm_model, name: "debug-bot-model") }
fab!(:debug_bot_user) do
enable_current_plugin
toggle_enabled_bots(bots: [debug_bot_model])
debug_bot_model.reload.user
end
fab!(:pm_topic) { Fabricate(:private_message_topic, user: user, recipient: debug_bot_user) }
fab!(:pm_post) { Fabricate(:post, topic: pm_topic, user: debug_bot_user) }
fab!(:pm_post2) { Fabricate(:post, topic: pm_topic, user: debug_bot_user) }
fab!(:pm_post3) { Fabricate(:post, topic: pm_topic, user: user) }
before { SiteSetting.ai_bot_enabled = true }
it "returns a 403 when the user cannot debug the AI bot conversation" do
get "/discourse-ai/ai-bot/post/#{pm_post.id}/show-debug-info"
expect(response.status).to eq(403)
end
it "does not disclose hidden whisper audit logs" do
debug_group = Fabricate(:group)
debug_group.add(user)
SiteSetting.ai_bot_debugging_allowed_groups = debug_group.id.to_s
SiteSetting.whispers_allowed_groups = Group::AUTO_GROUPS[:staff].to_s
visible_bot_post = Fabricate(:post, user: debug_bot_user)
hidden_whisper_post =
Fabricate(
:post,
topic: visible_bot_post.topic,
user: Fabricate(:admin),
post_type: Post.types[:whisper],
)
later_regular_post = Fabricate(:post, topic: visible_bot_post.topic, user: user)
visible_bot_log =
AiApiAuditLog.create!(
post_id: visible_bot_post.id,
provider_id: 1,
topic_id: visible_bot_post.topic_id,
feature_name: "ai_bot",
raw_request_payload: "bot request",
raw_response_payload: "bot response",
request_tokens: 1,
response_tokens: 2,
created_at: 2.minutes.ago,
)
hidden_bot_log =
AiApiAuditLog.create!(
post_id: hidden_whisper_post.id,
provider_id: 1,
topic_id: hidden_whisper_post.topic_id,
feature_name: "ai_bot",
raw_request_payload: "hidden bot request",
raw_response_payload: "hidden bot response",
request_tokens: 3,
response_tokens: 4,
created_at: 1.minute.ago,
)
hidden_translation_log =
AiApiAuditLog.create!(
post_id: hidden_whisper_post.id,
provider_id: 1,
topic_id: hidden_whisper_post.topic_id,
feature_name: "translation",
raw_request_payload: "hidden translation request",
raw_response_payload: "hidden translation response",
request_tokens: 5,
response_tokens: 6,
created_at: Time.zone.now,
)
get "/discourse-ai/ai-bot/post/#{later_regular_post.id}/show-debug-info"
show_debug_info_status = response.status
show_debug_info_body = response.parsed_body.deep_dup
get "/discourse-ai/ai-bot/show-debug-info/#{hidden_translation_log.id}"
hidden_translation_status = response.status
hidden_translation_body = response.parsed_body.deep_dup
get "/discourse-ai/ai-bot/show-debug-info/#{hidden_bot_log.id}"
hidden_bot_status = response.status
hidden_bot_body = response.parsed_body.deep_dup
aggregate_failures do
expect(show_debug_info_status).to eq(200)
expect(show_debug_info_body["id"]).to eq(visible_bot_log.id)
expect(show_debug_info_body["raw_request_payload"]).to eq("bot request")
expect(show_debug_info_body["raw_response_payload"]).to eq("bot response")
expect(hidden_translation_status).to eq(403)
expect(hidden_translation_body["errors"]).to include(I18n.t("invalid_access"))
expect(hidden_bot_status).to eq(403)
expect(hidden_bot_body["errors"]).to include(I18n.t("invalid_access"))
end
end
it "returns debug info if the user can debug the AI bot conversation" do
user = pm_topic.topic_allowed_users.first.user
sign_in(user)
log1 =
AiApiAuditLog.create!(
provider_id: 1,
topic_id: pm_topic.id,
feature_name: "ai_bot",
raw_request_payload: "request",
raw_response_payload: "response",
request_tokens: 1,
response_tokens: 2,
)
log2 =
AiApiAuditLog.create!(
post_id: pm_post.id,
provider_id: 1,
topic_id: pm_topic.id,
feature_name: "ai_bot",
raw_request_payload: "request",
raw_response_payload: "response",
request_tokens: 1,
response_tokens: 2,
)
log3 =
AiApiAuditLog.create!(
post_id: pm_post2.id,
provider_id: 1,
topic_id: pm_topic.id,
feature_name: "ai_bot",
raw_request_payload: "request",
raw_response_payload: "response",
request_tokens: 1,
response_tokens: 2,
)
Group.refresh_automatic_groups!
SiteSetting.ai_bot_debugging_allowed_groups = user.groups.first.id.to_s
get "/discourse-ai/ai-bot/post/#{pm_post.id}/show-debug-info"
expect(response.status).to eq(200)
expect(response.parsed_body["id"]).to eq(log2.id)
expect(response.parsed_body["next_log_id"]).to eq(log3.id)
expect(response.parsed_body["prev_log_id"]).to eq(log1.id)
expect(response.parsed_body["topic_id"]).to eq(pm_topic.id)
expect(response.parsed_body["request_tokens"]).to eq(1)
expect(response.parsed_body["response_tokens"]).to eq(2)
expect(response.parsed_body["raw_request_payload"]).to eq("request")
expect(response.parsed_body["raw_response_payload"]).to eq("response")
get "/discourse-ai/ai-bot/post/#{pm_post3.id}/show-debug-info"
expect(response.status).to eq(200)
expect(response.parsed_body["request_tokens"]).to eq(1)
expect(response.parsed_body["response_tokens"]).to eq(2)
get "/discourse-ai/ai-bot/show-debug-info/#{log1.id}"
expect(response.status).to eq(200)
expect(response.parsed_body["id"]).to eq(log1.id)
end
it "prefers the post's own log over a newer topic-scoped log like title generation" do
user = pm_topic.topic_allowed_users.first.user
sign_in(user)
reply_log =
AiApiAuditLog.create!(
post_id: pm_post.id,
provider_id: 1,
topic_id: pm_topic.id,
feature_name: "bot",
raw_request_payload: "reply request",
raw_response_payload: "reply response",
created_at: 2.minutes.ago,
)
title_log =
AiApiAuditLog.create!(
provider_id: 1,
topic_id: pm_topic.id,
feature_name: "bot_title",
raw_request_payload: "title request",
raw_response_payload: "title response",
created_at: 1.minute.ago,
)
Group.refresh_automatic_groups!
SiteSetting.ai_bot_debugging_allowed_groups = user.groups.first.id.to_s
get "/discourse-ai/ai-bot/post/#{pm_post.id}/show-debug-info"
expect(response.status).to eq(200)
expect(response.parsed_body["id"]).to eq(reply_log.id)
expect(response.parsed_body["next_log_id"]).to eq(title_log.id)
end
context "with conversation totals and spending" do
fab!(:llm_model) do
Fabricate(
:llm_model,
input_cost: 3.0,
output_cost: 6.0,
cached_input_cost: 1.0,
cache_write_cost: 2.0,
)
end
fab!(:other_llm_model) do
Fabricate(
:llm_model,
name: "other-model",
input_cost: 10.0,
output_cost: 20.0,
cached_input_cost: 5.0,
cache_write_cost: 8.0,
)
end
let(:allowed_user) { pm_topic.topic_allowed_users.first.user }
before do
sign_in(allowed_user)
Group.refresh_automatic_groups!
SiteSetting.ai_bot_debugging_allowed_groups = allowed_user.groups.first.id.to_s
end
it "computes per-turn spending and conversation totals across multiple models" do
log_a =
AiApiAuditLog.create!(
post_id: pm_post.id,
provider_id: 1,
topic_id: pm_topic.id,
llm_id: llm_model.id,
feature_name: "ai_bot",
raw_request_payload: "req",
raw_response_payload: "res",
request_tokens: 1_000_000,
response_tokens: 500_000,
cache_read_tokens: 200_000,
cache_write_tokens: 100_000,
)
AiApiAuditLog.create!(
post_id: pm_post2.id,
provider_id: 1,
topic_id: pm_topic.id,
llm_id: other_llm_model.id,
feature_name: "ai_bot",
raw_request_payload: "req",
raw_response_payload: "res",
request_tokens: 2_000_000,
response_tokens: 1_000_000,
)
get "/discourse-ai/ai-bot/show-debug-info/#{log_a.id}"
expect(response.status).to eq(200)
body = response.parsed_body
expected_turn_spending =
(1_000_000 * 3.0 + 500_000 * 6.0 + 200_000 * 1.0 + 100_000 * 2.0) / 1_000_000.0
expect(body["spending"]).to be_within(0.000001).of(expected_turn_spending)
expect(body["conversation_request_tokens"]).to eq(3_000_000)
expect(body["conversation_response_tokens"]).to eq(1_500_000)
expect(body["conversation_cache_read_tokens"]).to eq(200_000)
expect(body["conversation_cache_write_tokens"]).to eq(100_000)
expected_conversation_spending =
expected_turn_spending + (2_000_000 * 10.0 + 1_000_000 * 20.0) / 1_000_000.0
expect(body["conversation_spending"]).to be_within(0.000001).of(
expected_conversation_spending,
)
end
it "returns nil spending when the log has no llm_model" do
log_a =
AiApiAuditLog.create!(
post_id: pm_post.id,
provider_id: 1,
topic_id: pm_topic.id,
feature_name: "ai_bot",
raw_request_payload: "req",
raw_response_payload: "res",
request_tokens: 100,
response_tokens: 200,
)
get "/discourse-ai/ai-bot/show-debug-info/#{log_a.id}"
expect(response.status).to eq(200)
body = response.parsed_body
expect(body["spending"]).to be_nil
expect(body["conversation_request_tokens"]).to eq(100)
expect(body["conversation_response_tokens"]).to eq(200)
expect(body["conversation_spending"]).to be_nil
end
it "returns nil conversation fields when topic_id is absent" do
log_a =
AiApiAuditLog.create!(
provider_id: 1,
llm_id: llm_model.id,
feature_name: "ai_bot",
raw_request_payload: "req",
raw_response_payload: "res",
request_tokens: 100,
response_tokens: 200,
)
SiteSetting.ai_bot_debugging_allowed_groups = Group::AUTO_GROUPS[:admins].to_s
sign_in(Fabricate(:admin))
get "/discourse-ai/ai-bot/show-debug-info/#{log_a.id}"
# no topic means the endpoint 404s, so assert via serializer directly
serialized = AiApiAuditLogSerializer.new(log_a, root: false).as_json
expect(serialized[:conversation_request_tokens]).to be_nil
expect(serialized[:conversation_response_tokens]).to be_nil
expect(serialized[:conversation_cache_read_tokens]).to be_nil
expect(serialized[:conversation_cache_write_tokens]).to be_nil
expect(serialized[:conversation_spending]).to be_nil
end
end
it "returns topic-level debug info for any feature for a post in the conversation" do
user = pm_topic.topic_allowed_users.first.user
sign_in(user)
log =
AiApiAuditLog.create!(
provider_id: 1,
topic_id: pm_topic.id,
feature_name: "translation",
raw_request_payload: "request",
raw_response_payload: "response",
request_tokens: 1,
response_tokens: 2,
)
Group.refresh_automatic_groups!
SiteSetting.ai_bot_debugging_allowed_groups = user.groups.first.id.to_s
get "/discourse-ai/ai-bot/post/#{pm_post.id}/show-debug-info"
expect(response.status).to eq(200)
expect(response.parsed_body["id"]).to eq(log.id)
get "/discourse-ai/ai-bot/show-debug-info/#{log.id}"
expect(response.status).to eq(200)
expect(response.parsed_body["id"]).to eq(log.id)
end
end
describe "#stop_streaming_response" do
let(:redis_stream_key) { "gpt_cancel:#{pm_post.id}" }
before do
SiteSetting.ai_bot_enabled = true
Discourse.redis.setex(redis_stream_key, 60, 1)
end
it "returns a 403 when the user cannot see the PM" do
post "/discourse-ai/ai-bot/post/#{pm_post.id}/stop-streaming"
expect(response.status).to eq(403)
end
it "returns a 403 when the user can see the PM but is not in ai_bot_allowed_groups" do
allowed_user = pm_topic.topic_allowed_users.first.user
sign_in(allowed_user)
SiteSetting.ai_bot_allowed_groups = Group::AUTO_GROUPS[:staff].to_s
post "/discourse-ai/ai-bot/post/#{pm_post.id}/stop-streaming"
expect(response.status).to eq(403)
expect(Discourse.redis.get(redis_stream_key)).to eq("1")
end
it "returns a 403 when ai_bot_enabled is false" do
allowed_user = pm_topic.topic_allowed_users.first.user
sign_in(allowed_user)
SiteSetting.ai_bot_enabled = false
post "/discourse-ai/ai-bot/post/#{pm_post.id}/stop-streaming"
expect(response.status).to eq(403)
expect(Discourse.redis.get(redis_stream_key)).to eq("1")
end
it "deletes the key using to track the streaming" do
allowed_user = pm_topic.topic_allowed_users.first.user
sign_in(allowed_user)
Group.refresh_automatic_groups!
SiteSetting.ai_bot_allowed_groups = allowed_user.groups.first.id.to_s
post "/discourse-ai/ai-bot/post/#{pm_post.id}/stop-streaming"
expect(response.status).to eq(200)
expect(Discourse.redis.get(redis_stream_key)).to be_nil
end
end
describe "#retry_response" do
fab!(:bot_user, :user)
let!(:llm_model) { Fabricate(:llm_model, user: bot_user) }
let!(:ai_agent) do
Fabricate(
:ai_agent,
user: bot_user,
default_llm_id: llm_model.id,
allowed_group_ids: [Group::AUTO_GROUPS[:trust_level_0]],
)
end
let(:agent) { ai_agent.class_instance.new }
let(:bot) { DiscourseAi::Agents::Bot.as(bot_user, agent: agent) }
let!(:prompt_post) do
Fabricate(:post, topic: pm_topic, user: user, raw: "Hello @#{bot_user.username}")
end
let!(:reply_post) do
DiscourseAi::Completions::Llm.with_prepared_responses(["first try"]) do
DiscourseAi::AiBot::Playground.new(bot).reply_to(prompt_post)
end
pm_topic.reload.posts.last
end
before do
Group.refresh_automatic_groups!
SiteSetting.ai_bot_enabled = true
SiteSetting.ai_bot_allowed_groups = Group::AUTO_GROUPS[:trust_level_0].to_s
AiAgent.agent_cache.flush!
tl0_group =
Group.find_by(name: "trust_level_0") || Group.find(Group::AUTO_GROUPS[:trust_level_0])
GroupUser.find_or_create_by!(user: user, group: tl0_group)
user.reload
pm_topic.topic_allowed_users.find_or_create_by!(user: user)
unless pm_topic.topic_allowed_users.exists?(user: bot_user)
pm_topic.topic_allowed_users.create!(user: bot_user)
end
end
it "returns 403 when ai_bot_enabled is false" do
SiteSetting.ai_bot_enabled = false
post "/discourse-ai/ai-bot/post/#{reply_post.id}/retry"
expect(response.status).to eq(403)
end
it "returns 403 when user is not in ai_bot_allowed_groups" do
SiteSetting.ai_bot_allowed_groups = Group::AUTO_GROUPS[:admins].to_s
post "/discourse-ai/ai-bot/post/#{reply_post.id}/retry"
expect(response.status).to eq(403)
end
it "uses the topic creator to authorize a legacy restricted agent retry" do
admin = Fabricate(:admin, refresh_auto_groups: true)
restricted_agent =
Fabricate(
:ai_agent,
user: bot_user,
default_llm_id: llm_model.id,
allowed_group_ids: [Group::AUTO_GROUPS[:admins]],
)
topic = Fabricate(:private_message_topic, user: user, recipient: bot_user)
topic.topic_allowed_users.create!(user: admin)
prompt_post =
Fabricate(:post, topic: topic, user: admin, raw: "Please use the restricted agent")
reply_post = Fabricate(:post, topic: topic, user: bot_user, raw: "original restricted reply")
reply_post.custom_fields[DiscourseAi::AiBot::POST_AI_AGENT_ID_FIELD] = restricted_agent.id
reply_post.save_custom_fields
AiAgent.agent_cache.flush!
DiscourseAi::Completions::Llm.with_prepared_responses(
["restricted retry response", "restricted retry title"],
) do
post "/discourse-ai/ai-bot/post/#{reply_post.id}/retry"
job_args = Jobs::CreateAiReply.jobs.last["args"].first.symbolize_keys
Jobs::CreateAiReply.new.execute(job_args)
end
aggregate_failures do
expect(response.status).to eq(200)
expect(response.parsed_body).to eq("success" => "OK")
expect(Jobs::CreateAiReply.jobs.last["args"].first["authorization_user_id"]).to eq(user.id)
expect(reply_post.reload.raw).to eq("original restricted reply")
end
end
it "reuses the recorded authorization user when retrying" do
admin = Fabricate(:admin, refresh_auto_groups: true)
restricted_agent =
Fabricate(
:ai_agent,
user: bot_user,
default_llm_id: llm_model.id,
allowed_group_ids: [Group::AUTO_GROUPS[:admins]],
)
topic = Fabricate(:private_message_topic, user: user, recipient: bot_user)
topic.topic_allowed_users.create!(user: admin)
prompt_post = Fabricate(:post, topic: topic, user: admin, raw: "Use the restricted agent")
reply_post = Fabricate(:post, topic: topic, user: bot_user, raw: "original reply")
reply_post.custom_fields[DiscourseAi::AiBot::POST_AI_AGENT_ID_FIELD] = restricted_agent.id
reply_post.custom_fields[
DiscourseAi::AiBot::POST_AI_AGENT_AUTHORIZATION_USER_ID_FIELD
] = admin.id
reply_post.save_custom_fields
AiAgent.agent_cache.flush!
DiscourseAi::Completions::Llm.with_prepared_responses(
["retried response", "retried title"],
) do
post "/discourse-ai/ai-bot/post/#{reply_post.id}/retry"
job_args = Jobs::CreateAiReply.jobs.last["args"].first.symbolize_keys
Jobs::CreateAiReply.new.execute(job_args)
end
aggregate_failures do
expect(response.status).to eq(200)
expect(Jobs::CreateAiReply.jobs.last["args"].first["authorization_user_id"]).to eq(admin.id)
expect(reply_post.reload.raw).to eq("retried response")
end
end
it "fails closed when a legacy retry has no topic creator to authorize against" do
admin = Fabricate(:admin, refresh_auto_groups: true)
restricted_agent =
Fabricate(
:ai_agent,
user: bot_user,
default_llm_id: llm_model.id,
allowed_group_ids: [Group::AUTO_GROUPS[:admins]],
)
topic = Fabricate(:private_message_topic, user: user, recipient: bot_user)
topic.topic_allowed_users.create!(user: admin)
prompt_post = Fabricate(:post, topic: topic, user: admin, raw: "Use the restricted agent")
reply_post = Fabricate(:post, topic: topic, user: bot_user, raw: "original reply")
reply_post.custom_fields[DiscourseAi::AiBot::POST_AI_AGENT_ID_FIELD] = restricted_agent.id
reply_post.save_custom_fields
topic.update_columns(user_id: nil)
AiAgent.agent_cache.flush!
expect_not_enqueued_with(job: :create_ai_reply) do
post "/discourse-ai/ai-bot/post/#{reply_post.id}/retry"
end
expect(response.status).to eq(403)
end
it "streams a replacement into the existing bot reply" do
retry_text = "second attempt"
messages = nil
DiscourseAi::Completions::Llm.with_prepared_responses([retry_text]) do
post "/discourse-ai/ai-bot/post/#{reply_post.id}/retry"
messages =
MessageBus.track_publish("discourse-ai/ai-bot/topic/#{reply_post.topic_id}") do
Jobs::CreateAiReply.new.execute(
post_id: prompt_post.id,
bot_user_id: bot_user.id,
agent_id: reply_post.custom_fields[DiscourseAi::AiBot::POST_AI_AGENT_ID_FIELD].to_i,
reply_post_id: reply_post.id,
)
end
end
expect(response.status).to eq(200)
expect(reply_post.reload.raw).to eq(retry_text)
expect(messages.first.data).to include(post_id: reply_post.id, raw: "")
expect(reply_post.custom_fields[DiscourseAi::AiBot::POST_AI_AGENT_ID_FIELD].to_i).to eq(
agent.id,
)
end
it "returns a 404 when there is no previous non-bot prompt" do
lone_topic = Fabricate(:private_message_topic)
lone_topic.topic_allowed_users.create!(user: user)
lone_topic.topic_allowed_users.create!(user: bot_user)
bot_only_post = Fabricate(:post, topic: lone_topic, user: bot_user)
post "/discourse-ai/ai-bot/post/#{bot_only_post.id}/retry"
expect(response.status).to eq(404)
end
it "allows retrying if LLM model has a negative id (seeded)" do
seeded_llm_model = Fabricate(:llm_model, id: -9999, user: bot_user, name: "second-model")
bot = DiscourseAi::Agents::Bot.as(bot_user, agent: agent, model: seeded_llm_model)
DiscourseAi::Completions::Llm.with_prepared_responses(["first try"], llm: seeded_llm_model) do
DiscourseAi::AiBot::Playground.new(bot).reply_to(prompt_post)
end
reply = pm_topic.reload.posts.last
original_llm_id = reply.custom_fields[DiscourseAi::AiBot::POST_AI_LLM_MODEL_ID_FIELD]
expect(original_llm_id.to_i).to eq(-9999)
retry_text = "retry with original model"
DiscourseAi::Completions::Llm.with_prepared_responses([retry_text], llm: seeded_llm_model) do
post "/discourse-ai/ai-bot/post/#{reply.id}/retry"
job_args = Jobs::CreateAiReply.jobs.last["args"].first
expect(job_args["llm_model_id"]).to eq(-9999)
Jobs::CreateAiReply.new.execute(job_args.symbolize_keys)
end
expect(response.status).to eq(200)
expect(reply.reload.raw).to eq(retry_text)
end
it "uses the original LLM model when retrying even if agent default changed" do
second_bot_user = Fabricate(:user)
second_llm_model = Fabricate(:llm_model, user: second_bot_user, name: "second-model")
pm_topic.topic_allowed_users.find_or_create_by!(user: second_bot_user)
original_llm_name = reply_post.custom_fields[DiscourseAi::AiBot::POST_AI_LLM_NAME_FIELD]
original_llm_id = reply_post.custom_fields[DiscourseAi::AiBot::POST_AI_LLM_MODEL_ID_FIELD]
expect(original_llm_name).to be_present
expect(original_llm_id.to_i).to eq(llm_model.id)
ai_agent.update!(default_llm_id: second_llm_model.id)
AiAgent.agent_cache.flush!
retry_text = "retry with original model"
DiscourseAi::Completions::Llm.with_prepared_responses([retry_text], llm: llm_model) do
post "/discourse-ai/ai-bot/post/#{reply_post.id}/retry"
job_args = Jobs::CreateAiReply.jobs.last["args"].first
expect(job_args["llm_model_id"]).to eq(llm_model.id)
Jobs::CreateAiReply.new.execute(job_args.symbolize_keys)
end
expect(response.status).to eq(200)
expect(reply_post.reload.raw).to eq(retry_text)
expect(reply_post.custom_fields[DiscourseAi::AiBot::POST_AI_LLM_NAME_FIELD]).to eq(
original_llm_name,
)
expect(reply_post.custom_fields[DiscourseAi::AiBot::POST_AI_LLM_MODEL_ID_FIELD].to_i).to eq(
original_llm_id.to_i,
)
end
end
describe "#show_bot_username" do
it "returns the username_lower of the selected bot" do
gpt_35_bot = Fabricate(:llm_model, name: "gpt-3.5-turbo")
SiteSetting.ai_bot_enabled = true
toggle_enabled_bots(bots: [gpt_35_bot])
expected_username =
DiscourseAi::AiBot::EntryPoint.find_user_from_model("gpt-3.5-turbo").username_lower
get "/discourse-ai/ai-bot/bot-username", params: { username: gpt_35_bot.name }
expect(response.status).to eq(200)
expect(response.parsed_body["bot_username"]).to eq(expected_username)
end
end
end