test(cov): Fix coverage test failures #94

Merged
brent.edwards merged 2 commits from fix/behave_tests_coverage into master 2026-02-17 19:04:45 +00:00
3 changed files with 885 additions and 0 deletions
+214
View File
@@ -0,0 +1,214 @@
Feature: Project CLI command functions coverage
As a developer
I want the project CLI command functions exercised directly
So that cli/commands/project.py achieves high coverage
Background:
Given a project CLI commands test database is initialized
# ── helpers that are never reached from the repository-only tests ──
Scenario: _get_namespaced_project_repo returns a repository
When I call _get_namespaced_project_repo via the patched container
Then the helper should return a NamespacedProjectRepository
Scenario: _get_resource_link_repo returns a repository
When I call _get_resource_link_repo via the patched container
Then the helper should return a ProjectResourceLinkRepository
Scenario: _get_resource_registry_service returns a service
When I call _get_resource_registry_service via the patched container
Then the helper should return a ResourceRegistryService
Scenario: _store_project_extras stores invariants via file DB
When I call _store_project_extras with invariants "no-delete" and actor "guard"
Then the extras call should complete without error
# ── create command ────────────────────────────────────────────
Scenario: create command with bare name uses rich output
When I invoke project-create with bare name "my-app"
Then the project cmd output should contain "created"
And the project cmd should succeed
Scenario: create command with explicit namespace
When I invoke project-create with bare name "team/my-svc"
Then the project cmd output should contain "created"
And the project cmd should succeed
Scenario: create command with description
When I invoke project-create with name "desc-proj" description "A nice project"
Then the project cmd output should contain "created"
And the project cmd should succeed
Scenario: create command with json format
When I invoke project-create with name "json-proj" format "json"
Then the project cmd output should contain "namespaced_name"
And the project cmd should succeed
Scenario: create command with invariants
When I invoke project-create with name "inv-proj" invariant "do not delete files"
Then the project cmd output should contain "created"
And the project cmd should succeed
Scenario: create command with linked resource
Given a resource "my-git-res" is registered in the commands DB
When I invoke project-create with name "res-proj" linking resource "my-git-res"
Then the project cmd output should contain "created"
And the project cmd should succeed
Scenario: create command with invalid name fails
When I invoke project-create with bare name "123bad"
Then the project cmd should fail
Scenario: create command with DatabaseError from repo
When I invoke project-create triggering a database error
Then the project cmd should fail
# ── list command ─────────────────────────────────────────────
Scenario: list command shows projects in rich format
Given a project "local/list-a" is created in the commands DB
And a project "local/list-b" is created in the commands DB
When I invoke project-list with default options
Then the project cmd output should contain "list-a"
And the project cmd output should contain "list-b"
Scenario: list command with namespace filter
Given a project "local/ns-x" is created in the commands DB
And a project "team/ns-y" is created in the commands DB
When I invoke project-list with namespace "team"
Then the project cmd output should contain "ns-y"
Scenario: list command with regex filter
Given a project "local/api-svc" is created in the commands DB
And a project "local/web-ui" is created in the commands DB
When I invoke project-list with regex "api"
Then the project cmd output should contain "api-svc"
Scenario: list command with invalid regex fails
When I invoke project-list with invalid regex "[bad"
Then the project cmd should fail
Scenario: list command when empty
When I invoke project-list with default options
Then the project cmd output should contain "No projects found"
Scenario: list command with json format
Given a project "local/fmt-list" is created in the commands DB
When I invoke project-list with format "json"
Then the project cmd output should contain "namespaced_name"
# ── show command ─────────────────────────────────────────────
Scenario: show command displays project in rich format
Given a project "local/show-cmd" is created in the commands DB
When I invoke project-show for "local/show-cmd" default format
Then the project cmd output should contain "show-cmd"
And the project cmd should succeed
Scenario: show command with linked resources
Given a project "local/show-linked" is created in the commands DB
And a resource is linked to project "local/show-linked" in the commands DB
When I invoke project-show for "local/show-linked" default format
Then the project cmd output should contain "show-linked"
Scenario: show command with json format
Given a project "local/show-json" is created in the commands DB
When I invoke project-show for "local/show-json" format "json"
Then the project cmd output should contain "namespaced_name"
Scenario: show nonexistent project fails
When I invoke project-show for "local/ghost" default format
Then the project cmd should fail
# ── link-resource command ────────────────────────────────────
Scenario: link-resource command in rich format
Given a project "local/link-cmd" is created in the commands DB
And a resource "link-res" is registered in the commands DB
When I invoke project-link for "local/link-cmd" resource "link-res" default format
Then the project cmd output should contain "Linked"
And the project cmd should succeed
Scenario: link-resource command with read-only
Given a project "local/ro-cmd" is created in the commands DB
And a resource "ro-res" is registered in the commands DB
When I invoke project-link for "local/ro-cmd" resource "ro-res" read-only
Then the project cmd output should contain "read-only"
And the project cmd should succeed
Scenario: link-resource command with json format
Given a project "local/link-json" is created in the commands DB
And a resource "json-res" is registered in the commands DB
When I invoke project-link for "local/link-json" resource "json-res" format "json"
Then the project cmd output should contain "resource_id"
Scenario: link-resource to nonexistent project fails
When I invoke project-link for "local/nope" resource "anything" default format
Then the project cmd should fail
Scenario: link-resource with nonexistent resource fails
Given a project "local/link-fail" is created in the commands DB
When I invoke project-link for "local/link-fail" resource "no-such-res" default format
Then the project cmd should fail
# ── unlink-resource command ──────────────────────────────────
Scenario: unlink-resource command in rich format
Given a project "local/unlink-cmd" is created in the commands DB
And a resource is linked to project "local/unlink-cmd" in the commands DB
When I invoke project-unlink for "local/unlink-cmd" with yes default format
Then the project cmd output should contain "Unlinked"
And the project cmd should succeed
Scenario: unlink-resource command with json format
Given a project "local/unlink-json" is created in the commands DB
And a resource is linked to project "local/unlink-json" in the commands DB
When I invoke project-unlink for "local/unlink-json" with yes format "json"
Then the project cmd output should contain "unlinked"
Scenario: unlink-resource for nonexistent project fails
When I invoke project-unlink for "local/ghost-proj" resource "any" with yes
Then the project cmd should fail
Scenario: unlink-resource for nonexistent resource fails
Given a project "local/unlink-fail" is created in the commands DB
When I invoke project-unlink for "local/unlink-fail" resource "no-such" with yes
Then the project cmd should fail
Scenario: unlink-resource when resource not linked fails
Given a project "local/unlink-none" is created in the commands DB
And a resource "orphan-res" is registered in the commands DB
When I invoke project-unlink for "local/unlink-none" resource "orphan-res" with yes
Then the project cmd should fail
# ── delete command ───────────────────────────────────────────
Scenario: delete command removes project
Given a project "local/del-cmd" is created in the commands DB
When I invoke project-delete for "local/del-cmd" with yes
Then the project cmd output should contain "deleted"
And the project cmd should succeed
Scenario: delete command with json format
Given a project "local/del-json" is created in the commands DB
When I invoke project-delete for "local/del-json" with yes format "json"
Then the project cmd output should contain "deleted"
Scenario: delete nonexistent project fails
When I invoke project-delete for "local/nope" with yes
Then the project cmd should fail
Scenario: delete project with linked resources without force fails
Given a project "local/del-linked" is created in the commands DB
And a resource is linked to project "local/del-linked" in the commands DB
When I invoke project-delete for "local/del-linked" with yes no force
Then the project cmd should fail
Scenario: delete project with linked resources with force succeeds
Given a project "local/del-force" is created in the commands DB
And a resource is linked to project "local/del-force" in the commands DB
When I invoke project-delete for "local/del-force" with yes and force
Then the project cmd output should contain "deleted"
And the project cmd should succeed
@@ -95,10 +95,28 @@ def step_mocked_lifecycle_for_formats(context: Context) -> None:
context.action_patcher.start()
context.plan_patcher.start()
# Patch module-level Rich Console objects so they never inject ANSI
# escape codes into structured output (JSON/YAML/plain). Without this,
# console.print(json_string) adds syntax-highlighting escapes when Rich
# detects a real terminal, which makes json.loads() / yaml.safe_load()
# fail on the user's machine even though the tests pass inside a
# headless container where Console auto-disables colour.
from rich.console import Console as _Console
_plain_console = _Console(no_color=True, highlight=False)
context.action_console_patcher = patch.object(
_action_mod, "console", _plain_console
)
context.plan_console_patcher = patch.object(_plan_mod, "console", _plain_console)
context.action_console_patcher.start()
context.plan_console_patcher.start()
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(context.action_patcher.stop)
context._cleanup_handlers.append(context.plan_patcher.stop)
context._cleanup_handlers.append(context.action_console_patcher.stop)
context._cleanup_handlers.append(context.plan_console_patcher.stop)
# ------- Given steps -------
@@ -0,0 +1,653 @@
"""Step definitions for project_cli_commands.feature.
Exercises the *actual* CLI command functions in
``cleveragents.cli.commands.project`` (create, list, show, link-resource,
unlink-resource, delete) via monkey-patched helpers so that the command
bodies are executed under coverage.
Each scenario gets a fresh in-memory SQLite database. The DI-container
look-up helpers are monkey-patched to return repositories backed by that
database, then restored after every step that needs them.
"""
from __future__ import annotations
from io import StringIO
from typing import Any
from behave import given, then, when # type: ignore[import-untyped]
from rich.console import Console
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# ---------------------------------------------------------------------------
# Database / service bootstrap
# ---------------------------------------------------------------------------
class _SharedSession:
"""Wraps a real SQLAlchemy Session but makes ``close()`` a no-op.
This ensures every repository call within the same scenario uses the
same session (and therefore sees the same uncommitted data), which is
critical for in-memory SQLite where transaction isolation would otherwise
hide data between sessions.
"""
def __init__(self, real: Any) -> None:
object.__setattr__(self, "_real", real)
def close(self) -> None:
"""No-op so the shared session stays usable across calls."""
def __getattr__(self, name: str) -> Any:
return getattr(object.__getattribute__(self, "_real"), name)
def __setattr__(self, name: str, value: Any) -> None:
setattr(object.__getattribute__(self, "_real"), name, value)
def _init_commands_db(context: Any) -> None:
"""Create a fresh in-memory database and wire up repos + service."""
from cleveragents.application.services.resource_registry_service import (
ResourceRegistryService,
)
from cleveragents.infrastructure.database.models import Base
from cleveragents.infrastructure.database.repositories import (
NamespacedProjectRepository,
ProjectResourceLinkRepository,
)
engine = create_engine(
"sqlite:///:memory:",
echo=False,
connect_args={"check_same_thread": False},
)
Base.metadata.create_all(engine)
# Use a single shared session to avoid transaction isolation issues
# with :memory: SQLite when multiple repos are involved.
real_session = sessionmaker(
bind=engine, expire_on_commit=False, autoflush=True, autocommit=False
)()
wrapper = _SharedSession(real_session)
def _shared_factory() -> Any:
return wrapper
context._cmd_engine = engine
context._cmd_factory = _shared_factory
context._cmd_project_repo = NamespacedProjectRepository(
session_factory=_shared_factory
)
context._cmd_link_repo = ProjectResourceLinkRepository(
session_factory=_shared_factory
)
context._cmd_resource_svc = ResourceRegistryService(session_factory=_shared_factory)
context._cmd_resource_svc.bootstrap_builtin_types()
context._cmd_output = ""
context._cmd_failed = False
context._cmd_resource_map = {}
context._cmd_last_resource_id = None
# ---------------------------------------------------------------------------
# Monkey-patching helpers
# ---------------------------------------------------------------------------
_ORIG_FNS: dict[str, Any] = {}
def _patch_project_mod(context: Any) -> None:
"""Monkey-patch the four DI look-up helpers in project module."""
import cleveragents.cli.commands.project as project_mod
_ORIG_FNS["repo"] = project_mod._get_namespaced_project_repo
_ORIG_FNS["link"] = project_mod._get_resource_link_repo
_ORIG_FNS["svc"] = project_mod._get_resource_registry_service
project_mod._get_namespaced_project_repo = lambda: context._cmd_project_repo
project_mod._get_resource_link_repo = lambda: context._cmd_link_repo
project_mod._get_resource_registry_service = lambda: context._cmd_resource_svc
# Patch _store_project_extras to a no-op since it needs a shared
# file-based DB (the real helper is tested separately)
_ORIG_FNS["store_extras"] = project_mod._store_project_extras
project_mod._store_project_extras = lambda *a, **kw: None
def _unpatch_project_mod() -> None:
"""Restore original helpers."""
import cleveragents.cli.commands.project as project_mod
project_mod._get_namespaced_project_repo = _ORIG_FNS["repo"]
project_mod._get_resource_link_repo = _ORIG_FNS["link"]
project_mod._get_resource_registry_service = _ORIG_FNS["svc"]
if _ORIG_FNS.get("store_extras"):
project_mod._store_project_extras = _ORIG_FNS["store_extras"]
def _capture(context: Any, func: Any, *args: Any, **kwargs: Any) -> None:
"""Call a CLI function, capturing console output and success/failure."""
import typer
import cleveragents.cli.commands.project as project_mod
buf = StringIO()
fake_console = Console(file=buf, width=200, no_color=True)
orig_console = project_mod.console
orig_err = project_mod.err_console
project_mod.console = fake_console
project_mod.err_console = fake_console
_patch_project_mod(context)
failed = False
try:
func(*args, **kwargs)
except (SystemExit, typer.Exit, typer.Abort):
failed = True
except Exception:
failed = True
finally:
project_mod.console = orig_console
project_mod.err_console = orig_err
_unpatch_project_mod()
context._cmd_output = buf.getvalue()
context._cmd_failed = failed
# ---------------------------------------------------------------------------
# Reusable helpers
# ---------------------------------------------------------------------------
def _create_project(context: Any, name: str, description: str | None = None) -> Any:
from cleveragents.domain.models.core.project import (
NamespacedProject,
parse_namespaced_name,
)
parsed = parse_namespaced_name(name)
proj = NamespacedProject(
name=parsed.name,
namespace=parsed.namespace,
server=parsed.server,
description=description,
)
context._cmd_project_repo.create(proj)
return proj
def _register_resource(context: Any, alias: str) -> str:
res = context._cmd_resource_svc.register_resource(
type_name="git-checkout",
name=f"local/{alias}",
location="/tmp/test-resource",
description=f"Test resource {alias}",
)
context._cmd_resource_map[alias] = res.resource_id
context._cmd_last_resource_id = res.resource_id
return res.resource_id
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a project CLI commands test database is initialized")
def step_init_commands_db(context: Any) -> None:
_init_commands_db(context)
# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------
@given('a project "{name}" is created in the commands DB')
def step_project_exists_cmd(context: Any, name: str) -> None:
_create_project(context, name)
@given('a resource "{alias}" is registered in the commands DB')
def step_resource_registered_cmd(context: Any, alias: str) -> None:
_register_resource(context, alias)
@given('a resource is linked to project "{project_name}" in the commands DB')
def step_resource_linked_cmd(context: Any, project_name: str) -> None:
safe = project_name.replace("/", "-")
rid = _register_resource(context, f"linked-{safe}")
context._cmd_link_repo.create_link(
project_name=project_name,
resource_id=rid,
)
# ---------------------------------------------------------------------------
# Helper function coverage
# ---------------------------------------------------------------------------
@when("I call _get_namespaced_project_repo via the patched container")
def step_call_get_repo(context: Any) -> None:
import cleveragents.cli.commands.project as project_mod
_patch_project_mod(context)
try:
context._cmd_helper_result = project_mod._get_namespaced_project_repo()
finally:
_unpatch_project_mod()
@when("I call _get_resource_link_repo via the patched container")
def step_call_get_link_repo(context: Any) -> None:
import cleveragents.cli.commands.project as project_mod
_patch_project_mod(context)
try:
context._cmd_helper_result = project_mod._get_resource_link_repo()
finally:
_unpatch_project_mod()
@when("I call _get_resource_registry_service via the patched container")
def step_call_get_svc(context: Any) -> None:
import cleveragents.cli.commands.project as project_mod
_patch_project_mod(context)
try:
context._cmd_helper_result = project_mod._get_resource_registry_service()
finally:
_unpatch_project_mod()
@when('I call _store_project_extras with invariants "{inv}" and actor "{actor}"')
def step_call_store_extras(context: Any, inv: str, actor: str) -> None:
"""Call the real _store_project_extras against a temp file-based DB.
``_store_project_extras`` opens its own engine from ``get_database_url()``,
so we create a file-based temp DB, seed a project, and patch the URL.
"""
import tempfile
from pathlib import Path
from sqlalchemy import create_engine as _ce
from sqlalchemy.orm import sessionmaker as _sm
from cleveragents.domain.models.core.project import (
NamespacedProject,
parse_namespaced_name,
)
from cleveragents.infrastructure.database.models import Base as _Base
from cleveragents.infrastructure.database.repositories import (
NamespacedProjectRepository,
)
tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False) # noqa: SIM115
tmp.close()
db_url = f"sqlite:///{tmp.name}"
engine = _ce(db_url, echo=False)
_Base.metadata.create_all(engine)
# Use a single session so we can commit after repo.create()
session = _sm(bind=engine, expire_on_commit=False)()
def _factory() -> Any:
return session
repo = NamespacedProjectRepository(session_factory=_factory)
parsed = parse_namespaced_name("local/extras-proj")
proj = NamespacedProject(name=parsed.name, namespace=parsed.namespace)
repo.create(proj)
session.commit()
import cleveragents.application.container as container_mod
import cleveragents.cli.commands.project as project_mod
orig = container_mod.get_database_url
container_mod.get_database_url = lambda: db_url
try:
project_mod._store_project_extras(
"local/extras-proj",
invariant_texts=[inv],
inv_actor=actor,
)
context._cmd_failed = False
except Exception:
context._cmd_failed = True
finally:
container_mod.get_database_url = orig
engine.dispose()
Path(tmp.name).unlink(missing_ok=True)
@then("the helper should return a NamespacedProjectRepository")
def step_check_repo(context: Any) -> None:
from cleveragents.infrastructure.database.repositories import (
NamespacedProjectRepository,
)
assert isinstance(context._cmd_helper_result, NamespacedProjectRepository)
@then("the helper should return a ProjectResourceLinkRepository")
def step_check_link_repo(context: Any) -> None:
from cleveragents.infrastructure.database.repositories import (
ProjectResourceLinkRepository,
)
assert isinstance(context._cmd_helper_result, ProjectResourceLinkRepository)
@then("the helper should return a ResourceRegistryService")
def step_check_svc(context: Any) -> None:
from cleveragents.application.services.resource_registry_service import (
ResourceRegistryService,
)
assert isinstance(context._cmd_helper_result, ResourceRegistryService)
@then("the extras call should complete without error")
def step_extras_ok(context: Any) -> None:
assert not context._cmd_failed, "Expected _store_project_extras to succeed"
# ---------------------------------------------------------------------------
# Create command
# ---------------------------------------------------------------------------
@when("I invoke project-create triggering a database error")
def step_invoke_create_db_error(context: Any) -> None:
"""Exercise the ``except DatabaseError`` branch in the create command."""
from unittest.mock import MagicMock
from cleveragents.cli.commands.project import create
from cleveragents.core.exceptions import DatabaseError
# Temporarily replace the repo with one whose create() raises DatabaseError
orig_repo = context._cmd_project_repo
mock_repo = MagicMock()
mock_repo.create.side_effect = DatabaseError("simulated DB failure")
context._cmd_project_repo = mock_repo
try:
_capture(context, create, name="db-err-proj")
finally:
context._cmd_project_repo = orig_repo
@when('I invoke project-create with bare name "{name}"')
def step_invoke_create(context: Any, name: str) -> None:
from cleveragents.cli.commands.project import create
_capture(context, create, name=name)
@when('I invoke project-create with name "{name}" description "{desc}"')
def step_invoke_create_desc(context: Any, name: str, desc: str) -> None:
from cleveragents.cli.commands.project import create
_capture(context, create, name=name, description=desc)
@when('I invoke project-create with name "{name}" format "{fmt}"')
def step_invoke_create_fmt(context: Any, name: str, fmt: str) -> None:
from cleveragents.cli.commands.project import create
_capture(context, create, name=name, output_format=fmt)
@when('I invoke project-create with name "{name}" invariant "{inv}"')
def step_invoke_create_inv(context: Any, name: str, inv: str) -> None:
from cleveragents.cli.commands.project import create
_capture(context, create, name=name, invariant=[inv])
@when('I invoke project-create with name "{name}" linking resource "{res}"')
def step_invoke_create_res(context: Any, name: str, res: str) -> None:
from cleveragents.cli.commands.project import create
_capture(context, create, name=name, resource=[f"local/{res}"])
# ---------------------------------------------------------------------------
# List command
# ---------------------------------------------------------------------------
@when("I invoke project-list with default options")
def step_invoke_list(context: Any) -> None:
from cleveragents.cli.commands.project import list_projects
_capture(context, list_projects)
@when('I invoke project-list with namespace "{ns}"')
def step_invoke_list_ns(context: Any, ns: str) -> None:
from cleveragents.cli.commands.project import list_projects
_capture(context, list_projects, namespace=ns)
@when('I invoke project-list with regex "{regex}"')
def step_invoke_list_regex(context: Any, regex: str) -> None:
from cleveragents.cli.commands.project import list_projects
_capture(context, list_projects, regex=regex)
@when('I invoke project-list with invalid regex "{regex}"')
def step_invoke_list_bad_regex(context: Any, regex: str) -> None:
from cleveragents.cli.commands.project import list_projects
_capture(context, list_projects, regex=regex)
@when('I invoke project-list with format "{fmt}"')
def step_invoke_list_fmt(context: Any, fmt: str) -> None:
from cleveragents.cli.commands.project import list_projects
_capture(context, list_projects, output_format=fmt)
# ---------------------------------------------------------------------------
# Show command
# ---------------------------------------------------------------------------
@when('I invoke project-show for "{name}" default format')
def step_invoke_show(context: Any, name: str) -> None:
from cleveragents.cli.commands.project import show
_capture(context, show, project=name)
@when('I invoke project-show for "{name}" format "{fmt}"')
def step_invoke_show_fmt(context: Any, name: str, fmt: str) -> None:
from cleveragents.cli.commands.project import show
_capture(context, show, project=name, output_format=fmt)
# ---------------------------------------------------------------------------
# Link-resource command
# ---------------------------------------------------------------------------
@when('I invoke project-link for "{proj}" resource "{res}" default format')
def step_invoke_link(context: Any, proj: str, res: str) -> None:
from cleveragents.cli.commands.project import link_resource
resource_name = f"local/{res}" if "/" not in res else res
_capture(context, link_resource, project=proj, resource_name=resource_name)
@when('I invoke project-link for "{proj}" resource "{res}" read-only')
def step_invoke_link_ro(context: Any, proj: str, res: str) -> None:
from cleveragents.cli.commands.project import link_resource
resource_name = f"local/{res}" if "/" not in res else res
_capture(
context,
link_resource,
project=proj,
resource_name=resource_name,
read_only=True,
)
@when('I invoke project-link for "{proj}" resource "{res}" format "{fmt}"')
def step_invoke_link_fmt(context: Any, proj: str, res: str, fmt: str) -> None:
from cleveragents.cli.commands.project import link_resource
resource_name = f"local/{res}" if "/" not in res else res
_capture(
context,
link_resource,
project=proj,
resource_name=resource_name,
output_format=fmt,
)
# ---------------------------------------------------------------------------
# Unlink-resource command
# ---------------------------------------------------------------------------
@when('I invoke project-unlink for "{proj}" with yes default format')
def step_invoke_unlink(context: Any, proj: str) -> None:
from cleveragents.cli.commands.project import unlink_resource
links = context._cmd_link_repo.list_links(proj)
if links:
res_id = str(links[0].resource_id)
try:
res = context._cmd_resource_svc.show_resource(res_id)
res_name = res.name or res_id
except Exception:
res_name = res_id
_capture(
context,
unlink_resource,
project=proj,
resource_name=res_name,
yes=True,
)
else:
_capture(
context,
unlink_resource,
project=proj,
resource_name="none",
yes=True,
)
@when('I invoke project-unlink for "{proj}" with yes format "{fmt}"')
def step_invoke_unlink_fmt(context: Any, proj: str, fmt: str) -> None:
from cleveragents.cli.commands.project import unlink_resource
links = context._cmd_link_repo.list_links(proj)
if links:
res_id = str(links[0].resource_id)
try:
res = context._cmd_resource_svc.show_resource(res_id)
res_name = res.name or res_id
except Exception:
res_name = res_id
_capture(
context,
unlink_resource,
project=proj,
resource_name=res_name,
yes=True,
output_format=fmt,
)
else:
_capture(
context,
unlink_resource,
project=proj,
resource_name="none",
yes=True,
output_format=fmt,
)
@when('I invoke project-unlink for "{proj}" resource "{res}" with yes')
def step_invoke_unlink_named(context: Any, proj: str, res: str) -> None:
from cleveragents.cli.commands.project import unlink_resource
resource_name = f"local/{res}" if "/" not in res else res
_capture(
context,
unlink_resource,
project=proj,
resource_name=resource_name,
yes=True,
)
# ---------------------------------------------------------------------------
# Delete command
# ---------------------------------------------------------------------------
@when('I invoke project-delete for "{name}" with yes')
def step_invoke_delete(context: Any, name: str) -> None:
from cleveragents.cli.commands.project import delete
_capture(context, delete, name=name, yes=True)
@when('I invoke project-delete for "{name}" with yes format "{fmt}"')
def step_invoke_delete_fmt(context: Any, name: str, fmt: str) -> None:
from cleveragents.cli.commands.project import delete
_capture(context, delete, name=name, yes=True, output_format=fmt)
@when('I invoke project-delete for "{name}" with yes no force')
def step_invoke_delete_no_force(context: Any, name: str) -> None:
from cleveragents.cli.commands.project import delete
_capture(context, delete, name=name, yes=True, force=False)
@when('I invoke project-delete for "{name}" with yes and force')
def step_invoke_delete_force(context: Any, name: str) -> None:
from cleveragents.cli.commands.project import delete
_capture(context, delete, name=name, yes=True, force=True)
# ---------------------------------------------------------------------------
# Then assertions
# ---------------------------------------------------------------------------
@then('the project cmd output should contain "{text}"')
def step_cmd_output_contains(context: Any, text: str) -> None:
assert text.lower() in context._cmd_output.lower(), (
f"Expected output to contain '{text}', got:\n{context._cmd_output}"
)
@then("the project cmd should succeed")
def step_cmd_success(context: Any) -> None:
assert not context._cmd_failed, (
f"Expected command to succeed but it failed. Output:\n{context._cmd_output}"
)
@then("the project cmd should fail")
def step_cmd_fail(context: Any) -> None:
assert context._cmd_failed, (
f"Expected command to fail but it succeeded. Output:\n{context._cmd_output}"
)