@@ -38,7 +38,7 @@ test("inserts into cache based on change", async () => {
3838 // Call updateStateCache
3939 updateStateCache ( {
4040 lix,
41- change : testChange ,
41+ changes : [ testChange ] ,
4242 commit_id : commitId ,
4343 version_id : versionId ,
4444 } ) ;
@@ -109,7 +109,7 @@ test("upserts cache entry on conflict", async () => {
109109 // First insert
110110 updateStateCache ( {
111111 lix,
112- change : initialChange ,
112+ changes : [ initialChange ] ,
113113 commit_id : initialCommitId ,
114114 version_id : versionId ,
115115 } ) ;
@@ -153,7 +153,7 @@ test("upserts cache entry on conflict", async () => {
153153 // Second call should trigger onConflict upsert
154154 updateStateCache ( {
155155 lix,
156- change : updatedChange ,
156+ changes : [ updatedChange ] ,
157157 commit_id : updatedCommitId ,
158158 version_id : versionId ,
159159 } ) ;
@@ -233,7 +233,7 @@ test("moves cache entries to children on deletion, clears when no children remai
233233
234234 updateStateCache ( {
235235 lix,
236- change : createChange ,
236+ changes : [ createChange ] ,
237237 commit_id : "parent-commit" ,
238238 version_id : "parent" ,
239239 } ) ;
@@ -266,17 +266,20 @@ test("moves cache entries to children on deletion, clears when no children remai
266266
267267 updateStateCache ( {
268268 lix,
269- change : deleteFromParentChange ,
269+ changes : [ deleteFromParentChange ] ,
270270 commit_id : "parent-delete-commit" ,
271271 version_id : "parent" ,
272272 } ) ;
273273
274274 // Verify entity moved to child1 and child2, parent entry removed
275+ // Note: We need to exclude tombstones (inheritance_delete_marker = 1) and entries without content
275276 const cacheAfterParentDelete = await intDb
276277 . selectFrom ( "internal_state_cache" )
277278 . selectAll ( )
278279 . select ( sql `json(snapshot_content)` . as ( "snapshot_content" ) )
279280 . where ( "entity_id" , "=" , testEntity )
281+ . where ( "inheritance_delete_marker" , "=" , 0 ) // Exclude tombstones
282+ . where ( "snapshot_content" , "is not" , null ) // Exclude null snapshots
280283 . orderBy ( "version_id" , "asc" )
281284 . execute ( ) ;
282285
@@ -308,7 +311,7 @@ test("moves cache entries to children on deletion, clears when no children remai
308311
309312 updateStateCache ( {
310313 lix,
311- change : deleteFromChild1Change ,
314+ changes : [ deleteFromChild1Change ] ,
312315 commit_id : "child1-delete-commit" ,
313316 version_id : "child1" ,
314317 } ) ;
@@ -319,6 +322,8 @@ test("moves cache entries to children on deletion, clears when no children remai
319322 . selectAll ( )
320323 . select ( sql `json(snapshot_content)` . as ( "snapshot_content" ) )
321324 . where ( "entity_id" , "=" , testEntity )
325+ . where ( "inheritance_delete_marker" , "=" , 0 ) // Exclude tombstones
326+ . where ( "snapshot_content" , "is not" , null ) // Exclude null snapshots
322327 . execute ( ) ;
323328
324329 expect ( cacheAfterChild1Delete ) . toHaveLength ( 1 ) ;
@@ -343,20 +348,34 @@ test("moves cache entries to children on deletion, clears when no children remai
343348
344349 updateStateCache ( {
345350 lix,
346- change : deleteFromChild2Change ,
351+ changes : [ deleteFromChild2Change ] ,
347352 commit_id : "child2-delete-commit" ,
348353 version_id : "child2" ,
349354 } ) ;
350355
351- // Verify entity is completely removed from cache
356+ // Verify tombstones remain in cache (new behavior: tombstones are permanent)
357+ // The important thing is that state_all queries show no active entities
352358 const finalCache = await intDb
353359 . selectFrom ( "internal_state_cache" )
354360 . selectAll ( )
355361 . select ( sql `json(snapshot_content)` . as ( "snapshot_content" ) )
356362 . where ( "entity_id" , "=" , testEntity )
357363 . execute ( ) ;
358364
359- expect ( finalCache ) . toHaveLength ( 0 ) ;
365+ // Should have 3 tombstones (one for each version where we deleted)
366+ expect ( finalCache ) . toHaveLength ( 3 ) ;
367+ expect ( finalCache . every ( c => c . inheritance_delete_marker === 1 ) ) . toBe ( true ) ;
368+ expect ( finalCache . every ( c => c . snapshot_content === null ) ) . toBe ( true ) ;
369+
370+ // More importantly, verify that state_all shows no active entities
371+ const stateAllResults = await lix . db
372+ . selectFrom ( "state_all" )
373+ . selectAll ( )
374+ . where ( "entity_id" , "=" , testEntity )
375+ . execute ( ) ;
376+
377+ // This is what really matters - no visible entities
378+ expect ( stateAllResults ) . toHaveLength ( 0 ) ;
360379} ) ;
361380
362381test ( "handles inheritance chain deletions with tombstones" , async ( ) => {
@@ -403,7 +422,7 @@ test("handles inheritance chain deletions with tombstones", async () => {
403422
404423 updateStateCache ( {
405424 lix,
406- change : createChange ,
425+ changes : [ createChange ] ,
407426 commit_id : "parent-commit-123" ,
408427 version_id : "parent-version" ,
409428 } ) ;
@@ -440,7 +459,7 @@ test("handles inheritance chain deletions with tombstones", async () => {
440459
441460 updateStateCache ( {
442461 lix,
443- change : deleteChange ,
462+ changes : [ deleteChange ] ,
444463 commit_id : "child-commit-456" ,
445464 version_id : "child-version" ,
446465 } ) ;
@@ -519,3 +538,86 @@ test("handles inheritance chain deletions with tombstones", async () => {
519538 // Subchild should show NO entity through state_all (inherits deletion from child)
520539 expect ( subchildStateAll ) . toHaveLength ( 0 ) ;
521540} ) ;
541+
542+ test ( "copied entries retain original commit_id during deletion copy-down" , async ( ) => {
543+ const lix = await openLix ( {
544+ keyValues : [
545+ { key : "lix_deterministic_mode" , value : { enabled : true , bootstrap : true } } ,
546+ ] ,
547+ } ) ;
548+
549+ // Create inheritance chain: parent -> child1, child2
550+ await createVersion ( { lix, id : "parent-cid" , inherits_from_version_id : "global" } ) ;
551+ await createVersion ( { lix, id : "child1-cid" , inherits_from_version_id : "parent-cid" } ) ;
552+ await createVersion ( { lix, id : "child2-cid" , inherits_from_version_id : "parent-cid" } ) ;
553+
554+ const t1 = timestamp ( { lix } ) ;
555+ const entityId = "entity-commit-propagation" ;
556+
557+ // Create in parent with an original commit id
558+ const createChange : LixChangeRaw = {
559+ id : "change-create-cid" ,
560+ entity_id : entityId ,
561+ schema_key : "lix_test" ,
562+ schema_version : "1.0" ,
563+ file_id : "lix" ,
564+ plugin_key : "test_plugin" ,
565+ snapshot_content : JSON . stringify ( { id : entityId , value : "data" } ) ,
566+ created_at : t1 ,
567+ } ;
568+
569+ const originalCommitId = "original-commit-id-001" ;
570+ updateStateCache ( { lix, changes : [ createChange ] , commit_id : originalCommitId , version_id : "parent-cid" } ) ;
571+
572+ const intDb = lix . db as unknown as Kysely < LixInternalDatabaseSchema > ;
573+
574+ // Sanity: parent entry has original commit id
575+ const parentEntry = await intDb
576+ . selectFrom ( "internal_state_cache" )
577+ . selectAll ( )
578+ . where ( "entity_id" , "=" , entityId )
579+ . where ( "version_id" , "=" , "parent-cid" )
580+ . executeTakeFirstOrThrow ( ) ;
581+ expect ( parentEntry . commit_id ) . toBe ( originalCommitId ) ;
582+
583+ // Delete in parent with a different commit id; this should copy entries to children
584+ const t2 = timestamp ( { lix } ) ;
585+ const deleteChange : LixChangeRaw = {
586+ id : "change-delete-cid" ,
587+ entity_id : entityId ,
588+ schema_key : "lix_test" ,
589+ schema_version : "1.0" ,
590+ file_id : "lix" ,
591+ plugin_key : "test_plugin" ,
592+ snapshot_content : null ,
593+ created_at : t2 ,
594+ } ;
595+ const deletionCommitId = "deletion-commit-id-002" ;
596+ updateStateCache ( { lix, changes : [ deleteChange ] , commit_id : deletionCommitId , version_id : "parent-cid" } ) ;
597+
598+ // Verify copied entries exist in both children with the ORIGINAL commit id
599+ const childEntries = await intDb
600+ . selectFrom ( "internal_state_cache" )
601+ . selectAll ( )
602+ . where ( "entity_id" , "=" , entityId )
603+ . where ( "inheritance_delete_marker" , "=" , 0 )
604+ . where ( "snapshot_content" , "is not" , null )
605+ . where ( "version_id" , "in" , [ "child1-cid" , "child2-cid" ] )
606+ . execute ( ) ;
607+
608+ expect ( childEntries ) . toHaveLength ( 2 ) ;
609+ for ( const entry of childEntries ) {
610+ expect ( [ "child1-cid" , "child2-cid" ] ) . toContain ( entry . version_id ) ;
611+ expect ( entry . commit_id ) . toBe ( originalCommitId ) ;
612+ }
613+
614+ // Tombstone in parent should have the deletion commit id
615+ const tombstone = await intDb
616+ . selectFrom ( "internal_state_cache" )
617+ . selectAll ( )
618+ . where ( "entity_id" , "=" , entityId )
619+ . where ( "version_id" , "=" , "parent-cid" )
620+ . where ( "inheritance_delete_marker" , "=" , 1 )
621+ . executeTakeFirstOrThrow ( ) ;
622+ expect ( tombstone . commit_id ) . toBe ( deletionCommitId ) ;
623+ } ) ;
0 commit comments