deer-flow/backend/tests/test_skill_mutation_scanner.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

73 lines
3.4 KiB
Python

from unittest.mock import AsyncMock
import pytest
from deerflow.config.app_config import AppConfig
from deerflow.skills.mutations.assets import PackageFile, PackageSnapshot
from deerflow.skills.security_scanner import ScanResult
CONTENT = b"---\nname: example\ndescription: Fine\n---\nHelpful instructions.\n"
def configuration(enabled=True):
return AppConfig.model_validate({"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"}, "skill_scan": {"enabled": enabled}, "skill_evolution": {"security_fail_closed": False}})
@pytest.mark.asyncio
async def test_host_check_never_treats_disabled_scanner_as_allow(monkeypatch):
from deerflow.skills.mutations.scanner import CandidateScanner
model = AsyncMock(return_value=ScanResult("allow", "fine"))
monkeypatch.setattr("deerflow.skills.mutations.scanner.scan_skill_content", model)
result = await CandidateScanner(lambda: configuration(False)).scan(PackageSnapshot((PackageFile("SKILL.md", CONTENT, False),)), "example")
assert result.decision == "unavailable"
model.assert_not_called()
@pytest.mark.asyncio
async def test_host_scan_covers_support_files_and_forces_fail_closed(monkeypatch):
from deerflow.skills.mutations.scanner import CandidateScanner
model = AsyncMock(return_value=ScanResult("allow", "fine"))
monkeypatch.setattr("deerflow.skills.mutations.scanner.scan_skill_content", model)
scanner = CandidateScanner(configuration)
package = PackageSnapshot((PackageFile("SKILL.md", CONTENT, False), PackageFile("references/help.md", b"Supporting advice", False)))
result = await scanner.scan(package, "example")
assert result.decision == "allow"
assert result.policy_version == await scanner.policy_version()
assert model.await_count == 2
assert all(call.kwargs["app_config"].skill_evolution.security_fail_closed for call in model.await_args_list)
@pytest.mark.asyncio
async def test_static_block_prevents_model_scan(monkeypatch):
from deerflow.skills.mutations.scanner import CandidateScanner
model = AsyncMock(return_value=ScanResult("allow", "fine"))
monkeypatch.setattr("deerflow.skills.mutations.scanner.scan_skill_content", model)
package = PackageSnapshot((PackageFile("SKILL.md", CONTENT, False), PackageFile("scripts/run.py", b"eval(input())", True)))
result = await CandidateScanner(configuration).scan(package, "example")
assert result.decision == "reject"
model.assert_not_called()
@pytest.mark.asyncio
async def test_scanner_resource_limit_is_not_partial_approval(monkeypatch):
from deerflow.skills.mutations.scanner import CandidateScanner
model = AsyncMock(return_value=ScanResult("allow", "fine"))
monkeypatch.setattr("deerflow.skills.mutations.scanner.scan_skill_content", model)
package = PackageSnapshot((PackageFile("SKILL.md", CONTENT, False), PackageFile("notes.txt", b"x" * (128 * 1024 + 1), False)))
result = await CandidateScanner(configuration).scan(package, "example")
assert result.decision == "unavailable"
model.assert_not_called()
@pytest.mark.asyncio
async def test_host_scan_timeout_propagates_without_authoritative_verdict(monkeypatch):
from deerflow.skills.mutations.scanner import CandidateScanner
monkeypatch.setattr("deerflow.skills.mutations.scanner.CHECK_TIMEOUT_SECONDS", 0)
with pytest.raises(TimeoutError):
await CandidateScanner(configuration).scan(PackageSnapshot((PackageFile("SKILL.md", CONTENT, False),)), "example")