mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-19 09:24:23 +08:00
- Escape untrusted llm_response and automation_name when building discourse_automation.scriptables.llm_triage.flagged_post reason text. - Apply the same escaping in both automation triage and FlagPost tool paths.
122 lines
3.4 KiB
Ruby
Vendored
122 lines
3.4 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
RSpec.describe DiscourseAi::Agents::Tools::FlagPost do
|
|
fab!(:llm_model)
|
|
let(:bot_user) { DiscourseAi::AiBot::EntryPoint.find_user_from_model(llm_model.name) }
|
|
let(:llm) { DiscourseAi::Completions::Llm.proxy(llm_model) }
|
|
fab!(:post)
|
|
|
|
before do
|
|
enable_current_plugin
|
|
SiteSetting.ai_bot_enabled = true
|
|
end
|
|
|
|
def tool(params = nil, agent_options: {}, **kwargs)
|
|
params ||= kwargs
|
|
described_class.new(
|
|
params,
|
|
bot_user: bot_user,
|
|
llm: llm,
|
|
context: context,
|
|
agent_options: agent_options,
|
|
)
|
|
end
|
|
|
|
let(:context) { DiscourseAi::Agents::BotContext.new(post: post) }
|
|
|
|
it "flags the post when flag_post is true" do
|
|
result = nil
|
|
|
|
expect { result = tool(flag_post: true, reason: "Clear spam").invoke }.to change {
|
|
ReviewablePost.count
|
|
}.by(1)
|
|
|
|
expect(result[:status]).to eq("flagged")
|
|
reviewable = ReviewablePost.find_by(target: post)
|
|
score =
|
|
ReviewableScore.find_by(
|
|
reviewable: reviewable,
|
|
user: Discourse.system_user,
|
|
reviewable_score_type: ReviewableScore.types[:needs_approval],
|
|
)
|
|
expect(score.reason).to include("Clear spam")
|
|
end
|
|
|
|
it "skips when flag_post is false" do
|
|
result = nil
|
|
|
|
expect { result = tool(flag_post: false, reason: "Does not matter").invoke }.not_to change {
|
|
ReviewablePost.count
|
|
}
|
|
|
|
expect(result[:status]).to eq("skipped")
|
|
end
|
|
|
|
it "skips when the post is already flagged" do
|
|
reviewable = ReviewablePost.needs_review!(target: post, created_by: Discourse.system_user)
|
|
reviewable.add_score(
|
|
Discourse.system_user,
|
|
ReviewableScore.types[:needs_approval],
|
|
reason: "Existing flag",
|
|
force_review: true,
|
|
)
|
|
|
|
result = nil
|
|
|
|
expect { result = tool(flag_post: true, reason: "Duplicate flag").invoke }.not_to change {
|
|
ReviewableScore.count
|
|
}
|
|
|
|
expect(result[:status]).to eq("skipped")
|
|
end
|
|
|
|
it "returns an error when reason is blank" do
|
|
result = tool(flag_post: true, reason: " ").invoke
|
|
|
|
expect(result[:status]).to eq("error")
|
|
end
|
|
|
|
it "applies the configured flag_type option" do
|
|
result =
|
|
tool(
|
|
{ flag_post: true, reason: "Needs review" },
|
|
agent_options: {
|
|
flag_type: "review_hide",
|
|
},
|
|
).invoke
|
|
|
|
expect(result[:status]).to eq("flagged")
|
|
expect(post.reload).to be_hidden
|
|
end
|
|
|
|
it "escapes llm response and automation name in automation flag reasons" do
|
|
context =
|
|
DiscourseAi::Agents::BotContext.new(
|
|
post: post,
|
|
feature_context: {
|
|
automation_id: "123",
|
|
automation_name: %(rule"><img src=x onerror=1>),
|
|
llm_response: "<img src=x onerror=alert(1)>",
|
|
base_path: Discourse.base_path,
|
|
},
|
|
)
|
|
|
|
result =
|
|
described_class.new(
|
|
{ flag_post: true, reason: "fallback reason" },
|
|
bot_user: bot_user,
|
|
llm: llm,
|
|
context: context,
|
|
agent_options: {
|
|
},
|
|
).invoke
|
|
|
|
expect(result[:status]).to eq("flagged")
|
|
|
|
score_reason = ReviewablePost.last.reviewable_scores.first.reason
|
|
expect(score_reason).to include("<img src=x onerror=alert(1)>")
|
|
expect(score_reason).to include("rule"><img src=x onerror=1>")
|
|
expect(score_reason).not_to include("<img src=x onerror=alert(1)>")
|
|
expect(score_reason).not_to include(%(rule"><img src=x onerror=1>))
|
|
end
|
|
end
|