Files
cleveragents-core/features/steps/repo_indexing_assertion_steps.py
T
aditya 4e64544aae
CI / benchmark-publish (pull_request) Has been skipped
CI / quality (pull_request) Successful in 48s
CI / build (pull_request) Successful in 31s
CI / helm (pull_request) Successful in 21s
CI / lint (pull_request) Successful in 3m15s
CI / typecheck (pull_request) Successful in 4m4s
CI / security (pull_request) Successful in 4m13s
CI / unit_tests (pull_request) Successful in 4m28s
CI / docker (pull_request) Successful in 1m52s
CI / integration_tests (pull_request) Successful in 7m14s
CI / coverage (pull_request) Successful in 12m49s
CI / e2e_tests (pull_request) Successful in 22m8s
CI / status-check (pull_request) Successful in 2s
CI / benchmark-regression (pull_request) Successful in 57m27s
feat(acms): projects with 10,000+ files index without timeout
Add explicit indexing timeout bounds and propagate timeout_seconds through the repo indexing service, utility walker, and CLI entrypoint so large repositories fail predictably instead of hanging.

Add 10K-scale Behave/Robot verification and a dedicated benchmark path to validate that large-project indexing completes, persists status correctly, and remains measurable for regressions.

Address review feedback by splitting oversized Behave and Robot support files into focused modules, trimming repo_indexing_service.py back under the 500-line limit, and rebasing the branch onto current master.

ISSUES CLOSED: #851

# Conflicts:
#	CHANGELOG.md
#	robot/helper_m5_e2e_verification.py
2026-03-30 10:18:19 +00:00

329 lines
14 KiB
Python

