移除了agents/registry.json中的所有内置agents配置,将agents数组清空。 为web应用添加了CORS中间件支持,允许指定的前端地址跨域访问。 重构了技能上传功能,增加了LLM重写机制,自动规范化上传的技能格式。 新增了工具名称提取逻辑,从技能正文中自动识别Required Tools段落。 更新了技能学习候选者和草稿的载荷结构,添加评估报告统计信息。 修改了意图路由技能的说明,改进任务状态管理逻辑。
255 lines
8.4 KiB
Python
255 lines
8.4 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib
|
|
import sys
|
|
from types import ModuleType
|
|
from typing import Any
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.minio_provisioning import (
|
|
MinIOProvisioningConfig,
|
|
deprovision_user_file_minio_resources,
|
|
provision_user_file_minio_settings,
|
|
)
|
|
from app.models import MinIOSettings
|
|
|
|
|
|
class _FakeObject:
|
|
def __init__(self, object_name: str) -> None:
|
|
self.object_name = object_name
|
|
|
|
|
|
class _FakeMinio:
|
|
bucket_exists_value = True
|
|
objects: list[str] = []
|
|
removed_objects: list[str] = []
|
|
made_buckets: list[str] = []
|
|
|
|
def __init__(self, **_kwargs: Any) -> None:
|
|
pass
|
|
|
|
def bucket_exists(self, bucket: str) -> bool:
|
|
return self.bucket_exists_value
|
|
|
|
def make_bucket(self, bucket: str, location: str | None = None) -> None:
|
|
self.made_buckets.append(bucket)
|
|
|
|
def list_objects(self, bucket: str, *, prefix: str, recursive: bool) -> list[_FakeObject]:
|
|
return [_FakeObject(name) for name in self.objects if name.startswith(prefix)]
|
|
|
|
def remove_objects(self, bucket: str, objects: list[Any]) -> list[Any]:
|
|
self.removed_objects.extend(item.object_name for item in objects)
|
|
return []
|
|
|
|
|
|
class _FakeAdmin:
|
|
calls: list[tuple[str, Any]] = []
|
|
missing = False
|
|
attach_policy_already_applied = False
|
|
|
|
def __init__(self, **_kwargs: Any) -> None:
|
|
pass
|
|
|
|
def user_add(self, access_key: str, secret_key: str) -> None:
|
|
self.calls.append(("user_add", access_key))
|
|
|
|
def policy_add(self, policy_name: str, *, policy: dict[str, Any]) -> None:
|
|
self.calls.append(("policy_add", policy_name))
|
|
|
|
def attach_policy(self, **kwargs: Any) -> None:
|
|
self.calls.append(("attach_policy", kwargs))
|
|
if self.attach_policy_already_applied:
|
|
raise RuntimeError(
|
|
"admin request failed; Status: 400, Body: "
|
|
'{"Code":"XMinioAdminPolicyChangeAlreadyApplied",'
|
|
'"Message":"The specified policy change is already in effect."}'
|
|
)
|
|
|
|
def detach_policy(self, **kwargs: Any) -> None:
|
|
self.calls.append(("detach_policy", kwargs))
|
|
if self.missing:
|
|
raise RuntimeError("policy not found")
|
|
|
|
def user_remove(self, access_key: str) -> None:
|
|
self.calls.append(("user_remove", access_key))
|
|
if self.missing:
|
|
raise RuntimeError("user not found")
|
|
|
|
def policy_remove(self, policy_name: str) -> None:
|
|
self.calls.append(("policy_remove", policy_name))
|
|
if self.missing:
|
|
raise RuntimeError("policy not found")
|
|
|
|
|
|
class _FakeStaticProvider:
|
|
def __init__(self, **_kwargs: Any) -> None:
|
|
pass
|
|
|
|
|
|
class _FakeDeleteObject:
|
|
def __init__(self, object_name: str) -> None:
|
|
self.object_name = object_name
|
|
|
|
|
|
def _install_fake_minio(monkeypatch) -> None:
|
|
minio_module = ModuleType("minio")
|
|
minio_module.Minio = _FakeMinio
|
|
minio_module.MinioAdmin = _FakeAdmin
|
|
|
|
credentials_module = ModuleType("minio.credentials")
|
|
credentials_module.StaticProvider = _FakeStaticProvider
|
|
|
|
deleteobjects_module = ModuleType("minio.deleteobjects")
|
|
deleteobjects_module.DeleteObject = _FakeDeleteObject
|
|
|
|
monkeypatch.setitem(sys.modules, "minio", minio_module)
|
|
monkeypatch.setitem(sys.modules, "minio.credentials", credentials_module)
|
|
monkeypatch.setitem(sys.modules, "minio.deleteobjects", deleteobjects_module)
|
|
_FakeMinio.bucket_exists_value = True
|
|
_FakeMinio.objects = []
|
|
_FakeMinio.removed_objects = []
|
|
_FakeMinio.made_buckets = []
|
|
_FakeAdmin.calls = []
|
|
_FakeAdmin.missing = False
|
|
_FakeAdmin.attach_policy_already_applied = False
|
|
|
|
|
|
def _config() -> MinIOProvisioningConfig:
|
|
return MinIOProvisioningConfig(
|
|
enabled=True,
|
|
endpoint="minio.local:9000",
|
|
public_endpoint="minio.local:9000",
|
|
admin_access_key="admin",
|
|
admin_secret_key="admin-secret",
|
|
bucket="beaver-user-files",
|
|
secure=False,
|
|
region=None,
|
|
)
|
|
|
|
|
|
def _settings() -> MinIOSettings:
|
|
return MinIOSettings(
|
|
endpoint="minio.local:9000",
|
|
access_key="beaver-alice",
|
|
secret_key="alice-secret",
|
|
bucket="beaver-user-files",
|
|
namespace="users/alice",
|
|
)
|
|
|
|
|
|
def _client(tmp_path, monkeypatch) -> TestClient:
|
|
monkeypatch.setenv("AUTHZ_DATA_DIR", str(tmp_path))
|
|
monkeypatch.setenv("AUTHZ_PRIVATE_KEY_PATH", str(tmp_path / "signing_key.pem"))
|
|
monkeypatch.setenv("AUTHZ_INTERNAL_TOKEN", "test-internal-token")
|
|
monkeypatch.setenv("USER_FILES_MINIO_ENDPOINT", "minio.local:9000")
|
|
monkeypatch.setenv("USER_FILES_MINIO_ADMIN_ACCESS_KEY", "admin")
|
|
monkeypatch.setenv("USER_FILES_MINIO_ADMIN_SECRET_KEY", "admin-secret")
|
|
monkeypatch.setenv("USER_FILES_MINIO_BUCKET", "beaver-user-files")
|
|
import app.main as main
|
|
|
|
main = importlib.reload(main)
|
|
return TestClient(main.app)
|
|
|
|
|
|
def _register_backend(client: TestClient) -> None:
|
|
response = client.post(
|
|
"/backends/register",
|
|
json={"backend_id": "alice", "name": "Alice", "base_url": "http://alice.local"},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
|
|
def test_deprovision_removes_namespace_resources_without_secrets(monkeypatch) -> None:
|
|
_install_fake_minio(monkeypatch)
|
|
_FakeMinio.objects = [
|
|
"users/alice/uploads/a.txt",
|
|
"users/alice/outputs/b.txt",
|
|
"users/bob/uploads/c.txt",
|
|
]
|
|
|
|
result = deprovision_user_file_minio_resources(
|
|
backend_id="alice",
|
|
existing=_settings(),
|
|
config=_config(),
|
|
)
|
|
|
|
assert result["ok"] is True
|
|
assert result["objects"] == {"status": "removed", "deleted": 2}
|
|
assert _FakeMinio.removed_objects == ["users/alice/uploads/a.txt", "users/alice/outputs/b.txt"]
|
|
assert ("user_remove", "beaver-alice") in _FakeAdmin.calls
|
|
assert ("policy_remove", "beaver-user-files-alice") in _FakeAdmin.calls
|
|
assert "secret" not in str(result).lower()
|
|
|
|
|
|
def test_provision_treats_already_attached_policy_as_idempotent(monkeypatch) -> None:
|
|
_install_fake_minio(monkeypatch)
|
|
_FakeAdmin.attach_policy_already_applied = True
|
|
|
|
settings = provision_user_file_minio_settings(
|
|
backend_id="alice",
|
|
existing=None,
|
|
config=_config(),
|
|
)
|
|
|
|
assert settings is not None
|
|
assert settings.endpoint == "minio.local:9000"
|
|
assert settings.access_key == "beaver-alice"
|
|
assert settings.bucket == "beaver-user-files"
|
|
assert settings.namespace == "users/alice"
|
|
assert settings.secret_key
|
|
assert ("attach_policy", {"policies": ["beaver-user-files-alice"], "user": "beaver-alice"}) in _FakeAdmin.calls
|
|
|
|
|
|
def test_deprovision_is_idempotent_when_resources_are_absent(monkeypatch) -> None:
|
|
_install_fake_minio(monkeypatch)
|
|
_FakeMinio.bucket_exists_value = False
|
|
_FakeAdmin.missing = True
|
|
|
|
result = deprovision_user_file_minio_resources(
|
|
backend_id="alice",
|
|
existing=_settings(),
|
|
config=_config(),
|
|
)
|
|
|
|
assert result["ok"] is True
|
|
assert result["bucket"] == "absent"
|
|
assert result["objects"] == {"status": "absent", "deleted": 0}
|
|
assert result["user"] == {"status": "absent"}
|
|
assert result["policy"] == {"status": "absent"}
|
|
|
|
|
|
def test_internal_user_file_deprovision_requires_internal_token(tmp_path, monkeypatch) -> None:
|
|
with _client(tmp_path, monkeypatch) as client:
|
|
unauthorized = client.delete("/internal/backends/alice/user-files")
|
|
|
|
assert unauthorized.status_code == 401
|
|
|
|
|
|
def test_internal_user_file_deprovision_deletes_settings_without_returning_secret(tmp_path, monkeypatch) -> None:
|
|
_install_fake_minio(monkeypatch)
|
|
with _client(tmp_path, monkeypatch) as client:
|
|
_register_backend(client)
|
|
client.post(
|
|
"/backends/alice/settings/minio",
|
|
json={
|
|
"endpoint": "minio.local:9000",
|
|
"access_key": "beaver-alice",
|
|
"secret_key": "alice-secret",
|
|
"bucket": "beaver-user-files",
|
|
"namespace": "users/alice",
|
|
},
|
|
)
|
|
deleted = client.delete(
|
|
"/internal/backends/alice/user-files",
|
|
headers={"Authorization": "Bearer test-internal-token"},
|
|
)
|
|
after_delete = client.get("/backends/alice/settings/minio")
|
|
|
|
assert deleted.status_code == 200
|
|
payload = deleted.json()
|
|
assert payload["ok"] is True
|
|
assert payload["settings"] == {"status": "removed"}
|
|
assert "secret" not in str(payload).lower()
|
|
assert after_delete.json() == {"configured": False}
|