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
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:
@@ -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}"
|
||||
Reference in New Issue
Block a user