Files
temp/features/steps/repositories_uncovered_lines_steps.py
CoreRasurae 007af498b8 refactor(autonomy): rename automation profile task flags to spec names
Renamed all 11 task-type confidence threshold fields in AutomationProfile
from phase-transition semantics to spec-defined task-type semantics.
Updated all 8 built-in profiles, CLI formatting, YAML schema, services,
and all Behave/Robot tests referencing the old field names.

Post-review fixes:
- Fixed 24 stale old field names in M6 fixture files
  (automation_profiles.json, autonomy_guardrails.json)
- Added model_validator(mode='before') to detect legacy field names
  and raise actionable ValueError with rename mapping
- Added semantic bridge comments in PlanLifecycleService mapping
  task-type thresholds to phase-transition gates
- Added threshold_field to structured log messages for observability
- Restored categorised CLI automation-profile show output to match
  spec (Phase Transitions / Decision Automation / Self-Repair /
  Execution Controls) instead of flat list
- Added missing access_network field to spec show output examples
  (Rich, Plain, JSON, YAML variants)
- Aligned ADR-017 profile fields table to all 11 fields with
  descriptions matching spec Automatable Tasks table
- Aligned automation_profiles.md threshold descriptions with spec
- Added spec section references in phase_reversion.md, error_recovery.md,
  and plan_execute.md for field naming context
- Extended repository roundtrip test to assert all 11 threshold fields
- Fixed benchmark _make_profile() passing safety fields as top-level
  kwargs instead of via SafetyProfile sub-model (incompatible with
  extra="forbid")
- Aligned CLI JSON/YAML output structure for automation-profile show
  with the specification grouped format (phase_transitions,
  decision_automation, self_repair, execution_controls)
- Moved safety boolean fields into the Execution Controls section
  of Rich output per spec examples
- Reverted auto profile description to "Fully automatic except apply"
  per specification (line 16703, line 28406)
- Improved bridge comments in test steps with semantic context for
  threshold-to-gate mappings

ISSUES CLOSED: #902
2026-03-30 13:18:07 +01:00

1517 lines
53 KiB
Python

