7235d46ade
CI / quality (pull_request) Successful in 19s
CI / lint (pull_request) Successful in 21s
CI / benchmark-publish (pull_request) Has been skipped
CI / security (pull_request) Successful in 50s
CI / typecheck (pull_request) Successful in 58s
CI / build (pull_request) Successful in 29s
CI / integration_tests (pull_request) Successful in 4m16s
CI / unit_tests (pull_request) Successful in 12m18s
CI / docker (pull_request) Successful in 1m30s
CI / benchmark-regression (pull_request) Successful in 25m15s
CI / coverage (pull_request) Successful in 1h21m51s
CI / build (push) Successful in 15s
CI / quality (push) Successful in 17s
CI / lint (push) Successful in 21s
CI / security (push) Successful in 28s
CI / typecheck (push) Successful in 31s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 2m47s
CI / unit_tests (push) Successful in 11m0s
CI / docker (push) Successful in 40s
CI / benchmark-publish (push) Successful in 12m22s
CI / coverage (push) Successful in 44m42s
Add Alembic migration m4_002_skill_flattened_tools to extend the skills table with five new columns: flattened_tools_json, includes_json, capability_summary_json, yaml_text, and flattening_hash (SHA-256). A defence-in-depth uniqueness constraint (uq_skills_name) is also added. Update SkillModel with the new column definitions and extend SkillRepository with update_flattened_tools(), get_flattened_tools(), needs_refresh(), recompute_flattening_hash(), and invalidate_cached_summaries() methods. The existing update() method now nulls all cached fields on mutation (hash-based invalidation). All new repository methods follow the session-factory pattern with @database_retry and flush-but-don-t-commit semantics. Structured logging via structlog records cache updates and invalidations. Database schema docs updated with the new skills table columns and a persistence-field-to-domain-model mapping table. Tests: - 6 Behave scenarios covering create, invalidation, hash staleness, refresh recomputation, uniqueness constraint, and namespace filtering - 2 Robot Framework smoke tests (round-trip and invalidation) - 3 ASV benchmarks (persist, refresh check, namespace list) ISSUES CLOSED: #166
135 lines
4.0 KiB
Python
135 lines
4.0 KiB
Python
"""Helper script for Robot Framework skill registry persistence smoke tests.
|
|
|
|
Usage:
|
|
python helper_skill_registry_persistence.py <command>
|
|
|
|
Commands:
|
|
persist-round-trip Create a skill, persist flattened tools, retrieve
|
|
update-invalidation Update a skill and verify cache invalidation
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
# Ensure src is on the path
|
|
src_dir = str(Path(__file__).resolve().parents[1] / "src")
|
|
if src_dir not in sys.path:
|
|
sys.path.insert(0, src_dir)
|
|
|
|
from sqlalchemy import create_engine, event, text # noqa: E402
|
|
from sqlalchemy.orm import Session, sessionmaker # noqa: E402
|
|
|
|
from cleveragents.domain.models.core.skill import Skill # noqa: E402
|
|
from cleveragents.infrastructure.database.models import Base # noqa: E402
|
|
from cleveragents.infrastructure.database.repositories import ( # noqa: E402
|
|
SkillRepository,
|
|
)
|
|
|
|
|
|
def _enable_fk_pragma(dbapi_conn: Any, _connection_record: Any) -> None:
|
|
"""Enable SQLite foreign key enforcement per connection."""
|
|
cursor = dbapi_conn.cursor()
|
|
cursor.execute("PRAGMA foreign_keys=ON")
|
|
cursor.close()
|
|
|
|
|
|
def _setup() -> tuple[SkillRepository, sessionmaker[Session]]:
|
|
engine = create_engine("sqlite:///:memory:", echo=False)
|
|
event.listen(engine, "connect", _enable_fk_pragma)
|
|
Base.metadata.create_all(engine)
|
|
factory: sessionmaker[Session] = sessionmaker(bind=engine)
|
|
repo = SkillRepository(session_factory=factory)
|
|
return repo, factory
|
|
|
|
|
|
def _commit(factory: sessionmaker[Session]) -> None:
|
|
session = factory()
|
|
try:
|
|
session.execute(text("SELECT 1"))
|
|
session.commit()
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
def _make_skill(name: str, description: str = "Test skill") -> Skill:
|
|
return Skill(name=name, description=description)
|
|
|
|
|
|
def cmd_persist_round_trip() -> None:
|
|
"""Create skill, persist flattened tools, retrieve and verify."""
|
|
repo, factory = _setup()
|
|
|
|
skill = _make_skill("local/persist-test", "Persistence test")
|
|
repo.create(skill)
|
|
_commit(factory)
|
|
|
|
repo.update_flattened_tools(
|
|
name="local/persist-test",
|
|
flattened_tools_json='["tool-a","tool-b"]',
|
|
includes_json="[]",
|
|
capability_summary_json='{"total_tools": 2}',
|
|
yaml_text="name: local/persist-test",
|
|
flattening_hash="deadbeef",
|
|
)
|
|
_commit(factory)
|
|
|
|
data = repo.get_flattened_tools("local/persist-test")
|
|
assert data["flattened_tools_json"] == '["tool-a","tool-b"]'
|
|
assert data["flattening_hash"] == "deadbeef"
|
|
assert data["capability_summary_json"] == '{"total_tools": 2}'
|
|
|
|
print("persist-round-trip-ok")
|
|
|
|
|
|
def cmd_update_invalidation() -> None:
|
|
"""Update skill and verify cached fields are nulled."""
|
|
repo, factory = _setup()
|
|
|
|
skill = _make_skill("local/invalidate-test", "Before update")
|
|
repo.create(skill)
|
|
_commit(factory)
|
|
|
|
repo.update_flattened_tools(
|
|
name="local/invalidate-test",
|
|
flattened_tools_json='["x"]',
|
|
includes_json="[]",
|
|
capability_summary_json="{}",
|
|
yaml_text="name: local/invalidate-test",
|
|
flattening_hash="hash1",
|
|
)
|
|
_commit(factory)
|
|
|
|
# Now update the skill — should null the cache
|
|
updated = _make_skill("local/invalidate-test", "After update")
|
|
repo.update(updated)
|
|
_commit(factory)
|
|
|
|
data = repo.get_flattened_tools("local/invalidate-test")
|
|
assert data["flattened_tools_json"] is None, (
|
|
f"Expected None, got {data['flattened_tools_json']}"
|
|
)
|
|
assert data["flattening_hash"] is None, (
|
|
f"Expected None, got {data['flattening_hash']}"
|
|
)
|
|
|
|
print("update-invalidation-ok")
|
|
|
|
|
|
COMMANDS: dict[str, Any] = {
|
|
"persist-round-trip": cmd_persist_round_trip,
|
|
"update-invalidation": cmd_update_invalidation,
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
|
|
print(
|
|
f"Usage: {sys.argv[0]} <{'|'.join(COMMANDS)}>",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
COMMANDS[sys.argv[1]]()
|