"""Step definitions for skill add persistence (issue #620). Verifies that ``SkillService`` with a ``SkillRepository`` correctly persists skills to the database so that a fresh service instance sees the previously added skills. """ from __future__ import annotations import os import tempfile from typing import Any 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.application.services.skill_service import SkillService from cleveragents.infrastructure.database.models import Base from cleveragents.infrastructure.database.repositories import ( SkillRepository, ) from cleveragents.skills.schema import SkillConfigSchema # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _SKILL_YAML_TEMPLATE = """\ name: {name} description: "{name} test skill" tools: - name: builtin/read_file """ def _enable_fk(dbapi_conn: Any, _rec: Any) -> None: """Enable SQLite foreign key enforcement.""" cursor = dbapi_conn.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() def _make_service( factory: Any, ) -> SkillService: """Build a SkillService backed by the given session factory.""" repo = SkillRepository(session_factory=factory) return SkillService(skill_repo=repo, session_factory=factory) # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("a fresh skill service backed by an in-memory database") def step_fresh_persistent_service(context: Context) -> None: engine = create_engine("sqlite:///:memory:", echo=False) event.listen(engine, "connect", _enable_fk) Base.metadata.create_all(engine) shared_session = sessionmaker(bind=engine)() def _sf() -> Session: return shared_session context.sap_engine = engine context.sap_session_factory = _sf context.sap_service = _make_service(context.sap_session_factory) context.sap_yaml_paths = [] context.sap_configs = [] context.sap_listed = [] context.sap_error = None # --------------------------------------------------------------------------- # Given steps # --------------------------------------------------------------------------- @given('a skill config YAML for "{name}" at a temp path') def step_skill_config_yaml(context: Context, name: str) -> None: content = _SKILL_YAML_TEMPLATE.format(name=name) fd, path = tempfile.mkstemp(suffix=".yaml") with os.fdopen(fd, "w") as fh: fh.write(content) context.sap_yaml_paths.append(path) schema = SkillConfigSchema.from_yaml_file(path) context.sap_configs.append(schema) # --------------------------------------------------------------------------- # When steps # --------------------------------------------------------------------------- @when("I add the skill via the persistent service") def step_add_skill_persistent(context: Context) -> None: config = context.sap_configs[-1] path = context.sap_yaml_paths[-1] context.sap_service.add_skill(config, config_path=path) @when("I add all pending skill configs via the persistent service") def step_add_all_skills_persistent(context: Context) -> None: for cfg, path in zip(context.sap_configs, context.sap_yaml_paths, strict=True): context.sap_service.add_skill(cfg, config_path=path) @when("I create a second service instance from the same database") def step_second_service(context: Context) -> None: context.sap_service2 = _make_service(context.sap_session_factory) @when("I list skills from the second service instance") def step_list_from_second(context: Context) -> None: context.sap_listed = context.sap_service2.list_skills() @when('I remove the skill "{name}" via the persistent service') def step_remove_skill_persistent(context: Context, name: str) -> None: context.sap_service.remove_skill(name) # --------------------------------------------------------------------------- # Then steps # --------------------------------------------------------------------------- @then('the listed persistent skills should contain "{name}"') def step_listed_contains(context: Context, name: str) -> None: names = [s.name for s in context.sap_listed] assert name in names, f"{name} not in {names}" @then('the listed persistent skills should not contain "{name}"') def step_listed_not_contains(context: Context, name: str) -> None: names = [s.name for s in context.sap_listed] assert name not in names, f"{name} unexpectedly in {names}" @then("the listed persistent skills count should be {count:d}") def step_listed_count(context: Context, count: int) -> None: actual = len(context.sap_listed) assert actual == count, f"Expected {count}, got {actual}" @then('adding a duplicate "{name}" should raise ValueError') def step_duplicate_raises(context: Context, name: str) -> None: config = context.sap_configs[-1] try: context.sap_service.add_skill(config) raise AssertionError("Expected ValueError for duplicate skill") except ValueError as exc: assert "already registered" in str(exc)