"""Step definitions for repositories_uncovered_lines.feature.
Targets the remaining ~84 uncovered lines in repositories.py.
"""
from __future__ import annotations
import json
from datetime import UTC, datetime
from typing import Any
from unittest.mock import MagicMock
from behave import given, then, when # type: ignore[import-untyped]
from sqlalchemy import create_engine
from sqlalchemy.exc import IntegrityError, OperationalError
from sqlalchemy.orm import Session, sessionmaker
from cleveragents.infrastructure.database.models import (
Base,
ResourceLinkModel,
ResourceModel,
ResourceTypeModel,
ToolBindingModel,
ToolModel,
)
from cleveragents.infrastructure.database.repositories import (
AutomationProfileNotFoundError,
AutomationProfileRepository,
AutomationProfileSchemaVersionError,
DatabaseError,
DuplicateAutomationProfileError,
DuplicateResourceTypeError,
DuplicateValidationAttachmentError,
InvalidToolTypeError,
LinkNotFoundError,
ResourceNotFoundRepoError,
ResourceRepository,
ResourceTypeRepository,
ToolRepository,
ValidationAttachmentRepository,
)
# ── helpers ────────────────────────────────────────────────────────────────
def _make_engine_and_session():
"""Create an in-memory SQLite engine + session factory."""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
factory = sessionmaker(bind=engine)
return engine, factory
def _now_iso() -> str:
return datetime.now(tz=UTC).isoformat()
def _make_rt_model_row(
name: str,
description: str = "test type",
resource_kind: str = "physical",
sandbox_strategy: str = "none",
user_addable: bool = True,
allowed_child_types_json: str = "[]",
auto_discover_json: str | None = None,
equivalence_json: str | None = None,
capabilities_json: str = '{"read": true, "write": true, "sandbox": true, "checkpoint": false}',
) -> ResourceTypeModel:
"""Create a ResourceTypeModel with correct column names."""
now = _now_iso()
ns = name.split("/")[0] if "/" in name else "builtin"
return ResourceTypeModel(
name=name,
namespace=ns,
description=description,
resource_kind=resource_kind,
sandbox_strategy=sandbox_strategy,
user_addable=user_addable,
handler_ref=None,
args_schema_json=None,
allowed_parent_types_json=None,
allowed_child_types_json=allowed_child_types_json,
auto_discover_json=auto_discover_json,
equivalence_json=equivalence_json,
capabilities_json=capabilities_json,
source=None,
created_at=now,
updated_at=now,
)
def _make_automation_profile(
name: str,
description: str = "test",
schema_version: str = "1.0",
**overrides: Any,
) -> Any:
"""Create an AutomationProfile domain object."""
from cleveragents.domain.models.core.automation_profile import AutomationProfile
from cleveragents.domain.models.core.safety_profile import SafetyProfile
# Extract safety-related overrides into a SafetyProfile.
safety_keys = {
"require_sandbox",
"require_checkpoints",
"allow_unsafe_tools",
}
safety_defaults: dict[str, Any] = {
"require_sandbox": True,
"require_checkpoints": True,
"allow_unsafe_tools": False,
"require_human_approval": False,
"max_retries_per_step": 3,
}
for key in safety_keys:
if key in overrides:
safety_defaults[key] = overrides.pop(key)
defaults = dict(
name=name,
description=description,
schema_version=schema_version,
decompose_task=0.0,
create_tool=0.0,
select_tool=0.0,
edit_code=0.0,
execute_command=0.0,
create_file=0.0,
delete_content=0.0,
access_network=0.0,
install_dependency=0.0,
modify_config=0.0,
approve_plan=0.0,
safety=SafetyProfile(**safety_defaults),
)
defaults.update(overrides)
return AutomationProfile(**defaults)
# ── LinkNotFoundError ──────────────────────────────────────────────────────
@given('a LinkNotFoundError for parent "{pid}" and child "{cid}"')
def step_given_link_not_found_error(context, pid, cid):
context.link_error = LinkNotFoundError(pid, cid)
@then('the LinkNotFoundError message should contain "{pid}" and "{cid}"')
def step_then_link_error_message(context, pid, cid):
msg = str(context.link_error)
assert pid in msg, f"Expected '{pid}' in '{msg}'"
assert cid in msg, f"Expected '{cid}' in '{msg}'"
@then('the LinkNotFoundError parent_id should be "{pid}"')
def step_then_link_error_parent(context, pid):
assert context.link_error.parent_id == pid
@then('the LinkNotFoundError child_id should be "{cid}"')
def step_then_link_error_child(context, cid):
assert context.link_error.child_id == cid
# ── ResourceTypeRepository helpers ─────────────────────────────────────────
@given("a resource type repository backed by an in-memory database for uncovered lines")
def step_given_rt_repo_inmem(context):
engine, factory = _make_engine_and_session()
context._ucl_engine = engine
context._ucl_session_factory = factory
context._ucl_rt_repo = ResourceTypeRepository(factory)
context._ucl_res_repo = ResourceRepository(factory)
def _make_resource_type_spec(name: str, **kw: Any) -> Any:
"""Create a ResourceTypeSpec domain object."""
from cleveragents.domain.models.core.resource_type import (
ResourceKind,
ResourceTypeSpec,
SandboxStrategy,
)
defaults = dict(
name=name,
description="test type",
resource_kind=ResourceKind.PHYSICAL,
sandbox_strategy=SandboxStrategy.NONE,
user_addable=True,
cli_args=[],
parent_types=[],
child_types=[],
auto_discovery=None,
equivalence=None,
handler=None,
capabilities={
"read": True,
"write": True,
"sandbox": True,
"checkpoint": False,
},
built_in=False,
)
defaults.update(kw)
return ResourceTypeSpec(**defaults)
@given('a resource type "{name}" exists in the database for uncovered lines')
def step_given_rt_exists(context, name):
spec = _make_resource_type_spec(name)
context._ucl_rt_repo.create(spec)
@when('the same resource type "{name}" is created again for uncovered lines')
def step_when_rt_dup_create(context, name):
spec = _make_resource_type_spec(name)
try:
context._ucl_rt_repo.create(spec)
context._ucl_error = None
except DuplicateResourceTypeError as exc:
context._ucl_error = exc
except Exception as exc:
context._ucl_error = exc
@then("a DuplicateResourceTypeError should be raised for uncovered lines")
def step_then_dup_rt_error(context):
assert isinstance(context._ucl_error, DuplicateResourceTypeError), (
f"Expected DuplicateResourceTypeError, got {type(context._ucl_error)}: {context._ucl_error}"
)
# ── ResourceTypeRepository._to_domain equivalence_json ──────────────────────
@given("a resource type row with equivalence_json set to '{json_str}'")
def step_given_rt_with_equiv(context, json_str):
# Insert a resource type row directly with equivalence_json set
session = context._ucl_session_factory()
now = _now_iso()
row = ResourceTypeModel(
name="test/equiv-type",
namespace="test",
description="type with equivalence",
resource_kind="physical",
sandbox_strategy="none",
user_addable=True,
handler_ref=None,
args_schema_json=None,
allowed_parent_types_json=None,
allowed_child_types_json="[]",
auto_discover_json=None,
equivalence_json=json_str,
capabilities_json='{"read": true, "write": true, "sandbox": true, "checkpoint": false}',
source=None,
created_at=now,
updated_at=now,
)
session.add(row)
session.commit()
@when('the resource type is retrieved by name "{name}" for uncovered lines')
def step_when_rt_get_by_name(context, name):
context._ucl_rt_result = context._ucl_rt_repo.get(name)
@then('the resource type equivalence should contain key "{key}"')
def step_then_rt_equiv_key(context, key):
result = context._ucl_rt_result
assert result is not None, "Resource type was None"
equiv = result.equivalence
assert equiv is not None, "Equivalence was None"
assert key in equiv, f"Expected key '{key}' in equivalence {equiv}"
# ── ResourceRepository helpers ─────────────────────────────────────────────
@given("a resource repository backed by an in-memory database for uncovered lines")
def step_given_res_repo_inmem(context):
if not hasattr(context, "_ucl_engine"):
engine, factory = _make_engine_and_session()
context._ucl_engine = engine
context._ucl_session_factory = factory
context._ucl_rt_repo = ResourceTypeRepository(factory)
context._ucl_res_repo = ResourceRepository(context._ucl_session_factory)
def _ensure_resource_type(context, type_name: str = "test/default-type"):
"""Ensure a resource type exists."""
session = context._ucl_session_factory()
existing = session.query(ResourceTypeModel).filter_by(name=type_name).first()
if not existing:
now = _now_iso()
ns = type_name.split("/")[0] if "/" in type_name else "builtin"
row = ResourceTypeModel(
name=type_name,
namespace=ns,
description="test",
resource_kind="physical",
sandbox_strategy="none",
user_addable=True,
handler_ref=None,
args_schema_json=None,
allowed_parent_types_json=None,
allowed_child_types_json=None,
auto_discover_json=None,
capabilities_json='{"read": true, "write": true, "sandbox": true, "checkpoint": false}',
equivalence_json=None,
source=None,
created_at=now,
updated_at=now,
)
session.add(row)
session.commit()
def _create_resource_row(
context,
resource_id: str | None = None,
type_name: str = "test/default-type",
namespace: str | None = None,
name: str | None = None,
sandbox_strategy: str | None = None,
) -> str:
"""Insert a resource row directly and return its ID."""
from ulid import ULID as _ULID
if resource_id is None:
resource_id = str(_ULID())
_ensure_resource_type(context, type_name)
session = context._ucl_session_factory()
now = _now_iso()
row = ResourceModel(
resource_id=resource_id,
namespaced_name=name,
namespace=namespace,
type_name=type_name,
resource_kind="physical",
location=None,
description="test resource",
read_only=False,
auto_discovered=False,
sandbox_strategy=sandbox_strategy,
content_hash=None,
properties_json=None,
metadata_json=None,
created_at=now,
updated_at=now,
)
session.add(row)
session.commit()
return resource_id
# ── list_resources namespace filter ─────────────────────────────────────────
@given('resources exist in namespace "{ns1}" and "{ns2}" for uncovered lines')
def step_given_resources_in_namespaces(context, ns1, ns2):
from ulid import ULID
_create_resource_row(context, str(ULID()), namespace=ns1, name=f"{ns1}/res-a")
_create_resource_row(context, str(ULID()), namespace=ns1, name=f"{ns1}/res-b")
_create_resource_row(context, str(ULID()), namespace=ns2, name=f"{ns2}/res-a")
@when('resources are listed with namespace "{ns}" for uncovered lines')
def step_when_list_by_namespace(context, ns):
context._ucl_listed = context._ucl_res_repo.list_resources(namespace=ns)
@then('only resources from namespace "{ns}" should be returned')
def step_then_only_namespace_resources(context, ns):
resources = context._ucl_listed
assert len(resources) > 0, "Expected at least one resource"
for r in resources:
name = r.name or ""
assert name.startswith(ns), f"Resource '{name}' not in namespace '{ns}'"
# ── unlink_child - parent not found ────────────────────────────────────────
@when(
'unlink_child is called with non-existent parent "{pid}" and child "{cid}" for uncovered lines'
)
def step_when_unlink_parent_missing(context, pid, cid):
try:
context._ucl_res_repo.unlink_child(pid, cid)
context._ucl_error = None
except Exception as exc:
context._ucl_error = exc
@then("a ResourceNotFoundRepoError should be raised for the unlink parent")
def step_then_unlink_parent_error(context):
assert isinstance(context._ucl_error, ResourceNotFoundRepoError), (
f"Expected ResourceNotFoundRepoError, got {type(context._ucl_error)}: {context._ucl_error}"
)
# ── unlink_child - child not found ────────────────────────────────────────
@given('a resource "{rid}" exists for uncovered unlink tests')
def step_given_resource_for_unlink(context, rid):
_create_resource_row(context, rid)
context._ucl_unlink_parent_id = rid
@when(
'unlink_child is called with existing parent and non-existent child "{cid}" for uncovered lines'
)
def step_when_unlink_child_missing(context, cid):
try:
context._ucl_res_repo.unlink_child(context._ucl_unlink_parent_id, cid)
context._ucl_error = None
except Exception as exc:
context._ucl_error = exc
@then("a ResourceNotFoundRepoError should be raised for the unlink child")
def step_then_unlink_child_error(context):
assert isinstance(context._ucl_error, ResourceNotFoundRepoError), (
f"Expected ResourceNotFoundRepoError, got {type(context._ucl_error)}: {context._ucl_error}"
)
# ── unlink_child - link not found ─────────────────────────────────────────
@given('two resources "{pid}" and "{cid}" exist but are not linked for uncovered lines')
def step_given_two_unlinked_resources(context, pid, cid):
_create_resource_row(context, pid)
_create_resource_row(context, cid)
context._ucl_unlink_pid = pid
context._ucl_unlink_cid = cid
@when('unlink_child is called for "{pid}" and "{cid}" for uncovered lines')
def step_when_unlink_no_link(context, pid, cid):
try:
context._ucl_res_repo.unlink_child(pid, cid)
context._ucl_error = None
except Exception as exc:
context._ucl_error = exc
@then("a LinkNotFoundError should be raised for uncovered lines")
def step_then_link_not_found_error(context):
assert isinstance(context._ucl_error, LinkNotFoundError), (
f"Expected LinkNotFoundError, got {type(context._ucl_error)}: {context._ucl_error}"
)
# ── unlink_child - re-raise domain errors ─────────────────────────────────
@when(
'unlink_child is called with non-existent parent "{pid}" and child "{cid}" for uncovered lines re-raise'
)
def step_when_unlink_reraise(context, pid, cid):
try:
context._ucl_res_repo.unlink_child(pid, cid)
context._ucl_error = None
except Exception as exc:
context._ucl_error = exc
@then("a ResourceNotFoundRepoError should be raised and re-raised for uncovered lines")
def step_then_unlink_reraise(context):
assert isinstance(context._ucl_error, ResourceNotFoundRepoError), (
f"Expected ResourceNotFoundRepoError, got {type(context._ucl_error)}: {context._ucl_error}"
)
# ── auto_discover_children - type_row is None ─────────────────────────────
@given(
'a resource with type "missing-type" that has no type row in DB for uncovered lines'
)
def step_given_res_missing_type(context):
# Insert resource with a type_name that has no matching ResourceTypeModel
session = context._ucl_session_factory()
now = _now_iso()
row = ResourceModel(
resource_id="auto-disc-no-type",
namespaced_name=None,
namespace=None,
type_name="missing-type",
resource_kind="physical",
location=None,
description="test",
read_only=False,
auto_discovered=False,
sandbox_strategy=None,
content_hash=None,
properties_json=None,
metadata_json=None,
created_at=now,
updated_at=now,
)
session.add(row)
session.commit()
context._ucl_auto_disc_id = "auto-disc-no-type"
@when("auto_discover_children is called for that resource for uncovered lines")
def step_when_auto_disc_no_type(context):
context._ucl_auto_disc_result = context._ucl_res_repo.auto_discover_children(
context._ucl_auto_disc_id
)
@then("an empty list should be returned from auto_discover for uncovered lines")
def step_then_auto_disc_empty(context):
assert context._ucl_auto_disc_result == [], (
f"Expected empty list, got {context._ucl_auto_disc_result}"
)
# ── auto_discover_children - null auto_discover_json ──────────────────────
@given("a resource with type that has null auto_discover_json for uncovered lines")
def step_given_res_null_auto_disc(context):
_ensure_resource_type(context, "test/null-auto-disc")
_create_resource_row(context, "auto-disc-null", type_name="test/null-auto-disc")
context._ucl_auto_disc_id_null = "auto-disc-null"
@when(
"auto_discover_children is called for that typed resource for uncovered lines null"
)
def step_when_auto_disc_null(context):
context._ucl_auto_disc_result = context._ucl_res_repo.auto_discover_children(
context._ucl_auto_disc_id_null
)
# ── auto_discover_children - disabled ─────────────────────────────────────
@given("a resource with type that has auto_discover disabled for uncovered lines")
def step_given_res_disabled_auto_disc(context):
session = context._ucl_session_factory()
now = _now_iso()
row = ResourceTypeModel(
name="test/disabled-auto-disc",
namespace="test",
description="disabled auto-disc",
resource_kind="physical",
sandbox_strategy="none",
user_addable=True,
handler_ref=None,
args_schema_json=None,
allowed_parent_types_json=None,
allowed_child_types_json="[]",
auto_discover_json=json.dumps({"enabled": False, "rules": []}),
equivalence_json=None,
capabilities_json='{"read": true, "write": true, "sandbox": true, "checkpoint": false}',
source=None,
created_at=now,
updated_at=now,
)
session.add(row)
session.commit()
_create_resource_row(
context, "auto-disc-disabled", type_name="test/disabled-auto-disc"
)
context._ucl_auto_disc_id_disabled = "auto-disc-disabled"
@when(
"auto_discover_children is called for that typed resource for uncovered lines disabled"
)
def step_when_auto_disc_disabled(context):
context._ucl_auto_disc_result = context._ucl_res_repo.auto_discover_children(
context._ucl_auto_disc_id_disabled
)
# ── auto_discover_children - empty rules ──────────────────────────────────
@given(
"a resource with type that has auto_discover enabled but empty rules for uncovered lines"
)
def step_given_res_empty_rules(context):
session = context._ucl_session_factory()
_now_iso()
row = _make_rt_model_row(
"test/empty-rules",
description="empty rules",
auto_discover_json=json.dumps({"enabled": True, "rules": []}),
)
session.add(row)
session.commit()
_create_resource_row(context, "auto-disc-empty-rules", type_name="test/empty-rules")
context._ucl_auto_disc_id_empty = "auto-disc-empty-rules"
@when(
"auto_discover_children is called for that typed resource for uncovered lines empty rules"
)
def step_when_auto_disc_empty_rules(context):
context._ucl_auto_disc_result = context._ucl_res_repo.auto_discover_children(
context._ucl_auto_disc_id_empty
)
# ── auto_discover_children - child type not in DB ─────────────────────────
@given(
"a resource with auto_discover rules referencing non-existent child type for uncovered lines"
)
def step_given_res_no_child_type(context):
session = context._ucl_session_factory()
_now_iso()
row = _make_rt_model_row(
"test/no-child-type",
description="missing child type",
auto_discover_json=json.dumps(
{
"enabled": True,
"rules": [{"type": "nonexistent/child-type"}],
}
),
)
session.add(row)
session.commit()
_create_resource_row(context, "auto-disc-no-child", type_name="test/no-child-type")
context._ucl_auto_disc_id_no_child = "auto-disc-no-child"
@when(
"auto_discover_children is called for that resource with missing child type for uncovered lines"
)
def step_when_auto_disc_no_child(context):
context._ucl_auto_disc_result = context._ucl_res_repo.auto_discover_children(
context._ucl_auto_disc_id_no_child
)
# ── auto_discover_children - child type not in allowed list ───────────────
@given(
"a resource with auto_discover rules where child type is not in allowed list for uncovered lines"
)
def step_given_res_disallowed_child(context):
session = context._ucl_session_factory()
_now_iso()
# Create the child type in DB
child_type = _make_rt_model_row("test/disallowed-child", description="child type")
session.add(child_type)
session.commit()
# Create parent type that allows only "test/other-child", not "test/disallowed-child"
session2 = context._ucl_session_factory()
parent_type = _make_rt_model_row(
"test/disallow-parent",
description="parent type disallowing child",
allowed_child_types_json=json.dumps(["test/other-child"]),
auto_discover_json=json.dumps(
{
"enabled": True,
"rules": [{"type": "test/disallowed-child"}],
}
),
)
session2.add(parent_type)
session2.commit()
_create_resource_row(
context, "auto-disc-disallowed", type_name="test/disallow-parent"
)
context._ucl_auto_disc_id_disallowed = "auto-disc-disallowed"
@when(
"auto_discover_children is called for that resource with disallowed child type for uncovered lines"
)
def step_when_auto_disc_disallowed(context):
context._ucl_auto_disc_result = context._ucl_res_repo.auto_discover_children(
context._ucl_auto_disc_id_disallowed
)
# ── auto_discover_children - successful discovery ─────────────────────────
@given(
"a resource with auto_discover rules that match an existing child type for uncovered lines"
)
def step_given_res_full_discovery(context):
session = context._ucl_session_factory()
_now_iso()
# Create the child type
existing_ct = (
session.query(ResourceTypeModel).filter_by(name="test/valid-child").first()
)
if not existing_ct:
child_type = _make_rt_model_row(
"test/valid-child", description="valid child type"
)
session.add(child_type)
session.commit()
session2 = context._ucl_session_factory()
existing_pt = (
session2.query(ResourceTypeModel).filter_by(name="test/disc-parent").first()
)
if not existing_pt:
parent_type = _make_rt_model_row(
"test/disc-parent",
description="parent for discovery",
allowed_child_types_json=json.dumps(["test/valid-child"]),
auto_discover_json=json.dumps(
{
"enabled": True,
"rules": [{"type": "test/valid-child"}],
}
),
)
session2.add(parent_type)
session2.commit()
_create_resource_row(context, "auto-disc-full", type_name="test/disc-parent")
context._ucl_auto_disc_full_id = "auto-disc-full"
@when(
"auto_discover_children is called for that resource for full discovery for uncovered lines"
)
def step_when_auto_disc_full(context):
context._ucl_auto_disc_result = context._ucl_res_repo.auto_discover_children(
context._ucl_auto_disc_full_id
)
@then("the discovered children list should not be empty for uncovered lines")
def step_then_discovered_not_empty(context):
assert len(context._ucl_auto_disc_result) > 0, (
"Expected non-empty discovered children"
)
@then("the child resource should be linked to the parent for uncovered lines")
def step_then_child_linked(context):
session = context._ucl_session_factory()
links = (
session.query(ResourceLinkModel)
.filter_by(parent_id=context._ucl_auto_disc_full_id)
.all()
)
assert len(links) > 0, "Expected at least one link from parent to child"
# ── auto_discover_children - OperationalError ─────────────────────────────
@given(
"a resource repository with session raising OperationalError on auto_discover for uncovered lines"
)
def step_given_res_repo_op_error_auto_disc(context):
call_count = 0
def mock_session_factory():
nonlocal call_count
call_count += 1
mock_session = MagicMock(spec=Session)
# First query returns the resource row
parent_row = MagicMock()
parent_row.type_name = "some-type"
parent_row.resource_id = "res-op-err"
type_row = MagicMock()
type_row.auto_discover_json = json.dumps(
{"enabled": True, "rules": [{"type": "child-t"}]}
)
type_row.allowed_child_types_json = "[]"
# query().filter_by().first() returns parent_row first, then type_row, then raises
query_mock = MagicMock()
filter_mock = MagicMock()
filter_mock.first.side_effect = [
parent_row,
type_row,
OperationalError("db fail", {}, None),
]
query_mock.filter_by.return_value = filter_mock
mock_session.query.return_value = query_mock
mock_session.rollback = MagicMock()
return mock_session
context._ucl_res_repo = ResourceRepository(mock_session_factory)
context._ucl_auto_disc_op_err_id = "res-op-err"
@when(
"auto_discover_children is called and an OperationalError occurs for uncovered lines"
)
def step_when_auto_disc_op_error(context):
try:
context._ucl_res_repo.auto_discover_children(context._ucl_auto_disc_op_err_id)
context._ucl_error = None
except Exception as exc:
context._ucl_error = exc
@then("a DatabaseError should be raised mentioning auto-discover for uncovered lines")
def step_then_auto_disc_db_error(context):
assert isinstance(context._ucl_error, DatabaseError), (
f"Expected DatabaseError, got {type(context._ucl_error)}: {context._ucl_error}"
)
assert (
"auto-discover" in str(context._ucl_error).lower()
or "auto_discover" in str(context._ucl_error).lower()
), f"Expected 'auto-discover' in message: {context._ucl_error}"
# ── _get_ancestors - diamond graph ────────────────────────────────────────
@given("resources forming a diamond graph for ancestor traversal for uncovered lines")
def step_given_diamond_graph(context):
# Create: A -> B, A -> C, B -> D, C -> D (diamond)
# _get_ancestors(D) should visit D, B, C, A
for rid in ["anc-A", "anc-B", "anc-C", "anc-D"]:
_create_resource_row(context, rid)
session = context._ucl_session_factory()
now = _now_iso()
links = [
ResourceLinkModel(parent_id="anc-A", child_id="anc-B", created_at=now),
ResourceLinkModel(parent_id="anc-A", child_id="anc-C", created_at=now),
ResourceLinkModel(parent_id="anc-B", child_id="anc-D", created_at=now),
ResourceLinkModel(parent_id="anc-C", child_id="anc-D", created_at=now),
]
for link in links:
session.add(link)
session.commit()
context._ucl_ancestor_id = "anc-D"
@when("_get_ancestors is called on the bottom resource for uncovered lines")
def step_when_get_ancestors(context):
session = context._ucl_session_factory()
context._ucl_ancestors = ResourceRepository._get_ancestors(
session, context._ucl_ancestor_id
)
@then(
"all ancestor IDs should be returned including the bottom resource for uncovered lines"
)
def step_then_ancestors_returned(context):
expected = {"anc-A", "anc-B", "anc-C", "anc-D"}
assert context._ucl_ancestors == expected, (
f"Expected {expected}, got {context._ucl_ancestors}"
)
# ── ResourceRepository._to_domain - sandbox_strategy ─────────────────────
@given('a resource row with sandbox_strategy set to "git_worktree" for uncovered lines')
def step_given_res_with_sandbox(context):
from ulid import ULID
rid = str(ULID())
_create_resource_row(
context,
rid,
sandbox_strategy="git_worktree",
)
context._ucl_sandbox_id = rid
@when("the resource is retrieved by ID for uncovered lines sandbox")
def step_when_res_get_sandbox(context):
context._ucl_sandbox_result = context._ucl_res_repo.get(context._ucl_sandbox_id)
@then("the resource sandbox_strategy should be SandboxStrategy.GIT_WORKTREE")
def step_then_sandbox_strategy(context):
from cleveragents.domain.models.core.resource import SandboxStrategy
result = context._ucl_sandbox_result
assert result is not None, "Resource was None"
assert result.sandbox_strategy == SandboxStrategy.GIT_WORKTREE, (
f"Expected GIT_WORKTREE, got {result.sandbox_strategy}"
)
# ── ToolRepository helpers ────────────────────────────────────────────────
@given("a tool repository backed by an in-memory database for uncovered lines")
def step_given_tool_repo_inmem(context):
engine, factory = _make_engine_and_session()
context._ucl_tool_engine = engine
context._ucl_tool_session_factory = factory
context._ucl_tool_repo = ToolRepository(factory)
def _make_tool_domain(
name: str = "test/my-tool",
tool_type: str = "tool",
source: str = "builtin",
input_schema: dict | None = None,
output_schema: dict | None = None,
resource_slots: list | None = None,
) -> Any:
"""Create a mock tool domain object."""
tool = MagicMock()
tool.name = name
tool.description = "A test tool"
# tool_type enum-like
tt = MagicMock()
tt.value = tool_type
tool.tool_type = tt
# source enum-like
src = MagicMock()
src.value = source
tool.source = src
tool.input_schema = input_schema
tool.output_schema = output_schema
tool.config_yaml = None
tool.tool_id = ""
cap = MagicMock()
cap.read_only = False
cap.writes = True
cap.checkpointable = False
cap.side_effects = False
tool.capability = cap
tool.resource_slots = resource_slots or []
return tool
# ── ToolRepository.add - invalid tool_type ────────────────────────────────
@given('a tool domain object with tool_type "bogus" for uncovered lines')
def step_given_tool_bad_type(context):
context._ucl_tool = _make_tool_domain(tool_type="bogus")
@when("the tool is added to the repository for uncovered lines invalid type")
def step_when_tool_add_invalid(context):
try:
context._ucl_tool_repo.add(context._ucl_tool)
context._ucl_error = None
except Exception as exc:
context._ucl_error = exc
@then("an InvalidToolTypeError should be raised for uncovered lines")
def step_then_invalid_tool_type(context):
assert isinstance(context._ucl_error, InvalidToolTypeError), (
f"Expected InvalidToolTypeError, got {type(context._ucl_error)}: {context._ucl_error}"
)
# ── ToolRepository.add - schemas + resource slots ─────────────────────────
@given("a tool domain object with schemas and resource slots for uncovered lines")
def step_given_tool_with_schemas(context):
slot = MagicMock()
slot.name = "my_slot"
slot.resource_type = "test/resource-type"
binding = MagicMock()
binding.value = "contextual"
slot.binding = binding
access = MagicMock()
access.value = "read_only"
slot.access = access
context._ucl_tool = _make_tool_domain(
name="test/schema-tool",
input_schema={"type": "object", "properties": {"x": {"type": "string"}}},
output_schema={"type": "object", "properties": {"y": {"type": "integer"}}},
resource_slots=[slot],
)
@when("the tool is added to the repository for uncovered lines with schemas")
def step_when_tool_add_schemas(context):
context._ucl_tool_id = context._ucl_tool_repo.add(context._ucl_tool)
@then(
"the tool should be retrievable and have schemas and bindings for uncovered lines"
)
def step_then_tool_schemas_bindings(context):
session = context._ucl_tool_session_factory()
row = session.query(ToolModel).filter_by(name=context._ucl_tool_id).first()
assert row is not None, "Tool row not found"
assert row.input_schema_json is not None, "input_schema_json should not be None"
assert row.output_schema_json is not None, "output_schema_json should not be None"
bindings = (
session.query(ToolBindingModel).filter_by(tool_name=context._ucl_tool_id).all()
)
assert len(bindings) == 1, f"Expected 1 binding, got {len(bindings)}"
assert bindings[0].slot_name == "my_slot"
assert bindings[0].binding_mode == "contextual"
# ── ToolRepository.get - by ULID ──────────────────────────────────────────
@given("a tool has been persisted and its ULID captured for uncovered lines")
def step_given_tool_persisted(context):
tool = _make_tool_domain(name="test/get-tool")
context._ucl_persisted_tool_id = context._ucl_tool_repo.add(tool)
@when("the tool is fetched by ULID for uncovered lines")
def step_when_tool_get_by_ulid(context):
context._ucl_tool_result = context._ucl_tool_repo.get(
context._ucl_persisted_tool_id
)
@then("the tool domain object should be returned for uncovered lines")
def step_then_tool_returned(context):
assert context._ucl_tool_result is not None, "Tool was None"
result = context._ucl_tool_result
name = result["name"] if isinstance(result, dict) else result.name
assert name == "test/get-tool"
@when('a tool is fetched by ULID "{ulid}" for uncovered lines')
def step_when_tool_get_unknown(context, ulid):
context._ucl_tool_result = context._ucl_tool_repo.get(ulid)
@then("None should be returned for the tool get for uncovered lines")
def step_then_tool_none(context):
assert context._ucl_tool_result is None, (
f"Expected None, got {context._ucl_tool_result}"
)
# ── ToolRepository._to_domain - input/output schema ──────────────────────
@given("a tool with input_schema and output_schema stored as JSON for uncovered lines")
def step_given_tool_with_json_schemas(context):
tool = _make_tool_domain(
name="test/json-schema-tool",
input_schema={"type": "object"},
output_schema={"type": "array"},
)
context._ucl_tool_repo.add(tool)
context._ucl_schema_tool_name = "test/json-schema-tool"
@when("the tool is retrieved by name for uncovered lines schemas")
def step_when_tool_get_by_name_schemas(context):
context._ucl_schema_tool = context._ucl_tool_repo.get_by_name(
context._ucl_schema_tool_name
)
@then("the tool should have parsed input_schema and output_schema for uncovered lines")
def step_then_tool_parsed_schemas(context):
import json as _json
tool = context._ucl_schema_tool
assert tool is not None, "Tool was None"
# HEAD's to_domain() returns a dict with JSON strings
if isinstance(tool, dict):
input_raw = tool.get("input_schema_json")
output_raw = tool.get("output_schema_json")
assert input_raw is not None, "input_schema_json should not be None"
assert output_raw is not None, "output_schema_json should not be None"
input_schema = (
_json.loads(input_raw) if isinstance(input_raw, str) else input_raw
)
output_schema = (
_json.loads(output_raw) if isinstance(output_raw, str) else output_raw
)
else:
input_schema = tool.input_schema
output_schema = tool.output_schema
assert isinstance(input_schema, dict), f"Expected dict, got {type(input_schema)}"
assert isinstance(output_schema, dict), f"Expected dict, got {type(output_schema)}"
# ── ValidationAttachment - duplicate detection ────────────────────────────
@given(
"a validation attachment repository backed by an in-memory database for uncovered lines"
)
def step_given_va_repo(context):
engine, factory = _make_engine_and_session()
context._ucl_va_engine = engine
context._ucl_va_session_factory = factory
context._ucl_va_repo = ValidationAttachmentRepository(factory)
# We need a resource to attach to
_ensure_va_resource(context, factory)
def _ensure_va_resource(context, factory):
"""Create a resource to use in validation attachment tests."""
session = factory()
now = _now_iso()
# Resource type first
rt = _make_rt_model_row("test/va-type", description="for VA tests")
session.add(rt)
session.flush()
for rid in ["res-1", "res-2"]:
res = ResourceModel(
resource_id=rid,
namespaced_name=None,
namespace=None,
type_name="test/va-type",
resource_kind="physical",
location=None,
description="VA test resource",
read_only=False,
auto_discovered=False,
sandbox_strategy=None,
content_hash=None,
properties_json=None,
metadata_json=None,
created_at=now,
updated_at=now,
)
session.add(res)
session.commit()
@given(
'a validation "{vname}" is already attached to resource "{rid}" for uncovered lines'
)
def step_given_va_attached(context, vname, rid):
context._ucl_va_repo.attach(vname, rid, "required")
context._ucl_va_rid = rid
context._ucl_va_vname = vname
@when(
'the same validation "{vname}" is attached again to resource "{rid}" for uncovered lines'
)
def step_when_va_dup_attach(context, vname, rid):
try:
context._ucl_va_repo.attach(vname, rid, "required")
context._ucl_error = None
except Exception as exc:
context._ucl_error = exc
@then(
"a DuplicateValidationAttachmentError should be raised for uncovered lines attach"
)
def step_then_va_dup_error(context):
assert isinstance(context._ucl_error, DuplicateValidationAttachmentError), (
f"Expected DuplicateValidationAttachmentError, got {type(context._ucl_error)}: {context._ucl_error}"
)
# ── ValidationAttachment - re-raise ───────────────────────────────────────
@when(
'the same validation "{vname}" is attached again to resource "{rid}" for uncovered lines re-raise'
)
def step_when_va_dup_reraise(context, vname, rid):
try:
context._ucl_va_repo.attach(vname, rid, "required")
context._ucl_error = None
except Exception as exc:
context._ucl_error = exc
@then("the DuplicateValidationAttachmentError should propagate for uncovered lines")
def step_then_va_dup_propagate(context):
assert isinstance(context._ucl_error, DuplicateValidationAttachmentError), (
f"Expected DuplicateValidationAttachmentError, got {type(context._ucl_error)}: {context._ucl_error}"
)
# ── AutomationProfileRepository ──────────────────────────────────────────
@given(
"an automation profile repository backed by an in-memory database for uncovered lines"
)
def step_given_ap_repo(context):
engine, factory = _make_engine_and_session()
context._ucl_ap_engine = engine
context._ucl_ap_session_factory = factory
context._ucl_ap_repo = AutomationProfileRepository(factory)
@when('a profile is fetched by name "{name}" for uncovered lines')
def step_when_ap_get(context, name):
context._ucl_ap_result = context._ucl_ap_repo.get_by_name(name)
@then("None should be returned for the profile get for uncovered lines")
def step_then_ap_none(context):
assert context._ucl_ap_result is None, (
f"Expected None, got {context._ucl_ap_result}"
)
# ── get_by_name returns domain ────────────────────────────────────────────
@given('a profile "{name}" has been upserted for uncovered lines')
def step_given_ap_upserted(context, name):
profile = _make_automation_profile(name)
context._ucl_ap_repo.upsert(profile)
@then("the profile domain object should be returned for uncovered lines")
def step_then_ap_domain(context):
assert context._ucl_ap_result is not None, "Profile was None"
assert context._ucl_ap_result.name == "test-profile"
# ── list_all ──────────────────────────────────────────────────────────────
@given('profiles "{name1}" and "{name2}" have been upserted for uncovered lines')
def step_given_multiple_profiles(context, name1, name2):
for name in [name1, name2]:
profile = _make_automation_profile(name)
context._ucl_ap_repo.upsert(profile)
@when("all profiles are listed for uncovered lines")
def step_when_ap_list(context):
context._ucl_ap_list = context._ucl_ap_repo.list_all()
@then("two profile domain objects should be returned for uncovered lines")
def step_then_ap_list_count(context):
assert len(context._ucl_ap_list) == 2, (
f"Expected 2, got {len(context._ucl_ap_list)}"
)
# ── upsert update ────────────────────────────────────────────────────────
@when(
'the profile "{name}" is upserted again with changed description for uncovered lines'
)
def step_when_ap_update(context, name):
profile = _make_automation_profile(name, description="updated desc")
context._ucl_ap_repo.upsert(profile)
@then("the profile should have the updated description for uncovered lines")
def step_then_ap_updated_desc(context):
result = context._ucl_ap_repo.get_by_name("update-prof")
assert result is not None, "Profile was None"
assert result.description == "updated desc", (
f"Expected 'updated desc', got '{result.description}'"
)
# ── upsert schema version mismatch ──────────────────────────────────────
@given(
'a profile "{name}" has been upserted with schema_version "{ver}" for uncovered lines'
)
def step_given_ap_versioned(context, name, ver):
profile = _make_automation_profile(name, schema_version=ver)
context._ucl_ap_repo.upsert(profile)
@when(
'the profile "{name}" is upserted with expected_schema_version "{ver}" for uncovered lines'
)
def step_when_ap_version_mismatch(context, name, ver):
profile = _make_automation_profile(name, schema_version="1.0")
try:
context._ucl_ap_repo.upsert(profile, expected_schema_version=ver)
context._ucl_error = None
except Exception as exc:
context._ucl_error = exc
@then("an AutomationProfileSchemaVersionError should be raised for uncovered lines")
def step_then_ap_version_error(context):
assert isinstance(context._ucl_error, AutomationProfileSchemaVersionError), (
f"Expected AutomationProfileSchemaVersionError, got {type(context._ucl_error)}: {context._ucl_error}"
)
# ── upsert new insert ────────────────────────────────────────────────────
@when('a new profile "{name}" is upserted for uncovered lines')
def step_when_ap_new_insert(context, name):
profile = _make_automation_profile(name, description="new profile")
context._ucl_ap_repo.upsert(profile)
@then('the profile "{name}" should be retrievable for uncovered lines')
def step_then_ap_retrievable(context, name):
result = context._ucl_ap_repo.get_by_name(name)
assert result is not None, f"Profile '{name}' was None"
assert result.name == name
# ── upsert IntegrityError ────────────────────────────────────────────────
@given(
"an automation profile repository with session raising IntegrityError on flush for uncovered lines"
)
def step_given_ap_repo_integrity_error(context):
def mock_session_factory():
mock_session = MagicMock(spec=Session)
query_mock = MagicMock()
filter_mock = MagicMock()
filter_mock.first.return_value = None # No existing row
query_mock.filter_by.return_value = filter_mock
mock_session.query.return_value = query_mock
mock_session.add = MagicMock()
mock_session.flush.side_effect = IntegrityError("UNIQUE constraint", {}, None)
mock_session.rollback = MagicMock()
return mock_session
context._ucl_ap_repo = AutomationProfileRepository(mock_session_factory)
@when("a profile is upserted and IntegrityError occurs for uncovered lines")
def step_when_ap_integrity_error(context):
profile = _make_automation_profile("integrity-fail")
try:
context._ucl_ap_repo.upsert(profile)
context._ucl_error = None
except Exception as exc:
context._ucl_error = exc
@then("a DuplicateAutomationProfileError should be raised for uncovered lines")
def step_then_ap_dup_error(context):
assert isinstance(context._ucl_error, DuplicateAutomationProfileError), (
f"Expected DuplicateAutomationProfileError, got {type(context._ucl_error)}: {context._ucl_error}"
)
# ── delete not found ─────────────────────────────────────────────────────
@when('a profile "{name}" is deleted for uncovered lines')
def step_when_ap_delete(context, name):
try:
context._ucl_ap_repo.delete(name)
context._ucl_error = None
except Exception as exc:
context._ucl_error = exc
@then("an AutomationProfileNotFoundError should be raised for uncovered lines")
def step_then_ap_not_found(context):
assert isinstance(context._ucl_error, AutomationProfileNotFoundError), (
f"Expected AutomationProfileNotFoundError, got {type(context._ucl_error)}: {context._ucl_error}"
)
# ── delete success ────────────────────────────────────────────────────────
@when('the profile "{name}" is deleted for uncovered lines')
def step_when_ap_delete_success(context, name):
context._ucl_ap_repo.delete(name)
@then('the profile "{name}" should no longer exist for uncovered lines')
def step_then_ap_deleted(context, name):
result = context._ucl_ap_repo.get_by_name(name)
assert result is None, f"Profile '{name}' still exists"
# ── _to_domain full fields ───────────────────────────────────────────────
@given('a profile "{name}" with all fields set has been upserted for uncovered lines')
def step_given_ap_full_fields(context, name):
profile = _make_automation_profile(
name,
description="full fields profile",
schema_version="2.0",
decompose_task=0.1,
create_tool=0.2,
select_tool=0.3,
edit_code=0.4,
execute_command=0.5,
create_file=0.6,
delete_content=0.7,
access_network=0.8,
install_dependency=0.9,
modify_config=0.15,
approve_plan=0.25,
require_sandbox=False,
require_checkpoints=False,
allow_unsafe_tools=True,
)
context._ucl_ap_repo.upsert(profile)
context._ucl_ap_full = profile
@then("all profile fields should match the original values for uncovered lines")
def step_then_ap_all_fields(context):
result = context._ucl_ap_result
orig = context._ucl_ap_full
assert result is not None, "Profile was None"
assert result.name == orig.name
assert result.description == orig.description
assert result.schema_version == orig.schema_version
assert abs(result.decompose_task - orig.decompose_task) < 0.001
assert abs(result.create_tool - orig.create_tool) < 0.001
assert abs(result.select_tool - orig.select_tool) < 0.001
assert abs(result.edit_code - orig.edit_code) < 0.001
assert abs(result.execute_command - orig.execute_command) < 0.001
assert abs(result.create_file - orig.create_file) < 0.001
assert abs(result.delete_content - orig.delete_content) < 0.001
assert abs(result.access_network - orig.access_network) < 0.001
assert abs(result.install_dependency - orig.install_dependency) < 0.001
assert abs(result.modify_config - orig.modify_config) < 0.001
assert abs(result.approve_plan - orig.approve_plan) < 0.001
assert result.safety.require_sandbox == orig.safety.require_sandbox
assert result.safety.require_checkpoints == orig.safety.require_checkpoints
assert result.safety.allow_unsafe_tools == orig.safety.allow_unsafe_tools
# ── _from_domain + _update_row roundtrip ──────────────────────────────────
@given(
'a profile "{name}" with specific field values has been upserted for uncovered lines'
)
def step_given_ap_specific_fields(context, name):
profile = _make_automation_profile(
name,
description="original",
decompose_task=0.1,
create_tool=0.2,
require_sandbox=True,
allow_unsafe_tools=False,
)
context._ucl_ap_repo.upsert(profile)
@when(
'the profile "{name}" is upserted again with different field values for uncovered lines'
)
def step_when_ap_update_fields(context, name):
profile = _make_automation_profile(
name,
description="updated roundtrip",
decompose_task=0.9,
create_tool=0.8,
select_tool=0.7,
edit_code=0.6,
execute_command=0.5,
create_file=0.4,
delete_content=0.3,
access_network=0.2,
install_dependency=0.1,
modify_config=0.05,
approve_plan=0.15,
require_sandbox=False,
require_checkpoints=False,
allow_unsafe_tools=True,
)
context._ucl_ap_repo.upsert(profile)
context._ucl_ap_updated_vals = profile
@then('the profile "{name}" should have the new field values for uncovered lines')
def step_then_ap_new_values(context, name):
result = context._ucl_ap_repo.get_by_name(name)
expected = context._ucl_ap_updated_vals
assert result is not None, f"Profile '{name}' was None"
assert result.description == expected.description
assert abs(result.decompose_task - expected.decompose_task) < 0.001
assert abs(result.create_tool - expected.create_tool) < 0.001
assert abs(result.select_tool - expected.select_tool) < 0.001
assert abs(result.edit_code - expected.edit_code) < 0.001
assert abs(result.execute_command - expected.execute_command) < 0.001
assert abs(result.create_file - expected.create_file) < 0.001
assert abs(result.delete_content - expected.delete_content) < 0.001
assert abs(result.access_network - expected.access_network) < 0.001
assert abs(result.install_dependency - expected.install_dependency) < 0.001
assert abs(result.modify_config - expected.modify_config) < 0.001
assert abs(result.approve_plan - expected.approve_plan) < 0.001
assert result.safety.require_sandbox == expected.safety.require_sandbox
assert result.safety.require_checkpoints == expected.safety.require_checkpoints
assert result.safety.allow_unsafe_tools == expected.safety.allow_unsafe_tools