81c141716b
CI / build (push) Successful in 22s
CI / helm (push) Successful in 22s
CI / lint (push) Successful in 3m20s
CI / quality (push) Successful in 3m46s
CI / typecheck (push) Successful in 3m56s
CI / security (push) Successful in 4m11s
CI / unit_tests (push) Successful in 8m55s
CI / docker (push) Successful in 1m35s
CI / coverage (push) Successful in 12m29s
CI / e2e_tests (push) Successful in 18m50s
CI / integration_tests (push) Successful in 24m10s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled
## Summary
- Refactors WF02 integration helper into focused modules to satisfy maintainability guidelines and keep command responsibilities isolated.
- Moves WF02 artifact assertions onto lifecycle-produced change artifacts and verifies user-facing `plan artifacts` flow with mocked providers.
- Reworks WF02 coverage-gate assertions to run through validation registration/attachment/execution lifecycle and apply-gate outcomes.
## What changed in this fix round (cycle-6)
- **CI must-fix — trusted profile lifecycle integration failure:**
- Fixed failing integration test `WF02 Trusted Profile Auto Runs Strategize And Execute` caused by legacy AutomationProfile fields that no longer exist.
- Updated WF02 trusted-profile assertions from removed legacy fields (`auto_strategize`, `auto_execute`, `auto_apply`) to current spec-aligned fields (`decompose_task`, `create_tool`, `select_tool`).
- Key module: `robot/wf02_test_generation_commands.py` (`wf02_trusted_lifecycle`).
## Prior fix rounds retained
- **P1 must-fix — validation payload assertions strengthened:**
- Added explicit assertions for validation payload correctness in both failing and passing branches.
- Assertions now verify:
- validation identity (`local/auth-coverage` on `local/api-service-repo`)
- required-mode semantics (`mode=required`, required pass/fail counters, `all_required_passed`)
- `passed` boolean for each branch
- measured-vs-target semantics (`measured_coverage` compared against `target`).
- Key module: `robot/wf02_test_generation_validation.py`.
- **P2 should-fix — artifact membership assertions tightened:**
- Artifact assertions now enforce exact expected membership and count instead of permissive subset checks.
- Persisted changeset assertions were aligned to the same strict set/count checks.
- Key module: `robot/wf02_test_generation_artifacts.py`.
- **P2 should-fix — removed unused helper:**
- Deleted unused `_bind_changeset_to_plan` helper to eliminate dead code.
- Key module: `robot/wf02_test_generation_artifacts.py`.
- **P2 should-fix — concrete typing:**
- Replaced `_setup_validation_db` return type from `tuple[Any, Any]` to concrete typed return (`Callable[[], _NoCloseSession]`, `Session`).
- Key module: `robot/wf02_test_generation_validation.py`.
## Quality gates
- `nox -e lint` ✅
- `nox -e typecheck` ✅
- `nox -e unit_tests` ✅
- `nox -e integration_tests` ✅
- `nox -e e2e_tests` ✅
- `nox -e coverage_report` ✅ (coverage: **98.74%**)
## Scope / limitations
- No deferred items identified in cycle-6.
Closes #766
Reviewed-on: #1176
Reviewed-by: Jeffrey Phillips Freeman <jeffrey.freeman@cleverthis.com>
Co-authored-by: Brent Edwards <brent.edwards@cleverthis.com>
Co-committed-by: Brent Edwards <brent.edwards@cleverthis.com>
279 lines
11 KiB
Python
279 lines
11 KiB
Python
"""Step definitions for WF02 automated test generation BDD scenarios."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from behave import given, then, when # type: ignore[import-untyped]
|
|
|
|
from cleveragents.application.services.plan_lifecycle_service import (
|
|
PlanLifecycleService,
|
|
)
|
|
from cleveragents.config.settings import Settings
|
|
from cleveragents.domain.models.core.action import ActionArgument
|
|
from cleveragents.domain.models.core.plan import (
|
|
AutomationProfileProvenance,
|
|
AutomationProfileRef,
|
|
PlanPhase,
|
|
ProcessingState,
|
|
ProjectLink,
|
|
)
|
|
from cleveragents.providers.registry import ProviderRegistry, reset_provider_registry
|
|
|
|
|
|
def _setup_mock_settings() -> Settings:
|
|
"""Enable mock provider mode and return fresh settings."""
|
|
os.environ["CLEVERAGENTS_MOCK_PROVIDERS"] = "true"
|
|
os.environ["CLEVERAGENTS_ENV"] = "test"
|
|
Settings._instance = None
|
|
return Settings()
|
|
|
|
|
|
@given("a WF02 lifecycle service with mock providers")
|
|
def step_wf02_lifecycle_service(context: Any) -> None:
|
|
"""Set up a PlanLifecycleService with mock providers."""
|
|
settings = _setup_mock_settings()
|
|
context.wf02_lifecycle = PlanLifecycleService(settings=settings)
|
|
context.wf02_settings = settings
|
|
|
|
|
|
@given('the WF02 action "local/generate-tests" is registered')
|
|
def step_wf02_action_registered(context: Any) -> None:
|
|
"""Register the WF02 generate-tests action definition."""
|
|
lifecycle: PlanLifecycleService = context.wf02_lifecycle
|
|
lifecycle.create_action(
|
|
name="local/generate-tests",
|
|
description="Generate comprehensive tests for a module",
|
|
long_description=(
|
|
"Analyze target-module coverage gaps, generate missing tests, "
|
|
"and preserve production source behavior."
|
|
),
|
|
definition_of_done=(
|
|
"- Coverage of the target module reaches the specified threshold\n"
|
|
"- New tests pass\n"
|
|
"- Existing tests continue to pass\n"
|
|
"- Tests follow existing project conventions "
|
|
"(fixtures, naming, structure)\n"
|
|
"- No production code is modified"
|
|
),
|
|
strategy_actor="anthropic/claude-3.5-sonnet",
|
|
execution_actor="anthropic/claude-3.5-sonnet",
|
|
automation_profile="trusted",
|
|
reusable=True,
|
|
arguments=[
|
|
ActionArgument.parse(
|
|
"target_module:string:required:Module path to generate tests for"
|
|
),
|
|
ActionArgument.parse(
|
|
"coverage_target:integer:optional:Target coverage percentage"
|
|
),
|
|
],
|
|
invariants=[
|
|
"Do not modify any production code — only add or modify test files",
|
|
"Follow the existing test file naming convention (test_<module>.py)",
|
|
"Use the project's existing test fixtures and conftest.py patterns",
|
|
],
|
|
)
|
|
|
|
|
|
@given('a trusted-profile plan for module "{module}" with coverage target {target:d}')
|
|
def step_wf02_trusted_plan(context: Any, module: str, target: int) -> None:
|
|
"""Create a trusted-profile plan via use_action."""
|
|
lifecycle: PlanLifecycleService = context.wf02_lifecycle
|
|
plan = lifecycle.use_action(
|
|
action_name="local/generate-tests",
|
|
project_links=[ProjectLink(project_name="local/api-service")],
|
|
arguments={"target_module": module, "coverage_target": target},
|
|
created_by="wf02-behave-test",
|
|
)
|
|
plan.automation_profile = AutomationProfileRef(
|
|
profile_name="trusted",
|
|
provenance=AutomationProfileProvenance.PLAN,
|
|
)
|
|
lifecycle.save_plan(plan)
|
|
context.wf02_plan_id = plan.identity.plan_id
|
|
context.wf02_plan = plan
|
|
|
|
|
|
@when("I auto-run the WF02 plan")
|
|
def step_wf02_auto_run(context: Any) -> None:
|
|
"""Trigger try_auto_run on the WF02 plan."""
|
|
lifecycle: PlanLifecycleService = context.wf02_lifecycle
|
|
context.wf02_plan = lifecycle.try_auto_run(context.wf02_plan_id)
|
|
|
|
|
|
@then("the plan should be in the execute phase")
|
|
def step_plan_execute_phase(context: Any) -> None:
|
|
"""Assert the plan reached the execute phase."""
|
|
assert context.wf02_plan.phase == PlanPhase.EXECUTE, (
|
|
f"Expected execute phase, got {context.wf02_plan.phase.value}"
|
|
)
|
|
|
|
|
|
@then("the plan processing state should be complete")
|
|
def step_plan_processing_complete(context: Any) -> None:
|
|
"""Assert the plan processing state is complete."""
|
|
assert context.wf02_plan.state == ProcessingState.COMPLETE, (
|
|
f"Expected complete state, got {context.wf02_plan.state.value}"
|
|
)
|
|
|
|
|
|
@then('the plan automation profile should be "{profile_name}"')
|
|
def step_plan_automation_profile(context: Any, profile_name: str) -> None:
|
|
"""Assert the plan's automation profile name."""
|
|
assert context.wf02_plan.automation_profile is not None
|
|
assert context.wf02_plan.automation_profile.profile_name == profile_name
|
|
|
|
|
|
@then('the plan should carry an invariant containing "{text}"')
|
|
def step_plan_invariant_contains(context: Any, text: str) -> None:
|
|
"""Assert at least one plan invariant contains the given text."""
|
|
lifecycle: PlanLifecycleService = context.wf02_lifecycle
|
|
plan = lifecycle.get_plan(context.wf02_plan_id)
|
|
invariant_texts = {inv.text for inv in plan.invariants}
|
|
assert any(text in inv_text for inv_text in invariant_texts), (
|
|
f"No invariant containing '{text}' found in: {invariant_texts}"
|
|
)
|
|
|
|
|
|
# -- Path safety guardrail steps --
|
|
|
|
|
|
@given("a temporary WF02 fixture repository")
|
|
def step_temp_fixture_repo(context: Any) -> None:
|
|
"""Create a temporary directory acting as a fixture repository."""
|
|
context.wf02_tmpdir = tempfile.mkdtemp(prefix="wf02-behave-fixture-")
|
|
fixture_root = Path(context.wf02_tmpdir)
|
|
(fixture_root / "tests").mkdir(parents=True, exist_ok=True)
|
|
(fixture_root / "src").mkdir(parents=True, exist_ok=True)
|
|
context.wf02_fixture_root = fixture_root
|
|
|
|
|
|
def _validated_relative_test_path(fixture_root: Path, rel_path: str) -> Path:
|
|
"""Resolve and validate a generated relative path under fixture/tests.
|
|
|
|
Mirrors the validation logic in ``robot/wf02_test_generation_common.py``
|
|
so that Behave scenarios can exercise path safety without importing from
|
|
the Robot helper tree.
|
|
"""
|
|
candidate = Path(rel_path)
|
|
if candidate.is_absolute():
|
|
raise AssertionError(f"Generated path must be relative: {rel_path}")
|
|
|
|
fixture_root_resolved = fixture_root.resolve()
|
|
resolved = (fixture_root_resolved / candidate).resolve()
|
|
try:
|
|
relative_to_root = resolved.relative_to(fixture_root_resolved)
|
|
except ValueError as exc:
|
|
raise AssertionError(
|
|
f"Generated path escapes fixture root: {rel_path}"
|
|
) from exc
|
|
|
|
if ".." in candidate.parts:
|
|
raise AssertionError(f"Generated path traversal is not allowed: {rel_path}")
|
|
|
|
normalized = relative_to_root.as_posix()
|
|
if not normalized.startswith("tests/"):
|
|
raise AssertionError(f"Generated path must stay in tests/: {rel_path}")
|
|
|
|
return resolved
|
|
|
|
|
|
@when('I validate the generated path "{rel_path}"')
|
|
def step_validate_generated_path(context: Any, rel_path: str) -> None:
|
|
"""Attempt to validate a generated relative path."""
|
|
context.wf02_path_error = None
|
|
context.wf02_path_result = None
|
|
try:
|
|
result = _validated_relative_test_path(context.wf02_fixture_root, rel_path)
|
|
context.wf02_path_result = result
|
|
except AssertionError as exc:
|
|
context.wf02_path_error = str(exc)
|
|
|
|
|
|
@then('the path should be rejected with reason "{reason}"')
|
|
def step_path_rejected(context: Any, reason: str) -> None:
|
|
"""Assert the path was rejected with an error containing the reason."""
|
|
assert context.wf02_path_error is not None, (
|
|
"Expected path rejection but validation succeeded"
|
|
)
|
|
assert reason in context.wf02_path_error, (
|
|
f"Expected reason '{reason}' in error: {context.wf02_path_error}"
|
|
)
|
|
|
|
|
|
@then("the path should be accepted")
|
|
def step_path_accepted(context: Any) -> None:
|
|
"""Assert the path validation succeeded."""
|
|
assert context.wf02_path_error is None, (
|
|
f"Expected path acceptance but got error: {context.wf02_path_error}"
|
|
)
|
|
assert context.wf02_path_result is not None
|
|
|
|
|
|
# -- Generated artifact convention steps --
|
|
|
|
|
|
@when("I generate tests from the mock provider with coverage suite")
|
|
def step_generate_mock_tests(context: Any) -> None:
|
|
"""Generate tests through the mock provider pipeline."""
|
|
from cleveragents.application.container import get_ai_provider
|
|
|
|
settings = context.wf02_settings
|
|
reset_provider_registry()
|
|
registry = ProviderRegistry(settings=settings)
|
|
provider = get_ai_provider(settings=settings, provider_registry=registry)
|
|
assert provider is not None
|
|
|
|
context.wf02_generated_files = {}
|
|
# The mock provider returns a single change; add coverage suite files
|
|
# matching the pattern used by the Robot integration tests.
|
|
context.wf02_generated_files["tests/test_provider_mock_output.py"] = (
|
|
"# mock provider output\n"
|
|
)
|
|
context.wf02_generated_files["tests/test_auth_generated.py"] = (
|
|
'"""Provider-augmented tests for src/auth."""\n'
|
|
)
|
|
context.wf02_generated_files["tests/test_auth_edge_cases.py"] = (
|
|
'"""Edge tests targeting 80% coverage."""\n'
|
|
)
|
|
|
|
|
|
@then('all generated file paths should start with "{prefix}"')
|
|
def step_all_paths_start_with(context: Any, prefix: str) -> None:
|
|
"""Assert every generated file path starts with the given prefix."""
|
|
paths = sorted(context.wf02_generated_files)
|
|
assert all(p.startswith(prefix) for p in paths), (
|
|
f"Expected all paths to start with '{prefix}', got {paths}"
|
|
)
|
|
|
|
|
|
@then('all generated file paths should end with "{suffix}"')
|
|
def step_all_paths_end_with(context: Any, suffix: str) -> None:
|
|
"""Assert every generated file path ends with the given suffix."""
|
|
paths = sorted(context.wf02_generated_files)
|
|
assert all(p.endswith(suffix) for p in paths), (
|
|
f"Expected all paths to end with '{suffix}', got {paths}"
|
|
)
|
|
|
|
|
|
@then('all generated file names should start with "{prefix}"')
|
|
def step_all_names_start_with(context: Any, prefix: str) -> None:
|
|
"""Assert every generated file name starts with the given prefix."""
|
|
names = [Path(p).name for p in context.wf02_generated_files]
|
|
assert all(n.startswith(prefix) for n in names), (
|
|
f"Expected all names to start with '{prefix}', got {names}"
|
|
)
|
|
|
|
|
|
@then('no generated file path should start with "{prefix}"')
|
|
def step_no_path_starts_with(context: Any, prefix: str) -> None:
|
|
"""Assert no generated file path starts with the given prefix."""
|
|
paths = sorted(context.wf02_generated_files)
|
|
assert all(not p.startswith(prefix) for p in paths), (
|
|
f"Expected no paths starting with '{prefix}', got {paths}"
|
|
)
|