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
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
329 lines
14 KiB
Python
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}"
|