mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: 林玮 (Jade Lin) <linw1995@icloud.com> Co-authored-by: 盐粒 Yanli <mail@yanli.one> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: Jashwanth Reddy Gummula <gmrnlg1971@gmail.com> Co-authored-by: WH-2099 <wh2099@pm.me> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: wangxiaolei <fatelei@gmail.com> Co-authored-by: FFXN <31929997+FFXN@users.noreply.github.com> Co-authored-by: Yansong Zhang <916125788@qq.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
244 lines
8.8 KiB
Python
244 lines
8.8 KiB
Python
"""Behavior tests for the runtime Dify drive layer."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Literal
|
|
|
|
import pytest
|
|
|
|
from dify_agent.adapters.shell.shellctl import ShellctlProvider
|
|
from dify_agent.layers.drive import DifyDriveLayerConfig, DifyDriveSkillConfig
|
|
from dify_agent.layers.drive.layer import DifyDriveLayer, DifyDriveLayerError, _AGENT_FILE_UPLOAD_REPLY_HINT
|
|
from dify_agent.layers.shell import DifyShellLayerConfig
|
|
from dify_agent.layers.shell.layer import CompleteRemoteCommandResult, DifyShellLayer
|
|
|
|
|
|
def _unused_client_factory():
|
|
raise AssertionError("shellctl client should not be used by these drive-layer tests")
|
|
|
|
|
|
def _shell_layer() -> DifyShellLayer:
|
|
return DifyShellLayer.from_config_with_settings(
|
|
DifyShellLayerConfig(agent_stub_drive_ref="agent-1"),
|
|
shell_provider=ShellctlProvider(entrypoint="http://shellctl", token="", client_factory=_unused_client_factory),
|
|
)
|
|
|
|
|
|
def _build_layer() -> DifyDriveLayer:
|
|
layer = DifyDriveLayer.from_config(
|
|
DifyDriveLayerConfig(
|
|
drive_ref="agent-1",
|
|
skills=[
|
|
DifyDriveSkillConfig(
|
|
path="tender-analyzer",
|
|
name="Tender Analyzer",
|
|
description="Parses RFPs.",
|
|
skill_md_key="tender-analyzer/SKILL.md",
|
|
archive_key="tender-analyzer/.DIFY-SKILL-FULL.zip",
|
|
),
|
|
DifyDriveSkillConfig(
|
|
path="other-skill",
|
|
name="Other Skill",
|
|
description="Fallback catalog entry.",
|
|
skill_md_key="other-skill/SKILL.md",
|
|
archive_key=None,
|
|
),
|
|
],
|
|
mentioned_skill_keys=["tender-analyzer/SKILL.md"],
|
|
mentioned_file_keys=["files/report.pdf"],
|
|
)
|
|
)
|
|
layer.bind_deps({"shell": _shell_layer()})
|
|
return layer
|
|
|
|
|
|
def _remote_result(
|
|
output: str,
|
|
*,
|
|
exit_code: int | None = 0,
|
|
output_complete: bool = True,
|
|
incomplete_reason: Literal["output_limit", "timeout"] | None = None,
|
|
) -> CompleteRemoteCommandResult:
|
|
return CompleteRemoteCommandResult(
|
|
job_id="remote-drive-pull",
|
|
status="exited",
|
|
done=True,
|
|
exit_code=exit_code,
|
|
output=output,
|
|
output_complete=output_complete,
|
|
incomplete_reason=incomplete_reason,
|
|
offset=len(output),
|
|
output_path="/tmp/output.log",
|
|
)
|
|
|
|
|
|
def _pulled_output() -> str:
|
|
return (
|
|
"/mnt/drive/agent-1/tender-analyzer\n"
|
|
"/mnt/drive/agent-1/files/report.pdf\n"
|
|
"__DIFY_DRIVE_MENTIONED_PATH__\ttender-analyzer/SKILL.md\t/mnt/drive/agent-1/tender-analyzer/SKILL.md\n"
|
|
"__DIFY_DRIVE_SKILL_BEGIN__\ttender-analyzer/SKILL.md\n"
|
|
"# Tender Analyzer\n"
|
|
"Use carefully.\n"
|
|
"__DIFY_DRIVE_SKILL_END__\ttender-analyzer/SKILL.md\n"
|
|
"__DIFY_DRIVE_MENTIONED_PATH__\tfiles/report.pdf\t/mnt/drive/agent-1/files/report.pdf\n"
|
|
)
|
|
|
|
|
|
def _file_help_output(command: str) -> str:
|
|
return f"Usage: {command.removesuffix(' --help')} [OPTIONS]\n\nAgent Stub file command help.\n"
|
|
|
|
|
|
def _patch_file_help(monkeypatch: pytest.MonkeyPatch) -> list[str]:
|
|
captured_scripts: list[str] = []
|
|
|
|
async def fake_run_remote_script(
|
|
self: DifyShellLayer,
|
|
script: str,
|
|
*,
|
|
timeout: float = 10.0,
|
|
inject_agent_stub_env: bool = False,
|
|
) -> CompleteRemoteCommandResult:
|
|
del self, timeout, inject_agent_stub_env
|
|
captured_scripts.append(script)
|
|
return _remote_result(_file_help_output(script))
|
|
|
|
monkeypatch.setattr(DifyShellLayer, "run_remote_script", fake_run_remote_script)
|
|
return captured_scripts
|
|
|
|
|
|
def test_drive_layer_exposes_agent_stub_cli_usage_suffix_prompt() -> None:
|
|
layer = _build_layer()
|
|
layer._agent_stub_cli_help = {
|
|
"dify-agent file --help": _file_help_output("dify-agent file --help"),
|
|
"dify-agent file upload --help": _file_help_output("dify-agent file upload --help"),
|
|
"dify-agent file download --help": _file_help_output("dify-agent file download --help"),
|
|
}
|
|
|
|
assert len(layer.suffix_prompts) == 1
|
|
prompt = layer.suffix_prompts[0]
|
|
assert "Other available skills" in prompt
|
|
assert "other-skill: Other Skill" in prompt
|
|
assert "Agent Stub file CLI reference for installed `dify-agent`" in prompt
|
|
assert "$ dify-agent file upload --help" in prompt
|
|
assert "$ dify-agent file download --help" in prompt
|
|
assert prompt.index("$ dify-agent file upload --help") < prompt.index("$ dify-agent file download --help")
|
|
assert _AGENT_FILE_UPLOAD_REPLY_HINT in prompt
|
|
assert "dify-agent drive" not in prompt
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_on_context_create_pulls_mentioned_targets_through_shell(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
layer = _build_layer()
|
|
captured: dict[str, object] = {}
|
|
help_scripts = _patch_file_help(monkeypatch)
|
|
|
|
async def fake_run_remote_script_complete(
|
|
self: DifyShellLayer,
|
|
script: str,
|
|
*,
|
|
timeout: float = 10.0,
|
|
inject_agent_stub_env: bool = False,
|
|
) -> CompleteRemoteCommandResult:
|
|
del self, timeout
|
|
captured["script"] = script
|
|
captured["inject_agent_stub_env"] = inject_agent_stub_env
|
|
return _remote_result(_pulled_output())
|
|
|
|
monkeypatch.setattr(DifyShellLayer, "run_remote_script_complete", fake_run_remote_script_complete)
|
|
|
|
await layer.on_context_create()
|
|
|
|
assert help_scripts == [
|
|
"dify-agent file --help",
|
|
"dify-agent file upload --help",
|
|
"dify-agent file download --help",
|
|
]
|
|
assert "dify-agent file download --help" in layer._agent_stub_cli_help
|
|
script = captured["script"]
|
|
assert isinstance(script, str)
|
|
assert captured["inject_agent_stub_env"] is True
|
|
assert 'dify-agent drive pull tender-analyzer/ files/report.pdf --to "$base"' in script
|
|
prompt = layer.build_prompt_context()
|
|
assert "Loaded mentioned skills" in prompt
|
|
assert "# Tender Analyzer\nUse carefully." in prompt
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_on_context_create_raises_when_shell_pull_fails(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
layer = _build_layer()
|
|
_patch_file_help(monkeypatch)
|
|
|
|
async def fake_run_remote_script_complete(
|
|
self: DifyShellLayer,
|
|
script: str,
|
|
*,
|
|
timeout: float = 10.0,
|
|
inject_agent_stub_env: bool = False,
|
|
) -> CompleteRemoteCommandResult:
|
|
del self, script, timeout, inject_agent_stub_env
|
|
return _remote_result("permission denied\n", exit_code=1)
|
|
|
|
monkeypatch.setattr(DifyShellLayer, "run_remote_script_complete", fake_run_remote_script_complete)
|
|
|
|
with pytest.raises(DifyDriveLayerError) as exc_info:
|
|
await layer.on_context_create()
|
|
|
|
message = str(exc_info.value)
|
|
assert "drive mentioned pull failed in shell: exited exit_code=1" in message
|
|
assert "output_complete=True" in message
|
|
assert "output_path=/tmp/output.log" in message
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_on_context_create_raises_when_required_skill_marker_is_missing_from_complete_output(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
layer = _build_layer()
|
|
_patch_file_help(monkeypatch)
|
|
|
|
async def fake_run_remote_script_complete(
|
|
self: DifyShellLayer,
|
|
script: str,
|
|
*,
|
|
timeout: float = 10.0,
|
|
inject_agent_stub_env: bool = False,
|
|
) -> CompleteRemoteCommandResult:
|
|
del self, script, timeout, inject_agent_stub_env
|
|
return _remote_result("__DIFY_DRIVE_MENTIONED_PATH__\tfiles/report.pdf\t/mnt/drive/agent-1/files/report.pdf\n")
|
|
|
|
monkeypatch.setattr(DifyShellLayer, "run_remote_script_complete", fake_run_remote_script_complete)
|
|
|
|
with pytest.raises(DifyDriveLayerError, match="missing pulled SKILL.md"):
|
|
await layer.on_context_create()
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_on_context_create_reports_incomplete_capture_when_required_marker_is_missing(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
layer = _build_layer()
|
|
_patch_file_help(monkeypatch)
|
|
|
|
async def fake_run_remote_script_complete(
|
|
self: DifyShellLayer,
|
|
script: str,
|
|
*,
|
|
timeout: float = 10.0,
|
|
inject_agent_stub_env: bool = False,
|
|
) -> CompleteRemoteCommandResult:
|
|
del self, script, timeout, inject_agent_stub_env
|
|
output = (
|
|
"__DIFY_DRIVE_MENTIONED_PATH__\ttender-analyzer/SKILL.md\t/mnt/drive/agent-1/tender-analyzer/SKILL.md\n"
|
|
)
|
|
return _remote_result(output, output_complete=False, incomplete_reason="output_limit")
|
|
|
|
monkeypatch.setattr(DifyShellLayer, "run_remote_script_complete", fake_run_remote_script_complete)
|
|
|
|
with pytest.raises(DifyDriveLayerError) as exc_info:
|
|
await layer.on_context_create()
|
|
|
|
message = str(exc_info.value)
|
|
assert "output incomplete before required SKILL.md content was captured" in message
|
|
assert "reason=output_limit" in message
|