1e606553d4
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / build (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 18s
CI / security (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 38s
CI / unit_tests (pull_request) Successful in 2m32s
CI / integration_tests (pull_request) Successful in 3m18s
CI / docker (pull_request) Successful in 49s
CI / coverage (pull_request) Successful in 5m29s
CI / benchmark-regression (pull_request) Successful in 33m48s
Implement the UKOIndexer service that produces UKO triples from resources using pluggable domain-specific analyzers, wraps each triple with provenance metadata, and simultaneously indexes into text, vector, and graph backends. Key design decisions and components: - UKOIndexer orchestrates the full index lifecycle: add_resource, update_resource (remove-then-add), remove_resource, and maintenance triggers. Each operation fires lifecycle hooks (on_indexed, on_removed, on_error) so callers can observe progress. - Analyzer selection is pluggable via ContentAnalyzer protocol. The indexer accepts a registry mapping resource types to analyzers. PythonAnalyzer and MarkdownAnalyzer are provided as built-in implementations. - LocationContentReader protocol abstracts file I/O with a base_dir parameter for path-traversal prevention (post-resolve validation rejects paths escaping the base directory and non-regular files). - UKOTriple model includes a @model_validator ensuring at least one of object_uri or object_value is populated, preventing empty triples at construction time. - Triple removal uses scoped deletion via uko:sourceResource predicate to avoid shared-subject collision — only triples originating from the specific resource are removed, not all triples for a shared subject. - _resource_subjects.pop is deferred until after all backend removal operations succeed, preventing inconsistent state on partial failure. - analyzer.analyze() is wrapped in try/except so that analyzer errors produce an IndexResult with error details rather than propagating exceptions to callers. - All lifecycle hook calls are guarded via _fire_on_indexed, _fire_on_removed, and _fire_on_error helpers that catch and log hook exceptions without disrupting the indexing pipeline. - max_triples parameter (default 50,000) bounds analyzer output size to prevent runaway resource consumption. - ResourceFileWatcher monitors filesystem paths via watchdog and triggers re-indexing callbacks on file changes with configurable debouncing. Emits RESOURCE_MODIFIED domain events via EventBus when file changes are detected. Debounce timers coalesce rapid edits into a single callback invocation. Thread-safe design with daemon threads for clean shutdown. - SearchResult.__post_init__ validates score is in [0.0, 1.0], correctly rejecting NaN values. - Placeholder embedding uses [1.0] instead of [float(len(content))] to avoid leaking content size information. - isinstance check on graph_backend ensures GraphIndexBackend protocol compliance at runtime. - Test doubles extracted to features/mocks/uko_indexer_mocks.py for reuse across BDD steps and Robot helpers. Spec reference: Architecture > ACMS > Real-time Index Synchronization (specification.md lines ~43205-43300). ISSUES CLOSED: #578
1084 lines
53 KiB
Gherkin
1084 lines
53 KiB
Gherkin
Feature: UKO Indexer — Real-time Index Synchronization
|
|
As a context pipeline consumer
|
|
I need the UKOIndexer to analyze resources and index into backends
|
|
So that context strategies can query pre-built indices
|
|
|
|
Background:
|
|
Given idx a clean analyzer registry
|
|
And idx in-memory index backends
|
|
|
|
# =================================================================
|
|
# Index backend protocols and stubs
|
|
# =================================================================
|
|
|
|
Scenario: InMemoryTextIndexBackend indexes and searches documents
|
|
When idx I index a text document "doc1" with content "hello world" in project "local/app"
|
|
Then idx searching "hello" in project "local/app" should return 1 result
|
|
And idx the search result doc_id should be "doc1"
|
|
|
|
Scenario: InMemoryTextIndexBackend returns empty for non-matching query
|
|
When idx I index a text document "doc1" with content "hello world" in project "local/app"
|
|
Then idx searching "foobar" in project "local/app" should return 0 results
|
|
|
|
Scenario: InMemoryTextIndexBackend scopes by project
|
|
When idx I index a text document "doc1" with content "hello world" in project "local/app"
|
|
Then idx searching "hello" in project "local/other" should return 0 results
|
|
|
|
Scenario: InMemoryTextIndexBackend removes documents
|
|
When idx I index a text document "doc1" with content "hello world" in project "local/app"
|
|
And idx I remove text document "doc1" from project "local/app"
|
|
Then idx searching "hello" in project "local/app" should return 0 results
|
|
|
|
Scenario: InMemoryTextIndexBackend rebuilds index
|
|
When idx I index a text document "doc1" with content "hello world" in project "local/app"
|
|
And idx I rebuild text index for project "local/app"
|
|
Then idx searching "hello" in project "local/app" should return 0 results
|
|
|
|
Scenario: InMemoryTextIndexBackend rejects empty project
|
|
Then idx indexing a text document with empty project should raise ValueError
|
|
|
|
Scenario: InMemoryTextIndexBackend rejects empty content
|
|
Then idx indexing a text document with empty content should raise ValueError
|
|
|
|
Scenario: InMemoryTextIndexBackend rejects empty query
|
|
Then idx searching with empty query should raise ValueError
|
|
|
|
Scenario: InMemoryTextIndexBackend rejects non-positive limit
|
|
Then idx searching with limit 0 should raise ValueError
|
|
|
|
Scenario: InMemoryVectorIndexBackend indexes and searches embeddings
|
|
When idx I index an embedding "emb1" with vector "1.0,2.0,3.0" in project "local/app"
|
|
Then idx searching similar in project "local/app" should return 1 result
|
|
|
|
Scenario: InMemoryVectorIndexBackend removes embeddings
|
|
When idx I index an embedding "emb1" with vector "1.0,2.0,3.0" in project "local/app"
|
|
And idx I remove embedding "emb1" from project "local/app"
|
|
Then idx searching similar in project "local/app" should return 0 results
|
|
|
|
Scenario: InMemoryVectorIndexBackend rejects out-of-range min_relevance
|
|
When idx I index an embedding "emb1" with vector "1.0,2.0,3.0" in project "local/app"
|
|
Then idx searching similar with min_relevance 1.1 should raise ValueError
|
|
|
|
Scenario: InMemoryVectorIndexBackend rejects empty embedding
|
|
Then idx indexing an embedding with empty vector should raise ValueError
|
|
|
|
Scenario: InMemoryGraphIndexBackend adds and queries triples
|
|
When idx I add a triple "uko://s" "uko:type" "uko://Class" in project "local/app"
|
|
Then idx querying project "local/app" should return 1 binding
|
|
And idx the binding should have subject "uko://s"
|
|
|
|
Scenario: InMemoryGraphIndexBackend removes triples by subject
|
|
When idx I add a triple "uko://s" "uko:type" "uko://Class" in project "local/app"
|
|
And idx I add a triple "uko://s" "uko:name" "Foo" in project "local/app"
|
|
And idx I remove triples with subject "uko://s" from project "local/app"
|
|
Then idx querying project "local/app" should return 0 bindings
|
|
|
|
Scenario: InMemoryGraphIndexBackend rejects all-None pattern
|
|
Then idx removing triples with all-None pattern should raise ValueError
|
|
|
|
Scenario: InMemoryGraphIndexBackend rejects empty project
|
|
Then idx adding a triple with empty project should raise ValueError
|
|
|
|
# =================================================================
|
|
# Provenance model
|
|
# =================================================================
|
|
|
|
Scenario: ProvenanceMetadata has required fields
|
|
When idx I create provenance for resource "01HQ8ZDRX50000000000000001"
|
|
Then idx the provenance should have source_resource "01HQ8ZDRX50000000000000001"
|
|
And idx the provenance should be current
|
|
And idx the provenance should have a valid_from timestamp
|
|
|
|
Scenario: ProvenanceMetadata rejects empty source_resource
|
|
Then idx creating provenance with empty source_resource should raise ValidationError
|
|
|
|
Scenario: ProvenancedTriple wraps triple with provenance
|
|
When idx I create a provenanced triple for resource "01HQ8ZDRX50000000000000001"
|
|
Then idx the provenanced triple should have the triple
|
|
And idx the provenanced triple should have the provenance
|
|
|
|
Scenario: ProvenanceMetadata is frozen
|
|
When idx I create provenance for resource "01HQ8ZDRX50000000000000001"
|
|
Then idx mutating the provenance should raise ValidationError
|
|
|
|
Scenario: IndexResult tracks indexing summary
|
|
When idx I create an IndexResult with 5 triples and 1 text doc
|
|
Then idx the IndexResult should have triple_count 5
|
|
And idx the IndexResult should have text_docs_indexed 1
|
|
And idx the IndexResult should have analyzer_domain "python"
|
|
|
|
Scenario: IndexResult rejects negative triple_count
|
|
Then idx creating IndexResult with negative triple_count should raise ValidationError
|
|
|
|
# =================================================================
|
|
# AnalyzerRegistry.get_for_resource
|
|
# =================================================================
|
|
|
|
Scenario: AnalyzerRegistry.get_for_resource finds analyzer by location extension
|
|
Given idx a PythonAnalyzer is registered
|
|
When idx I look up analyzer for a resource with location "src/main.py"
|
|
Then idx the analyzer domain should be "python"
|
|
|
|
Scenario: AnalyzerRegistry.get_for_resource returns None for unknown extension
|
|
Given idx a PythonAnalyzer is registered
|
|
When idx I look up analyzer for a resource with location "data.csv"
|
|
Then idx no analyzer should be found
|
|
|
|
Scenario: AnalyzerRegistry.get_for_resource returns None for no location
|
|
Given idx a PythonAnalyzer is registered
|
|
When idx I look up analyzer for a resource with no location
|
|
Then idx no analyzer should be found
|
|
|
|
Scenario: AnalyzerRegistry.get_for_resource finds MarkdownAnalyzer
|
|
Given idx a MarkdownAnalyzer is registered
|
|
When idx I look up analyzer for a resource with location "README.md"
|
|
Then idx the analyzer domain should be "markdown"
|
|
|
|
Scenario: AnalyzerRegistry higher-priority analyzer supersedes lower-priority
|
|
Given idx a clean analyzer registry
|
|
And idx a PythonAnalyzer is registered with priority 0
|
|
And idx an alternative Python analyzer "python-advanced" is registered with priority 10
|
|
When idx I look up analyzer for a resource with location "main.py"
|
|
Then idx the analyzer domain should be "python-advanced"
|
|
|
|
Scenario: AnalyzerRegistry lower-priority analyzer does not supersede higher-priority
|
|
Given idx a clean analyzer registry
|
|
And idx an alternative Python analyzer "python-advanced" is registered with priority 10
|
|
And idx a PythonAnalyzer is registered with priority 0
|
|
When idx I look up analyzer for a resource with location "main.py"
|
|
Then idx the analyzer domain should be "python-advanced"
|
|
|
|
# =================================================================
|
|
# UKOIndexer core
|
|
# =================================================================
|
|
|
|
Scenario: UKOIndexer indexes a Python resource into all backends
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with all backends
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
Then idx the index result should have triple_count greater than 0
|
|
And idx the index result should have text_docs_indexed 1
|
|
And idx the index result should have embeddings_indexed 1
|
|
And idx the index result should have analyzer_domain "python"
|
|
And idx the index result should have 0 errors
|
|
And idx the graph backend should have triples for project "local/test"
|
|
And idx the text backend should have 1 document
|
|
And idx the vector backend should have 1 embedding
|
|
And idx the indexer should track 1 indexed resource
|
|
|
|
Scenario: UKOIndexer indexes a Markdown resource
|
|
Given idx a MarkdownAnalyzer is registered
|
|
And idx a UKOIndexer with all backends
|
|
When idx I index a Markdown resource "01HQ8ZDRX50000000000000002" with content "# Title\nSome text"
|
|
Then idx the index result should have triple_count greater than 0
|
|
And idx the index result should have analyzer_domain "markdown"
|
|
|
|
Scenario: UKOIndexer returns none-domain when no analyzer matches
|
|
And idx a UKOIndexer with all backends
|
|
When idx I index a CSV resource "01HQ8ZDRX50000000000000003" with location "data.csv"
|
|
Then idx the index result should have analyzer_domain "none"
|
|
And idx the index result should have triple_count 0
|
|
|
|
Scenario: UKOIndexer degrades gracefully without text backend
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer without text backend
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "x = 1"
|
|
Then idx the index result should have text_docs_indexed 0
|
|
And idx the index result should have 0 errors
|
|
|
|
Scenario: UKOIndexer degrades gracefully without vector backend
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer without vector backend
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "x = 1"
|
|
Then idx the index result should have embeddings_indexed 0
|
|
And idx the index result should have 0 errors
|
|
|
|
Scenario: UKOIndexer degrades gracefully without text and vector backends
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with only graph backend
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "x = 1"
|
|
Then idx the index result should have text_docs_indexed 0
|
|
And idx the index result should have embeddings_indexed 0
|
|
And idx the index result should have triple_count greater than 0
|
|
|
|
Scenario: UKOIndexer reports content read errors without side effects
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with a failing content reader
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "x = 1"
|
|
Then idx the index result should have 1 errors
|
|
And idx the index result error should mention "read"
|
|
And idx the indexer should track 0 indexed resources
|
|
And idx the text backend should have 0 documents
|
|
And idx the vector backend should have 0 embeddings
|
|
|
|
Scenario: UKOIndexer rejects empty project
|
|
Given idx a UKOIndexer with all backends
|
|
Then idx indexing with empty project should raise ValueError
|
|
|
|
# =================================================================
|
|
# UKOIndexer remove and reindex
|
|
# =================================================================
|
|
|
|
Scenario: UKOIndexer removes a resource from all backends
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with all backends
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
And idx I remove resource "01HQ8ZDRX50000000000000001" from project "local/test"
|
|
Then idx the graph backend should have 0 triples for project "local/test"
|
|
And idx the text backend should have 0 documents
|
|
And idx the vector backend should have 0 embeddings
|
|
And idx the indexer should track 0 indexed resources
|
|
|
|
Scenario: UKOIndexer reindexes a resource
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with all backends
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
And idx I reindex resource "01HQ8ZDRX50000000000000001" with content "class Bar:\n pass"
|
|
Then idx the index result should have triple_count greater than 0
|
|
And idx the indexer should track 1 indexed resource
|
|
|
|
Scenario: UKOIndexer remove rejects empty resource_id
|
|
Given idx a UKOIndexer with all backends
|
|
Then idx removing with empty resource_id should raise ValueError
|
|
|
|
Scenario: UKOIndexer remove rejects empty project
|
|
Given idx a UKOIndexer with all backends
|
|
Then idx removing with empty project should raise ValueError
|
|
|
|
# =================================================================
|
|
# Lifecycle hooks
|
|
# =================================================================
|
|
|
|
Scenario: DefaultLifecycleHook receives on_indexed event
|
|
Given idx a UKOIndexer with a tracking lifecycle hook
|
|
And idx a PythonAnalyzer is registered
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "x = 1"
|
|
Then idx the lifecycle hook should have received 1 on_indexed event
|
|
And idx the lifecycle hook should have received 0 on_error events
|
|
|
|
Scenario: DefaultLifecycleHook receives on_removed event
|
|
Given idx a UKOIndexer with a tracking lifecycle hook
|
|
And idx a PythonAnalyzer is registered
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "x = 1"
|
|
And idx I remove resource "01HQ8ZDRX50000000000000001" from project "local/test"
|
|
Then idx the lifecycle hook should have received 1 on_removed event
|
|
|
|
Scenario: Lifecycle hook receives on_error for read failure
|
|
Given idx a UKOIndexer with a tracking lifecycle hook and failing reader
|
|
And idx a PythonAnalyzer is registered
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "x = 1"
|
|
Then idx the lifecycle hook should have received 1 on_error events
|
|
|
|
# =================================================================
|
|
# UKOIndexer constructor validation
|
|
# =================================================================
|
|
|
|
Scenario: UKOIndexer rejects invalid analyzer_registry type
|
|
Then idx creating UKOIndexer with invalid registry should raise TypeError
|
|
|
|
Scenario: UKOIndexer has_text_backend property
|
|
Given idx a UKOIndexer with all backends
|
|
Then idx the indexer should have text backend
|
|
And idx the indexer should have vector backend
|
|
|
|
Scenario: UKOIndexer without optional backends reports availability
|
|
Given idx a UKOIndexer with only graph backend
|
|
Then idx the indexer should not have text backend
|
|
And idx the indexer should not have vector backend
|
|
|
|
# =================================================================
|
|
# SearchResult and IndexedDocument models
|
|
# =================================================================
|
|
|
|
Scenario: SearchResult validates score range
|
|
Then idx creating SearchResult with score 1.5 should raise ValueError
|
|
|
|
Scenario: SearchResult rejects empty doc_id
|
|
Then idx creating SearchResult with empty doc_id should raise ValueError
|
|
|
|
Scenario: IndexedDocument validates non-negative char_count
|
|
Then idx creating IndexedDocument with negative char_count should raise ValueError
|
|
|
|
Scenario: IndexedDocument rejects empty project
|
|
Then idx creating IndexedDocument with empty project should raise ValueError
|
|
|
|
# =================================================================
|
|
# Review-discovered edge cases
|
|
# =================================================================
|
|
|
|
Scenario: UKOIndexer double-index is idempotent (no stale accumulation)
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with all backends
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
And idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
Then idx the indexer should track 1 indexed resource
|
|
And idx the text backend should have 1 document
|
|
And idx the vector backend should have 1 embedding
|
|
|
|
Scenario: UKOIndexer reindex replaces old triples with new
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with all backends
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
And idx I reindex resource "01HQ8ZDRX50000000000000001" with content "class Bar:\n pass"
|
|
Then idx the indexer should track 1 indexed resource
|
|
And idx the graph backend should contain triple with predicate "rdfs:label" and object "Bar"
|
|
And idx the graph backend should not contain triple with object "Foo"
|
|
|
|
Scenario: UKOIndexer remove of never-indexed resource is no-op
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with all backends
|
|
When idx I remove resource "01HQ8ZDRX50000000000000099" from project "local/test"
|
|
Then idx the indexer should track 0 indexed resources
|
|
|
|
Scenario: ProvenancedTriple has correct source_resource value
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx an AnalyzerRegistry with the PythonAnalyzer
|
|
When idx I analyze content "x = 1" with resource URI "uko://resource/01HQ8ZDRX50000000000000001"
|
|
And idx I wrap the first triple with provenance for resource "01HQ8ZDRX50000000000000001" at path "src/main.py"
|
|
Then idx the provenance source_resource should be "01HQ8ZDRX50000000000000001"
|
|
And idx the provenance source_path should be "src/main.py"
|
|
|
|
# =================================================================
|
|
# Bug-fix verifications (deep review round 2)
|
|
# =================================================================
|
|
|
|
Scenario: Idempotent re-index does NOT fire on_removed lifecycle event
|
|
Given idx a UKOIndexer with a tracking lifecycle hook
|
|
And idx a PythonAnalyzer is registered
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
And idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Bar:\n pass"
|
|
Then idx the lifecycle hook should have received 0 on_removed events
|
|
And idx the lifecycle hook should have received 2 on_indexed events
|
|
|
|
Scenario: Cross-project re-index cleans up old project data
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with all backends
|
|
When idx I index Python resource "01HQ8ZDRX50000000000000001" under project "org/old" content "class Foo:\n pass"
|
|
And idx I index Python resource "01HQ8ZDRX50000000000000001" under project "org/new" content "class Bar:\n pass"
|
|
Then idx the graph backend should have 0 triples for project "org/old"
|
|
And idx the graph backend should have triples for project "org/new"
|
|
And idx the text backend should have 1 document
|
|
And idx the indexer should track 1 indexed resource
|
|
|
|
Scenario: Reindex with different project cleans old project
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with all backends
|
|
When idx I index Python resource "01HQ8ZDRX50000000000000001" under project "org/old" content "class Foo:\n pass"
|
|
And idx I reindex resource "01HQ8ZDRX50000000000000001" under project "org/new" content "class Bar:\n pass"
|
|
Then idx the graph backend should have 0 triples for project "org/old"
|
|
And idx the graph backend should have triples for project "org/new"
|
|
And idx the indexer should track 1 indexed resource
|
|
|
|
Scenario: Whitespace-only project is rejected by text backend
|
|
Then idx indexing a text document with whitespace-only project should raise ValueError
|
|
|
|
Scenario: Whitespace-only content is rejected by text backend
|
|
Then idx indexing a text document with whitespace-only content should raise ValueError
|
|
|
|
Scenario: Whitespace-only subject is rejected by graph backend
|
|
Then idx adding a triple with whitespace-only subject should raise ValueError
|
|
|
|
# =================================================================
|
|
# Backend error path scenarios (graceful degradation)
|
|
# =================================================================
|
|
|
|
Scenario: UKOIndexer handles graph backend error gracefully
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with a failing graph backend
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
Then idx the index result should have triple_count 0
|
|
And idx the index result errors should mention "Graph backend"
|
|
|
|
Scenario: UKOIndexer handles text backend error gracefully
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with a failing text backend
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
Then idx the index result should have text_docs_indexed 0
|
|
And idx the index result errors should mention "Text backend"
|
|
And idx the index result should have triple_count greater than 0
|
|
|
|
Scenario: UKOIndexer handles vector backend error gracefully
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with a failing vector backend
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
Then idx the index result should have embeddings_indexed 0
|
|
And idx the index result errors should mention "Vector backend"
|
|
And idx the index result should have triple_count greater than 0
|
|
|
|
# =================================================================
|
|
# Strengthened content assertions
|
|
# =================================================================
|
|
|
|
Scenario: Text backend stores correct doc_id and content after indexing
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with all backends
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
Then idx the text backend should contain doc_id "uko://resource/01HQ8ZDRX50000000000000001" in project "local/test"
|
|
|
|
Scenario: Vector backend stores correct doc_id after indexing
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with all backends
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
Then idx the vector backend should contain doc_id "uko://resource/01HQ8ZDRX50000000000000001" in project "local/test"
|
|
|
|
Scenario: Graph backend stores sourceResource provenance link
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with all backends
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
Then idx the graph backend should contain triple with predicate "uko:sourceResource" and object "uko://resource/01HQ8ZDRX50000000000000001"
|
|
|
|
# =================================================================
|
|
# Additional negative validation tests
|
|
# =================================================================
|
|
|
|
Scenario: IndexResult rejects empty resource_id
|
|
Then idx creating IndexResult with empty resource_id should raise ValidationError
|
|
|
|
Scenario: SearchResult rejects negative score
|
|
Then idx creating SearchResult with score -0.1 should raise ValueError
|
|
|
|
Scenario: Reindex rejects empty project
|
|
Given idx a UKOIndexer with all backends
|
|
Then idx reindexing with empty project should raise ValueError
|
|
|
|
Scenario: Reindex rejects whitespace-only project
|
|
Given idx a UKOIndexer with all backends
|
|
Then idx reindexing with whitespace-only project should raise ValueError
|
|
|
|
# =================================================================
|
|
# LocationContentReader tests (P2-5)
|
|
# =================================================================
|
|
|
|
Scenario: LocationContentReader rejects path with dotdot components
|
|
Then idx LocationContentReader should reject path with dotdot component
|
|
|
|
Scenario: LocationContentReader rejects resource with no location
|
|
Then idx LocationContentReader should reject resource with no location
|
|
|
|
Scenario: LocationContentReader rejects non-positive max_content_size
|
|
Then idx LocationContentReader should reject max_content_size of 0
|
|
|
|
Scenario: LocationContentReader reads a real file
|
|
Then idx LocationContentReader should read a real file successfully
|
|
|
|
Scenario: LocationContentReader rejects non-existent file
|
|
Then idx LocationContentReader should reject non-existent file
|
|
|
|
Scenario: LocationContentReader rejects path escaping base_dir
|
|
Then idx LocationContentReader should reject path escaping base_dir
|
|
|
|
Scenario: SearchResult rejects NaN score
|
|
Then idx creating SearchResult with NaN score should raise ValueError
|
|
|
|
# =================================================================
|
|
# File-watching integration (P1-1: file-watching acceptance criterion)
|
|
# =================================================================
|
|
|
|
@file-watching
|
|
Scenario: ResourceFileWatcher starts and stops cleanly
|
|
Given idx a ResourceFileWatcher with default settings
|
|
When idx the file watcher is started
|
|
Then idx the file watcher should be running
|
|
When idx the file watcher is stopped
|
|
Then idx the file watcher should not be running
|
|
|
|
@file-watching
|
|
Scenario: Watching a file path registers it for monitoring
|
|
Given idx a ResourceFileWatcher with default settings
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
When idx the watcher registers the file for resource "res-001" project "local/test"
|
|
Then idx the watcher should have 1 watched paths
|
|
|
|
@file-watching
|
|
Scenario: Unwatching a file path removes it from monitoring
|
|
Given idx a ResourceFileWatcher with default settings
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
When idx the watcher registers the file for resource "res-001" project "local/test"
|
|
And idx the watcher unregisters the watched file
|
|
Then idx the watcher should have 0 watched paths
|
|
|
|
@file-watching
|
|
Scenario: File modification triggers on_change callback
|
|
Given idx a ResourceFileWatcher with callback tracking and debounce 0.05
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
And idx the file watcher is started
|
|
And idx the watcher registers the file for resource "res-001" project "local/test"
|
|
When idx a FileModifiedEvent is simulated for the watched file
|
|
Then idx the on_change callback should fire within 2 seconds
|
|
And idx the callback should receive resource "res-001" project "local/test" change "modified"
|
|
|
|
@file-watching
|
|
Scenario: File deletion triggers on_change callback
|
|
Given idx a ResourceFileWatcher with callback tracking and debounce 0.05
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
And idx the file watcher is started
|
|
And idx the watcher registers the file for resource "res-001" project "local/test"
|
|
When idx a FileDeletedEvent is simulated for the watched file
|
|
Then idx the on_change callback should fire within 2 seconds
|
|
And idx the callback should receive resource "res-001" project "local/test" change "deleted"
|
|
|
|
@file-watching
|
|
Scenario: Debouncing coalesces rapid changes into one callback
|
|
Given idx a ResourceFileWatcher with callback tracking and debounce 0.1
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
And idx the file watcher is started
|
|
And idx the watcher registers the file for resource "res-001" project "local/test"
|
|
When idx 5 FileModifiedEvents are simulated rapidly for the watched file
|
|
Then idx the on_change callback should fire within 2 seconds
|
|
And idx the on_change callback should have fired exactly 1 time
|
|
|
|
@file-watching
|
|
Scenario: EventBus receives RESOURCE_MODIFIED event on file change
|
|
Given idx a ResourceFileWatcher with EventBus tracking and debounce 0.05
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
And idx the file watcher is started
|
|
And idx the watcher registers the file for resource "res-001" project "local/test"
|
|
When idx a FileModifiedEvent is simulated for the watched file
|
|
Then idx the EventBus should receive a RESOURCE_MODIFIED event within 2 seconds
|
|
And idx the event details should contain resource_id "res-001"
|
|
|
|
@file-watching
|
|
Scenario: Callback errors do not crash the watcher
|
|
Given idx a ResourceFileWatcher with a failing callback and debounce 0.05
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
And idx the file watcher is started
|
|
And idx the watcher registers the file for resource "res-001" project "local/test"
|
|
When idx a FileModifiedEvent is simulated for the watched file
|
|
And idx we wait 0.2 seconds for debounce
|
|
Then idx the file watcher should be running
|
|
|
|
@file-watching
|
|
Scenario: Watching a non-existent path raises ValueError
|
|
Given idx a ResourceFileWatcher with default settings
|
|
Then idx watching path "/nonexistent/path.py" should raise ValueError containing "does not exist"
|
|
|
|
@file-watching
|
|
Scenario: Watching a directory path raises ValueError
|
|
Given idx a ResourceFileWatcher with default settings
|
|
And idx a temporary watched directory
|
|
Then idx watching the temporary directory should raise ValueError containing "not a file"
|
|
|
|
@file-watching
|
|
Scenario: Negative debounce_seconds raises ValueError
|
|
Then idx creating ResourceFileWatcher with debounce -1.0 should raise ValueError
|
|
|
|
@file-watching
|
|
Scenario: Unwatched file changes are ignored
|
|
Given idx a ResourceFileWatcher with callback tracking and debounce 0.05
|
|
And idx the file watcher is started
|
|
When idx a FileModifiedEvent is simulated for path "/tmp/unwatched.py"
|
|
And idx we wait 0.2 seconds for debounce
|
|
Then idx the on_change callback should have fired exactly 0 times
|
|
|
|
@file-watching
|
|
Scenario: Directory events are ignored by the watcher
|
|
Given idx a ResourceFileWatcher with callback tracking and debounce 0.05
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
And idx the file watcher is started
|
|
And idx the watcher registers the file for resource "res-001" project "local/test"
|
|
When idx a directory event is simulated for the watched path
|
|
And idx we wait 0.2 seconds for debounce
|
|
Then idx the on_change callback should have fired exactly 0 times
|
|
|
|
# =================================================================
|
|
# Coverage — file-watcher validation and edge cases
|
|
# =================================================================
|
|
|
|
@file-watching
|
|
Scenario: Watch rejects empty resource_id
|
|
Given idx a ResourceFileWatcher with default settings
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
Then idx watching with empty resource_id should raise ValueError
|
|
|
|
@file-watching
|
|
Scenario: Watch rejects empty project
|
|
Given idx a ResourceFileWatcher with default settings
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
Then idx watching with empty project should raise ValueError
|
|
|
|
@file-watching
|
|
Scenario: Starting an already-running watcher raises RuntimeError
|
|
Given idx a ResourceFileWatcher with default settings
|
|
And idx the file watcher is started
|
|
Then idx starting the watcher again should raise RuntimeError
|
|
And idx I stop the file watcher
|
|
|
|
@file-watching
|
|
Scenario: Stopping a non-running watcher is a no-op
|
|
Given idx a ResourceFileWatcher with default settings
|
|
When idx I stop the file watcher
|
|
Then idx the file watcher should not be running
|
|
|
|
@file-watching
|
|
Scenario: debounce_seconds property returns configured value
|
|
Given idx a ResourceFileWatcher with callback tracking and debounce 0.25
|
|
Then idx the debounce_seconds should be 0.25
|
|
|
|
@file-watching
|
|
Scenario: File creation triggers on_change with created type
|
|
Given idx a ResourceFileWatcher with callback tracking and debounce 0.05
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
And idx the file watcher is started
|
|
And idx the watcher registers the file for resource "res-001" project "local/test"
|
|
When idx a FileCreatedEvent is simulated for the watched file
|
|
Then idx the on_change callback should fire within 2 seconds
|
|
And idx the callback should receive resource "res-001" project "local/test" change "created"
|
|
|
|
@file-watching
|
|
Scenario: File move triggers on_change with moved type
|
|
Given idx a ResourceFileWatcher with callback tracking and debounce 0.05
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
And idx the file watcher is started
|
|
And idx the watcher registers the file for resource "res-001" project "local/test"
|
|
When idx a FileMovedEvent is simulated for the watched file
|
|
Then idx the on_change callback should fire within 2 seconds
|
|
And idx the callback should receive resource "res-001" project "local/test" change "moved"
|
|
|
|
@file-watching
|
|
Scenario: Unwatch cancels pending debounce timer
|
|
Given idx a ResourceFileWatcher with callback tracking and debounce 5.0
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
And idx the file watcher is started
|
|
And idx the watcher registers the file for resource "res-001" project "local/test"
|
|
When idx a FileModifiedEvent is simulated for the watched file
|
|
And idx the watcher unregisters the watched file
|
|
And idx we wait 0.2 seconds for debounce
|
|
Then idx the on_change callback should have fired exactly 0 times
|
|
|
|
# =================================================================
|
|
# Coverage — UKOIndexer validation and error paths
|
|
# =================================================================
|
|
|
|
Scenario: UKOIndexer rejects invalid graph_backend type
|
|
Then idx creating UKOIndexer with invalid graph_backend should raise TypeError
|
|
|
|
Scenario: UKOIndexer analyzer_registry property returns the registry
|
|
Given idx a content reader with resource "01HQ8ZDRX50000000000000001" content "x = 1"
|
|
When idx I create an indexer
|
|
Then idx the indexer analyzer_registry should be the same registry
|
|
|
|
Scenario: Lifecycle hook error on on_indexed does not crash indexer
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a content reader with resource "01HQ8ZDRX50000000000000001" content "x = 1\n"
|
|
And idx a lifecycle hook that raises on on_indexed
|
|
When idx I create an indexer with the failing hook
|
|
And idx I index resource "01HQ8ZDRX50000000000000001" in project "local/test"
|
|
Then idx the index result should have 0 errors
|
|
|
|
Scenario: Lifecycle hook error on on_removed does not crash indexer
|
|
Given idx a content reader with resource "01HQ8ZDRX50000000000000001" content "x = 1\n"
|
|
And idx a lifecycle hook that raises on on_removed
|
|
When idx I create an indexer with the failing hook
|
|
And idx I index resource "01HQ8ZDRX50000000000000001" in project "local/test"
|
|
Then idx removing resource "01HQ8ZDRX50000000000000001" should not raise
|
|
|
|
Scenario: Lifecycle hook error on on_error does not crash indexer
|
|
Given idx a PythonAnalyzer is registered
|
|
And idx a lifecycle hook that raises on on_error
|
|
And idx a content reader that raises on read
|
|
When idx I create an indexer with the failing hook
|
|
And idx I index resource "01HQ8ZDRX50000000000000001" in project "local/test"
|
|
Then idx the index result should have errors
|
|
|
|
Scenario: Analyzer exception returns IndexResult with error
|
|
Given idx a content reader with resource "01HQ8ZDRX50000000000000001" content "x = 1\n"
|
|
And idx an analyzer that raises RuntimeError
|
|
When idx I create an indexer
|
|
And idx I index resource "01HQ8ZDRX50000000000000001" in project "local/test"
|
|
Then idx the index result should have errors
|
|
And idx the index result error should mention "Analyzer error"
|
|
|
|
Scenario: max_triples caps analyzer output
|
|
Given idx a content reader with resource "01HQ8ZDRX50000000000000001" content "x = 1\n"
|
|
And idx an analyzer that produces 100 triples
|
|
When idx I create an indexer with max_triples 5
|
|
And idx I index resource "01HQ8ZDRX50000000000000001" in project "local/test"
|
|
Then idx the index result triple_count should be at most 5
|
|
|
|
# =================================================================
|
|
# Coverage — LocationContentReader edge cases
|
|
# =================================================================
|
|
|
|
Scenario: LocationContentReader rejects non-regular file (e.g. named pipe)
|
|
Then idx LocationContentReader should reject a non-regular file
|
|
|
|
Scenario: LocationContentReader rejects content exceeding max size
|
|
Then idx LocationContentReader should reject content exceeding max size
|
|
|
|
# =================================================================
|
|
# Coverage — ResourceFileWatcher start with pre-registered paths
|
|
# =================================================================
|
|
|
|
@file-watching
|
|
Scenario: Starting watcher with pre-registered paths schedules them
|
|
Given idx a ResourceFileWatcher with callback tracking and debounce 0.05
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
And idx the watcher registers the file for resource "res-001" project "local/test"
|
|
When idx the file watcher is started
|
|
Then idx the file watcher should be running
|
|
And idx the watcher should have 1 watched paths
|
|
|
|
# =================================================================
|
|
# Coverage — IndexedDocument validation
|
|
# =================================================================
|
|
|
|
Scenario: IndexedDocument rejects empty doc_id
|
|
Then idx creating IndexedDocument with empty doc_id should raise ValueError
|
|
|
|
# =================================================================
|
|
# Coverage — Protocol stub NotImplementedError
|
|
# =================================================================
|
|
|
|
Scenario: ContentReader protocol raises NotImplementedError
|
|
Then idx calling ContentReader.read_content directly should raise NotImplementedError
|
|
|
|
Scenario: IndexLifecycleHook protocol raises NotImplementedError on on_indexed
|
|
Then idx calling IndexLifecycleHook.on_indexed directly should raise NotImplementedError
|
|
|
|
Scenario: IndexLifecycleHook protocol raises NotImplementedError on on_removed
|
|
Then idx calling IndexLifecycleHook.on_removed directly should raise NotImplementedError
|
|
|
|
Scenario: IndexLifecycleHook protocol raises NotImplementedError on on_error
|
|
Then idx calling IndexLifecycleHook.on_error directly should raise NotImplementedError
|
|
|
|
Scenario: AnalyzerProtocol raises NotImplementedError on supported_extensions
|
|
Then idx calling AnalyzerProtocol.supported_extensions directly should raise NotImplementedError
|
|
|
|
Scenario: AnalyzerProtocol raises NotImplementedError on domain
|
|
Then idx calling AnalyzerProtocol.domain directly should raise NotImplementedError
|
|
|
|
Scenario: AnalyzerProtocol raises NotImplementedError on analyze
|
|
Then idx calling AnalyzerProtocol.analyze directly should raise NotImplementedError
|
|
|
|
# =================================================================
|
|
# Coverage — UKOTriple validation edge cases
|
|
# =================================================================
|
|
|
|
Scenario: UKOTriple rejects whitespace-only subject_uri
|
|
Then idx creating UKOTriple with whitespace-only subject_uri should raise ValueError
|
|
|
|
Scenario: UKOTriple rejects whitespace-only predicate
|
|
Then idx creating UKOTriple with whitespace-only predicate should raise ValueError
|
|
|
|
Scenario: UKOTriple rejects both object_uri and object_value empty
|
|
Then idx creating UKOTriple with empty object_uri and empty object_value should raise ValueError
|
|
|
|
# =================================================================
|
|
# Coverage — AnalyzerRegistry edge cases
|
|
# =================================================================
|
|
|
|
Scenario: AnalyzerRegistry.get_for_resource returns None for extensionless location
|
|
Given idx a PythonAnalyzer is registered
|
|
When idx I look up analyzer for a resource with location "Makefile"
|
|
Then idx no analyzer should be found
|
|
|
|
# =================================================================
|
|
# Coverage — AnalyzerRegistry validation and listing
|
|
# =================================================================
|
|
|
|
Scenario: AnalyzerRegistry.register rejects non-protocol object
|
|
Then idx registering a non-protocol object should raise TypeError
|
|
|
|
Scenario: AnalyzerRegistry.register rejects analyzer with empty extensions
|
|
Then idx registering an analyzer with empty extensions should raise ValueError
|
|
|
|
Scenario: AnalyzerRegistry.get_for_extension with empty string returns None
|
|
Then idx looking up empty extension should return None
|
|
|
|
Scenario: AnalyzerRegistry.list_extensions returns registered extensions
|
|
Given idx a PythonAnalyzer is registered
|
|
Then idx list_extensions should include ".py"
|
|
|
|
Scenario: AnalyzerRegistry.list_analyzers returns registered analyzers
|
|
Given idx a PythonAnalyzer is registered
|
|
Then idx list_analyzers should return 1 analyzer
|
|
|
|
# =================================================================
|
|
# Coverage — fire_on_removed direct call with raising hook
|
|
# =================================================================
|
|
|
|
Scenario: fire_on_removed swallows hook exception
|
|
Then idx calling fire_on_removed with a raising hook should not raise
|
|
|
|
# =================================================================
|
|
# Coverage — ResourceFileWatcher additional paths
|
|
# =================================================================
|
|
|
|
@file-watching
|
|
Scenario: ResourceFileWatcher with auto_reindex disabled does not start observer
|
|
Given idx a ResourceFileWatcher with auto_reindex disabled
|
|
When idx the file watcher is started
|
|
Then idx the file watcher should not be running
|
|
|
|
@file-watching
|
|
Scenario: ResourceFileWatcher auto_reindex property reflects construction
|
|
Given idx a ResourceFileWatcher with auto_reindex disabled
|
|
Then idx the auto_reindex property should be false
|
|
|
|
@file-watching
|
|
Scenario: _fire_change with failing callback logs error but does not crash
|
|
Given idx a ResourceFileWatcher with a directly-failing callback and debounce 0.05
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
And idx the file watcher is started
|
|
And idx the watcher registers the file for resource "res-001" project "local/test"
|
|
When idx _fire_change is called directly with failing callback
|
|
Then idx the file watcher should be running
|
|
|
|
@file-watching
|
|
Scenario: _fire_change with failing event_bus logs error but does not crash
|
|
Given idx a ResourceFileWatcher with a failing event bus and debounce 0.05
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
And idx the file watcher is started
|
|
And idx the watcher registers the file for resource "res-001" project "local/test"
|
|
When idx _fire_change is called directly with failing event bus
|
|
Then idx the file watcher should be running
|
|
|
|
@file-watching
|
|
Scenario: _fire_change after stop is a no-op
|
|
Given idx a ResourceFileWatcher with callback tracking and debounce 0.05
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
And idx the file watcher is started
|
|
And idx the watcher registers the file for resource "res-001" project "local/test"
|
|
When idx the file watcher is stopped
|
|
And idx _fire_change is called directly after stop
|
|
Then idx the on_change callback should have fired exactly 0 times
|
|
|
|
@file-watching
|
|
Scenario: _ResourceChangeHandler routes events to watcher
|
|
Given idx a ResourceFileWatcher with callback tracking and debounce 0.05
|
|
And idx a temporary watched file "test.py" with content "x = 1"
|
|
And idx the file watcher is started
|
|
And idx the watcher registers the file for resource "res-001" project "local/test"
|
|
When idx a _ResourceChangeHandler is used to route events
|
|
Then idx the on_change callback should fire within 2 seconds
|
|
|
|
# =================================================================
|
|
# Coverage — removal backend failure paths
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: remove_resource tolerates graph backend failure
|
|
Given idx a clean analyzer registry
|
|
And idx in-memory index backends
|
|
And idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with removal-failing graph backend
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
And idx I remove resource "01HQ8ZDRX50000000000000001" from project "local/test"
|
|
Then idx the indexer should track 0 indexed resources
|
|
|
|
@coverage
|
|
Scenario: remove_resource tolerates text backend failure
|
|
Given idx a clean analyzer registry
|
|
And idx in-memory index backends
|
|
And idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with removal-failing text backend
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
And idx I remove resource "01HQ8ZDRX50000000000000001" from project "local/test"
|
|
Then idx the indexer should track 0 indexed resources
|
|
|
|
@coverage
|
|
Scenario: remove_resource tolerates vector backend failure
|
|
Given idx a clean analyzer registry
|
|
And idx in-memory index backends
|
|
And idx a PythonAnalyzer is registered
|
|
And idx a UKOIndexer with removal-failing vector backend
|
|
When idx I index a Python resource "01HQ8ZDRX50000000000000001" with content "class Foo:\n pass"
|
|
And idx I remove resource "01HQ8ZDRX50000000000000001" from project "local/test"
|
|
Then idx the indexer should track 0 indexed resources
|
|
|
|
# =================================================================
|
|
# Coverage — max_triples validation
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: max_triples of zero raises ValueError
|
|
Then idx creating a UKOIndexer with max_triples 0 should raise ValueError
|
|
|
|
# =================================================================
|
|
# Coverage — reindex_resource on a fresh (not previously indexed) resource
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: reindex_resource on a never-indexed resource succeeds
|
|
Given idx a clean analyzer registry
|
|
And idx in-memory index backends
|
|
And idx a PythonAnalyzer is registered
|
|
And idx a content reader with resource "01HQ8ZDRX50000000000000001" content "x = 1\n"
|
|
When idx I create an indexer
|
|
And idx I reindex resource "01HQ8ZDRX50000000000000001" in project "local/test"
|
|
Then idx the index result should have triple_count greater than 0
|
|
|
|
# =================================================================
|
|
# Coverage — index_graph edge cases (internals)
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: index_graph skips triple with no object_uri and no object_value
|
|
Then idx index_graph should skip triples with empty object
|
|
|
|
@coverage
|
|
Scenario: index_graph handles rdfs:label failure for dual-object triple
|
|
Then idx index_graph should tolerate rdfs:label failure
|
|
|
|
@coverage
|
|
Scenario: index_graph handles provenance link failure
|
|
Then idx index_graph should tolerate provenance link failure
|
|
|
|
# =================================================================
|
|
# Coverage — index_stubs edge cases
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: InMemoryTextIndexBackend search respects limit
|
|
Then idx text search should stop at the limit
|
|
|
|
@coverage
|
|
Scenario: InMemoryVectorIndexBackend rejects empty query_embedding
|
|
Then idx vector search_similar with empty embedding should raise ValueError
|
|
|
|
@coverage
|
|
Scenario: InMemoryVectorIndexBackend rejects zero limit
|
|
Then idx vector search_similar with limit 0 should raise ValueError
|
|
|
|
@coverage
|
|
Scenario: InMemoryVectorIndexBackend search filters by project
|
|
Then idx vector search_similar filters by project
|
|
|
|
@coverage
|
|
Scenario: InMemoryGraphIndexBackend remove_triples on missing project is no-op
|
|
Then idx graph remove_triples on missing project should be no-op
|
|
|
|
@coverage
|
|
Scenario: InMemoryGraphIndexBackend triple_count across all projects
|
|
Then idx graph triple_count without project should sum all projects
|
|
|
|
# =================================================================
|
|
# Coverage — index_backends protocol base methods
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: Protocol base method bodies are exercisable
|
|
Then idx all index backend protocol base methods should be callable
|
|
|
|
# =================================================================
|
|
# Coverage — min_relevance filtering (P3-3)
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: InMemoryVectorIndexBackend min_relevance above 1.0 raises ValueError
|
|
Then idx vector search_similar with min_relevance above 1.0 raises ValueError
|
|
|
|
@coverage
|
|
Scenario: InMemoryVectorIndexBackend min_relevance 1.0 includes exact matches
|
|
Then idx vector search_similar with min_relevance 1.0 includes exact matches
|
|
|
|
@coverage
|
|
Scenario: InMemoryVectorIndexBackend min_relevance 0.0 includes all matches
|
|
Then idx vector search_similar with min_relevance 0.0 includes all matches
|
|
|
|
# =================================================================
|
|
# Coverage — stub size limits (P2-16)
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: InMemoryTextIndexBackend enforces max_entries limit
|
|
Then idx text stub should raise RuntimeError when max_entries exceeded
|
|
|
|
@coverage
|
|
Scenario: InMemoryVectorIndexBackend enforces max_entries limit
|
|
Then idx vector stub should raise RuntimeError when max_entries exceeded
|
|
|
|
@coverage
|
|
Scenario: InMemoryGraphIndexBackend enforces max_entries limit
|
|
Then idx graph stub should raise RuntimeError when max_entries exceeded
|
|
|
|
# =================================================================
|
|
# Coverage — provenance metadata persistence (P2-22, P2-27)
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: index_graph stores full provenance metadata and confidence
|
|
Then idx index_graph should store provenance metadata triples
|
|
|
|
# =================================================================
|
|
# Coverage — callback + event_bus combo (P3-#18)
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: ResourceFileWatcher fires both callback and EventBus
|
|
Then idx file watcher should fire both callback and event_bus on change
|
|
|
|
# =================================================================
|
|
# Coverage — source_range validation (#28)
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: ProvenanceMetadata rejects invalid source_range format
|
|
Then idx creating ProvenanceMetadata with invalid source_range should raise
|
|
|
|
@coverage
|
|
Scenario: ProvenanceMetadata accepts empty source_range
|
|
Then idx creating ProvenanceMetadata with empty source_range should succeed
|
|
|
|
# =================================================================
|
|
# Coverage — move to new directory (#4)
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: FileMovedEvent to new directory schedules observer for dest parent
|
|
Then idx file watcher should schedule new dir on cross-directory move
|
|
|
|
# =================================================================
|
|
# Coverage — concurrency (#58)
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: Concurrent indexing of different resources is thread-safe
|
|
Then idx concurrent indexing of different resources should be thread-safe
|
|
|
|
# =================================================================
|
|
# Coverage — move then modify sequence (#59)
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: FileMovedEvent followed by FileModifiedEvent on new path
|
|
Then idx file watcher should handle move then modify sequence
|
|
|
|
# =================================================================
|
|
# Coverage — empty-string filter validation in remove_triples
|
|
# =================================================================
|
|
|
|
@coverage
|
|
Scenario: Graph remove_triples rejects empty-string subject filter
|
|
Given idx a clean analyzer registry
|
|
And idx in-memory index backends
|
|
Then idx removing triples with empty-string subject should raise ValueError
|
|
|
|
@coverage
|
|
Scenario: Graph remove_triples rejects empty-string predicate filter
|
|
Given idx a clean analyzer registry
|
|
And idx in-memory index backends
|
|
Then idx removing triples with empty-string predicate should raise ValueError
|
|
|
|
@coverage
|
|
Scenario: Graph remove_triples rejects empty-string obj filter
|
|
Given idx a clean analyzer registry
|
|
And idx in-memory index backends
|
|
Then idx removing triples with empty-string obj should raise ValueError
|
|
|
|
# =================================================================
|
|
# Coverage — dest timer cancel and EventBus move dest_path
|
|
# =================================================================
|
|
|
|
@coverage @file-watching
|
|
Scenario: Rapid create-then-move cancels prior dest timer
|
|
Given idx a clean analyzer registry
|
|
And idx in-memory index backends
|
|
Then idx file watcher should cancel prior dest timer on rapid sequence
|
|
|
|
@coverage @file-watching
|
|
Scenario: EventBus move event includes dest_path in details
|
|
Given idx a clean analyzer registry
|
|
And idx in-memory index backends
|
|
Then idx file watcher EventBus move event should include dest_path
|