deer-flow/backend/tests/test_skill_evolution_host_integration.py
Ryker_Feng d1d0958bea
feat(skills): add guarded evolution host APIs (#5645)
* feat(skills): add guarded evolution host APIs

* docs(agents): avoid growing saturated guidance chains

* fix(skills): address mutation host review findings

* fix(skills): order name fence before projection lock

* fix(skills): preserve composite toggle lock order

* fix(skills): address catalog and pagination review feedback

* docs: keep subagent guidance within merged instruction budget

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-18 21:17:02 +08:00

84 lines
4.4 KiB
Python

"""Real SQLite, host assembly, evidence, projection and public-only consumer."""
import importlib.util
from pathlib import Path
from types import SimpleNamespace
import pytest
from deerflow_extension_api import ExtensionData, ExtensionRuntimeDeps, HostPolicySnapshot
from sqlalchemy import create_engine
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from deerflow.config.database_config import DatabaseConfig
from deerflow.config.paths import Paths
from deerflow.extensions.host_capabilities import HostCapabilities
from deerflow.extensions.loader import ExtensionSpec
from deerflow.persistence.base import Base
from deerflow.persistence.run import RunRepository
from deerflow.persistence.run.model import RunRow
from deerflow.persistence.user.model import UserRow
from deerflow.runtime.events.store.db import DbRunEventStore
from deerflow.skills.mutations.scanner import CandidateScanner, ScanVerdict
from deerflow.skills.storage.user_scoped_skill_storage import UserScopedSkillStorage
@pytest.mark.asyncio
async def test_public_consumer_end_to_end_and_plugin_removed_restart(tmp_path, monkeypatch):
monkeypatch.setattr("deerflow.config.paths._paths", Paths(base_dir=tmp_path / "home"))
storage = UserScopedSkillStorage("owner", host_path=str(tmp_path / "skills"))
root = storage.get_custom_skill_dir("example")
root.mkdir(parents=True)
content = "---\nname: example\ndescription: A sample skill\n---\nOriginal body.\n"
(root / "SKILL.md").write_text(content, encoding="utf-8")
database = DatabaseConfig(backend="sqlite", sqlite_dir=str(tmp_path))
engine = create_engine(database.app_sync_sqlalchemy_url)
Base.metadata.create_all(engine)
engine.dispose()
async_engine = create_async_engine(database.app_sqlalchemy_url)
sf = async_sessionmaker(async_engine, expire_on_commit=False)
async with sf.begin() as session:
session.add(UserRow(id="owner", email="owner@example.test"))
session.add(RunRow(run_id="run", thread_id="thread", user_id="owner", status="success", evidence_seal_state="sealed", evidence_agent_id="lead_agent", evidence_origin="interactive", evidence_upper_seq=0, evidence_event_count=0))
async def scan(self, package, name):
assert name == "example"
assert package.main_content.startswith("---")
return ScanVerdict("allow", "CHECK_PASSED", "test-policy")
async def policy(self):
return "test-policy"
monkeypatch.setattr(CandidateScanner, "scan", scan)
monkeypatch.setattr(CandidateScanner, "policy_version", policy)
monkeypatch.setattr(CandidateScanner, "policy_version_sync", lambda _: "test-policy")
spec = ExtensionSpec(
use="fixture:install",
name="fixture",
host_access={
"evidence": {"owners": ["owner"]},
"skill_mutations": {"owners": ["owner"], "operations": ["stage", "check", "commit", "revert"], "trigger_agents": ["lead_agent"], "target_skills": ["example"], "topology": "single_host_local"},
},
)
config = SimpleNamespace(database=database, plugins=[spec])
host = HostCapabilities(config, RunRepository(sf), DbRunEventStore(sf), root=tmp_path, storage_factory=lambda _: storage)
try:
await host.start()
evidence, mutations = host.bindings[spec.use]
deps = ExtensionRuntimeDeps(app_store=ExtensionData("fixture"), policy=HostPolicySnapshot(), completed_run_evidence=evidence, skill_mutations=mutations)
module_spec = importlib.util.spec_from_file_location("external_consumer", Path(__file__).parent / "fixtures" / "skill_evolution_consumer.py")
consumer = importlib.util.module_from_spec(module_spec)
module_spec.loader.exec_module(consumer)
operation, reverted = await consumer.exercise(deps, thread_id="thread", run_id="run", name="example")
assert (operation.views, reverted.views) == ("READY", "READY")
assert (root / "SKILL.md").read_text(encoding="utf-8") == content
await host.close()
config.plugins = []
host = HostCapabilities(config, RunRepository(sf), DbRunEventStore(sf), root=tmp_path, storage_factory=lambda _: storage)
await host.start()
assert host.bindings == {}
assert "owner" in host.runtime.owners
assert host.recovery.get_operation(reverted.operation_id).publication == "APPLIED"
finally:
await host.close()
await async_engine.dispose()