@@ -18,6 +18,126 @@ protected override IMiniLcmApi GetApi(SyncFixture fixture)
1818 {
1919 return fixture . CrdtApi ;
2020 }
21+
22+ // These delete-win cases live only in the CRDT subclass: the CRDT deletion must win when an object it
23+ // deleted is still edited from the other side, whereas FwData intentionally still throws on a missing target.
24+
25+ [ Fact ]
26+ public async Task SyncFull_EntryEditedButDeletedInCrdt_DoesNotThrow ( )
27+ {
28+ var entry = await Api . CreateEntry ( new ( ) { Id = Guid . NewGuid ( ) , LexemeForm = { { "en" , "victim" } } } ) ;
29+ await Api . DeleteEntry ( entry . Id ) ;
30+ var after = entry . Copy ( ) ;
31+ after . CitationForm [ "en" ] = "edited" ;
32+
33+ await EntrySync . SyncFull ( entry , after , Api ) ;
34+
35+ ( await Api . GetEntry ( entry . Id ) ) . Should ( ) . BeNull ( ) ;
36+ }
37+
38+ [ Fact ]
39+ public async Task SyncFull_SenseAddedToEntryDeletedInCrdt_DoesNotThrow ( )
40+ {
41+ var entry = await Api . CreateEntry ( new ( ) { Id = Guid . NewGuid ( ) , LexemeForm = { { "en" , "victim" } } } ) ;
42+ await Api . DeleteEntry ( entry . Id ) ;
43+ var after = entry . Copy ( ) ;
44+ after . Senses . Add ( new Sense { Id = Guid . NewGuid ( ) , Gloss = { { "en" , "gloss" } } } ) ;
45+
46+ await EntrySync . SyncFull ( entry , after , Api ) ;
47+
48+ ( await Api . GetEntry ( entry . Id ) ) . Should ( ) . BeNull ( ) ;
49+ }
50+
51+ [ Fact ]
52+ public async Task SyncFull_SenseEditedButDeletedInCrdt_DoesNotThrow ( )
53+ {
54+ var entry = await Api . CreateEntry ( new ( )
55+ {
56+ Id = Guid . NewGuid ( ) ,
57+ LexemeForm = { { "en" , "victim" } } ,
58+ Senses = [ new Sense { Id = Guid . NewGuid ( ) , Gloss = { { "en" , "gloss" } } } ]
59+ } ) ;
60+ await Api . DeleteSense ( entry . Id , entry . Senses [ 0 ] . Id ) ;
61+ var after = entry . Copy ( ) ;
62+ after . Senses [ 0 ] . Gloss [ "en" ] = "edited" ;
63+
64+ await EntrySync . SyncFull ( entry , after , Api ) ;
65+
66+ var actual = await Api . GetEntry ( entry . Id ) ;
67+ actual . Should ( ) . NotBeNull ( ) ;
68+ actual . Senses . Should ( ) . BeEmpty ( ) ;
69+ }
70+
71+ [ Fact ]
72+ public async Task SyncFull_ExampleSentenceEditedButDeletedInCrdt_DoesNotThrow ( )
73+ {
74+ var entry = await Api . CreateEntry ( new ( )
75+ {
76+ Id = Guid . NewGuid ( ) ,
77+ LexemeForm = { { "en" , "victim" } } ,
78+ Senses =
79+ [
80+ new Sense
81+ {
82+ Id = Guid . NewGuid ( ) ,
83+ Gloss = { { "en" , "gloss" } } ,
84+ ExampleSentences = [ new ExampleSentence { Id = Guid . NewGuid ( ) , Sentence = { { "en" , new RichString ( "sentence" ) } } } ]
85+ }
86+ ]
87+ } ) ;
88+ var sense = entry . Senses [ 0 ] ;
89+ await Api . DeleteExampleSentence ( entry . Id , sense . Id , sense . ExampleSentences [ 0 ] . Id ) ;
90+ var after = entry . Copy ( ) ;
91+ after . Senses [ 0 ] . ExampleSentences [ 0 ] . Sentence [ "en" ] = new RichString ( "edited" ) ;
92+
93+ await EntrySync . SyncFull ( entry , after , Api ) ;
94+
95+ var actual = await Api . GetEntry ( entry . Id ) ;
96+ actual . Should ( ) . NotBeNull ( ) ;
97+ actual . Senses [ 0 ] . ExampleSentences . Should ( ) . BeEmpty ( ) ;
98+ }
99+
100+ [ Fact ]
101+ public async Task SyncFull_ComplexFormComponentReferencingEntryDeletedInCrdt_DoesNotThrow ( )
102+ {
103+ var component = await Api . CreateEntry ( new ( ) { Id = Guid . NewGuid ( ) , LexemeForm = { { "en" , "component" } } } ) ;
104+ var complexForm = await Api . CreateEntry ( new ( ) { Id = Guid . NewGuid ( ) , LexemeForm = { { "en" , "complexForm" } } } ) ;
105+ await Api . DeleteEntry ( component . Id ) ;
106+ var after = complexForm . Copy ( ) ;
107+ after . Components . Add ( ComplexFormComponent . FromEntries ( complexForm , component ) ) ;
108+
109+ await EntrySync . SyncFull ( complexForm , after , Api ) ;
110+
111+ ( await Api . GetEntry ( component . Id ) ) . Should ( ) . BeNull ( ) ;
112+ ( await Api . GetEntry ( complexForm . Id ) ) ! . Components . Should ( ) . BeEmpty ( ) ;
113+ }
114+
115+ [ Fact ]
116+ public async Task SyncFull_ComplexFormComponentReorderedButEntryDeletedInCrdt_DoesNotThrow ( )
117+ {
118+ var componentA = await Api . CreateEntry ( new ( ) { Id = Guid . NewGuid ( ) , LexemeForm = { { "en" , "a" } } } ) ;
119+ var componentB = await Api . CreateEntry ( new ( ) { Id = Guid . NewGuid ( ) , LexemeForm = { { "en" , "b" } } } ) ;
120+ var complexForm = new Entry { Id = Guid . NewGuid ( ) , LexemeForm = { { "en" , "complexForm" } } } ;
121+ complexForm . Components =
122+ [
123+ ComplexFormComponent . FromEntries ( complexForm , componentA ) ,
124+ ComplexFormComponent . FromEntries ( complexForm , componentB ) ,
125+ ] ;
126+ var before = await Api . CreateEntry ( complexForm ) ;
127+ await Api . DeleteEntry ( before . Id ) ;
128+
129+ // Id-less components (as FwData produces them) force the move to resolve the now-deleted component — the case that used to throw.
130+ var after = before . Copy ( ) ;
131+ after . Components =
132+ [
133+ new ComplexFormComponent { ComplexFormEntryId = before . Id , ComponentEntryId = componentB . Id , ComponentHeadword = "b" } ,
134+ new ComplexFormComponent { ComplexFormEntryId = before . Id , ComponentEntryId = componentA . Id , ComponentHeadword = "a" } ,
135+ ] ;
136+
137+ await EntrySync . SyncFull ( before , after , Api ) ;
138+
139+ ( await Api . GetEntry ( before . Id ) ) . Should ( ) . BeNull ( ) ;
140+ }
21141}
22142
23143public class FwDataEntrySyncTests ( ExtraWritingSystemsSyncFixture fixture ) : EntrySyncTestsBase ( fixture )
0 commit comments