forked from HAL9000/cleveragents-core
051ee7c290
Added 52 new .feature files and corresponding _steps.py files targeting previously uncovered code paths in the following areas: - TUI layer: app, commands, persona (state/schema/registry), widgets, input (shell_exec, reference_parser) - Application services: plan lifecycle/service/executor, session, project, repo indexing, correction, checkpoint, actor, llm_actors, strategy coordinator, resource file watcher, service retry wiring - CLI commands: session, resource, repl, plan, db, automation_profile - Domain models: retry_policy, resource_type, cost_budget, docker_compose_analyzer, detail_level, _sql_string_aware, _postgresql_helpers - Core: circuit_breaker, retry_service_patterns - Infrastructure: repositories, transaction_sandbox, strategy_registry, plugins/loader, container - Config: settings - Agents: plan_generation, context_analysis, auto_debug - A2A: facade All new tests follow the Behave/Gherkin BDD standard. Resolved step definition collisions with unique prefixes. Fixed Alembic fileConfig logger disabling issue (disable_existing_loggers=False). ISSUES CLOSED: #1068
248 lines
9.5 KiB
Python
248 lines
9.5 KiB
Python
"""Step definitions for tui_reference_parser_coverage.feature.
|
|
|
|
These steps target specific uncovered lines in reference_parser.py:
|
|
- Lines 59-60: ValueError fallback in _catalog file listing
|
|
- Lines 113-114: Escaped \\@ token pass-through
|
|
- Lines 123-126: Category parsing from @category:query syntax
|
|
- Lines 129-135: Typed reference resolves successfully
|
|
- Lines 139-141: Typed reference fails to resolve (unresolved)
|
|
- Lines 160-161: Untyped reference fails across all categories
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from behave import given, then, when
|
|
|
|
from cleveragents.tui.input import reference_parser as rp_module
|
|
from cleveragents.tui.input.reference_parser import (
|
|
_REFERENCE_TYPES,
|
|
_catalog,
|
|
_catalog_cache,
|
|
parse_references,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
@given("the reference parser module is imported")
|
|
def step_reference_parser_imported(context):
|
|
"""Verify the module is importable."""
|
|
assert parse_references is not None
|
|
assert _catalog is not None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers - mock catalog setup
|
|
# ---------------------------------------------------------------------------
|
|
def _empty_catalog():
|
|
"""Return a catalog with empty lists for every category."""
|
|
return {cat: [] for cat in _REFERENCE_TYPES}
|
|
|
|
|
|
def _make_catalog(**overrides):
|
|
"""Return an empty catalog with specific category overrides applied."""
|
|
catalog = _empty_catalog()
|
|
catalog.update(overrides)
|
|
return catalog
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given steps - mock catalog variants
|
|
# ---------------------------------------------------------------------------
|
|
@given("the catalog is mocked with minimal data")
|
|
def step_catalog_mocked_minimal(context):
|
|
"""Prepare a minimal catalog mock that returns no entries."""
|
|
context.catalog_patch = patch.object(
|
|
rp_module, "_catalog", return_value=_empty_catalog()
|
|
)
|
|
context.catalog_patch.start()
|
|
context.add_cleanup(context.catalog_patch.stop)
|
|
|
|
|
|
@given('the catalog is mocked with resource entries "{entry1}" and "{entry2}"')
|
|
def step_catalog_mocked_resources(context, entry1, entry2):
|
|
"""Mock the catalog to contain specific resource entries."""
|
|
catalog = _make_catalog(resource=sorted([entry1, entry2]))
|
|
context.catalog_patch = patch.object(rp_module, "_catalog", return_value=catalog)
|
|
context.catalog_patch.start()
|
|
context.add_cleanup(context.catalog_patch.stop)
|
|
|
|
|
|
@given('the catalog is mocked with plan entries "{entry1}" and "{entry2}"')
|
|
def step_catalog_mocked_plans(context, entry1, entry2):
|
|
"""Mock the catalog to contain specific plan entries."""
|
|
catalog = _make_catalog(plan=sorted([entry1, entry2]))
|
|
context.catalog_patch = patch.object(rp_module, "_catalog", return_value=catalog)
|
|
context.catalog_patch.start()
|
|
context.add_cleanup(context.catalog_patch.stop)
|
|
|
|
|
|
@given('the catalog is mocked with actor entries "{entry1}" and "{entry2}"')
|
|
def step_catalog_mocked_actors(context, entry1, entry2):
|
|
"""Mock the catalog to contain specific actor entries."""
|
|
catalog = _make_catalog(actor=sorted([entry1, entry2]))
|
|
context.catalog_patch = patch.object(rp_module, "_catalog", return_value=catalog)
|
|
context.catalog_patch.start()
|
|
context.add_cleanup(context.catalog_patch.stop)
|
|
|
|
|
|
@given("the catalog is mocked with empty lists for all categories")
|
|
def step_catalog_mocked_empty(context):
|
|
"""Mock the catalog to return empty lists for every category."""
|
|
context.catalog_patch = patch.object(
|
|
rp_module, "_catalog", return_value=_empty_catalog()
|
|
)
|
|
context.catalog_patch.start()
|
|
context.add_cleanup(context.catalog_patch.stop)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given steps - catalog cache / os.walk mocking for lines 59-60
|
|
# ---------------------------------------------------------------------------
|
|
@given("the catalog cache is invalidated")
|
|
def step_invalidate_catalog_cache(context):
|
|
"""Reset the module-level catalog cache so _catalog() rebuilds."""
|
|
context.original_cache = {
|
|
"cwd": _catalog_cache.get("cwd"),
|
|
"created_at": _catalog_cache.get("created_at"),
|
|
"catalog": _catalog_cache.get("catalog"),
|
|
}
|
|
_catalog_cache["cwd"] = None
|
|
_catalog_cache["created_at"] = 0.0
|
|
_catalog_cache["catalog"] = None
|
|
|
|
def restore_cache():
|
|
_catalog_cache["cwd"] = context.original_cache["cwd"]
|
|
_catalog_cache["created_at"] = context.original_cache["created_at"]
|
|
_catalog_cache["catalog"] = context.original_cache["catalog"]
|
|
|
|
context.add_cleanup(restore_cache)
|
|
|
|
|
|
@given("os.walk is mocked to return a path outside the working directory")
|
|
def step_mock_os_walk_outside_cwd(context):
|
|
"""Mock os.walk to yield a directory outside cwd.
|
|
|
|
When Path.relative_to(cwd) is called on a file from an outside dir,
|
|
it raises ValueError, triggering the fallback on lines 59-60.
|
|
"""
|
|
outside_root = "/outside/foreign/dir"
|
|
mock_filenames = ["external_file.txt"]
|
|
|
|
def fake_walk(top, followlinks=False):
|
|
# First yield the real cwd with nothing, then yield the outside path
|
|
yield outside_root, [], mock_filenames
|
|
|
|
context.walk_patch = patch.object(rp_module.os, "walk", side_effect=fake_walk)
|
|
context.walk_patch.start()
|
|
|
|
# Also mock the actors/tools/skills dirs so they don't touch the real FS
|
|
context.is_dir_patch = patch.object(Path, "is_dir", return_value=False)
|
|
context.is_dir_patch.start()
|
|
|
|
def cleanup():
|
|
context.walk_patch.stop()
|
|
context.is_dir_patch.stop()
|
|
|
|
context.add_cleanup(cleanup)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps
|
|
# ---------------------------------------------------------------------------
|
|
@when('I parse references from the line "{line}"')
|
|
def step_parse_references(context, line):
|
|
"""Call parse_references with the given line."""
|
|
context.parse_result = parse_references(line)
|
|
|
|
|
|
@when("I build the catalog")
|
|
def step_build_catalog(context):
|
|
"""Call _catalog() directly to exercise the file-listing code."""
|
|
context.catalog_result = _catalog()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then steps - expanded line
|
|
# ---------------------------------------------------------------------------
|
|
@then('the expanded line should be "{expected}"')
|
|
def step_expanded_line_equals(context, expected):
|
|
"""Verify the expanded line matches exactly."""
|
|
actual = context.parse_result.expanded_line
|
|
assert actual == expected, f"Expected '{expected}', got '{actual}'"
|
|
|
|
|
|
@then('the expanded line should contain "{fragment}"')
|
|
def step_expanded_line_contains(context, fragment):
|
|
"""Verify the expanded line contains a fragment."""
|
|
actual = context.parse_result.expanded_line
|
|
assert fragment in actual, f"Expected '{fragment}' in '{actual}'"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then steps - matches
|
|
# ---------------------------------------------------------------------------
|
|
@then("there should be no resolved matches")
|
|
def step_no_matches(context):
|
|
"""Verify no references were resolved."""
|
|
assert len(context.parse_result.matches) == 0, (
|
|
f"Expected 0 matches, got {context.parse_result.matches}"
|
|
)
|
|
|
|
|
|
@then("there should be {count:d} resolved match")
|
|
def step_match_count(context, count):
|
|
"""Verify the exact number of resolved matches."""
|
|
actual = len(context.parse_result.matches)
|
|
assert actual == count, (
|
|
f"Expected {count} match(es), got {actual}: {context.parse_result.matches}"
|
|
)
|
|
|
|
|
|
@then('the resolved match category should be "{category}"')
|
|
def step_match_category(context, category):
|
|
"""Verify the first resolved match has the expected category."""
|
|
match = context.parse_result.matches[0]
|
|
assert match.category == category, (
|
|
f"Expected category '{category}', got '{match.category}'"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then steps - unresolved tokens
|
|
# ---------------------------------------------------------------------------
|
|
@then("there should be no unresolved tokens")
|
|
def step_no_unresolved(context):
|
|
"""Verify no tokens were left unresolved."""
|
|
assert len(context.parse_result.unresolved_tokens) == 0, (
|
|
f"Expected 0 unresolved, got {context.parse_result.unresolved_tokens}"
|
|
)
|
|
|
|
|
|
@then('"{token}" should be in the unresolved tokens')
|
|
def step_token_unresolved(context, token):
|
|
"""Verify a specific token is listed as unresolved."""
|
|
assert token in context.parse_result.unresolved_tokens, (
|
|
f"Expected '{token}' in unresolved list "
|
|
f"{context.parse_result.unresolved_tokens}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then steps - catalog ValueError fallback (lines 59-60)
|
|
# ---------------------------------------------------------------------------
|
|
@then("the catalog resource list should contain the absolute fallback path")
|
|
def step_catalog_has_absolute_path(context):
|
|
"""Verify that the catalog collected the file using its absolute path.
|
|
|
|
When relative_to(cwd) raises ValueError, the code falls back to
|
|
``(root_path / filename).as_posix()`` -- the absolute path.
|
|
"""
|
|
resources = context.catalog_result.get("resource", [])
|
|
expected = "/outside/foreign/dir/external_file.txt"
|
|
assert expected in resources, (
|
|
f"Expected '{expected}' in resource list, got {resources}"
|
|
)
|