feat(ai): unify verified expense application workflow

This commit is contained in:
caoxiaozhu
2026-07-14 16:03:05 +08:00
parent 5b24630710
commit 211f85d981
33 changed files with 2793 additions and 405 deletions

View File

@@ -4,13 +4,16 @@ from datetime import UTC, date, datetime
from decimal import Decimal
import pytest
from sqlalchemy import create_engine
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
from app.api.deps import CurrentUserContext
from app.db.base import Base
from app.models.agent_asset import AgentAsset
from app.models.agent_run import AgentRun
from app.models.ai_application_preview import AIApplicationPreviewDecision
from app.models.ai_learning import AIDecision, AIDecisionFeedback
from app.models.employee import Employee
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
from app.schemas.ontology import OntologyParseResult, OntologyPermission
@@ -881,3 +884,135 @@ def test_orchestrator_application_submit_bypasses_generic_operation_block(
assert "当前仅返回确认摘要" not in submitted.result["answer"]
assert "申请单据已生成,并已进入审批流程" in submitted.result["answer"]
assert submitted.result["draft_payload"]["status"] == "submitted"
def test_authenticated_orchestrator_application_draft_consumes_server_preview_decision(
monkeypatch,
) -> None:
monkeypatch.setattr(
"app.services.runtime_chat.RuntimeChatService.complete",
lambda *_args, **_kwargs: None,
)
def parse_application_for_run(self, request, run_id): # noqa: ANN001
return OntologyParseResult(
scenario="expense",
intent="operate",
entities=[],
permission=OntologyPermission(
level="approval_required",
allowed=True,
reason="费用申请由可信预览工作流执行。",
),
confidence=0.99,
missing_slots=[],
ambiguity=[],
clarification_required=False,
clarification_question=None,
run_id=run_id,
)
monkeypatch.setattr(
"app.services.ontology.SemanticOntologyService.parse_for_run",
parse_application_for_run,
)
session_factory = build_session_factory()
current_user = CurrentUserContext(
username="trusted-application@example.com",
name="可信申请员工",
role_codes=["user"],
is_admin=False,
tenant_id="tenant-orchestrator-preview",
department_name="交付部",
position="实施顾问",
grade="P4",
employee_no="E-ORCH-001",
employee_id="employee-orchestrator-preview",
manager_name="陈硕",
auth_session_id="session-orchestrator-preview",
)
context_json = {
"session_type": "application",
"entry_source": "application",
"user_id": "forged@example.com",
"tenant_id": "forged-tenant",
"is_admin": True,
"manager_name": "伪造审批人",
}
with session_factory() as db:
service = OrchestratorService(db)
first = service.run(
OrchestratorRequest(
source="user_message",
user_id="forged@example.com",
message=(
"发生时间2026-05-25\n"
"地点:上海\n"
"事由:支持上海国网服务器部署\n"
"天数3天"
),
context_json=context_json,
),
current_user=current_user,
)
preview = service.run(
OrchestratorRequest(
source="user_message",
user_id="forged@example.com",
conversation_id=first.conversation_id,
message="飞机",
context_json=context_json,
),
current_user=current_user,
)
decision_id = str(preview.result.get("decision_id") or "")
assert preview.status == "blocked"
assert preview.requires_confirmation is True
assert decision_id
assert preview.result["application_preview"]["decisionId"] == decision_id
decision = db.get(AIApplicationPreviewDecision, decision_id)
assert decision is not None
assert decision.status == "issued"
assert decision.actor_id == current_user.employee_id
assert decision.tenant_id == current_user.tenant_id
conversation = service.conversation_service.get_conversation(first.conversation_id or "")
assert conversation is not None
assert conversation.state_json["application_preview_decision"]["decision_id"] == decision_id
saved = service.run(
OrchestratorRequest(
source="user_message",
user_id="another-forged@example.com",
conversation_id=first.conversation_id,
message="保存草稿",
context_json={
**context_json,
"application_preview_decision": {
"decision_id": "forged-decision",
"status": "issued",
},
},
),
current_user=current_user,
)
assert saved.status == "succeeded", saved.result
assert saved.result["draft_payload"]["status"] == "draft"
assert saved.result["draft_payload"]["draft_type"] == "expense_application"
assert saved.result["draft_payload"]["claim_no"]
assert saved.result["decision_id"]
assert saved.result["decision_id"] != decision_id
db.refresh(decision)
assert decision.status == "consumed"
assert decision.consumed_action == "save_draft"
learning_decision = db.scalar(
select(AIDecision).where(AIDecision.preview_decision_id == decision_id)
)
assert learning_decision is not None
feedback = db.scalar(
select(AIDecisionFeedback).where(
AIDecisionFeedback.decision_id == learning_decision.id
)
)
assert feedback is not None
assert feedback.feedback_type == "accepted"