mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-19 06:43:54 +08:00
Adds an experimental "Starred" section to the AI bot conversation sidebar. Users can star up to 200 conversations they own; starred items appear in a dedicated section above the time-bucketed list and are skipped from the regular pagination. Introduces: - `discourse_ai_ai_bot_conversation_stars` table and `DiscourseAi::AiBot::ConversationStar` model - `ListConversations` and `UpdateConversationStar` services backing the existing `conversations#index` and a new `PUT /ai-bot/conversations/:topic_id/starred` endpoint - Sidebar context menu component plus updates to the sidebar manager to render and toggle starred state The feature is gated by the hidden, experimental `enable_ai_bot_starred_conversations` upcoming change setting and is off by default. <img width="1216" height="537" alt="image" src="https://github.com/user-attachments/assets/6c7d15bb-8b7c-48d9-9f02-f449764e63ff" /> <img width="955" height="648" alt="image" src="https://github.com/user-attachments/assets/0f87bd8e-e43e-483d-bf68-886cbfb09eea" /> --------- Co-authored-by: discourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
245 lines
8.4 KiB
Ruby
Vendored
245 lines
8.4 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
RSpec.describe DiscourseAi::AiBot::ConversationsController do
|
|
fab!(:current_user, :user)
|
|
fab!(:other_user, :user)
|
|
fab!(:bot_user, :user)
|
|
fab!(:conversation) do
|
|
Fabricate(:private_message_topic, user: current_user, recipient: bot_user, title: "AI PM")
|
|
end
|
|
fab!(:starred_conversation) do
|
|
Fabricate(
|
|
:private_message_topic,
|
|
user: current_user,
|
|
recipient: bot_user,
|
|
title: "Starred AI PM",
|
|
)
|
|
end
|
|
fab!(:other_conversation) do
|
|
Fabricate(:private_message_topic, user: other_user, recipient: bot_user, title: "Other AI PM")
|
|
end
|
|
fab!(:normal_pm) { Fabricate(:private_message_topic, user: current_user, recipient: other_user) }
|
|
fab!(:regular_topic, :topic)
|
|
|
|
before do
|
|
enable_current_plugin
|
|
SiteSetting.enable_ai_bot_starred_conversations = true
|
|
sign_in(current_user)
|
|
[conversation, starred_conversation, other_conversation].each { |topic| mark_ai_bot_pm(topic) }
|
|
end
|
|
|
|
describe "GET /discourse-ai/ai-bot/conversations.json" do
|
|
before do
|
|
DiscourseAi::AiBot::ConversationStar.create!(user: current_user, topic: starred_conversation)
|
|
DiscourseAi::AiBot::ConversationStar.create!(user: other_user, topic: other_conversation)
|
|
end
|
|
|
|
it "returns starred conversations first" do
|
|
get "/discourse-ai/ai-bot/conversations.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
json = response.parsed_body
|
|
|
|
expect(json).not_to have_key("starred_conversations")
|
|
expect(json["conversations"].first["id"]).to eq(starred_conversation.id)
|
|
expect(json["conversations"].first["ai_conversation_starred"]).to eq(true)
|
|
expect(json["conversations"].map { |topic| topic["id"] }).to include(conversation.id)
|
|
expect(json["conversations"].map { |topic| topic["id"] }).not_to include(normal_pm.id)
|
|
expect(json["conversations"].map { |topic| topic["id"] }).not_to include(
|
|
other_conversation.id,
|
|
)
|
|
end
|
|
|
|
it "caps starred conversations returned on the first page" do
|
|
extra_starred_conversations =
|
|
2.times.map do |index|
|
|
topic =
|
|
Fabricate(
|
|
:private_message_topic,
|
|
user: current_user,
|
|
recipient: bot_user,
|
|
title: "Extra starred AI PM #{index}",
|
|
)
|
|
mark_ai_bot_pm(topic)
|
|
DiscourseAi::AiBot::ConversationStar.create!(user: current_user, topic: topic)
|
|
topic
|
|
end
|
|
|
|
stub_const(DiscourseAi::AiBot::ConversationStar, :MAX_STARS_PER_USER, 2) do
|
|
get "/discourse-ai/ai-bot/conversations.json"
|
|
end
|
|
|
|
expect(response.status).to eq(200)
|
|
starred_records =
|
|
response.parsed_body["conversations"].select { |topic| topic["ai_conversation_starred"] }
|
|
expect(starred_records.length).to eq(2)
|
|
expect(starred_records.map { |topic| topic["id"] }).to all(
|
|
be_in([starred_conversation.id, *extra_starred_conversations.map(&:id)]),
|
|
)
|
|
end
|
|
|
|
it "returns the legacy response when the upcoming change is disabled" do
|
|
SiteSetting.enable_ai_bot_starred_conversations = false
|
|
|
|
get "/discourse-ai/ai-bot/conversations.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
json = response.parsed_body
|
|
expect(json).not_to have_key("starred_conversations")
|
|
starred_topic = json["conversations"].find { |topic| topic["id"] == starred_conversation.id }
|
|
expect(starred_topic["ai_conversation_starred"]).to eq(false)
|
|
expect(json["conversations"].map { |topic| topic["id"] }).to include(starred_conversation.id)
|
|
end
|
|
end
|
|
|
|
describe "PUT /discourse-ai/ai-bot/conversations/:topic_id/starred.json" do
|
|
context "when the upcoming change is disabled" do
|
|
before { SiteSetting.enable_ai_bot_starred_conversations = false }
|
|
|
|
it "returns 404 and does not star the conversation" do
|
|
put "/discourse-ai/ai-bot/conversations/#{conversation.id}/starred.json",
|
|
params: {
|
|
starred: true,
|
|
}
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(
|
|
DiscourseAi::AiBot::ConversationStar.exists?(user: current_user, topic: conversation),
|
|
).to eq(false)
|
|
end
|
|
it "returns 404 even when params are invalid" do
|
|
put "/discourse-ai/ai-bot/conversations/#{conversation.id}/starred.json",
|
|
params: {
|
|
starred: "wat",
|
|
}
|
|
|
|
expect(response.status).to eq(404)
|
|
end
|
|
end
|
|
|
|
it "stars a conversation for the current user" do
|
|
put "/discourse-ai/ai-bot/conversations/#{conversation.id}/starred.json",
|
|
params: {
|
|
starred: true,
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["starred"]).to eq(true)
|
|
expect(
|
|
DiscourseAi::AiBot::ConversationStar.exists?(user: current_user, topic: conversation),
|
|
).to eq(true)
|
|
end
|
|
|
|
it "does not honor a supplied user_id" do
|
|
put "/discourse-ai/ai-bot/conversations/#{conversation.id}/starred.json",
|
|
params: {
|
|
starred: true,
|
|
user_id: other_user.id,
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(
|
|
DiscourseAi::AiBot::ConversationStar.exists?(user: current_user, topic: conversation),
|
|
).to eq(true)
|
|
expect(
|
|
DiscourseAi::AiBot::ConversationStar.exists?(user: other_user, topic: conversation),
|
|
).to eq(false)
|
|
end
|
|
|
|
it "is idempotent when starring" do
|
|
DiscourseAi::AiBot::ConversationStar.create!(user: current_user, topic: conversation)
|
|
|
|
expect do
|
|
put "/discourse-ai/ai-bot/conversations/#{conversation.id}/starred.json",
|
|
params: {
|
|
starred: true,
|
|
}
|
|
end.not_to change { DiscourseAi::AiBot::ConversationStar.count }
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
it "unstars a conversation for only the current user" do
|
|
DiscourseAi::AiBot::ConversationStar.create!(user: current_user, topic: conversation)
|
|
DiscourseAi::AiBot::ConversationStar.create!(user: other_user, topic: conversation)
|
|
|
|
put "/discourse-ai/ai-bot/conversations/#{conversation.id}/starred.json",
|
|
params: {
|
|
starred: false,
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(response.parsed_body["starred"]).to eq(false)
|
|
expect(
|
|
DiscourseAi::AiBot::ConversationStar.exists?(user: current_user, topic: conversation),
|
|
).to eq(false)
|
|
expect(
|
|
DiscourseAi::AiBot::ConversationStar.exists?(user: other_user, topic: conversation),
|
|
).to eq(true)
|
|
end
|
|
|
|
it "is idempotent when unstarring" do
|
|
expect do
|
|
put "/discourse-ai/ai-bot/conversations/#{conversation.id}/starred.json",
|
|
params: {
|
|
starred: false,
|
|
}
|
|
end.not_to change { DiscourseAi::AiBot::ConversationStar.count }
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
it "rejects a missing starred param" do
|
|
put "/discourse-ai/ai-bot/conversations/#{conversation.id}/starred.json"
|
|
|
|
expect(response.status).to eq(400)
|
|
end
|
|
|
|
it "returns 404 for a non-existent topic" do
|
|
put "/discourse-ai/ai-bot/conversations/99999999/starred.json", params: { starred: true }
|
|
|
|
expect(response.status).to eq(404)
|
|
end
|
|
|
|
it "returns 404 and does not star another user's AI bot PM" do
|
|
put "/discourse-ai/ai-bot/conversations/#{other_conversation.id}/starred.json",
|
|
params: {
|
|
starred: true,
|
|
}
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(
|
|
DiscourseAi::AiBot::ConversationStar.exists?(user: current_user, topic: other_conversation),
|
|
).to eq(false)
|
|
end
|
|
|
|
it "returns 404 and does not star a non-AI PM" do
|
|
put "/discourse-ai/ai-bot/conversations/#{normal_pm.id}/starred.json",
|
|
params: {
|
|
starred: true,
|
|
}
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(
|
|
DiscourseAi::AiBot::ConversationStar.exists?(user: current_user, topic: normal_pm),
|
|
).to eq(false)
|
|
end
|
|
|
|
it "returns 404 and does not star a regular topic" do
|
|
put "/discourse-ai/ai-bot/conversations/#{regular_topic.id}/starred.json",
|
|
params: {
|
|
starred: true,
|
|
}
|
|
|
|
expect(response.status).to eq(404)
|
|
expect(
|
|
DiscourseAi::AiBot::ConversationStar.exists?(user: current_user, topic: regular_topic),
|
|
).to eq(false)
|
|
end
|
|
end
|
|
|
|
def mark_ai_bot_pm(topic)
|
|
topic.custom_fields[DiscourseAi::AiBot::TOPIC_AI_BOT_PM_FIELD] = "t"
|
|
topic.save!
|
|
end
|
|
end
|