Skip to content

test: Add Tests for ExoRDF to RDF/RDFS Mapping #368

@kitelev

Description

@kitelev

Context & Problem Statement

Background

ExoRDF to RDF/RDFS mapping implementation needs comprehensive test coverage to ensure correctness, performance, and backward compatibility. Tests should cover URI construction, vocabulary mapping, triple generation, and SPARQL inference.

Current State

  • No tests exist for RDF/RDFS mapping (not yet implemented)
  • Need unit tests for all new services and utilities
  • Need SPARQL query tests using standard RDF/RDFS predicates
  • Need performance tests for triple generation overhead
  • Need E2E tests demonstrating semantic queries

Desired State

  • Unit test coverage >80% for all RDF mapping code
  • SPARQL query tests using rdfs:subClassOf, rdf:type, etc.
  • Performance tests verify acceptable overhead (<5ms per asset)
  • E2E tests demonstrate end-to-end semantic queries
  • Edge case tests for missing UIDs, invalid URLs, etc.

User Impact

  • High confidence in RDF/RDFS mapping correctness
  • Regression prevention for semantic interoperability
  • Performance guarantees for production use
  • Clear examples of RDF/RDFS query patterns

Acceptance Criteria

Functional Requirements

Given URIConstructionService
When running unit tests
Then coverage is >80% with all edge cases tested

Given RDFVocabularyMapper
When running unit tests
Then all class and property mappings are tested

Given InMemoryTripleStore with RDF/RDFS mapping
When running unit tests
Then vocabulary triple generation and asset triple generation are tested

Given SPARQL queries using rdfs:subClassOf
When executing test queries
Then transitive inference returns correct results

Given SPARQL queries using rdf:type
When executing test queries
Then mapped instance types are returned

Non-Functional Requirements

  • Unit test coverage: >80% for new code
  • Performance tests: Triple generation <5ms per asset
  • E2E tests: Complete SPARQL workflow tested
  • All tests pass consistently (no flakiness)

Definition of Done

  • Unit tests for URIConstructionService (>80% coverage)
  • Unit tests for RDFVocabularyMapper (>80% coverage)
  • Unit tests for InMemoryTripleStore RDF/RDFS integration (>80% coverage)
  • SPARQL query tests using rdfs:subClassOf
  • SPARQL query tests using rdf:type
  • SPARQL query tests with rdfs:subClassOf* transitive inference
  • Performance tests for triple generation overhead
  • E2E tests demonstrating semantic queries
  • Edge case tests (missing UIDs, invalid URLs, circular hierarchies)
  • Backward compatibility tests (existing SPARQL queries still work)
  • All tests pass
  • BDD coverage >80%
  • PR merged to main

Technical Details

Architecture

Affected Layers:

  • Tests (packages/core/tests/unit/application/services/)
  • Tests (packages/core/tests/unit/infrastructure/rdf/)
  • Tests (packages/obsidian-plugin/tests/e2e/)

Key Files to Create:

packages/core/tests/unit/application/services/URIConstructionService.test.ts
packages/core/tests/unit/infrastructure/rdf/RDFVocabularyMapper.test.ts
packages/core/tests/unit/infrastructure/rdf/InMemoryTripleStore.rdf.test.ts
packages/obsidian-plugin/tests/e2e/sparql-rdf-mapping.spec.ts

Test Structure

1. URIConstructionService Tests

// packages/core/tests/unit/application/services/URIConstructionService.test.ts

