I found an area where I don’t totally understand the behavior of the dirty tracking feature in Active Record. It appears as though changes are tracked when I assign a persistent object (already has an id) to the association but not tracked when I assign a new record to the association.
Poking around a little, it's completely an implementation side-effect, and only appears in the exact case you've described. For instance, assigning to a has_many doesn't show up in .changes either:
def test_changed_includes_new_record_assignments
album = Album.create! :title => "Give Up the Ghost"
album.artist = Artist.new :name => "Brandi Carlile"
assert album.changes.any?
end
In the belongs_to case in your example, note that the change does get recorded fairly early (before the before_save callback is invoked), so you're OK using the dirty tracking in callbacks / observers.