@@ -13,28 +13,31 @@ class StateControllerSuite extends munit.FunSuite:
1313 // ---- test tree --------------------------------------------------------
1414 // Animalia (2, alternateNames=["animals"])
1515 // Mollusca (51)
16- // Polyplacophora (55, alternateNames=["chitons"])
16+ // Polyplacophora (55, alternateNames=["chitons", "shells"]) ← "shells" shared
1717 // Arthropoda (100)
18- // Decapoda (120, alternateNames=["decapods"])
18+ // Decapoda (120, alternateNames=["decapods", "shells"]) ← "shells" shared
1919 // OldArthropoda (200, acceptedAphiaId=100) ← synonym for Arthropoda
2020 // -----------------------------------------------------------------------
21+ // "shells" is intentionally shared between Polyplacophora and Decapoda to verify
22+ // that the namesMap multimap correctly preserves both nodes and that descendant
23+ // lookups by a shared alternate name return the union of their descendants.
2124
22- private val polyplacophora = WormsNode (" Polyplacophora" , " Class" , 55 , 55 , Seq (" chitons" ), Nil )
23- private val mollusca = WormsNode (" Mollusca" , " Phylum" , 51 , 51 , Nil , Seq (polyplacophora))
24- private val decapoda = WormsNode (" Decapoda" , " Order" , 120 , 120 , Seq (" decapods" ), Nil )
25- private val arthropoda = WormsNode (" Arthropoda" , " Phylum" , 100 , 100 , Nil , Seq (decapoda))
26- private val oldArthropoda = WormsNode (" OldArthropoda" , " Phylum" , 200 , 100 , Nil , Nil )
27- private val root = WormsNode (" Animalia" , " Kingdom" , 2 , 2 , Seq (" animals" ), Seq (mollusca, arthropoda, oldArthropoda))
25+ private val polyplacophora = WormsNode (" Polyplacophora" , " Class" , 55 , 55 , Seq (" chitons" , " shells " ), Nil )
26+ private val mollusca = WormsNode (" Mollusca" , " Phylum" , 51 , 51 , Nil , Seq (polyplacophora))
27+ private val decapoda = WormsNode (" Decapoda" , " Order" , 120 , 120 , Seq (" decapods" , " shells " ), Nil )
28+ private val arthropoda = WormsNode (" Arthropoda" , " Phylum" , 100 , 100 , Nil , Seq (decapoda))
29+ private val oldArthropoda = WormsNode (" OldArthropoda" , " Phylum" , 200 , 100 , Nil , Nil )
30+ private val root = WormsNode (" Animalia" , " Kingdom" , 2 , 2 , Seq (" animals" ), Seq (mollusca, arthropoda, oldArthropoda))
2831
2932 private def cn (name : String , primary : Boolean = true ) = WormsConceptName (name, primary)
3033
3134 private val wormsConcepts = Seq (
32- WormsConcept (2 , 2 , None , Seq (cn(" Animalia" ), cn(" animals" , false )), " Kingdom" , isMarine = Some (true )),
33- WormsConcept (51 , 51 , Some (2 ), Seq (cn(" Mollusca" )), " Phylum" , isMarine = Some (true )),
34- WormsConcept (55 , 55 , Some (51 ), Seq (cn(" Polyplacophora" ), cn(" chitons" , false )), " Class" , isMarine = Some (true )),
35- WormsConcept (100 , 100 , Some (2 ), Seq (cn(" Arthropoda" )), " Phylum" ),
36- WormsConcept (120 , 120 , Some (100 ), Seq (cn(" Decapoda" ), cn(" decapods" , false )), " Order" ),
37- WormsConcept (200 , 100 , Some (2 ), Seq (cn(" OldArthropoda" )), " Phylum" ),
35+ WormsConcept (2 , 2 , None , Seq (cn(" Animalia" ), cn(" animals" , false )), " Kingdom" , isMarine = Some (true )),
36+ WormsConcept (51 , 51 , Some (2 ), Seq (cn(" Mollusca" )), " Phylum" , isMarine = Some (true )),
37+ WormsConcept (55 , 55 , Some (51 ), Seq (cn(" Polyplacophora" ), cn(" chitons" , false ), cn( " shells " , false )), " Class" , isMarine = Some (true )),
38+ WormsConcept (100 , 100 , Some (2 ), Seq (cn(" Arthropoda" )), " Phylum" ),
39+ WormsConcept (120 , 120 , Some (100 ), Seq (cn(" Decapoda" ), cn(" decapods" , false ), cn( " shells " , false )), " Order" ),
40+ WormsConcept (200 , 100 , Some (2 ), Seq (cn(" OldArthropoda" )), " Phylum" ),
3841 )
3942
4043 private val testData = Data (root, wormsConcepts)
@@ -55,12 +58,13 @@ class StateControllerSuite extends munit.FunSuite:
5558 // ---- findAllNames / countAllNames -------------------------------------
5659
5760 test(" findAllNames returns paginated results with correct total" ):
58- // The tree has 9 distinct names: Animalia, animals, Arthropoda, chitons, Decapoda,
59- // decapods, Mollusca, OldArthropoda, Polyplacophora
61+ // 10 distinct names: Animalia, animals, Arthropoda, chitons, Decapoda,
62+ // decapods, Mollusca, OldArthropoda, Polyplacophora, shells
63+ // ("shells" is shared between Polyplacophora and Decapoda but counts once)
6064 val page = StateController .findAllNames(3 , 0 ).getOrElse(fail(" expected Right" ))
6165 assertEquals(page.limit, 3 )
6266 assertEquals(page.offset, 0 )
63- assertEquals(page.total, 9 )
67+ assertEquals(page.total, 10 )
6468 assertEquals(page.items.size, 3 )
6569
6670 test(" findAllNames honours offset" ):
@@ -69,7 +73,7 @@ class StateControllerSuite extends munit.FunSuite:
6973 assertEquals(page.items.size, 3 )
7074
7175 test(" countAllNames returns total number of distinct names" ):
72- assertEquals(StateController .countAllNames().getOrElse(fail(" expected Right" )), 9 )
76+ assertEquals(StateController .countAllNames().getOrElse(fail(" expected Right" )), 10 )
7377
7478 // ---- queryNames -------------------------------------------------------
7579
@@ -114,6 +118,26 @@ class StateControllerSuite extends munit.FunSuite:
114118 test(" descendantNames returns NotFound for unknown name" ):
115119 assert(StateController .descendantNames(" Unknown" ).isLeft)
116120
121+ test(" descendantNames via a shared alternate name returns union of descendants from all matching nodes" ):
122+ // "shells" is an alternate name for both Polyplacophora (55) and Decapoda (120).
123+ // Before the multimap fix, whichever node was inserted last would silently overwrite
124+ // the other; the "lost" node's descendants would be missing from the result.
125+ val names = StateController .descendantNames(" shells" ).getOrElse(fail(" expected Right" ))
126+ assert(names.contains(" Polyplacophora" ), s " expected Polyplacophora in $names" )
127+ assert(names.contains(" Decapoda" ), s " expected Decapoda in $names" )
128+
129+ test(" findNodesByName returns all nodes sharing a name" ):
130+ val nodes = State .data.get.findNodesByName(" shells" )
131+ assertEquals(nodes.size, 2 )
132+ assert(nodes.map(_.name).toSet == Set (" Polyplacophora" , " Decapoda" ))
133+
134+ test(" findNodeByName on a shared alternate name returns an accepted node" ):
135+ val nodeOpt = State .data.get.findNodeByName(" shells" )
136+ assert(nodeOpt.isDefined)
137+ val node = nodeOpt.get
138+ assert(node.name == " Polyplacophora" || node.name == " Decapoda" )
139+ assert(node.isAccepted)
140+
117141 // ---- ancestorNames ----------------------------------------------------
118142
119143 test(" ancestorNames returns full path from root to node" ):
0 commit comments