describe("URIConstructionService", () => {
  let service: URIConstructionService;
  let mockFileSystem: jest.Mocked<IFileSystemAdapter>;

  beforeEach(() => {
    mockFileSystem = createMockFileSystem();
    service = new URIConstructionService(mockFileSystem);
  });

  describe("constructAssetURI", () => {
    it("should construct URI using UID and ontology URL", async () => {
      const asset = createMockAsset({
        frontmatter: {
          exo__Asset_uid: "550e8400-e29b-41d4-a716-446655440000",
          exo__Asset_isDefinedBy: "[[Ontology/EMS]]",
        },
      });

      mockOntologyFile("Ontology/EMS", {
        exo__Ontology_url: "http://exocortex.org/ems/",
      });

      const uri = await service.constructAssetURI(asset);

      expect(uri).toBe("http://exocortex.org/ems/550e8400-e29b-41d4-a716-446655440000");
    });

    it("should throw error for missing UID in strict mode", async () => {
      const asset = createMockAsset({
        frontmatter: {
          exo__Asset_isDefinedBy: "[[Ontology/EMS]]",
        },
      });

      await expect(service.constructAssetURI(asset)).rejects.toThrow(
        "Asset missing exo__Asset_uid"
      );
    });

    it("should use fallback for missing UID in non-strict mode", async () => {
      const serviceNonStrict = new URIConstructionService(mockFileSystem, {
        strictValidation: false,
      });

      const asset = createMockAsset({
        path: "Tasks/review-pr.md",
        frontmatter: {},
      });

      const uri = await serviceNonStrict.constructAssetURI(asset);

      expect(uri).toContain("review-pr");
      expect(console.warn).toHaveBeenCalledWith(
        expect.stringContaining("missing UID")
      );
    });

    it("should use default ontology URL when isDefinedBy missing", async () => {
      const asset = createMockAsset({
        frontmatter: {
          exo__Asset_uid: "550e8400-e29b-41d4-a716-446655440000",
        },
      });

      const uri = await service.constructAssetURI(asset);

      expect(uri).toBe("http://exocortex.org/default/550e8400-e29b-41d4-a716-446655440000");
    });

    it("should handle trailing slashes in ontology URL", async () => {
      const asset = createMockAsset({
        frontmatter: {
          exo__Asset_uid: "550e8400-e29b-41d4-a716-446655440000",
          exo__Asset_isDefinedBy: "[[Ontology/EMS]]",
        },
      });

      mockOntologyFile("Ontology/EMS", {
        exo__Ontology_url: "http://exocortex.org/ems",  // No trailing slash
      });

      const uri = await service.constructAssetURI(asset);

      expect(uri).toBe("http://exocortex.org/ems/550e8400-e29b-41d4-a716-446655440000");
    });
  });

  describe("validateOntologyURL", () => {
    it("should accept valid HTTP URL", () => {
      expect(service.validateOntologyURL("http://exocortex.org/")).toBe(true);
    });

    it("should accept valid HTTPS URL", () => {
      expect(service.validateOntologyURL("https://exocortex.org/")).toBe(true);
    });

    it("should reject non-HTTP(S) URL", () => {
      expect(service.validateOntologyURL("ftp://exocortex.org/")).toBe(false);
    });

    it("should reject invalid URL format", () => {
      expect(service.validateOntologyURL("not-a-url")).toBe(false);
    });

    it("should reject empty URL", () => {
      expect(service.validateOntologyURL("")).toBe(false);
    });
  });
});

2. RDFVocabularyMapper Tests

// packages/core/tests/unit/infrastructure/rdf/RDFVocabularyMapper.test.ts

