Files
cleveragents-core/robot/wf02_test_generation_validation.py
brent.edwards 81c141716b test(integration): workflow example 2 — automated test generation for a module (trusted profile) (#1176)
## 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: cleveragents/cleveragents-core#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>
2026-04-01 03:30:00 +00:00

453 lines
16 KiB
Python

"""WF02 coverage validation lifecycle assertions."""
from __future__ import annotations
import importlib
import importlib.util
import inspect
import json
import sys
import trace
import traceback
import uuid
from collections.abc import Callable
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from wf02_test_generation_common import (
auth_module_fixture,
create_trusted_plan,
create_wf02_action,
generate_tests_from_mock_provider,
setup_lifecycle,
write_generated_files_safely,
)
from wf02_test_generation_lifecycle import (
record_files_via_execution_context,
)
from cleveragents.application.services.plan_apply_service import (
ApplyOutcome,
PlanApplyService,
)
from cleveragents.application.services.resource_registry_service import (
ResourceRegistryService,
)
from cleveragents.application.services.tool_registry_service import ToolRegistryService
from cleveragents.application.services.validation_pipeline import (
ValidationCommand,
ValidationPipeline,
)
from cleveragents.domain.models.core.change import InMemoryChangeSetStore
from cleveragents.domain.models.core.tool import ValidationMode
from cleveragents.infrastructure.database.models import Base
from cleveragents.infrastructure.database.repositories import (
ToolRegistryRepository,
ValidationAttachmentRepository,
)
class _NoCloseSession:
"""Thin proxy that delegates every attribute to a real SQLAlchemy Session
but suppresses ``close()``.
Repository helpers call ``session.close()`` after each operation. In
integration tests we share a single in-memory SQLite session across
multiple repositories so that all writes are visible without a second
connection. Closing the session would invalidate the connection and
lose all in-memory data.
``object.__setattr__`` / ``object.__getattribute__`` are used in the
dunder overrides to access this wrapper's own ``_s`` slot without
triggering the proxy's ``__getattr__`` / ``__setattr__``, which would
recurse into the wrapped session instead.
"""
def __init__(self, session: object) -> None:
object.__setattr__(self, "_s", session)
def close(self) -> None:
pass
def __setattr__(self, name: str, value: object) -> None:
setattr(object.__getattribute__(self, "_s"), name, value)
def __getattr__(self, name: str) -> object:
return getattr(object.__getattribute__(self, "_s"), name)
def _setup_validation_db() -> tuple[Callable[[], _NoCloseSession], Session]:
"""Create in-memory DB and return (session_factory, session)."""
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(engine)
session = sessionmaker(bind=engine, expire_on_commit=False)()
wrapper = _NoCloseSession(session)
def factory() -> _NoCloseSession:
return wrapper
return factory, session
def _assert_validation_payload(
validation_summary: dict[str, Any],
*,
expected_passed: bool,
target: int,
) -> None:
"""Assert validation summary payload semantics for WF02 coverage gate."""
assert validation_summary["total"] == 1
assert validation_summary["informational_passed"] == 0
assert validation_summary["informational_failed"] == 0
if expected_passed:
assert validation_summary["required_passed"] == 1
assert validation_summary["required_failed"] == 0
assert validation_summary["all_required_passed"] is True
else:
assert validation_summary["required_passed"] == 0
assert validation_summary["required_failed"] == 1
assert validation_summary["all_required_passed"] is False
results = validation_summary.get("results")
assert isinstance(results, list) and len(results) == 1
result = results[0]
assert result["validation_name"] == "local/auth-coverage"
assert result["resource_name"] == "local/api-service-repo"
assert result["mode"] == "required"
assert result["passed"] is expected_passed
data = result.get("data")
assert isinstance(data, dict)
measured = data.get("measured_coverage")
assert isinstance(measured, float)
assert data.get("target") == target
if expected_passed:
assert measured >= float(target)
else:
assert measured < float(target)
def _measure_auth_coverage_with_trace(
fixture_root: Path,
) -> tuple[float, str, str, int]:
"""Measure auth.py line coverage by tracing test function execution."""
import os
from contextlib import redirect_stderr, redirect_stdout
from io import StringIO
target_file = (fixture_root / "src" / "auth.py").resolve()
source_lines = target_file.read_text(encoding="utf-8").splitlines()
executable_lines = {
line_no
for line_no, text in enumerate(source_lines, start=1)
if text.strip() and not text.strip().startswith("#")
}
if not executable_lines:
return 0.0, "", "No executable lines found in auth.py", 3
tracer = trace.Trace(count=True, trace=False)
old_cwd = Path.cwd()
old_sys_path = list(sys.path)
stdout_capture = StringIO()
stderr_capture = StringIO()
try:
os.chdir(fixture_root)
sys.path.insert(0, str(fixture_root))
importlib.invalidate_caches()
with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
for test_file in sorted((fixture_root / "tests").glob("test_*.py")):
if test_file.suffix != ".py":
continue
if not test_file.name.startswith("test_auth"):
continue
module_name = f"_wf02_test_{test_file.stem}_{uuid.uuid4().hex}"
spec = importlib.util.spec_from_file_location(module_name, test_file)
if spec is None or spec.loader is None:
return (
0.0,
stdout_capture.getvalue(),
f"Unable to load {test_file}",
4,
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
fixture_values: dict[str, object] = {
"auth_credentials": ("admin", "secret"),
}
for attr_name in dir(module):
if not attr_name.startswith("test_"):
continue
maybe_test = getattr(module, attr_name)
if not callable(maybe_test):
continue
signature = inspect.signature(maybe_test)
kwargs: dict[str, object] = {}
for param_name, param in signature.parameters.items():
if param.kind not in (
inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.KEYWORD_ONLY,
):
raise AssertionError(
f"Unsupported test function parameter kind for "
f"{attr_name}: {param.kind}"
)
if param_name in fixture_values:
kwargs[param_name] = fixture_values[param_name]
continue
if param.default is inspect.Parameter.empty:
raise AssertionError(
"Unsupported required test fixture parameter "
f"'{param_name}' in {attr_name}"
)
tracer.runfunc(maybe_test, **kwargs)
for loaded_name in list(sys.modules):
if loaded_name == module_name or loaded_name.startswith(
"_wf02_test_"
):
sys.modules.pop(loaded_name, None)
for cached_name in ("src.auth", "src"):
sys.modules.pop(cached_name, None)
importlib.invalidate_caches()
except Exception:
return 0.0, stdout_capture.getvalue(), traceback.format_exc(), 1
finally:
sys.path = old_sys_path
os.chdir(old_cwd)
counts = tracer.results().counts
covered_lines = {
lineno
for (filename, lineno), count in counts.items()
if count > 0
and Path(filename).name == "auth.py"
and "src" in Path(filename).parts
}
measured_pct = (
len(covered_lines & executable_lines) / len(executable_lines)
) * 100.0
return measured_pct, stdout_capture.getvalue(), stderr_capture.getvalue(), 0
def _register_attach_and_run_validation(
fixture_root: Path,
plan_id: str,
target: int,
) -> dict[str, Any]:
"""Run validation through registration/attachment/execution lifecycle."""
session_factory, _ = _setup_validation_db()
tool_repo = ToolRegistryRepository(session_factory=session_factory)
attachment_repo = ValidationAttachmentRepository(session_factory=session_factory)
tool_service = ToolRegistryService(
tool_repo=tool_repo,
attachment_repo=attachment_repo,
)
now = datetime.now(tz=UTC).isoformat()
tool_service.register_tool(
{
"name": "local/auth-coverage",
"description": "Auth module coverage gate",
"tool_type": "validation",
"source": "builtin",
"timeout": 300,
"created_at": now,
"updated_at": now,
"resource_bindings": [],
"mode": "required",
}
)
resource_service = ResourceRegistryService(session_factory=session_factory)
resource_service.bootstrap_builtin_types()
registered_resource = resource_service.register_resource(
type_name="git-checkout",
name="local/api-service-repo",
location=str(fixture_root),
description="WF02 coverage fixture repository",
)
resource_id = registered_resource.resource_id
tool_service.attach_validation(
validation_name="local/auth-coverage",
resource_id=resource_id,
mode="required",
plan_id=plan_id,
args={"target": target},
)
attachments = tool_service.list_validations_for_resource(
resource_id=resource_id,
plan_id=plan_id,
)
assert attachments, "Expected at least one attached validation"
commands: list[ValidationCommand] = []
for attachment in attachments:
args_json = attachment.get("args_json")
args = json.loads(args_json) if isinstance(args_json, str) and args_json else {}
mode_text = str(attachment.get("mode", "required"))
mode = (
ValidationMode.REQUIRED
if mode_text == "required"
else ValidationMode.INFORMATIONAL
)
commands.append(
ValidationCommand(
validation_name=str(attachment["validation_name"]),
resource_id=str(attachment["resource_id"]),
resource_name="local/api-service-repo",
mode=mode,
arguments=args,
timeout_seconds=30.0,
)
)
def executor(
validation_name: str,
arguments: dict[str, object],
) -> dict[str, object]:
target_value = arguments.get("target")
if not isinstance(target_value, int):
raise TypeError("target must be an int")
measured_pct, stdout, stderr, exit_code = _measure_auth_coverage_with_trace(
fixture_root
)
return {
"passed": exit_code == 0 and measured_pct >= float(target_value),
"message": (
f"{validation_name}: measured={measured_pct:.1f}% "
f"target={target_value}% exit_code={exit_code}"
),
"data": {
"stdout": stdout,
"stderr": stderr,
"measured_coverage": measured_pct,
"target": target_value,
},
}
plan_metadata: dict[str, Any] = {}
summary = ValidationPipeline(
commands=commands,
executor=executor,
max_workers=1,
).run_for_plan(plan_metadata=plan_metadata)
assert summary.results, "Expected at least one validation result"
validation_summary = plan_metadata.get("validation_summary")
assert isinstance(validation_summary, dict), "Expected validation_summary metadata"
return validation_summary
def wf02_coverage_validation() -> None:
"""Verify coverage gate blocks failing coverage then applies passing coverage."""
target = 80
lifecycle_fail = setup_lifecycle()
create_wf02_action(lifecycle_fail)
fail_plan_id = create_trusted_plan(lifecycle_fail)
lifecycle_fail.try_auto_run(fail_plan_id)
store_fail = InMemoryChangeSetStore()
apply_fail = PlanApplyService(
lifecycle_service=lifecycle_fail, changeset_store=store_fail
)
with auth_module_fixture() as (fail_fixture_root, _):
placeholder_path = "tests/test_auth_placeholder.py"
(fail_fixture_root / placeholder_path).write_text(
"""def test_placeholder() -> None:
assert True
""",
encoding="utf-8",
)
fail_changeset_id = record_files_via_execution_context(
plan_id=fail_plan_id,
store=store_fail,
files={placeholder_path: "placeholder"},
)
fail_plan = lifecycle_fail.get_plan(fail_plan_id)
fail_plan.changeset_id = fail_changeset_id
fail_plan.sandbox_refs = [f"sandbox-{fail_plan_id[:8]}"]
fail_plan.validation_summary = _register_attach_and_run_validation(
fail_fixture_root,
fail_plan_id,
target,
)
_assert_validation_payload(
fail_plan.validation_summary,
expected_passed=False,
target=target,
)
lifecycle_fail.save_plan(fail_plan)
lifecycle_fail.apply_plan(fail_plan_id)
lifecycle_fail.start_apply(fail_plan_id)
fail_result = apply_fail.apply_with_validation_gate(fail_plan_id)
assert fail_result.outcome == ApplyOutcome.CONSTRAINED
constrained_plan = lifecycle_fail.get_plan(fail_plan_id)
assert constrained_plan.processing_state.value == "constrained"
lifecycle_pass = setup_lifecycle()
create_wf02_action(lifecycle_pass)
pass_plan_id = create_trusted_plan(lifecycle_pass)
lifecycle_pass.try_auto_run(pass_plan_id)
store_pass = InMemoryChangeSetStore()
apply_pass = PlanApplyService(
lifecycle_service=lifecycle_pass, changeset_store=store_pass
)
with auth_module_fixture() as (pass_fixture_root, _):
generated_files = generate_tests_from_mock_provider(
pass_fixture_root,
"src/auth",
target,
include_coverage_suite=True,
)
normalized_files = write_generated_files_safely(
pass_fixture_root,
generated_files,
)
pass_changeset_id = record_files_via_execution_context(
plan_id=pass_plan_id,
store=store_pass,
files=normalized_files,
)
pass_plan = lifecycle_pass.get_plan(pass_plan_id)
pass_plan.changeset_id = pass_changeset_id
pass_plan.sandbox_refs = [f"sandbox-{pass_plan_id[:8]}"]
pass_plan.validation_summary = _register_attach_and_run_validation(
pass_fixture_root,
pass_plan_id,
target,
)
_assert_validation_payload(
pass_plan.validation_summary,
expected_passed=True,
target=target,
)
lifecycle_pass.save_plan(pass_plan)
lifecycle_pass.apply_plan(pass_plan_id)
lifecycle_pass.start_apply(pass_plan_id)
pass_result = apply_pass.apply_with_validation_gate(pass_plan_id)
assert pass_result.outcome == ApplyOutcome.APPLIED
applied_plan = lifecycle_pass.get_plan(pass_plan_id)
assert applied_plan.processing_state.value == "applied"
print("wf02-coverage-validation-ok")