-
Notifications
You must be signed in to change notification settings - Fork 1
Description
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
- 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
-
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
-
Write unit tests first (TDD approach)
- URIConstructionService tests (all edge cases)
- RDFVocabularyMapper tests (class/property hierarchies)
- InMemoryTripleStore tests (RDF/RDFS integration)
-
Write SPARQL query tests
- Standard RDF/RDFS predicate queries
- Transitive inference queries
- Backward compatibility queries
-
Write performance tests
- Triple generation overhead (<5ms)
- Memory usage (<20% increase)
-
Write E2E tests
- End-to-end semantic query workflow
- Real vault data integration
-
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:
- docs: Document ExoRDF to RDF/RDFS Mapping Specification #365 (ExoRDF Mapping Documentation) - Defines what to test
- feat: Implement UID-Based URI Construction for RDF Resources #366 (URI Construction) - Implementation to test
- feat: Integrate ExoRDF to RDF/RDFS Mapping into SPARQL Triple Store #367 (Integrate Mapping) - Implementation to test
Blocks:
- Issue Add AGENTS guide for GPT workflows #5 (Update SPARQL Documentation) - Needs test examples
Additional Notes
Timeline Estimate: 4-6 hours (unit tests + SPARQL tests + performance tests + E2E tests)
Key Testing Strategy:
- Unit tests first (TDD approach)
- SPARQL query tests for inference
- Performance tests for production readiness
- E2E tests for integration confidence
- BDD scenarios for specification
Success Criteria: >80% coverage, all edge cases tested, performance validated, backward compatibility verified