mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
feat: Unify Agent v2 console routes (#37465)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
a18635e566
commit
1e8329f02c
28 files changed
+6705
-3735
No files matched your search
@@ -1,16 +1,17 @@
|
||||
from inspect import unwrap
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.agent import composer as composer_controller
|
||||
from controllers.console.agent import roster as roster_controller
|
||||
from controllers.console.agent.composer import (
|
||||
AgentAppComposerApi,
|
||||
AgentAppComposerCandidatesApi,
|
||||
AgentAppComposerValidateApi,
|
||||
AgentComposerApi,
|
||||
AgentComposerCandidatesApi,
|
||||
AgentComposerValidateApi,
|
||||
WorkflowAgentComposerApi,
|
||||
WorkflowAgentComposerCandidatesApi,
|
||||
WorkflowAgentComposerImpactApi,
|
||||
@@ -18,42 +19,15 @@ from controllers.console.agent.composer import (
|
||||
WorkflowAgentComposerValidateApi,
|
||||
)
|
||||
from controllers.console.agent.roster import (
|
||||
AgentAppApi,
|
||||
AgentAppListApi,
|
||||
AgentInviteOptionsApi,
|
||||
AgentRosterDetailApi,
|
||||
AgentRosterListApi,
|
||||
AgentRosterVersionDetailApi,
|
||||
AgentRosterVersionsApi,
|
||||
)
|
||||
from models.model import AppMode
|
||||
from services.entities.agent_entities import ComposerSaveStrategy, ComposerVariant
|
||||
|
||||
|
||||
def _agent_response(agent_id: str = "agent-1") -> dict:
|
||||
return {
|
||||
"id": agent_id,
|
||||
"name": "Analyst",
|
||||
"description": "",
|
||||
"icon_type": None,
|
||||
"icon": None,
|
||||
"icon_background": None,
|
||||
"agent_kind": "dify_agent",
|
||||
"scope": "roster",
|
||||
"source": "agent_app",
|
||||
"app_id": None,
|
||||
"workflow_id": None,
|
||||
"workflow_node_id": None,
|
||||
"active_config_snapshot_id": "version-1",
|
||||
"active_config_snapshot": _version_response(),
|
||||
"status": "active",
|
||||
"created_by": "account-1",
|
||||
"updated_by": "account-1",
|
||||
"archived_by": None,
|
||||
"archived_at": None,
|
||||
"created_at": None,
|
||||
"updated_at": None,
|
||||
}
|
||||
|
||||
|
||||
def _version_response(version_id: str = "version-1") -> dict:
|
||||
return {
|
||||
"id": version_id,
|
||||
@@ -103,6 +77,37 @@ def _agent_app_composer_response() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _app_detail_obj(**overrides):
|
||||
data = {
|
||||
"id": "app-1",
|
||||
"name": "Iris",
|
||||
"description": "Agent app",
|
||||
"mode_compatible_with_agent": "agent",
|
||||
"icon_type": "emoji",
|
||||
"icon": "robot",
|
||||
"icon_background": "#fff",
|
||||
"enable_site": False,
|
||||
"enable_api": False,
|
||||
"app_model_config": None,
|
||||
"workflow": None,
|
||||
"tracing": None,
|
||||
"use_icon_as_answer_icon": False,
|
||||
"created_by": "account-1",
|
||||
"created_at": None,
|
||||
"updated_by": "account-1",
|
||||
"updated_at": None,
|
||||
"access_mode": None,
|
||||
"tags": [],
|
||||
"api_base_url": None,
|
||||
"max_active_requests": 0,
|
||||
"deleted_tools": [],
|
||||
"site": None,
|
||||
"bound_agent_id": "00000000-0000-0000-0000-000000000001",
|
||||
}
|
||||
data.update(overrides)
|
||||
return SimpleNamespace(**data)
|
||||
|
||||
|
||||
def _candidates_response(variant: str) -> dict:
|
||||
return {
|
||||
"variant": variant,
|
||||
@@ -112,20 +117,38 @@ def _candidates_response(variant: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _get_app_model_modes(view) -> list[AppMode]:
|
||||
current = view
|
||||
while current is not None:
|
||||
closure = getattr(current, "__closure__", None)
|
||||
if closure is not None:
|
||||
for cell in closure:
|
||||
try:
|
||||
value = cell.cell_contents
|
||||
except ValueError:
|
||||
continue
|
||||
if isinstance(value, list) and all(isinstance(item, AppMode) for item in value):
|
||||
return value
|
||||
current = getattr(current, "__wrapped__", None)
|
||||
return []
|
||||
def test_agent_v2_console_routes_are_agent_id_first() -> None:
|
||||
paths = {route for item in console_ns.resources for route in item.urls}
|
||||
|
||||
for route in (
|
||||
"/agent",
|
||||
"/agent/<uuid:agent_id>",
|
||||
"/agent/<uuid:agent_id>/composer",
|
||||
"/agent/<uuid:agent_id>/composer/validate",
|
||||
"/agent/<uuid:agent_id>/composer/candidates",
|
||||
"/agent/<uuid:agent_id>/features",
|
||||
"/agent/<uuid:agent_id>/referencing-workflows",
|
||||
"/agent/<uuid:agent_id>/drive/files",
|
||||
"/agent/<uuid:agent_id>/sandbox/files",
|
||||
"/agent/<uuid:agent_id>/skills/upload",
|
||||
"/agent/<uuid:agent_id>/files",
|
||||
"/agent/invite-options",
|
||||
):
|
||||
assert route in paths
|
||||
|
||||
for route in (
|
||||
"/agents",
|
||||
"/agents/invite-options",
|
||||
"/agents/<uuid:agent_id>",
|
||||
"/agents/<uuid:agent_id>/versions",
|
||||
"/apps/<uuid:app_id>/agent-composer",
|
||||
"/apps/<uuid:app_id>/agent-composer/validate",
|
||||
"/apps/<uuid:app_id>/agent-composer/candidates",
|
||||
"/apps/<uuid:app_id>/agent-features",
|
||||
"/apps/<uuid:app_id>/agent-referencing-workflows",
|
||||
"/apps/<uuid:app_id>/agent-sandbox/files",
|
||||
):
|
||||
assert route not in paths
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -133,26 +156,114 @@ def account_id() -> str:
|
||||
return "account-1"
|
||||
|
||||
|
||||
def test_roster_list_get_parses_query_and_calls_service(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_agent_app_list_and_create_use_agent_route(
|
||||
app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str
|
||||
) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def list_roster_agents(_self: object, **kwargs: object) -> dict[str, object]:
|
||||
captured.update(kwargs)
|
||||
return {"data": [], "page": kwargs["page"], "limit": kwargs["limit"], "total": 0, "has_more": False}
|
||||
class FakeAppService:
|
||||
def get_app(self, app_obj: object) -> object:
|
||||
return app_obj
|
||||
|
||||
monkeypatch.setattr(roster_controller.AgentRosterService, "list_roster_agents", list_roster_agents)
|
||||
def get_paginate_apps(self, user_id: str, tenant_id: str, params) -> object:
|
||||
captured["list"] = {"user_id": user_id, "tenant_id": tenant_id, "params": params}
|
||||
return SimpleNamespace(
|
||||
page=1,
|
||||
per_page=10,
|
||||
total=1,
|
||||
has_next=False,
|
||||
items=[_app_detail_obj(id="app-list", bound_agent_id="agent-list")],
|
||||
)
|
||||
|
||||
with app.test_request_context("/console/api/agents?page=2&limit=5&keyword=analyst"):
|
||||
result = unwrap(AgentRosterListApi.get)(AgentRosterListApi(), "tenant-1")
|
||||
def create_app(self, tenant_id: str, params, current_user: object) -> object:
|
||||
captured["create"] = {"tenant_id": tenant_id, "params": params, "current_user": current_user}
|
||||
return _app_detail_obj(id="app-created", bound_agent_id="agent-created")
|
||||
|
||||
assert result["page"] == 2
|
||||
assert captured == {"tenant_id": "tenant-1", "page": 2, "limit": 5, "keyword": "analyst"}
|
||||
monkeypatch.setattr(roster_controller, "AppService", FakeAppService)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.FeatureService,
|
||||
"get_system_features",
|
||||
lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
|
||||
)
|
||||
|
||||
with app.test_request_context("/console/api/agent?page=1&limit=10&mode=workflow"):
|
||||
listed = unwrap(AgentAppListApi.get)(AgentAppListApi(), "tenant-1", SimpleNamespace(id=account_id))
|
||||
|
||||
assert listed["page"] == 1
|
||||
assert listed["limit"] == 10
|
||||
assert listed["total"] == 1
|
||||
assert listed["data"][0]["id"] == "agent-list"
|
||||
assert "bound_agent_id" not in listed["data"][0]
|
||||
list_call = cast(dict[str, object], captured["list"])
|
||||
list_params = cast(Any, list_call["params"])
|
||||
assert list_params.mode == "agent"
|
||||
assert list_params.status == "normal"
|
||||
|
||||
with app.test_request_context(
|
||||
"/console/api/agent",
|
||||
json={"name": "Iris", "description": "Agent app", "icon_type": "emoji", "icon": "robot"},
|
||||
):
|
||||
created, status = unwrap(AgentAppListApi.post)(AgentAppListApi(), "tenant-1", SimpleNamespace(id=account_id))
|
||||
|
||||
assert status == 201
|
||||
assert created["id"] == "agent-created"
|
||||
assert "bound_agent_id" not in created
|
||||
create_call = cast(dict[str, object], captured["create"])
|
||||
create_params = cast(Any, create_call["params"])
|
||||
assert create_params.mode == "agent"
|
||||
|
||||
|
||||
def test_roster_direct_mutation_endpoints_are_not_exposed() -> None:
|
||||
assert not hasattr(AgentRosterListApi, "post")
|
||||
assert not hasattr(AgentRosterDetailApi, "patch")
|
||||
assert not hasattr(AgentRosterDetailApi, "delete")
|
||||
def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
|
||||
app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str
|
||||
) -> None:
|
||||
agent_id = "00000000-0000-0000-0000-000000000001"
|
||||
app_model = _app_detail_obj(id="app-1", bound_agent_id=agent_id)
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
monkeypatch.setattr(
|
||||
roster_controller.AgentRosterService,
|
||||
"get_agent_app_model",
|
||||
lambda _self, **kwargs: app_model,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.FeatureService,
|
||||
"get_system_features",
|
||||
lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)),
|
||||
)
|
||||
|
||||
class FakeAppService:
|
||||
def get_app(self, app_obj: object) -> object:
|
||||
captured["get_app"] = app_obj
|
||||
return app_obj
|
||||
|
||||
def update_app(self, app_obj: object, args: dict[str, object]) -> object:
|
||||
captured["update"] = {"app": app_obj, "args": args}
|
||||
return _app_detail_obj(id="app-1", name=args["name"], bound_agent_id=agent_id)
|
||||
|
||||
def delete_app(self, app_obj: object) -> None:
|
||||
captured["delete"] = app_obj
|
||||
|
||||
monkeypatch.setattr(roster_controller, "AppService", FakeAppService)
|
||||
|
||||
detail = unwrap(AgentAppApi.get)(AgentAppApi(), "tenant-1", agent_id)
|
||||
assert detail["id"] == agent_id
|
||||
assert "bound_agent_id" not in detail
|
||||
|
||||
with app.test_request_context(
|
||||
"/console/api/agent/00000000-0000-0000-0000-000000000001",
|
||||
json={"name": "Renamed", "description": "", "icon_type": "emoji", "icon": "R"},
|
||||
):
|
||||
updated = unwrap(AgentAppApi.put)(AgentAppApi(), "tenant-1", agent_id)
|
||||
|
||||
assert updated["name"] == "Renamed"
|
||||
assert updated["id"] == agent_id
|
||||
assert "bound_agent_id" not in updated
|
||||
update_call = cast(dict[str, object], captured["update"])
|
||||
assert update_call["app"] is app_model
|
||||
|
||||
deleted, status = unwrap(AgentAppApi.delete)(AgentAppApi(), "tenant-1", agent_id)
|
||||
assert (deleted, status) == ("", 204)
|
||||
assert captured["delete"] is app_model
|
||||
|
||||
|
||||
def test_invite_options_get_parses_app_id(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -164,21 +275,16 @@ def test_invite_options_get_parses_app_id(app: Flask, monkeypatch: pytest.Monkey
|
||||
|
||||
monkeypatch.setattr(roster_controller.AgentRosterService, "list_invite_options", list_invite_options)
|
||||
|
||||
with app.test_request_context("/console/api/agents/invite-options?page=1&limit=10&app_id=app-1"):
|
||||
with app.test_request_context("/console/api/agent/invite-options?page=1&limit=10&app_id=app-1"):
|
||||
result = unwrap(AgentInviteOptionsApi.get)(AgentInviteOptionsApi(), "tenant-1")
|
||||
|
||||
assert result == {"data": [], "page": 1, "limit": 10, "total": 0, "has_more": False}
|
||||
assert captured == {"tenant_id": "tenant-1", "page": 1, "limit": 10, "keyword": None, "app_id": "app-1"}
|
||||
|
||||
|
||||
def test_roster_detail_and_versions_call_services(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_agent_versions_call_services(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
agent_id = "00000000-0000-0000-0000-000000000001"
|
||||
version_id = "00000000-0000-0000-0000-000000000002"
|
||||
monkeypatch.setattr(
|
||||
roster_controller.AgentRosterService,
|
||||
"get_roster_agent_detail",
|
||||
lambda _self, **kwargs: _agent_response(cast(str, kwargs["agent_id"])),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
roster_controller.AgentRosterService,
|
||||
"list_agent_versions",
|
||||
@@ -207,7 +313,6 @@ def test_roster_detail_and_versions_call_services(app: Flask, monkeypatch: pytes
|
||||
},
|
||||
)
|
||||
|
||||
assert unwrap(AgentRosterDetailApi.get)(AgentRosterDetailApi(), "tenant-1", agent_id)["id"] == agent_id
|
||||
assert (
|
||||
unwrap(AgentRosterVersionsApi.get)(AgentRosterVersionsApi(), "tenant-1", agent_id)["data"][0]["id"]
|
||||
== "version-1"
|
||||
@@ -294,59 +399,76 @@ def test_workflow_impact_returns_empty_without_version(app: Flask) -> None:
|
||||
assert result == {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []}
|
||||
|
||||
|
||||
def test_agent_app_composer_get_put_validate_and_candidates(
|
||||
def test_agent_composer_routes_resolve_app_from_agent_id(
|
||||
app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str
|
||||
) -> None:
|
||||
app_model = SimpleNamespace(id="app-1")
|
||||
agent_id = "00000000-0000-0000-0000-000000000001"
|
||||
captured: dict[str, object] = {}
|
||||
payload = {
|
||||
"variant": ComposerVariant.AGENT_APP.value,
|
||||
"save_strategy": ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION.value,
|
||||
"agent_soul": {"prompt": {"system_prompt": "x"}},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(composer_controller, "resolve_agent_app_model", lambda **kwargs: SimpleNamespace(id="app-1"))
|
||||
|
||||
def load_agent_app_composer(**kwargs: object) -> dict:
|
||||
captured["load"] = kwargs
|
||||
return _agent_app_composer_response()
|
||||
|
||||
def save_agent_app_composer(**kwargs: object) -> dict:
|
||||
captured["save"] = kwargs
|
||||
return _agent_app_composer_response()
|
||||
|
||||
def collect_validation_findings(**kwargs: object) -> dict:
|
||||
captured["validate"] = kwargs
|
||||
return {"warnings": [], "knowledge_retrieval_placeholder": []}
|
||||
|
||||
def get_agent_app_candidates(**kwargs: object) -> dict:
|
||||
captured["candidates"] = kwargs
|
||||
return _candidates_response("agent_app")
|
||||
|
||||
monkeypatch.setattr(
|
||||
composer_controller.AgentComposerService,
|
||||
"load_agent_app_composer",
|
||||
lambda **kwargs: _agent_app_composer_response(),
|
||||
load_agent_app_composer,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
composer_controller.AgentComposerService,
|
||||
"save_agent_app_composer",
|
||||
lambda **kwargs: _agent_app_composer_response(),
|
||||
save_agent_app_composer,
|
||||
)
|
||||
monkeypatch.setattr(composer_controller.ComposerConfigValidator, "validate_save_payload", lambda payload: None)
|
||||
monkeypatch.setattr(
|
||||
composer_controller.AgentComposerService, "resolve_workflow_node_agent_id", lambda **kwargs: None
|
||||
composer_controller.AgentComposerService,
|
||||
"collect_validation_findings",
|
||||
collect_validation_findings,
|
||||
)
|
||||
monkeypatch.setattr(composer_controller.AgentComposerService, "resolve_bound_agent_id", lambda **kwargs: None)
|
||||
monkeypatch.setattr(
|
||||
composer_controller.AgentComposerService,
|
||||
"get_agent_app_candidates",
|
||||
lambda **kwargs: _candidates_response("agent_app"),
|
||||
get_agent_app_candidates,
|
||||
)
|
||||
|
||||
assert unwrap(AgentAppComposerApi.get)(AgentAppComposerApi(), "tenant-1", app_model)["variant"] == "agent_app"
|
||||
assert unwrap(AgentComposerApi.get)(AgentComposerApi(), "tenant-1", agent_id)["variant"] == "agent_app"
|
||||
assert cast(dict[str, object], captured["load"])["app_id"] == "app-1"
|
||||
|
||||
with app.test_request_context(json=payload):
|
||||
assert (
|
||||
unwrap(AgentAppComposerApi.put)(AgentAppComposerApi(), "tenant-1", account_id, app_model)["variant"]
|
||||
== "agent_app"
|
||||
unwrap(AgentComposerApi.put)(AgentComposerApi(), "tenant-1", account_id, agent_id)["variant"] == "agent_app"
|
||||
)
|
||||
assert unwrap(AgentAppComposerValidateApi.post)(AgentAppComposerValidateApi(), "tenant-1", app_model) == {
|
||||
assert cast(dict[str, object], captured["save"])["app_id"] == "app-1"
|
||||
assert unwrap(AgentComposerValidateApi.post)(AgentComposerValidateApi(), "tenant-1", agent_id) == {
|
||||
"result": "success",
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
"knowledge_retrieval_placeholder": [],
|
||||
}
|
||||
agent_app_candidates = unwrap(AgentAppComposerCandidatesApi.get)(
|
||||
AgentAppComposerCandidatesApi(), "tenant-1", account_id, app_model
|
||||
)
|
||||
assert agent_app_candidates["variant"] == "agent_app"
|
||||
assert cast(dict[str, object], captured["validate"])["agent_id"] == agent_id
|
||||
|
||||
|
||||
def test_agent_app_composer_routes_are_agent_mode_only() -> None:
|
||||
assert _get_app_model_modes(AgentAppComposerApi.get) == [AppMode.AGENT]
|
||||
assert _get_app_model_modes(AgentAppComposerApi.put) == [AppMode.AGENT]
|
||||
assert _get_app_model_modes(AgentAppComposerValidateApi.post) == [AppMode.AGENT]
|
||||
assert _get_app_model_modes(AgentAppComposerCandidatesApi.get) == [AppMode.AGENT]
|
||||
candidates = unwrap(AgentComposerCandidatesApi.get)(AgentComposerCandidatesApi(), "tenant-1", account_id, agent_id)
|
||||
assert candidates["variant"] == "agent_app"
|
||||
assert cast(dict[str, object], captured["candidates"])["app_id"] == "app-1"
|
||||
|
||||
|
||||
def test_dify_tool_candidate_response_keeps_granularity_fields():
|
||||
|
||||
@@ -119,6 +119,7 @@ def test_handle_maps_sandbox_and_agent_backend_errors() -> None:
|
||||
def test_agent_app_sandbox_resources_proxy_service(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
service = _AgentAppService()
|
||||
monkeypatch.setattr(module, "AgentAppSandboxService", lambda: service)
|
||||
monkeypatch.setattr(module, "resolve_agent_app_model", lambda *, tenant_id, agent_id: _app_model())
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"query_params_from_request",
|
||||
@@ -129,11 +130,10 @@ def test_agent_app_sandbox_resources_proxy_service(monkeypatch: pytest.MonkeyPat
|
||||
"request",
|
||||
SimpleNamespace(get_json=lambda silent=True: {"conversation_id": "conv-1", "path": "report.txt"}),
|
||||
)
|
||||
app_model = _app_model()
|
||||
|
||||
listing = unwrap(module.AgentAppSandboxListResource.get)(object(), "tenant-1", app_model)
|
||||
preview = unwrap(module.AgentAppSandboxReadResource.get)(object(), "tenant-1", app_model)
|
||||
upload = unwrap(module.AgentAppSandboxUploadResource.post)(object(), "tenant-1", app_model)
|
||||
listing = unwrap(module.AgentAppSandboxListResource.get)(object(), "tenant-1", "agent-1")
|
||||
preview = unwrap(module.AgentAppSandboxReadResource.get)(object(), "tenant-1", "agent-1")
|
||||
upload = unwrap(module.AgentAppSandboxUploadResource.post)(object(), "tenant-1", "agent-1")
|
||||
|
||||
assert listing["path"] == "sub/report.txt"
|
||||
assert preview["text"] == "hello"
|
||||
@@ -151,11 +151,12 @@ def test_agent_app_sandbox_resource_returns_normalized_errors(monkeypatch: pytes
|
||||
raise AgentSandboxInspectorError("no_active_session", "no active session", status_code=404)
|
||||
|
||||
monkeypatch.setattr(module, "AgentAppSandboxService", FailingService)
|
||||
monkeypatch.setattr(module, "resolve_agent_app_model", lambda *, tenant_id, agent_id: _app_model())
|
||||
monkeypatch.setattr(
|
||||
module, "query_params_from_request", lambda model: SimpleNamespace(conversation_id="conv-1", path=".")
|
||||
)
|
||||
|
||||
assert unwrap(module.AgentAppSandboxListResource.get)(object(), "tenant-1", _app_model()) == (
|
||||
assert unwrap(module.AgentAppSandboxListResource.get)(object(), "tenant-1", "agent-1") == (
|
||||
{"code": "no_active_session", "message": "no active session"},
|
||||
404,
|
||||
)
|
||||
|
||||
@@ -15,8 +15,11 @@ from flask import Flask
|
||||
|
||||
from controllers.console.app.agent_drive_inspector import (
|
||||
AgentDriveDownloadApi,
|
||||
AgentDriveDownloadByAgentApi,
|
||||
AgentDriveListApi,
|
||||
AgentDriveListByAgentApi,
|
||||
AgentDrivePreviewApi,
|
||||
AgentDrivePreviewByAgentApi,
|
||||
)
|
||||
from services.agent_drive_service import AgentDriveError
|
||||
|
||||
@@ -53,6 +56,32 @@ def test_list_filters_value_pointers_out_of_console_payload():
|
||||
assert drive.return_value.manifest.call_args.kwargs["prefix"] == "pdf-toolkit/"
|
||||
|
||||
|
||||
def test_list_by_agent_filters_value_pointers_out_of_console_payload():
|
||||
raw = _raw(AgentDriveListByAgentApi.get)
|
||||
with app.test_request_context("/?prefix=pdf-toolkit/"):
|
||||
with (
|
||||
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
|
||||
patch(f"{_MOD}.AgentDriveService") as drive,
|
||||
):
|
||||
drive.return_value.manifest.return_value = [
|
||||
{
|
||||
"key": "pdf-toolkit/SKILL.md",
|
||||
"size": 5,
|
||||
"hash": "h",
|
||||
"mime_type": "text/markdown",
|
||||
"file_kind": "tool_file",
|
||||
"file_id": "tf-1",
|
||||
"created_at": 1718000000,
|
||||
}
|
||||
]
|
||||
body = raw(AgentDriveListByAgentApi(), "tenant-1", "agent-1")
|
||||
|
||||
assert body["items"][0]["key"] == "pdf-toolkit/SKILL.md"
|
||||
assert "file_id" not in body["items"][0]
|
||||
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
|
||||
assert drive.return_value.manifest.call_args.kwargs["agent_id"] == "agent-1"
|
||||
|
||||
|
||||
def test_list_resolves_workflow_node_binding_agent():
|
||||
raw = _raw(AgentDriveListApi.get)
|
||||
with app.test_request_context("/?node_id=agent-node-1"):
|
||||
@@ -101,6 +130,37 @@ def test_preview_passes_through_and_maps_errors():
|
||||
assert body["code"] == "drive_key_not_found"
|
||||
|
||||
|
||||
def test_preview_by_agent_passes_through_and_maps_errors():
|
||||
raw = _raw(AgentDrivePreviewByAgentApi.get)
|
||||
with app.test_request_context("/?key=pdf-toolkit/SKILL.md"):
|
||||
with (
|
||||
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
|
||||
patch(f"{_MOD}.AgentDriveService") as drive,
|
||||
):
|
||||
drive.return_value.preview.return_value = {
|
||||
"key": "pdf-toolkit/SKILL.md",
|
||||
"size": 5,
|
||||
"truncated": False,
|
||||
"binary": False,
|
||||
"text": "# hi",
|
||||
}
|
||||
body = raw(AgentDrivePreviewByAgentApi(), "tenant-1", "agent-1")
|
||||
assert body["text"] == "# hi"
|
||||
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
|
||||
|
||||
with app.test_request_context("/?key=ghost/SKILL.md"):
|
||||
with (
|
||||
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP),
|
||||
patch(f"{_MOD}.AgentDriveService") as drive,
|
||||
):
|
||||
drive.return_value.preview.side_effect = AgentDriveError(
|
||||
"drive_key_not_found", "no drive entry", status_code=404
|
||||
)
|
||||
body, status = raw(AgentDrivePreviewByAgentApi(), "tenant-1", "agent-1")
|
||||
assert status == 404
|
||||
assert body["code"] == "drive_key_not_found"
|
||||
|
||||
|
||||
def test_download_returns_signed_url_json():
|
||||
raw = _raw(AgentDriveDownloadApi.get)
|
||||
with app.test_request_context("/?key=pdf-toolkit/.DIFY-SKILL-FULL.zip"):
|
||||
@@ -108,3 +168,16 @@ def test_download_returns_signed_url_json():
|
||||
drive.return_value.download_url.return_value = "https://signed.example/zip"
|
||||
body = raw(AgentDriveDownloadApi(), _APP)
|
||||
assert body == {"url": "https://signed.example/zip"}
|
||||
|
||||
|
||||
def test_download_by_agent_returns_signed_url_json():
|
||||
raw = _raw(AgentDriveDownloadByAgentApi.get)
|
||||
with app.test_request_context("/?key=pdf-toolkit/.DIFY-SKILL-FULL.zip"):
|
||||
with (
|
||||
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
|
||||
patch(f"{_MOD}.AgentDriveService") as drive,
|
||||
):
|
||||
drive.return_value.download_url.return_value = "https://signed.example/zip"
|
||||
body = raw(AgentDriveDownloadByAgentApi(), "tenant-1", "agent-1")
|
||||
assert body == {"url": "https://signed.example/zip"}
|
||||
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
|
||||
@@ -14,7 +14,16 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from controllers.console.app.agent import AgentSkillStandardizeApi, AgentSkillUploadApi
|
||||
from controllers.console.app.agent import (
|
||||
AgentDriveFilesByAgentApi,
|
||||
AgentSkillByAgentApi,
|
||||
AgentSkillInferToolsByAgentApi,
|
||||
AgentSkillStandardizeApi,
|
||||
AgentSkillStandardizeByAgentApi,
|
||||
AgentSkillUploadApi,
|
||||
AgentSkillUploadByAgentApi,
|
||||
)
|
||||
from models.model import AppMode
|
||||
from services.agent.skill_package_service import SkillPackageError
|
||||
from services.agent_drive_service import AgentDriveError
|
||||
|
||||
@@ -32,7 +41,8 @@ def _file_ctx(*, files: dict[str, bytes] | None = None):
|
||||
|
||||
|
||||
_USER = SimpleNamespace(id="user-1")
|
||||
_APP = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id="agent-1")
|
||||
_APP = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.AGENT, bound_agent_id="agent-1")
|
||||
_WORKFLOW_APP = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW, bound_agent_id=None)
|
||||
|
||||
|
||||
def test_upload_validates_and_returns_skill_ref():
|
||||
@@ -56,6 +66,28 @@ def test_upload_validates_and_returns_skill_ref():
|
||||
manifest.to_skill_ref.assert_called_once_with(file_id="uf-1")
|
||||
|
||||
|
||||
def test_upload_by_agent_resolves_app_and_returns_skill_ref():
|
||||
raw = _raw(AgentSkillUploadByAgentApi.post)
|
||||
manifest = MagicMock()
|
||||
manifest.to_skill_ref.return_value.model_dump.return_value = {"name": "S", "file_id": "uf-1"}
|
||||
manifest.model_dump.return_value = {"name": "S"}
|
||||
|
||||
with _file_ctx(files={"file": b"zip-bytes"}):
|
||||
with (
|
||||
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
|
||||
patch(f"{_MOD}.SkillPackageService") as pkg,
|
||||
patch(f"{_MOD}.FileService") as fs,
|
||||
patch(f"{_MOD}.db"),
|
||||
):
|
||||
pkg.return_value.validate_and_extract.return_value = manifest
|
||||
fs.return_value.upload_file.return_value = SimpleNamespace(id="uf-1")
|
||||
body, status = raw(AgentSkillUploadByAgentApi(), "tenant-1", _USER, "agent-1")
|
||||
|
||||
assert status == 201
|
||||
assert body["skill"] == {"name": "S", "file_id": "uf-1"}
|
||||
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
|
||||
|
||||
|
||||
def test_upload_no_file_is_400():
|
||||
raw = _raw(AgentSkillUploadApi.post)
|
||||
with _file_ctx(files={}):
|
||||
@@ -87,9 +119,25 @@ def test_standardize_returns_result():
|
||||
assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1"
|
||||
|
||||
|
||||
def test_standardize_by_agent_resolves_app():
|
||||
raw = _raw(AgentSkillStandardizeByAgentApi.post)
|
||||
with _file_ctx(files={"file": b"zip"}):
|
||||
with (
|
||||
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
|
||||
patch(f"{_MOD}.SkillStandardizeService") as svc,
|
||||
):
|
||||
svc.return_value.standardize.return_value = {"skill": {"path": "s"}, "manifest": {}}
|
||||
body, status = raw(AgentSkillStandardizeByAgentApi(), "tenant-1", _USER, "agent-1")
|
||||
|
||||
assert status == 201
|
||||
assert body["skill"] == {"path": "s"}
|
||||
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
|
||||
assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1"
|
||||
|
||||
|
||||
def test_standardize_no_bound_agent_is_400():
|
||||
raw = _raw(AgentSkillStandardizeApi.post)
|
||||
app_without_agent = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id=None)
|
||||
app_without_agent = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.AGENT, bound_agent_id=None)
|
||||
with _file_ctx(files={"file": b"zip"}):
|
||||
body, status = raw(AgentSkillStandardizeApi(), _USER, app_without_agent)
|
||||
assert status == 400
|
||||
@@ -98,7 +146,6 @@ def test_standardize_no_bound_agent_is_400():
|
||||
|
||||
def test_standardize_resolves_workflow_node_agent():
|
||||
raw = _raw(AgentSkillStandardizeApi.post)
|
||||
workflow_app = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id=None)
|
||||
with app.test_request_context(
|
||||
"/?node_id=agent-node-1", method="POST", data={"file": (io.BytesIO(b"zip"), "skill.zip")}
|
||||
):
|
||||
@@ -108,7 +155,7 @@ def test_standardize_resolves_workflow_node_agent():
|
||||
):
|
||||
composer.resolve_workflow_node_agent_id.return_value = "wf-agent-1"
|
||||
svc.return_value.standardize.return_value = {"skill": {"path": "s"}, "manifest": {}}
|
||||
body, status = raw(AgentSkillStandardizeApi(), _USER, workflow_app)
|
||||
body, status = raw(AgentSkillStandardizeApi(), _USER, _WORKFLOW_APP)
|
||||
|
||||
assert status == 201
|
||||
assert body["skill"] == {"path": "s"}
|
||||
@@ -165,6 +212,31 @@ def test_files_commit_validates_upload_and_returns_drive_ref():
|
||||
assert composer.add_drive_file_ref.call_args.kwargs["app_id"] == "app-1"
|
||||
|
||||
|
||||
def test_files_by_agent_commit_uses_agent_route_and_ignores_node_id():
|
||||
raw = _raw(AgentDriveFilesByAgentApi.post)
|
||||
upload = SimpleNamespace(id="uf-1", name="sample.pdf")
|
||||
with _json_ctx({"upload_file_id": "0fa6f9bc-3416-4476-8857-a13129704dd9"}, query_string="node_id=ignored"):
|
||||
with (
|
||||
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
|
||||
patch(f"{_MOD}.console_ns") as ns,
|
||||
patch(f"{_MOD}.db") as db_mock,
|
||||
patch(f"{_MOD}.AgentDriveService") as drive,
|
||||
patch(f"{_MOD}.AgentComposerService") as composer,
|
||||
):
|
||||
ns.payload = {"upload_file_id": "0fa6f9bc-3416-4476-8857-a13129704dd9"}
|
||||
db_mock.session.scalar.return_value = upload
|
||||
drive.return_value.commit.return_value = [
|
||||
{"key": "files/sample.pdf", "size": 5, "mime_type": "application/pdf"}
|
||||
]
|
||||
composer.add_drive_file_ref.return_value = "ver-2"
|
||||
body, status = raw(AgentDriveFilesByAgentApi(), "tenant-1", _USER, "agent-1")
|
||||
|
||||
assert status == 201
|
||||
assert body["config_version_id"] == "ver-2"
|
||||
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
|
||||
assert composer.add_drive_file_ref.call_args.kwargs["node_id"] is None
|
||||
|
||||
|
||||
def test_files_commit_404_when_upload_not_in_tenant():
|
||||
from controllers.console.app.agent import AgentDriveFilesApi
|
||||
|
||||
@@ -186,7 +258,6 @@ def test_files_commit_resolves_workflow_node_agent():
|
||||
|
||||
raw = _raw(AgentDriveFilesApi.post)
|
||||
upload = SimpleNamespace(id="uf-1", name="sample.pdf")
|
||||
workflow_app = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id=None)
|
||||
with _json_ctx({"upload_file_id": "0fa6f9bc-3416-4476-8857-a13129704dd9"}, query_string="node_id=agent-node-1"):
|
||||
with (
|
||||
patch(f"{_MOD}.console_ns") as ns,
|
||||
@@ -201,7 +272,7 @@ def test_files_commit_resolves_workflow_node_agent():
|
||||
{"key": "files/sample.pdf", "size": 5, "mime_type": "application/pdf"}
|
||||
]
|
||||
composer.add_drive_file_ref.return_value = "ver-2"
|
||||
body, status = raw(AgentDriveFilesApi(), _USER, workflow_app)
|
||||
body, status = raw(AgentDriveFilesApi(), _USER, _WORKFLOW_APP)
|
||||
|
||||
assert status == 201
|
||||
assert body["config_version_id"] == "ver-2"
|
||||
@@ -229,11 +300,27 @@ def test_files_delete_updates_soul_then_drive():
|
||||
assert composer.remove_drive_refs.call_args.kwargs["app_id"] == "app-1"
|
||||
|
||||
|
||||
def test_files_by_agent_delete_uses_agent_route_and_ignores_node_id():
|
||||
raw = _raw(AgentDriveFilesByAgentApi.delete)
|
||||
with _json_ctx(method="DELETE", query_string="key=files/sample.pdf&node_id=ignored"):
|
||||
with (
|
||||
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
|
||||
patch(f"{_MOD}.AgentComposerService") as composer,
|
||||
patch(f"{_MOD}.AgentDriveService") as drive,
|
||||
):
|
||||
composer.remove_drive_refs.return_value = "ver-2"
|
||||
drive.return_value.delete.return_value = ["files/sample.pdf"]
|
||||
body = raw(AgentDriveFilesByAgentApi(), "tenant-1", _USER, "agent-1")
|
||||
|
||||
assert body["config_version_id"] == "ver-2"
|
||||
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
|
||||
assert composer.remove_drive_refs.call_args.kwargs["node_id"] is None
|
||||
|
||||
|
||||
def test_files_delete_resolves_workflow_node_agent():
|
||||
from controllers.console.app.agent import AgentDriveFilesApi
|
||||
|
||||
raw = _raw(AgentDriveFilesApi.delete)
|
||||
workflow_app = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id=None)
|
||||
with _json_ctx(method="DELETE", query_string="key=files/sample.pdf&node_id=agent-node-1"):
|
||||
with (
|
||||
patch(f"{_MOD}.AgentComposerService") as composer,
|
||||
@@ -242,7 +329,7 @@ def test_files_delete_resolves_workflow_node_agent():
|
||||
composer.resolve_workflow_node_agent_id.return_value = "wf-agent-1"
|
||||
composer.remove_drive_refs.return_value = "ver-2"
|
||||
drive.return_value.delete.return_value = ["files/sample.pdf"]
|
||||
body = raw(AgentDriveFilesApi(), _USER, workflow_app)
|
||||
body = raw(AgentDriveFilesApi(), _USER, _WORKFLOW_APP)
|
||||
|
||||
assert body["config_version_id"] == "ver-2"
|
||||
assert drive.return_value.delete.call_args.kwargs["agent_id"] == "wf-agent-1"
|
||||
@@ -284,6 +371,23 @@ def test_skill_delete_uses_slug_prefix_and_is_idempotent():
|
||||
assert composer.remove_drive_refs.call_args.kwargs["app_id"] == "app-1"
|
||||
|
||||
|
||||
def test_skill_delete_by_agent_uses_agent_route():
|
||||
raw = _raw(AgentSkillByAgentApi.delete)
|
||||
with _json_ctx(method="DELETE", query_string="node_id=ignored"):
|
||||
with (
|
||||
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
|
||||
patch(f"{_MOD}.AgentComposerService") as composer,
|
||||
patch(f"{_MOD}.AgentDriveService") as drive,
|
||||
):
|
||||
composer.remove_drive_refs.return_value = "ver-2"
|
||||
drive.return_value.delete.return_value = ["tender-analyzer/SKILL.md"]
|
||||
body = raw(AgentSkillByAgentApi(), "tenant-1", _USER, "agent-1", "tender-analyzer")
|
||||
|
||||
assert body["config_version_id"] == "ver-2"
|
||||
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
|
||||
assert composer.remove_drive_refs.call_args.kwargs["node_id"] is None
|
||||
|
||||
|
||||
def test_skill_delete_rejects_path_like_slug():
|
||||
from controllers.console.app.agent import AgentSkillApi
|
||||
|
||||
@@ -314,11 +418,25 @@ def test_infer_tools_returns_draft_suggestions():
|
||||
assert svc.return_value.infer.call_args.kwargs["slug"] == "audio-transcribe"
|
||||
|
||||
|
||||
def test_infer_tools_by_agent_uses_agent_route():
|
||||
raw = _raw(AgentSkillInferToolsByAgentApi.post)
|
||||
with _json_ctx(query_string="node_id=ignored"):
|
||||
with (
|
||||
patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app,
|
||||
patch(f"{_MOD}.SkillToolInferenceService") as svc,
|
||||
):
|
||||
svc.return_value.infer.return_value = {"inferable": True, "cli_tools": [], "reason": None}
|
||||
body = raw(AgentSkillInferToolsByAgentApi(), "tenant-1", "agent-1", "audio-transcribe")
|
||||
|
||||
assert body["inferable"] is True
|
||||
resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1")
|
||||
assert svc.return_value.infer.call_args.kwargs["agent_id"] == "agent-1"
|
||||
|
||||
|
||||
def test_infer_tools_resolves_workflow_node_agent():
|
||||
from controllers.console.app.agent import AgentSkillInferToolsApi
|
||||
|
||||
raw = _raw(AgentSkillInferToolsApi.post)
|
||||
workflow_app = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id=None)
|
||||
with _json_ctx(query_string="node_id=agent-node-1"):
|
||||
with (
|
||||
patch(f"{_MOD}.AgentComposerService") as composer,
|
||||
@@ -326,7 +444,7 @@ def test_infer_tools_resolves_workflow_node_agent():
|
||||
):
|
||||
composer.resolve_workflow_node_agent_id.return_value = "wf-agent-1"
|
||||
svc.return_value.infer.return_value = {"inferable": False, "cli_tools": [], "reason": "none"}
|
||||
body = raw(AgentSkillInferToolsApi(), workflow_app, "audio-transcribe")
|
||||
body = raw(AgentSkillInferToolsApi(), _WORKFLOW_APP, "audio-transcribe")
|
||||
|
||||
assert body["inferable"] is False
|
||||
assert svc.return_value.infer.call_args.kwargs["agent_id"] == "wf-agent-1"
|
||||
@@ -355,7 +473,7 @@ def test_infer_tools_rejects_path_like_slug_and_unbound_app():
|
||||
body, status = raw(AgentSkillInferToolsApi(), _APP, "a/b")
|
||||
assert (status, body["code"]) == (400, "drive_key_invalid")
|
||||
|
||||
app_without_agent = SimpleNamespace(id="app-1", tenant_id="tenant-1", bound_agent_id=None)
|
||||
app_without_agent = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.AGENT, bound_agent_id=None)
|
||||
with _json_ctx():
|
||||
body, status = raw(AgentSkillInferToolsApi(), app_without_agent, "x")
|
||||
assert (status, body["code"]) == (400, "agent_not_bound")
|
||||
@@ -314,37 +314,19 @@ def test_app_list_query_rejects_flat_tag_ids(app_module):
|
||||
app_module.AppListQuery.model_validate(normalized)
|
||||
|
||||
|
||||
def test_create_agent_app_response_includes_bound_agent_id(app_module, monkeypatch: pytest.MonkeyPatch):
|
||||
def test_create_app_endpoint_rejects_agent_mode(app_module, monkeypatch: pytest.MonkeyPatch):
|
||||
payload = {"name": "Iris", "mode": "agent", "description": "Agent app"}
|
||||
app_obj = SimpleNamespace(
|
||||
id="app-1",
|
||||
name="Iris",
|
||||
description="Agent app",
|
||||
mode_compatible_with_agent="agent",
|
||||
icon_type="emoji",
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
enable_site=False,
|
||||
enable_api=False,
|
||||
created_at=_ts(),
|
||||
updated_at=_ts(),
|
||||
bound_agent_id="agent-1",
|
||||
)
|
||||
app_service = MagicMock()
|
||||
app_service.create_app.return_value = app_obj
|
||||
monkeypatch.setattr(app_module, "AppService", lambda: app_service)
|
||||
|
||||
app_module.console_ns.payload = payload
|
||||
try:
|
||||
response, status = _unwrap(app_module.AppListApi().post)("tenant-1", SimpleNamespace(id="account-1"))
|
||||
with pytest.raises(ValidationError):
|
||||
_unwrap(app_module.AppListApi().post)("tenant-1", SimpleNamespace(id="account-1"))
|
||||
finally:
|
||||
app_module.console_ns.payload = None
|
||||
|
||||
assert status == 201
|
||||
assert response["id"] == "app-1"
|
||||
assert response["bound_agent_id"] == "agent-1"
|
||||
created_params = app_service.create_app.call_args.args[1]
|
||||
assert created_params.mode == "agent"
|
||||
app_service.create_app.assert_not_called()
|
||||
|
||||
|
||||
def test_app_partial_serialization_uses_aliases(app_models):
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
"""Regression tests for CreateAppPayload mode validation.
|
||||
|
||||
The HTTP create-app payload must accept the new "agent" app mode; without it a
|
||||
user cannot create an Agent App through POST /console/api/apps even though the
|
||||
service layer (CreateAppParams) supports it.
|
||||
"""
|
||||
"""Regression tests for CreateAppPayload mode validation."""
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
@@ -14,12 +9,16 @@ from controllers.console.app.app import CreateAppPayload
|
||||
class TestCreateAppPayloadMode:
|
||||
@pytest.mark.parametrize(
|
||||
"mode",
|
||||
["chat", "agent-chat", "agent", "advanced-chat", "workflow", "completion"],
|
||||
["chat", "agent-chat", "advanced-chat", "workflow", "completion"],
|
||||
)
|
||||
def test_accepts_supported_modes(self, mode: str):
|
||||
payload = CreateAppPayload.model_validate({"name": "X", "mode": mode})
|
||||
assert payload.mode == mode
|
||||
|
||||
def test_rejects_agent_mode(self):
|
||||
with pytest.raises(ValidationError):
|
||||
CreateAppPayload.model_validate({"name": "X", "mode": "agent"})
|
||||
|
||||
def test_rejects_unknown_mode(self):
|
||||
with pytest.raises(ValidationError):
|
||||
CreateAppPayload.model_validate({"name": "X", "mode": "not-a-mode"})
|
||||
@@ -1037,6 +1037,31 @@ class TestAgentAppBackingAgent:
|
||||
|
||||
assert service.get_app_backing_agent(tenant_id="tenant-1", app_id="app-x") is None
|
||||
|
||||
def test_get_agent_app_model_resolves_app_backing_agent(self):
|
||||
agent = Agent(
|
||||
id="agent-1",
|
||||
tenant_id="tenant-1",
|
||||
name="Iris",
|
||||
description="",
|
||||
agent_kind=AgentKind.DIFY_AGENT,
|
||||
scope=AgentScope.ROSTER,
|
||||
source=AgentSource.AGENT_APP,
|
||||
status=AgentStatus.ACTIVE,
|
||||
app_id="app-1",
|
||||
)
|
||||
app = SimpleNamespace(id="app-1", mode="agent", status="normal")
|
||||
session = FakeSession(scalar=[agent, app])
|
||||
service = AgentRosterService(session)
|
||||
|
||||
assert service.get_agent_app_model(tenant_id="tenant-1", agent_id="agent-1") is app
|
||||
|
||||
def test_get_agent_app_model_rejects_unbound_agent(self):
|
||||
session = FakeSession()
|
||||
service = AgentRosterService(session)
|
||||
|
||||
with pytest.raises(roster_service.AgentNotFoundError):
|
||||
service.get_agent_app_model(tenant_id="tenant-1", agent_id="agent-x")
|
||||
|
||||
|
||||
class TestListWorkflowsReferencingAppAgent:
|
||||
def test_groups_bindings_by_workflow_app_and_sorts_by_name(self):
|
||||
|
||||
Reference in new issue
Block a user