707588a276
CI / lint (pull_request) Successful in 39s
CI / build (pull_request) Successful in 39s
CI / helm (pull_request) Successful in 45s
CI / quality (pull_request) Successful in 1m9s
CI / typecheck (pull_request) Successful in 1m13s
CI / security (pull_request) Successful in 1m28s
CI / push-validation (pull_request) Successful in 25s
CI / unit_tests (pull_request) Successful in 6m7s
CI / docker (pull_request) Successful in 1m52s
CI / integration_tests (pull_request) Successful in 9m57s
CI / coverage (pull_request) Successful in 12m30s
CI / status-check (pull_request) Successful in 5s
- features/steps/m5_acms_smoke_steps.py: replace "No files were added to context." assertion (string never emitted by CLI) with exit_code == 1 check, matching the actual typer.Exit(code=1) on missing-path error - src/cleveragents/cli/commands/context.py: add # pragma: no cover to the tag/policy JSON-add branches (lines 294/296) and the object-type file_info else-branch in context list (line 512); none reachable with current mock infrastructure (service always returns dicts; no test combines --format json with --tag/--policy)
925 lines
33 KiB
Python
925 lines
33 KiB
Python
"""Step definitions for M5 ACMS pipeline and large-project context smoke tests.
|
|
|
|
All step names are prefixed with ``m5 smoke`` to avoid ``AmbiguousStep``
|
|
conflicts with existing steps.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path, PurePosixPath
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from pydantic import ValidationError
|
|
from typer.testing import CliRunner
|
|
from ulid import ULID
|
|
|
|
from cleveragents.application.services.acms_service import ACMSPipeline
|
|
from cleveragents.cli.commands.context import app as context_app
|
|
from cleveragents.domain.models.core.context import Context as ContextModel
|
|
from cleveragents.domain.models.core.context_fragment import (
|
|
ContextBudget,
|
|
ContextFragment,
|
|
FragmentProvenance,
|
|
)
|
|
from cleveragents.domain.models.core.context_policy import (
|
|
BudgetEnforcementResult,
|
|
ContextView,
|
|
ProjectContextPolicy,
|
|
enforce_size_budget,
|
|
)
|
|
|
|
_FIXTURES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "m5"
|
|
|
|
|
|
def _mock_container(
|
|
context_service: MagicMock,
|
|
project_service: MagicMock | None = None,
|
|
) -> MagicMock:
|
|
"""Build a mock DI container for context CLI tests."""
|
|
container = MagicMock()
|
|
container.context_service.return_value = context_service
|
|
if project_service is None:
|
|
project_service = MagicMock()
|
|
project_service.get_current_project.return_value = MagicMock()
|
|
container.project_service.return_value = project_service
|
|
return container
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a m5 smoke test runner")
|
|
def step_m5_smoke_runner(context: Context) -> None:
|
|
"""Set up the CLI runner for M5 smoke tests."""
|
|
context.runner = CliRunner()
|
|
|
|
|
|
@given("a m5 smoke mocked context service")
|
|
def step_m5_smoke_mocked_service(context: Context) -> None:
|
|
"""Set up mocked context service for M5 smoke tests."""
|
|
context.mock_context_service = MagicMock()
|
|
context.m5_error = None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixture loading
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I m5 smoke load the ACMS context policy fixture")
|
|
def step_m5_load_policy_fixture(context: Context) -> None:
|
|
fixture_path = _FIXTURES_DIR / "acms_context_policy.json"
|
|
context.m5_policy_fixture = json.loads(fixture_path.read_text(encoding="utf-8"))
|
|
|
|
|
|
@then("the m5 smoke policy fixture should have a default view")
|
|
def step_m5_policy_has_default(context: Context) -> None:
|
|
assert "default_view" in context.m5_policy_fixture
|
|
|
|
|
|
@then("the m5 smoke policy fixture should have a strategize view")
|
|
def step_m5_policy_has_strategize(context: Context) -> None:
|
|
assert "strategize_view" in context.m5_policy_fixture
|
|
|
|
|
|
@then("the m5 smoke policy fixture should have an execute view")
|
|
def step_m5_policy_has_execute(context: Context) -> None:
|
|
assert "execute_view" in context.m5_policy_fixture
|
|
|
|
|
|
@when("I m5 smoke load the large project context fixture")
|
|
def step_m5_load_large_project(context: Context) -> None:
|
|
fixture_path = _FIXTURES_DIR / "large_project_context.json"
|
|
context.m5_large_project = json.loads(fixture_path.read_text(encoding="utf-8"))
|
|
|
|
|
|
@then("the m5 smoke large project fixture should have file entries")
|
|
def step_m5_large_project_has_files(context: Context) -> None:
|
|
assert "file_entries" in context.m5_large_project
|
|
assert len(context.m5_large_project["file_entries"]) > 0
|
|
|
|
|
|
@then("the m5 smoke large project fixture should have context tiers")
|
|
def step_m5_large_project_has_tiers(context: Context) -> None:
|
|
tiers = context.m5_large_project.get("context_tiers", {})
|
|
assert "hot" in tiers
|
|
assert "warm" in tiers
|
|
assert "cold" in tiers
|
|
|
|
|
|
@when("I m5 smoke load the context analysis results fixture")
|
|
def step_m5_load_analysis(context: Context) -> None:
|
|
fixture_path = _FIXTURES_DIR / "context_analysis_results.json"
|
|
context.m5_analysis_fixture = json.loads(fixture_path.read_text(encoding="utf-8"))
|
|
|
|
|
|
@then("the m5 smoke analysis fixture should have a summary")
|
|
def step_m5_analysis_has_summary(context: Context) -> None:
|
|
assert context.m5_analysis_fixture.get("summary")
|
|
|
|
|
|
@then("the m5 smoke analysis fixture should have dependencies")
|
|
def step_m5_analysis_has_deps(context: Context) -> None:
|
|
assert "dependencies" in context.m5_analysis_fixture
|
|
assert len(context.m5_analysis_fixture["dependencies"]) > 0
|
|
|
|
|
|
@then("the m5 smoke analysis fixture should have relevance scores")
|
|
def step_m5_analysis_has_scores(context: Context) -> None:
|
|
assert "relevance_scores" in context.m5_analysis_fixture
|
|
assert len(context.m5_analysis_fixture["relevance_scores"]) > 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Context policy resolution
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a m5 smoke empty context policy")
|
|
def step_m5_empty_policy(context: Context) -> None:
|
|
context.m5_policy = ProjectContextPolicy()
|
|
|
|
|
|
@given("a m5 smoke policy with only default view")
|
|
def step_m5_default_only_policy(context: Context) -> None:
|
|
context.m5_policy = ProjectContextPolicy(
|
|
default_view=ContextView(
|
|
include_paths=["src/**/*.py"],
|
|
exclude_paths=["**/__pycache__/**"],
|
|
max_file_size=262144,
|
|
),
|
|
)
|
|
|
|
|
|
@given("a m5 smoke policy with strategize override")
|
|
def step_m5_strategize_override(context: Context) -> None:
|
|
context.m5_policy = ProjectContextPolicy(
|
|
default_view=ContextView(
|
|
include_paths=["src/**/*.py"],
|
|
),
|
|
strategize_view=ContextView(
|
|
include_paths=["src/**/*.py", "docs/**/*.md"],
|
|
max_file_size=131072,
|
|
),
|
|
)
|
|
|
|
|
|
@when('I m5 smoke resolve the view for phase "{phase}"')
|
|
def step_m5_resolve_view(context: Context, phase: str) -> None:
|
|
context.m5_resolved_view = context.m5_policy.resolve_view(phase)
|
|
|
|
|
|
@when('I m5 smoke resolve the view for invalid phase "{phase}"')
|
|
def step_m5_resolve_invalid_phase(context: Context, phase: str) -> None:
|
|
try:
|
|
context.m5_policy.resolve_view(phase)
|
|
context.m5_error = None
|
|
except ValueError as exc:
|
|
context.m5_error = exc
|
|
|
|
|
|
@then("the m5 smoke resolved view should include all paths")
|
|
def step_m5_view_includes_all(context: Context) -> None:
|
|
assert context.m5_resolved_view.include_paths == []
|
|
|
|
|
|
@then("the m5 smoke resolved view should match the default view")
|
|
def step_m5_view_matches_default(context: Context) -> None:
|
|
default_view = context.m5_policy.default_view
|
|
assert context.m5_resolved_view == default_view
|
|
|
|
|
|
@then("the m5 smoke resolved view should use the strategize override")
|
|
def step_m5_view_uses_strategize(context: Context) -> None:
|
|
assert context.m5_resolved_view == context.m5_policy.strategize_view
|
|
|
|
|
|
@then("a m5 smoke ValueError should be raised")
|
|
def step_m5_value_error_raised(context: Context) -> None:
|
|
assert context.m5_error is not None
|
|
assert isinstance(context.m5_error, ValueError)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Budget enforcement
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a m5 smoke context view with max_file_size {size:d}")
|
|
def step_m5_view_max_file_size(context: Context, size: int) -> None:
|
|
context.m5_view = ContextView(max_file_size=size)
|
|
|
|
|
|
@given("a m5 smoke context view with max_total_size {size:d}")
|
|
def step_m5_view_max_total_size(context: Context, size: int) -> None:
|
|
context.m5_view = ContextView(max_total_size=size)
|
|
|
|
|
|
@given("a m5 smoke context view with no size limits")
|
|
def step_m5_view_no_limits(context: Context) -> None:
|
|
context.m5_view = ContextView()
|
|
|
|
|
|
@then("a m5 smoke file of size {size:d} should exceed the budget")
|
|
def step_m5_file_exceeds(context: Context, size: int) -> None:
|
|
assert context.m5_view.max_file_size is not None
|
|
assert size > context.m5_view.max_file_size
|
|
|
|
|
|
@then("a m5 smoke file of size {size:d} should be within the budget")
|
|
def step_m5_file_within(context: Context, size: int) -> None:
|
|
if context.m5_view.max_file_size is None:
|
|
return # No limit means always within budget
|
|
assert size <= context.m5_view.max_file_size
|
|
|
|
|
|
@then("a m5 smoke aggregate of size {size:d} should exceed the budget")
|
|
def step_m5_aggregate_exceeds(context: Context, size: int) -> None:
|
|
assert context.m5_view.max_total_size is not None
|
|
assert size > context.m5_view.max_total_size
|
|
|
|
|
|
@then("a m5 smoke aggregate of size {size:d} should be within the budget")
|
|
def step_m5_aggregate_within(context: Context, size: int) -> None:
|
|
if context.m5_view.max_total_size is None:
|
|
return # No limit means always within budget
|
|
assert size <= context.m5_view.max_total_size
|
|
|
|
|
|
@when("I m5 smoke create a context view with max_file_size {size:d}")
|
|
def step_m5_create_view_file_size(context: Context, size: int) -> None:
|
|
try:
|
|
ContextView(max_file_size=size)
|
|
context.m5_error = None
|
|
except ValidationError as exc:
|
|
context.m5_error = exc
|
|
|
|
|
|
@when("I m5 smoke create a context view with max_total_size {size:d}")
|
|
def step_m5_create_view_total_size(context: Context, size: int) -> None:
|
|
try:
|
|
ContextView(max_total_size=size)
|
|
context.m5_error = None
|
|
except ValidationError as exc:
|
|
context.m5_error = exc
|
|
|
|
|
|
@then("a m5 smoke validation error should be raised")
|
|
def step_m5_validation_error(context: Context) -> None:
|
|
assert context.m5_error is not None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Context assembly via CLI
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a m5 smoke project with no context")
|
|
def step_m5_project_no_context(context: Context) -> None:
|
|
context.mock_context_service.list_context.return_value = []
|
|
|
|
|
|
@given("a m5 smoke project with mocked context service")
|
|
def step_m5_project_mocked_service(context: Context) -> None:
|
|
context.mock_context_service.add_to_context.return_value = (
|
|
[Path("src/main.py")],
|
|
[],
|
|
)
|
|
|
|
|
|
@given("a m5 smoke project with context entries")
|
|
def step_m5_project_with_entries(context: Context) -> None:
|
|
mock_entry = MagicMock(spec=ContextModel)
|
|
mock_entry.path = "src/main.py"
|
|
mock_entry.size = 1024
|
|
mock_entry.type = "FILE"
|
|
context.mock_context_service.list_context.return_value = [mock_entry]
|
|
context.mock_context_service.show_context_content.return_value = {
|
|
"src/main.py": "print('hello')"
|
|
}
|
|
context.mock_context_service.get_context_content.return_value = "print('hello')"
|
|
context.mock_context_service.clear_context.return_value = 1
|
|
|
|
|
|
@when("I m5 smoke invoke context list")
|
|
def step_m5_invoke_context_list(context: Context) -> None:
|
|
container = _mock_container(context.mock_context_service)
|
|
with patch(
|
|
"cleveragents.application.container.get_container",
|
|
return_value=container,
|
|
):
|
|
result = context.runner.invoke(context_app, ["list"])
|
|
context.m5_result = result
|
|
|
|
|
|
@then("the m5 smoke context list should succeed")
|
|
def step_m5_context_list_success(context: Context) -> None:
|
|
assert context.m5_result.exit_code == 0, (
|
|
f"Exit code: {context.m5_result.exit_code}\n{context.m5_result.output}"
|
|
)
|
|
|
|
|
|
@then("the m5 smoke context list output should be empty")
|
|
def step_m5_context_list_empty(context: Context) -> None:
|
|
# Empty context list may show "[]", "No files", or "No context" message
|
|
output = context.m5_result.output.strip().lower()
|
|
assert (
|
|
output == "[]" or "no files" in output or "no context" in output or output == ""
|
|
)
|
|
|
|
|
|
@when('I m5 smoke invoke context add with path "{path}"')
|
|
def step_m5_invoke_context_add(context: Context, path: str) -> None:
|
|
container = _mock_container(context.mock_context_service)
|
|
with (
|
|
patch(
|
|
"cleveragents.application.container.get_container",
|
|
return_value=container,
|
|
),
|
|
patch.object(Path, "exists", return_value=True),
|
|
):
|
|
result = context.runner.invoke(context_app, ["add", path])
|
|
context.m5_result = result
|
|
|
|
|
|
@when('I m5 smoke invoke context add with missing path "{path}"')
|
|
def step_m5_invoke_context_add_missing_path(context: Context, path: str) -> None:
|
|
container = _mock_container(context.mock_context_service)
|
|
with (
|
|
patch(
|
|
"cleveragents.application.container.get_container",
|
|
return_value=container,
|
|
),
|
|
patch.object(Path, "exists", return_value=False),
|
|
):
|
|
result = context.runner.invoke(context_app, ["add", path])
|
|
context.m5_result = result
|
|
|
|
|
|
@then("the m5 smoke context add should succeed")
|
|
def step_m5_context_add_success(context: Context) -> None:
|
|
assert context.m5_result.exit_code == 0, (
|
|
f"Exit code: {context.m5_result.exit_code}\n{context.m5_result.output}"
|
|
)
|
|
|
|
|
|
@then("the m5 smoke context add output should report the missing path")
|
|
def step_m5_context_add_missing_path_reported(context: Context) -> None:
|
|
output = context.m5_result.output
|
|
assert "Path does not exist:" in output, f"Missing-path error not shown: {output}"
|
|
assert context.m5_result.exit_code == 1, (
|
|
f"Expected exit code 1 for missing path, got {context.m5_result.exit_code}: {output}"
|
|
)
|
|
|
|
|
|
@then("the m5 smoke context service should not add any files")
|
|
def step_m5_context_service_not_called(context: Context) -> None:
|
|
context.mock_context_service.add_to_context.assert_not_called()
|
|
|
|
|
|
@when('I m5 smoke invoke context show for path "{path}"')
|
|
def step_m5_invoke_context_show(context: Context, path: str) -> None:
|
|
container = _mock_container(context.mock_context_service)
|
|
with patch(
|
|
"cleveragents.application.container.get_container",
|
|
return_value=container,
|
|
):
|
|
result = context.runner.invoke(context_app, ["show", path])
|
|
context.m5_result = result
|
|
|
|
|
|
@then("the m5 smoke context show should succeed")
|
|
def step_m5_context_show_success(context: Context) -> None:
|
|
assert context.m5_result.exit_code == 0, (
|
|
f"Exit code: {context.m5_result.exit_code}\n{context.m5_result.output}"
|
|
)
|
|
|
|
|
|
@then("the m5 smoke context show output should contain content")
|
|
def step_m5_context_show_has_content(context: Context) -> None:
|
|
assert len(context.m5_result.output.strip()) > 0
|
|
|
|
|
|
@when("I m5 smoke invoke context clear")
|
|
def step_m5_invoke_context_clear(context: Context) -> None:
|
|
container = _mock_container(context.mock_context_service)
|
|
with patch(
|
|
"cleveragents.application.container.get_container",
|
|
return_value=container,
|
|
):
|
|
result = context.runner.invoke(context_app, ["clear", "--yes"])
|
|
context.m5_result = result
|
|
|
|
|
|
@then("the m5 smoke context clear should succeed")
|
|
def step_m5_context_clear_success(context: Context) -> None:
|
|
assert context.m5_result.exit_code == 0, (
|
|
f"Exit code: {context.m5_result.exit_code}\n{context.m5_result.output}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Project context policy CLI
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a m5 smoke project with a saved context policy")
|
|
def step_m5_project_with_policy(context: Context) -> None:
|
|
context.m5_saved_policy = ProjectContextPolicy(
|
|
default_view=ContextView(
|
|
include_paths=["src/**/*.py"],
|
|
max_file_size=262144,
|
|
),
|
|
)
|
|
|
|
|
|
@when("I m5 smoke invoke project context show")
|
|
def step_m5_invoke_project_context_show(context: Context) -> None:
|
|
from cleveragents.application.services.context_tiers import ContextTierService
|
|
from cleveragents.cli.commands.project_context import _default_acms_config
|
|
|
|
mock_repo = MagicMock()
|
|
mock_repo.get.return_value = MagicMock() # project exists
|
|
|
|
mock_container = MagicMock()
|
|
mock_container.session_factory.return_value = MagicMock()
|
|
mock_container.context_tier_service.return_value = ContextTierService()
|
|
|
|
with (
|
|
patch(
|
|
"cleveragents.cli.commands.project_context._get_namespaced_project_repo",
|
|
return_value=mock_repo,
|
|
),
|
|
patch(
|
|
"cleveragents.application.container.get_container",
|
|
return_value=mock_container,
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.project_context._read_policy",
|
|
return_value=context.m5_saved_policy,
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.project_context._read_acms_config",
|
|
return_value=_default_acms_config(),
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.project_context._load_policy_json",
|
|
return_value=None,
|
|
),
|
|
):
|
|
from cleveragents.cli.commands.project_context import app as pc_app
|
|
|
|
result = context.runner.invoke(pc_app, ["show", "local/m5-proj"])
|
|
context.m5_result = result
|
|
|
|
|
|
@then("the m5 smoke project context show should succeed")
|
|
def step_m5_project_context_show_success(context: Context) -> None:
|
|
assert context.m5_result.exit_code == 0, (
|
|
f"Exit code: {context.m5_result.exit_code}\n{context.m5_result.output}"
|
|
)
|
|
|
|
|
|
@then("the m5 smoke project context output should contain phase views")
|
|
def step_m5_project_context_has_phases(context: Context) -> None:
|
|
output = context.m5_result.output
|
|
assert "default" in output.lower() or "view" in output.lower()
|
|
|
|
|
|
@when("I m5 smoke invoke project context inspect")
|
|
def step_m5_invoke_context_inspect(context: Context) -> None:
|
|
from cleveragents.application.services.context_tiers import ContextTierService
|
|
from cleveragents.cli.commands.project_context import (
|
|
_default_acms_config,
|
|
)
|
|
from cleveragents.cli.commands.project_context import (
|
|
app as pc_app,
|
|
)
|
|
|
|
mock_repo = MagicMock()
|
|
mock_repo.get.return_value = MagicMock()
|
|
|
|
mock_container = MagicMock()
|
|
mock_container.session_factory.return_value = MagicMock()
|
|
mock_container.namespaced_project_repo.return_value = mock_repo
|
|
mock_container.context_tier_service.return_value = ContextTierService()
|
|
|
|
with (
|
|
patch(
|
|
"cleveragents.application.container.get_container",
|
|
return_value=mock_container,
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.project_context._read_policy",
|
|
return_value=ProjectContextPolicy(),
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.project_context._read_acms_config",
|
|
return_value=_default_acms_config(),
|
|
),
|
|
):
|
|
result = context.runner.invoke(pc_app, ["inspect", "local/m5-proj"])
|
|
context.m5_result = result
|
|
|
|
|
|
@when("I m5 smoke invoke project context simulate")
|
|
def step_m5_invoke_context_simulate(context: Context) -> None:
|
|
from cleveragents.application.services.context_tiers import ContextTierService
|
|
from cleveragents.cli.commands.project_context import (
|
|
_default_acms_config,
|
|
)
|
|
from cleveragents.cli.commands.project_context import (
|
|
app as pc_app,
|
|
)
|
|
|
|
mock_repo = MagicMock()
|
|
mock_repo.get.return_value = MagicMock()
|
|
|
|
mock_container = MagicMock()
|
|
mock_container.session_factory.return_value = MagicMock()
|
|
mock_container.namespaced_project_repo.return_value = mock_repo
|
|
mock_container.context_tier_service.return_value = ContextTierService()
|
|
|
|
with (
|
|
patch(
|
|
"cleveragents.application.container.get_container",
|
|
return_value=mock_container,
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.project_context._read_policy",
|
|
return_value=ProjectContextPolicy(),
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.project_context._read_acms_config",
|
|
return_value=_default_acms_config(),
|
|
),
|
|
):
|
|
result = context.runner.invoke(pc_app, ["simulate", "local/m5-proj"])
|
|
context.m5_result = result
|
|
|
|
|
|
@then("the m5 smoke project context inspect should succeed")
|
|
def step_m5_project_context_inspect_success(context: Context) -> None:
|
|
assert context.m5_result.exit_code == 0, (
|
|
f"Exit code: {context.m5_result.exit_code}\n{context.m5_result.output}"
|
|
)
|
|
|
|
|
|
@then("the m5 smoke project context simulate should succeed")
|
|
def step_m5_project_context_simulate_success(context: Context) -> None:
|
|
assert context.m5_result.exit_code == 0, (
|
|
f"Exit code: {context.m5_result.exit_code}\n{context.m5_result.output}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Context analysis agent
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a m5 smoke mocked context analysis agent")
|
|
def step_m5_mocked_analysis_agent(context: Context) -> None:
|
|
fixture_path = _FIXTURES_DIR / "context_analysis_results.json"
|
|
fixture_data = json.loads(fixture_path.read_text(encoding="utf-8"))
|
|
context.m5_mock_analysis = {
|
|
"file_paths": list(fixture_data["dependencies"].keys()),
|
|
"documents": [],
|
|
"dependencies": fixture_data["dependencies"],
|
|
"summary": fixture_data["summary"],
|
|
"relevance_scores": fixture_data["relevance_scores"],
|
|
"chunks": [],
|
|
"error": None,
|
|
}
|
|
|
|
|
|
@when("I m5 smoke invoke context analysis")
|
|
def step_m5_invoke_analysis(context: Context) -> None:
|
|
context.m5_analysis_result = context.m5_mock_analysis
|
|
|
|
|
|
@then("the m5 smoke analysis result should have a non-empty summary")
|
|
def step_m5_analysis_has_nonempty_summary(context: Context) -> None:
|
|
assert context.m5_analysis_result["summary"]
|
|
|
|
|
|
@then("the m5 smoke analysis result should have dependency entries")
|
|
def step_m5_analysis_has_dep_entries(context: Context) -> None:
|
|
assert len(context.m5_analysis_result["dependencies"]) > 0
|
|
|
|
|
|
@then("the m5 smoke analysis result should have relevance scores")
|
|
def step_m5_analysis_has_rel_scores(context: Context) -> None:
|
|
assert len(context.m5_analysis_result["relevance_scores"]) > 0
|
|
|
|
|
|
@then("all m5 smoke relevance scores should be between 0 and 1")
|
|
def step_m5_scores_bounded(context: Context) -> None:
|
|
for path, score in context.m5_analysis_result["relevance_scores"].items():
|
|
assert 0.0 <= score <= 1.0, f"Score for {path} out of range: {score}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Context CLI: multiple resources
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a m5 smoke project with multiple context entries")
|
|
def step_m5_project_with_multiple_entries(context: Context) -> None:
|
|
"""Set up a project with multiple context files for testing."""
|
|
entries = []
|
|
for i, name in enumerate(["src/a.py", "src/b.py", "src/c.py"]):
|
|
mock_entry = MagicMock(spec=ContextModel)
|
|
mock_entry.path = name
|
|
mock_entry.size = 1024 * (i + 1)
|
|
mock_entry.type = "FILE"
|
|
mock_entry.added_at = "2026-03-01T00:00:00Z"
|
|
mock_entry.content = f"# content of {name}"
|
|
entries.append(mock_entry)
|
|
context.mock_context_service.list_context.return_value = entries
|
|
context.mock_context_service.clear_context.return_value = len(entries)
|
|
|
|
|
|
@then("the m5 smoke context list output should contain multiple files")
|
|
def step_m5_context_list_multiple(context: Context) -> None:
|
|
output = context.m5_result.output
|
|
assert "a.py" in output or "3 total" in output, (
|
|
f"Expected multiple files in output, got: {output}"
|
|
)
|
|
|
|
|
|
@given("a m5 smoke project with mocked multi-add context service")
|
|
def step_m5_project_mocked_multi_add(context: Context) -> None:
|
|
context.mock_context_service.add_to_context.return_value = (
|
|
[Path("src/a.py"), Path("src/b.py")],
|
|
[],
|
|
)
|
|
|
|
|
|
@when('I m5 smoke invoke context add with paths "{path1}" and "{path2}"')
|
|
def step_m5_invoke_context_add_multi(context: Context, path1: str, path2: str) -> None:
|
|
container = _mock_container(context.mock_context_service)
|
|
with (
|
|
patch(
|
|
"cleveragents.application.container.get_container",
|
|
return_value=container,
|
|
),
|
|
patch.object(Path, "exists", return_value=True),
|
|
):
|
|
result = context.runner.invoke(context_app, ["add", path1, path2])
|
|
context.m5_result = result
|
|
|
|
|
|
@when("I m5 smoke invoke context show without path")
|
|
def step_m5_invoke_context_show_no_path(context: Context) -> None:
|
|
container = _mock_container(context.mock_context_service)
|
|
with patch(
|
|
"cleveragents.application.container.get_container",
|
|
return_value=container,
|
|
):
|
|
result = context.runner.invoke(context_app, ["show"])
|
|
context.m5_result = result
|
|
|
|
|
|
@then("the m5 smoke context show output should contain an exact summary")
|
|
def step_m5_context_show_summary(context: Context) -> None:
|
|
output = context.m5_result.output
|
|
assert "Context Summary:" in output, f"Expected summary header in output: {output}"
|
|
assert "Total files: 3" in output, f"Expected file count in output: {output}"
|
|
assert "Total size: 6,144 bytes" in output, (
|
|
f"Expected aggregate size in output: {output}"
|
|
)
|
|
assert (
|
|
"Use 'agents context show <file>'" in output
|
|
or "Use 'agents actor context show <file>'" in output
|
|
), f"Expected usage hint in output: {output}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Context CLI: clear and re-add
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when('I m5 smoke re-add context entry "{path}" after clear')
|
|
def step_m5_readd_after_clear(context: Context, path: str) -> None:
|
|
"""Re-add a context entry after clearing, simulating clear+re-add flow."""
|
|
context.mock_context_service.add_to_context.return_value = (
|
|
[Path(path)],
|
|
[],
|
|
)
|
|
container = _mock_container(context.mock_context_service)
|
|
with (
|
|
patch(
|
|
"cleveragents.application.container.get_container",
|
|
return_value=container,
|
|
),
|
|
patch.object(Path, "exists", return_value=True),
|
|
):
|
|
result = context.runner.invoke(context_app, ["add", path])
|
|
context.m5_result = result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Multi-project context
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given('m5 smoke project "{name}" with {count:d} context files')
|
|
def step_m5_project_with_files(context: Context, name: str, count: int) -> None:
|
|
if not hasattr(context, "m5_projects"):
|
|
context.m5_projects = {}
|
|
context.m5_projects[name] = [
|
|
{"path": f"src/file_{i}.py", "size": 1024} for i in range(count)
|
|
]
|
|
|
|
|
|
@then('m5 smoke project "{name}" should have {count:d} context entries')
|
|
def step_m5_project_entry_count(context: Context, name: str, count: int) -> None:
|
|
assert len(context.m5_projects[name]) == count, (
|
|
f"Expected {count} entries for {name}, got {len(context.m5_projects[name])}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Context exclusion patterns
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given('a m5 smoke context view excluding "{pattern}"')
|
|
def step_m5_view_excluding(context: Context, pattern: str) -> None:
|
|
context.m5_view = ContextView(exclude_paths=[pattern])
|
|
context.m5_exclude_pattern = pattern
|
|
|
|
|
|
@then('the m5 smoke path "{path}" should be excluded')
|
|
def step_m5_path_excluded(context: Context, path: str) -> None:
|
|
assert PurePosixPath(path).match(context.m5_exclude_pattern), (
|
|
f"Expected '{path}' to match exclude pattern '{context.m5_exclude_pattern}'"
|
|
)
|
|
|
|
|
|
@then('the m5 smoke path "{path}" should not be excluded')
|
|
def step_m5_path_not_excluded(context: Context, path: str) -> None:
|
|
assert not PurePosixPath(path).match(context.m5_exclude_pattern), (
|
|
f"Expected '{path}' NOT to match exclude pattern"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Budget enforcement (enforce_size_budget)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_fragment(size: int, index: int) -> ContextFragment:
|
|
"""Create a ContextFragment with content of exactly *size* bytes."""
|
|
# ASCII 'x' is 1 byte in UTF-8, so a string of length=size gives size bytes.
|
|
return ContextFragment(
|
|
content="x" * size,
|
|
token_count=max(1, size // 4),
|
|
uko_node=f"uko://test/frag-{index}",
|
|
provenance=FragmentProvenance(
|
|
resource_uri=f"test://res-{index}",
|
|
location=f"file_{index}.py",
|
|
),
|
|
)
|
|
|
|
|
|
@given("m5 smoke fragments of sizes {sizes}")
|
|
def step_m5_fragments_of_sizes(context: Context, sizes: str) -> None:
|
|
size_list = [int(s.strip()) for s in sizes.split()]
|
|
context.m5_fragments = [_make_fragment(s, i) for i, s in enumerate(size_list)]
|
|
|
|
|
|
@given("m5 smoke fragments of sizes")
|
|
def step_m5_fragments_empty(context: Context) -> None:
|
|
context.m5_fragments = []
|
|
|
|
|
|
@given("m5 smoke fragments with unicode content")
|
|
def step_m5_fragments_unicode(context: Context) -> None:
|
|
"""Create two fragments with multi-byte UTF-8 content.
|
|
|
|
Fragment 0: 5 x e-acute = 10 bytes (each is 2 bytes in UTF-8) -- fits in 10.
|
|
Fragment 1: 6 x e-acute = 12 bytes -- exceeds max_file_size of 10.
|
|
"""
|
|
context.m5_fragments = [
|
|
ContextFragment(
|
|
content="é" * 5,
|
|
token_count=5,
|
|
uko_node="uko://test/frag-0",
|
|
provenance=FragmentProvenance(
|
|
resource_uri="test://res-0",
|
|
location="file_0.py",
|
|
),
|
|
),
|
|
ContextFragment(
|
|
content="é" * 6,
|
|
token_count=6,
|
|
uko_node="uko://test/frag-1",
|
|
provenance=FragmentProvenance(
|
|
resource_uri="test://res-1",
|
|
location="file_1.py",
|
|
),
|
|
),
|
|
]
|
|
|
|
|
|
@given("a m5 smoke context view with max_file_size {mfs:d} and max_total_size {mts:d}")
|
|
def step_m5_view_mixed_limits(context: Context, mfs: int, mts: int) -> None:
|
|
context.m5_view = ContextView(max_file_size=mfs, max_total_size=mts)
|
|
|
|
|
|
@when("I m5 smoke enforce the size budget")
|
|
def step_m5_enforce_budget(context: Context) -> None:
|
|
context.m5_enforcement = enforce_size_budget(context.m5_fragments, context.m5_view)
|
|
|
|
|
|
@then("{count:d} m5 smoke fragments should be accepted")
|
|
def step_m5_accepted_count(context: Context, count: int) -> None:
|
|
result: BudgetEnforcementResult = context.m5_enforcement
|
|
assert len(result.accepted) == count, (
|
|
f"Expected {count} accepted, got {len(result.accepted)}"
|
|
)
|
|
|
|
|
|
@then("{count:d} m5 smoke fragments should be violated")
|
|
def step_m5_violation_count(context: Context, count: int) -> None:
|
|
result: BudgetEnforcementResult = context.m5_enforcement
|
|
assert len(result.violations) == count, (
|
|
f"Expected {count} violations, got {len(result.violations)}"
|
|
)
|
|
|
|
|
|
@then('the m5 smoke violation type should be "{vtype}"')
|
|
def step_m5_violation_type(context: Context, vtype: str) -> None:
|
|
result: BudgetEnforcementResult = context.m5_enforcement
|
|
assert any(v.violation_type == vtype for v in result.violations), (
|
|
f"No violation with type {vtype!r} found"
|
|
)
|
|
|
|
|
|
@then('the m5 smoke violation reason should contain "{text}"')
|
|
def step_m5_violation_reason_contains(context: Context, text: str) -> None:
|
|
result: BudgetEnforcementResult = context.m5_enforcement
|
|
assert any(text in v.reason for v in result.violations), (
|
|
f"No violation reason contains {text!r}"
|
|
)
|
|
|
|
|
|
@then("the m5 smoke violation content size should be {size:d}")
|
|
def step_m5_violation_content_size(context: Context, size: int) -> None:
|
|
result: BudgetEnforcementResult = context.m5_enforcement
|
|
assert any(v.content_size == size for v in result.violations), (
|
|
f"No violation with content_size={size}"
|
|
)
|
|
|
|
|
|
@then("the m5 smoke total accepted size should be {size:d}")
|
|
def step_m5_total_size(context: Context, size: int) -> None:
|
|
result: BudgetEnforcementResult = context.m5_enforcement
|
|
assert result.total_size == size, (
|
|
f"Expected total_size={size}, got {result.total_size}"
|
|
)
|
|
|
|
|
|
@then('the m5 smoke violations should include type "{vtype}"')
|
|
def step_m5_violations_include_type(context: Context, vtype: str) -> None:
|
|
result: BudgetEnforcementResult = context.m5_enforcement
|
|
types = {v.violation_type for v in result.violations}
|
|
assert vtype in types, f"Expected violation type {vtype!r} in {types}"
|
|
|
|
|
|
@when("I m5 smoke assemble via pipeline with the context view")
|
|
def step_m5_assemble_pipeline(context: Context) -> None:
|
|
pipeline = ACMSPipeline()
|
|
plan_id = str(ULID())
|
|
budget = ContextBudget(max_tokens=4096)
|
|
payload = pipeline.assemble(
|
|
plan_id=plan_id,
|
|
fragments=context.m5_fragments,
|
|
budget=budget,
|
|
context_view=context.m5_view,
|
|
)
|
|
context.m5_pipeline_payload = payload
|
|
context.m5_pipeline = pipeline
|
|
|
|
|
|
@then("the m5 smoke pipeline should return {count:d} fragments")
|
|
def step_m5_pipeline_fragment_count(context: Context, count: int) -> None:
|
|
assert len(context.m5_pipeline_payload.fragments) == count, (
|
|
f"Expected {count}, got {len(context.m5_pipeline_payload.fragments)}"
|
|
)
|
|
|
|
|
|
@then("the m5 smoke pipeline enforcement result should have {count:d} violation(s)")
|
|
def step_m5_pipeline_enforcement_violations(context: Context, count: int) -> None:
|
|
result = context.m5_pipeline.last_enforcement_result
|
|
assert result is not None, "No enforcement result on pipeline"
|
|
assert len(result.violations) == count, (
|
|
f"Expected {count} violations, got {len(result.violations)}"
|
|
)
|