Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# This migration comes from co_plan (originally 20260609000000)
class BackfillLocalAgentCommentAuthorIds < ActiveRecord::Migration[8.1]
# For local_agent comments whose author_id still points at a
# coplan_api_tokens.id, rewrite it to that token's user_id. This
# pairs with the model change in engine/app/models/coplan/comment.rb:
# Comment#author now resolves via a direct find_by(id:) for both
# human and local_agent comments instead of joining coplan_api_tokens.
#
# Idempotent: once author_id holds a user UUID it won't match any
# coplan_api_tokens.id, so re-runs skip those rows.
def up
CoPlan::Comment.where(author_type: "local_agent").find_each do |comment|
token = CoPlan::ApiToken.find_by(id: comment.author_id)
next unless token

comment.update_columns(author_id: token.user_id) # rubocop:disable Rails/SkipsModelValidations
end
end

def down
# no-op (not reversible)
end
end
2 changes: 1 addition & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions engine/app/controllers/coplan/api/v1/comments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def create

comment = thread.comments.create!(
author_type: api_author_type,
author_id: api_actor_id,
author_id: current_user&.id,
body_markdown: params[:body_markdown],
agent_name: params[:agent_name]
)
Expand Down Expand Up @@ -94,7 +94,7 @@ def reply

comment = thread.comments.create!(
author_type: api_author_type,
author_id: api_actor_id,
author_id: current_user&.id,
body_markdown: params[:body_markdown],
agent_name: params[:agent_name]
)
Expand Down
13 changes: 7 additions & 6 deletions engine/app/models/coplan/comment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ def agent?

# Resolves the comment author to a CoPlan::User instance, or nil for
# author types that don't map to a user (cloud_persona, system).
# For both human and local_agent comments, author_id is the user's id
# (local_agent rows store the user behind the API token, not the token
# id), so a single find_by resolves both — agent_name distinguishes
# which agent posted on the user's behalf.
def author
case author_type
when "human"
CoPlan::User.find_by(id: author_id)
when "local_agent"
CoPlan::User.joins(:api_tokens).where(coplan_api_tokens: { id: author_id }).first
end
return unless author_type.in?(%w[human local_agent])

CoPlan::User.find_by(id: author_id)
end

private
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class BackfillLocalAgentCommentAuthorIds < ActiveRecord::Migration[8.1]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ship the backfill as an engine migration

This backfill is added only under the host app's db/migrate, but the code it supports is in the packaged engine. I checked engine/coplan.gemspec, and the gem only packages files under the engine directory ({app,config,db,lib,prompts}), so upgrading a non-repo host with coplan-engine will get the new Comment#author behavior without receiving this migration; existing local_agent comments whose author_id still points at coplan_api_tokens.id will stop resolving to a user. Please put the migration in engine/db/migrate (and copy it into the host as usual) so engine consumers receive the backfill.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in c870c1d. Moved the backfill to engine/db/migrate/ (mirroring the existing backfill_structured_tags precedent) and copied it into the host via co_plan:install:migrations (db/migrate/...backfill_local_agent_comment_author_ids.co_plan.rb), so gem consumers now receive the backfill alongside the engine Comment#author change.

# For local_agent comments whose author_id still points at a
# coplan_api_tokens.id, rewrite it to that token's user_id. This
# pairs with the model change in engine/app/models/coplan/comment.rb:
# Comment#author now resolves via a direct find_by(id:) for both
# human and local_agent comments instead of joining coplan_api_tokens.
#
# Idempotent: once author_id holds a user UUID it won't match any
# coplan_api_tokens.id, so re-runs skip those rows.
def up
CoPlan::Comment.where(author_type: "local_agent").find_each do |comment|
token = CoPlan::ApiToken.find_by(id: comment.author_id)
next unless token

comment.update_columns(author_id: token.user_id) # rubocop:disable Rails/SkipsModelValidations
end
end

def down
# no-op (not reversible)
end
end
19 changes: 19 additions & 0 deletions spec/helpers/coplan/comments_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require "rails_helper"

