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"] == "差旅费"