175 lines
5.7 KiB
Python
175 lines
5.7 KiB
Python
import asyncio
|
|
|
|
import pytest
|
|
|
|
from app.agent.permissions import PermissionMode
|
|
from app.agent.tools import ToolExecutionContext
|
|
from app.container import build_container
|
|
from app.contracts import (
|
|
AgentRunCreateRequest,
|
|
AgentRunStatus,
|
|
SkillStatus,
|
|
ToolCall,
|
|
)
|
|
from app.extensions import ExtensionError
|
|
from app.services import note_service
|
|
|
|
|
|
def run(coroutine):
|
|
return asyncio.run(coroutine)
|
|
|
|
|
|
def test_bundled_plugin_registers_tool_and_skill_is_ready() -> None:
|
|
async def scenario() -> None:
|
|
container = build_container()
|
|
|
|
plugin = container.plugins.get("text-tools")
|
|
skill = container.skills.get("knowledge-assistant")
|
|
definition = container.tools.get("text.uppercase").definition
|
|
result = await container.tools.execute(
|
|
ToolCall(
|
|
tool_call_id="call_uppercase",
|
|
name="text.uppercase",
|
|
arguments={"text": "hello plugin"},
|
|
),
|
|
ToolExecutionContext(run_id="run_extension_test"),
|
|
)
|
|
|
|
assert plugin.enabled is True and plugin.status == "ready"
|
|
assert skill.enabled is True and skill.status == SkillStatus.ready
|
|
assert definition.source == "plugin"
|
|
assert result.success is True
|
|
assert result.output == {"text": "HELLO PLUGIN"}
|
|
|
|
run(scenario())
|
|
|
|
|
|
def test_skill_drives_agent_and_can_call_plugin_tool() -> None:
|
|
async def scenario() -> None:
|
|
container = build_container()
|
|
created = await container.agent.create_run(
|
|
AgentRunCreateRequest(
|
|
input='/tool text.uppercase {"text":"skill plugin"}',
|
|
provider_id="mock",
|
|
model="mock-1",
|
|
skill_id="knowledge-assistant",
|
|
)
|
|
)
|
|
|
|
completed = await container.agent.wait(created.run_id)
|
|
|
|
assert completed.status == AgentRunStatus.completed
|
|
assert completed.tool_results[0].success is True
|
|
assert completed.tool_results[0].output == {"text": "SKILL PLUGIN"}
|
|
|
|
run(scenario())
|
|
|
|
|
|
def test_plugin_disable_updates_skill_dependency_status() -> None:
|
|
container = build_container()
|
|
|
|
disabled = container.plugins.disable("text-tools")
|
|
skill = container.skills.get("knowledge-assistant")
|
|
|
|
assert disabled.status == "disabled"
|
|
assert not container.tools.contains("text.uppercase")
|
|
assert skill.status == SkillStatus.dependency_missing
|
|
assert skill.missing_dependencies == ["text.uppercase"]
|
|
|
|
with pytest.raises(ExtensionError) as exc:
|
|
container.skills.build_agent_configuration(
|
|
"knowledge-assistant",
|
|
container.providers.get("mock").config.capabilities,
|
|
)
|
|
assert exc.value.code == "SKILL_NOT_READY"
|
|
|
|
container.plugins.enable("text-tools")
|
|
assert container.skills.get("knowledge-assistant").status == SkillStatus.ready
|
|
|
|
|
|
def test_enabled_skill_blocks_plugin_uninstall() -> None:
|
|
container = build_container()
|
|
plugin = container.plugins.get("text-tools")
|
|
dependencies = container.skills.depending_on_tools(plugin.manifest.contributes.tools)
|
|
|
|
with pytest.raises(ExtensionError) as exc:
|
|
container.plugins.uninstall("text-tools", dependencies)
|
|
|
|
assert exc.value.code == "PLUGIN_IN_USE"
|
|
assert exc.value.details["skills"] == ["knowledge-assistant"]
|
|
|
|
|
|
def test_agent_note_search_tool_collects_citations() -> None:
|
|
async def scenario() -> None:
|
|
container = build_container()
|
|
await note_service.create_note(
|
|
title="Agent 检索",
|
|
markdown="# Agent\n\nAgent 可以通过工具检索本地知识库。",
|
|
folder="",
|
|
tags=["agent"],
|
|
)
|
|
created = await container.agent.create_run(
|
|
AgentRunCreateRequest(
|
|
input='/tool notes.search {"query":"本地知识库","mode":"fts"}',
|
|
provider_id="mock",
|
|
model="mock-1",
|
|
skill_id="knowledge-assistant",
|
|
)
|
|
)
|
|
|
|
completed = await container.agent.wait(created.run_id)
|
|
|
|
assert completed.status == AgentRunStatus.completed
|
|
assert completed.tool_results[0].success is True
|
|
assert completed.citations
|
|
assert completed.citations[0].file_path == "Agent 检索.md"
|
|
|
|
run(scenario())
|
|
|
|
|
|
def test_network_tool_requires_run_level_network_permission() -> None:
|
|
async def scenario() -> None:
|
|
container = build_container()
|
|
tool = container.tools.get("system.echo")
|
|
tool.definition.permission = "network.request"
|
|
container.permissions.policy.set_rule("network.request", PermissionMode.allow)
|
|
|
|
created = await container.agent.create_run(
|
|
AgentRunCreateRequest(
|
|
input='/tool system.echo {"text":"network"}',
|
|
provider_id="mock",
|
|
model="mock-1",
|
|
allowed_tools=["system.echo"],
|
|
allow_network=False,
|
|
)
|
|
)
|
|
completed = await container.agent.wait(created.run_id)
|
|
|
|
assert completed.tool_results[0].success is False
|
|
assert completed.tool_results[0].error_code == "NETWORK_NOT_ALLOWED"
|
|
|
|
run(scenario())
|
|
|
|
|
|
def test_skill_install_reports_missing_tool_dependency(tmp_path) -> None:
|
|
package = tmp_path / "missing-tool-skill"
|
|
package.mkdir()
|
|
(package / "skill.yaml").write_text(
|
|
"""
|
|
id: missing-tool
|
|
name: Missing Tool
|
|
version: 1.0.0
|
|
tools: [plugin.not-installed]
|
|
""".strip(),
|
|
encoding="utf-8",
|
|
)
|
|
container = build_container()
|
|
|
|
installed = container.skills.install(package)
|
|
|
|
assert installed.status == SkillStatus.dependency_missing
|
|
assert installed.missing_dependencies == ["plugin.not-installed"]
|
|
with pytest.raises(ExtensionError) as exc:
|
|
container.skills.enable("missing-tool")
|
|
assert exc.value.code == "SKILL_DEPENDENCY_MISSING"
|