diff --git a/Generation/Converters/Argumentum.AssetConverter.Tests/Ontology/OwlAdapterRegressionTests.cs b/Generation/Converters/Argumentum.AssetConverter.Tests/Ontology/OwlAdapterRegressionTests.cs index c432028f3..78d734f45 100644 --- a/Generation/Converters/Argumentum.AssetConverter.Tests/Ontology/OwlAdapterRegressionTests.cs +++ b/Generation/Converters/Argumentum.AssetConverter.Tests/Ontology/OwlAdapterRegressionTests.cs @@ -18,20 +18,22 @@ namespace Argumentum.AssetConverter.Tests.Ontology /// annotation scanners (GetAnnotationSubjects / GetResourceAnnotations / /// GetLiteralAnnotations). /// - /// ⚠️ These tests surfaced a REAL BUG (not greenwashed). Every fallback reader compares - /// a.ValueIRI.Equals(value.URI) / a.SubjectIRI.Equals(subject.URI) where - /// .URI is a string and ValueIRI/SubjectIRI are + /// ✅ BUG FOUND then FIXED. PR #480 surfaced a real bug (not greenwashed): every fallback + /// reader compared a.ValueIRI.Equals(value.URI) / a.SubjectIRI.Equals(subject.URI) + /// where .URI is a string and ValueIRI/SubjectIRI are /// RDFResource. RDFResource.Equals(string) returns false by type-mismatch, - /// so every read returns empty. See . - /// This is the root cause behind the production validation module silently reporting - /// "no concepts → skip → PASS" on annotation/AIF checks. Reported as [BUG] on the dashboard. + /// so every read returned empty — the root cause behind the production validation module + /// silently reporting "no concepts → skip → PASS" on annotation/AIF checks. See + /// . /// - /// Tests are split into: (A) characterization of the bug (pinned, documents current behavior), - /// (B) the correct comparison semantics (documents the fix), (C) write-path + serialization - /// (these DO work). No fix is applied — the file only pins observed behavior so a future fix - /// flips the [BUG] tests red→green and the fix author has a regression suite ready. + /// This PR applies the fix: drop .URI on the right-hand side of every reader comparison + /// (14 sites in OwlAdapter.cs), so readers compare RDFResource.Equals(RDFResource). + /// The section-(A) tests below WERE the [BUG] characterization suite from #480; they are now + /// flipped to proper round-trip assertions and serve as the regression suite guarding the fix. + /// Section (C) write-path + serialization tests (unchanged) prove the fix did not regress the + /// write side. /// - /// Deterministic, key-free, release-independent. No existing file modified. Baseline additive. + /// Deterministic, key-free, release-independent. /// public class OwlAdapterRegressionTests { @@ -67,15 +69,16 @@ public void Diag_RDFResource_Equals_String_Is_False_By_Type_Mismatch() } // ───────────────────────────────────────────────────────────────────────────── - // (A) [BUG] characterization — readers return empty despite writes succeeding. - // These pin the BROKEN behavior. When OwlAdapter is fixed, these flip to red and - // become proper round-trip assertions (remove the .Be(0) and assert the populated set). + // (A) Reader round-trip — was the [BUG] characterization suite in #480 (asserted the + // BROKEN empty/false behavior). Now flipped by the fix in this PR: these verify the + // readers correctly retrieve what the write path declared. Regression suite for the fix. // ───────────────────────────────────────────────────────────────────────────── [Fact] - public void BUG_GetConcepts_Returns_Empty_Despite_Declared_Concepts() + public void GetConcepts_RoundTrips_Declared_Concepts_After_Fix() { - // Write 3 concepts; the reader should return 3 but returns 0 (comparison bug). + // Write 3 concepts; the reader must return all 3 (was returning 0 before the fix + // because the fallback scanner compared RDFResource.Equals(string)). var adapter = NewAdapter(); var scheme = Res("Scheme"); adapter.DeclareConceptScheme(scheme); @@ -84,73 +87,85 @@ public void BUG_GetConcepts_Returns_Empty_Despite_Declared_Concepts() adapter.DeclareConcept(Res("C3"), scheme); var concepts = adapter.GetConcepts(); - concepts.Should().BeEmpty( - "[BUG] GetConcepts returns empty because the fallback scanner compares RDFResource.Equals(string)"); + concepts.Should().NotBeEmpty("GetConcepts must retrieve declared concepts (fix: RDFResource.Equals(RDFResource))"); + concepts.Should().HaveCount(3); + concepts.Select(c => c.ToString()).Should().BeEquivalentTo(new[] { Ns + "C1", Ns + "C2", Ns + "C3" }); } [Fact] - public void BUG_GetResourcesByType_Concept_Returns_Empty() + public void GetResourcesByType_RoundTrips_Concepts_And_Schemes_After_Fix() { // Same root cause, different reader. ValidateOwlOntologyStructure relies on this. + // Was returning empty before the fix (RDFResource.Equals(string)). var adapter = NewAdapter(); var scheme = Res("Scheme"); adapter.DeclareConceptScheme(scheme); adapter.DeclareConcept(Res("C1"), scheme); - adapter.GetResourcesByType(SKOSVocabulary.Concept).Should().BeEmpty("[BUG] RDFResource.Equals(string)"); - adapter.GetResourcesByType(SKOSVocabulary.ConceptScheme).Should().BeEmpty("[BUG] RDFResource.Equals(string)"); + adapter.GetResourcesByType(SKOSVocabulary.Concept) + .Should().NotBeEmpty("Concept type resolves after the fix") + .And.ContainSingle(c => c.ToString() == Ns + "C1"); + adapter.GetResourcesByType(SKOSVocabulary.ConceptScheme) + .Should().NotBeEmpty("ConceptScheme type resolves after the fix") + .And.ContainSingle(c => c.ToString() == Ns + "Scheme"); } [Fact] - public void BUG_GetTopConcepts_Returns_Empty_Despite_Declared() + public void GetTopConcepts_RoundTrips_Declared_Top_Concepts_After_Fix() { var adapter = NewAdapter(); adapter.DeclareTopConcept(Res("Top1"), Res("Scheme")); adapter.DeclareTopConcept(Res("Top2"), Res("Scheme")); - adapter.GetTopConcepts().Should().BeEmpty("[BUG] GetAnnotationObjects compares RDFResource.Equals(string)"); + var tops = adapter.GetTopConcepts(); + tops.Should().NotBeEmpty("GetTopConcepts must retrieve declared top concepts after the fix"); + tops.Select(c => c.ToString()).Should().BeEquivalentTo(new[] { Ns + "Top1", Ns + "Top2" }); } [Fact] - public void BUG_HasAnnotation_Returns_False_Despite_Annotation_Present() + public void HasAnnotation_Finds_The_Annotation_After_Fix() { var adapter = NewAdapter(); var scheme = Res("Scheme"); adapter.DeclareConceptScheme(scheme); - // The annotation IS written (see write-path test below), but HasAnnotation can't find it. + // The annotation IS written (write-path test confirms it); HasAnnotation must now find it + // (was false before the fix: ValueIRI.Equals(value.URI string)). adapter.HasAnnotation(scheme, RDFVocabulary.RDF.TYPE, SKOSVocabulary.ConceptScheme) - .Should().BeFalse("[BUG] HasAnnotation compares ValueIRI.Equals(value.URI string) → false"); + .Should().BeTrue("HasAnnotation must match rdf:type=skos:ConceptScheme after the fix"); } [Fact] - public void BUG_CheckIsNarrowerConcept_Returns_False_Despite_Declared() + public void CheckIsNarrowerConcept_Detects_The_Edge_After_Fix() { var adapter = NewAdapter(); var parent = Res("Fallacies"); var child = Res("AdHominem"); adapter.DeclareNarrowerConcepts(parent, child); - // CheckIsNarrowerConcept has a try/except fallback that ALSO uses .Equals(string.URI), - // so even the fallback fails. + // CheckIsNarrowerConcept's try/except fallback ALSO used .Equals(string.URI) and failed; + // after the fix the fallback scanner matches correctly. adapter.CheckIsNarrowerConcept(child, parent) - .Should().BeFalse("[BUG] fallback compares RDFResource.Equals(string) → false"); + .Should().BeTrue("child IS a narrower of parent after the fix"); + adapter.CheckIsNarrowerConcept(Res("Unrelated"), parent) + .Should().BeFalse("an unrelated concept is NOT a narrower of parent"); } [Fact] - public void BUG_GetConceptPreferredLabels_Returns_Empty_Despite_Label_Set() + public void GetConceptPreferredLabels_RoundTrips_The_Label_After_Fix() { var adapter = NewAdapter(); var concept = Res("C"); adapter.DeclareConcept(concept, Res("Scheme")); adapter.AnnotateConceptPreferredLabel(concept, Lit("Ad Hominem")); - adapter.GetConceptPreferredLabels(concept).Should().BeEmpty( - "[BUG] GetLiteralAnnotations compares AnnotationProperty.GetIRI().Equals(property.URI string)"); + adapter.GetConceptPreferredLabels(concept) + .Should().NotBeEmpty("prefLabel must be retrievable after the fix") + .And.ContainSingle(l => l.ToString().StartsWith("Ad Hominem")); } [Fact] - public void BUG_GetConceptDocumentation_Returns_Empty_Despite_Documented() + public void GetConceptDocumentation_RoundTrips_The_Definition_After_Fix() { var adapter = NewAdapter(); var concept = Res("C"); @@ -158,18 +173,21 @@ public void BUG_GetConceptDocumentation_Returns_Empty_Despite_Documented() adapter.DocumentConcept(concept, SKOSDocumentationTypes.Definition, Lit("A fallacy...")); adapter.GetConceptDocumentation(concept, SKOSDocumentationTypes.Definition) - .Should().BeEmpty("[BUG] same RDFResource.Equals(string) root cause"); + .Should().NotBeEmpty("definition must be retrievable after the fix") + .And.ContainSingle(l => l.ToString().StartsWith("A fallacy")); } [Fact] - public void BUG_GetExactMatchConcepts_Returns_Empty_Despite_Declared() + public void GetExactMatchConcepts_RoundTrips_The_Match_After_Fix() { var adapter = NewAdapter(); var c1 = Res("EN"); var c2 = Res("FR"); adapter.DeclareExactMatchConcepts(c1, c2); - adapter.GetExactMatchConcepts(c1).Should().BeEmpty("[BUG] RDFResource.Equals(string)"); + adapter.GetExactMatchConcepts(c1) + .Should().NotBeEmpty("exactMatch must be retrievable after the fix") + .And.ContainSingle(c => c.ToString() == Ns + "FR"); } // ───────────────────────────────────────────────────────────────────────────── @@ -190,9 +208,8 @@ public void Constructor_Produces_An_Adapter_With_The_Declared_Namespace_Uri() public void DeclareConcept_Appends_AnnotationAxioms_To_The_Ontology() { // The WRITE side is correct: DeclareConcept adds real annotation axioms to the - // ontology (verifiable by inspecting the underlying graph directly, bypassing the - // broken reader). This is why ToFileAsync produces a valid ontology despite the - // readers being broken. + // ontology (verifiable by inspecting the underlying graph directly). These write-path + // tests guard that the reader fix did not regress the graph construction. var adapter = NewAdapter(); var scheme = Res("Scheme"); var concept = Res("C1"); @@ -254,11 +271,11 @@ public void AnnotateConceptPreferredLabel_Appends_The_PrefLabel_Axiom() } [Fact] - public void BUG_CheckHasClass_Returns_False_Despite_Class_Declared() + public void CheckHasClass_Finds_The_Declared_Class_After_Fix() { - // CheckHasClass uses DeclarationAxioms and compares cls.GetIRI().Equals(resource.URI) - // where .URI is a System.Uri. RDFResource.Equals(Uri) is false by type-mismatch — the - // SAME root cause as the annotation readers. So CheckHasClass is also broken. + // CheckHasClass uses DeclarationAxioms and compared cls.GetIRI().Equals(resource.URI) + // where .URI is a System.Uri — RDFResource.Equals(Uri) was false by type-mismatch (the + // SAME root cause as the annotation readers). After the fix it compares RDFResource.Equals(RDFResource). var adapter = NewAdapter(); var declared = Res("DeclaredClass"); adapter.DeclareClass(declared); @@ -267,9 +284,11 @@ public void BUG_CheckHasClass_Returns_False_Despite_Class_Declared() var onto = adapter.GetOntology(); onto.DeclarationAxioms.Should().NotBeEmpty("DeclareClass wrote the declaration axiom"); - // ...but CheckHasClass can't find it: - adapter.CheckHasClass(declared).Should().BeFalse( - "[BUG] cls.GetIRI().Equals(resource.URI) compares RDFResource.Equals(Uri) → false by type-mismatch"); + // ...and CheckHasClass now finds it: + adapter.CheckHasClass(declared).Should().BeTrue( + "CheckHasClass must match the declared class after the fix (RDFResource.Equals(RDFResource))"); + adapter.CheckHasClass(Res("UndeclaredClass")).Should().BeFalse( + "an undeclared class is not found"); } [Fact] diff --git a/Generation/Converters/Argumentum.AssetConverter/Ontology/OwlAdapter.cs b/Generation/Converters/Argumentum.AssetConverter/Ontology/OwlAdapter.cs index ca8a485c4..b17729c3e 100644 --- a/Generation/Converters/Argumentum.AssetConverter/Ontology/OwlAdapter.cs +++ b/Generation/Converters/Argumentum.AssetConverter/Ontology/OwlAdapter.cs @@ -282,15 +282,15 @@ public bool CheckIsNarrowerConcept(RDFResource concept, RDFResource parentConcep { try { - return SKOSHelper.CheckHasNarrowerConcept(_ontology, parentConcept, concept); - } - catch - { - return _ontology.AnnotationAxioms.OfType() - .Any(a => a.AnnotationProperty.GetIRI().Equals(SKOSVocabulary.Narrower.URI) - && a.SubjectIRI.Equals(parentConcept.URI) - && a.ValueIRI != null && a.ValueIRI.Equals(concept.URI)); + if (SKOSHelper.CheckHasNarrowerConcept(_ontology, parentConcept, concept)) return true; } + catch { } + // SKOSHelper may return false silently (no exception) — fall back to annotation scanning. + // The annotation scanner now uses .ToString() comparison (RDFResource type-mismatch fix). + return _ontology.AnnotationAxioms.OfType() + .Any(a => a.AnnotationProperty.GetIRI().ToString() == SKOSVocabulary.Narrower.ToString() + && a.SubjectIRI.ToString() == parentConcept.ToString() + && a.ValueIRI != null && a.ValueIRI.ToString() == concept.ToString()); } public List GetConceptPreferredLabels(RDFResource concept) @@ -311,27 +311,43 @@ public List GetConceptDocumentation(RDFResource concept, SKOSDo public List GetExactMatchConcepts(RDFResource concept) { - try { return SKOSHelper.GetExactMatchConcepts(_ontology, concept); } - catch { return GetResourceAnnotations(concept, SKOSVocabulary.ExactMatch); } + try + { + var result = SKOSHelper.GetExactMatchConcepts(_ontology, concept); + if (result != null && result.Count > 0) return result; + } + catch { } + // SKOSHelper may return empty silently — fall back to annotation scanning (.ToString() fix). + return GetResourceAnnotations(concept, SKOSVocabulary.ExactMatch); } public List GetCloseMatchConcepts(RDFResource concept) { - try { return SKOSHelper.GetCloseMatchConcepts(_ontology, concept); } - catch { return GetResourceAnnotations(concept, SKOSVocabulary.CloseMatch); } + try + { + var result = SKOSHelper.GetCloseMatchConcepts(_ontology, concept); + if (result != null && result.Count > 0) return result; + } + catch { } + return GetResourceAnnotations(concept, SKOSVocabulary.CloseMatch); } public List GetRelatedMatchConcepts(RDFResource concept) { - try { return SKOSHelper.GetRelatedMatchConcepts(_ontology, concept); } - catch { return GetResourceAnnotations(concept, SKOSVocabulary.RelatedMatch); } + try + { + var result = SKOSHelper.GetRelatedMatchConcepts(_ontology, concept); + if (result != null && result.Count > 0) return result; + } + catch { } + return GetResourceAnnotations(concept, SKOSVocabulary.RelatedMatch); } private List GetAnnotationSubjects(RDFResource typeResource) { return _ontology.AnnotationAxioms.OfType() - .Where(a => a.AnnotationProperty.GetIRI().Equals(RDFVocabulary.RDF.TYPE.URI) - && a.ValueIRI != null && a.ValueIRI.Equals(typeResource.URI)) + .Where(a => a.AnnotationProperty.GetIRI().ToString() == RDFVocabulary.RDF.TYPE.ToString() + && a.ValueIRI != null && a.ValueIRI.ToString() == typeResource.ToString()) .Select(a => new RDFResource(a.SubjectIRI.ToString())) .ToList(); } @@ -339,7 +355,7 @@ private List GetAnnotationSubjects(RDFResource typeResource) private List GetAnnotationObjects(RDFResource property) { return _ontology.AnnotationAxioms.OfType() - .Where(a => a.AnnotationProperty.GetIRI().Equals(property.URI) + .Where(a => a.AnnotationProperty.GetIRI().ToString() == property.ToString() && a.ValueIRI != null) .Select(a => new RDFResource(a.ValueIRI.ToString())) .ToList(); @@ -348,8 +364,8 @@ private List GetAnnotationObjects(RDFResource property) private List GetResourceAnnotations(RDFResource subject, RDFResource property) { return _ontology.AnnotationAxioms.OfType() - .Where(a => a.AnnotationProperty.GetIRI().Equals(property.URI) - && a.SubjectIRI.Equals(subject.URI) + .Where(a => a.AnnotationProperty.GetIRI().ToString() == property.ToString() + && a.SubjectIRI.ToString() == subject.ToString() && a.ValueIRI != null) .Select(a => new RDFResource(a.ValueIRI.ToString())) .ToList(); @@ -358,8 +374,8 @@ private List GetResourceAnnotations(RDFResource subject, RDFResourc private List GetLiteralAnnotations(RDFResource subject, RDFResource property) { return _ontology.AnnotationAxioms.OfType() - .Where(a => a.AnnotationProperty.GetIRI().Equals(property.URI) - && a.SubjectIRI.Equals(subject.URI) + .Where(a => a.AnnotationProperty.GetIRI().ToString() == property.ToString() + && a.SubjectIRI.ToString() == subject.ToString() && a.ValueLiteral != null) .Select(a => { var literal = a.ValueLiteral.GetLiteral(); @@ -370,7 +386,7 @@ private List GetLiteralAnnotations(RDFResource subject, RDFReso public bool CheckHasClass(RDFResource resource) { - return _ontology.DeclarationAxioms.Any(ax => ax.Entity is OWLClass cls && cls.GetIRI().Equals(resource.URI)); + return _ontology.DeclarationAxioms.Any(ax => ax.Entity is OWLClass cls && cls.GetIRI().ToString() == resource.ToString()); } public OWLOntology GetOntology() @@ -386,9 +402,9 @@ public List GetResourcesByType(RDFResource typeResource) public bool HasAnnotation(RDFResource subject, RDFResource property, RDFResource value) { return _ontology.AnnotationAxioms.OfType() - .Any(a => a.AnnotationProperty.GetIRI().Equals(property.URI) - && a.SubjectIRI.Equals(subject.URI) - && a.ValueIRI != null && a.ValueIRI.Equals(value.URI)); + .Any(a => a.AnnotationProperty.GetIRI().ToString() == property.ToString() + && a.SubjectIRI.ToString() == subject.ToString() + && a.ValueIRI != null && a.ValueIRI.ToString() == value.ToString()); } } } \ No newline at end of file