808 lines
28 KiB
Python
808 lines
28 KiB
Python
"""Step definitions for resource_repository.feature."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
from datetime import UTC, datetime
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from sqlalchemy import create_engine, event
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from cleveragents.domain.models.core.resource import (
|
|
PhysVirt,
|
|
Resource,
|
|
ResourceCapabilities,
|
|
)
|
|
from cleveragents.domain.models.core.resource_type import (
|
|
ResourceKind,
|
|
ResourceTypeArgument,
|
|
ResourceTypeSpec,
|
|
SandboxStrategy,
|
|
)
|
|
from cleveragents.infrastructure.database.models import (
|
|
Base,
|
|
ResourceEdgeModel,
|
|
)
|
|
from cleveragents.infrastructure.database.repositories import (
|
|
DuplicateResourceError,
|
|
DuplicateResourceTypeError,
|
|
ResourceHasEdgesError,
|
|
ResourceNotFoundRepoError,
|
|
ResourceRepository,
|
|
ResourceTypeHasResourcesError,
|
|
ResourceTypeNotFoundError,
|
|
ResourceTypeRepository,
|
|
)
|
|
|
|
# Crockford base32 alphabet for generating ULIDs
|
|
_CB32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
_ULID_CTR = 0
|
|
|
|
|
|
def _next_ulid() -> str:
|
|
"""Return a unique, valid ULID string for each call."""
|
|
global _ULID_CTR
|
|
_ULID_CTR += 1
|
|
n = _ULID_CTR
|
|
suffix = ""
|
|
for _ in range(8):
|
|
suffix = _CB32[n % 32] + suffix
|
|
n //= 32
|
|
return f"01HGZ6FE0AQDYTR4BX{suffix}"
|
|
|
|
|
|
def _make_resource_type_spec(
|
|
name: str = "myorg/test-type",
|
|
user_addable: bool = True,
|
|
) -> ResourceTypeSpec:
|
|
"""Create a minimal valid ResourceTypeSpec."""
|
|
return ResourceTypeSpec(
|
|
name=name,
|
|
description="Test resource type",
|
|
resource_kind=ResourceKind.PHYSICAL,
|
|
sandbox_strategy=SandboxStrategy.GIT_WORKTREE,
|
|
user_addable=user_addable,
|
|
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,
|
|
)
|
|
|
|
|
|
def _make_resource(
|
|
name: str | None = "myorg/test-resource",
|
|
type_name: str = "myorg/test-type",
|
|
resource_id: str | None = None,
|
|
) -> Resource:
|
|
"""Create a minimal valid Resource domain object."""
|
|
return Resource(
|
|
resource_id=resource_id or _next_ulid(),
|
|
name=name,
|
|
resource_type_name=type_name,
|
|
classification=PhysVirt.PHYSICAL,
|
|
description="Test resource",
|
|
properties={},
|
|
location="/tmp/test",
|
|
content_hash=None,
|
|
sandbox_strategy=None,
|
|
capabilities=ResourceCapabilities(
|
|
readable=True,
|
|
writable=True,
|
|
sandboxable=True,
|
|
checkpointable=False,
|
|
),
|
|
created_at=datetime.now(tz=UTC),
|
|
updated_at=datetime.now(tz=UTC),
|
|
)
|
|
|
|
|
|
# ── Background ────────────────────────────────────────────────
|
|
|
|
|
|
@given("a clean resource repository database")
|
|
def step_clean_resource_repo_db(context: Context) -> None:
|
|
from sqlalchemy.pool import StaticPool
|
|
|
|
engine = create_engine(
|
|
"sqlite:///:memory:",
|
|
poolclass=StaticPool,
|
|
connect_args={"check_same_thread": False},
|
|
)
|
|
|
|
@event.listens_for(engine, "connect")
|
|
def _set_fk_pragma(dbapi_conn: object, _rec: object) -> None:
|
|
cursor = dbapi_conn.cursor() # type: ignore[union-attr]
|
|
cursor.execute("PRAGMA foreign_keys=ON")
|
|
cursor.close()
|
|
|
|
Base.metadata.create_all(engine)
|
|
|
|
# Use a single shared session so that flush() data is visible across
|
|
# all repository calls within the same scenario (avoids rollback issues
|
|
# when multiple Session objects share the same SQLite :memory: connection).
|
|
_shared_session = sessionmaker(bind=engine)()
|
|
|
|
def _session_factory() -> object:
|
|
return _shared_session
|
|
|
|
context.repo_session_factory = _session_factory
|
|
context.repo_engine = engine
|
|
context.repo_error = None
|
|
|
|
|
|
@given("a resource type repository backed by the database")
|
|
def step_resource_type_repo(context: Context) -> None:
|
|
context.rt_repo = ResourceTypeRepository(context.repo_session_factory)
|
|
|
|
|
|
@given("a resource repository backed by the database")
|
|
def step_resource_repo(context: Context) -> None:
|
|
context.res_repo = ResourceRepository(context.repo_session_factory)
|
|
|
|
|
|
# ── ResourceTypeRepository: Create ────────────────────────────
|
|
|
|
|
|
@given('a valid resource type spec named "{name}"')
|
|
def step_valid_rt_spec(context: Context, name: str) -> None:
|
|
context.rt_spec = _make_resource_type_spec(name=name)
|
|
|
|
|
|
@given("the resource type has already been saved once")
|
|
def step_rt_saved_once(context: Context) -> None:
|
|
context.rt_repo.create(context.rt_spec)
|
|
|
|
|
|
@when("the resource type is saved through the repository")
|
|
def step_save_rt(context: Context) -> None:
|
|
try:
|
|
context.rt_repo.create(context.rt_spec)
|
|
context.repo_error = None
|
|
except Exception as exc:
|
|
context.repo_error = exc
|
|
|
|
|
|
@when('a second resource type with the same name "{name}" is saved')
|
|
def step_save_dup_rt(context: Context, name: str) -> None:
|
|
try:
|
|
dup = _make_resource_type_spec(name=name)
|
|
context.rt_repo.create(dup)
|
|
context.repo_error = None
|
|
except Exception as exc:
|
|
context.repo_error = exc
|
|
|
|
|
|
@then("the resource type repository should not raise an error")
|
|
def step_rt_no_error(context: Context) -> None:
|
|
assert context.repo_error is None, f"Unexpected error: {context.repo_error}"
|
|
|
|
|
|
@then('the persisted resource type should have name "{name}"')
|
|
def step_rt_persisted_name(context: Context, name: str) -> None:
|
|
result = context.rt_repo.get(name)
|
|
assert result is not None, f"Resource type '{name}' not found"
|
|
assert result.name == name
|
|
|
|
|
|
@then('a DuplicateResourceTypeError should be raised mentioning "{name}"')
|
|
def step_dup_rt_error(context: Context, name: str) -> None:
|
|
assert isinstance(context.repo_error, DuplicateResourceTypeError), (
|
|
f"Expected DuplicateResourceTypeError, got {type(context.repo_error)}"
|
|
)
|
|
assert name in str(context.repo_error)
|
|
|
|
|
|
# ── ResourceTypeRepository: Read ──────────────────────────────
|
|
|
|
|
|
@given("the resource type has been saved through the repository")
|
|
def step_rt_saved(context: Context) -> None:
|
|
context.rt_repo.create(context.rt_spec)
|
|
|
|
|
|
@when('the resource type is looked up by name "{name}"')
|
|
def step_lookup_rt_by_name(context: Context, name: str) -> None:
|
|
context.rt_result = context.rt_repo.get(name)
|
|
|
|
|
|
@then('the returned resource type should have name "{name}"')
|
|
def step_rt_has_name(context: Context, name: str) -> None:
|
|
assert context.rt_result is not None, "Expected a resource type, got None"
|
|
assert context.rt_result.name == name
|
|
|
|
|
|
@then("no resource type should be returned")
|
|
def step_no_rt_returned(context: Context) -> None:
|
|
assert context.rt_result is None, f"Expected None, got {context.rt_result}"
|
|
|
|
|
|
# ── ResourceTypeRepository: List ──────────────────────────────
|
|
|
|
|
|
@given('resource types "{name1}" and "{name2}" have been saved')
|
|
def step_two_rts_saved(context: Context, name1: str, name2: str) -> None:
|
|
context.rt_repo.create(_make_resource_type_spec(name=name1))
|
|
context.rt_repo.create(_make_resource_type_spec(name=name2))
|
|
|
|
|
|
@when("all resource types are listed")
|
|
def step_list_all_rts(context: Context) -> None:
|
|
context.rt_list = context.rt_repo.list_types()
|
|
|
|
|
|
@when('resource types are listed with namespace "{namespace}"')
|
|
def step_list_rts_by_namespace(context: Context, namespace: str) -> None:
|
|
context.rt_list = context.rt_repo.list_types(namespace=namespace)
|
|
|
|
|
|
@given('a resource type "{name}" with user_addable {flag} is saved')
|
|
def step_rt_with_addable_flag(context: Context, name: str, flag: str) -> None:
|
|
spec = _make_resource_type_spec(name=name, user_addable=flag.lower() == "true")
|
|
context.rt_repo.create(spec)
|
|
|
|
|
|
@when("resource types are listed with user_addable {flag}")
|
|
def step_list_rts_by_addable(context: Context, flag: str) -> None:
|
|
context.rt_list = context.rt_repo.list_types(user_addable=flag.lower() == "true")
|
|
|
|
|
|
@then("the resource type list should have {count:d} entries")
|
|
def step_rt_list_count(context: Context, count: int) -> None:
|
|
assert len(context.rt_list) == count, (
|
|
f"Expected {count} entries, got {len(context.rt_list)}"
|
|
)
|
|
|
|
|
|
@then('the first resource type in the list should be "{name}"')
|
|
def step_rt_list_first(context: Context, name: str) -> None:
|
|
assert context.rt_list[0].name == name, (
|
|
f"Expected first entry '{name}', got '{context.rt_list[0].name}'"
|
|
)
|
|
|
|
|
|
# ── ResourceTypeRepository: Update ────────────────────────────
|
|
|
|
|
|
@when('the resource type description is updated to "{desc}"')
|
|
def step_update_rt_desc(context: Context, desc: str) -> None:
|
|
updated = ResourceTypeSpec(
|
|
name=context.rt_spec.name,
|
|
description=desc,
|
|
resource_kind=context.rt_spec.resource_kind,
|
|
sandbox_strategy=context.rt_spec.sandbox_strategy,
|
|
user_addable=context.rt_spec.user_addable,
|
|
cli_args=context.rt_spec.cli_args,
|
|
parent_types=context.rt_spec.parent_types,
|
|
child_types=context.rt_spec.child_types,
|
|
auto_discovery=context.rt_spec.auto_discovery,
|
|
equivalence=context.rt_spec.equivalence,
|
|
handler=context.rt_spec.handler,
|
|
capabilities=context.rt_spec.capabilities,
|
|
built_in=context.rt_spec.built_in,
|
|
)
|
|
context.rt_repo.update(updated)
|
|
|
|
|
|
@then('the resource type should be retrievable with description "{desc}"')
|
|
def step_rt_has_desc(context: Context, desc: str) -> None:
|
|
result = context.rt_repo.get(context.rt_spec.name)
|
|
assert result is not None
|
|
assert result.description == desc, (
|
|
f"Expected description '{desc}', got '{result.description}'"
|
|
)
|
|
|
|
|
|
@when('a non-existent resource type "{name}" is updated')
|
|
def step_update_nonexistent_rt(context: Context, name: str) -> None:
|
|
try:
|
|
fake = _make_resource_type_spec(name=name)
|
|
context.rt_repo.update(fake)
|
|
context.repo_error = None
|
|
except Exception as exc:
|
|
context.repo_error = exc
|
|
|
|
|
|
@then("a ResourceTypeNotFoundError should be raised")
|
|
def step_rt_not_found_error(context: Context) -> None:
|
|
assert isinstance(context.repo_error, ResourceTypeNotFoundError), (
|
|
f"Expected ResourceTypeNotFoundError, got {type(context.repo_error)}"
|
|
)
|
|
|
|
|
|
# ── ResourceTypeRepository: Delete ────────────────────────────
|
|
|
|
|
|
@when('the resource type "{name}" is deleted')
|
|
def step_delete_rt(context: Context, name: str) -> None:
|
|
try:
|
|
context.delete_result = context.rt_repo.delete(name)
|
|
context.repo_error = None
|
|
except Exception as exc:
|
|
context.repo_error = exc
|
|
context.delete_result = None
|
|
|
|
|
|
@then("the resource type deletion should return true")
|
|
def step_rt_delete_true(context: Context) -> None:
|
|
assert context.delete_result is True, f"Expected True, got {context.delete_result}"
|
|
|
|
|
|
@then("the resource type deletion should return false")
|
|
def step_rt_delete_false(context: Context) -> None:
|
|
assert context.delete_result is False, (
|
|
f"Expected False, got {context.delete_result}"
|
|
)
|
|
|
|
|
|
@then('the resource type "{name}" should no longer exist')
|
|
def step_rt_no_longer_exists(context: Context, name: str) -> None:
|
|
result = context.rt_repo.get(name)
|
|
assert result is None, f"Resource type '{name}' still exists"
|
|
|
|
|
|
@given('a resource of type "{type_name}" exists in the database')
|
|
def step_resource_of_type_exists(context: Context, type_name: str) -> None:
|
|
res = _make_resource(name=None, type_name=type_name)
|
|
context.res_repo.create(res)
|
|
|
|
|
|
@then("a ResourceTypeHasResourcesError should be raised")
|
|
def step_rt_has_resources_error(context: Context) -> None:
|
|
assert isinstance(context.repo_error, ResourceTypeHasResourcesError), (
|
|
f"Expected ResourceTypeHasResourcesError, got {type(context.repo_error)}"
|
|
)
|
|
|
|
|
|
# ── ResourceRepository: Create ────────────────────────────────
|
|
|
|
|
|
@given('a resource type "{name}" exists for resource tests')
|
|
def step_rt_exists_for_res(context: Context, name: str) -> None:
|
|
with contextlib.suppress(DuplicateResourceTypeError):
|
|
context.rt_repo.create(_make_resource_type_spec(name=name))
|
|
context.res_type_name = name
|
|
|
|
|
|
@given('a valid resource domain object with name "{name}"')
|
|
def step_valid_res_obj(context: Context, name: str) -> None:
|
|
type_name = "myorg/res-type"
|
|
if hasattr(context, "res_type_name"):
|
|
type_name = context.res_type_name
|
|
context.res_obj = _make_resource(name=name, type_name=type_name)
|
|
context.res_type_name = type_name
|
|
|
|
|
|
@given('a valid resource domain object with type "{type_name}"')
|
|
def step_valid_res_obj_with_type(context: Context, type_name: str) -> None:
|
|
context.res_obj = _make_resource(name="myorg/orphan-res", type_name=type_name)
|
|
|
|
|
|
@when("the resource is saved through the repository")
|
|
def step_save_res(context: Context) -> None:
|
|
try:
|
|
context.res_repo.create(context.res_obj)
|
|
context.repo_error = None
|
|
except Exception as exc:
|
|
context.repo_error = exc
|
|
|
|
|
|
@then("the resource repository should not raise an error")
|
|
def step_res_no_error(context: Context) -> None:
|
|
assert context.repo_error is None, f"Unexpected error: {context.repo_error}"
|
|
|
|
|
|
@then('the persisted resource should have name "{name}"')
|
|
def step_res_persisted_name(context: Context, name: str) -> None:
|
|
result = context.res_repo.get_by_name(name)
|
|
assert result is not None, f"Resource '{name}' not found"
|
|
assert result.name == name
|
|
|
|
|
|
@then('a ResourceTypeNotFoundError should be raised for type "{type_name}"')
|
|
def step_rt_not_found_for_type(context: Context, type_name: str) -> None:
|
|
assert isinstance(context.repo_error, ResourceTypeNotFoundError), (
|
|
f"Expected ResourceTypeNotFoundError, got {type(context.repo_error)}"
|
|
)
|
|
assert type_name in str(context.repo_error)
|
|
|
|
|
|
@given('a resource with name "{name}" has been saved')
|
|
def step_res_named_saved(context: Context, name: str) -> None:
|
|
# Determine the type name from the most recent resource type created
|
|
type_name = "myorg/res-type"
|
|
if hasattr(context, "res_type_name"):
|
|
type_name = context.res_type_name
|
|
else:
|
|
# Try to guess from existing resource types
|
|
for attr_name in ("rt_spec",):
|
|
if hasattr(context, attr_name):
|
|
type_name = getattr(context, attr_name).name
|
|
break
|
|
|
|
# Use type name from context if set up by previous step
|
|
rt_list = context.rt_repo.list_types()
|
|
if rt_list:
|
|
type_name = rt_list[-1].name
|
|
|
|
res = _make_resource(name=name, type_name=type_name)
|
|
context.res_repo.create(res)
|
|
context.last_saved_resource = res
|
|
context.res_type_name = type_name
|
|
|
|
|
|
@when('a second resource with name "{name}" is saved')
|
|
def step_save_dup_res(context: Context, name: str) -> None:
|
|
try:
|
|
dup = _make_resource(name=name, type_name=context.res_type_name)
|
|
context.res_repo.create(dup)
|
|
context.repo_error = None
|
|
except Exception as exc:
|
|
context.repo_error = exc
|
|
|
|
|
|
@then('a DuplicateResourceError should be raised mentioning "{name}"')
|
|
def step_dup_res_error(context: Context, name: str) -> None:
|
|
assert isinstance(context.repo_error, DuplicateResourceError), (
|
|
f"Expected DuplicateResourceError, got {type(context.repo_error)}"
|
|
)
|
|
assert name in str(context.repo_error)
|
|
|
|
|
|
# ── ResourceRepository: Read ──────────────────────────────────
|
|
|
|
|
|
@given("a resource with known ULID has been saved")
|
|
def step_res_known_ulid_saved(context: Context) -> None:
|
|
rt_list = context.rt_repo.list_types()
|
|
type_name = rt_list[-1].name if rt_list else "myorg/get-type"
|
|
ulid = _next_ulid()
|
|
res = _make_resource(
|
|
name=f"myorg/ulid-res-{ulid[-6:]}",
|
|
type_name=type_name,
|
|
resource_id=ulid,
|
|
)
|
|
context.res_repo.create(res)
|
|
context.known_ulid = ulid
|
|
context.last_saved_resource = res
|
|
|
|
|
|
@when("the resource is looked up by its ULID")
|
|
def step_lookup_res_by_ulid(context: Context) -> None:
|
|
context.res_result = context.res_repo.get(context.known_ulid)
|
|
|
|
|
|
@then("the returned resource should have the correct ULID")
|
|
def step_res_has_ulid(context: Context) -> None:
|
|
assert context.res_result is not None, "Expected a resource, got None"
|
|
assert context.res_result.resource_id == context.known_ulid
|
|
|
|
|
|
@when('the resource is looked up by name "{name}"')
|
|
def step_lookup_res_by_name(context: Context, name: str) -> None:
|
|
context.res_result = context.res_repo.get_by_name(name)
|
|
|
|
|
|
@then('the returned resource should have name "{name}"')
|
|
def step_res_has_name(context: Context, name: str) -> None:
|
|
assert context.res_result is not None, "Expected a resource, got None"
|
|
assert context.res_result.name == name
|
|
|
|
|
|
@when('the resource is looked up by ULID "{ulid}"')
|
|
def step_lookup_res_by_specific_ulid(context: Context, ulid: str) -> None:
|
|
context.res_result = context.res_repo.get(ulid)
|
|
|
|
|
|
@then("no resource should be returned")
|
|
def step_no_res_returned(context: Context) -> None:
|
|
assert context.res_result is None, f"Expected None, got {context.res_result}"
|
|
|
|
|
|
# ── ResourceRepository: List ──────────────────────────────────
|
|
|
|
|
|
@given('{count:d} resources of type "{type_name}" have been saved')
|
|
def step_n_resources_saved(context: Context, count: int, type_name: str) -> None:
|
|
for i in range(count):
|
|
res = _make_resource(
|
|
name=f"myorg/batch-{type_name.split('/')[-1]}-{i}",
|
|
type_name=type_name,
|
|
)
|
|
context.res_repo.create(res)
|
|
|
|
|
|
@when("resources are listed with limit {limit:d} and offset {offset:d}")
|
|
def step_list_res_paginated(context: Context, limit: int, offset: int) -> None:
|
|
context.res_list = context.res_repo.list_resources(limit=limit, offset=offset)
|
|
|
|
|
|
@when('resources are listed with type_name "{type_name}"')
|
|
def step_list_res_by_type(context: Context, type_name: str) -> None:
|
|
context.res_list = context.res_repo.list_resources(type_name=type_name)
|
|
|
|
|
|
@given('resource types "{name1}" and "{name2}" exist for resource tests')
|
|
def step_two_rts_for_res(context: Context, name1: str, name2: str) -> None:
|
|
for n in (name1, name2):
|
|
with contextlib.suppress(DuplicateResourceTypeError):
|
|
context.rt_repo.create(_make_resource_type_spec(name=n))
|
|
|
|
|
|
@then("the resource list should have {count:d} entries")
|
|
def step_res_list_count(context: Context, count: int) -> None:
|
|
assert len(context.res_list) == count, (
|
|
f"Expected {count} entries, got {len(context.res_list)}"
|
|
)
|
|
|
|
|
|
# ── ResourceRepository: Update ────────────────────────────────
|
|
|
|
|
|
@when('the resource description is updated to "{desc}"')
|
|
def step_update_res_desc(context: Context, desc: str) -> None:
|
|
original = context.last_saved_resource
|
|
updated = Resource(
|
|
resource_id=original.resource_id,
|
|
name=original.name,
|
|
resource_type_name=original.resource_type_name,
|
|
classification=PhysVirt.PHYSICAL,
|
|
description=desc,
|
|
properties=original.properties,
|
|
location=original.location,
|
|
content_hash=original.content_hash,
|
|
sandbox_strategy=original.sandbox_strategy,
|
|
capabilities=original.capabilities,
|
|
created_at=original.created_at,
|
|
updated_at=datetime.now(tz=UTC),
|
|
)
|
|
context.res_repo.update(updated)
|
|
|
|
|
|
@then('the resource should be retrievable with description "{desc}"')
|
|
def step_res_has_desc(context: Context, desc: str) -> None:
|
|
result = context.res_repo.get(context.last_saved_resource.resource_id)
|
|
assert result is not None
|
|
assert result.description == desc, (
|
|
f"Expected description '{desc}', got '{result.description}'"
|
|
)
|
|
|
|
|
|
@when('a non-existent resource with ULID "{ulid}" is updated')
|
|
def step_update_nonexistent_res(context: Context, ulid: str) -> None:
|
|
try:
|
|
fake = _make_resource(
|
|
name="myorg/phantom-res",
|
|
type_name="myorg/phantom-type",
|
|
resource_id=ulid,
|
|
)
|
|
context.res_repo.update(fake)
|
|
context.repo_error = None
|
|
except Exception as exc:
|
|
context.repo_error = exc
|
|
|
|
|
|
@then("a ResourceNotFoundRepoError should be raised")
|
|
def step_res_not_found_error(context: Context) -> None:
|
|
assert isinstance(context.repo_error, ResourceNotFoundRepoError), (
|
|
f"Expected ResourceNotFoundRepoError, got {type(context.repo_error)}"
|
|
)
|
|
|
|
|
|
# ── ResourceRepository: Delete ────────────────────────────────
|
|
|
|
|
|
@when("the resource is deleted by its ULID")
|
|
def step_delete_res_by_ulid(context: Context) -> None:
|
|
try:
|
|
ulid = context.last_saved_resource.resource_id
|
|
context.delete_result = context.res_repo.delete(ulid)
|
|
context.repo_error = None
|
|
except Exception as exc:
|
|
context.repo_error = exc
|
|
context.delete_result = None
|
|
|
|
|
|
@then("the resource deletion should return true")
|
|
def step_res_delete_true(context: Context) -> None:
|
|
assert context.delete_result is True, f"Expected True, got {context.delete_result}"
|
|
|
|
|
|
@then("the resource deletion should return false")
|
|
def step_res_delete_false(context: Context) -> None:
|
|
assert context.delete_result is False, (
|
|
f"Expected False, got {context.delete_result}"
|
|
)
|
|
|
|
|
|
@then("the resource should no longer exist")
|
|
def step_res_no_longer_exists(context: Context) -> None:
|
|
result = context.res_repo.get(context.last_saved_resource.resource_id)
|
|
assert result is None, "Resource still exists"
|
|
|
|
|
|
@when('the resource with ULID "{ulid}" is deleted')
|
|
def step_delete_res_specific_ulid(context: Context, ulid: str) -> None:
|
|
try:
|
|
context.delete_result = context.res_repo.delete(ulid)
|
|
context.repo_error = None
|
|
except Exception as exc:
|
|
context.repo_error = exc
|
|
context.delete_result = None
|
|
|
|
|
|
@given('a parent and child resource of type "{type_name}" are linked')
|
|
def step_parent_child_linked(context: Context, type_name: str) -> None:
|
|
parent = _make_resource(
|
|
name=f"myorg/parent-{_next_ulid()[-6:]}",
|
|
type_name=type_name,
|
|
)
|
|
child = _make_resource(
|
|
name=f"myorg/child-{_next_ulid()[-6:]}",
|
|
type_name=type_name,
|
|
)
|
|
context.res_repo.create(parent)
|
|
context.res_repo.create(child)
|
|
context.parent_resource = parent
|
|
context.child_resource = child
|
|
|
|
# Insert edge directly via session
|
|
session: Session = context.repo_session_factory()
|
|
edge = ResourceEdgeModel(
|
|
parent_id=parent.resource_id,
|
|
child_id=child.resource_id,
|
|
link_type="contains",
|
|
auto_discovered=False,
|
|
created_at=datetime.now(tz=UTC).isoformat(),
|
|
)
|
|
session.add(edge)
|
|
session.flush()
|
|
session.commit()
|
|
|
|
|
|
@when("the parent resource is deleted")
|
|
def step_delete_parent_res(context: Context) -> None:
|
|
try:
|
|
context.delete_result = context.res_repo.delete(
|
|
context.parent_resource.resource_id
|
|
)
|
|
context.repo_error = None
|
|
except Exception as exc:
|
|
context.repo_error = exc
|
|
context.delete_result = None
|
|
|
|
|
|
@then("a ResourceHasEdgesError should be raised")
|
|
def step_res_has_edges_error(context: Context) -> None:
|
|
assert isinstance(context.repo_error, ResourceHasEdgesError), (
|
|
f"Expected ResourceHasEdgesError, got {type(context.repo_error)}"
|
|
)
|
|
|
|
|
|
# ── ResourceRepository: resolve_namespaced_name ───────────────
|
|
|
|
|
|
@when('the resource is resolved by name_or_id "{name_or_id}"')
|
|
def step_resolve_res(context: Context, name_or_id: str) -> None:
|
|
context.resolved_result = context.res_repo.resolve_namespaced_name(name_or_id)
|
|
|
|
|
|
@then('the resolved resource should have name "{name}"')
|
|
def step_resolved_has_name(context: Context, name: str) -> None:
|
|
assert context.resolved_result is not None, "Expected a resource, got None"
|
|
assert context.resolved_result.name == name
|
|
|
|
|
|
@then("no resolved resource should be returned")
|
|
def step_no_resolved_res(context: Context) -> None:
|
|
assert context.resolved_result is None, (
|
|
f"Expected None, got {context.resolved_result}"
|
|
)
|
|
|
|
|
|
@given("a resource with known ULID has been saved for resolve test")
|
|
def step_res_ulid_for_resolve(context: Context) -> None:
|
|
type_name = getattr(context, "res_type_name", "myorg/resolve-ulid-type")
|
|
ulid = _next_ulid()
|
|
res = _make_resource(
|
|
name=None,
|
|
type_name=type_name,
|
|
resource_id=ulid,
|
|
)
|
|
context.res_repo.create(res)
|
|
context.known_ulid = ulid
|
|
context.last_saved_resource = res
|
|
|
|
|
|
@when("the resource is resolved by its ULID")
|
|
def step_resolve_by_ulid(context: Context) -> None:
|
|
context.resolved_result = context.res_repo.resolve_namespaced_name(
|
|
context.known_ulid
|
|
)
|
|
|
|
|
|
@then("the resolved resource should have the correct ULID")
|
|
def step_resolved_has_ulid(context: Context) -> None:
|
|
assert context.resolved_result is not None, "Expected a resource, got None"
|
|
assert context.resolved_result.resource_id == context.known_ulid
|
|
|
|
|
|
# ── ResourceTypeRepository: domain round-trip ─────────────────
|
|
|
|
|
|
@given("a full resource type spec with cli_args and capabilities")
|
|
def step_full_rt_spec(context: Context) -> None:
|
|
context.full_rt_spec = ResourceTypeSpec(
|
|
name="myorg/full-type",
|
|
description="Full test type with all fields",
|
|
resource_kind=ResourceKind.PHYSICAL,
|
|
sandbox_strategy=SandboxStrategy.COPY_ON_WRITE,
|
|
user_addable=True,
|
|
cli_args=[
|
|
ResourceTypeArgument(
|
|
name="path",
|
|
type="path",
|
|
required=True,
|
|
description="Path to resource",
|
|
),
|
|
ResourceTypeArgument(
|
|
name="branch",
|
|
type="string",
|
|
required=False,
|
|
description="Branch name",
|
|
default="main",
|
|
),
|
|
],
|
|
parent_types=["myorg/parent-type"],
|
|
child_types=["myorg/child-type"],
|
|
auto_discovery={"strategy": "glob", "pattern": "**/*.py"},
|
|
equivalence=None,
|
|
handler="myorg.handlers:FullHandler",
|
|
capabilities={
|
|
"read": True,
|
|
"write": True,
|
|
"sandbox": True,
|
|
"checkpoint": True,
|
|
},
|
|
built_in=False,
|
|
)
|
|
|
|
|
|
@when("the full resource type is saved and retrieved")
|
|
def step_save_and_retrieve_full_rt(context: Context) -> None:
|
|
context.rt_repo.create(context.full_rt_spec)
|
|
context.restored_rt = context.rt_repo.get(context.full_rt_spec.name)
|
|
|
|
|
|
@then("the restored resource type should match the original fields")
|
|
def step_rt_fields_match(context: Context) -> None:
|
|
original = context.full_rt_spec
|
|
restored = context.restored_rt
|
|
assert restored is not None, "Restored resource type is None"
|
|
assert restored.name == original.name
|
|
assert restored.description == original.description
|
|
assert restored.resource_kind == original.resource_kind
|
|
assert restored.sandbox_strategy == original.sandbox_strategy
|
|
assert restored.user_addable == original.user_addable
|
|
assert len(restored.cli_args) == len(original.cli_args)
|
|
assert restored.cli_args[0].name == "path"
|
|
assert restored.cli_args[0].type == "path"
|
|
assert restored.cli_args[0].required is True
|
|
assert restored.cli_args[1].name == "branch"
|
|
assert restored.cli_args[1].default == "main"
|
|
assert restored.parent_types == original.parent_types
|
|
assert restored.child_types == original.child_types
|
|
assert restored.auto_discovery == original.auto_discovery
|
|
assert restored.handler == original.handler
|
|
assert restored.capabilities["checkpoint"] is True
|