@@ -39,8 +39,9 @@ def test_row_has_expected_keys(self):
3939 assert result ["rows" ], "Expected at least one row"
4040 row = result ["rows" ][0 ]
4141 expected_keys = {
42- "id" , "downstream_class" , "total_n" , "connected_n" ,
43- "percent_connected" , "pairwise_connections" , "total_weight" , "avg_weight" ,
42+ "id" , "query_id" , "upstream_class" , "downstream_class" ,
43+ "total_n" , "connected_n" , "percent_connected" ,
44+ "pairwise_connections" , "total_weight" , "avg_weight" ,
4445 }
4546 assert expected_keys .issubset (row .keys ())
4647
@@ -87,8 +88,9 @@ def test_dataframe_has_expected_columns(self):
8788 TEST_CLASS , return_dataframe = True , limit = 1 , force_refresh = True
8889 )
8990 expected_cols = {
90- "id" , "downstream_class" , "total_n" , "connected_n" ,
91- "percent_connected" , "pairwise_connections" , "total_weight" , "avg_weight" ,
91+ "id" , "query_id" , "upstream_class" , "downstream_class" ,
92+ "total_n" , "connected_n" , "percent_connected" ,
93+ "pairwise_connections" , "total_weight" , "avg_weight" ,
9294 }
9395 assert expected_cols .issubset (set (df .columns ))
9496
@@ -129,7 +131,7 @@ def test_parent_class_appears_with_sensible_counts(self, result):
129131 """
130132 from vfbquery .vfb_queries import vc , get_dict_cursor
131133
132- rows = result ["rows" ]
134+ rows = [ r for r in result ["rows" ] if r [ "query_id" ] == TEST_CLASS ]
133135 ids = [r ["id" ] for r in rows ]
134136 assert ids , "Expected at least one row to test against"
135137
@@ -147,7 +149,7 @@ def test_parent_class_appears_with_sensible_counts(self, result):
147149 parent_id = pairs [0 ]["parent" ]
148150 child_id = pairs [0 ]["child" ]
149151 parent_row = next (r for r in rows if r ["id" ] == parent_id )
150- # Sum connected_n across all descendant rows (not just the one returned) .
152+ # Sum connected_n across all descendant rows.
151153 desc_q = (
152154 "MATCH (p:Class {short_form: '%s'})<-[:SUBCLASSOF*1..]-(c:Class) "
153155 "WHERE c.short_form IN %s "
@@ -169,18 +171,67 @@ def test_parent_class_appears_with_sensible_counts(self, result):
169171 )
170172
171173 @pytest .mark .integration
172- def test_total_n_is_constant_across_rows (self , result ):
173- """`total_n` is the queried-side instance count and must be the same
174- for every output row (regression for the previous summed-across-
175- subclasses value).
174+ def test_total_n_constant_within_each_query_class (self , result ):
175+ """In the downstream direction the presynaptic side is the queried
176+ class, so (matching VFB_connect's normalization) `total_n` is the
177+ queried (sub)class instance count: constant within each query block (it
178+ varies between blocks), and `connected_n` never exceeds it.
176179 """
180+ from collections import defaultdict
181+
177182 rows = result ["rows" ]
178183 assert rows , "Expected at least one row"
179- total_ns = {r ["total_n" ] for r in rows }
180- assert len (total_ns ) == 1 , (
181- f"Expected total_n to be constant across rows, got: { total_ns } "
184+ by_query = defaultdict (set )
185+ for r in rows :
186+ assert r ["connected_n" ] <= r ["total_n" ], (
187+ f"connected_n={ r ['connected_n' ]} > total_n={ r ['total_n' ]} "
188+ f"for { r ['id' ]} "
189+ )
190+ by_query [r ["query_id" ]].add (r ["total_n" ])
191+ for qid , totals in by_query .items ():
192+ assert len (totals ) == 1 , (
193+ f"Expected total_n constant within block { qid } , got: { totals } "
194+ )
195+ assert next (iter (totals )) > 0
196+
197+ @pytest .mark .integration
198+ def test_includes_subclass_breakdown (self , result ):
199+ """The result should contain the input term's own rows plus a block of
200+ rows for each subclass that has connectivity instances. Any non-input
201+ query_id must be a genuine subclass of the input term.
202+ """
203+ from vfbquery .vfb_queries import vc , get_dict_cursor
204+
205+ rows = result ["rows" ]
206+ query_ids = {r ["query_id" ] for r in rows }
207+ assert TEST_CLASS in query_ids , "Expected the input term's own rows"
208+
209+ # Full subclass closure (incl. the input term itself).
210+ q = (
211+ "MATCH (sub:Class)-[:SUBCLASSOF*0..]->(:Class {short_form: '%s'}) "
212+ "RETURN collect(DISTINCT sub.short_form) AS ids" % TEST_CLASS
213+ )
214+ subtree_rows = get_dict_cursor ()(vc .nc .commit_list ([q ]))
215+ subtree = set (subtree_rows [0 ]["ids" ]) if subtree_rows else set ()
216+ offenders = [q for q in query_ids if q not in subtree ]
217+ assert not offenders , (
218+ f"query_id(s) not in the input term's subclass closure: { offenders } "
219+ )
220+
221+ # Subclasses of the input term that have connectivity instances.
222+ sub_q = (
223+ "MATCH (sub:Class)-[:SUBCLASSOF*1..]->(:Class {short_form: '%s'}) "
224+ "WHERE (sub)<-[:SUBCLASSOF*0..]-(:Class)<-[:INSTANCEOF]-"
225+ "(:Individual:has_neuron_connectivity) "
226+ "RETURN collect(DISTINCT sub.short_form) AS ids" % TEST_CLASS
227+ )
228+ sub_rows = get_dict_cursor ()(vc .nc .commit_list ([sub_q ]))
229+ connected_subclasses = set (sub_rows [0 ]["ids" ]) if sub_rows else set ()
230+ if not connected_subclasses :
231+ pytest .skip ("Input term has no connectivity-bearing subclasses" )
232+ assert query_ids & connected_subclasses , (
233+ "Expected subclass breakdown rows but none were present"
182234 )
183- assert next (iter (total_ns )) > 0
184235
185236 @pytest .mark .integration
186237 def test_no_rows_above_neuron_root (self , result ):
0 commit comments