11"""Tests for cyteonto.cyteonto pure units (no live agents or network)."""
22
33from pathlib import Path
4+ from unittest .mock import AsyncMock , Mock
45
56import numpy as np
7+ import pytest
68
7- from cyteonto .cyteonto import CyteOnto , _api_key_for_provider
9+ from cyteonto import storage
10+ from cyteonto .cyteonto import CyteOnto , _api_key_for_provider , _is_empty
11+ from cyteonto .models import AgentUsage , CellDescription
812
913
1014class TestApiKeyForProvider :
@@ -43,9 +47,7 @@ class TestMatch:
4347 def _instance (self ):
4448 # Bypass __init__ to test the pure matching logic in isolation.
4549 inst = object .__new__ (CyteOnto )
46- inst ._ontology_embeddings = np .array (
47- [[1.0 , 0.0 ], [0.0 , 1.0 ]], dtype = np .float32
48- )
50+ inst ._ontology_embeddings = np .array ([[1.0 , 0.0 ], [0.0 , 1.0 ]], dtype = np .float32 )
4951 inst ._ontology_ids = ["CL:0000001" , "CL:0000002" ]
5052 return inst
5153
@@ -57,9 +59,7 @@ def test_exact_match(self):
5759
5860 def test_below_threshold_returns_none (self ):
5961 inst = self ._instance ()
60- out = inst ._match (
61- np .array ([[0.7 , 0.7 ]], dtype = np .float32 ), min_similarity = 0.99
62- )
62+ out = inst ._match (np .array ([[0.7 , 0.7 ]], dtype = np .float32 ), min_similarity = 0.99 )
6363 assert out [0 ][0 ] is None
6464
6565 def test_one_dimensional_query_reshaped (self ):
@@ -76,3 +76,122 @@ def test_counts_only_files(self, temp_dir: Path):
7676 nested .mkdir ()
7777 (nested / "b.txt" ).write_text ("y" )
7878 assert CyteOnto ._count_files (temp_dir ) == 2
79+
80+
81+ class TestIsEmpty :
82+ def test_empty_string (self ):
83+ assert _is_empty ("" ) is True
84+
85+ def test_whitespace_only (self ):
86+ assert _is_empty (" " ) is True
87+ assert _is_empty ("\t \n " ) is True
88+
89+ def test_non_empty (self ):
90+ assert _is_empty ("T cell" ) is False
91+ assert _is_empty (" NK cell " ) is False
92+
93+
94+ class TestEmbedUserLabelsSkipsEmpty :
95+ @pytest .mark .asyncio
96+ async def test_empty_labels_not_described_and_zero_vectors (self , monkeypatch ):
97+ inst = object .__new__ (CyteOnto )
98+ inst .paths = Mock ()
99+ inst .paths .user_embeddings .return_value = Path ("/tmp/emb.npz" )
100+ inst .paths .user_descriptions .return_value = Path ("/tmp/desc.json" )
101+ inst .llm_key = Mock ()
102+ inst .embd_key = Mock ()
103+ inst .reasoning = False
104+ inst .usage = AgentUsage (agentName = "CyteOnto" )
105+
106+ described = CellDescription (
107+ initialLabel = "T cell" ,
108+ descriptiveName = "CD4+ helper T lymphocyte" ,
109+ function = "Coordinates immune responses" ,
110+ diseaseRelevance = "Autoimmune disease" ,
111+ developmentalStage = "Mature" ,
112+ )
113+
114+ describe_mock = AsyncMock (
115+ return_value = ([described ], AgentUsage (agentName = "CellDescriptionAgent" ))
116+ )
117+ inst ._describe_labels = describe_mock
118+ inst ._embed_with_failover = AsyncMock (
119+ return_value = np .array ([[1.0 , 2.0 ]], dtype = np .float32 )
120+ )
121+
122+ monkeypatch .setattr (storage , "save_descriptions" , lambda * a , ** k : None )
123+ monkeypatch .setattr (storage , "save_user_embeddings" , lambda * a , ** k : None )
124+
125+ result = await inst ._embed_user_labels (
126+ labels = ["T cell" , "" , " " ],
127+ run_id = "run-test" ,
128+ kind = "author" ,
129+ identifier = "author" ,
130+ use_cache = False ,
131+ )
132+
133+ # Only the single non-empty label is sent for description generation.
134+ describe_mock .assert_awaited_once_with (["T cell" ])
135+ # Only the non-empty description sentence is embedded.
136+ embed_arg = inst ._embed_with_failover .await_args .args [0 ]
137+ assert embed_arg == [described .to_sentence ()]
138+
139+ assert result .shape == (3 , 2 )
140+ np .testing .assert_array_equal (result [0 ], np .array ([1.0 , 2.0 ], dtype = np .float32 ))
141+ np .testing .assert_array_equal (result [1 ], np .zeros (2 , dtype = np .float32 ))
142+ np .testing .assert_array_equal (result [2 ], np .zeros (2 , dtype = np .float32 ))
143+
144+
145+ class TestCompareEmptyHandling :
146+ @pytest .mark .asyncio
147+ async def test_empty_positions_get_blank_id_zero_score_empty_method (self ):
148+ inst = object .__new__ (CyteOnto )
149+ inst ._embed_user_labels = AsyncMock (
150+ return_value = np .zeros ((2 , 2 ), dtype = np .float32 )
151+ )
152+ inst ._match = Mock (return_value = [("CL:0000001" , 0.95 ), ("CL:0000002" , 0.80 )])
153+ sim = Mock ()
154+ sim .similarity .return_value = 0.9
155+ inst ._ensure_similarity = Mock (return_value = sim )
156+
157+ df = await inst .compare (
158+ author_labels = ["T cell" , "" ],
159+ algorithms = {"algo0" : ["B cell" , "" ], "algo1" : ["" , "NK cell" ]},
160+ run_id = "run-test" ,
161+ )
162+
163+ def row (algo : str , idx : int ) -> dict :
164+ return df [(df .algorithm == algo ) & (df .pair_index == idx )].iloc [0 ].to_dict ()
165+
166+ # Both labels present -> normal cytescore path.
167+ r = row ("algo0" , 0 )
168+ assert r ["author_ontology_id" ] == "CL:0000001"
169+ assert r ["algorithm_ontology_id" ] == "CL:0000001"
170+ assert r ["cytescore_similarity" ] == 0.9
171+ assert r ["similarity_method" ] == "cytescore"
172+
173+ # Both labels empty -> blank ids, zero scores, empty method.
174+ r = row ("algo0" , 1 )
175+ assert r ["author_ontology_id" ] == ""
176+ assert r ["algorithm_ontology_id" ] == ""
177+ assert r ["author_embedding_similarity" ] == 0.0
178+ assert r ["algorithm_embedding_similarity" ] == 0.0
179+ assert r ["cytescore_similarity" ] == 0.0
180+ assert r ["similarity_method" ] == "empty"
181+
182+ # Only algorithm label empty -> author side kept, algorithm blanked.
183+ r = row ("algo1" , 0 )
184+ assert r ["author_ontology_id" ] == "CL:0000001"
185+ assert r ["author_embedding_similarity" ] == 0.95
186+ assert r ["algorithm_ontology_id" ] == ""
187+ assert r ["algorithm_embedding_similarity" ] == 0.0
188+ assert r ["cytescore_similarity" ] == 0.0
189+ assert r ["similarity_method" ] == "empty"
190+
191+ # Only author label empty -> algorithm side kept, author blanked.
192+ r = row ("algo1" , 1 )
193+ assert r ["author_ontology_id" ] == ""
194+ assert r ["algorithm_ontology_id" ] == "CL:0000002"
195+ assert r ["algorithm_embedding_similarity" ] == 0.80
196+ assert r ["cytescore_similarity" ] == 0.0
197+ assert r ["similarity_method" ] == "empty"
0 commit comments