@@ -167,6 +167,52 @@ describe('installAttachmentLifecycleHooks — tombstoning', () => {
167167 expect ( engine . updates [ 0 ] . data ) . toMatchObject ( { id : 'f1' , status : 'committed' , deleted_at : null } ) ;
168168 } ) ;
169169
170+ // [#5906] `input.data` is the ONE key an insert payload arrives under — measured
171+ // on the real engine by objectql's `hook-input-shape-contract.test.ts` ("insert
172+ // carries `data` — never `doc`", #5273). The fixture above cannot pin that: it
173+ // supplies `result`, which sits FIRST in the handler's read, so it stays green
174+ // whatever the limbs below it spell. These two carry the weight instead, and the
175+ // NEGATIVE one is the load-bearing half — it goes red the moment the deleted
176+ // `input.doc` alias limb is put back (that limb sat ahead of `data`, so a
177+ // `doc`-only context would be read again).
178+ const tombstonedFile = ( ) => ( {
179+ id : 'f1' ,
180+ key : 'attachments/f1.bin' ,
181+ scope : 'attachments' ,
182+ status : 'deleted' ,
183+ deleted_at : '2026-01-01T00:00:00Z' ,
184+ } ) ;
185+
186+ it ( 'un-tombstones from input.data when the context carries no result' , async ( ) => {
187+ const engine = fakeEngine ( { attachments : [ ] , files : [ tombstonedFile ( ) ] } ) ;
188+ installAttachmentLifecycleHooks ( engine , silentLogger ( ) ) ;
189+
190+ await engine . trigger ( 'afterInsert' , {
191+ object : 'sys_attachment' ,
192+ event : 'afterInsert' ,
193+ input : { data : { file_id : 'f1' } } ,
194+ } ) ;
195+
196+ expect ( engine . updates ) . toHaveLength ( 1 ) ;
197+ expect ( engine . updates [ 0 ] . data ) . toMatchObject ( { id : 'f1' , status : 'committed' , deleted_at : null } ) ;
198+ } ) ;
199+
200+ it ( 'does NOT read an `input.doc` alias — no engine path produces that key' , async ( ) => {
201+ const engine = fakeEngine ( { attachments : [ ] , files : [ tombstonedFile ( ) ] } ) ;
202+ installAttachmentLifecycleHooks ( engine , silentLogger ( ) ) ;
203+
204+ await engine . trigger ( 'afterInsert' , {
205+ object : 'sys_attachment' ,
206+ event : 'afterInsert' ,
207+ // The spelling the deleted limb defended. With it gone the handler finds no
208+ // `file_id` at all, so the tombstone stands.
209+ input : { doc : { file_id : 'f1' } } ,
210+ } ) ;
211+
212+ expect ( engine . updates ) . toHaveLength ( 0 ) ;
213+ expect ( engine . tables . sys_file [ 0 ] . status ) . toBe ( 'deleted' ) ;
214+ } ) ;
215+
170216 it ( 'a failing lookup never blocks the delete (best-effort)' , async ( ) => {
171217 const engine = fakeEngine ( { attachments : [ ] , files : [ ] } ) ;
172218 engine . findOne = async ( ) => {
0 commit comments