feat(cli): repo indexing CLI functional #982
@@ -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,273 @@
|
||||
"""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"},
|
||||
)
|
||||
+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}'
|
||||
Should Contain ${result.stdout} initialized
|
||||
@@ -46,10 +46,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
|
||||
@@ -57,21 +57,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
|
||||
|
||||
@@ -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,
|
||||
@@ -206,6 +207,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
|
||||
|
||||
@@ -627,6 +633,7 @@ def main(args: list[str] | None = None) -> int:
|
||||
"repl", # Interactive REPL
|
||||
"tui", # Textual TUI
|
||||
"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