Files
cleveragents-core/features/steps/domain_analyzer_steps.py
hamza.khyari 7d958121ad
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 16s
CI / build (pull_request) Successful in 16s
CI / quality (pull_request) Successful in 23s
CI / security (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 43s
CI / unit_tests (pull_request) Successful in 2m40s
CI / integration_tests (pull_request) Successful in 3m10s
CI / docker (pull_request) Successful in 40s
CI / coverage (pull_request) Successful in 5m1s
CI / benchmark-regression (pull_request) Successful in 31m13s
feat(context): add repo indexing service
Add RepoIndexingService with incremental refresh, language detection,
SHA-256 hashing, and policy enforcement. Includes domain models,
DB persistence, DI wiring, Behave/Robot/ASV tests, and reference docs.

ISSUES CLOSED: #195
2026-03-09 23:55:33 +00:00

402 lines
14 KiB
Python

"""Step definitions for domain-specific analyzers feature.
Covers the four built-in analyzers (Python, Markdown, PostgreSQL,
Docker Compose), the ``AnalyzerRegistry``, and protocol compliance
checks.
Step patterns are prefixed or worded to avoid collisions with existing
step definitions in ``uko_analyzers_steps.py``. Triple-assertion steps
use ``use_step_matcher("re")`` to avoid ``parse``-format ambiguity.
"""
from __future__ import annotations
from behave import given, then, use_step_matcher, when
from behave.runner import Context
from cleveragents.domain.models.acms.analyzers import (
AnalyzerProtocol,
AnalyzerRegistry,
UKOTriple,
)
from cleveragents.domain.models.acms.docker_compose_analyzer import (
DockerComposeAnalyzer,
)
from cleveragents.domain.models.acms.markdown_analyzer import MarkdownAnalyzer
from cleveragents.domain.models.acms.postgresql_analyzer import PostgreSQLAnalyzer
from cleveragents.domain.models.acms.python_analyzer import PythonAnalyzer
__all__: list[str] = []
# ---------------------------------------------------------------------------
# Analyzer type mapping
# ---------------------------------------------------------------------------
_ANALYZER_CLASSES: dict[str, type] = {
"PythonAnalyzer": PythonAnalyzer,
"MarkdownAnalyzer": MarkdownAnalyzer,
"PostgreSQLAnalyzer": PostgreSQLAnalyzer,
"DockerComposeAnalyzer": DockerComposeAnalyzer,
}
_ALL_ANALYZERS = (
PythonAnalyzer,
MarkdownAnalyzer,
PostgreSQLAnalyzer,
DockerComposeAnalyzer,
)
# Sample content for each analyzer (cross-analyzer scenarios).
_SAMPLE_CONTENT: dict[str, str] = {
"PythonAnalyzer": "class Example:\n pass\n",
"MarkdownAnalyzer": "# Title\nSome text.\n",
"PostgreSQLAnalyzer": ("CREATE TABLE sample (\n id SERIAL PRIMARY KEY\n);\n"),
"DockerComposeAnalyzer": "services:\n web:\n image: nginx\n",
}
_SAMPLE_URI = "uko://test/resource"
# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------
@given("a {analyzer_type} analyzer")
def step_given_named_analyzer(context: Context, analyzer_type: str) -> None:
cls = _ANALYZER_CLASSES.get(analyzer_type)
if cls is None:
raise ValueError(
f"Unknown analyzer type {analyzer_type!r}. "
f"Known types: {sorted(_ANALYZER_CLASSES)}"
)
context.analyzer = cls()
# Track all created analyzers for cross-analyzer scenarios.
if not hasattr(context, "analyzers"):
context.analyzers = {}
context.analyzers[analyzer_type] = context.analyzer
@given("a fresh analyzer registry")
def step_given_fresh_registry(context: Context) -> None:
context.registry = AnalyzerRegistry()
@given("a fresh analyzer registry with all domain analyzers registered")
def step_given_fresh_registry_with_all(context: Context) -> None:
context.registry = AnalyzerRegistry()
for cls in _ALL_ANALYZERS:
context.registry.register(cls())
# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------
@when("I register all 4 domain analyzers")
def step_when_register_all_four(context: Context) -> None:
for cls in _ALL_ANALYZERS:
context.registry.register(cls())
@when("I register a PythonAnalyzer")
def step_when_register_python(context: Context) -> None:
context.registry.register(PythonAnalyzer())
@when('I register a duplicate analyzer that also claims ".py"')
def step_when_register_duplicate_py(context: Context) -> None:
"""Register a second analyzer whose supported_extensions include .py."""
class _DuplicatePyAnalyzer:
@property
def supported_extensions(self) -> frozenset[str]:
return frozenset({".py"})
@property
def domain(self) -> str:
return "duplicate"
def analyze(self, content: str, resource_uri: str) -> list[UKOTriple]:
return [] # pragma: no cover
_dup: type[AnalyzerProtocol] = _DuplicatePyAnalyzer
del _dup # only used for static type-checking assertion
context.registry.register(_DuplicatePyAnalyzer())
@when('I look up extension "{ext}" in the registry')
def step_when_lookup_extension_in_registry(context: Context, ext: str) -> None:
result = context.registry.get_for_extension(ext)
context.lookup_result = result
if result is not None:
context.analyzer = result
@when("I analyze the Python content:")
def step_when_analyze_python(context: Context) -> None:
context.triples = context.analyzer.analyze(context.text, _SAMPLE_URI)
@when("I analyze the Markdown content:")
def step_when_analyze_markdown(context: Context) -> None:
context.triples = context.analyzer.analyze(context.text, _SAMPLE_URI)
@when("I analyze the SQL content:")
def step_when_analyze_sql(context: Context) -> None:
context.triples = context.analyzer.analyze(context.text, _SAMPLE_URI)
@when("I analyze the Docker Compose content:")
def step_when_analyze_docker_compose(context: Context) -> None:
context.triples = context.analyzer.analyze(context.text, _SAMPLE_URI)
@when("each analyzer processes its sample content")
def step_when_each_analyzer_processes_sample(context: Context) -> None:
all_triples: list[UKOTriple] = []
for name, analyzer in context.analyzers.items():
sample = _SAMPLE_CONTENT[name]
triples = analyzer.analyze(sample, _SAMPLE_URI)
all_triples.extend(triples)
context.triples = all_triples
# ---------------------------------------------------------------------------
# Then steps — protocol compliance
# ---------------------------------------------------------------------------
@then("the analyzer should satisfy the AnalyzerProtocol")
def step_then_satisfies_protocol(context: Context) -> None:
assert isinstance(context.analyzer, AnalyzerProtocol), (
f"{type(context.analyzer).__name__} does not satisfy AnalyzerProtocol"
)
@then('the current analyzer domain should be "{domain}"')
def step_then_current_analyzer_domain(context: Context, domain: str) -> None:
actual = context.analyzer.domain
assert actual == domain, f"Expected domain={domain!r}, got {actual!r}"
# ---------------------------------------------------------------------------
# Then steps — registry assertions
# ---------------------------------------------------------------------------
@then("the registry should contain {count:d} analyzers")
def step_then_registry_count(context: Context, count: int) -> None:
actual = len(context.registry)
assert actual == count, f"Expected {count} analyzers in registry, got {actual}"
@then("the extension lookup result should be None")
def step_then_extension_lookup_none(context: Context) -> None:
assert context.lookup_result is None, (
f"Expected None, got {context.lookup_result!r}"
)
@then('the registry extensions should include "{ext}"')
def step_then_registry_extensions_include(context: Context, ext: str) -> None:
extensions = context.registry.list_extensions()
assert ext in extensions, (
f"Extension {ext!r} not in registered extensions {extensions}"
)
# ---------------------------------------------------------------------------
# Then steps — triple count assertions
# ---------------------------------------------------------------------------
@then("no triples should be returned")
def step_then_no_triples(context: Context) -> None:
actual = len(context.triples)
assert actual == 0, f"Expected 0 triples, got {actual}.\nTriples: {context.triples}"
@then("the number of Table type triples should be {count:d}")
def step_then_table_type_count(context: Context, count: int) -> None:
actual = sum(
1
for t in context.triples
if t.predicate == "rdf:type" and t.object_uri == "uko-data:Table"
)
assert actual == count, f"Expected {count} Table type triples, got {actual}"
# =========================================================================
# Switch to regex matcher for triple-assertion steps to avoid ambiguity
# that behave's ``parse`` format causes when patterns share a prefix
# (e.g. "{pred}" greedily consuming "… and object_uri …").
# =========================================================================
use_step_matcher("re")
@then(
r'the triples should include predicate "(?P<pred>[^"]+)"'
r' with object_uri "(?P<obj>[^"]+)"'
)
def step_then_triple_pred_obj_uri(context: Context, pred: str, obj: str) -> None:
found = any(t.predicate == pred and t.object_uri == obj for t in context.triples)
assert found, (
f"No triple with predicate={pred!r} object_uri={obj!r} in {context.triples}"
)
@then(
r'the triples should include predicate "(?P<pred>[^"]+)"'
r' with object_value "(?P<val>[^"]+)"'
)
def step_then_triple_pred_obj_val(context: Context, pred: str, val: str) -> None:
found = any(t.predicate == pred and t.object_value == val for t in context.triples)
assert found, (
f"No triple with predicate={pred!r} object_value={val!r} in {context.triples}"
)
@then(
r'the triples should include predicate "(?P<pred>[^"]+)"'
r' with object_uri containing "(?P<fragment>[^"]+)"'
)
def step_then_triple_pred_obj_uri_contains(
context: Context, pred: str, fragment: str
) -> None:
found = any(
t.predicate == pred and t.object_uri is not None and fragment in t.object_uri
for t in context.triples
)
assert found, (
f"No triple with predicate={pred!r} and object_uri containing "
f"{fragment!r} in {context.triples}"
)
@then(
r'the triples should include predicate "(?P<pred>[^"]+)"'
r' linking subject "(?P<s_frag>[^"]+)"'
r' to object "(?P<o_frag>[^"]+)"'
)
def step_then_triple_links_subject_to_object(
context: Context, pred: str, s_frag: str, o_frag: str
) -> None:
found = any(
t.predicate == pred
and s_frag in t.subject_uri
and (
(t.object_uri is not None and o_frag in t.object_uri)
or o_frag in t.object_value
)
for t in context.triples
)
assert found, (
f"No triple with predicate={pred!r} linking subject containing "
f"{s_frag!r} to object containing {o_frag!r} in {context.triples}"
)
@then(
r'the triples should not include predicate "(?P<pred>[^"]+)"'
r' with object_uri "(?P<obj>[^"]+)"'
)
def step_then_no_triple_pred_obj_uri(context: Context, pred: str, obj: str) -> None:
found = any(t.predicate == pred and t.object_uri == obj for t in context.triples)
assert not found, (
f"Unexpected triple with predicate={pred!r} object_uri={obj!r} "
f"found in {context.triples}"
)
@then(
r'the triples should not include predicate "(?P<pred>[^"]+)"'
r' with object_value "(?P<val>[^"]+)"'
)
def step_then_no_triple_pred_obj_val(context: Context, pred: str, val: str) -> None:
found = any(t.predicate == pred and t.object_value == val for t in context.triples)
assert not found, (
f"Unexpected triple with predicate={pred!r} object_value={val!r} "
f"found in {context.triples}"
)
@then(r'the triples should include predicate "(?P<pred>[^"]+)"')
def step_then_triple_has_predicate(context: Context, pred: str) -> None:
found = any(t.predicate == pred for t in context.triples)
assert found, f"No triple with predicate={pred!r} in {context.triples}"
# Switch back to parse matcher for remaining steps.
use_step_matcher("parse")
# ---------------------------------------------------------------------------
# Then steps — error handling
# ---------------------------------------------------------------------------
@then("analyzing empty content with this analyzer should raise ValueError")
def step_then_empty_content_raises(context: Context) -> None:
try:
context.analyzer.analyze("", _SAMPLE_URI)
except ValueError:
return # Expected
raise AssertionError(
f"{type(context.analyzer).__name__}.analyze('', ...) did not raise ValueError"
)
@then("analyzing whitespace-only content with this analyzer should raise ValueError")
def step_then_whitespace_only_raises(context: Context) -> None:
try:
context.analyzer.analyze(" \n\t ", _SAMPLE_URI)
except ValueError:
return # Expected
raise AssertionError(
f"{type(context.analyzer).__name__}.analyze(' \\n\\t ', ...) "
"did not raise ValueError"
)
@then("analyzing content with empty resource_uri should raise ValueError")
def step_then_empty_resource_uri_raises(context: Context) -> None:
# Use a minimal valid content for each analyzer domain.
_domain_sample: dict[str, str] = {
"python": "class X:\n pass\n",
"markdown": "# Title\n",
"postgresql": "CREATE TABLE t (id SERIAL);\n",
"docker-compose": "services:\n web:\n image: nginx\n",
}
sample = _domain_sample.get(context.analyzer.domain, "content")
try:
context.analyzer.analyze(sample, "")
except ValueError:
return # Expected
raise AssertionError(
f"{type(context.analyzer).__name__}.analyze(..., '') did not raise ValueError"
)
# ---------------------------------------------------------------------------
# Then steps — URI scheme and confidence
# ---------------------------------------------------------------------------
@then('every triple subject_uri should start with "{prefix}"')
def step_then_all_subject_uris_start_with(context: Context, prefix: str) -> None:
for triple in context.triples:
assert triple.subject_uri.startswith(prefix), (
f"Triple subject_uri {triple.subject_uri!r} does not start with {prefix!r}"
)
@then("every triple confidence should be {score:g}")
def step_then_all_confidence_scores(context: Context, score: float) -> None:
for triple in context.triples:
assert triple.confidence == score, (
f"Triple {triple!r} has confidence={triple.confidence}, expected {score}"
)