220 lines
8.0 KiB
Python
220 lines
8.0 KiB
Python
from __future__ import annotations
|
||
|
||
from datetime import UTC, date, datetime
|
||
from decimal import Decimal
|
||
|
||
from sqlalchemy import create_engine
|
||
from sqlalchemy.orm import Session, sessionmaker
|
||
from sqlalchemy.pool import StaticPool
|
||
|
||
from app.db.base import Base
|
||
from app.models.employee import Employee
|
||
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
|
||
from app.schemas.orchestrator import OrchestratorRequest
|
||
from app.services.agent_conversations import AgentConversationService
|
||
from app.services.orchestrator import OrchestratorService
|
||
|
||
|
||
def build_session_factory() -> sessionmaker[Session]:
|
||
engine = create_engine(
|
||
"sqlite+pysqlite:///:memory:",
|
||
connect_args={"check_same_thread": False},
|
||
poolclass=StaticPool,
|
||
)
|
||
Base.metadata.create_all(bind=engine)
|
||
return sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
||
|
||
|
||
def test_review_next_step_run_submits_existing_claim_and_returns_draft_payload(
|
||
monkeypatch,
|
||
) -> None:
|
||
monkeypatch.setattr(
|
||
"app.services.runtime_chat.RuntimeChatService.complete",
|
||
lambda *_args, **_kwargs: None,
|
||
)
|
||
session_factory = build_session_factory()
|
||
with session_factory() as db:
|
||
manager = Employee(
|
||
employee_no="E9000",
|
||
name="李经理",
|
||
email="manager-next@example.com",
|
||
)
|
||
employee = Employee(
|
||
employee_no="E9001",
|
||
name="张三",
|
||
email="emp-next@example.com",
|
||
manager=manager,
|
||
)
|
||
claim = ExpenseClaim(
|
||
id="claim-next-step",
|
||
claim_no="EXP-202605-001",
|
||
employee=employee,
|
||
employee_id=employee.id,
|
||
employee_name="张三",
|
||
department_name="销售部",
|
||
expense_type="office",
|
||
reason="采购办公用品",
|
||
location="上海",
|
||
amount=Decimal("128.00"),
|
||
currency="CNY",
|
||
invoice_count=1,
|
||
occurred_at=datetime(2026, 5, 20, 9, 0, tzinfo=UTC),
|
||
status="draft",
|
||
approval_stage="待提交",
|
||
items=[
|
||
ExpenseClaimItem(
|
||
item_date=date(2026, 5, 20),
|
||
item_type="office",
|
||
item_reason="采购办公用品",
|
||
item_location="上海",
|
||
item_amount=Decimal("128.00"),
|
||
invoice_id="office-invoice.png",
|
||
)
|
||
],
|
||
)
|
||
db.add_all([manager, employee, claim])
|
||
db.commit()
|
||
|
||
response = OrchestratorService(db).run(
|
||
OrchestratorRequest(
|
||
source="user_message",
|
||
user_id="emp-next@example.com",
|
||
message="我已核对右侧识别结果,请进入下一步。",
|
||
context_json={
|
||
"review_action": "next_step",
|
||
"draft_claim_id": claim.id,
|
||
"attachment_count": 1,
|
||
"name": "张三",
|
||
},
|
||
)
|
||
)
|
||
|
||
db.refresh(claim)
|
||
assert response.status == "succeeded"
|
||
assert response.requires_confirmation is False
|
||
assert response.result["draft_payload"]["status"] == "submitted"
|
||
assert response.result["draft_payload"]["approval_stage"] == "直属领导审批"
|
||
assert claim.status == "submitted"
|
||
assert claim.approval_stage == "直属领导审批"
|
||
assert claim.submitted_at is not None
|
||
assert response.conversation_id
|
||
assert AgentConversationService(db).get_conversation(response.conversation_id) is None
|
||
|
||
|
||
def test_review_next_step_blocked_returns_reasons_and_removes_next_step_action(
|
||
monkeypatch,
|
||
) -> None:
|
||
monkeypatch.setattr(
|
||
"app.services.runtime_chat.RuntimeChatService.complete",
|
||
lambda *_args, **_kwargs: None,
|
||
)
|
||
session_factory = build_session_factory()
|
||
with session_factory() as db:
|
||
employee = Employee(
|
||
employee_no="E9011",
|
||
name="张三",
|
||
email="emp-blocked@example.com",
|
||
)
|
||
claim = ExpenseClaim(
|
||
id="claim-next-step-blocked",
|
||
claim_no="EXP-202605-002",
|
||
employee=employee,
|
||
employee_id=employee.id,
|
||
employee_name="张三",
|
||
department_name="待补充",
|
||
expense_type="office",
|
||
reason="采购办公用品",
|
||
location="上海",
|
||
amount=Decimal("128.00"),
|
||
currency="CNY",
|
||
invoice_count=1,
|
||
occurred_at=datetime(2026, 5, 20, 9, 0, tzinfo=UTC),
|
||
status="draft",
|
||
approval_stage="待提交",
|
||
items=[
|
||
ExpenseClaimItem(
|
||
item_date=date(2026, 5, 20),
|
||
item_type="office",
|
||
item_reason="采购办公用品",
|
||
item_location="上海",
|
||
item_amount=Decimal("128.00"),
|
||
invoice_id="office-invoice.png",
|
||
)
|
||
],
|
||
)
|
||
db.add_all([employee, claim])
|
||
db.commit()
|
||
|
||
response = OrchestratorService(db).run(
|
||
OrchestratorRequest(
|
||
source="user_message",
|
||
user_id="emp-blocked@example.com",
|
||
message="我已核对右侧识别结果,请进入下一步。",
|
||
context_json={
|
||
"review_action": "next_step",
|
||
"draft_claim_id": claim.id,
|
||
"attachment_count": 1,
|
||
"name": "张三",
|
||
},
|
||
)
|
||
)
|
||
|
||
result = response.result
|
||
review_payload = result["review_payload"]
|
||
actions = {
|
||
str(item.get("action_type") or "").strip()
|
||
for item in review_payload["confirmation_actions"]
|
||
}
|
||
|
||
assert response.status == "succeeded"
|
||
assert result["draft_payload"]["status"] == "draft"
|
||
assert response.conversation_id
|
||
assert AgentConversationService(db).get_conversation(response.conversation_id) is not None
|
||
assert "AI预审暂未通过" in result["answer"]
|
||
assert "所属部门未完善" in result["answer"]
|
||
assert "next_step" not in actions
|
||
assert "save_draft" in actions
|
||
assert any(
|
||
"所属部门未完善" in str(item.get("content") or "")
|
||
for item in review_payload["risk_briefs"]
|
||
)
|
||
|
||
|
||
def test_conversation_hydration_does_not_reuse_review_type_for_fresh_expense_prompt() -> None:
|
||
session_factory = build_session_factory()
|
||
with session_factory() as db:
|
||
service = AgentConversationService(db)
|
||
conversation = service.get_or_create_conversation(
|
||
conversation_id="conv-review-type-lock",
|
||
user_id="emp-review-type@example.com",
|
||
source="user_message",
|
||
context_json={
|
||
"session_type": "expense",
|
||
"draft_claim_id": "claim-old",
|
||
"attachment_names": ["old-train-ticket.pdf"],
|
||
"attachment_count": 1,
|
||
"review_form_values": {
|
||
"expense_type": "差旅费",
|
||
"business_location": "北京",
|
||
},
|
||
},
|
||
)
|
||
|
||
fresh_context = service.hydrate_context_json(
|
||
conversation=conversation,
|
||
context_json={},
|
||
message="业务发生时间:2026-02-20 至 2026-02-23,去上海支持上海电力部署项目,申请报销",
|
||
)
|
||
continued_context = service.hydrate_context_json(
|
||
conversation=conversation,
|
||
context_json={},
|
||
message="继续补充酒店发票",
|
||
)
|
||
|
||
assert "draft_claim_id" not in fresh_context
|
||
assert "attachment_names" not in fresh_context
|
||
assert "review_form_values" not in fresh_context
|
||
assert fresh_context["conversation_state"]["review_form_values"]["expense_type"] == "差旅费"
|
||
assert continued_context["draft_claim_id"] == "claim-old"
|
||
assert continued_context["review_form_values"]["expense_type"] == "差旅费"
|