test(acms): add CLI coverage BDD scenarios for acms show and acms clear commands
CI / push-validation (pull_request) Successful in 45s
CI / build (pull_request) Successful in 59s
CI / lint (pull_request) Successful in 1m10s
CI / quality (pull_request) Successful in 1m21s
CI / helm (pull_request) Successful in 1m19s
CI / typecheck (pull_request) Successful in 1m26s
CI / security (pull_request) Successful in 1m48s
CI / unit_tests (pull_request) Failing after 6m28s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / integration_tests (pull_request) Failing after 10m18s
CI / status-check (pull_request) Failing after 3s

Add features/acms_context_cli_coverage.feature and
features/steps/acms_context_cli_coverage_steps.py to exercise the
acms_show and acms_clear typer CLI command functions via CliRunner,
covering all code paths (invalid view, empty service, rich table,
JSON output, truncated flag, invalid tier, no-filter, path/tag/tier
filters, confirmation prompt) that the existing unit-level BDD tests
do not reach. Fixes coverage gate failure.
This commit is contained in:
2026-04-24 13:32:33 +00:00
committed by drew
parent 521a46599f
commit 7e4fa26373
2 changed files with 373 additions and 0 deletions
@@ -0,0 +1,97 @@
Feature: ACMS context CLI command coverage
As a developer maintaining the CleverAgents codebase
I want the acms_show and acms_clear CLI commands to be fully covered
So that the coverage gate passes at 97%
# acms show CLI
Scenario: acms show with invalid view name exits with error
When I invoke acms show CLI with invalid view "badview"
Then the acms CLI exit code should be 1
Scenario: acms show with empty service renders no-entries panel
Given a mocked ACMS tier service with no fragments
When I invoke acms show CLI with view "default" only
Then the acms CLI exit code should be 0
And the acms CLI output should contain "No context entries found"
Scenario: acms show with fragments renders rich table
Given a mocked ACMS tier service with one hot fragment "f1" path "src/a.py" size 100
When I invoke acms show CLI with view "default" only
Then the acms CLI exit code should be 0
And the acms CLI output should contain "src/a.py"
Scenario: acms show with JSON format outputs JSON
Given a mocked ACMS tier service with one hot fragment "f2" path "src/b.py" size 50
When I invoke acms show CLI with view "default" and output format "json"
Then the acms CLI exit code should be 0
And the acms CLI output should contain "entries"
And the acms CLI output should contain "total_tokens"
Scenario: acms show with project scope renders scoped table
Given a mocked ACMS tier service with one hot fragment "f3" path "src/c.py" size 75
When I invoke acms show CLI with view "default" and project scope "local/proj"
Then the acms CLI exit code should be 0
Scenario: acms show with truncated entries shows truncated flag
Given a mocked ACMS tier service with two fragments exceeding budget
When I invoke acms show CLI with view "default" and token budget 80
Then the acms CLI exit code should be 0
And the acms CLI output should contain "truncated"
Scenario: acms show with custom budget renders budget utilization
Given a mocked ACMS tier service with one hot fragment "f4" path "src/d.py" size 400
When I invoke acms show CLI with view "default" and token budget 1000
Then the acms CLI exit code should be 0
And the acms CLI output should contain "400"
# ── acms clear CLI ────────────────────────────────────────────────────────
Scenario: acms clear with invalid tier exits with error
When I invoke acms clear CLI with invalid tier "badtier"
Then the acms CLI exit code should be 1
Scenario: acms clear with no filter shows no-filter message
Given a mocked ACMS tier service with no fragments
When I invoke acms clear CLI with no filter
Then the acms CLI exit code should be 0
And the acms CLI output should contain "No filter specified"
Scenario: acms clear with no filter and JSON format outputs JSON
Given a mocked ACMS tier service with no fragments
When I invoke acms clear CLI with no filter and output format "json"
Then the acms CLI exit code should be 0
And the acms CLI output should contain "removed_count"
Scenario: acms clear with path filter and yes flag removes entries
Given a mocked ACMS tier service with one hot fragment "c1" path "old.py" size 50
When I invoke acms clear CLI with path filter "old.py" and yes flag
Then the acms CLI exit code should be 0
And the acms CLI output should contain "Removed"
Scenario: acms clear with path filter yes flag and JSON format
Given a mocked ACMS tier service with one hot fragment "c2" path "old2.py" size 50
When I invoke acms clear CLI with path filter "old2.py" yes flag and output format "json"
Then the acms CLI exit code should be 0
And the acms CLI output should contain "removed_count"
Scenario: acms clear with tier filter and yes flag removes entries
Given a mocked ACMS tier service with one hot fragment "c3" path "hot.py" size 50
When I invoke acms clear CLI with tier filter "hot" and yes flag
Then the acms CLI exit code should be 0
Scenario: acms clear with tag filter and yes flag removes entries
Given a mocked ACMS tier service with one tagged fragment "c4" path "tagged.py" size 50 tag "stale"
When I invoke acms clear CLI with tag filter "stale" and yes flag
Then the acms CLI exit code should be 0
Scenario: acms clear with path filter confirmed removes entries
Given a mocked ACMS tier service with one hot fragment "c5" path "confirm.py" size 50
When I invoke acms clear CLI with path filter "confirm.py" and confirmation "y"
Then the acms CLI exit code should be 0
Scenario: acms clear with path filter declined leaves entries intact
Given a mocked ACMS tier service with one hot fragment "c6" path "decline.py" size 50
When I invoke acms clear CLI with path filter "decline.py" and confirmation "n"
Then the acms CLI exit code should be 0
And the acms CLI output should contain "Cancelled"
@@ -0,0 +1,276 @@
"""Step definitions for ACMS context CLI command coverage tests.
Uses typer.testing.CliRunner with a mocked container to exercise the
acms_show and acms_clear CLI command functions directly, covering all
code paths that the unit-level BDD tests (which call internal helpers)
do not reach.
"""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock, patch
from behave import given, then, when
from typer.testing import CliRunner
from cleveragents.application.services.context_tiers import ContextTierService
from cleveragents.cli.commands.acms_context import app as acms_app
from cleveragents.domain.models.acms.tiers import ContextTier, TieredFragment
__all__: list[str] = []
# The get_container function is imported lazily inside the CLI command bodies,
# so we patch it at the source module level.
_PATCH_TARGET = "cleveragents.application.container.get_container"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_fragment(
fid: str,
path: str,
size: int,
tier: ContextTier = ContextTier.HOT,
metadata: dict[str, Any] | None = None,
) -> TieredFragment:
return TieredFragment(
fragment_id=fid,
content=f"content of {path}",
tier=tier,
resource_id=path,
project_name="local/proj",
token_count=size,
metadata=metadata or {},
)
def _mock_container(fragments: list[TieredFragment], removed: int = 0) -> MagicMock:
"""Build a mock container whose context_tier_service() returns a mock service."""
mock_service = MagicMock(spec=ContextTierService)
mock_service.get_all_fragments.return_value = list(fragments)
mock_service.get_scoped_view.return_value = list(fragments)
mock_service.remove_by_filter.return_value = removed
mock_cont = MagicMock()
mock_cont.context_tier_service.return_value = mock_service
return mock_cont
# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------
@given("a mocked ACMS tier service with no fragments")
def step_given_mock_no_fragments(context: Any) -> None:
context.mock_fragments: list[TieredFragment] = []
context.mock_removed: int = 0
@given(
'a mocked ACMS tier service with one hot fragment "{fid}" path "{path}" size {size:d}'
)
def step_given_mock_one_hot_fragment(
context: Any, fid: str, path: str, size: int
) -> None:
context.mock_fragments = [_make_fragment(fid, path, size, ContextTier.HOT)]
context.mock_removed = 1
@given(
'a mocked ACMS tier service with one tagged fragment "{fid}" path "{path}" size {size:d} tag "{tag}"'
)
def step_given_mock_one_tagged_fragment(
context: Any, fid: str, path: str, size: int, tag: str
) -> None:
context.mock_fragments = [
_make_fragment(fid, path, size, ContextTier.HOT, metadata={tag: True})
]
context.mock_removed = 1
@given("a mocked ACMS tier service with two fragments exceeding budget")
def step_given_mock_two_fragments_exceeding_budget(context: Any) -> None:
context.mock_fragments = [
_make_fragment("big-1", "big1.py", 60, ContextTier.HOT),
_make_fragment("big-2", "big2.py", 60, ContextTier.HOT),
]
context.mock_removed = 0
# ---------------------------------------------------------------------------
# When steps — acms show
# ---------------------------------------------------------------------------
@when('I invoke acms show CLI with invalid view "{view}"')
def step_when_acms_show_invalid_view(context: Any, view: str) -> None:
mock_cont = _mock_container([])
with patch(_PATCH_TARGET, return_value=mock_cont):
runner = CliRunner()
result = runner.invoke(acms_app, ["show", view])
context.cli_result = result
@when('I invoke acms show CLI with view "{view}" only')
def step_when_acms_show_view_only(context: Any, view: str) -> None:
fragments = getattr(context, "mock_fragments", [])
removed = getattr(context, "mock_removed", 0)
mock_cont = _mock_container(fragments, removed)
with patch(_PATCH_TARGET, return_value=mock_cont):
runner = CliRunner()
result = runner.invoke(acms_app, ["show", view])
context.cli_result = result
@when('I invoke acms show CLI with view "{view}" and output format "{fmt}"')
def step_when_acms_show_view_format(context: Any, view: str, fmt: str) -> None:
fragments = getattr(context, "mock_fragments", [])
removed = getattr(context, "mock_removed", 0)
mock_cont = _mock_container(fragments, removed)
with patch(_PATCH_TARGET, return_value=mock_cont):
runner = CliRunner()
result = runner.invoke(acms_app, ["show", view, "--format", fmt])
context.cli_result = result
@when('I invoke acms show CLI with view "{view}" and project scope "{proj}"')
def step_when_acms_show_view_project(context: Any, view: str, proj: str) -> None:
fragments = getattr(context, "mock_fragments", [])
removed = getattr(context, "mock_removed", 0)
mock_cont = _mock_container(fragments, removed)
with patch(_PATCH_TARGET, return_value=mock_cont):
runner = CliRunner()
result = runner.invoke(acms_app, ["show", view, "--project", proj])
context.cli_result = result
@when('I invoke acms show CLI with view "{view}" and token budget {budget:d}')
def step_when_acms_show_view_budget(context: Any, view: str, budget: int) -> None:
fragments = getattr(context, "mock_fragments", [])
removed = getattr(context, "mock_removed", 0)
mock_cont = _mock_container(fragments, removed)
with patch(_PATCH_TARGET, return_value=mock_cont):
runner = CliRunner()
result = runner.invoke(acms_app, ["show", view, "--budget", str(budget)])
context.cli_result = result
# ---------------------------------------------------------------------------
# When steps — acms clear
# ---------------------------------------------------------------------------
@when('I invoke acms clear CLI with invalid tier "{tier}"')
def step_when_acms_clear_invalid_tier(context: Any, tier: str) -> None:
mock_cont = _mock_container([])
with patch(_PATCH_TARGET, return_value=mock_cont):
runner = CliRunner()
result = runner.invoke(acms_app, ["clear", "--tier", tier])
context.cli_result = result
@when("I invoke acms clear CLI with no filter")
def step_when_acms_clear_no_filter(context: Any) -> None:
fragments = getattr(context, "mock_fragments", [])
removed = getattr(context, "mock_removed", 0)
mock_cont = _mock_container(fragments, removed)
with patch(_PATCH_TARGET, return_value=mock_cont):
runner = CliRunner()
result = runner.invoke(acms_app, ["clear"])
context.cli_result = result
@when('I invoke acms clear CLI with no filter and output format "{fmt}"')
def step_when_acms_clear_no_filter_format(context: Any, fmt: str) -> None:
fragments = getattr(context, "mock_fragments", [])
removed = getattr(context, "mock_removed", 0)
mock_cont = _mock_container(fragments, removed)
with patch(_PATCH_TARGET, return_value=mock_cont):
runner = CliRunner()
result = runner.invoke(acms_app, ["clear", "--format", fmt])
context.cli_result = result
@when('I invoke acms clear CLI with path filter "{path}" and yes flag')
def step_when_acms_clear_path_yes(context: Any, path: str) -> None:
fragments = getattr(context, "mock_fragments", [])
removed = getattr(context, "mock_removed", 0)
mock_cont = _mock_container(fragments, removed)
with patch(_PATCH_TARGET, return_value=mock_cont):
runner = CliRunner()
result = runner.invoke(acms_app, ["clear", "--path", path, "--yes"])
context.cli_result = result
@when(
'I invoke acms clear CLI with path filter "{path}" yes flag and output format "{fmt}"'
)
def step_when_acms_clear_path_yes_format(context: Any, path: str, fmt: str) -> None:
fragments = getattr(context, "mock_fragments", [])
removed = getattr(context, "mock_removed", 0)
mock_cont = _mock_container(fragments, removed)
with patch(_PATCH_TARGET, return_value=mock_cont):
runner = CliRunner()
result = runner.invoke(
acms_app, ["clear", "--path", path, "--yes", "--format", fmt]
)
context.cli_result = result
@when('I invoke acms clear CLI with tier filter "{tier}" and yes flag')
def step_when_acms_clear_tier_yes(context: Any, tier: str) -> None:
fragments = getattr(context, "mock_fragments", [])
removed = getattr(context, "mock_removed", 0)
mock_cont = _mock_container(fragments, removed)
with patch(_PATCH_TARGET, return_value=mock_cont):
runner = CliRunner()
result = runner.invoke(acms_app, ["clear", "--tier", tier, "--yes"])
context.cli_result = result
@when('I invoke acms clear CLI with tag filter "{tag}" and yes flag')
def step_when_acms_clear_tag_yes(context: Any, tag: str) -> None:
fragments = getattr(context, "mock_fragments", [])
removed = getattr(context, "mock_removed", 0)
mock_cont = _mock_container(fragments, removed)
with patch(_PATCH_TARGET, return_value=mock_cont):
runner = CliRunner()
result = runner.invoke(acms_app, ["clear", "--tag", tag, "--yes"])
context.cli_result = result
@when('I invoke acms clear CLI with path filter "{path}" and confirmation "{answer}"')
def step_when_acms_clear_path_confirm(context: Any, path: str, answer: str) -> None:
fragments = getattr(context, "mock_fragments", [])
removed = getattr(context, "mock_removed", 0)
mock_cont = _mock_container(fragments, removed)
with patch(_PATCH_TARGET, return_value=mock_cont):
runner = CliRunner()
result = runner.invoke(acms_app, ["clear", "--path", path], input=f"{answer}\n")
context.cli_result = result
# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------
@then("the acms CLI exit code should be {code:d}")
def step_then_acms_exit_code(context: Any, code: int) -> None:
result = context.cli_result
assert result.exit_code == code, (
f"Expected exit code {code}, got {result.exit_code}.\n"
f"Output: {result.output!r}\n"
f"Exception: {result.exception}"
)
@then('the acms CLI output should contain "{text}"')
def step_then_acms_output_contains(context: Any, text: str) -> None:
result = context.cli_result
assert text in result.output, f"Expected {text!r} in output, got: {result.output!r}"