1d36449a98
CI / build (push) Has been cancelled
CI / e2e_tests (push) Has been cancelled
CI / typecheck (push) Has been cancelled
CI / integration_tests (push) Has been cancelled
CI / quality (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / benchmark-publish (push) Has been cancelled
CI / security (push) Has been cancelled
CI / helm (push) Has been cancelled
CI / unit_tests (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled
CI / coverage (push) Has been cancelled
CI / docker (push) Has been cancelled
CI / status-check (push) Has been cancelled
Remove the --mode/-m CLI flag from the validation attach command to align with the specification, which defines validation mode as an inherent property of the validation definition set at registration time via validation add, not as a per-attachment override. The service layer (ToolRegistryService.attach_validation) no longer accepts mode as a parameter; instead it reads the mode from the validation's registered definition. The ToolRegistryRepository _to_legacy_domain now exposes the mode field so the service can access it. All tests that passed --mode to the CLI or service have been updated or removed. The invalid-mode validation scenarios were removed since the mode is no longer caller-supplied. Coverage remains at 98.7%. ISSUES CLOSED: #913 Co-authored-by: Jeffrey Phillips Freeman <jeffrey.freeman@cleverthis.com> Co-committed-by: Jeffrey Phillips Freeman <jeffrey.freeman@cleverthis.com>
452 lines
16 KiB
Python
452 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,
|
|
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")
|