4f950a1600
CI / lint (push) Successful in 24s
CI / build (push) Successful in 16s
CI / typecheck (push) Successful in 38s
CI / quality (push) Successful in 43s
CI / security (push) Successful in 53s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 2m39s
CI / unit_tests (push) Successful in 3m58s
CI / docker (push) Successful in 1m8s
CI / e2e_tests (push) Successful in 5m21s
CI / coverage (push) Successful in 6m31s
CI / benchmark-publish (push) Successful in 19m39s
## Summary Add `agents repo index` and `agents repo status` CLI commands, wiring the existing `RepoIndexingService` backend to the CLI layer. ### Commands - **`agents repo index <resource_name>`** — Triggers full or incremental indexing of a repository resource. Options: `--full` (force full re-index), `--format text|json` - **`agents repo status <resource_name>`** — Displays indexing metadata: status, file count, token estimate, primary language, last indexed timestamp. Options: `--format text|json` ### Implementation - New CLI module `src/cleveragents/cli/commands/repo.py` (238 lines) - Registered as `repo` subcommand group in `cli/main.py` - Resolves resource by name via `ResourceRegistryService` - Calls `RepoIndexingService.index_resource()` / `refresh_index()` / `get_index_status()` - Both text (Rich panel) and JSON output formats ### Quality Gates | Session | Result | |---|---| | `nox -s lint` | PASS | | `nox -s typecheck` | PASS (0 errors) | | `nox -s unit_tests` | PASS (10,819 scenarios) | | `nox -s coverage_report` | 98% (>= 97%) | | Integration tests | 5/5 PASS | Closes #856 Reviewed-on: #982 Co-authored-by: Brent Edwards <brent.edwards@cleverthis.com> Co-committed-by: Brent Edwards <brent.edwards@cleverthis.com>
274 lines
9.7 KiB
Python
274 lines
9.7 KiB
Python
"""Step definitions for Repository Indexing CLI feature tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import json
|
|
import tempfile
|
|
from io import StringIO
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
|
|
from behave import given, then, when # type: ignore[import-untyped]
|
|
from behave.runner import Context # type: ignore[import-untyped]
|
|
from rich.console import Console
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from cleveragents.application.services.repo_indexing_service import (
|
|
RepoIndexingService,
|
|
)
|
|
from cleveragents.application.services.resource_registry_service import (
|
|
ResourceRegistryService,
|
|
)
|
|
from cleveragents.infrastructure.database.models import Base
|
|
|
|
|
|
def _setup_services(
|
|
context: Context,
|
|
) -> tuple[ResourceRegistryService, RepoIndexingService]:
|
|
"""Return the cached services created by the Background step."""
|
|
return context._repo_cli_resource_svc, context._repo_cli_indexing_svc
|
|
|
|
|
|
def _capture_repo_command(
|
|
context: Context,
|
|
func: Any,
|
|
*args: Any,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
"""Run a CLI function capturing output and success status.
|
|
|
|
Captures both Rich console output (via *buf*) **and** direct
|
|
``sys.stdout`` writes (used by ``format_output`` for JSON/YAML
|
|
to avoid Rich line-wrapping).
|
|
"""
|
|
buf = StringIO()
|
|
stdout_buf = StringIO()
|
|
console = Console(
|
|
file=buf, width=200, no_color=True, highlight=False, force_terminal=False
|
|
)
|
|
|
|
import cleveragents.cli.commands.repo as repo_mod
|
|
|
|
orig_console = repo_mod.console
|
|
repo_mod.console = console
|
|
|
|
failed = False
|
|
try:
|
|
with contextlib.redirect_stdout(stdout_buf):
|
|
func(*args, **kwargs)
|
|
except SystemExit:
|
|
failed = True
|
|
except Exception:
|
|
failed = True
|
|
finally:
|
|
repo_mod.console = orig_console
|
|
|
|
# Combine Rich console output with any direct stdout writes
|
|
context.repo_cli_output = buf.getvalue() + stdout_buf.getvalue()
|
|
context.repo_cli_failed = failed
|
|
|
|
|
|
def _run_with_mock(
|
|
context: Context,
|
|
func: Any,
|
|
*args: Any,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
"""Run a CLI function with mocked container providing test services."""
|
|
resource_svc, indexing_svc = _setup_services(context)
|
|
|
|
with patch("cleveragents.cli.commands.repo.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.resource_registry_service.return_value = resource_svc
|
|
container.repo_indexing_service.return_value = indexing_svc
|
|
_capture_repo_command(context, func, *args, **kwargs)
|
|
|
|
|
|
@given("a fresh in-memory repo indexing environment")
|
|
def step_fresh_repo_env(context: Context) -> None:
|
|
"""Set up fresh in-memory services for repo indexing tests."""
|
|
# Create fresh in-memory database for each scenario. We cannot
|
|
# ``delattr`` on behave Context (it only works at the current level),
|
|
# so we unconditionally set new services each time.
|
|
engine = create_engine("sqlite:///:memory:", echo=False)
|
|
Base.metadata.create_all(engine)
|
|
factory = sessionmaker(bind=engine, expire_on_commit=False)
|
|
context._repo_cli_engine = engine
|
|
context._repo_cli_factory = factory
|
|
context._repo_cli_resource_svc = ResourceRegistryService(session_factory=factory)
|
|
context._repo_cli_resource_svc.bootstrap_builtin_types()
|
|
context._repo_cli_indexing_svc = RepoIndexingService(session_factory=factory)
|
|
context.repo_cli_output = ""
|
|
context.repo_cli_failed = False
|
|
context._repo_cli_temp_dirs = {} # dict[str, str]
|
|
|
|
|
|
@given('a registered git-checkout resource "{name}" with a temp directory')
|
|
def step_register_resource_with_tempdir(context: Context, name: str) -> None:
|
|
"""Register a git-checkout resource backed by a real temporary directory."""
|
|
resource_svc, _indexing_svc = _setup_services(context)
|
|
|
|
tmp_dir = tempfile.mkdtemp()
|
|
context._repo_cli_temp_dirs[name] = tmp_dir
|
|
|
|
sample = Path(tmp_dir) / "README.md"
|
|
sample.write_text("# Test Repository\n\nSample content for indexing.\n")
|
|
|
|
resource_svc.register_resource(
|
|
type_name="git-checkout",
|
|
name=name,
|
|
location=tmp_dir,
|
|
properties={"path": tmp_dir},
|
|
)
|
|
|
|
|
|
@given('I have already indexed "{name}"')
|
|
def step_already_indexed(context: Context, name: str) -> None:
|
|
"""Perform an initial index on the resource."""
|
|
resource_svc, indexing_svc = _setup_services(context)
|
|
resource = resource_svc.show_resource(name)
|
|
root_path = resource.location or ""
|
|
if not root_path and resource.properties:
|
|
path_val = resource.properties.get("path")
|
|
root_path = str(path_val) if path_val is not None else ""
|
|
indexing_svc.index_resource(resource.resource_id, root_path)
|
|
|
|
|
|
@when('I run repo index on resource "{name}"')
|
|
def step_run_repo_index(context: Context, name: str) -> None:
|
|
"""Run the repo index command (incremental)."""
|
|
from cleveragents.cli.commands.repo import index_command
|
|
|
|
_run_with_mock(context, index_command, name, full=False, fmt="text")
|
|
|
|
|
|
@when('I run repo full-index on resource "{name}"')
|
|
def step_run_repo_full_index(context: Context, name: str) -> None:
|
|
"""Run the repo index command with --full flag."""
|
|
from cleveragents.cli.commands.repo import index_command
|
|
|
|
_run_with_mock(context, index_command, name, full=True, fmt="text")
|
|
|
|
|
|
@when('I run repo index-json on resource "{name}"')
|
|
def step_run_repo_index_json(context: Context, name: str) -> None:
|
|
"""Run the repo index command with JSON format."""
|
|
from cleveragents.cli.commands.repo import index_command
|
|
|
|
_run_with_mock(context, index_command, name, full=False, fmt="json")
|
|
|
|
|
|
@when('I run repo status on resource "{name}"')
|
|
def step_run_repo_status(context: Context, name: str) -> None:
|
|
"""Run the repo status command."""
|
|
from cleveragents.cli.commands.repo import status_command
|
|
|
|
_run_with_mock(context, status_command, name, fmt="text")
|
|
|
|
|
|
@when('I run repo status-json on resource "{name}"')
|
|
def step_run_repo_status_json(context: Context, name: str) -> None:
|
|
"""Run the repo status command with JSON format."""
|
|
from cleveragents.cli.commands.repo import status_command
|
|
|
|
_run_with_mock(context, status_command, name, fmt="json")
|
|
|
|
|
|
@then("the repo command should succeed")
|
|
def step_repo_should_succeed(context: Context) -> None:
|
|
"""Assert the repo command did not fail."""
|
|
assert not context.repo_cli_failed, (
|
|
f"Expected success but command failed. Output:\n{context.repo_cli_output}"
|
|
)
|
|
|
|
|
|
@then("the repo command should fail")
|
|
def step_repo_should_fail(context: Context) -> None:
|
|
"""Assert the repo command failed."""
|
|
assert context.repo_cli_failed, (
|
|
f"Expected failure but command succeeded. Output:\n{context.repo_cli_output}"
|
|
)
|
|
|
|
|
|
@then('the repo output should contain "{text}"')
|
|
def step_repo_output_contains(context: Context, text: str) -> None:
|
|
"""Assert the repo output contains the given text."""
|
|
assert text in context.repo_cli_output, (
|
|
f"Expected output to contain '{text}'. Got:\n{context.repo_cli_output}"
|
|
)
|
|
|
|
|
|
@then("the repo output should be valid JSON")
|
|
def step_repo_output_valid_json(context: Context) -> None:
|
|
"""Assert the repo output is valid JSON."""
|
|
output = context.repo_cli_output.strip()
|
|
try:
|
|
context.repo_cli_json = json.loads(output)
|
|
except json.JSONDecodeError as exc:
|
|
raise AssertionError(
|
|
f"Output is not valid JSON: {exc}\nOutput:\n{output}"
|
|
) from exc
|
|
|
|
|
|
@then('the repo JSON output should contain key "{key}"')
|
|
def step_repo_json_has_key(context: Context, key: str) -> None:
|
|
"""Assert the parsed JSON output contains a key."""
|
|
assert hasattr(context, "repo_cli_json"), "JSON not parsed; call 'valid JSON' first"
|
|
assert key in context.repo_cli_json, (
|
|
f"Key '{key}' not found in JSON: {context.repo_cli_json}"
|
|
)
|
|
|
|
|
|
@then('the repo JSON output field "{key}" should equal "{value}"')
|
|
def step_repo_json_field_equals(context: Context, key: str, value: str) -> None:
|
|
"""Assert a JSON field equals the expected value."""
|
|
assert hasattr(context, "repo_cli_json"), "JSON not parsed; call 'valid JSON' first"
|
|
actual = str(context.repo_cli_json.get(key, ""))
|
|
assert actual == value, f"Expected {key}='{value}', got '{actual}'"
|
|
|
|
|
|
@given('a registered resource "{name}" with no location')
|
|
def step_register_resource_no_location(context: Context, name: str) -> None:
|
|
"""Register a resource with no location or path property."""
|
|
resource_svc, _indexing_svc = _setup_services(context)
|
|
resource_svc.register_resource(
|
|
type_name="git-checkout",
|
|
name=name,
|
|
location=None,
|
|
properties={},
|
|
)
|
|
|
|
|
|
@given('a registered resource "{name}" with path in properties only')
|
|
def step_register_resource_properties_path(context: Context, name: str) -> None:
|
|
"""Register a resource with path only in properties (no location)."""
|
|
resource_svc, _indexing_svc = _setup_services(context)
|
|
|
|
tmp_dir = tempfile.mkdtemp()
|
|
context._repo_cli_temp_dirs[name] = tmp_dir
|
|
|
|
sample = Path(tmp_dir) / "README.md"
|
|
sample.write_text("# Properties path test\n")
|
|
|
|
resource_svc.register_resource(
|
|
type_name="git-checkout",
|
|
name=name,
|
|
location=None,
|
|
properties={"path": tmp_dir},
|
|
)
|
|
|
|
|
|
@given('a registered resource "{name}" with nonexistent path')
|
|
def step_register_resource_bad_path(context: Context, name: str) -> None:
|
|
"""Register a resource pointing to a nonexistent directory."""
|
|
resource_svc, _indexing_svc = _setup_services(context)
|
|
resource_svc.register_resource(
|
|
type_name="git-checkout",
|
|
name=name,
|
|
location="/tmp/nonexistent-path-for-indexing-test-12345",
|
|
properties={"path": "/tmp/nonexistent-path-for-indexing-test-12345"},
|
|
)
|