mirror of
https://github.com/langgenius/dify.git
synced 2026-11-13 23:06:40 +08:00
567 lines
22 KiB
Python
567 lines
22 KiB
Python
import logging
|
|
from collections.abc import Sequence
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from sqlalchemy import select, text
|
|
from sqlalchemy.engine import Engine
|
|
from sqlalchemy.orm import Session
|
|
|
|
from models.credential_permission import CredentialPermission, CredentialType
|
|
from models.enums import PermissionEnum
|
|
from services.entities.model_provider_entities import ProviderWithModelsResponse
|
|
from tests.unit_tests.services.dify_builder.installed_tool_inventory import (
|
|
ACCOUNT_A,
|
|
ACCOUNT_B,
|
|
ACCOUNT_C,
|
|
METADATA_ROUTES,
|
|
OTHER_TENANT_ID,
|
|
SECRET_MARKER,
|
|
TENANT_ID,
|
|
FakeTool,
|
|
FakeToolDaemon,
|
|
add_credential,
|
|
bind_database,
|
|
forbid_secret_and_business_calls,
|
|
install_fake_daemon,
|
|
provider_payload,
|
|
record_sql,
|
|
)
|
|
|
|
|
|
def _provider_models(provider: str, models: list[str]) -> ProviderWithModelsResponse:
|
|
from core.entities.model_entities import ModelStatus, ProviderModelWithStatusEntity
|
|
from graphon.model_runtime.entities.common_entities import I18nObject
|
|
from graphon.model_runtime.entities.model_entities import FetchFrom, ModelType
|
|
from services.entities.model_provider_entities import CustomConfigurationStatus, ProviderWithModelsResponse
|
|
|
|
return ProviderWithModelsResponse(
|
|
tenant_id="t1",
|
|
provider=provider,
|
|
label=I18nObject(en_US=provider),
|
|
status=CustomConfigurationStatus.ACTIVE,
|
|
models=[
|
|
ProviderModelWithStatusEntity(
|
|
model=name,
|
|
label=I18nObject(en_US=name),
|
|
model_type=ModelType.LLM,
|
|
fetch_from=FetchFrom.PREDEFINED_MODEL,
|
|
model_properties={},
|
|
status=ModelStatus.ACTIVE,
|
|
)
|
|
for name in models
|
|
],
|
|
)
|
|
|
|
|
|
def test_list_tenant_resources_normalizes_all_three_kinds() -> None:
|
|
from services.dify_builder.agent import resources
|
|
|
|
class _DS:
|
|
def __init__(self, i: str, n: str) -> None:
|
|
self.id, self.name = i, n
|
|
|
|
tool_record = {
|
|
"provider_name": "google",
|
|
"tool_name": "search",
|
|
"tool_label": "Google Search",
|
|
"description": "",
|
|
}
|
|
with (
|
|
patch.object(resources, "_list_models", return_value=[_provider_models("anthropic", ["claude-opus-4-8"])]),
|
|
patch.object(resources, "_list_datasets", return_value=[_DS("kb-1", "Company KB")]),
|
|
patch.object(resources, "_list_tools", return_value=[tool_record]),
|
|
):
|
|
out = resources.list_tenant_resources("t1")
|
|
|
|
assert out.models[0].id == "anthropic/claude-opus-4-8"
|
|
assert out.datasets[0].id == "kb-1"
|
|
assert out.datasets[0].label == "Company KB"
|
|
assert out.tools[0].id == "google/search"
|
|
assert out.tools[0].label == "Google Search"
|
|
|
|
|
|
def test_each_source_degrades_to_empty() -> None:
|
|
from services.dify_builder.agent import resources
|
|
|
|
with (
|
|
patch.object(resources, "_list_models", side_effect=RuntimeError("boom")),
|
|
patch.object(resources, "_list_datasets", side_effect=RuntimeError("boom")),
|
|
patch.object(resources, "_list_tools", side_effect=RuntimeError("boom")),
|
|
):
|
|
out = resources.list_tenant_resources("t1")
|
|
assert out.models == out.datasets == out.tools == []
|
|
assert out.model_source_error == out.tool_source_error == "inventory_unavailable"
|
|
|
|
|
|
def test_mixed_failure_partial_success() -> None:
|
|
from services.dify_builder.agent import resources
|
|
|
|
class _DS:
|
|
def __init__(self, i: str, n: str) -> None:
|
|
self.id, self.name = i, n
|
|
|
|
tool_record = {
|
|
"provider_name": "google",
|
|
"tool_name": "search",
|
|
"tool_label": "Google Search",
|
|
"description": "",
|
|
}
|
|
with (
|
|
patch.object(resources, "_list_models", side_effect=RuntimeError("boom")),
|
|
patch.object(resources, "_list_datasets", return_value=[_DS("kb-1", "Company KB")]),
|
|
patch.object(resources, "_list_tools", return_value=[tool_record]),
|
|
):
|
|
out = resources.list_tenant_resources("t1")
|
|
|
|
assert out.models == []
|
|
assert len(out.datasets) == 1
|
|
assert out.datasets[0].id == "kb-1"
|
|
assert len(out.tools) == 1
|
|
assert out.tools[0].id == "google/search"
|
|
|
|
|
|
def test_malformed_tool_record_degrades() -> None:
|
|
from services.dify_builder.agent import resources
|
|
|
|
with (
|
|
patch.object(resources, "_list_models", return_value=[]),
|
|
patch.object(resources, "_list_datasets", return_value=[]),
|
|
patch.object(resources, "_list_tools", return_value=[{"provider_name": "g"}]),
|
|
): # missing tool_name
|
|
out = resources.list_tenant_resources("t1")
|
|
|
|
assert out.tools == []
|
|
assert out.models == []
|
|
assert out.datasets == []
|
|
|
|
|
|
def test_an_unauthorized_tool_is_reported_as_missing_config(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Recommending an unusable tool is how a build fails at test time."""
|
|
from services.dify_builder.agent import resources
|
|
|
|
monkeypatch.setattr(
|
|
resources,
|
|
"_list_tools",
|
|
lambda _t: [
|
|
{
|
|
"provider_name": "gmail",
|
|
"tool_name": "send",
|
|
"tool_label": "Send",
|
|
"description": "",
|
|
"needs_credentials": True,
|
|
}
|
|
],
|
|
)
|
|
monkeypatch.setattr(resources, "_authorized_providers", lambda *_args, **_kwargs: set())
|
|
inv = resources.list_tenant_resources("t1")
|
|
assert inv.tools[0].readiness == "missing_config"
|
|
|
|
|
|
def test_a_tool_needing_no_credentials_is_ready(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
from services.dify_builder.agent import resources
|
|
|
|
monkeypatch.setattr(
|
|
resources,
|
|
"_list_tools",
|
|
lambda _t: [
|
|
{
|
|
"provider_name": "time",
|
|
"tool_name": "now",
|
|
"tool_label": "Now",
|
|
"description": "",
|
|
"needs_credentials": False,
|
|
}
|
|
],
|
|
)
|
|
monkeypatch.setattr(resources, "_authorized_providers", lambda *_args, **_kwargs: set())
|
|
assert resources.list_tenant_resources("t1").tools[0].readiness == "ready"
|
|
|
|
|
|
def test_an_authorized_tool_is_ready(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
from services.dify_builder.agent import resources
|
|
|
|
monkeypatch.setattr(
|
|
resources,
|
|
"_list_tools",
|
|
lambda _t: [
|
|
{
|
|
"provider_name": "gmail",
|
|
"tool_name": "send",
|
|
"tool_label": "Send",
|
|
"description": "",
|
|
"needs_credentials": True,
|
|
}
|
|
],
|
|
)
|
|
monkeypatch.setattr(resources, "_authorized_providers", lambda *_args, **_kwargs: {"gmail"})
|
|
assert resources.list_tenant_resources("t1").tools[0].readiness == "ready"
|
|
|
|
|
|
def test_authorization_lookup_failure_is_never_reported_ready(caplog: pytest.LogCaptureFixture) -> None:
|
|
"""ENG-1191 ruling: an unverified credential lookup must not claim a tool
|
|
is usable. The tool stays discoverable as missing_config with an
|
|
explanatory note; runtime authorization remains the final gate."""
|
|
from services.dify_builder.agent import resources
|
|
|
|
def _boom(*_args: object, **_kwargs: object) -> None:
|
|
raise RuntimeError("db down")
|
|
|
|
with (
|
|
patch.object(resources, "_list_models", return_value=[]),
|
|
patch.object(resources, "_list_datasets", return_value=[]),
|
|
patch.object(
|
|
resources,
|
|
"_list_tools",
|
|
return_value=[
|
|
{
|
|
"provider_name": "gmail",
|
|
"tool_name": "send",
|
|
"tool_label": "Send",
|
|
"description": "Send an email.",
|
|
"needs_credentials": True,
|
|
},
|
|
{
|
|
"provider_name": "time",
|
|
"tool_name": "now",
|
|
"tool_label": "Now",
|
|
"description": "",
|
|
"needs_credentials": False,
|
|
},
|
|
],
|
|
),
|
|
patch.object(resources, "_authorized_providers", _boom),
|
|
caplog.at_level(logging.WARNING, logger="services.dify_builder.agent.resources"),
|
|
):
|
|
out = resources.list_tenant_resources("t1")
|
|
|
|
by_id = {tool.id: tool for tool in out.tools}
|
|
assert by_id["gmail/send"].readiness == "missing_config"
|
|
assert "could not be verified" in by_id["gmail/send"].meta
|
|
assert "Send an email." in by_id["gmail/send"].meta
|
|
# a credential-free tool needs no lookup to be usable
|
|
assert by_id["time/now"].readiness == "ready"
|
|
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
|
|
assert len(warnings) == 1
|
|
assert "authorization" in warnings[0].getMessage()
|
|
|
|
|
|
def test_list_tenant_resources_logs_when_a_source_fails(caplog: pytest.LogCaptureFixture) -> None:
|
|
from services.dify_builder.agent import resources
|
|
|
|
with (
|
|
patch.object(resources, "_list_models", return_value=[_provider_models("anthropic", ["claude-opus-4-8"])]),
|
|
patch.object(resources, "_list_datasets", side_effect=RuntimeError("db down")),
|
|
patch.object(resources, "_list_tools", return_value=[]),
|
|
caplog.at_level(logging.WARNING, logger="services.dify_builder.agent.resources"),
|
|
):
|
|
out = resources.list_tenant_resources("t1")
|
|
|
|
assert out.datasets == []
|
|
assert out.models[0].id == "anthropic/claude-opus-4-8"
|
|
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
|
|
assert len(warnings) == 1
|
|
assert "datasets" in warnings[0].getMessage()
|
|
assert warnings[0].exc_info is not None
|
|
|
|
|
|
# ---- ENG-1191: real installed inventory, real credential rows ----------------
|
|
#
|
|
# Daemon I/O is the only fake: providers flow through PluginToolManager,
|
|
# ToolManager and build_tool_catalogue; credential readiness is computed from
|
|
# real tool_builtin_providers / credential_permissions rows in SQLite.
|
|
|
|
|
|
_HUDDLE = "acme/huddle/huddle"
|
|
|
|
|
|
def _tenant_providers() -> list[dict[str, object]]:
|
|
def creds(
|
|
plugin_id: str,
|
|
provider: str,
|
|
tool: FakeTool,
|
|
*,
|
|
label: str = "",
|
|
description: str = "",
|
|
tags: Sequence[str] = (),
|
|
) -> dict[str, object]:
|
|
return provider_payload(
|
|
plugin_id,
|
|
provider,
|
|
[tool],
|
|
needs_credentials=True,
|
|
label=label,
|
|
description=description,
|
|
tags=tags,
|
|
)
|
|
|
|
return [
|
|
creds(
|
|
"acme/huddle",
|
|
"huddle",
|
|
FakeTool(name="post", label="Post", description="Send text to a channel."),
|
|
label="Huddle",
|
|
description="Notify teammates in shared chat channels.",
|
|
tags=["social"],
|
|
),
|
|
creds("acme/vault", "vault", FakeTool(name="lookup", label="Lookup", description="Read a record.")),
|
|
creds("acme/onlyme", "onlyme", FakeTool(name="run", label="Run", description="Private tool.")),
|
|
creds("acme/partial", "partial", FakeTool(name="run", label="Run", description="Shared with some.")),
|
|
creds("langgenius/google", "google", FakeTool(name="search", label="Search", description="Web search.")),
|
|
creds("acme/google", "google", FakeTool(name="search", label="Search", description="Acme search.")),
|
|
creds("langgenius/jina_tool", "jina", FakeTool(name="read", label="Read", description="Read a URL.")),
|
|
provider_payload("acme/free", "free", [FakeTool(name="echo", label="Echo", description="Echo input.")]),
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def installed(monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine) -> tuple[FakeToolDaemon, list[str]]:
|
|
"""Real inventory owners over a faked daemon and real SQLite credential rows."""
|
|
bind_database(monkeypatch, sqlite_engine)
|
|
daemon = install_fake_daemon(
|
|
monkeypatch,
|
|
FakeToolDaemon(
|
|
providers_by_tenant={
|
|
TENANT_ID: _tenant_providers(),
|
|
OTHER_TENANT_ID: [
|
|
provider_payload(
|
|
"other/elsewhere", "elsewhere", [FakeTool(name="go", label="Go", description="Other tenant.")]
|
|
)
|
|
],
|
|
}
|
|
),
|
|
)
|
|
from services.dify_builder.agent import resources
|
|
|
|
# models/datasets come from their own owners; this suite is about tools
|
|
monkeypatch.setattr(resources, "_list_models", lambda _tenant_id: [])
|
|
monkeypatch.setattr(resources, "_list_datasets", lambda _tenant_id: [])
|
|
forbidden = forbid_secret_and_business_calls(monkeypatch)
|
|
return daemon, forbidden
|
|
|
|
|
|
def _readiness(tenant_id: str = TENANT_ID, *, account_id: str | None = None) -> dict[str, str]:
|
|
from services.dify_builder.agent import resources
|
|
|
|
return {tool.id: tool.readiness for tool in resources.list_tenant_resources(tenant_id, account_id=account_id).tools}
|
|
|
|
|
|
def test_installed_unauthenticated_tool_is_discoverable_as_missing_config(
|
|
installed: tuple[FakeToolDaemon, list[str]], sqlite_engine: Engine
|
|
) -> None:
|
|
from services.dify_builder.agent import resources
|
|
|
|
daemon, forbidden = installed
|
|
statements = record_sql(sqlite_engine)
|
|
|
|
tools = {tool.id: tool for tool in resources.list_tenant_resources(TENANT_ID).tools}
|
|
|
|
huddle = tools[f"{_HUDDLE}/post"]
|
|
assert huddle.readiness == "missing_config"
|
|
assert "Send text to a channel." in huddle.meta
|
|
assert "requires setup" in huddle.meta
|
|
# discovery reads metadata only: no secret, OAuth, validate, invoke or
|
|
# install owner, no daemon call besides the tool listing, and no SQL
|
|
# statement that selects the encrypted credential column
|
|
assert forbidden == []
|
|
assert daemon.requests
|
|
# only public tool-metadata routes, for this tenant
|
|
assert all(r.route in METADATA_ROUTES and r.tenant_id == TENANT_ID for r in daemon.requests)
|
|
assert not [s for s in statements if "encrypted_credentials" in s]
|
|
assert not [s for s in statements if SECRET_MARKER in s]
|
|
|
|
|
|
@pytest.mark.usefixtures("installed")
|
|
def test_configured_no_auth_and_hardcoded_tools_are_ready(sqlite_session: Session) -> None:
|
|
add_credential(sqlite_session, tenant_id=TENANT_ID, provider="acme/vault/vault", user_id=ACCOUNT_B)
|
|
|
|
readiness = _readiness()
|
|
|
|
assert readiness["acme/vault/vault/lookup"] == "ready"
|
|
assert readiness["acme/free/free/echo"] == "ready"
|
|
assert readiness["time/current_time"] == "ready"
|
|
assert readiness[f"{_HUDDLE}/post"] == "missing_config"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("account_id", "provider", "expected"),
|
|
[
|
|
(ACCOUNT_A, "acme/onlyme/onlyme", "missing_config"), # teammate's private credential
|
|
(ACCOUNT_B, "acme/onlyme/onlyme", "ready"), # the creator's own
|
|
(ACCOUNT_A, "acme/partial/partial", "ready"), # listed partial member
|
|
(ACCOUNT_C, "acme/partial/partial", "missing_config"), # not a member
|
|
(ACCOUNT_B, "acme/partial/partial", "ready"), # creator
|
|
(ACCOUNT_C, "acme/vault/vault", "ready"), # all-team
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("installed")
|
|
def test_credential_visibility_follows_the_acting_account(
|
|
sqlite_session: Session, account_id: str, provider: str, expected: str
|
|
) -> None:
|
|
add_credential(
|
|
sqlite_session,
|
|
tenant_id=TENANT_ID,
|
|
provider="acme/onlyme/onlyme",
|
|
user_id=ACCOUNT_B,
|
|
visibility=PermissionEnum.ONLY_ME,
|
|
)
|
|
add_credential(
|
|
sqlite_session,
|
|
tenant_id=TENANT_ID,
|
|
provider="acme/partial/partial",
|
|
user_id=ACCOUNT_B,
|
|
visibility=PermissionEnum.PARTIAL_TEAM,
|
|
partial_members=[ACCOUNT_A],
|
|
)
|
|
add_credential(sqlite_session, tenant_id=TENANT_ID, provider="acme/vault/vault", user_id=ACCOUNT_B)
|
|
|
|
readiness = _readiness(account_id=account_id)
|
|
|
|
tool_name = "lookup" if provider == "acme/vault/vault" else "run"
|
|
assert readiness[f"{provider}/{tool_name}"] == expected
|
|
|
|
|
|
@pytest.mark.usefixtures("installed")
|
|
def test_without_an_actor_only_team_shared_credentials_count(sqlite_session: Session) -> None:
|
|
"""A compatibility caller with no actor must not borrow a teammate's
|
|
private or partial credential to claim readiness."""
|
|
add_credential(
|
|
sqlite_session,
|
|
tenant_id=TENANT_ID,
|
|
provider="acme/onlyme/onlyme",
|
|
user_id=ACCOUNT_B,
|
|
visibility=PermissionEnum.ONLY_ME,
|
|
)
|
|
add_credential(
|
|
sqlite_session,
|
|
tenant_id=TENANT_ID,
|
|
provider="acme/partial/partial",
|
|
user_id=ACCOUNT_B,
|
|
visibility=PermissionEnum.PARTIAL_TEAM,
|
|
partial_members=[ACCOUNT_A],
|
|
)
|
|
add_credential(sqlite_session, tenant_id=TENANT_ID, provider="acme/vault/vault", user_id=ACCOUNT_B)
|
|
|
|
readiness = _readiness()
|
|
|
|
assert readiness["acme/onlyme/onlyme/run"] == "missing_config"
|
|
assert readiness["acme/partial/partial/run"] == "missing_config"
|
|
assert readiness["acme/vault/vault/lookup"] == "ready"
|
|
|
|
|
|
@pytest.mark.parametrize("invalid_grant", ["disabled", "foreign-tenant", "wrong-type"])
|
|
@pytest.mark.usefixtures("installed")
|
|
def test_invalid_partial_grants_do_not_make_a_tool_ready(sqlite_session: Session, invalid_grant: str) -> None:
|
|
credential = add_credential(
|
|
sqlite_session,
|
|
tenant_id=TENANT_ID,
|
|
provider="acme/partial/partial",
|
|
user_id=ACCOUNT_B,
|
|
visibility=PermissionEnum.PARTIAL_TEAM,
|
|
partial_members=[ACCOUNT_A],
|
|
)
|
|
grant = sqlite_session.scalar(
|
|
select(CredentialPermission).where(CredentialPermission.credential_id == credential.id)
|
|
)
|
|
assert grant is not None
|
|
if invalid_grant == "disabled":
|
|
grant.has_permission = False
|
|
elif invalid_grant == "foreign-tenant":
|
|
grant.tenant_id = OTHER_TENANT_ID
|
|
else:
|
|
grant.credential_type = CredentialType.PROVIDER_CREDENTIAL
|
|
sqlite_session.commit()
|
|
|
|
assert _readiness(account_id=ACCOUNT_A)["acme/partial/partial/run"] == "missing_config"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("row_provider", "ready_ids", "missing_ids"),
|
|
[
|
|
# legacy short id normalizes to the langgenius plugin only
|
|
("google", {"langgenius/google/google/search"}, {"acme/google/google/search"}),
|
|
# a same-short-name plugin from another org never borrows it
|
|
("acme/google/google", {"acme/google/google/search"}, {"langgenius/google/google/search"}),
|
|
("langgenius/google/google", {"langgenius/google/google/search"}, {"acme/google/google/search"}),
|
|
# native ToolProviderID remap: jina -> langgenius/jina_tool/jina
|
|
("jina", {"langgenius/jina_tool/jina/read"}, set()),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("installed")
|
|
def test_legacy_and_colliding_provider_ids_follow_native_normalization(
|
|
sqlite_session: Session, row_provider: str, ready_ids: set[str], missing_ids: set[str]
|
|
) -> None:
|
|
add_credential(sqlite_session, tenant_id=TENANT_ID, provider=row_provider, user_id=ACCOUNT_B)
|
|
|
|
readiness = _readiness()
|
|
|
|
assert {tool_id: readiness[tool_id] for tool_id in ready_ids} == dict.fromkeys(ready_ids, "ready")
|
|
assert {tool_id: readiness[tool_id] for tool_id in missing_ids} == dict.fromkeys(missing_ids, "missing_config")
|
|
|
|
|
|
@pytest.mark.usefixtures("installed")
|
|
def test_other_tenants_rows_and_installs_never_leak(sqlite_session: Session) -> None:
|
|
add_credential(sqlite_session, tenant_id=OTHER_TENANT_ID, provider=_HUDDLE, user_id=ACCOUNT_A)
|
|
|
|
readiness = _readiness(account_id=ACCOUNT_A)
|
|
|
|
assert readiness[f"{_HUDDLE}/post"] == "missing_config"
|
|
# uninstalled here: a plugin installed only in another workspace is absent
|
|
assert not any(tool_id.startswith("other/elsewhere/") for tool_id in readiness)
|
|
|
|
|
|
@pytest.mark.usefixtures("installed")
|
|
def test_an_unverifiable_credential_lookup_reports_not_ready_with_a_reason(
|
|
sqlite_engine: Engine, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
from services.dify_builder.agent import resources
|
|
|
|
with sqlite_engine.begin() as connection:
|
|
connection.execute(text("DROP TABLE tool_builtin_providers"))
|
|
|
|
with caplog.at_level(logging.WARNING, logger="services.dify_builder.agent.resources"):
|
|
tools = {tool.id: tool for tool in resources.list_tenant_resources(TENANT_ID, account_id=ACCOUNT_A).tools}
|
|
|
|
assert tools[f"{_HUDDLE}/post"].readiness == "missing_config"
|
|
assert "could not be verified" in tools[f"{_HUDDLE}/post"].meta
|
|
assert tools["acme/free/free/echo"].readiness == "ready"
|
|
assert any("authorization" in r.getMessage() for r in caplog.records if r.levelno == logging.WARNING)
|
|
|
|
|
|
def test_a_plugin_daemon_outage_keeps_hardcoded_tools_in_the_inventory(
|
|
monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine
|
|
) -> None:
|
|
from services.dify_builder.agent import resources
|
|
|
|
bind_database(monkeypatch, sqlite_engine)
|
|
install_fake_daemon(monkeypatch, FakeToolDaemon(fail_all=True))
|
|
monkeypatch.setattr(resources, "_list_models", lambda _tenant_id: [])
|
|
monkeypatch.setattr(resources, "_list_datasets", lambda _tenant_id: [])
|
|
|
|
readiness = _readiness(account_id=ACCOUNT_A)
|
|
|
|
assert readiness.get("time/current_time") == "ready"
|
|
|
|
|
|
def test_models_and_datasets_keep_their_semantics_for_an_actor(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
from services.dify_builder.agent import resources
|
|
|
|
class _DS:
|
|
def __init__(self, i: str, n: str) -> None:
|
|
self.id, self.name = i, n
|
|
|
|
monkeypatch.setattr(
|
|
resources, "_list_models", lambda _tenant_id: [_provider_models("anthropic", ["claude-opus-4-8"])]
|
|
)
|
|
monkeypatch.setattr(resources, "_list_datasets", lambda _tenant_id: [_DS("kb-1", "Company KB")])
|
|
monkeypatch.setattr(resources, "_list_tools", lambda _tenant_id: [])
|
|
|
|
anonymous = resources.list_tenant_resources("t1")
|
|
acting = resources.list_tenant_resources("t1", account_id=ACCOUNT_A)
|
|
|
|
assert acting.models == anonymous.models
|
|
assert [ref.id for ref in acting.models] == ["anthropic/claude-opus-4-8"]
|
|
assert acting.models[0].binding == {"provider": "anthropic", "name": "claude-opus-4-8"}
|
|
assert acting.models[0].availability["runtime"] == "unverified"
|
|
assert acting.datasets == anonymous.datasets == [resources.ResourceRef(id="kb-1", label="Company KB")]
|