"""Assertion and cleanup steps for ``features/repo_indexing.feature``."""
from __future__ import annotations
from behave import given, then, when # type: ignore[import-untyped]
from behave.runner import Context # type: ignore[import-untyped]
from cleveragents.application.services.repo_indexing_service import (
RepoIndexingService,
)
from cleveragents.domain.models.core.repo_index import (
FileRecord,
IndexMetadata,
RepoIndex,
)
from cleveragents.infrastructure.database.models import RepoIndexModel
_RESOURCE_ID = "01KK0D8WNATFNEX2JMG5GKF6FM"
_STALE_RESOURCE_ID = "01KK0D8WNATFNEX2JMG5GKF6FN"
@then('the repo-index result status should be "{status}"')
def step_check_status(context: Context, status: str) -> None:
"""Assert the index result status."""
result: RepoIndex = context.repo_index_result # type: ignore[attr-defined]
actual = result.metadata.status.value
assert actual == status, f"Expected status '{status}', got '{actual}'"
@then("the repo-index result file count should be {count:d}")
def step_check_file_count(context: Context, count: int) -> None:
"""Assert the file count."""
result: RepoIndex = context.repo_index_result # type: ignore[attr-defined]
actual = result.metadata.file_count
assert actual == count, (
f"Expected {count} files, got {actual}. Files: {[f.path for f in result.files]}"
)
@then("the repo-index result file count should be less than {count:d}")
def step_check_file_count_less(context: Context, count: int) -> None:
"""Assert the file count is less than a value."""
result: RepoIndex = context.repo_index_result # type: ignore[attr-defined]
actual = result.metadata.file_count
assert actual < count, f"Expected fewer than {count} files, got {actual}"
@then("the repo-index indexing elapsed seconds should be less than {seconds:d}")
def step_check_index_elapsed(context: Context, seconds: int) -> None:
"""Assert indexing runtime remained under the configured bound."""
elapsed = context.repo_index_elapsed_seconds # type: ignore[attr-defined]
assert elapsed < float(seconds), (
f"Expected elapsed < {seconds}s, got {elapsed:.2f}s"
)
@then('the repo-index primary language should be "{lang}"')
def step_check_language(context: Context, lang: str) -> None:
"""Assert the primary language."""
result: RepoIndex = context.repo_index_result # type: ignore[attr-defined]
actual = result.metadata.primary_language
assert actual == lang, f"Expected language '{lang}', got '{actual}'"
@then("the repo-index token estimate should be positive")
def step_check_tokens_positive(context: Context) -> None:
"""Assert token estimate is > 0."""
result: RepoIndex = context.repo_index_result # type: ignore[attr-defined]
assert result.metadata.token_estimate > 0, (
f"Expected positive token_estimate, got {result.metadata.token_estimate}"
)
@then('the repo-index file "{filename}" should have language "{lang}"')
def step_check_file_language(context: Context, filename: str, lang: str) -> None:
"""Assert a specific file's detected language."""
result: RepoIndex = context.repo_index_result # type: ignore[attr-defined]
matches = [f for f in result.files if f.path == filename]
assert matches, (
f"File '{filename}' not found in index. Files: {[f.path for f in result.files]}"
)
actual = matches[0].language
assert actual == lang, f"Expected '{filename}' language='{lang}', got '{actual}'"
@then("the repo-index retrieved index should match the original")
def step_check_retrieved_matches(context: Context) -> None:
"""Assert the retrieved index matches the original."""
original: RepoIndex = context.repo_index_original_result # type: ignore[attr-defined]
retrieved: RepoIndex = context.repo_index_retrieved # type: ignore[attr-defined]
assert retrieved is not None, "Retrieved index is None"
assert retrieved.metadata.file_count == original.metadata.file_count
assert retrieved.metadata.token_estimate == original.metadata.token_estimate
assert retrieved.metadata.primary_language == original.metadata.primary_language
assert len(retrieved.files) == len(original.files)
@then("the repo-index retrieved file hashes should match the original")
def step_check_retrieved_hashes(context: Context) -> None:
"""Assert the retrieved index has identical per-file content hashes."""
original: RepoIndex = context.repo_index_original_result # type: ignore[attr-defined]
retrieved: RepoIndex = context.repo_index_retrieved # type: ignore[attr-defined]
orig_map = {f.path: f.content_hash for f in original.files}
retr_map = {f.path: f.content_hash for f in retrieved.files}
assert orig_map == retr_map, (
f"File hash mismatch.\nOriginal: {orig_map}\nRetrieved: {retr_map}"
)
@then('the repo-index file "{filename}" content hash should differ from original')
def step_check_hash_differs(context: Context, filename: str) -> None:
"""Assert a file's hash changed after modification."""
original: RepoIndex = context.repo_index_original_result # type: ignore[attr-defined]
current: RepoIndex = context.repo_index_result # type: ignore[attr-defined]
orig_hash = next(
(f.content_hash for f in original.files if f.path == filename),
None,
)
curr_hash = next(
(f.content_hash for f in current.files if f.path == filename),
None,
)
assert orig_hash is not None, f"'{filename}' not found in original index"
assert curr_hash is not None, f"'{filename}' not found in current index"
assert orig_hash != curr_hash, (
f"Expected hash for '{filename}' to change, but both are '{orig_hash}'"
)
@then('the repo-index file "{filename}" content hash should match original')
def step_check_hash_matches(context: Context, filename: str) -> None:
"""Assert a file's hash is unchanged."""
original: RepoIndex = context.repo_index_original_result # type: ignore[attr-defined]
current: RepoIndex = context.repo_index_result # type: ignore[attr-defined]
orig_hash = next(
(f.content_hash for f in original.files if f.path == filename),
None,
)
curr_hash = next(
(f.content_hash for f in current.files if f.path == filename),
None,
)
assert orig_hash is not None, f"'{filename}' not found in original index"
assert curr_hash is not None, f"'{filename}' not found in current index"
assert orig_hash == curr_hash, (
f"Expected hash for '{filename}' to match, but got '{orig_hash}' vs '{curr_hash}'"
)
@then('the repo-index result should not include file "{filename}"')
def step_check_file_excluded(context: Context, filename: str) -> None:
"""Assert a file is not in the index."""
result: RepoIndex = context.repo_index_result # type: ignore[attr-defined]
paths = [f.path for f in result.files]
assert filename not in paths, f"File '{filename}' should be excluded but was found"
@then("a repo-index error should be raised")
def step_check_any_error(context: Context) -> None:
"""Assert some error was raised."""
err = context.repo_index_error # type: ignore[attr-defined]
assert err is not None, "Expected an error but none raised"
@then('querying repo-index status for the resource should show "{status}"')
def step_check_error_status_persisted(context: Context, status: str) -> None:
"""Assert the persisted index status matches."""
service: RepoIndexingService = context.repo_index_service # type: ignore[attr-defined]
meta = service.get_index_status(_RESOURCE_ID)
assert meta is not None, (
f"Expected persisted status '{status}' but get_index_status returned None"
)
actual = meta.status.value
assert actual == status, f"Expected status '{status}', got '{actual}'"
@then("the repo-index previous index should still be retrievable with {count:d} files")
def step_check_previous_index(context: Context, count: int) -> None:
"""Assert the previous index is still intact."""
service: RepoIndexingService = context.repo_index_service # type: ignore[attr-defined]
idx = service.get_index(_RESOURCE_ID)
assert idx is not None, "Previous index was lost"
assert idx.metadata.file_count == count, (
f"Expected {count} files, got {idx.metadata.file_count}"
)
@then("the FileRecord last_modified should have UTC timezone")
def step_check_filerecord_utc(context: Context) -> None:
"""Assert the FileRecord has UTC timezone on last_modified."""
from datetime import UTC
fr: FileRecord = context.repo_index_filerecord # type: ignore[attr-defined]
assert fr.last_modified.tzinfo is not None, "last_modified has no timezone"
assert fr.last_modified.tzinfo == UTC, (
f"Expected UTC, got {fr.last_modified.tzinfo}"
)
@then("the IndexMetadata indexed_at should have UTC timezone")
def step_check_metadata_utc(context: Context) -> None:
"""Assert the IndexMetadata has UTC timezone on indexed_at."""
from datetime import UTC
meta: IndexMetadata = context.repo_index_metadata_naive # type: ignore[attr-defined]
assert meta.indexed_at.tzinfo is not None, "indexed_at has no timezone"
assert meta.indexed_at.tzinfo == UTC, f"Expected UTC, got {meta.indexed_at.tzinfo}"
@then('the repo-index status metadata should have status "{status}"')
def step_check_status_metadata(context: Context, status: str) -> None:
"""Assert the status metadata has the expected status."""
meta: IndexMetadata = context.repo_index_status # type: ignore[attr-defined]
assert meta is not None, "get_index_status returned None"
actual = meta.status.value
assert actual == status, f"Expected status '{status}', got '{actual}'"
@then("the repo-index status metadata file count should be {count:d}")
def step_check_status_file_count(context: Context, count: int) -> None:
"""Assert the status metadata file count."""
meta: IndexMetadata = context.repo_index_status # type: ignore[attr-defined]
assert meta is not None, "get_index_status returned None"
actual = meta.file_count
assert actual == count, f"Expected {count} files, got {actual}"
@then("querying repo-index for the resource should return nothing")
def step_check_removed(context: Context) -> None:
"""Assert the index is gone."""
service: RepoIndexingService = context.repo_index_service # type: ignore[attr-defined]
result = service.get_index(_RESOURCE_ID)
assert result is None, f"Expected None after removal, got {result}"
@then("the repo-index removal result should be false")
def step_check_removal_false(context: Context) -> None:
"""Assert removal returned False."""
assert context.repo_index_removal_result is False # type: ignore[attr-defined]
@then('a repo-index ValueError should be raised mentioning "{text}"')
def step_check_value_error(context: Context, text: str) -> None:
"""Assert a ValueError was raised with expected message."""
err = context.repo_index_error # type: ignore[attr-defined]
assert err is not None, "Expected ValueError but none raised"
assert isinstance(err, ValueError), f"Expected ValueError, got {type(err).__name__}"
assert text in str(err), f"Expected '{text}' in error: {err}"
@then("a repo-index FileNotFoundError should be raised")
def step_check_fnf_error(context: Context) -> None:
"""Assert a FileNotFoundError was raised."""
err = context.repo_index_error # type: ignore[attr-defined]
assert err is not None, "Expected FileNotFoundError but none raised"
assert isinstance(err, FileNotFoundError), (
f"Expected FileNotFoundError, got {type(err).__name__}"
)
@then('the repo-index metadata should have field "{field}"')
def step_check_metadata_field(context: Context, field: str) -> None:
"""Assert the metadata has a specific field."""
result: RepoIndex = context.repo_index_result # type: ignore[attr-defined]
assert hasattr(result.metadata, field), (
f"IndexMetadata missing field '{field}'. "
f"Available: {list(result.metadata.model_fields.keys())}"
)
@then('each repo-index file record should have field "{field}"')
def step_check_file_field(context: Context, field: str) -> None:
"""Assert every file record has a specific field."""
result: RepoIndex = context.repo_index_result # type: ignore[attr-defined]
assert len(result.files) > 0, "No file records to check"
for fr in result.files:
assert hasattr(fr, field), (
f"FileRecord missing field '{field}'. "
f"Available: {list(fr.model_fields.keys())}"
)
@given("a repo-index stale INDEXING row is inserted for a resource")
def step_insert_stale_indexing_row(context: Context) -> None:
"""Manually insert an INDEXING-status row to simulate a crash."""
from datetime import UTC, datetime
svc: RepoIndexingService = context.repo_index_service # type: ignore[attr-defined]
session = svc._session_factory()
try:
row = RepoIndexModel(
index_id="01KK0D8WNATFNEX2JMG5GKF6FZ",
resource_id=_STALE_RESOURCE_ID,
file_count=0,
token_estimate=0,
primary_language="unknown",
status="indexing",
indexed_at=datetime.now(tz=UTC).isoformat(),
created_at=datetime.now(tz=UTC).isoformat(),
)
session.add(row)
session.commit()
finally:
session.close()
context.repo_index_stale_resource_id = _STALE_RESOURCE_ID # type: ignore[attr-defined]
@when("I call repo-index cleanup stale indexing")
def step_call_cleanup(context: Context) -> None:
"""Call cleanup_stale_indexing on the service."""
svc: RepoIndexingService = context.repo_index_service # type: ignore[attr-defined]
context.repo_index_cleanup_count = svc.cleanup_stale_indexing() # type: ignore[attr-defined]
@then("the repo-index cleanup should report {count:d} stale row removed")
def step_check_cleanup_count(context: Context, count: int) -> None:
"""Assert the cleanup returned the expected count."""
actual = context.repo_index_cleanup_count # type: ignore[attr-defined]
assert actual == count, f"Expected {count} stale rows, got {actual}"
@then("querying repo-index for the stale resource should return nothing")
def step_check_stale_removed(context: Context) -> None:
"""Assert the stale row no longer exists."""
svc: RepoIndexingService = context.repo_index_service # type: ignore[attr-defined]
rid = context.repo_index_stale_resource_id # type: ignore[attr-defined]
result = svc.get_index(rid)
assert result is None, f"Expected None, got {result}"