forked from cleveragents/cleveragents-core
1208 lines
46 KiB
Python
1208 lines
46 KiB
Python
"""Step definitions for context unit tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import tempfile
|
|
from contextlib import contextmanager
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from behave import given, then, when
|
|
from typer.testing import CliRunner
|
|
|
|
from cleveragents.cli.commands import context as context_app
|
|
from cleveragents.core.exceptions import CleverAgentsError, FileSystemError
|
|
from cleveragents.domain.models.core import Context, ContextType
|
|
|
|
|
|
def strip_ansi_codes(text):
|
|
"""Remove ANSI escape sequences from text."""
|
|
# This regex matches ANSI escape sequences
|
|
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
return ansi_escape.sub("", text)
|
|
|
|
|
|
@contextmanager
|
|
def patched_container(context, project):
|
|
"""Patch the CleverAgents container to provide mocked services."""
|
|
with patch(
|
|
"cleveragents.application.container.get_container"
|
|
) as mock_get_container:
|
|
container = MagicMock()
|
|
container.context_service.return_value = context.mock_service
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = project
|
|
container.project_service.return_value = project_service
|
|
mock_get_container.return_value = container
|
|
yield container
|
|
|
|
|
|
@given("a mock context service is configured")
|
|
def step_mock_context_service(context):
|
|
"""Set up a mock context service."""
|
|
context.mock_service = MagicMock()
|
|
context.temp_dir = tempfile.mkdtemp()
|
|
|
|
|
|
@given("a mock context service that raises FileSystemError")
|
|
def step_mock_filesystem_error_service(context):
|
|
"""Set up mock that raises FileSystemError."""
|
|
context.mock_service = MagicMock()
|
|
context.mock_error = FileSystemError("Filesystem access denied")
|
|
context.temp_dir = tempfile.mkdtemp()
|
|
|
|
|
|
@given("a mock context service that raises CleverAgentsError")
|
|
def step_mock_cleveragents_error_service(context):
|
|
"""Set up mock that raises CleverAgentsError."""
|
|
context.mock_service = MagicMock()
|
|
context.mock_error = CleverAgentsError("Context operation failed")
|
|
context.temp_dir = tempfile.mkdtemp()
|
|
|
|
|
|
@given("a mock context service with remove capability")
|
|
def step_mock_remove_service(context):
|
|
"""Set up mock for remove operations."""
|
|
context.mock_service = MagicMock()
|
|
# The remove function is called twice (once for each file)
|
|
# Each call returns 1, so total removed is 2
|
|
context.mock_service.remove_from_context.side_effect = [1, 1]
|
|
|
|
|
|
@given("a mock context service returning zero removals")
|
|
def step_mock_zero_removals(context):
|
|
"""Set up mock that removes nothing."""
|
|
context.mock_service = MagicMock()
|
|
# Called twice, each returns 0
|
|
context.mock_service.remove_from_context.side_effect = [0, 0]
|
|
|
|
|
|
@given("a mock context service with already tracked files")
|
|
def step_mock_already_tracked(context):
|
|
"""Set up mock that reports already tracked files."""
|
|
context.mock_service = MagicMock()
|
|
context.temp_dir = tempfile.mkdtemp()
|
|
context.already_paths = [
|
|
Path(context.temp_dir) / f"existing_{i}.txt" for i in range(12)
|
|
]
|
|
context.mock_service.add_to_context.return_value = ([], context.already_paths)
|
|
|
|
|
|
@given("a mock context service with partial removals")
|
|
def step_mock_partial_removals(context):
|
|
"""Set up mock that removes some but not all files."""
|
|
context.mock_service = MagicMock()
|
|
context.mock_service.remove_from_context.side_effect = [1, 0]
|
|
|
|
|
|
@given("a mock context service with files")
|
|
def step_mock_with_files(context):
|
|
"""Set up mock with file list."""
|
|
context.mock_service = MagicMock()
|
|
context.mock_files = [
|
|
{"path": "/src/main.py", "size": "1.2 KB", "added_at": "2024-01-01"},
|
|
{"path": "/src/test.py", "size": "0.8 KB", "added_at": "2024-01-02"},
|
|
]
|
|
context.mock_service.list_context.return_value = context.mock_files
|
|
|
|
|
|
@given("a mock context service with numeric file sizes")
|
|
def step_mock_numeric_sizes(context):
|
|
"""Set up mock with numeric size values."""
|
|
context.mock_service = MagicMock()
|
|
context.mock_files = [
|
|
{"path": "/src/data.bin", "size": 0, "added_at": "2024-02-01"},
|
|
{"path": "/src/large.bin", "size": 2048, "added_at": "2024-02-02"},
|
|
]
|
|
context.mock_service.list_context.return_value = context.mock_files
|
|
|
|
|
|
@given("a mock context service with no files")
|
|
def step_mock_no_files(context):
|
|
"""Set up mock with empty context."""
|
|
context.mock_service = MagicMock()
|
|
context.mock_service.list_context.return_value = []
|
|
|
|
|
|
@given("a mock context service with file content")
|
|
def step_mock_with_content(context):
|
|
"""Set up mock with file content."""
|
|
context.mock_service = MagicMock()
|
|
context.mock_content = "File content here\nLine 2"
|
|
context.mock_service.get_context_content.return_value = context.mock_content
|
|
|
|
|
|
@given("a mock context service returning no content")
|
|
def step_mock_no_content(context):
|
|
"""Set up mock returning no content."""
|
|
context.mock_service = MagicMock()
|
|
context.mock_service.get_context_content.return_value = None
|
|
|
|
|
|
@given("a mock context service with multiple files")
|
|
def step_mock_multiple_files(context):
|
|
"""Set up mock with multiple files."""
|
|
context.mock_service = MagicMock()
|
|
context.mock_files = [
|
|
{"path": f"/file{i}.txt", "size_bytes": i * 1000} for i in range(1, 6)
|
|
]
|
|
context.mock_service.list_context.return_value = context.mock_files
|
|
|
|
|
|
@given("a mock context service with single file content")
|
|
def step_mock_single_file_content(context):
|
|
"""Set up mock with a single file containing content."""
|
|
context.mock_service = MagicMock()
|
|
context.single_file_entry = {
|
|
"path": "docs/report.txt",
|
|
"content": "Generated summary",
|
|
"size": 512,
|
|
"added_at": "2024-03-01",
|
|
}
|
|
context.mock_service.list_context.return_value = [context.single_file_entry]
|
|
|
|
|
|
@given("a mock context service with single file lacking content")
|
|
def step_mock_single_file_blank(context):
|
|
"""Set up mock with a single file but no stored content."""
|
|
context.mock_service = MagicMock()
|
|
context.single_file_entry = {
|
|
"path": "docs/empty.txt",
|
|
"content": None,
|
|
"size": 128,
|
|
"added_at": "2024-03-02",
|
|
}
|
|
context.mock_service.list_context.return_value = [context.single_file_entry]
|
|
|
|
|
|
@given("a mock context service with clearable files")
|
|
def step_mock_clearable(context):
|
|
"""Set up mock for clear operations."""
|
|
context.mock_service = MagicMock()
|
|
context.mock_files = [{"path": f"/file{i}.txt"} for i in range(3)]
|
|
context.mock_service.list_context.return_value = context.mock_files
|
|
|
|
|
|
@when("the context add command is executed with valid paths")
|
|
def step_exec_context_add_valid(context):
|
|
"""Execute context add with valid paths."""
|
|
runner = CliRunner()
|
|
|
|
# Create temp files
|
|
test_file = Path(context.temp_dir) / "test.txt"
|
|
test_file.write_text("content")
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
# add_to_context should return a tuple: (added_files, already_in_context)
|
|
context.mock_service.add_to_context.return_value = ([test_file], [])
|
|
mock_container.return_value.context_service.return_value = context.mock_service
|
|
|
|
result = runner.invoke(context_app.app, ["add", str(test_file)])
|
|
context.result = result
|
|
|
|
|
|
@when("the context add command is executed with non-existent path")
|
|
def step_exec_context_add_nonexistent(context):
|
|
"""Execute context add with missing path."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
mock_container.return_value.context_service.return_value = context.mock_service
|
|
|
|
result = runner.invoke(context_app.app, ["add", "/nonexistent/path.txt"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context add command is executed without an active project")
|
|
def step_exec_context_add_no_project(context):
|
|
"""Execute context add when no project is configured."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = None
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["add", "placeholder.txt"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context add command processes already tracked paths")
|
|
def step_exec_context_add_already(context):
|
|
"""Execute context add when files are already tracked."""
|
|
runner = CliRunner()
|
|
|
|
test_file = Path(context.temp_dir) / "tracked.txt"
|
|
test_file.write_text("content")
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = object()
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["add", str(test_file)])
|
|
context.result = result
|
|
|
|
|
|
@when("the context add command is executed and fails")
|
|
def step_exec_context_add_fails(context):
|
|
"""Execute context add that fails."""
|
|
runner = CliRunner()
|
|
|
|
test_file = Path(context.temp_dir) / "test.txt"
|
|
test_file.write_text("content")
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
context.mock_service.add_to_context.side_effect = context.mock_error
|
|
mock_container.return_value.context_service.return_value = context.mock_service
|
|
|
|
result = runner.invoke(context_app.app, ["add", str(test_file)])
|
|
context.result = result
|
|
|
|
|
|
@when("the context load alias is executed")
|
|
def step_exec_context_load(context):
|
|
"""Execute context load."""
|
|
runner = CliRunner()
|
|
|
|
test_file = Path(context.temp_dir) / "test.txt"
|
|
test_file.write_text("content")
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
# add_to_context should return a tuple: (added_files, already_in_context)
|
|
context.mock_service.add_to_context.return_value = ([test_file], [])
|
|
mock_container.return_value.context_service.return_value = context.mock_service
|
|
|
|
result = runner.invoke(context_app.app, ["load", str(test_file)])
|
|
context.result = result
|
|
|
|
|
|
@when("the context remove command is executed")
|
|
def step_exec_context_remove(context):
|
|
"""Execute context remove."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = object()
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["rm", "/file1.txt", "/file2.txt"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context remove command is executed without an active project")
|
|
def step_exec_context_remove_no_project(context):
|
|
"""Execute context remove when no project is configured."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = None
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["rm", "/file1.txt"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context remove command is executed for relative paths")
|
|
def step_exec_context_remove_relative(context):
|
|
"""Execute context remove for relative file names."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = object()
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["rm", "file1.txt", "file2.txt"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context remove command is executed with service failure")
|
|
def step_exec_context_remove_failure(context):
|
|
"""Execute context remove that raises CleverAgentsError."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
context.mock_service.remove_from_context.side_effect = context.mock_error
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = object()
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["rm", "/file1.txt"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context list command is executed")
|
|
def step_exec_context_list(context):
|
|
"""Execute context list."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = object()
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["list"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context list command is executed without an active project")
|
|
def step_exec_context_list_no_project(context):
|
|
"""Execute context list when no project is configured."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = None
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["list"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context list command is executed with service failure")
|
|
def step_exec_context_list_failure(context):
|
|
"""Execute context list when the service raises an error."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
context.mock_service.list_context.side_effect = context.mock_error
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = object()
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["list"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context show command is executed with path")
|
|
def step_exec_context_show_with_path(context):
|
|
"""Execute context show with path."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
mock_container.return_value.context_service.return_value = context.mock_service
|
|
|
|
result = runner.invoke(context_app.app, ["show", "/test.txt"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context show command is executed without path")
|
|
def step_exec_context_show_without_path(context):
|
|
"""Execute context show without path."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
mock_container.return_value.context_service.return_value = context.mock_service
|
|
|
|
result = runner.invoke(context_app.app, ["show"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context show command is executed without an active project")
|
|
def step_exec_context_show_no_project(context):
|
|
"""Execute context show when no project is configured."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = None
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["show"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context show command is executed with service failure")
|
|
def step_exec_context_show_failure(context):
|
|
"""Execute context show when the service raises an error."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
context.mock_service.get_context_content.side_effect = context.mock_error
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = object()
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["show", "/test.txt"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context clear command is executed with yes flag")
|
|
def step_exec_context_clear_yes(context):
|
|
"""Execute context clear with --yes."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = object()
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["clear", "--yes"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context clear command is executed and user cancels")
|
|
def step_exec_context_clear_cancel(context):
|
|
"""Execute context clear with cancel."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = object()
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["clear"], input="n\n")
|
|
context.result = result
|
|
|
|
|
|
@when("the context clear command is executed")
|
|
def step_exec_context_clear(context):
|
|
"""Execute context clear."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = object()
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["clear"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context clear command is executed without an active project")
|
|
def step_exec_context_clear_no_project(context):
|
|
"""Execute context clear when no project is configured."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = None
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["clear"])
|
|
context.result = result
|
|
|
|
|
|
@when("the context clear command is executed with service failure")
|
|
def step_exec_context_clear_failure(context):
|
|
"""Execute context clear when the service raises an error."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
container = mock_container.return_value
|
|
container.context_service.return_value = context.mock_service
|
|
context.mock_service.list_context.return_value = [{"path": "pending.txt"}]
|
|
context.mock_service.clear_context.side_effect = context.mock_error
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = object()
|
|
container.project_service.return_value = project_service
|
|
|
|
result = runner.invoke(context_app.app, ["clear", "--yes"])
|
|
context.result = result
|
|
|
|
|
|
@when("context command is invoked without subcommand")
|
|
def step_exec_context_no_subcommand(context):
|
|
"""Execute context without subcommand."""
|
|
runner = CliRunner()
|
|
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
mock_container.return_value.context_service.return_value = context.mock_service
|
|
|
|
result = runner.invoke(context_app.app, [])
|
|
context.result = result
|
|
|
|
|
|
@then("files should be added successfully with output")
|
|
def step_check_add_success(context):
|
|
"""Check add success."""
|
|
assert context.result.exit_code == 0, (
|
|
f"Command failed with exit code {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}\n"
|
|
f"Exception: {context.result.exception}"
|
|
)
|
|
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
clean_output_lower = clean_output.lower()
|
|
|
|
# Check for success message - actual output is "✓ Added 1 file(s) to context:" (with colon)
|
|
assert (
|
|
"added" in clean_output_lower and "file(s) to context" in clean_output_lower
|
|
), (
|
|
f"Expected message about adding files to context.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'\n"
|
|
f"Looking for: 'added' and 'file(s) to context' (case-insensitive)\n"
|
|
f"Note: Checking after stripping ANSI color codes"
|
|
)
|
|
|
|
|
|
@then("already tracked file details should be reported")
|
|
def step_check_already_tracked(context):
|
|
"""Verify output when files are already tracked."""
|
|
assert context.result.exit_code == 0, (
|
|
f"Command failed with exit code {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}"
|
|
)
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
assert "File(s) already in context" in clean_output, (
|
|
f"Expected warning about already tracked files.\n"
|
|
f"Output: {context.result.output}"
|
|
)
|
|
assert "... and 2 more files" in clean_output, (
|
|
f"Expected summary of remaining already tracked files.\n"
|
|
f"Output: {context.result.output}"
|
|
)
|
|
|
|
|
|
@then("path does not exist error should be shown")
|
|
def step_check_path_error(context):
|
|
"""Check path error."""
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
|
|
assert "Path does not exist" in clean_output, (
|
|
f"Expected 'Path does not exist' error message.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'\n"
|
|
f"Exit code: {context.result.exit_code}"
|
|
)
|
|
|
|
|
|
@then("a no active project error should be shown")
|
|
def step_check_no_project_error(context):
|
|
"""Ensure commands abort when no project is active."""
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
assert context.result.exit_code != 0, (
|
|
f"Expected non-zero exit code when project is missing.\n"
|
|
f"Output: {context.result.output}"
|
|
)
|
|
assert "No active project" in clean_output, (
|
|
f"Expected 'No active project' message.\nOutput: {context.result.output}"
|
|
)
|
|
|
|
|
|
@then("FileSystemError should be displayed and command aborted")
|
|
def step_check_filesystem_error(context):
|
|
"""Check filesystem error."""
|
|
assert context.result.exit_code != 0, (
|
|
f"Expected non-zero exit code for error, got {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}"
|
|
)
|
|
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
|
|
assert "File System Error" in clean_output, (
|
|
f"Expected 'File System Error' in output.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'"
|
|
)
|
|
|
|
|
|
@then("CleverAgentsError should be displayed and command aborted")
|
|
def step_check_cleveragents_error(context):
|
|
"""Check CleverAgentsError."""
|
|
assert context.result.exit_code != 0, (
|
|
f"Expected non-zero exit code for error, got {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}"
|
|
)
|
|
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
|
|
assert "Error:" in clean_output, (
|
|
f"Expected 'Error:' in output for CleverAgentsError.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'"
|
|
)
|
|
|
|
|
|
@then("it should delegate to context add function")
|
|
def step_check_load_delegation(context):
|
|
"""Check load delegates to add."""
|
|
assert context.result.exit_code == 0, (
|
|
f"Command failed with exit code {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}\n"
|
|
f"Exception: {context.result.exception}"
|
|
)
|
|
|
|
assert context.mock_service.add_to_context.called, (
|
|
f"Expected add_to_context to be called (load should delegate to add).\n"
|
|
f"Mock called: {context.mock_service.add_to_context.called}\n"
|
|
f"Call count: {context.mock_service.add_to_context.call_count}"
|
|
)
|
|
|
|
|
|
@then("files should be removed with confirmation message")
|
|
def step_check_remove_success(context):
|
|
"""Check remove success."""
|
|
# First check exit code
|
|
assert context.result.exit_code == 0, (
|
|
f"Command failed with exit code {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}\n"
|
|
f"Exception: {context.result.exception}"
|
|
)
|
|
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
clean_output_lower = clean_output.lower()
|
|
|
|
# Check for the success message - the actual output is "✓ Removed 2 file(s) from context."
|
|
assert (
|
|
"removed" in clean_output_lower and "file(s) from context" in clean_output_lower
|
|
), (
|
|
f"Expected message about removing files from context.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'\n"
|
|
f"Looking for: 'Removed' and 'file(s) from context' (case-insensitive)"
|
|
)
|
|
|
|
|
|
@then("no files removed message should be shown")
|
|
def step_check_no_remove(context):
|
|
"""Check no files removed."""
|
|
# First check if command succeeded
|
|
assert context.result.exit_code == 0, (
|
|
f"Command failed with exit code {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}\n"
|
|
f"Exception: {context.result.exception}"
|
|
)
|
|
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
|
|
# Check for the exact message
|
|
assert "No files were removed from context" in clean_output, (
|
|
f"Expected 'No files were removed from context' message.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'\n"
|
|
f"Note: This message appears when remove_from_context returns 0 for all paths"
|
|
)
|
|
|
|
|
|
@then("missing context files should trigger an abort")
|
|
def step_check_missing_context_abort(context):
|
|
"""Ensure missing files cause the command to abort."""
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
assert context.result.exit_code != 0, (
|
|
f"Expected abort when files are missing from context.\n"
|
|
f"Output: {context.result.output}"
|
|
)
|
|
assert "File(s) not in context" in clean_output, (
|
|
f"Expected message about missing context files.\n"
|
|
f"Output: {context.result.output}"
|
|
)
|
|
|
|
|
|
@then("a formatted table of files should be displayed")
|
|
def step_check_table_display(context):
|
|
"""Check table display."""
|
|
assert context.result.exit_code == 0, (
|
|
f"Command failed with exit code {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}\n"
|
|
f"Exception: {context.result.exception}"
|
|
)
|
|
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
|
|
# Check for table headers or file content
|
|
assert "File Path" in clean_output or "main.py" in clean_output, (
|
|
f"Expected table headers ('File Path') or file names in output.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'"
|
|
)
|
|
|
|
|
|
@then("numeric sizes should be rendered as bytes")
|
|
def step_check_numeric_sizes(context):
|
|
"""Ensure numeric sizes are humanized as byte strings."""
|
|
assert context.result.exit_code == 0, (
|
|
f"Command failed with exit code {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}"
|
|
)
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
assert "bytes" in clean_output, (
|
|
f"Expected sizes to be formatted in bytes.\nOutput: {context.result.output}"
|
|
)
|
|
|
|
|
|
@then("empty context message should be shown with help")
|
|
def step_check_empty_message(context):
|
|
"""Check empty message."""
|
|
assert context.result.exit_code == 0, (
|
|
f"Command failed with exit code {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}\n"
|
|
f"Exception: {context.result.exception}"
|
|
)
|
|
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
|
|
assert "No files in context" in clean_output, (
|
|
f"Expected 'No files in context' message.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'"
|
|
)
|
|
|
|
assert "agents context add" in clean_output, (
|
|
f"Expected help text 'agents context add' in output.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'"
|
|
)
|
|
|
|
|
|
@then("file content should be displayed in panel")
|
|
def step_check_content_panel(context):
|
|
"""Check content panel."""
|
|
assert context.result.exit_code == 0, (
|
|
f"Command failed with exit code {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}\n"
|
|
f"Exception: {context.result.exception}"
|
|
)
|
|
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
|
|
# Check for either the content or the panel structure
|
|
assert (
|
|
"File content" in clean_output
|
|
or "Content:" in clean_output
|
|
or "Panel" in clean_output
|
|
), (
|
|
f"Expected content display with 'File content', 'Content:', or 'Panel'.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'"
|
|
)
|
|
|
|
|
|
@then("file not found message should be displayed")
|
|
def step_check_not_found(context):
|
|
"""Check not found message."""
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
|
|
assert "File not found in context" in clean_output, (
|
|
f"Expected 'File not found in context' message.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'"
|
|
)
|
|
|
|
|
|
@then("context summary with stats should be displayed")
|
|
def step_check_summary(context):
|
|
"""Check summary display."""
|
|
assert context.result.exit_code == 0, (
|
|
f"Command failed with exit code {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}\n"
|
|
f"Exception: {context.result.exception}"
|
|
)
|
|
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
|
|
assert "Total files" in clean_output or "Context Summary" in clean_output, (
|
|
f"Expected summary with 'Total files' or 'Context Summary'.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'"
|
|
)
|
|
|
|
|
|
@then("the show command should report an empty context")
|
|
def step_check_show_empty_context(context):
|
|
"""Verify show command informs when no files exist."""
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
assert context.result.exit_code == 0, (
|
|
f"Expected successful exit for empty context message.\n"
|
|
f"Output: {context.result.output}"
|
|
)
|
|
assert "No files in context" in clean_output, (
|
|
f"Expected 'No files in context' message.\nOutput: {context.result.output}"
|
|
)
|
|
|
|
|
|
@then("the show command should report missing content")
|
|
def step_check_show_missing_content(context):
|
|
"""Ensure show command highlights missing file content."""
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
assert context.result.exit_code == 0, (
|
|
f"Expected successful exit when content is missing.\n"
|
|
f"Output: {context.result.output}"
|
|
)
|
|
assert "No content available for" in clean_output, (
|
|
f"Expected message indicating missing content.\nOutput: {context.result.output}"
|
|
)
|
|
|
|
assert "No content available" in clean_output, (
|
|
f"Expected message indicating missing content.\nOutput: {context.result.output}"
|
|
)
|
|
|
|
|
|
@then("context should be cleared with success message")
|
|
def step_check_clear_success(context):
|
|
"""Check clear success."""
|
|
assert context.result.exit_code == 0, (
|
|
f"Command failed with exit code {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}\n"
|
|
f"Exception: {context.result.exception}"
|
|
)
|
|
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
|
|
assert "Cleared all files from context" in clean_output, (
|
|
f"Expected 'Cleared all files from context' message.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'"
|
|
)
|
|
|
|
|
|
@then("cancelled message should appear and context unchanged")
|
|
def step_check_cancelled(context):
|
|
"""Check cancelled."""
|
|
assert context.result.exit_code == 0, (
|
|
f"Command failed with exit code {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}\n"
|
|
f"Exception: {context.result.exception}"
|
|
)
|
|
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
|
|
assert "Cancelled" in clean_output, (
|
|
f"Expected 'Cancelled' message when user cancels clear operation.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'"
|
|
)
|
|
|
|
|
|
@then("already empty message should be shown")
|
|
def step_check_already_empty(context):
|
|
"""Check already empty."""
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
|
|
assert "Context is already empty" in clean_output, (
|
|
f"Expected 'Context is already empty' message.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'"
|
|
)
|
|
|
|
|
|
@then("the list function should be called by default")
|
|
def step_check_default_list(context):
|
|
"""Check default list behavior."""
|
|
assert context.result.exit_code == 0, (
|
|
f"Command failed with exit code {context.result.exit_code}\n"
|
|
f"Output: {context.result.output}\n"
|
|
f"Exception: {context.result.exception}"
|
|
)
|
|
|
|
# Strip ANSI color codes before checking
|
|
clean_output = strip_ansi_codes(context.result.output)
|
|
|
|
# Default behavior shows the list
|
|
assert "File Path" in clean_output or "main.py" in clean_output, (
|
|
f"Expected default list output with table headers or file names.\n"
|
|
f"Raw output: '{context.result.output}'\n"
|
|
f"Clean output (no color codes): '{clean_output}'"
|
|
)
|
|
|
|
|
|
@given("a dictionary-style context entry with missing metadata")
|
|
def step_given_dict_entry(context):
|
|
"""Prepare dictionary entry missing optional fields."""
|
|
context.dict_entry = {
|
|
"path": "relative/file.txt",
|
|
"type": "NOTE",
|
|
"size": "5 KB",
|
|
}
|
|
|
|
|
|
@given("a domain Context model entry with metadata")
|
|
def step_given_context_model_entry(context):
|
|
"""Prepare a Context model instance for normalization."""
|
|
context.dict_entry = Context(
|
|
id=7,
|
|
plan_id=42,
|
|
type=ContextType.NOTE,
|
|
path="/project/docs/readme.md",
|
|
content="Example context body",
|
|
file_hash="abc123",
|
|
size=2048,
|
|
added_at=datetime(2024, 1, 5, 15, 30, 45),
|
|
)
|
|
|
|
|
|
@when("the normalization helper processes the context entry")
|
|
def step_when_normalize_entry(context):
|
|
"""Normalize dictionary-based context entry."""
|
|
context.normalized_entry = context_app._normalize_context_entry(context.dict_entry)
|
|
|
|
|
|
@then("the normalized context tuple should include placeholder defaults")
|
|
def step_then_normalized_defaults(context):
|
|
"""Verify normalization uses defaults for missing fields."""
|
|
path_text, type_label, size, added_at, content_text = context.normalized_entry
|
|
assert path_text == "relative/file.txt"
|
|
assert type_label == "NOTE"
|
|
assert size == "5 KB"
|
|
assert added_at == "Unknown"
|
|
assert content_text == ""
|
|
|
|
|
|
@then("the normalized context tuple should match the model attributes")
|
|
def step_then_normalized_model(context):
|
|
"""Verify normalization preserves Context model details."""
|
|
path_text, type_label, size, added_at, content_text = context.normalized_entry
|
|
assert path_text == "/project/docs/readme.md"
|
|
assert type_label == "note"
|
|
assert size == 2048
|
|
assert added_at == "2024-01-05 15:30:45"
|
|
assert content_text == "Example context body"
|
|
|
|
|
|
@when("the programmatic add command runs with a missing file path")
|
|
def step_when_programmatic_add_missing(context):
|
|
"""Run add_command with a path that does not exist."""
|
|
missing_path = Path(context.temp_dir) / "missing.txt"
|
|
context.programmatic_exception = None
|
|
context.mock_service.add_to_context.reset_mock()
|
|
with patched_container(context, project=object()):
|
|
try:
|
|
context_app.add_command([str(missing_path)])
|
|
except Exception as exc: # pragma: no cover - captured for assertions
|
|
context.programmatic_exception = exc
|
|
context.programmatic_result = None
|
|
|
|
|
|
@then("no files are sent to the context service")
|
|
def step_then_no_files_sent(context):
|
|
"""Ensure add_to_context is not called for missing files."""
|
|
assert context.programmatic_exception is None
|
|
context.mock_service.add_to_context.assert_not_called()
|
|
|
|
|
|
@when("the programmatic add command runs without an active project")
|
|
def step_when_programmatic_add_no_project(context):
|
|
"""Run add_command without an active project."""
|
|
test_file = Path(context.temp_dir) / "active.txt"
|
|
test_file.write_text("data")
|
|
context.programmatic_exception = None
|
|
with patched_container(context, project=None):
|
|
try:
|
|
context_app.add_command([str(test_file)])
|
|
except Exception as exc:
|
|
context.programmatic_exception = exc
|
|
|
|
|
|
@then("a CleverAgentsError is raised for programmatic add")
|
|
def step_then_add_requires_project(context):
|
|
"""Ensure add_command raises when project is missing."""
|
|
assert isinstance(context.programmatic_exception, CleverAgentsError)
|
|
|
|
|
|
@when("the programmatic list command is executed")
|
|
def step_when_programmatic_list(context):
|
|
"""Execute list_command with active project."""
|
|
context.programmatic_exception = None
|
|
with patched_container(context, project=object()):
|
|
context.programmatic_result = context_app.list_command()
|
|
|
|
|
|
@then("the programmatic list command should return those files")
|
|
def step_then_list_returns_files(context):
|
|
"""Verify list_command returns mock files."""
|
|
assert context.programmatic_result == context.mock_files
|
|
|
|
|
|
@when("the programmatic list command runs without an active project")
|
|
def step_when_programmatic_list_no_project(context):
|
|
"""Execute list_command without active project."""
|
|
context.programmatic_exception = None
|
|
with patched_container(context, project=None):
|
|
try:
|
|
context_app.list_command()
|
|
except Exception as exc:
|
|
context.programmatic_exception = exc
|
|
|
|
|
|
@then("a CleverAgentsError is raised for programmatic list")
|
|
def step_then_list_requires_project(context):
|
|
"""Ensure list_command raises when project is missing."""
|
|
assert isinstance(context.programmatic_exception, CleverAgentsError)
|
|
|
|
|
|
@when("the programmatic show command runs without an active project")
|
|
def step_when_programmatic_show_no_project(context):
|
|
"""Execute show_command without an active project configured."""
|
|
context.programmatic_exception = None
|
|
context.programmatic_result = None
|
|
with patched_container(context, project=None):
|
|
try:
|
|
context_app.show_command()
|
|
except Exception as exc:
|
|
context.programmatic_exception = exc
|
|
|
|
|
|
@when("the programmatic show command runs without a path")
|
|
def step_when_programmatic_show_summary(context):
|
|
"""Execute show_command when listing context entries."""
|
|
context.programmatic_exception = None
|
|
with patched_container(context, project=object()):
|
|
context.programmatic_result = context_app.show_command()
|
|
|
|
|
|
@then("the programmatic show command should return a summary string")
|
|
def step_then_show_returns_summary(context):
|
|
"""Verify show_command returns summary when no path provided."""
|
|
expected = f"Total files: {len(context.mock_files)}"
|
|
assert context.programmatic_result == expected
|
|
|
|
|
|
@then("a CleverAgentsError is raised for programmatic show")
|
|
def step_then_show_requires_project(context):
|
|
"""Ensure show_command raises when no project is active."""
|
|
assert isinstance(context.programmatic_exception, CleverAgentsError)
|
|
|
|
|
|
@then("the programmatic show command should return no content summary")
|
|
def step_then_show_returns_none(context):
|
|
"""Ensure show_command returns None when context has no files."""
|
|
assert context.programmatic_exception is None
|
|
assert context.programmatic_result is None
|
|
|
|
|
|
@when("the programmatic show command runs for a specific path")
|
|
def step_when_programmatic_show_specific(context):
|
|
"""Execute show_command for a single file path."""
|
|
context.programmatic_exception = None
|
|
with patched_container(context, project=object()):
|
|
context.programmatic_result = context_app.show_command("/test.txt")
|
|
|
|
|
|
@then("the programmatic show command should return that content")
|
|
def step_then_show_returns_content(context):
|
|
"""Ensure show_command returns content for requested file."""
|
|
assert context.programmatic_result == context.mock_content
|
|
|
|
|
|
@when("the programmatic remove command runs without an active project")
|
|
def step_when_programmatic_remove_no_project(context):
|
|
"""Execute rm_command without an active project configured."""
|
|
context.programmatic_exception = None
|
|
with patched_container(context, project=None):
|
|
try:
|
|
context_app.rm_command(["missing.txt"])
|
|
except Exception as exc:
|
|
context.programmatic_exception = exc
|
|
|
|
|
|
@when("the programmatic remove command runs for files")
|
|
def step_when_programmatic_remove(context):
|
|
"""Execute rm_command where files are missing."""
|
|
context.programmatic_exception = None
|
|
with patched_container(context, project=object()):
|
|
try:
|
|
context_app.rm_command(["missing.txt", "other.txt"])
|
|
except Exception as exc:
|
|
context.programmatic_exception = exc
|
|
|
|
|
|
@when("the programmatic remove command runs with removable files")
|
|
def step_when_programmatic_remove_success(context):
|
|
"""Execute rm_command when files are present in context."""
|
|
context.programmatic_exception = None
|
|
context.mock_service.remove_from_context.reset_mock()
|
|
with patched_container(context, project=object()):
|
|
try:
|
|
context_app.rm_command(["/file1.txt", "/file2.txt"])
|
|
except Exception as exc:
|
|
context.programmatic_exception = exc
|
|
|
|
|
|
@then("a CleverAgentsError is raised for programmatic remove")
|
|
def step_then_remove_requires_presence(context):
|
|
"""Ensure rm_command raises when files are not in context."""
|
|
assert isinstance(context.programmatic_exception, CleverAgentsError)
|
|
|
|
|
|
@then("the programmatic remove command should complete successfully")
|
|
def step_then_remove_success(context):
|
|
"""Ensure rm_command succeeds when files are removed."""
|
|
assert context.programmatic_exception is None
|
|
assert context.mock_service.remove_from_context.call_count == 2
|
|
|
|
|
|
@when("the programmatic clear command runs without an active project")
|
|
def step_when_programmatic_clear_no_project(context):
|
|
"""Execute clear_command without an active project."""
|
|
context.programmatic_exception = None
|
|
with patched_container(context, project=None):
|
|
try:
|
|
context_app.clear_command()
|
|
except Exception as exc:
|
|
context.programmatic_exception = exc
|
|
|
|
|
|
@then("a CleverAgentsError is raised for programmatic clear")
|
|
def step_then_clear_requires_project(context):
|
|
"""Ensure clear_command raises when project is missing."""
|
|
assert isinstance(context.programmatic_exception, CleverAgentsError)
|
|
|
|
|
|
@when("the programmatic clear command runs with an active project")
|
|
def step_when_programmatic_clear_active(context):
|
|
"""Execute clear_command with active project."""
|
|
context.programmatic_exception = None
|
|
context.mock_service.clear_context.reset_mock()
|
|
with patched_container(context, project=object()):
|
|
try:
|
|
context_app.clear_command()
|
|
except Exception as exc:
|
|
context.programmatic_exception = exc
|
|
|
|
|
|
@then("the context service clear function should be called")
|
|
def step_then_clear_calls_service(context):
|
|
"""Verify clear_command calls the context service clear method."""
|
|
assert context.programmatic_exception is None
|
|
context.mock_service.clear_context.assert_called_once()
|