main ← fix/move-posts-tombstoned-collection
merged 07:00PM - 19 Jun 26 UTC
### What
Moving one or more posts into a `full_topic` topic could raise
`Active…Record::RecordNotUnique` and surface to the user as an HTTP 500 — from
core's `TopicsController#move_posts`, which only rescues `RecordInvalid` /
`RecordNotSaved`, so anything else becomes a 500.
Reported on Meta: https://meta.discourse.org/t/error-500-when-moving-posts/397654
### Why
ActivityPub records are hidden once tombstoned, via `IdentifierValidations`'
`default_scope { where.not(ap_type: Tombstone.type) }`. The row still exists,
though, and still occupies the unique `(model_type, model_id)` index
(`unique_activity_pub_collection_models`).
The `:first_post_moved` / `:post_moved` handlers check
`!topic.activity_pub_object` — which the default scope reports as `nil` for a
tombstoned collection — and call `create_activity_pub_collection!`, which
blindly inserts a second collection for the same topic and collides on the
unique index.
It's data-dependent (the destination topic's collection must have been
tombstoned previously, e.g. via a delete/restore cycle), which matches the
intermittent, "no visible preconditions" reports.
### How
`create_activity_pub_collection!` now looks the collection up `unscoped`,
restores it when tombstoned, and reuses it instead of inserting a duplicate.
This makes the method idempotent for all of its callers, and preserves the
collection's `ap_id` (its federation identity) rather than orphaning it.
### Tests
- Adds a regression test for moving posts into a topic whose collection was
tombstoned.
- A second commit simplifies the `move_posts` topic specs — collapsing the
per-scenario example fan-out into one example each, with `:aggregate_failures`
on the group. Same coverage, 38 → 14 examples, roughly half the runtime.