describe("RDFVocabularyMapper", () => {
  let mapper: RDFVocabularyMapper;

  beforeEach(() => {
    mapper = new RDFVocabularyMapper();
  });

  describe("generateClassHierarchyTriples", () => {
    it("should generate rdfs:subClassOf triples for ExoRDF classes", () => {
      const triples = mapper.generateClassHierarchyTriples();

      // Verify ems__Task rdfs:subClassOf exo__Asset
      const taskSubclassTriple = triples.find(
        (t) =>
          t.subject.value.includes("Task") &&
          t.predicate.value === "http://www.w3.org/2000/01/rdf-schema#subClassOf"
      );

      expect(taskSubclassTriple).toBeDefined();
      expect(taskSubclassTriple.object.value).toContain("Asset");
    });

    it("should generate triples for all ExoRDF classes", () => {
      const triples = mapper.generateClassHierarchyTriples();

      const classNames = ["Task", "Project", "Area", "Asset", "Class", "Property"];
      
      for (const className of classNames) {
        const classTriple = triples.find((t) => t.subject.value.includes(className));
        expect(classTriple).toBeDefined();
      }
    });

    it("should map exo__Asset to rdfs:Resource", () => {
      const triples = mapper.generateClassHierarchyTriples();

      const assetTriple = triples.find(
        (t) =>
          t.subject.value.includes("Asset") &&
          t.object.value === "http://www.w3.org/2000/01/rdf-schema#Resource"
      );

      expect(assetTriple).toBeDefined();
    });
  });

  describe("generatePropertyHierarchyTriples", () => {
    it("should generate rdfs:subPropertyOf triples for ExoRDF properties", () => {
      const triples = mapper.generatePropertyHierarchyTriples();

      expect(triples.length).toBeGreaterThanOrEqual(6);  // 6 mapped properties
    });

    it("should map exo__Instance_class to rdf:type", () => {
      const triples = mapper.generatePropertyHierarchyTriples();

      const instanceClassTriple = triples.find(
        (t) =>
          t.subject.value.includes("Instance_class") &&
          t.object.value === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
      );

      expect(instanceClassTriple).toBeDefined();
    });

    it("should map all 6 ExoRDF properties to RDF/RDFS", () => {
      const triples = mapper.generatePropertyHierarchyTriples();

      const expectedMappings = [
        "Asset_isDefinedBy",
        "Class_superClass",
        "Instance_class",
        "Property_range",
        "Property_domain",
        "Property_superProperty",
      ];

      for (const mapping of expectedMappings) {
        const triple = triples.find((t) => t.subject.value.includes(mapping));
        expect(triple).toBeDefined();
      }
    });
  });

  describe("generateMappedTriple", () => {
    it("should generate rdf:type triple for exo__Instance_class", () => {
      const subjectIRI = new IRI("http://exocortex.org/ems/550e8400-e29b-41d4-a716-446655440000");
      
      const triple = mapper.generateMappedTriple(
        subjectIRI,
        "exo__Instance_class",
        "ems__Task"
      );

      expect(triple).toBeDefined();
      expect(triple.predicate.value).toBe("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
      expect(triple.object.value).toContain("Task");
    });

    it("should return null for unmapped ExoRDF property", () => {
      const subjectIRI = new IRI("http://exocortex.org/ems/550e8400-e29b-41d4-a716-446655440000");
      
      const triple = mapper.generateMappedTriple(
        subjectIRI,
        "ems__Task_size",  // No RDF/RDFS mapping
        "M"
      );

      expect(triple).toBeNull();
    });
  });
});

3. SPARQL Query Tests

// packages/obsidian-plugin/tests/e2e/sparql-rdf-mapping.spec.ts

describe("SPARQL with RDF/RDFS Mapping", () => {
  let tripleStore: ObsidianTripleStore;

  beforeEach(async () => {
    tripleStore = await setupTripleStoreWithAssets([
      createMockTask({ label: "Task 1", class: "ems__Task" }),
      createMockProject({ label: "Project 1", class: "ems__Project" }),
      createMockArea({ label: "Area 1", class: "ems__Area" }),
    ]);
  });

  it("should query all assets using rdfs:subClassOf", async () => {
    const query = `
      PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
      PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
      PREFIX exo: <http://exocortex.org/ontology/>
      
      SELECT ?asset ?type
      WHERE {
        ?asset rdf:type ?type .
        ?type rdfs:subClassOf* exo:Asset .
      }
    `;

    const result = await tripleStore.query(query);

    expect(result.bindings.length).toBe(3);  // Task, Project, Area
  });

  it("should query using rdf:type instead of exo__Instance_class", async () => {
    const query = `
      PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
      PREFIX ems: <http://exocortex.org/ems/>
      
      SELECT ?task
      WHERE {
        ?task rdf:type ems:Task .
      }
    `;

    const result = await tripleStore.query(query);

    expect(result.bindings.length).toBe(1);
    expect(result.bindings[0].get("task").value).toContain("Task");
  });

  it("should support transitive rdfs:subClassOf* queries", async () => {
    const query = `
      PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
      
      SELECT ?class ?superClass
      WHERE {
        ?class rdfs:subClassOf* <http://www.w3.org/2000/01/rdf-schema#Resource> .
      }
    `;

    const result = await tripleStore.query(query);

    // Should return: exo:Asset, ems:Task, ems:Project, ems:Area (all subclasses)
    expect(result.bindings.length).toBeGreaterThanOrEqual(4);
  });
});

4. Performance Tests

describe("RDF/RDFS Mapping Performance", () => {
  it("should generate triples with <5ms overhead per asset", async () => {
    const asset = createMockAsset({
      frontmatter: {
        exo__Asset_uid: "550e8400-e29b-41d4-a716-446655440000",
        exo__Instance_class: "ems__Task",
        // ... 20 more properties
      },
    });

    const startTime = performance.now();
    
    await tripleStore.addAssetTriples(assetURI, asset);
    
    const endTime = performance.now();
    const duration = endTime - startTime;

    expect(duration).toBeLessThan(5);  // <5ms per asset
  });

  it("should not exceed 20% memory increase with RDF/RDFS triples", async () => {
    const assets = Array.from({ length: 1000 }, (_, i) =>
      createMockAsset({ uid: `uid-${i}`, class: "ems__Task" })
    );

    const memBefore = process.memoryUsage().heapUsed;

    for (const asset of assets) {
      await tripleStore.addAssetTriples(constructURI(asset), asset);
    }

    const memAfter = process.memoryUsage().heapUsed;
    const increase = (memAfter - memBefore) / memBefore;

    expect(increase).toBeLessThan(0.20);  // <20% increase
  });
});

Gotchas & Edge Cases

⚠️ Tests must cover:

  • Missing exo__Asset_uid (strict and non-strict modes)
  • Missing exo__Asset_isDefinedBy (default ontology)
  • Invalid ontology URL format
  • Circular subclass relationships
  • Transitive closure performance (large hierarchies)
  • Backward compatibility (existing queries)
  • Memory usage with large vaults

AI Agent Guidance

Step-by-Step Implementation

  1. Create test structure

    mkdir -p packages/core/tests/unit/application/services
    mkdir -p packages/core/tests/unit/infrastructure/rdf
    mkdir -p packages/obsidian-plugin/tests/e2e
  2. Write unit tests first (TDD approach)

    • URIConstructionService tests (all edge cases)
    • RDFVocabularyMapper tests (class/property hierarchies)
    • InMemoryTripleStore tests (RDF/RDFS integration)
  3. Write SPARQL query tests

    • Standard RDF/RDFS predicate queries
    • Transitive inference queries
    • Backward compatibility queries
  4. Write performance tests

    • Triple generation overhead (<5ms)
    • Memory usage (<20% increase)
  5. Write E2E tests

    • End-to-end semantic query workflow
    • Real vault data integration
  6. Run tests

    npm run test:unit
    npm run test:e2e
    npm run test:coverage
    npm run bdd:coverage

Example Code References

Similar test patterns:

  • packages/core/tests/unit/application/services/TaskCreationService.test.ts
  • packages/core/tests/unit/infrastructure/rdf/InMemoryTripleStore.test.ts
  • packages/obsidian-plugin/tests/e2e/sparql-queries.spec.ts

Common Mistakes to Avoid

❌ Not testing edge cases (missing data, invalid formats)
✅ Comprehensive edge case coverage

❌ Flaky tests (non-deterministic timing)
✅ Use mocks, deterministic test data

❌ Not testing backward compatibility
✅ Verify existing queries still work

❌ Not testing performance
✅ Measure triple generation overhead, memory usage


Testing Requirements

Unit Test Coverage

Target: >80% for all new code

Test Files:

  • URIConstructionService.test.ts (>80%)
  • RDFVocabularyMapper.test.ts (>80%)
  • InMemoryTripleStore.rdf.test.ts (>80%)

SPARQL Query Tests

  • Query using rdfs:subClassOf
  • Query using rdf:type
  • Query with rdfs:subClassOf* (transitive)
  • Query using rdfs:isDefinedBy
  • Backward compatibility (existing ExoRDF queries)

Performance Tests

  • Triple generation <5ms per asset
  • Memory increase <20% with RDF/RDFS triples
  • Transitive closure computation performance

E2E Tests

  • Load vault with real assets
  • Execute semantic query workflow
  • Verify RDF/RDFS triples in results

BDD Coverage

  • BDD scenarios for URI construction
  • BDD scenarios for RDF/RDFS mapping
  • BDD scenarios for SPARQL inference
  • Target: >80% BDD coverage

Documentation Requirements

Test Documentation

  • Test file comments explaining test strategy
  • Edge case documentation in test names
  • Performance test rationale

Developer Documentation

  • Update docs/rdf/ExoRDF-Mapping.md (testing section)
  • Update docs/sparql/Developer-Guide.md (RDF/RDFS query examples)

Related Issues

Depends on:

Blocks:


Additional Notes

Timeline Estimate: 4-6 hours (unit tests + SPARQL tests + performance tests + E2E tests)

Key Testing Strategy:

  1. Unit tests first (TDD approach)
  2. SPARQL query tests for inference
  3. Performance tests for production readiness
  4. E2E tests for integration confidence
  5. BDD scenarios for specification

Success Criteria: >80% coverage, all edge cases tested, performance validated, backward compatibility verified

Metadata

Metadata

Assignees

No one assigned

    Labels

    epic:rdf-storeEpic 1: RDF Triple Store Foundationpackage:core@exocortex/core packagepriority:P1High prioritytestingTesting and test coverage improvements

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions