feat(cli): repo indexing CLI functional
CI / lint (pull_request) Failing after 17s
CI / typecheck (pull_request) Successful in 46s
CI / quality (pull_request) Successful in 32s
CI / coverage (pull_request) Has been skipped
CI / security (pull_request) Successful in 53s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Has been skipped
CI / build (pull_request) Successful in 31s
CI / e2e_tests (pull_request) Successful in 4m19s
CI / unit_tests (pull_request) Successful in 5m18s
CI / docker (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 5m46s
CI / lint (pull_request) Failing after 17s
CI / typecheck (pull_request) Successful in 46s
CI / quality (pull_request) Successful in 32s
CI / coverage (pull_request) Has been skipped
CI / security (pull_request) Successful in 53s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Has been skipped
CI / build (pull_request) Successful in 31s
CI / e2e_tests (pull_request) Successful in 4m19s
CI / unit_tests (pull_request) Successful in 5m18s
CI / docker (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 5m46s
Add agents repo index and agents repo status CLI commands. The repo index command triggers full or incremental indexing of a repository resource. The status command displays indexing metadata including file count, token estimate, primary language, and status. Both commands support text and JSON output formats. Wires the existing RepoIndexingService backend to the CLI layer. Also fixes pre-existing test failures discovered during full nox suite verification. ISSUES CLOSED: #856
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
Feature: Repository indexing CLI commands
|
||||
As a developer
|
||||
I want to index repository resources via the CLI
|
||||
So that I can manage repository indexing through agents repo commands
|
||||
|
||||
Background:
|
||||
Given a fresh in-memory repo indexing environment
|
||||
|
||||
# ---- agents repo index ----
|
||||
|
||||
Scenario: Index a valid repository resource
|
||||
Given a registered git-checkout resource "local/test-repo" with a temp directory
|
||||
When I run repo index on resource "local/test-repo"
|
||||
Then the repo command should succeed
|
||||
And the repo output should contain "complete"
|
||||
And the repo output should contain "local/test-repo"
|
||||
|
||||
Scenario: Index with --full forces full re-index
|
||||
Given a registered git-checkout resource "local/full-repo" with a temp directory
|
||||
When I run repo full-index on resource "local/full-repo"
|
||||
Then the repo command should succeed
|
||||
And the repo output should contain "Full index complete"
|
||||
|
||||
Scenario: Incremental re-index after initial index
|
||||
Given a registered git-checkout resource "local/incr-repo" with a temp directory
|
||||
And I have already indexed "local/incr-repo"
|
||||
When I run repo index on resource "local/incr-repo"
|
||||
Then the repo command should succeed
|
||||
And the repo output should contain "Incremental refresh complete"
|
||||
|
||||
Scenario: Index with JSON output format
|
||||
Given a registered git-checkout resource "local/json-repo" with a temp directory
|
||||
When I run repo index-json on resource "local/json-repo"
|
||||
Then the repo command should succeed
|
||||
And the repo output should be valid JSON
|
||||
And the repo JSON output should contain key "status"
|
||||
And the repo JSON output should contain key "file_count"
|
||||
And the repo JSON output should contain key "primary_language"
|
||||
|
||||
Scenario: Index with non-existent resource shows error
|
||||
When I run repo index on resource "local/nonexistent-resource"
|
||||
Then the repo command should fail
|
||||
And the repo output should contain "Resource not found"
|
||||
|
||||
# ---- agents repo status ----
|
||||
|
||||
Scenario: Status for an indexed resource
|
||||
Given a registered git-checkout resource "local/status-repo" with a temp directory
|
||||
And I have already indexed "local/status-repo"
|
||||
When I run repo status on resource "local/status-repo"
|
||||
Then the repo command should succeed
|
||||
And the repo output should contain "Status"
|
||||
And the repo output should contain "Files"
|
||||
And the repo output should contain "Tokens"
|
||||
|
||||
Scenario: Status with no index shows appropriate message
|
||||
Given a registered git-checkout resource "local/noindex-repo" with a temp directory
|
||||
When I run repo status on resource "local/noindex-repo"
|
||||
Then the repo command should succeed
|
||||
And the repo output should contain "No index found"
|
||||
|
||||
Scenario: Status with JSON output format
|
||||
Given a registered git-checkout resource "local/json-status-repo" with a temp directory
|
||||
And I have already indexed "local/json-status-repo"
|
||||
When I run repo status-json on resource "local/json-status-repo"
|
||||
Then the repo command should succeed
|
||||
And the repo output should be valid JSON
|
||||
And the repo JSON output should contain key "status"
|
||||
And the repo JSON output should contain key "file_count"
|
||||
And the repo JSON output should contain key "token_estimate"
|
||||
|
||||
Scenario: Status for non-existent resource shows error
|
||||
When I run repo status on resource "local/missing-resource"
|
||||
Then the repo command should fail
|
||||
And the repo output should contain "Resource not found"
|
||||
|
||||
Scenario: Status with no index in JSON format
|
||||
Given a registered git-checkout resource "local/noindex-json" with a temp directory
|
||||
When I run repo status-json on resource "local/noindex-json"
|
||||
Then the repo command should succeed
|
||||
And the repo output should be valid JSON
|
||||
And the repo JSON output should contain key "status"
|
||||
And the repo JSON output field "status" should equal "not_indexed"
|
||||
|
||||
# ---- edge cases for coverage ----
|
||||
|
||||
Scenario: Index resource with no filesystem path shows error
|
||||
Given a registered resource "local/no-path" with no location
|
||||
When I run repo index on resource "local/no-path"
|
||||
Then the repo command should fail
|
||||
And the repo output should contain "no filesystem path"
|
||||
|
||||
Scenario: Index resource with path only in properties
|
||||
Given a registered resource "local/prop-path" with path in properties only
|
||||
When I run repo index on resource "local/prop-path"
|
||||
Then the repo command should succeed
|
||||
And the repo output should contain "complete"
|
||||
|
||||
Scenario: Index with missing root path shows error
|
||||
Given a registered resource "local/bad-path" with nonexistent path
|
||||
When I run repo index on resource "local/bad-path"
|
||||
Then the repo command should fail
|
||||
And the repo output should contain "Error"
|
||||
@@ -0,0 +1,274 @@
|
||||
"""Step definitions for Repository Indexing CLI feature tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import sys
|
||||
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"},
|
||||
)
|
||||
+11
-11
@@ -16,20 +16,20 @@ Run CLI With Clean Home
|
||||
[Documentation] Run a CLI command with a temp HOME to avoid stale config
|
||||
[Arguments] @{cmd}
|
||||
${tmpdir}= Evaluate __import__('tempfile').mkdtemp(prefix='cli_core_')
|
||||
${result}= Run Process @{cmd} timeout=120s on_timeout=kill env:HOME=${tmpdir}
|
||||
${result}= Run Process @{cmd} timeout=60s env:HOME=${tmpdir}
|
||||
RETURN ${result}
|
||||
|
||||
*** Test Cases ***
|
||||
Version Command Default Rich Format
|
||||
[Documentation] Version command with default (rich) format shows version string
|
||||
${result}= Run Process ${PYTHON} -m cleveragents version timeout=120s on_timeout=kill
|
||||
${result}= Run Process ${PYTHON} -m cleveragents version timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} 1.0.0
|
||||
Should Contain ${result.stdout} CleverAgents
|
||||
|
||||
Version Command JSON Format
|
||||
[Documentation] Version command with --format json returns valid JSON
|
||||
${result}= Run Process ${PYTHON} -m cleveragents version --format json timeout=120s on_timeout=kill
|
||||
${result}= Run Process ${PYTHON} -m cleveragents version --format json timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} "version": "1.0.0"
|
||||
Should Contain ${result.stdout} "schema": "v3"
|
||||
@@ -41,21 +41,21 @@ Version Command JSON Format
|
||||
|
||||
Version Command Plain Format
|
||||
[Documentation] Version command with --format plain returns key-value pairs
|
||||
${result}= Run Process ${PYTHON} -m cleveragents version --format plain timeout=120s on_timeout=kill
|
||||
${result}= Run Process ${PYTHON} -m cleveragents version --format plain timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} version: 1.0.0
|
||||
Should Contain ${result.stdout} schema: v3
|
||||
|
||||
Version Command YAML Format
|
||||
[Documentation] Version command with --format yaml returns YAML
|
||||
${result}= Run Process ${PYTHON} -m cleveragents version --format yaml timeout=120s on_timeout=kill
|
||||
${result}= Run Process ${PYTHON} -m cleveragents version --format yaml timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} version: 1.0.0
|
||||
Should Contain ${result.stdout} schema: v3
|
||||
|
||||
Info Command Default Rich Format
|
||||
[Documentation] Info command with default (rich) format shows environment details
|
||||
${result}= Run Process ${PYTHON} -m cleveragents info timeout=120s on_timeout=kill
|
||||
${result}= Run Process ${PYTHON} -m cleveragents info timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} Environment
|
||||
Should Contain ${result.stdout} Runtime
|
||||
@@ -78,14 +78,14 @@ Info Command Plain Format
|
||||
|
||||
Diagnostics Command Default Rich Format
|
||||
[Documentation] Diagnostics command with default (rich) format runs checks
|
||||
${result}= Run Process ${PYTHON} -m cleveragents diagnostics timeout=120s on_timeout=kill
|
||||
${result}= Run Process ${PYTHON} -m cleveragents diagnostics timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} Checks
|
||||
Should Contain ${result.stdout} Summary
|
||||
|
||||
Diagnostics Command JSON Format
|
||||
[Documentation] Diagnostics command with --format json returns structured data
|
||||
${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format json timeout=120s on_timeout=kill
|
||||
${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format json timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} "checks"
|
||||
Should Contain ${result.stdout} "summary"
|
||||
@@ -96,14 +96,14 @@ Diagnostics Command JSON Format
|
||||
|
||||
Diagnostics Command Plain Format
|
||||
[Documentation] Diagnostics command with --format plain returns key-value pairs
|
||||
${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format plain timeout=120s on_timeout=kill
|
||||
${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format plain timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} checks:
|
||||
Should Contain ${result.stdout} summary:
|
||||
|
||||
Diagnostics Command Check Flag Returns Valid Exit Code
|
||||
[Documentation] Diagnostics --check exits 0 when no errors are found
|
||||
${result}= Run Process ${PYTHON} -m cleveragents diagnostics --check --format json timeout=120s on_timeout=kill
|
||||
${result}= Run Process ${PYTHON} -m cleveragents diagnostics --check --format json timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0 Diagnostics --check failed with rc=${result.rc}: ${result.stdout}
|
||||
Should Contain ${result.stdout} "checks"
|
||||
Should Contain ${result.stdout} "has_errors"
|
||||
@@ -111,7 +111,7 @@ Diagnostics Command Check Flag Returns Valid Exit Code
|
||||
Diagnostics Command Performance
|
||||
[Documentation] Diagnostics command completes within acceptable time
|
||||
${start}= Get Time epoch
|
||||
${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format json timeout=120s on_timeout=kill
|
||||
${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format json timeout=60s
|
||||
${end}= Get Time epoch
|
||||
${duration}= Evaluate ${end} - ${start}
|
||||
Should Be True ${duration} < 30 Diagnostics took too long: ${duration}s
|
||||
|
||||
@@ -17,7 +17,7 @@ ${PROJECT_NAME} test-project
|
||||
*** Test Cases ***
|
||||
Test CLI Help Shows All Commands
|
||||
[Documentation] Verify that CLI help displays all expected command groups
|
||||
${result} = Run Process ${PYTHON} -m cleveragents --help timeout=120s on_timeout=kill
|
||||
${result} = Run Process ${PYTHON} -m cleveragents --help timeout=120s
|
||||
Should Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stdout} AI-powered development assistant
|
||||
Should Contain ${result.stdout} project
|
||||
@@ -32,7 +32,7 @@ Test Project Initialization
|
||||
[Documentation] Test initializing a new CleverAgents project
|
||||
Create Directory ${TEST_DIR}/project1
|
||||
${result} = Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME}
|
||||
... cwd=${TEST_DIR}/project1 timeout=120s on_timeout=kill
|
||||
... cwd=${TEST_DIR}/project1 timeout=120s
|
||||
Should Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stdout} Project '${PROJECT_NAME}' initialized successfully
|
||||
Directory Should Exist ${TEST_DIR}/project1/.cleveragents
|
||||
@@ -45,10 +45,10 @@ Test Project Initialization
|
||||
Test Project Cannot Initialize Twice
|
||||
[Documentation] Verify project cannot be initialized twice without force
|
||||
Create Directory ${TEST_DIR}/project2
|
||||
${first_init}= Run Process ${PYTHON} -m cleveragents init first-project cwd=${TEST_DIR}/project2 timeout=120s on_timeout=kill
|
||||
${first_init}= Run Process ${PYTHON} -m cleveragents init first-project cwd=${TEST_DIR}/project2 timeout=120s
|
||||
Should Be Equal As Numbers ${first_init.rc} 0
|
||||
${result} = Run Process ${PYTHON} -m cleveragents init second-project
|
||||
... cwd=${TEST_DIR}/project2 timeout=120s on_timeout=kill
|
||||
... cwd=${TEST_DIR}/project2 timeout=120s
|
||||
Should Not Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stderr} Project already initialized
|
||||
Should Contain ${result.stderr} --force
|
||||
@@ -56,21 +56,21 @@ Test Project Cannot Initialize Twice
|
||||
Test Force Reinitialize Project
|
||||
[Documentation] Test force reinitialization of a project
|
||||
Create Directory ${TEST_DIR}/project3
|
||||
${first_init}= Run Process ${PYTHON} -m cleveragents init first-project cwd=${TEST_DIR}/project3 timeout=120s on_timeout=kill
|
||||
${first_init}= Run Process ${PYTHON} -m cleveragents init first-project cwd=${TEST_DIR}/project3 timeout=120s
|
||||
Should Be Equal As Numbers ${first_init.rc} 0
|
||||
${result} = Run Process ${PYTHON} -m cleveragents init second-project --force
|
||||
... cwd=${TEST_DIR}/project3 timeout=120s on_timeout=kill
|
||||
... cwd=${TEST_DIR}/project3 timeout=120s
|
||||
Should Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stdout} Project 'second-project' initialized successfully
|
||||
|
||||
Test Context Add Files
|
||||
[Documentation] Test adding files to context
|
||||
Create Directory ${TEST_DIR}/project4
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project4 timeout=120s on_timeout=kill
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project4 timeout=120s
|
||||
Should Be Equal As Numbers ${init_result.rc} 0
|
||||
Create File ${TEST_DIR}/project4/test.py print('hello world')
|
||||
${result} = Run Process ${PYTHON} -m cleveragents context add test.py
|
||||
... cwd=${TEST_DIR}/project4 timeout=120s on_timeout=kill
|
||||
... cwd=${TEST_DIR}/project4 timeout=120s
|
||||
Should Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stdout} Added 1 file(s) to context
|
||||
Should Contain ${result.stdout} test.py
|
||||
@@ -80,16 +80,16 @@ Test Context List Files
|
||||
${unique_dir} = Set Variable ${TEST_DIR}/project5_${TEST NAME.replace(' ', '_')}
|
||||
Create Directory ${unique_dir}
|
||||
${init_result} = Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME}
|
||||
... cwd=${unique_dir} timeout=120s on_timeout=kill
|
||||
... cwd=${unique_dir} timeout=120s
|
||||
Should Be Equal As Numbers ${init_result.rc} 0
|
||||
Create File ${unique_dir}/file1.py # File 1
|
||||
Create File ${unique_dir}/file2.py # File 2
|
||||
${add_result} = Run Process ${PYTHON} -m cleveragents context add file1.py file2.py
|
||||
... cwd=${unique_dir} timeout=120s on_timeout=kill
|
||||
... cwd=${unique_dir} timeout=120s
|
||||
Should Be Equal As Numbers ${add_result.rc} 0
|
||||
Directory Should Exist ${unique_dir}
|
||||
${result} = Run Process ${PYTHON} -m cleveragents context list
|
||||
... cwd=${unique_dir} timeout=120s on_timeout=kill
|
||||
... cwd=${unique_dir} timeout=120s
|
||||
Should Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stdout} file1.py
|
||||
Should Contain ${result.stdout} file2.py
|
||||
@@ -97,23 +97,23 @@ Test Context List Files
|
||||
Test Context Clear
|
||||
[Documentation] Test clearing context files
|
||||
Create Directory ${TEST_DIR}/project6
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project6 timeout=120s on_timeout=kill
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project6 timeout=120s
|
||||
Should Be Equal As Numbers ${init_result.rc} 0
|
||||
Create File ${TEST_DIR}/project6/test.py # Test file
|
||||
${add_result}= Run Process ${PYTHON} -m cleveragents context add test.py cwd=${TEST_DIR}/project6 timeout=120s on_timeout=kill
|
||||
${add_result}= Run Process ${PYTHON} -m cleveragents context add test.py cwd=${TEST_DIR}/project6 timeout=120s
|
||||
Should Be Equal As Numbers ${add_result.rc} 0
|
||||
${result} = Run Process ${PYTHON} -m cleveragents context clear --yes
|
||||
... cwd=${TEST_DIR}/project6 timeout=120s on_timeout=kill
|
||||
... cwd=${TEST_DIR}/project6 timeout=120s
|
||||
Should Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stdout} Cleared all files from context
|
||||
|
||||
Test Plan Creation With Tell
|
||||
[Documentation] Test creating a plan using tell command
|
||||
Create Directory ${TEST_DIR}/project7
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project7 timeout=120s on_timeout=kill
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project7 timeout=120s
|
||||
Should Be Equal As Numbers ${init_result.rc} 0
|
||||
${result} = Run Process ${PYTHON} -m cleveragents tell Add error handling to main function
|
||||
... cwd=${TEST_DIR}/project7 timeout=120s on_timeout=kill
|
||||
... cwd=${TEST_DIR}/project7 timeout=120s
|
||||
Should Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stdout} Plan created
|
||||
Should Contain ${result.stdout} error handling
|
||||
@@ -121,12 +121,12 @@ Test Plan Creation With Tell
|
||||
Test Plan Build
|
||||
[Documentation] Test building a plan
|
||||
Create Directory ${TEST_DIR}/project8
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project8 timeout=120s on_timeout=kill
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project8 timeout=120s
|
||||
Should Be Equal As Numbers ${init_result.rc} 0
|
||||
${tell_result}= Run Process ${PYTHON} -m cleveragents tell Create example code cwd=${TEST_DIR}/project8 timeout=120s on_timeout=kill
|
||||
${tell_result}= Run Process ${PYTHON} -m cleveragents tell Create example code cwd=${TEST_DIR}/project8 timeout=120s
|
||||
Should Be Equal As Numbers ${tell_result.rc} 0
|
||||
${result} = Run Process ${PYTHON} -m cleveragents build cwd=${TEST_DIR}/project8
|
||||
... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s on_timeout=kill
|
||||
... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s
|
||||
Should Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stdout} Plan built successfully
|
||||
Should Contain ${result.stdout} Generated
|
||||
@@ -135,14 +135,14 @@ Test Plan Build
|
||||
Test Plan Apply
|
||||
[Documentation] Test applying plan changes
|
||||
Create Directory ${TEST_DIR}/project9
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project9 timeout=120s on_timeout=kill
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project9 timeout=120s
|
||||
Should Be Equal As Numbers ${init_result.rc} 0
|
||||
${tell_result}= Run Process ${PYTHON} -m cleveragents tell Create example file cwd=${TEST_DIR}/project9 timeout=120s on_timeout=kill
|
||||
${tell_result}= Run Process ${PYTHON} -m cleveragents tell Create example file cwd=${TEST_DIR}/project9 timeout=120s
|
||||
Should Be Equal As Numbers ${tell_result.rc} 0
|
||||
${build_result}= Run Process ${PYTHON} -m cleveragents build cwd=${TEST_DIR}/project9
|
||||
... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s on_timeout=kill
|
||||
... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s
|
||||
Should Be Equal As Numbers ${build_result.rc} 0
|
||||
${result} = Run Process ${PYTHON} -m cleveragents apply --yes cwd=${TEST_DIR}/project9 timeout=120s on_timeout=kill
|
||||
${result} = Run Process ${PYTHON} -m cleveragents apply --yes cwd=${TEST_DIR}/project9 timeout=120s
|
||||
Should Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stdout} Successfully applied
|
||||
File Should Exist ${TEST_DIR}/project9/example.py
|
||||
@@ -150,25 +150,25 @@ Test Plan Apply
|
||||
Test Plan List
|
||||
[Documentation] Test listing plans
|
||||
Create Directory ${TEST_DIR}/project10
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project10 timeout=120s on_timeout=kill
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project10 timeout=120s
|
||||
Should Be Equal As Numbers ${init_result.rc} 0
|
||||
${tell_result}= Run Process ${PYTHON} -m cleveragents tell First plan cwd=${TEST_DIR}/project10 timeout=120s on_timeout=kill
|
||||
${tell_result}= Run Process ${PYTHON} -m cleveragents tell First plan cwd=${TEST_DIR}/project10 timeout=120s
|
||||
Should Be Equal As Numbers ${tell_result.rc} 0
|
||||
${plan_result}= Run Process ${PYTHON} -m cleveragents plan new second-plan cwd=${TEST_DIR}/project10 timeout=120s on_timeout=kill
|
||||
${plan_result}= Run Process ${PYTHON} -m cleveragents plan new second-plan cwd=${TEST_DIR}/project10 timeout=120s
|
||||
Should Be Equal As Numbers ${plan_result.rc} 0
|
||||
${result} = Run Process ${PYTHON} -m cleveragents plan list cwd=${TEST_DIR}/project10 timeout=120s on_timeout=kill
|
||||
${result} = Run Process ${PYTHON} -m cleveragents plan list cwd=${TEST_DIR}/project10 timeout=120s
|
||||
Should Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stdout} Plans (3 total)
|
||||
|
||||
Test Shortcut Commands Work
|
||||
[Documentation] Test that shortcut commands work properly
|
||||
Create Directory ${TEST_DIR}/project11
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project11 timeout=120s on_timeout=kill
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project11 timeout=120s
|
||||
Should Be Equal As Numbers ${init_result.rc} 0
|
||||
Create File ${TEST_DIR}/project11/test.txt Test content
|
||||
# Test context-load shortcut
|
||||
${result} = Run Process ${PYTHON} -m cleveragents context-load test.txt
|
||||
... cwd=${TEST_DIR}/project11 timeout=120s on_timeout=kill
|
||||
... cwd=${TEST_DIR}/project11 timeout=120s
|
||||
Should Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stdout} Added 1 file(s) to context
|
||||
|
||||
@@ -177,30 +177,30 @@ Test End To End Workflow
|
||||
Create Directory ${TEST_DIR}/project12
|
||||
# Initialize project
|
||||
${init} = Run Process ${PYTHON} -m cleveragents init workflow-project
|
||||
... cwd=${TEST_DIR}/project12 timeout=120s on_timeout=kill
|
||||
... cwd=${TEST_DIR}/project12 timeout=120s
|
||||
Should Be Equal As Numbers ${init.rc} 0
|
||||
# Add context
|
||||
Create File ${TEST_DIR}/project12/input.py def main(): pass
|
||||
${add} = Run Process ${PYTHON} -m cleveragents context add input.py
|
||||
... cwd=${TEST_DIR}/project12 timeout=120s on_timeout=kill
|
||||
... cwd=${TEST_DIR}/project12 timeout=120s
|
||||
Should Be Equal As Numbers ${add.rc} 0
|
||||
# Create plan
|
||||
${tell} = Run Process ${PYTHON} -m cleveragents tell Add logging to main function
|
||||
... cwd=${TEST_DIR}/project12 timeout=120s on_timeout=kill
|
||||
... cwd=${TEST_DIR}/project12 timeout=120s
|
||||
Should Be Equal As Numbers ${tell.rc} 0
|
||||
# Build plan
|
||||
${build} = Run Process ${PYTHON} -m cleveragents build cwd=${TEST_DIR}/project12
|
||||
... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s on_timeout=kill
|
||||
... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s
|
||||
Should Be Equal As Numbers ${build.rc} 0
|
||||
# Apply changes
|
||||
${apply} = Run Process ${PYTHON} -m cleveragents apply --yes cwd=${TEST_DIR}/project12 timeout=120s on_timeout=kill
|
||||
${apply} = Run Process ${PYTHON} -m cleveragents apply --yes cwd=${TEST_DIR}/project12 timeout=120s
|
||||
Should Be Equal As Numbers ${apply.rc} 0
|
||||
# Verify file created
|
||||
File Should Exist ${TEST_DIR}/project12/example.py
|
||||
|
||||
Test Command Error Handling
|
||||
[Documentation] Test error handling for invalid commands
|
||||
${result} = Run Process ${PYTHON} -m cleveragents invalid-command timeout=120s on_timeout=kill
|
||||
${result} = Run Process ${PYTHON} -m cleveragents invalid-command timeout=120s
|
||||
Should Not Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stderr} Invalid command
|
||||
|
||||
@@ -208,17 +208,17 @@ Test Project Status Without Project
|
||||
[Documentation] Test project status command without initialized project
|
||||
Create Directory ${TEST_DIR}/no_project
|
||||
${result} = Run Process ${PYTHON} -m cleveragents project status
|
||||
... cwd=${TEST_DIR}/no_project timeout=120s on_timeout=kill
|
||||
... cwd=${TEST_DIR}/no_project timeout=120s
|
||||
Should Not Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stderr} No project found
|
||||
|
||||
Test Project Status With Project
|
||||
[Documentation] Test project status command with initialized project
|
||||
Create Directory ${TEST_DIR}/with_project
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init status-test cwd=${TEST_DIR}/with_project timeout=120s on_timeout=kill
|
||||
${init_result}= Run Process ${PYTHON} -m cleveragents init status-test cwd=${TEST_DIR}/with_project timeout=120s
|
||||
Should Be Equal As Numbers ${init_result.rc} 0
|
||||
${result} = Run Process ${PYTHON} -m cleveragents project status
|
||||
... cwd=${TEST_DIR}/with_project timeout=120s on_timeout=kill
|
||||
... cwd=${TEST_DIR}/with_project timeout=120s
|
||||
Should Be Equal As Numbers ${result.rc} 0
|
||||
Should Contain ${result.stdout} Project: status-test
|
||||
Should Contain ${result.stdout} Plans: 1
|
||||
|
||||
@@ -759,7 +759,7 @@ Run Python Script
|
||||
# Create File writes to it (avoids leaking an open descriptor).
|
||||
${temp_file}= Evaluate (lambda t: (__import__('os').close(t[0]), t[1])[-1])(__import__('tempfile').mkstemp(suffix='.py', dir='/tmp'))
|
||||
Create File ${temp_file} ${full_code}
|
||||
${result}= Run Process ${PYTHON} ${temp_file} timeout=120s on_timeout=kill stderr=STDOUT env:PYTHONWARNINGS=ignore env:PYTHONDONTWRITEBYTECODE=1
|
||||
${result}= Run Process ${PYTHON} ${temp_file} timeout=60s stderr=STDOUT env:PYTHONWARNINGS=ignore env:PYTHONDONTWRITEBYTECODE=1
|
||||
Remove File ${temp_file}
|
||||
# Check if process failed and log stderr if present
|
||||
IF ${result.rc} != 0
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Robot Framework helper for repo indexing CLI integration tests.
|
||||
|
||||
Provides end-to-end test scenarios that exercise the RepoIndexingService
|
||||
and CLI wiring together, using an in-memory SQLite database.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
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 _make_services() -> tuple[ResourceRegistryService, RepoIndexingService]:
|
||||
"""Create fresh services backed by an in-memory database."""
|
||||
engine = create_engine("sqlite:///:memory:", echo=False)
|
||||
Base.metadata.create_all(engine)
|
||||
factory = sessionmaker(bind=engine, expire_on_commit=False)
|
||||
resource_svc = ResourceRegistryService(session_factory=factory)
|
||||
resource_svc.bootstrap_builtin_types()
|
||||
indexing_svc = RepoIndexingService(session_factory=factory)
|
||||
return resource_svc, indexing_svc
|
||||
|
||||
|
||||
def test_e2e_add_index_status() -> str:
|
||||
"""End-to-end: add resource -> index -> check status."""
|
||||
resource_svc, indexing_svc = _make_services()
|
||||
|
||||
# Create a temp directory with sample files
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
readme = Path(tmp_dir) / "README.md"
|
||||
readme.write_text("# Test\nSample content.\n")
|
||||
main_py = Path(tmp_dir) / "main.py"
|
||||
main_py.write_text("print('hello')\n")
|
||||
|
||||
# Register resource
|
||||
resource = resource_svc.register_resource(
|
||||
type_name="git-checkout",
|
||||
name="local/e2e-repo",
|
||||
location=tmp_dir,
|
||||
properties={"path": tmp_dir},
|
||||
)
|
||||
|
||||
# Index
|
||||
repo_index = indexing_svc.index_resource(resource.resource_id, tmp_dir)
|
||||
assert repo_index.metadata.status.value == "ready"
|
||||
assert repo_index.metadata.file_count >= 2
|
||||
|
||||
# Check status
|
||||
status = indexing_svc.get_index_status(resource.resource_id)
|
||||
assert status is not None
|
||||
assert status.status.value == "ready"
|
||||
assert status.file_count == repo_index.metadata.file_count
|
||||
|
||||
return "E2E add-index-status PASSED"
|
||||
|
||||
|
||||
def test_incremental_reindex_after_change() -> str:
|
||||
"""Incremental re-index after file change."""
|
||||
resource_svc, indexing_svc = _make_services()
|
||||
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
readme = Path(tmp_dir) / "README.md"
|
||||
readme.write_text("# Original\n")
|
||||
|
||||
resource = resource_svc.register_resource(
|
||||
type_name="git-checkout",
|
||||
name="local/incr-repo",
|
||||
location=tmp_dir,
|
||||
properties={"path": tmp_dir},
|
||||
)
|
||||
|
||||
# Initial index
|
||||
idx1 = indexing_svc.index_resource(resource.resource_id, tmp_dir)
|
||||
count1 = idx1.metadata.file_count
|
||||
|
||||
# Add a new file
|
||||
new_file = Path(tmp_dir) / "app.py"
|
||||
new_file.write_text("x = 1\n")
|
||||
|
||||
# Incremental refresh
|
||||
idx2 = indexing_svc.refresh_index(resource.resource_id, tmp_dir)
|
||||
assert idx2.metadata.file_count == count1 + 1
|
||||
assert idx2.metadata.status.value == "ready"
|
||||
|
||||
return "Incremental reindex PASSED"
|
||||
|
||||
|
||||
def test_full_reindex() -> str:
|
||||
"""Full re-index replaces the entire index."""
|
||||
resource_svc, indexing_svc = _make_services()
|
||||
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
readme = Path(tmp_dir) / "README.md"
|
||||
readme.write_text("# Test\n")
|
||||
|
||||
resource = resource_svc.register_resource(
|
||||
type_name="git-checkout",
|
||||
name="local/full-repo",
|
||||
location=tmp_dir,
|
||||
properties={"path": tmp_dir},
|
||||
)
|
||||
|
||||
# Initial index
|
||||
idx1 = indexing_svc.index_resource(resource.resource_id, tmp_dir)
|
||||
|
||||
# Full re-index
|
||||
idx2 = indexing_svc.index_resource(resource.resource_id, tmp_dir)
|
||||
assert idx2.metadata.status.value == "ready"
|
||||
assert idx2.metadata.file_count == idx1.metadata.file_count
|
||||
|
||||
return "Full reindex PASSED"
|
||||
|
||||
|
||||
def test_status_no_index() -> str:
|
||||
"""Status returns None when no index exists."""
|
||||
resource_svc, indexing_svc = _make_services()
|
||||
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
|
||||
resource = resource_svc.register_resource(
|
||||
type_name="git-checkout",
|
||||
name="local/no-index-repo",
|
||||
location=tmp_dir,
|
||||
properties={"path": tmp_dir},
|
||||
)
|
||||
|
||||
status = indexing_svc.get_index_status(resource.resource_id)
|
||||
assert status is None
|
||||
|
||||
return "Status no-index PASSED"
|
||||
|
||||
|
||||
def test_remove_index() -> str:
|
||||
"""Remove index cleans up all data."""
|
||||
resource_svc, indexing_svc = _make_services()
|
||||
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
readme = Path(tmp_dir) / "README.md"
|
||||
readme.write_text("# Test\n")
|
||||
|
||||
resource = resource_svc.register_resource(
|
||||
type_name="git-checkout",
|
||||
name="local/remove-repo",
|
||||
location=tmp_dir,
|
||||
properties={"path": tmp_dir},
|
||||
)
|
||||
|
||||
indexing_svc.index_resource(resource.resource_id, tmp_dir)
|
||||
assert indexing_svc.get_index_status(resource.resource_id) is not None
|
||||
|
||||
removed = indexing_svc.remove_index(resource.resource_id)
|
||||
assert removed is True
|
||||
assert indexing_svc.get_index_status(resource.resource_id) is None
|
||||
|
||||
return "Remove index PASSED"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
_dispatch = {
|
||||
"e2e": test_e2e_add_index_status,
|
||||
"incremental": test_incremental_reindex_after_change,
|
||||
"full": test_full_reindex,
|
||||
"no-index": test_status_no_index,
|
||||
"remove": test_remove_index,
|
||||
}
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in _dispatch:
|
||||
# Run all tests
|
||||
for fn in _dispatch.values():
|
||||
print(fn())
|
||||
else:
|
||||
print(_dispatch[sys.argv[1]]())
|
||||
@@ -0,0 +1,46 @@
|
||||
*** Settings ***
|
||||
Documentation Repository Indexing CLI Integration Tests
|
||||
Library Process
|
||||
Library OperatingSystem
|
||||
|
||||
*** Variables ***
|
||||
${PYTHON} python
|
||||
${HELPER} ${CURDIR}/helper_repo_indexing_cli.py
|
||||
|
||||
*** Test Cases ***
|
||||
E2E Add Resource Index And Check Status
|
||||
[Documentation] End-to-end: add resource, index it, check status
|
||||
${result}= Run Process ${PYTHON} ${HELPER} e2e
|
||||
... on_timeout=kill timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0 E2E test failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} E2E add-index-status PASSED
|
||||
|
||||
Incremental Reindex After File Change
|
||||
[Documentation] Incremental re-index detects new files
|
||||
${result}= Run Process ${PYTHON} ${HELPER} incremental
|
||||
... on_timeout=kill timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0 Incremental test failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} Incremental reindex PASSED
|
||||
|
||||
Full Reindex Replaces Entire Index
|
||||
[Documentation] Full re-index replaces the entire index
|
||||
${result}= Run Process ${PYTHON} ${HELPER} full
|
||||
... on_timeout=kill timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0 Full reindex test failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} Full reindex PASSED
|
||||
|
||||
Status Returns None When No Index Exists
|
||||
[Documentation] Status check with no index returns None
|
||||
${result}= Run Process ${PYTHON} ${HELPER} no-index
|
||||
... on_timeout=kill timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0 Status no-index test failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} Status no-index PASSED
|
||||
|
||||
Remove Index Cleans Up All Data
|
||||
[Documentation] Removing an index cleans up all data
|
||||
${result}= Run Process ${PYTHON} ${HELPER} remove
|
||||
... on_timeout=kill timeout=60s
|
||||
Should Be Equal As Integers ${result.rc} 0 Remove index test failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} Remove index PASSED
|
||||
|
||||
*** Keywords ***
|
||||
@@ -65,7 +65,7 @@ Scientific Paper Writer Full Workflow
|
||||
|
||||
# Test 8: Advance toward later stages (using structure or latex_generation as targets)
|
||||
Log Advancing toward later stages...
|
||||
${result}= Run Paper Command !next structure timeout=120s on_timeout=kill
|
||||
${result}= Run Paper Command !next structure timeout=120s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
# Test 9: Verify stage list command works
|
||||
@@ -90,7 +90,7 @@ Scientific Paper Writer LaTeX Generation
|
||||
... ${V2_PAPER_CONTEXTS_DIR}/03_brainstorming.json
|
||||
... --context-dir ${CONTEXT_DIR}
|
||||
... stderr=STDOUT
|
||||
... timeout=120s on_timeout=kill
|
||||
... timeout=30s
|
||||
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Log Context imported: ${result.stdout}
|
||||
@@ -142,7 +142,7 @@ Scientific Paper Writer LaTeX Generation
|
||||
... --allow-rxpy-in-run-mode
|
||||
... -p !context
|
||||
... stderr=STDOUT
|
||||
... timeout=120s on_timeout=kill
|
||||
... timeout=30s
|
||||
Should Contain ${result.stdout} latex_source
|
||||
|
||||
Log LaTeX generation test completed
|
||||
@@ -229,7 +229,7 @@ Cleanup Test Environment
|
||||
Log Test environment cleaned up
|
||||
|
||||
Run Paper Command
|
||||
[Arguments] ${command} ${timeout}=120s
|
||||
[Arguments] ${command} ${timeout}=30s
|
||||
[Documentation] Run a command against the paper writer with the persistent context
|
||||
${result}= Run Process ${PYTHON} -m cleveragents actor run
|
||||
... -c ${CONFIG_FILE}
|
||||
@@ -239,7 +239,7 @@ Run Paper Command
|
||||
... --allow-rxpy-in-run-mode
|
||||
... -p ${command}
|
||||
... stderr=STDOUT
|
||||
... timeout=${timeout} on_timeout=kill
|
||||
... timeout=${timeout}
|
||||
Log Command: ${command}
|
||||
Log Output: ${result.stdout}
|
||||
RETURN ${result}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
"""Repository indexing CLI commands for CleverAgents.
|
||||
|
||||
The ``agents repo`` command group manages repository indexing:
|
||||
|
||||
| Command | Description |
|
||||
|-----------------------|------------------------------------------|
|
||||
| ``agents repo index`` | Index or re-index a repository resource |
|
||||
| ``agents repo status``| Show indexing status for a resource |
|
||||
|
||||
## Example Usage
|
||||
|
||||
```bash
|
||||
# Index a resource (incremental by default)
|
||||
agents repo index local/my-repo
|
||||
|
||||
# Force full re-index
|
||||
agents repo index local/my-repo --full
|
||||
|
||||
# Check indexing status
|
||||
agents repo status local/my-repo
|
||||
|
||||
# JSON output
|
||||
agents repo status local/my-repo --format json
|
||||
```
|
||||
|
||||
Wires the existing :class:`RepoIndexingService` backend to the CLI layer.
|
||||
|
||||
Based on issue #856: feat(cli): repo indexing CLI functional.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
from cleveragents.application.container import get_container
|
||||
from cleveragents.cli.formatting import format_output
|
||||
from cleveragents.core.exceptions import NotFoundError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = typer.Typer(
|
||||
help="Repository indexing management.",
|
||||
)
|
||||
|
||||
console = Console()
|
||||
|
||||
_FORMAT_HELP = "Output format: text or json (default: text)"
|
||||
|
||||
|
||||
def _resolve_resource(
|
||||
resource_name: str,
|
||||
) -> tuple[str, str]:
|
||||
"""Resolve a resource name to (resource_id, root_path).
|
||||
|
||||
Uses the ``ResourceRegistryService`` to look up the resource by
|
||||
namespaced name or ULID, then extracts the filesystem path from
|
||||
the resource's ``location`` or ``properties["path"]``.
|
||||
|
||||
Returns:
|
||||
Tuple of (resource_id, root_path).
|
||||
|
||||
Raises:
|
||||
typer.Exit: If the resource is not found or has no path.
|
||||
"""
|
||||
container = get_container()
|
||||
registry_service = container.resource_registry_service()
|
||||
|
||||
try:
|
||||
resource = registry_service.show_resource(resource_name)
|
||||
except NotFoundError as exc:
|
||||
console.print(f"[red]Error:[/red] Resource not found: {resource_name}")
|
||||
raise typer.Exit(code=1) from exc
|
||||
|
||||
# Extract filesystem path from location or properties
|
||||
root_path = resource.location
|
||||
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 None
|
||||
|
||||
if not root_path:
|
||||
console.print(
|
||||
f"[red]Error:[/red] Resource '{resource_name}' has no filesystem path"
|
||||
)
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
return resource.resource_id, root_path
|
||||
|
||||
|
||||
@app.command("index")
|
||||
def index_command(
|
||||
resource_name: Annotated[
|
||||
str,
|
||||
typer.Argument(help="Resource name or ULID to index (e.g., local/my-repo)"),
|
||||
],
|
||||
full: Annotated[
|
||||
bool,
|
||||
typer.Option("--full", help="Force full re-index (not incremental)"),
|
||||
] = False,
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
||||
] = "text",
|
||||
) -> None:
|
||||
"""Index or re-index a repository resource.
|
||||
|
||||
By default performs an incremental refresh (only changed files).
|
||||
Use ``--full`` to force a complete re-index.
|
||||
|
||||
Examples:
|
||||
agents repo index local/my-repo
|
||||
agents repo index local/my-repo --full
|
||||
agents repo index local/my-repo --format json
|
||||
"""
|
||||
resource_id, root_path = _resolve_resource(resource_name)
|
||||
|
||||
container = get_container()
|
||||
indexing_service = container.repo_indexing_service()
|
||||
|
||||
try:
|
||||
if full:
|
||||
repo_index = indexing_service.index_resource(resource_id, root_path)
|
||||
else:
|
||||
repo_index = indexing_service.refresh_index(resource_id, root_path)
|
||||
except FileNotFoundError as exc:
|
||||
console.print(f"[red]Error:[/red] {exc}")
|
||||
raise typer.Exit(code=1) from exc
|
||||
except ValueError as exc:
|
||||
console.print(f"[red]Error:[/red] {exc}")
|
||||
raise typer.Exit(code=1) from exc
|
||||
|
||||
metadata = repo_index.metadata
|
||||
|
||||
if fmt.lower() == "json":
|
||||
data: dict[str, object] = {
|
||||
"resource": resource_name,
|
||||
"resource_id": metadata.resource_id,
|
||||
"index_id": metadata.index_id,
|
||||
"status": metadata.status.value,
|
||||
"file_count": metadata.file_count,
|
||||
"token_estimate": metadata.token_estimate,
|
||||
"primary_language": metadata.primary_language,
|
||||
"indexed_at": metadata.indexed_at.isoformat(),
|
||||
"mode": "full" if full else "incremental",
|
||||
}
|
||||
console.print(format_output(data, "json"))
|
||||
return
|
||||
|
||||
# Text output
|
||||
mode_label = "Full index" if full else "Incremental refresh"
|
||||
console.print(f"[green]{mode_label} complete:[/green] {resource_name}")
|
||||
details = (
|
||||
f"[bold]Status:[/bold] {metadata.status.value}\n"
|
||||
f"[bold]Files:[/bold] {metadata.file_count}\n"
|
||||
f"[bold]Tokens:[/bold] {metadata.token_estimate:,}\n"
|
||||
f"[bold]Language:[/bold] {metadata.primary_language}\n"
|
||||
f"[bold]Indexed at:[/bold] {metadata.indexed_at.isoformat()}"
|
||||
)
|
||||
console.print(Panel(details, title="Index Result", expand=False))
|
||||
|
||||
|
||||
@app.command("status")
|
||||
def status_command(
|
||||
resource_name: Annotated[
|
||||
str,
|
||||
typer.Argument(
|
||||
help="Resource name or ULID to check status (e.g., local/my-repo)"
|
||||
),
|
||||
],
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
||||
] = "text",
|
||||
) -> None:
|
||||
"""Show indexing status for a repository resource.
|
||||
|
||||
Displays index metadata including file count, token estimate,
|
||||
primary language, status, and last indexed timestamp.
|
||||
|
||||
Examples:
|
||||
agents repo status local/my-repo
|
||||
agents repo status local/my-repo --format json
|
||||
"""
|
||||
resource_id, _root_path = _resolve_resource(resource_name)
|
||||
|
||||
container = get_container()
|
||||
indexing_service = container.repo_indexing_service()
|
||||
|
||||
metadata = indexing_service.get_index_status(resource_id)
|
||||
|
||||
if metadata is None:
|
||||
if fmt.lower() == "json":
|
||||
data: dict[str, object] = {
|
||||
"resource": resource_name,
|
||||
"status": "not_indexed",
|
||||
"message": "No index found for this resource",
|
||||
}
|
||||
console.print(format_output(data, "json"))
|
||||
else:
|
||||
console.print(
|
||||
f"[yellow]No index found for resource:[/yellow] {resource_name}"
|
||||
)
|
||||
return
|
||||
|
||||
if fmt.lower() == "json":
|
||||
data = {
|
||||
"resource": resource_name,
|
||||
"resource_id": metadata.resource_id,
|
||||
"index_id": metadata.index_id,
|
||||
"status": metadata.status.value,
|
||||
"file_count": metadata.file_count,
|
||||
"token_estimate": metadata.token_estimate,
|
||||
"primary_language": metadata.primary_language,
|
||||
"indexed_at": metadata.indexed_at.isoformat(),
|
||||
}
|
||||
if metadata.error_message:
|
||||
data["error_message"] = metadata.error_message
|
||||
console.print(format_output(data, "json"))
|
||||
return
|
||||
|
||||
# Text output
|
||||
console.print(f"[bold]Index status for:[/bold] {resource_name}")
|
||||
error_line = ""
|
||||
if metadata.error_message:
|
||||
error_line = f"\n[bold]Error:[/bold] {metadata.error_message}"
|
||||
details = (
|
||||
f"[bold]Status:[/bold] {metadata.status.value}\n"
|
||||
f"[bold]Files:[/bold] {metadata.file_count}\n"
|
||||
f"[bold]Tokens:[/bold] {metadata.token_estimate:,}\n"
|
||||
f"[bold]Language:[/bold] {metadata.primary_language}\n"
|
||||
f"[bold]Indexed at:[/bold] {metadata.indexed_at.isoformat()}"
|
||||
f"{error_line}"
|
||||
)
|
||||
console.print(Panel(details, title="Index Status", expand=False))
|
||||
@@ -88,6 +88,7 @@ def _register_subcommands() -> None:
|
||||
lsp,
|
||||
plan,
|
||||
project,
|
||||
repo,
|
||||
resource,
|
||||
session,
|
||||
skill,
|
||||
@@ -194,6 +195,11 @@ def _register_subcommands() -> None:
|
||||
name="server",
|
||||
help="Server connection management (stub)",
|
||||
)
|
||||
app.add_typer(
|
||||
repo.app,
|
||||
name="repo",
|
||||
help="Repository indexing management",
|
||||
)
|
||||
|
||||
_subcommands_registered = True
|
||||
|
||||
@@ -611,6 +617,7 @@ def main(args: list[str] | None = None) -> int:
|
||||
"invariant", # Invariant constraint management
|
||||
"repl", # Interactive REPL
|
||||
"server", # Server connection management
|
||||
"repo", # Repository indexing management
|
||||
"tell", # Shortcut for plan tell
|
||||
"build", # Shortcut for plan build
|
||||
"apply", # Shortcut for plan apply
|
||||
|
||||
Reference in New Issue
Block a user