A replayed INSERT can carry a dropped column's value too, and that path still stores it.
A BEFORE INSERT trigger that returns a copy of an existing row does it:
r := (SELECT t FROM demo t WHERE id = 1); r.id := NEW.id;
RETURN r;
So does an UPDATE that moves the row to another partition while the trigger returns OLD. COPY, MERGE and INSERT ON CONFLICT go the same way.
It can also make REPACK fail. If the columns that are left need no TOAST table, the new heap has none, and on master I get:
ERROR: row is too big: size 16424, maximum size 8160
0001 clears dropped columns in restore_tuple(), so every kind of change is covered, and the block added to prepare_concurrent_update() is no longer needed. 0002 adds a concurrent INSERT to repack_dropped.