RSpec.describe CoPlan::CommentsHelper, type: :helper do
describe "#comment_author_name" do
it "renders 'Agent (via User)' for a local_agent comment" do
user = create(:coplan_user, name: "Alice")
comment = create(:comment, author_type: "local_agent", agent_name: "Amp", author_id: user.id)

expect(helper.comment_author_name(comment)).to eq("Amp (via Alice)")
end

it "renders just the user name for a human comment" do
user = create(:coplan_user, name: "Bob")
comment = create(:comment, author_type: "human", author_id: user.id)

expect(helper.comment_author_name(comment)).to eq("Bob")
end
end
end
48 changes: 48 additions & 0 deletions spec/migrations/backfill_local_agent_comment_author_ids_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require "rails_helper"
require CoPlan::Engine.root.join("db/migrate/20260609000000_backfill_local_agent_comment_author_ids.rb")

RSpec.describe BackfillLocalAgentCommentAuthorIds do
subject(:migration) { described_class.new }

before { migration.verbose = false }

it "rewrites local_agent author_id from a token id to the token's user_id" do
user = create(:coplan_user)
token = create(:api_token, user: user)
comment = create(:comment, author_type: "local_agent", agent_name: "Amp", author_id: token.id)

migration.up

expect(comment.reload.author_id).to eq(user.id)
expect(comment.author).to eq(user)
end

it "leaves human comments untouched" do
user = create(:coplan_user)
comment = create(:comment, author_type: "human", author_id: user.id)

migration.up

expect(comment.reload.author_id).to eq(user.id)
end

it "leaves local_agent comments whose author_id is already a user_id untouched" do
user = create(:coplan_user)
comment = create(:comment, author_type: "local_agent", agent_name: "Amp", author_id: user.id)

migration.up

expect(comment.reload.author_id).to eq(user.id)
end

it "is idempotent across repeated runs" do
user = create(:coplan_user)
token = create(:api_token, user: user)
comment = create(:comment, author_type: "local_agent", agent_name: "Amp", author_id: token.id)

migration.up
migration.up

expect(comment.reload.author_id).to eq(user.id)
end
end
19 changes: 19 additions & 0 deletions spec/models/comment_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,25 @@
expect(comment.comment_thread).to eq(thread)
end

describe "#author" do
it "resolves a human comment to its user via direct lookup" do
user = create(:coplan_user)
comment = create(:comment, author_type: "human", author_id: user.id)
expect(comment.author).to eq(user)
end

it "resolves a local_agent comment to its user via direct lookup (no join)" do
user = create(:coplan_user)
comment = create(:comment, author_type: "local_agent", agent_name: "Amp", author_id: user.id)
expect(comment.author).to eq(user)
end

it "returns nil for author types that don't map to a user" do
comment = create(:comment, author_type: "system", author_id: SecureRandom.uuid)
expect(comment.author).to be_nil
end
end

describe "Slack notification callback" do
let(:thread_record) { create(:comment_thread) }

Expand Down
10 changes: 10 additions & 0 deletions spec/requests/api/v1/comments_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
body = JSON.parse(response.body)
expect(body["thread_id"]).to be_present
expect(body["comment_id"]).to be_present

comment = CoPlan::Comment.find(body["comment_id"])
expect(comment.author_type).to eq("local_agent")
expect(comment.author_id).to eq(alice.id)
expect(comment.author_id).not_to eq(alice_token.id)
expect(comment.author).to eq(alice)
end

it "create general comment thread" do
Expand All @@ -49,6 +55,10 @@
expect(response).to have_http_status(:created)
body = JSON.parse(response.body)
expect(body["thread_id"]).to eq(thread_record.id)

comment = CoPlan::Comment.find(body["comment_id"])
expect(comment.author_id).to eq(alice.id)
expect(comment.author_id).not_to eq(alice_token.id)
end

it "reply to nonexistent thread" do
Expand Down
Loading