9704281bdd
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 19s
CI / build (pull_request) Successful in 19s
CI / security (pull_request) Successful in 36s
CI / typecheck (pull_request) Successful in 39s
CI / unit_tests (pull_request) Successful in 2m29s
CI / docker (pull_request) Successful in 41s
CI / integration_tests (pull_request) Successful in 3m14s
CI / coverage (pull_request) Successful in 4m26s
CI / benchmark-regression (pull_request) Successful in 30m9s
CI / lint (push) Successful in 14s
CI / build (push) Successful in 16s
CI / quality (push) Successful in 17s
CI / security (push) Successful in 33s
CI / typecheck (push) Successful in 34s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m18s
CI / docker (push) Successful in 39s
CI / integration_tests (push) Successful in 3m7s
CI / coverage (push) Successful in 4m29s
CI / benchmark-publish (push) Has been cancelled
Wire SkillService to use SkillRepository for database persistence, fixing the bug where `agents skill add` stored skills only in an in-memory OrderedDict that was lost when the CLI process exited. Changes: - SkillService now accepts optional skill_repo and session_factory parameters. When provided, add_skill() persists to the database and the constructor pre-loads existing skills from DB rows. - _get_skill_service() in the CLI now creates a DB-backed service following the same engine/sessionmaker/repository pattern used by the tool CLI (tool.py). - _reset_skill_service() now installs a fresh in-memory SkillService (instead of setting None) to avoid DB side-effects during unit testing with parallel runners. - remove_skill() also persists the deletion to the database. Test coverage: - Behave BDD: features/skill_add_persist.feature (4 scenarios) - Robot Framework: robot/skill_add_persist.robot (3 smoke tests) - ASV benchmark: benchmarks/skill_add_persist_bench.py ISSUES CLOSED: #620
158 lines
5.2 KiB
Python
158 lines
5.2 KiB
Python
"""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)
|