From e50172645dcaffb85799d1fa3ac4130bbb781c0d Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Thu, 22 Oct 2026 15:47:09 -0400 Subject: [PATCH 1/4] OpenConceptLab/ocl_online#456 | Print the full diff when the includeMappings list tests fail test_inverse_mappings_are_unchanged failed on master CI twice at 0c4bae20, with a diff truncated to its first difference (checksum key order). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TjM9dGow2KV1Gwr2dWQdph --- core/integration_tests/tests_concepts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/integration_tests/tests_concepts.py b/core/integration_tests/tests_concepts.py index 87215f9c..cbd2b0ce 100644 --- a/core/integration_tests/tests_concepts.py +++ b/core/integration_tests/tests_concepts.py @@ -4631,6 +4631,8 @@ def test_post_429_refused_by_capacity_limit(self, reranker_mock): class ConceptListIncludeMappingsTest(OCLAPITestCase): + maxDiff = None + def setUp(self): super().setUp() self.source = OrganizationSourceFactory() From 5c03b0808d7adeb6ef026ec98bcd3bfce5a2ef3b Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Thu, 22 Oct 2026 16:05:35 -0400 Subject: [PATCH 2/4] OpenConceptLab/ocl_online#456 | Temporary: print the unequal leaves when the inverse-mappings test fails Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TjM9dGow2KV1Gwr2dWQdph --- core/integration_tests/tests_concepts.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/integration_tests/tests_concepts.py b/core/integration_tests/tests_concepts.py index cbd2b0ce..da0207d4 100644 --- a/core/integration_tests/tests_concepts.py +++ b/core/integration_tests/tests_concepts.py @@ -4704,4 +4704,16 @@ def test_inverse_mappings_are_unchanged(self): batched = self.client.get(url).data with patch.object(ConceptAbstractSerializer, 'preload_page', lambda *args: None): per_concept = self.client.get(url).data + if batched != per_concept: # temporary diagnostic, removed before merge + def unequal(left, right, path=''): + if isinstance(left, dict) and isinstance(right, dict): + for key in set(left) | set(right): + yield from unequal(left.get(key), right.get(key), f'{path}.{key}') + elif isinstance(left, list) and isinstance(right, list) and len(left) == len(right): + for index, (lval, rval) in enumerate(zip(left, right)): + yield from unequal(lval, rval, f'{path}[{index}]') + elif left != right: + yield f'{path}: {left!r} ({type(left).__name__}, self-equal={left == left}) != ' \ + f'{right!r} ({type(right).__name__}, self-equal={right == right})' + print('\nDIAG-UNEQUAL', list(unequal(batched, per_concept))[:20]) self.assertEqual(batched, per_concept) From de6304c3ee8e9a2e1832f34780896779ed3923eb Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Thu, 22 Oct 2026 16:12:06 -0400 Subject: [PATCH 3/4] OpenConceptLab/ocl_online#456 | Temporary diagnostic: satisfy pylint Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TjM9dGow2KV1Gwr2dWQdph --- core/integration_tests/tests_concepts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/integration_tests/tests_concepts.py b/core/integration_tests/tests_concepts.py index da0207d4..ab3c1235 100644 --- a/core/integration_tests/tests_concepts.py +++ b/core/integration_tests/tests_concepts.py @@ -4705,6 +4705,7 @@ def test_inverse_mappings_are_unchanged(self): with patch.object(ConceptAbstractSerializer, 'preload_page', lambda *args: None): per_concept = self.client.get(url).data if batched != per_concept: # temporary diagnostic, removed before merge + import operator def unequal(left, right, path=''): if isinstance(left, dict) and isinstance(right, dict): for key in set(left) | set(right): @@ -4713,7 +4714,7 @@ def unequal(left, right, path=''): for index, (lval, rval) in enumerate(zip(left, right)): yield from unequal(lval, rval, f'{path}[{index}]') elif left != right: - yield f'{path}: {left!r} ({type(left).__name__}, self-equal={left == left}) != ' \ - f'{right!r} ({type(right).__name__}, self-equal={right == right})' + yield f'{path}: {left!r} ({type(left).__name__}, self-equal={operator.eq(left, left)}) != ' \ + f'{right!r} ({type(right).__name__}, self-equal={operator.eq(right, right)})' print('\nDIAG-UNEQUAL', list(unequal(batched, per_concept))[:20]) self.assertEqual(batched, per_concept) From 155eaf30b6067ba552bd0c46805dd381604a7bed Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Thu, 22 Oct 2026 16:40:16 -0400 Subject: [PATCH 4/4] OpenConceptLab/ocl_online#456 | Order a concept's mappings by id in API responses The per-concept mapping querysets had no ORDER BY, so two identical requests could list a concept's mappings in different orders once rows were rewritten (the first request saves their checksums). That made test_inverse_mappings_are_unchanged fail intermittently on CI. Both get_mappings implementations now order by id, as the batched path from #949 already does. The test compares plain lists, so a failure shows the differing element, and checks the mapping order. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TjM9dGow2KV1Gwr2dWQdph --- core/concepts/serializers.py | 8 ++++---- core/integration_tests/tests_concepts.py | 21 +++++---------------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/core/concepts/serializers.py b/core/concepts/serializers.py index dbec928f..77774717 100644 --- a/core/concepts/serializers.py +++ b/core/concepts/serializers.py @@ -297,7 +297,7 @@ def get_mappings(self, obj): # pylint: disable=too-many-locals ) if is_collection else obj.get_bidirectional_mappings() if not include_retired: mappings = mappings.filter(retired=False) - return serializer_class(mappings, many=True).data + return serializer_class(mappings.order_by('id'), many=True).data if self.include_direct_mappings: mappings = obj.get_unidirectional_mappings_for_collection( parent_uri, collection_version) if is_collection else obj.get_unidirectional_mappings() @@ -307,7 +307,7 @@ def get_mappings(self, obj): # pylint: disable=too-many-locals mappings = mappings.filter(to_source_url__in=target_repo_urls) if not include_retired: mappings = mappings.filter(retired=False) - return serializer_class(mappings, many=True).data + return serializer_class(mappings.order_by('id'), many=True).data return [] @@ -726,11 +726,11 @@ def get_mappings(self, obj): mappings = obj.get_bidirectional_mappings_for_collection( parent_uri, collection_version ) if is_collection else obj.get_bidirectional_mappings() - return MappingDetailSerializer(mappings, many=True, context=context).data + return MappingDetailSerializer(mappings.order_by('id'), many=True, context=context).data if self.include_direct_mappings: mappings = obj.get_unidirectional_mappings_for_collection( parent_uri, collection_version) if is_collection else obj.get_unidirectional_mappings() - return MappingDetailSerializer(mappings, many=True, context=context).data + return MappingDetailSerializer(mappings.order_by('id'), many=True, context=context).data return [] diff --git a/core/integration_tests/tests_concepts.py b/core/integration_tests/tests_concepts.py index ab3c1235..0c943f14 100644 --- a/core/integration_tests/tests_concepts.py +++ b/core/integration_tests/tests_concepts.py @@ -4631,8 +4631,6 @@ def test_post_429_refused_by_capacity_limit(self, reranker_mock): class ConceptListIncludeMappingsTest(OCLAPITestCase): - maxDiff = None - def setUp(self): super().setUp() self.source = OrganizationSourceFactory() @@ -4704,17 +4702,8 @@ def test_inverse_mappings_are_unchanged(self): batched = self.client.get(url).data with patch.object(ConceptAbstractSerializer, 'preload_page', lambda *args: None): per_concept = self.client.get(url).data - if batched != per_concept: # temporary diagnostic, removed before merge - import operator - def unequal(left, right, path=''): - if isinstance(left, dict) and isinstance(right, dict): - for key in set(left) | set(right): - yield from unequal(left.get(key), right.get(key), f'{path}.{key}') - elif isinstance(left, list) and isinstance(right, list) and len(left) == len(right): - for index, (lval, rval) in enumerate(zip(left, right)): - yield from unequal(lval, rval, f'{path}[{index}]') - elif left != right: - yield f'{path}: {left!r} ({type(left).__name__}, self-equal={operator.eq(left, left)}) != ' \ - f'{right!r} ({type(right).__name__}, self-equal={operator.eq(right, right)})' - print('\nDIAG-UNEQUAL', list(unequal(batched, per_concept))[:20]) - self.assertEqual(batched, per_concept) + # plain lists, so a failure shows the differing element + self.assertEqual(list(batched), list(per_concept)) + for row in batched: + mapping_ids = [int(mapping['uuid']) for mapping in row['mappings']] + self.assertEqual(mapping_ids, sorted(mapping_ids))