test(cov): Fix coverage test failures #94
@@ -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}"
|
||||
)
|
||||
Reference in New Issue
Block a user