discourse/plugins/discourse-ai/spec/models/reviewable_ai_post_spec.rb
Krzysztof Kotlarek d792c77895
FEATURE: Link staff action log entries to originating reviewable (#39519)
The staff action log records moderation actions (post deletions, user
suspensions, silences, user approvals, etc.) as `UserHistory` rows, but
entries triggered from the reviewable queue have no reference back to
the reviewable that caused them. When auditing a decision in the log,
staff can't jump to the flagged post, queued post, or user-approval
review that produced it.

Link to review is visible in the log:
<img width="900" height="674" alt="Screenshot 2026-04-24 at 2 17 04 pm"
src="https://github.com/user-attachments/assets/791ebc58-0a4d-4c2c-a363-efe482f5f755"
/>
2026-04-30 09:15:45 +08:00

254 lines
7.9 KiB
Ruby
Vendored

# frozen_string_literal: true
describe ReviewableAiPost do
subject(:reviewable_ai_post) { described_class.new }
fab!(:target, :post)
before { enable_current_plugin }
describe "#build_actions" do
let(:guardian) { Guardian.new }
let(:reviewable) do
reviewable_ai_post.tap do |r|
r.target = target
r.target_created_by = target.user
r.created_by = Discourse.system_user
end
end
def reviewable_actions(a_guardian)
actions = Reviewable::Actions.new(reviewable, a_guardian, {})
reviewable.build_actions(actions, a_guardian, {})
actions
end
context "when the reviewable isn't pending" do
before { reviewable.status = Reviewable.statuses[:rejected] }
it "returns no actions" do
expect(reviewable_actions(guardian)).to be_blank
end
end
describe "actions that don't require special permissions" do
it "has the disagree action" do
expect(reviewable_actions(guardian).has?(:disagree)).to eq(true)
end
it "has the ignore action" do
expect(reviewable_actions(guardian).has?(:ignore)).to eq(true)
end
it "has the agree and hide or agree and keep actions" do
actions = reviewable_actions(guardian)
expect(actions.has?(:agree_and_hide)).to eq(true)
expect(actions.has?(:agree_and_keep)).to eq(true)
expect(actions.has?(:agree_and_keep_hidden)).to eq(false)
end
it "doesn't have the penalize actions" do
actions = reviewable_actions(guardian)
expect(actions.has?(:agree_and_suspend)).to eq(false)
expect(actions.has?(:agree_and_silence)).to eq(false)
end
it "doesn't has the delete + replies actions" do
actions = reviewable_actions(guardian)
expect(actions.has?(:delete_and_ignore_replies)).to eq(false)
expect(actions.has?(:delete_and_agree_replies)).to eq(false)
end
context "when the post is hidden" do
before { target.hide!(PostActionType.types[:inappropriate]) }
it "can agree and keep hidden" do
actions = reviewable_actions(guardian)
expect(actions.has?(:agree_and_hide)).to eq(false)
expect(actions.has?(:agree_and_keep)).to eq(false)
expect(actions.has?(:agree_and_keep_hidden)).to eq(true)
end
it "has the disagree and restore action" do
actions = reviewable_actions(guardian)
expect(actions.has?(:disagree)).to eq(false)
expect(actions.has?(:disagree_and_restore)).to eq(true)
end
end
context "when the post was deleted by the user" do
before { target.user_deleted = true }
it "lets you restore it but not hiding it" do
actions = reviewable_actions(guardian)
expect(actions.has?(:agree_and_restore)).to eq(true)
expect(actions.has?(:agree_and_keep)).to eq(true)
expect(actions.has?(:agree_and_keep_hidden)).to eq(false)
expect(actions.has?(:agree_and_hide)).to eq(false)
end
end
end
context "when the reviewer can suspend the poster" do
let(:mod_guardian) { Guardian.new(Fabricate(:moderator)) }
it "has the penalization actions" do
actions = reviewable_actions(mod_guardian)
expect(actions.has?(:agree_and_suspend)).to eq(true)
expect(actions.has?(:agree_and_silence)).to eq(true)
end
end
context "when the reviewer can delete the post and topic" do
let(:mod_guardian) { Guardian.new(Fabricate(:moderator)) }
it "has the delete + replies actions" do
target.reply_count = 3
actions = reviewable_actions(mod_guardian)
expect(actions.has?(:delete_and_ignore_replies)).to eq(true)
expect(actions.has?(:delete_and_agree_replies)).to eq(true)
end
end
end
describe "#perform" do
let(:reviewable) do
described_class.needs_review!(target: target, created_by: Discourse.system_user)
end
fab!(:admin)
before do
reviewable.add_score(
Discourse.system_user,
ReviewableScore.types[:inappropriate],
created_at: reviewable.created_at,
)
end
describe "agree variations" do
it "hides the topic when performing the agree_and_hide action" do
result = reviewable.perform(admin, :agree_and_hide)
expect(result.transition_to).to eq :approved
expect(target.reload.hidden?).to eq(true)
end
it "doesn't unhide the topic when performing the agree_and_keep_hidden action" do
target.hide!(ReviewableScore.types[:inappropriate])
result = reviewable.perform(admin, :agree_and_keep_hidden)
expect(result.transition_to).to eq :approved
expect(target.reload.hidden?).to eq(true)
end
it "un-deletes the post when performing the agree_and_restore action" do
target.update!(deleted_at: 1.minute.ago, deleted_by: target.user, user_deleted: true)
result = reviewable.perform(admin, :agree_and_restore)
expect(result.transition_to).to eq :approved
expect(target.reload.deleted_at).to be_nil
expect(target.user_deleted).to eq(false)
end
end
describe "disagree variations" do
it "disagree_and_restore disagrees with the flag and unhides the post" do
target.hide!(ReviewableScore.types[:inappropriate])
result = reviewable.perform(admin, :disagree_and_restore)
expect(result.transition_to).to eq :rejected
expect(target.reload.hidden?).to eq(false)
end
it "disagree disagrees with the flag" do
result = reviewable.perform(admin, :disagree)
expect(result.transition_to).to eq :rejected
end
end
describe "delete post variations" do
def create_reply(post)
PostCreator.create(
Fabricate(:user),
raw: "this is the reply text",
reply_to_post_number: post.post_number,
topic_id: post.topic_id,
)
end
before { target.update!(reply_count: 1) }
it "ignores the reviewable with delete_and_ignore" do
result = reviewable.perform(admin, :delete_and_ignore)
expect(result.transition_to).to eq :ignored
expect(target.reload.deleted_at).to be_present
end
it "ignores the reviewable and replies with delete_and_ignore_replies" do
reply = create_reply(target)
result = reviewable.perform(admin, :delete_and_ignore_replies)
expect(result.transition_to).to eq :ignored
expect(target.reload.deleted_at).to be_present
expect(reply.reload.deleted_at).to be_present
end
it "agrees with the reviewable with delete_and_agree" do
result = reviewable.perform(admin, :delete_and_agree)
expect(result.transition_to).to eq :approved
expect(target.reload.deleted_at).to be_present
end
it "agrees with the reviewables and its replies with delete_and_agree_replies" do
reply = create_reply(target)
result = reviewable.perform(admin, :delete_and_agree_replies)
expect(result.transition_to).to eq :approved
expect(target.reload.deleted_at).to be_present
expect(reply.reload.deleted_at).to be_present
end
end
describe "delete user variations" do
it "deletes the user and agrees with the reviewable" do
result = reviewable.perform(admin, :delete_user)
expect(result.transition_to).to eq :approved
expect { target.user.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it "links the staff action log to the reviewable" do
expect { reviewable.perform(admin, :delete_user) }.to change {
UserHistory.where(
action: UserHistory.actions[:delete_user],
reviewable_id: reviewable.id,
).count
}.by(1)
end
end
it "ignores the reviewable" do
result = reviewable.perform(admin, :ignore)
expect(result.transition_to).to eq :ignored
end
end
end