feat: deliver agent foundation day 1
This commit is contained in:
186
server/tests/test_agent_asset_service.py
Normal file
186
server/tests/test_agent_asset_service.py
Normal file
@@ -0,0 +1,186 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.core.agent_enums import (
|
||||
AgentAssetContentType,
|
||||
AgentAssetDomain,
|
||||
AgentAssetStatus,
|
||||
AgentAssetType,
|
||||
AgentName,
|
||||
AgentReviewStatus,
|
||||
AgentRunSource,
|
||||
AgentRunStatus,
|
||||
)
|
||||
from app.db.base import Base
|
||||
from app.schemas.agent_asset import (
|
||||
AgentAssetCreate,
|
||||
AgentAssetReviewCreate,
|
||||
AgentAssetVersionCreate,
|
||||
)
|
||||
from app.services.agent_assets import AgentAssetService
|
||||
from app.services.agent_runs import AgentRunService
|
||||
from app.services.audit import AuditLogService
|
||||
|
||||
|
||||
def build_session() -> Session:
|
||||
engine = create_engine(
|
||||
"sqlite+pysqlite:///:memory:",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
||||
return session_factory()
|
||||
|
||||
|
||||
def test_agent_asset_service_seeds_assets_and_enforces_review_before_activation() -> None:
|
||||
with build_session() as db:
|
||||
service = AgentAssetService(db)
|
||||
|
||||
rules = service.list_assets(asset_type=AgentAssetType.RULE.value)
|
||||
assert len(rules) >= 3
|
||||
|
||||
pending_rule = next(item for item in rules if item.status == AgentAssetStatus.REVIEW.value)
|
||||
|
||||
with pytest.raises(PermissionError):
|
||||
service.activate_asset(pending_rule.id, actor="pytest")
|
||||
|
||||
|
||||
def test_agent_asset_service_seeds_all_foundation_asset_types() -> None:
|
||||
with build_session() as db:
|
||||
service = AgentAssetService(db)
|
||||
|
||||
assert len(service.list_assets(asset_type=AgentAssetType.RULE.value)) >= 3
|
||||
assert len(service.list_assets(asset_type=AgentAssetType.SKILL.value)) >= 2
|
||||
assert len(service.list_assets(asset_type=AgentAssetType.MCP.value)) >= 2
|
||||
assert len(service.list_assets(asset_type=AgentAssetType.TASK.value)) >= 3
|
||||
|
||||
|
||||
def test_agent_asset_service_can_activate_rule_after_review() -> None:
|
||||
with build_session() as db:
|
||||
service = AgentAssetService(db)
|
||||
|
||||
created = service.create_asset(
|
||||
AgentAssetCreate(
|
||||
asset_type=AgentAssetType.RULE,
|
||||
code=f"rule.test.{uuid.uuid4().hex[:8]}",
|
||||
name="测试规则",
|
||||
description="用于测试审核和上线流程。",
|
||||
domain=AgentAssetDomain.EXPENSE,
|
||||
scenario_json=["expense", "risk_check"],
|
||||
owner="pytest",
|
||||
reviewer="reviewer",
|
||||
status=AgentAssetStatus.DRAFT,
|
||||
config_json={"enabled": False},
|
||||
),
|
||||
actor="pytest",
|
||||
)
|
||||
service.create_version(
|
||||
created.id,
|
||||
AgentAssetVersionCreate(
|
||||
version="v1.0.0",
|
||||
content="# 测试规则\n\n- 仅用于测试。",
|
||||
content_type=AgentAssetContentType.MARKDOWN,
|
||||
change_note="初始化版本",
|
||||
created_by="pytest",
|
||||
),
|
||||
actor="pytest",
|
||||
)
|
||||
service.create_review(
|
||||
created.id,
|
||||
AgentAssetReviewCreate(
|
||||
version="v1.0.0",
|
||||
reviewer="reviewer",
|
||||
review_status=AgentReviewStatus.APPROVED,
|
||||
review_note="可以上线",
|
||||
),
|
||||
actor="reviewer",
|
||||
)
|
||||
|
||||
activated = service.activate_asset(created.id, actor="reviewer")
|
||||
|
||||
assert activated.status == AgentAssetStatus.ACTIVE.value
|
||||
assert activated.current_version == "v1.0.0"
|
||||
assert activated.latest_review is not None
|
||||
assert activated.latest_review.review_status == AgentReviewStatus.APPROVED.value
|
||||
|
||||
|
||||
def test_agent_asset_service_returns_recent_versions_for_rule_detail() -> None:
|
||||
with build_session() as db:
|
||||
service = AgentAssetService(db)
|
||||
|
||||
rule = next(
|
||||
item
|
||||
for item in service.list_assets(asset_type=AgentAssetType.RULE.value)
|
||||
if item.code == "rule.expense.duplicate_expense_check"
|
||||
)
|
||||
detail = service.get_asset(rule.id)
|
||||
|
||||
assert detail is not None
|
||||
assert detail.current_version == "v1.1.0"
|
||||
assert detail.current_version_content_type == AgentAssetContentType.MARKDOWN.value
|
||||
assert isinstance(detail.current_version_content, str)
|
||||
assert len(detail.recent_versions) >= 2
|
||||
assert any(item.is_current for item in detail.recent_versions)
|
||||
assert {item.version for item in detail.recent_versions} >= {"v1.0.0", "v1.1.0"}
|
||||
|
||||
|
||||
def test_agent_run_service_lists_seeded_trace_data() -> None:
|
||||
with build_session() as db:
|
||||
service = AgentRunService(db)
|
||||
|
||||
runs = service.list_runs()
|
||||
|
||||
assert len(runs) >= 3
|
||||
assert any(item.tool_calls for item in runs)
|
||||
assert any(item.semantic_parse is not None for item in runs)
|
||||
|
||||
|
||||
def test_agent_run_service_creates_run_and_persists_error_message() -> None:
|
||||
with build_session() as db:
|
||||
service = AgentRunService(db)
|
||||
|
||||
created = service.create_run(
|
||||
agent=AgentName.ORCHESTRATOR.value,
|
||||
source=AgentRunSource.SYSTEM_EVENT.value,
|
||||
status=AgentRunStatus.FAILED.value,
|
||||
error_message="simulated failure",
|
||||
result_summary="failed to route request",
|
||||
)
|
||||
fetched = service.get_run(created.run_id)
|
||||
|
||||
assert fetched is not None
|
||||
assert fetched.run_id.startswith("run_")
|
||||
assert fetched.status == AgentRunStatus.FAILED.value
|
||||
assert fetched.error_message == "simulated failure"
|
||||
assert fetched.result_summary == "failed to route request"
|
||||
|
||||
|
||||
def test_agent_asset_creation_writes_audit_log() -> None:
|
||||
with build_session() as db:
|
||||
service = AgentAssetService(db)
|
||||
|
||||
created = service.create_asset(
|
||||
AgentAssetCreate(
|
||||
asset_type=AgentAssetType.SKILL,
|
||||
code=f"skill.test.{uuid.uuid4().hex[:8]}",
|
||||
name="测试技能",
|
||||
description="用于测试审计日志写入。",
|
||||
domain=AgentAssetDomain.KNOWLEDGE,
|
||||
scenario_json=["knowledge", "query"],
|
||||
owner="pytest",
|
||||
reviewer="reviewer",
|
||||
status=AgentAssetStatus.DRAFT,
|
||||
config_json={"enabled": True},
|
||||
),
|
||||
actor="pytest",
|
||||
)
|
||||
logs = AuditLogService(db).list_logs(resource_id=created.id)
|
||||
|
||||
assert any(item.action == "create_agent_asset" for item in logs)
|
||||
92
server/tests/test_agent_foundation_endpoints.py
Normal file
92
server/tests/test_agent_foundation_endpoints.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.api.deps import get_db
|
||||
from app.core.agent_enums import AgentAssetStatus
|
||||
from app.db.base import Base
|
||||
from app.main import create_app
|
||||
from app.services.agent_assets import AgentAssetService
|
||||
|
||||
|
||||
def build_client() -> tuple[TestClient, sessionmaker[Session]]:
|
||||
engine = create_engine(
|
||||
"sqlite+pysqlite:///:memory:",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
||||
|
||||
app = create_app()
|
||||
|
||||
def override_db() -> Generator[Session, None, None]:
|
||||
db = session_factory()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
app.dependency_overrides[get_db] = override_db
|
||||
return TestClient(app), session_factory
|
||||
|
||||
|
||||
def test_list_agent_assets_endpoint_returns_seeded_items() -> None:
|
||||
client, _ = build_client()
|
||||
|
||||
response = client.get("/api/v1/agent-assets", params={"asset_type": "rule"})
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload
|
||||
assert all(item["asset_type"] == "rule" for item in payload)
|
||||
|
||||
|
||||
def test_get_agent_asset_detail_endpoint_returns_version_history() -> None:
|
||||
client, _ = build_client()
|
||||
|
||||
list_response = client.get("/api/v1/agent-assets", params={"asset_type": "rule"})
|
||||
asset_id = list_response.json()[0]["id"]
|
||||
|
||||
response = client.get(f"/api/v1/agent-assets/{asset_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["recent_versions"]
|
||||
assert payload["current_version_content_type"] == "markdown"
|
||||
assert len(payload["recent_versions"]) >= 2
|
||||
|
||||
|
||||
def test_activate_pending_rule_endpoint_is_blocked() -> None:
|
||||
client, session_factory = build_client()
|
||||
|
||||
with session_factory() as db:
|
||||
pending_rule = next(
|
||||
item
|
||||
for item in AgentAssetService(db).list_assets(asset_type="rule")
|
||||
if item.status == AgentAssetStatus.REVIEW.value
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1/agent-assets/{pending_rule.id}/activate",
|
||||
headers={"x-actor": "pytest"},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "审核" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_list_audit_logs_endpoint_returns_seeded_logs() -> None:
|
||||
client, _ = build_client()
|
||||
|
||||
response = client.get("/api/v1/audit-logs")
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload
|
||||
assert any(item["action"] == "review_rule" for item in payload)
|
||||
Reference in New Issue
Block a user