2026-09-12 14:27:37 +08:00
|
|
|
from uuid import uuid4
|
|
|
|
|
|
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
from sqlalchemy import inspect
|
|
|
|
|
|
2026-09-12 16:33:07 +08:00
|
|
|
from app.advisor_db import AgentSessionLocal, agent_engine
|
2026-09-12 14:27:37 +08:00
|
|
|
from app.main import app
|
|
|
|
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
|
|
|
|
|
def token_for(username: str, password: str) -> str:
|
|
|
|
|
response = client.post(
|
|
|
|
|
"/api/v1/auth/login",
|
|
|
|
|
json={"username": username, "password": password},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
return response.json()["data"]["access_token"]
|
|
|
|
|
|
|
|
|
|
def advisor_token() -> str:
|
|
|
|
|
return token_for("advisor_test", "advisor_test")
|
|
|
|
|
|
|
|
|
|
def compliance_token() -> str:
|
|
|
|
|
return token_for("compliance_test", "compliance_test")
|
|
|
|
|
|
|
|
|
|
def create_template_payload(title: str | None = None) -> dict:
|
|
|
|
|
suffix = uuid4().hex[:8]
|
|
|
|
|
return {
|
|
|
|
|
"scene": "loss_comfort",
|
|
|
|
|
"customer_type": "C3",
|
|
|
|
|
"title": title or f"亏损安抚模板 {suffix}",
|
|
|
|
|
"content": f"您好,近期市场波动较大,请结合风险承受能力理性看待。{suffix}",
|
|
|
|
|
"tags": ["亏损", "安抚", "市场波动"],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def create_template(headers: dict, payload: dict | None = None) -> dict:
|
|
|
|
|
response = client.post(
|
2026-09-12 16:33:07 +08:00
|
|
|
"/api/advisor-agent/script-templates",
|
2026-09-12 14:27:37 +08:00
|
|
|
json=payload or create_template_payload(),
|
|
|
|
|
headers=headers,
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
return response.json()["data"]
|
|
|
|
|
|
|
|
|
|
def latest_use_log(use_id: str):
|
2026-09-12 16:33:07 +08:00
|
|
|
from app.model.entities_advisor import TemplateUseLog
|
2026-09-12 14:27:37 +08:00
|
|
|
|
|
|
|
|
with AgentSessionLocal() as session:
|
|
|
|
|
row = session.query(TemplateUseLog).filter(TemplateUseLog.use_id == use_id).one()
|
|
|
|
|
session.expunge(row)
|
|
|
|
|
return row
|
|
|
|
|
|
|
|
|
|
def load_template(template_id: int):
|
2026-09-12 16:33:07 +08:00
|
|
|
from app.model.entities_advisor import ScriptTemplate
|
2026-09-12 14:27:37 +08:00
|
|
|
|
|
|
|
|
with AgentSessionLocal() as session:
|
|
|
|
|
row = session.get(ScriptTemplate, template_id)
|
|
|
|
|
assert row is not None
|
|
|
|
|
session.expunge(row)
|
|
|
|
|
return row
|
|
|
|
|
|
|
|
|
|
def test_template_migration_creates_template_and_use_log_tables():
|
|
|
|
|
inspector = inspect(agent_engine)
|
|
|
|
|
|
|
|
|
|
assert "script_template" in inspector.get_table_names()
|
|
|
|
|
assert "template_use_log" in inspector.get_table_names()
|
|
|
|
|
|
|
|
|
|
template_columns = {column["name"] for column in inspector.get_columns("script_template")}
|
|
|
|
|
use_log_columns = {column["name"] for column in inspector.get_columns("template_use_log")}
|
|
|
|
|
|
|
|
|
|
assert {
|
|
|
|
|
"scene",
|
|
|
|
|
"customer_type",
|
|
|
|
|
"title",
|
|
|
|
|
"content",
|
|
|
|
|
"tags",
|
|
|
|
|
"embedding_id",
|
|
|
|
|
"is_approved",
|
|
|
|
|
"approved_by",
|
|
|
|
|
"approved_at",
|
|
|
|
|
"version",
|
|
|
|
|
"usage_count",
|
|
|
|
|
"is_active",
|
|
|
|
|
"created_by",
|
|
|
|
|
}.issubset(template_columns)
|
|
|
|
|
assert {
|
|
|
|
|
"use_id",
|
|
|
|
|
"trace_id",
|
|
|
|
|
"template_id",
|
|
|
|
|
"advisor_id",
|
|
|
|
|
"is_modified",
|
|
|
|
|
"original_content",
|
|
|
|
|
"modified_content",
|
|
|
|
|
"content_diff",
|
|
|
|
|
}.issubset(use_log_columns)
|
|
|
|
|
|
|
|
|
|
def test_advisor_cannot_find_or_use_unapproved_template():
|
|
|
|
|
compliance_headers = {"Authorization": f"Bearer {compliance_token()}"}
|
|
|
|
|
advisor_headers = {"Authorization": f"Bearer {advisor_token()}"}
|
|
|
|
|
template = create_template(compliance_headers)
|
|
|
|
|
|
2026-09-12 16:33:07 +08:00
|
|
|
list_response = client.get("/api/advisor-agent/script-templates", headers=advisor_headers)
|
2026-09-12 14:27:37 +08:00
|
|
|
search_response = client.get(
|
2026-09-12 16:33:07 +08:00
|
|
|
"/api/advisor-agent/script-templates/search",
|
2026-09-12 14:27:37 +08:00
|
|
|
params={"q": template["title"]},
|
|
|
|
|
headers=advisor_headers,
|
|
|
|
|
)
|
|
|
|
|
use_response = client.post(
|
2026-09-12 16:33:07 +08:00
|
|
|
f"/api/advisor-agent/script-templates/{template['id']}/use",
|
2026-09-12 14:27:37 +08:00
|
|
|
json={"is_modified": False},
|
|
|
|
|
headers={**advisor_headers, "X-Trace-Id": f"trace-template-denied-{uuid4().hex}"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert list_response.status_code == 200
|
|
|
|
|
assert all(item["id"] != template["id"] for item in list_response.json()["data"]["items"])
|
|
|
|
|
assert search_response.status_code == 200
|
|
|
|
|
assert search_response.json()["data"]["items"] == []
|
|
|
|
|
assert use_response.status_code == 400
|
|
|
|
|
assert use_response.json()["code"] == "40002"
|
|
|
|
|
|
|
|
|
|
def test_compliance_can_approve_template_and_advisor_use_writes_log_and_increments_count():
|
|
|
|
|
compliance_headers = {"Authorization": f"Bearer {compliance_token()}"}
|
|
|
|
|
advisor_headers = {"Authorization": f"Bearer {advisor_token()}"}
|
|
|
|
|
template = create_template(compliance_headers)
|
|
|
|
|
|
|
|
|
|
approve_response = client.put(
|
2026-09-12 16:33:07 +08:00
|
|
|
f"/api/advisor-agent/script-templates/{template['id']}",
|
2026-09-12 14:27:37 +08:00
|
|
|
json={"is_approved": True},
|
|
|
|
|
headers=compliance_headers,
|
|
|
|
|
)
|
|
|
|
|
assert approve_response.status_code == 200
|
|
|
|
|
assert approve_response.json()["data"]["is_approved"] is True
|
|
|
|
|
|
|
|
|
|
use_trace_id = f"trace-template-use-{uuid4().hex}"
|
|
|
|
|
modified_content = template["content"] + " 客户可自行决定是否继续了解。"
|
|
|
|
|
use_response = client.post(
|
2026-09-12 16:33:07 +08:00
|
|
|
f"/api/advisor-agent/script-templates/{template['id']}/use",
|
2026-09-12 14:27:37 +08:00
|
|
|
json={"is_modified": True, "modified_content": modified_content},
|
|
|
|
|
headers={**advisor_headers, "X-Trace-Id": use_trace_id},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert use_response.status_code == 200
|
|
|
|
|
body = use_response.json()["data"]
|
|
|
|
|
log = latest_use_log(body["use_id"])
|
|
|
|
|
stored_template = load_template(template["id"])
|
|
|
|
|
|
|
|
|
|
assert body["template_id"] == template["id"]
|
|
|
|
|
assert body["is_modified"] is True
|
|
|
|
|
assert "-您好,近期市场波动较大" in body["diff"]
|
|
|
|
|
assert "+您好,近期市场波动较大" in body["diff"]
|
|
|
|
|
assert log.trace_id == use_trace_id
|
|
|
|
|
assert log.advisor_id == "STAFF-10086"
|
|
|
|
|
assert log.original_content == template["content"]
|
|
|
|
|
assert log.modified_content == modified_content
|
|
|
|
|
assert stored_template.usage_count == 1
|
|
|
|
|
|
|
|
|
|
def test_template_update_resets_approval_and_increments_version():
|
|
|
|
|
compliance_headers = {"Authorization": f"Bearer {compliance_token()}"}
|
|
|
|
|
advisor_headers = {"Authorization": f"Bearer {advisor_token()}"}
|
|
|
|
|
template = create_template(compliance_headers)
|
|
|
|
|
approved = client.put(
|
2026-09-12 16:33:07 +08:00
|
|
|
f"/api/advisor-agent/script-templates/{template['id']}",
|
2026-09-12 14:27:37 +08:00
|
|
|
json={"is_approved": True},
|
|
|
|
|
headers=compliance_headers,
|
|
|
|
|
).json()["data"]
|
|
|
|
|
assert approved["version"] == 1
|
|
|
|
|
|
|
|
|
|
update_response = client.put(
|
2026-09-12 16:33:07 +08:00
|
|
|
f"/api/advisor-agent/script-templates/{template['id']}",
|
2026-09-12 14:27:37 +08:00
|
|
|
json={"content": "您好,市场短期波动较大,请先阅读风险揭示材料。"},
|
|
|
|
|
headers=compliance_headers,
|
|
|
|
|
)
|
|
|
|
|
search_response = client.get(
|
2026-09-12 16:33:07 +08:00
|
|
|
"/api/advisor-agent/script-templates/search",
|
2026-09-12 14:27:37 +08:00
|
|
|
params={"q": approved["title"]},
|
|
|
|
|
headers=advisor_headers,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert update_response.status_code == 200
|
|
|
|
|
updated = update_response.json()["data"]
|
|
|
|
|
assert updated["is_approved"] is False
|
|
|
|
|
assert updated["approved_by"] is None
|
|
|
|
|
assert updated["version"] == 2
|
|
|
|
|
assert search_response.json()["data"]["items"] == []
|