Files
cleveragents-core/robot/helper_uko_indexer.py
T
aditya 2764fcef5c
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 16s
CI / build (pull_request) Successful in 25s
CI / quality (pull_request) Successful in 34s
CI / typecheck (pull_request) Successful in 39s
CI / security (pull_request) Successful in 52s
CI / unit_tests (pull_request) Successful in 2m51s
CI / integration_tests (pull_request) Successful in 3m41s
CI / docker (pull_request) Successful in 56s
CI / e2e_tests (pull_request) Successful in 3m56s
CI / coverage (pull_request) Successful in 6m28s
CI / benchmark-regression (pull_request) Successful in 38m39s
fix(actor,preflight,tests): resolve PR #975 review findings and stabilize full-suite coverage runs
Address review-driven fixes across actor schema, preflight guardrails, docs/examples,
and Behave/Robot coverage: unify preflight warning behavior with shared role-warning logic,
resolve actor-name to config payloads in production preflight flow, harden response_format
validation/coercion edge cases, extract duplicated helper logic, and expand negative-path
test coverage. Also fix cross-scenario patcher leakage in step modules to eliminate
full-run-only coverage failures.
2026-03-18 06:58:39 +00:00

496 lines
16 KiB
Python

"""Robot Framework helper for UKO Indexer smoke tests.
Provides a CLI-style interface for Robot to invoke UKOIndexer creation,
index lifecycle operations, graceful degradation, and provenance tracking.
Exit code 0 = success, 1 = failure.
Usage:
python robot/helper_uko_indexer.py <command>
"""
from __future__ import annotations
import sys
from collections.abc import Callable
from pathlib import Path
# Ensure the src directory is on the import path.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.application.services.uko_indexer import UKOIndexer # noqa: E402
from cleveragents.application.services.uko_indexer_protocols import ( # noqa: E402
ContentReader,
DefaultLifecycleHook,
LocationContentReader,
)
from cleveragents.domain.models.acms.analyzers import ( # noqa: E402
AnalyzerRegistry,
)
from cleveragents.domain.models.acms.index_backends import ( # noqa: E402
IndexedDocument,
SearchResult,
)
from cleveragents.domain.models.acms.index_stubs import ( # noqa: E402
InMemoryGraphIndexBackend,
InMemoryTextIndexBackend,
InMemoryVectorIndexBackend,
)
from cleveragents.domain.models.acms.provenance import ( # noqa: E402
IndexResult,
ProvenanceMetadata,
)
from cleveragents.domain.models.acms.python_analyzer import ( # noqa: E402
PythonAnalyzer,
)
from cleveragents.domain.models.core.resource import Resource # noqa: E402
# Ensure features/ is importable for shared mocks
_FEATURES = str(Path(__file__).resolve().parents[1])
if _FEATURES not in sys.path:
sys.path.insert(0, _FEATURES)
from features.mocks.uko_indexer_mocks import InMemoryContentReader # noqa: E402
ULID_1 = "01HQ8ZDRX50000000000000001"
ULID_2 = "01HQ8ZDRX50000000000000002"
PROJECT = "local/test"
def _make_resource(
resource_id: str,
location: str = "src/example.py",
) -> Resource:
"""Create a minimal Resource for testing."""
return Resource(
resource_id=resource_id,
resource_type_name="git-checkout",
location=location,
classification="physical",
)
def _make_indexer(
*,
text: bool = True,
vector: bool = True,
) -> tuple[
UKOIndexer,
InMemoryContentReader,
InMemoryTextIndexBackend | None,
InMemoryVectorIndexBackend | None,
InMemoryGraphIndexBackend,
]:
"""Create a fully-wired UKOIndexer with in-memory backends."""
registry = AnalyzerRegistry()
registry.register(PythonAnalyzer())
reader = InMemoryContentReader()
graph = InMemoryGraphIndexBackend()
text_be = InMemoryTextIndexBackend() if text else None
vector_be = InMemoryVectorIndexBackend() if vector else None
indexer = UKOIndexer(
analyzer_registry=registry,
graph_backend=graph,
text_backend=text_be,
vector_backend=vector_be,
content_reader=reader,
)
return (
indexer,
reader,
text_be,
vector_be,
graph,
)
def cmd_index_resource() -> int:
"""Test basic index_resource pipeline."""
try:
indexer, reader, _text, _vec, _graph = _make_indexer()
resource = _make_resource(ULID_1)
code = '"""Module doc."""\nclass Foo:\n """Class doc."""\n pass\n'
reader.set_content(ULID_1, code)
result = indexer.index_resource(resource, project=PROJECT)
assert result.resource_id == ULID_1
assert result.triple_count > 0
assert result.analyzer_domain == "python"
assert result.text_docs_indexed == 1
assert result.embeddings_indexed == 1
assert len(result.errors) == 0
assert indexer.indexed_resource_count == 1
print("uko-indexer-index-resource-ok")
return 0
except Exception as exc:
print(f"uko-indexer-index-resource-fail: {exc}")
return 1
def cmd_remove_resource() -> int:
"""Test remove_resource cleanup including backend state."""
try:
indexer, reader, text_be, vec_be, graph_be = _make_indexer()
resource = _make_resource(ULID_1)
reader.set_content(ULID_1, "x = 1\n")
indexer.index_resource(resource, project=PROJECT)
assert indexer.indexed_resource_count == 1
# Verify backends have data before removal
assert graph_be.triple_count(PROJECT) > 0
assert text_be is not None and text_be.document_count > 0
assert vec_be is not None and vec_be.embedding_count > 0
indexer.remove_resource(ULID_1, project=PROJECT)
assert indexer.indexed_resource_count == 0
# Verify backends are cleaned up after removal
assert graph_be.triple_count(PROJECT) == 0
assert text_be.document_count == 0
assert vec_be.embedding_count == 0
print("uko-indexer-remove-resource-ok")
return 0
except Exception as exc:
print(f"uko-indexer-remove-resource-fail: {exc}")
return 1
def cmd_reindex_resource() -> int:
"""Test reindex_resource (remove + re-index)."""
try:
indexer, reader, _text, _vec, _graph = _make_indexer()
resource = _make_resource(ULID_1)
reader.set_content(ULID_1, "x = 1\n")
result1 = indexer.index_resource(resource, project=PROJECT)
assert result1.triple_count > 0
# Update content and reindex
reader.set_content(
ULID_1,
'"""Reindexed."""\nclass Bar:\n pass\n',
)
result2 = indexer.reindex_resource(resource, project=PROJECT)
assert result2.resource_id == ULID_1
assert result2.triple_count > 0
assert indexer.indexed_resource_count == 1
print("uko-indexer-reindex-resource-ok")
return 0
except Exception as exc:
print(f"uko-indexer-reindex-resource-fail: {exc}")
return 1
def cmd_graceful_degradation() -> int:
"""Test graceful degradation without text/vector backends."""
try:
indexer, reader, _, _, _graph = _make_indexer(text=False, vector=False)
resource = _make_resource(ULID_1)
reader.set_content(ULID_1, "y = 2\n")
result = indexer.index_resource(resource, project=PROJECT)
assert result.triple_count > 0
assert result.text_docs_indexed == 0
assert result.embeddings_indexed == 0
assert not indexer.has_text_backend
assert not indexer.has_vector_backend
assert len(result.errors) == 0
print("uko-indexer-graceful-degradation-ok")
return 0
except Exception as exc:
print(f"uko-indexer-graceful-degradation-fail: {exc}")
return 1
def cmd_provenance() -> int:
"""Test provenance metadata is attached to triples."""
try:
# Verify ProvenanceMetadata construction
prov = ProvenanceMetadata(
source_resource=ULID_1,
source_path="src/example.py",
)
assert prov.is_current is True
assert prov.valid_from is not None
# Verify provenance is attached during indexing via IndexResult
indexer, reader, _, _, _graph = _make_indexer()
resource = _make_resource(ULID_1)
reader.set_content(ULID_1, "z = 3\n")
result = indexer.index_resource(resource, project=PROJECT)
assert result.triple_count > 0
assert result.resource_id == ULID_1
print("uko-indexer-provenance-ok")
return 0
except Exception as exc:
print(f"uko-indexer-provenance-fail: {exc}")
return 1
def cmd_index_backends() -> int:
"""Test in-memory index backend operations."""
try:
# Text backend
text_be = InMemoryTextIndexBackend()
text_be.index_document(PROJECT, "doc1", "hello world", {"key": "val"})
results = text_be.search(PROJECT, "hello", limit=5)
assert len(results) == 1
assert results[0].doc_id == "doc1"
text_be.remove_document(PROJECT, "doc1")
results = text_be.search(PROJECT, "hello", limit=5)
assert len(results) == 0
# Vector backend
vec_be = InMemoryVectorIndexBackend()
vec_be.index_embedding(PROJECT, "emb1", [1.0, 2.0, 3.0], {"key": "val"})
results = vec_be.search_similar(PROJECT, [1.0, 2.0, 3.0], limit=5)
assert len(results) == 1
assert results[0].doc_id == "emb1"
vec_be.remove_embedding(PROJECT, "emb1")
results = vec_be.search_similar(PROJECT, [1.0, 2.0, 3.0], limit=5)
assert len(results) == 0
# Graph backend
graph_be = InMemoryGraphIndexBackend()
graph_be.add_triple(PROJECT, "s1", "p1", "o1")
bindings = graph_be.query(PROJECT, "SELECT * WHERE { ?s ?p ?o }")
assert len(bindings) == 1
assert bindings[0]["s"] == "s1"
assert bindings[0]["p"] == "p1"
assert bindings[0]["o"] == "o1"
graph_be.remove_triples(PROJECT, subject="s1", predicate=None, obj=None)
bindings = graph_be.query(PROJECT, "SELECT * WHERE { ?s ?p ?o }")
assert len(bindings) == 0
# Validation
doc = IndexedDocument(project=PROJECT, doc_id="d1", char_count=10)
assert doc.char_count == 10
sr = SearchResult(doc_id="d1", score=0.5, content="test")
assert sr.score == 0.5
print("uko-indexer-index-backends-ok")
return 0
except Exception as exc:
print(f"uko-indexer-index-backends-fail: {exc}")
return 1
def cmd_validation() -> int:
"""Test input validation guards."""
try:
indexer, reader, _, _, _ = _make_indexer()
resource = _make_resource(ULID_1)
reader.set_content(ULID_1, "a = 1\n")
# Empty project
try:
indexer.index_resource(resource, project="")
print("uko-indexer-validation-fail: no ValueError for empty project")
return 1
except ValueError:
pass
# Empty resource_id on remove
try:
indexer.remove_resource("", project=PROJECT)
print("uko-indexer-validation-fail: no ValueError for empty resource_id")
return 1
except ValueError:
pass
# Invalid analyzer_registry type
try:
UKOIndexer(
analyzer_registry="not_a_registry", # type: ignore[arg-type]
graph_backend=InMemoryGraphIndexBackend(),
)
print("uko-indexer-validation-fail: no TypeError for invalid registry")
return 1
except TypeError:
pass
# SearchResult score out of range
try:
SearchResult(doc_id="x", score=1.5, content="test")
print("uko-indexer-validation-fail: no ValueError for score > 1.0")
return 1
except ValueError:
pass
print("uko-indexer-validation-ok")
return 0
except Exception as exc:
print(f"uko-indexer-validation-fail: {exc}")
return 1
def cmd_protocol_compliance() -> int:
"""Verify protocol compliance for key types."""
try:
reader = InMemoryContentReader()
assert isinstance(reader, ContentReader)
hook = DefaultLifecycleHook()
# DefaultLifecycleHook logs but doesn't fail
result = IndexResult(
resource_id=ULID_1,
analyzer_domain="python",
triple_count=5,
)
hook.on_indexed(result)
hook.on_removed(ULID_1, PROJECT)
hook.on_error(ULID_1, "test error")
# LocationContentReader protocol compliance
loc_reader = LocationContentReader()
assert isinstance(loc_reader, ContentReader)
print("uko-indexer-protocol-compliance-ok")
return 0
except Exception as exc:
print(f"uko-indexer-protocol-compliance-fail: {exc}")
return 1
def cmd_file_watching() -> int:
"""Test ResourceFileWatcher lifecycle, change detection, and EventBus."""
import threading
import time
from watchdog.events import FileDeletedEvent, FileModifiedEvent
from cleveragents.application.services.resource_file_watcher import (
FileChangeType,
ResourceFileWatcher,
)
from cleveragents.infrastructure.events.models import DomainEvent
from cleveragents.infrastructure.events.types import EventType
class _MiniEventBus:
def __init__(self) -> None:
self.events: list[DomainEvent] = []
self._ev = threading.Event()
def emit(self, event: DomainEvent) -> None:
self.events.append(event)
self._ev.set()
def subscribe(self, event_type: EventType, handler: object) -> None:
_ = event_type, handler
try:
import tempfile as _tf
from pathlib import Path as _P
tmpdir = _tf.mkdtemp()
fpath = _P(tmpdir) / "watched.py"
fpath.write_text("x = 1\n")
changes: list[tuple[str, str, FileChangeType]] = []
cb_event = threading.Event()
def _on_change(rid: str, proj: str, ct: FileChangeType) -> None:
changes.append((rid, proj, ct))
cb_event.set()
bus = _MiniEventBus()
watcher = ResourceFileWatcher(
on_change=_on_change,
event_bus=bus,
debounce_seconds=0.05,
)
# 1. Lifecycle
# Avoid creating real OS-level inotify watches in CI/parallel runs,
# which can fail with ENOSPC when global watch limits are exhausted.
# We only need the watcher in a "running" state to exercise internal
# debounce/callback/event-bus behavior via synthetic watchdog events.
assert not watcher.is_running
watcher._running = True
assert watcher.is_running
# 2. Watch a file
watcher.watch(fpath, resource_id=ULID_1, project=PROJECT)
assert len(watcher.watched_paths) == 1
# 3. Simulate modification
ev = FileModifiedEvent(str(fpath.resolve()))
watcher._handle_fs_event(ev)
assert cb_event.wait(timeout=2.0), "Callback did not fire"
assert changes[-1][0] == ULID_1
assert changes[-1][2] == FileChangeType.MODIFIED
# 4. EventBus received event
time.sleep(0.1)
assert len(bus.events) > 0
assert bus.events[-1].event_type == EventType.RESOURCE_MODIFIED
assert bus.events[-1].details["resource_id"] == ULID_1
# 5. Simulate deletion
cb_event.clear()
ev2 = FileDeletedEvent(str(fpath.resolve()))
watcher._handle_fs_event(ev2)
assert cb_event.wait(timeout=2.0), "Callback did not fire for delete"
assert changes[-1][2] == FileChangeType.DELETED
# 6. Unwatch
watcher.unwatch(fpath)
assert len(watcher.watched_paths) == 0
# 7. Stop
watcher.stop()
assert not watcher.is_running
# Cleanup
fpath.unlink(missing_ok=True)
_P(tmpdir).rmdir()
print("uko-indexer-file-watching-ok")
return 0
except Exception as exc:
print(f"uko-indexer-file-watching-fail: {exc}")
import traceback
traceback.print_exc()
return 1
COMMANDS: dict[str, Callable[[], int]] = {
"index-resource": cmd_index_resource,
"remove-resource": cmd_remove_resource,
"reindex-resource": cmd_reindex_resource,
"graceful-degradation": cmd_graceful_degradation,
"provenance": cmd_provenance,
"index-backends": cmd_index_backends,
"validation": cmd_validation,
"protocol-compliance": cmd_protocol_compliance,
"file-watching": cmd_file_watching,
}
def main() -> int:
"""Entry point called by Robot Framework ``Run Process``."""
if len(sys.argv) < 2:
print("Usage: helper_uko_indexer.py <command>")
print(f"Commands: {', '.join(COMMANDS)}")
return 1
command: str = sys.argv[1]
handler = COMMANDS.get(command)
if handler is None:
print(f"Unknown command: {command}")
print(f"Commands: {', '.join(COMMANDS)}")
return 1
return handler()
if __name__ == "__main__":
sys.exit(main())