a808c395f9
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / build (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 17s
CI / typecheck (pull_request) Successful in 35s
CI / security (pull_request) Successful in 50s
CI / unit_tests (pull_request) Successful in 2m46s
CI / integration_tests (pull_request) Successful in 3m16s
CI / docker (pull_request) Successful in 40s
CI / coverage (pull_request) Successful in 5m6s
CI / lint (push) Successful in 13s
CI / quality (push) Successful in 16s
CI / build (push) Successful in 18s
CI / security (push) Successful in 32s
CI / typecheck (push) Successful in 35s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m52s
CI / integration_tests (push) Successful in 3m8s
CI / docker (push) Successful in 39s
CI / coverage (push) Successful in 5m53s
CI / benchmark-publish (push) Successful in 16m55s
CI / benchmark-regression (pull_request) Successful in 33m0s
Add 53 new .feature files and corresponding step definition files targeting uncovered lines identified in build/coverage.xml. Fix AmbiguousStep conflicts in 7 pre-existing step files by disambiguating step text. New tests cover: ACP clients/facade, actor CLI/config, application container, ACMS service/strategies, async worker, automation profile CLI, autonomy guardrail, bridge, change model, config CLI/service, context service, cross-plan correction, database models, decision service, decomposition clustering/service, discovery handler, langchain chat provider, langgraph nodes, materializers, multi-project service, plan apply/CLI/lifecycle/model/ preflight/resume/service, PostgreSQL analyzer, project CLI/context CLI, provider registry, reactive application/route, repositories, resolver handler, resource registry service, resume model, retry patterns, sandbox protocol, server CLI, skill CLI/service, skills registry, subplan execution/service, system CLI, UKO loader, UoW, and YAML template engine. Closes #645
438 lines
14 KiB
Python
438 lines
14 KiB
Python
"""Behave step implementations for UKO ontology feature."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.application.services.uko_loader import (
|
|
UKOLoader,
|
|
UKOValidationError,
|
|
)
|
|
from cleveragents.domain.models.core.uko import (
|
|
UKONode,
|
|
UKOOntology,
|
|
UKOPrefix,
|
|
UKOVersion,
|
|
)
|
|
|
|
_TTL_FILE = Path(__file__).resolve().parents[2] / "docs" / "ontology" / "uko.ttl"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a TTL string")
|
|
@given("a TTL string:")
|
|
def step_given_ttl_string(context: Context) -> None:
|
|
context.ttl_text = context.text
|
|
|
|
|
|
@given("the UKO ontology is loaded from the TTL file")
|
|
def step_given_uko_loaded(context: Context) -> None:
|
|
loader = UKOLoader()
|
|
context.ontology = loader.load(_TTL_FILE)
|
|
context.loader = loader
|
|
|
|
|
|
@given("a TTL string with a node missing rdf_type")
|
|
@given("a TTL string with a node missing rdf_type:")
|
|
def step_given_ttl_missing_rdf_type(context: Context) -> None:
|
|
context.ttl_text = context.text
|
|
|
|
|
|
@given("a UKO ontology with an undefined prefix node")
|
|
def step_given_undefined_prefix(context: Context) -> None:
|
|
context.ontology = UKOOntology(
|
|
version=UKOVersion(
|
|
version_iri="https://cleveragents.ai/ontology/uko/0.1.0",
|
|
version="0.1.0",
|
|
label="Test",
|
|
comment="Test",
|
|
),
|
|
prefixes=(
|
|
UKOPrefix(
|
|
prefix="uko",
|
|
uri="https://cleveragents.ai/ontology/uko#",
|
|
),
|
|
),
|
|
nodes=(
|
|
UKONode(
|
|
uri="badprefix:Thing",
|
|
rdf_type="owl:Class",
|
|
label="Thing",
|
|
layer=0,
|
|
),
|
|
),
|
|
)
|
|
context.loader = UKOLoader()
|
|
|
|
|
|
@given("a UKO ontology with a cycle in inheritance")
|
|
def step_given_cycle(context: Context) -> None:
|
|
context.ontology = UKOOntology(
|
|
version=UKOVersion(
|
|
version_iri="https://cleveragents.ai/ontology/uko/0.1.0",
|
|
version="0.1.0",
|
|
label="Test",
|
|
comment="Test",
|
|
),
|
|
prefixes=(
|
|
UKOPrefix(
|
|
prefix="uko",
|
|
uri="https://cleveragents.ai/ontology/uko#",
|
|
),
|
|
),
|
|
nodes=(
|
|
UKONode(
|
|
uri="https://cleveragents.ai/ontology/uko#A",
|
|
rdf_type="owl:Class",
|
|
label="A",
|
|
parent_uris=("https://cleveragents.ai/ontology/uko#B",),
|
|
layer=0,
|
|
),
|
|
UKONode(
|
|
uri="https://cleveragents.ai/ontology/uko#B",
|
|
rdf_type="owl:Class",
|
|
label="B",
|
|
parent_uris=("https://cleveragents.ai/ontology/uko#A",),
|
|
layer=0,
|
|
),
|
|
),
|
|
)
|
|
context.loader = UKOLoader()
|
|
|
|
|
|
@given("a UKO ontology with a node referencing a non-existent parent")
|
|
def step_given_nonexistent_parent(context: Context) -> None:
|
|
context.ontology = UKOOntology(
|
|
version=UKOVersion(
|
|
version_iri="https://cleveragents.ai/ontology/uko/0.1.0",
|
|
version="0.1.0",
|
|
label="Test",
|
|
comment="Test",
|
|
),
|
|
prefixes=(
|
|
UKOPrefix(
|
|
prefix="uko",
|
|
uri="https://cleveragents.ai/ontology/uko#",
|
|
),
|
|
),
|
|
nodes=(
|
|
UKONode(
|
|
uri="https://cleveragents.ai/ontology/uko#Orphan",
|
|
rdf_type="owl:Class",
|
|
label="Orphan",
|
|
parent_uris=("https://cleveragents.ai/ontology/uko#DoesNotExist",),
|
|
layer=0,
|
|
),
|
|
),
|
|
)
|
|
context.loader = UKOLoader()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I parse the TTL string")
|
|
def step_when_parse_ttl(context: Context) -> None:
|
|
loader = UKOLoader()
|
|
context.ontology = loader.load_from_string(context.ttl_text)
|
|
context.loader = loader
|
|
|
|
|
|
@when("I get nodes at layer {layer:d}")
|
|
def step_when_get_layer(context: Context, layer: int) -> None:
|
|
loader: UKOLoader = getattr(context, "loader", UKOLoader())
|
|
context.layer_nodes = loader.get_layer_nodes(context.ontology, layer)
|
|
|
|
|
|
@when("I validate the ontology")
|
|
def step_when_validate(context: Context) -> None:
|
|
loader: UKOLoader = getattr(context, "loader", UKOLoader())
|
|
context.validation_errors = loader.validate(context.ontology)
|
|
|
|
|
|
@when('I resolve inheritance for "{uri}"')
|
|
def step_when_resolve_inheritance(context: Context, uri: str) -> None:
|
|
loader: UKOLoader = getattr(context, "loader", UKOLoader())
|
|
context.inheritance_chain = loader.resolve_inheritance(context.ontology, uri)
|
|
|
|
|
|
@when("I parse the TTL string expecting no nodes")
|
|
def step_when_parse_expecting_no_nodes(context: Context) -> None:
|
|
loader = UKOLoader()
|
|
# This TTL has a node without rdf:type — the parser skips it
|
|
context.ontology = loader.load_from_string(context.ttl_text)
|
|
context.loader = loader
|
|
|
|
|
|
@when("I inject a node without rdf_type into the ontology")
|
|
def step_inject_typeless_node(context: Context) -> None:
|
|
typeless = UKONode(
|
|
uri="https://cleveragents.ai/ontology/uko#Broken",
|
|
rdf_type="",
|
|
label="Broken",
|
|
)
|
|
existing = list(context.ontology.nodes)
|
|
existing.append(typeless)
|
|
context.ontology = context.ontology.model_copy(
|
|
update={"nodes": tuple(existing)},
|
|
)
|
|
|
|
|
|
@when("I validate the modified ontology")
|
|
def step_validate_modified(context: Context) -> None:
|
|
loader = UKOLoader()
|
|
context.validation_errors = loader.validate(context.ontology)
|
|
|
|
|
|
@when("I attempt to resolve inheritance for the cyclic node")
|
|
def step_when_resolve_cycle(context: Context) -> None:
|
|
loader: UKOLoader = context.loader
|
|
try:
|
|
loader.resolve_inheritance(
|
|
context.ontology,
|
|
"https://cleveragents.ai/ontology/uko#A",
|
|
)
|
|
context.cycle_error = None
|
|
except UKOValidationError as exc:
|
|
context.cycle_error = str(exc)
|
|
|
|
|
|
@when("I attempt to parse the TTL string expecting a validation error")
|
|
def step_when_parse_expecting_error(context: Context) -> None:
|
|
loader = UKOLoader()
|
|
try:
|
|
loader.load_from_string(context.ttl_text)
|
|
context.validation_error_msg = None
|
|
except UKOValidationError as exc:
|
|
context.validation_error_msg = str(exc)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then('the prefix "{name}" should map to "{uri}"')
|
|
def step_then_prefix_maps(context: Context, name: str, uri: str) -> None:
|
|
prefix_map = {p.prefix: p.uri for p in context.ontology.prefixes}
|
|
assert name in prefix_map, f"Prefix '{name}' not found; have {list(prefix_map)}"
|
|
assert prefix_map[name] == uri, f"Expected {uri}, got {prefix_map[name]}"
|
|
|
|
|
|
@then('I should find a node with label "{label}"')
|
|
def step_then_find_node(context: Context, label: str) -> None:
|
|
labels = [n.label for n in context.layer_nodes]
|
|
assert label in labels, f"Label '{label}' not found in {labels}"
|
|
|
|
|
|
@then('the ontology version should be "{version}"')
|
|
def step_then_version(context: Context, version: str) -> None:
|
|
assert context.ontology.version.version == version
|
|
|
|
|
|
@then('the ontology label should be "{label}"')
|
|
def step_then_label(context: Context, label: str) -> None:
|
|
assert context.ontology.version.label == label
|
|
|
|
|
|
@then('the ontology comment should be "{comment}"')
|
|
def step_then_comment(context: Context, comment: str) -> None:
|
|
assert context.ontology.version.comment == comment
|
|
|
|
|
|
@then('the version IRI should be "{iri}"')
|
|
def step_then_version_iri(context: Context, iri: str) -> None:
|
|
assert context.ontology.version.version_iri == iri
|
|
|
|
|
|
@then("no nodes should be present in the ontology")
|
|
def step_then_no_nodes(context: Context) -> None:
|
|
# The "Broken" node has no rdf:type, so the parser skips it
|
|
broken_nodes = [n for n in context.ontology.nodes if n.label == "Broken"]
|
|
assert len(broken_nodes) == 0, f"Expected no 'Broken' nodes, found {broken_nodes}"
|
|
|
|
|
|
# REMOVED: Duplicate @then step - already defined in subplan_service_coverage_boost_steps.py
|
|
|
|
|
|
@then("there should be no validation errors")
|
|
def step_then_no_errors(context: Context) -> None:
|
|
assert context.validation_errors == [], (
|
|
f"Expected no errors, got: {context.validation_errors}"
|
|
)
|
|
|
|
|
|
@then("the inheritance chain should have at least {count:d} nodes")
|
|
def step_then_chain_length(context: Context, count: int) -> None:
|
|
assert len(context.inheritance_chain) >= count, (
|
|
f"Expected >= {count} nodes, got {len(context.inheritance_chain)}"
|
|
)
|
|
|
|
|
|
@then('the first node in the chain should have label "{label}"')
|
|
def step_then_first_chain_label(context: Context, label: str) -> None:
|
|
assert context.inheritance_chain[0].label == label, (
|
|
f"Expected '{label}', got '{context.inheritance_chain[0].label}'"
|
|
)
|
|
|
|
|
|
@then('the last node in the chain should have label "{label}"')
|
|
def step_then_last_chain_label(context: Context, label: str) -> None:
|
|
assert context.inheritance_chain[-1].label == label, (
|
|
f"Expected '{label}', got '{context.inheritance_chain[-1].label}'"
|
|
)
|
|
|
|
|
|
@then("there should be at least {count:d} nodes")
|
|
def step_then_min_nodes(context: Context, count: int) -> None:
|
|
assert len(context.layer_nodes) >= count, (
|
|
f"Expected >= {count}, got {len(context.layer_nodes)}"
|
|
)
|
|
|
|
|
|
@then("the ontology should have prefixes defined")
|
|
def step_then_has_prefixes(context: Context) -> None:
|
|
assert len(context.ontology.prefixes) > 0
|
|
|
|
|
|
@then("the ontology should have nodes defined")
|
|
def step_then_has_nodes(context: Context) -> None:
|
|
assert len(context.ontology.nodes) > 0
|
|
|
|
|
|
@then('"{version}" should be in the supported versions')
|
|
def step_then_supported_version(context: Context, version: str) -> None:
|
|
assert version in context.ontology.supported_versions
|
|
|
|
|
|
@then("the node count should be {count:d}")
|
|
def step_node_count(context: Context, count: int) -> None:
|
|
assert len(context.layer_nodes) == count, (
|
|
f"Expected {count} nodes but got {len(context.layer_nodes)}"
|
|
)
|
|
|
|
|
|
@then('validation errors should include "{text}"')
|
|
def step_validation_errors_include(context: Context, text: str) -> None:
|
|
assert any(text in e for e in context.validation_errors), (
|
|
f"Expected '{text}' in validation errors: {context.validation_errors}"
|
|
)
|
|
|
|
|
|
@then('the node with label "{label}" should have parent "{parent_uri}"')
|
|
def step_then_node_has_parent(
|
|
context: Context,
|
|
label: str,
|
|
parent_uri: str,
|
|
) -> None:
|
|
matching = [n for n in context.layer_nodes if n.label == label]
|
|
assert matching, f"No node with label '{label}' found in layer nodes"
|
|
node = matching[0]
|
|
assert parent_uri in node.parent_uris, (
|
|
f"Expected parent '{parent_uri}' for node '{label}'; got: {node.parent_uris}"
|
|
)
|
|
|
|
|
|
@then(
|
|
'the node with label "{label}" should have property "{prop}" resolved to a full URI'
|
|
)
|
|
def step_then_property_resolved(context: Context, label: str, prop: str) -> None:
|
|
matching = [n for n in context.layer_nodes if n.label == label]
|
|
assert matching, f"No node with label '{label}' found in layer nodes"
|
|
node = matching[0]
|
|
prop_dict = dict(node.properties)
|
|
assert prop in prop_dict, (
|
|
f"Property '{prop}' not found on node '{label}'; have: {list(prop_dict)}"
|
|
)
|
|
value = prop_dict[prop]
|
|
assert value.startswith("http://") or value.startswith("https://"), (
|
|
f"Expected full URI for '{prop}' on '{label}', got: '{value}'"
|
|
)
|
|
|
|
|
|
@then(
|
|
'the node with label "{label}" should have property "{prop}" equal to "{expected}"'
|
|
)
|
|
def step_then_property_equal(
|
|
context: Context,
|
|
label: str,
|
|
prop: str,
|
|
expected: str,
|
|
) -> None:
|
|
matching = [n for n in context.layer_nodes if n.label == label]
|
|
assert matching, f"No node with label '{label}' found in layer nodes"
|
|
node = matching[0]
|
|
prop_dict = dict(node.properties)
|
|
assert prop in prop_dict, (
|
|
f"Property '{prop}' not found on node '{label}'; have: {list(prop_dict)}"
|
|
)
|
|
assert prop_dict[prop] == expected, (
|
|
f"Expected '{prop}' == '{expected}' on '{label}', got: '{prop_dict[prop]}'"
|
|
)
|
|
|
|
|
|
@then('the node with label "{label}" should not have property "{prop}"')
|
|
def step_then_property_absent(context: Context, label: str, prop: str) -> None:
|
|
matching = [n for n in context.layer_nodes if n.label == label]
|
|
assert matching, f"No node with label '{label}' found in layer nodes"
|
|
node = matching[0]
|
|
prop_dict = dict(node.properties)
|
|
assert prop not in prop_dict, (
|
|
f"Expected no '{prop}' on '{label}', but found: '{prop_dict[prop]}'"
|
|
)
|
|
|
|
|
|
@then("a cycle detection error should be raised")
|
|
def step_then_cycle_error(context: Context) -> None:
|
|
assert context.cycle_error is not None, "Expected cycle error but none raised"
|
|
assert "Cycle" in context.cycle_error or "cycle" in context.cycle_error
|
|
|
|
|
|
@then('a validation error should mention "{text}"')
|
|
def step_then_validation_error_mention(context: Context, text: str) -> None:
|
|
assert context.validation_error_msg is not None, (
|
|
"Expected a UKOValidationError but none was raised"
|
|
)
|
|
assert text in context.validation_error_msg, (
|
|
f"Expected '{text}' in error message: {context.validation_error_msg}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Duplicate-key handling in properties tuple
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a UKO node with duplicate property keys")
|
|
def step_given_duplicate_props(context: Context) -> None:
|
|
context.dup_node = UKONode(
|
|
uri="https://cleveragents.ai/ontology/uko#TestDup",
|
|
rdf_type="owl:DatatypeProperty",
|
|
label="TestDup",
|
|
properties=(
|
|
("rdfs:domain", "https://cleveragents.ai/ontology/uko#First"),
|
|
("rdfs:domain", "https://cleveragents.ai/ontology/uko#Second"),
|
|
),
|
|
)
|
|
|
|
|
|
@when("I convert the node properties to a dict")
|
|
def step_when_convert_props_dict(context: Context) -> None:
|
|
context.prop_dict = dict(context.dup_node.properties)
|
|
|
|
|
|
@then("the dict should contain only the last value for the duplicated key")
|
|
def step_then_last_value(context: Context) -> None:
|
|
assert context.prop_dict["rdfs:domain"] == (
|
|
"https://cleveragents.ai/ontology/uko#Second"
|
|
), f"Expected last value 'Second', got: {context.prop_dict['rdfs:domain']}"
|