- divergence_tolerance was loaded from config but diff_flow_versions never consulted it, so structural_divergence was always plain sequence-equality regardless of the configured tolerance. Now actually applied per design.md D4. - The skill-catalog-subscription isolation guard test asserted 'skills.catalog' not in sys.modules, which is vacuously true since that module doesn't exist anywhere yet. Replaced with a real check against store.py's actual imports. openspec: skill-versioning capability, archived change skill-learning-runtime
88 lines
2.8 KiB
Python
88 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
import ast
|
|
import inspect
|
|
|
|
from skills_learning import store as skill_store_module
|
|
from skills_learning.models import (
|
|
LOCAL_SYNTHESIS_SOURCE,
|
|
FlowStep,
|
|
FlowTemplateSkill,
|
|
SkillMetadata,
|
|
)
|
|
from skills_learning.store import SkillStore
|
|
|
|
|
|
def _imported_module_names(module) -> set[str]:
|
|
"""Statically collect every module name imported by ``module``'s source.
|
|
|
|
Covers both ``import x.y`` and ``from x.y import z`` forms so the check
|
|
actually fails if a catalog-subscription import is ever added, instead
|
|
of only checking whether some module happened to already be imported by
|
|
something else in the running interpreter.
|
|
"""
|
|
source = inspect.getsource(module)
|
|
tree = ast.parse(source)
|
|
names: set[str] = set()
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
names.update(alias.name for alias in node.names)
|
|
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
names.add(node.module)
|
|
return names
|
|
|
|
|
|
def _skill(
|
|
*,
|
|
name: str = "search web",
|
|
steps: list[FlowStep] | None = None,
|
|
parameters: dict[str, dict[str, object]] | None = None,
|
|
) -> FlowTemplateSkill:
|
|
return FlowTemplateSkill(
|
|
metadata=SkillMetadata(
|
|
name=name,
|
|
description=f"Learned flow for {name}",
|
|
source="external-source",
|
|
originating_goal=name,
|
|
),
|
|
steps=steps or [FlowStep("input_text", {"text": "coffee"})],
|
|
parameters=parameters or {},
|
|
)
|
|
|
|
|
|
def test_skill_store_enforces_local_synthesis_source_without_catalog_write_path() -> None:
|
|
store = SkillStore()
|
|
|
|
stored = store.create_version(_skill())
|
|
|
|
assert stored.source == LOCAL_SYNTHESIS_SOURCE
|
|
|
|
imported_modules = _imported_module_names(skill_store_module)
|
|
catalog_coupled = {
|
|
name
|
|
for name in imported_modules
|
|
if "catalog" in name.lower() or "sync_client" in name.lower()
|
|
}
|
|
assert catalog_coupled == set(), (
|
|
"skills_learning/store.py must not import from the "
|
|
"skill-catalog-subscription store/sync-client (D6: locally-authored "
|
|
"skills live in their own store, composed only in prose); found: "
|
|
f"{catalog_coupled}"
|
|
)
|
|
|
|
|
|
def test_skill_store_keeps_version_chain_and_returns_latest_by_name() -> None:
|
|
store = SkillStore()
|
|
first = store.create_version(_skill())
|
|
second = store.create_version(
|
|
_skill(steps=[FlowStep("input_text", {"text": "tea"})]),
|
|
parent=first,
|
|
)
|
|
|
|
assert first.version == 1
|
|
assert second.version == 2
|
|
assert second.parent_version_id == first.id
|
|
assert store.get_latest_by_name("search web") == second
|
|
assert store.get_by_id(first.id) == first
|
|
assert [skill.version for skill in store.list_versions("search web")] == [1, 2]
|