feat: 完善文档中心与报销申请交互及侧边栏重构

后端优化编排器报销查询和本体检测精度,增强报销单草稿保
存和附件回填逻辑,前端重构侧边栏组件支持折叠和图标导
航,完善文档中心状态筛选和详情提示,报销创建和审批详情
页优化会话管理和费用明细交互,新增助手应用服务和预设动
作工具函数,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-25 13:35:39 +08:00
parent 50b1c3f9a9
commit d0e946cf47
59 changed files with 5117 additions and 416 deletions

View File

@@ -497,7 +497,7 @@ def test_spreadsheet_change_records_include_all_modified_sheets() -> None:
def test_editable_spreadsheet_onlyoffice_config_enables_forcesave(monkeypatch) -> None:
with build_session() as db:
monkeypatch.setattr(
"app.services.agent_assets.resolve_onlyoffice_settings",
"app.services.agent_asset_onlyoffice.resolve_onlyoffice_settings",
lambda: OnlyOfficeRuntimeConfig(
enabled=True,
public_url="http://onlyoffice.example.com",

View File

@@ -2883,6 +2883,133 @@ def test_direct_manager_can_approve_subordinate_claim_to_finance_review() -> Non
)
def test_application_submit_skips_ai_review_and_receipt_requirements(monkeypatch: pytest.MonkeyPatch) -> None:
current_user = CurrentUserContext(
username="application-owner@example.com",
name="张三",
role_codes=["employee"],
is_admin=True,
)
with build_session() as db:
claim = ExpenseClaim(
claim_no="APP-20260525-SUBMIT",
employee_name="张三",
department_name="交付部",
project_code="PRJ-A",
expense_type="travel_application",
reason="支撑国网服务器上线部署",
location="上海",
amount=Decimal("12000.00"),
currency="CNY",
invoice_count=0,
occurred_at=datetime(2026, 5, 25, 9, 0, tzinfo=UTC),
submitted_at=None,
status="draft",
approval_stage="待提交",
risk_flags_json=[
{
"source": "submission_review",
"severity": "medium",
"message": "旧 AI 预审提示不应保留到申请单提交结果。",
}
],
)
db.add(claim)
db.commit()
claim_id = claim.id
service = ExpenseClaimService(db)
def fail_ai_review(_claim: ExpenseClaim) -> dict[str, object]:
raise AssertionError("费用申请提交不应进入 AI 预审")
monkeypatch.setattr(service, "_run_ai_submission_review", fail_ai_review)
submitted = service.submit_claim(claim_id, current_user)
assert submitted is not None
assert submitted.status == "submitted"
assert submitted.approval_stage == "直属领导审批"
assert submitted.invoice_count == 0
assert submitted.items == []
assert not any(
isinstance(flag, dict) and flag.get("source") == "submission_review"
for flag in submitted.risk_flags_json
)
assert any(
isinstance(flag, dict)
and flag.get("source") == "application_submission"
and flag.get("event_type") == "expense_application_submission"
and flag.get("next_approval_stage") == "直属领导审批"
for flag in submitted.risk_flags_json
)
def test_direct_manager_can_approve_application_claim_to_completed_stage() -> None:
current_user = CurrentUserContext(
username="manager-application-approve@example.com",
name="李经理",
role_codes=["manager"],
is_admin=False,
)
with build_session() as db:
manager = Employee(
employee_no="E8112",
name="李经理",
email="manager-application-approve@example.com",
)
employee = Employee(
employee_no="E8113",
name="张三",
email="zhangsan-application-approve@example.com",
manager=manager,
)
db.add_all([manager, employee])
db.flush()
claim = ExpenseClaim(
claim_no="APP-20260525-APPROVE",
employee_id=employee.id,
employee_name="张三",
department_name="交付部",
project_code="PRJ-A",
expense_type="travel_application",
reason="支撑国网服务器上线部署",
location="上海",
amount=Decimal("12000.00"),
currency="CNY",
invoice_count=0,
occurred_at=datetime(2026, 5, 25, 9, 0, tzinfo=UTC),
submitted_at=datetime(2026, 5, 25, 10, 0, tzinfo=UTC),
status="submitted",
approval_stage="直属领导审批",
risk_flags_json=[],
)
db.add(claim)
db.commit()
claim_id = claim.id
approved = ExpenseClaimService(db).approve_claim(
claim_id,
current_user,
opinion="业务必要,同意申请。",
)
assert approved is not None
assert approved.status == "approved"
assert approved.approval_stage == "审批完成"
assert any(
isinstance(flag, dict)
and flag.get("source") == "manual_approval"
and flag.get("event_type") == "expense_application_approval"
and flag.get("opinion") == "业务必要,同意申请。"
and flag.get("previous_approval_stage") == "直属领导审批"
and flag.get("next_status") == "approved"
and flag.get("next_approval_stage") == "审批完成"
for flag in approved.risk_flags_json
)
def test_finance_can_approve_claim_to_archive_stage() -> None:
current_user = CurrentUserContext(
username="finance-approve@example.com",

View File

@@ -649,6 +649,40 @@ def test_semantic_ontology_service_requires_attachment_for_meeting_application()
assert "attachments" in result.missing_slots
def test_semantic_ontology_service_treats_application_session_as_application_context() -> None:
session_factory = build_session_factory()
with session_factory() as db:
result = SemanticOntologyService(db).parse(
OntologyParseRequest(
query=(
"发生时间2026-05-25\n"
"地点:上海\n"
"事由:支持上海国网服务器部署\n"
"天数3天"
),
user_id="pytest",
context_json={
"session_type": "application",
"entry_source": "application",
"attachment_count": 0,
},
)
)
assert result.scenario == "expense"
assert result.intent == "draft"
assert any(
item.type == "document_type" and item.normalized_value == "expense_application"
for item in result.entities
)
assert any(
item.type == "workflow_stage" and item.normalized_value == "pre_approval"
for item in result.entities
)
assert "expense_type" in result.missing_slots
assert "amount" in result.missing_slots
def test_semantic_ontology_service_uses_model_parse_when_available(monkeypatch) -> None:
session_factory = build_session_factory()
with session_factory() as db:

View File

@@ -11,6 +11,7 @@ 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.ontology import OntologyParseResult, OntologyPermission
from app.schemas.orchestrator import OrchestratorRequest
from app.services.agent_conversations import AgentConversationService
from app.services.orchestrator import OrchestratorService
@@ -228,6 +229,50 @@ def test_conversation_hydration_does_not_reuse_review_type_for_fresh_expense_pro
assert continued_context["review_form_values"]["expense_type"] == "差旅费"
def test_conversation_hydration_preserves_incoming_application_time_context() -> None:
session_factory = build_session_factory()
with session_factory() as db:
service = AgentConversationService(db)
conversation = service.get_or_create_conversation(
conversation_id="conv-application-time-context",
user_id="emp-application-time@example.com",
source="user_message",
context_json={
"session_type": "application",
"entry_source": "application",
"business_time_context": {
"mode": "single",
"start_date": "2026-05-01",
"end_date": "2026-05-01",
"display_value": "2026-05-01",
},
},
)
stale_context = service.hydrate_context_json(
conversation=conversation,
context_json={"session_type": "application", "entry_source": "application"},
message="apply travel expense",
)
fresh_context = service.hydrate_context_json(
conversation=conversation,
context_json={
"session_type": "application",
"entry_source": "application",
"business_time_context": {
"mode": "single",
"start_date": "2026-05-25",
"end_date": "2026-05-25",
"display_value": "2026-05-25",
},
},
message="apply travel expense",
)
assert "business_time_context" not in stale_context
assert fresh_context["business_time_context"]["start_date"] == "2026-05-25"
def test_conversation_scope_creates_new_session_for_different_claim() -> None:
session_factory = build_session_factory()
with session_factory() as db:
@@ -543,3 +588,225 @@ def test_orchestrator_prompts_scene_choices_before_review_for_fresh_ambiguous_ex
assert result.get("draft_payload") is None
assert "请先在下面选择报销场景" in result["answer"]
assert [item["label"] for item in result["suggested_actions"][:3]] == ["差旅费", "交通费", "住宿费"]
def test_orchestrator_application_session_does_not_use_reimbursement_scene_prompt(
monkeypatch,
) -> None:
monkeypatch.setattr(
"app.services.runtime_chat.RuntimeChatService.complete",
lambda *_args, **_kwargs: None,
)
session_factory = build_session_factory()
message = (
"发生时间2026-05-25\n"
"地点:上海\n"
"事由:支持上海国网服务器部署\n"
"天数3天"
)
with session_factory() as db:
response = OrchestratorService(db).run(
OrchestratorRequest(
source="user_message",
user_id="application-session@example.com",
message=message,
context_json={
"session_type": "application",
"entry_source": "application",
"name": "申请员工",
},
)
)
result = response.result
assert response.status == "blocked"
assert response.trace_summary.scenario == "expense"
assert "费用申请" in result["answer"]
assert "| 发生时间 | 2026-05-25" in result["answer"]
assert "请先在下面选择报销场景" not in result["answer"]
assert result.get("review_payload") is None
def test_orchestrator_application_session_guides_transport_amount_and_submit(
monkeypatch,
) -> None:
monkeypatch.setattr(
"app.services.runtime_chat.RuntimeChatService.complete",
lambda *_args, **_kwargs: None,
)
session_factory = build_session_factory()
initial_message = (
"发生时间2026-05-25\n"
"地点:上海\n"
"事由:支持上海国网服务器部署\n"
"天数3天"
)
context_json = {
"session_type": "application",
"entry_source": "application",
"name": "申请员工",
"manager_name": "陈硕",
}
with session_factory() as db:
service = OrchestratorService(db)
first = service.run(
OrchestratorRequest(
source="user_message",
user_id="application-flow@example.com",
message=initial_message,
context_json=context_json,
)
)
second = service.run(
OrchestratorRequest(
source="user_message",
user_id="application-flow@example.com",
conversation_id=first.conversation_id,
message="飞机",
context_json=context_json,
)
)
third = service.run(
OrchestratorRequest(
source="user_message",
user_id="application-flow@example.com",
conversation_id=first.conversation_id,
message="预计总费用12000元",
context_json=context_json,
)
)
fourth = service.run(
OrchestratorRequest(
source="user_message",
user_id="application-flow@example.com",
conversation_id=first.conversation_id,
message="确认提交",
context_json=context_json,
)
)
assert first.status == "blocked"
assert "当前还需要补充:出行方式、预计金额/预算" in first.result["answer"]
assert [item["label"] for item in first.result["suggested_actions"]] == ["一次性补充申请信息"]
assert first.result["suggested_actions"][0]["payload"]["prompt_prefill"] == "出行方式:\n预计总费用:"
assert "当前还需要补充:预计金额/预算" in second.result["answer"]
assert [item["label"] for item in second.result["suggested_actions"]] == ["一次性补充申请信息"]
assert second.result["suggested_actions"][0]["action_type"] == "prefill_composer"
assert second.result["suggested_actions"][0]["payload"]["prompt_prefill"] == "预计总费用:"
assert "这是模拟的费用申请结果" in third.result["answer"]
assert "| 事由 | 支持上海国网服务器部署 |" in third.result["answer"]
assert "请核对上述信息无误" in third.result["answer"]
assert "[确认](#application-submit)" in third.result["answer"]
assert third.status == "blocked"
assert third.result["requires_confirmation"] is True
assert third.result["suggested_actions"] == []
assert fourth.status == "succeeded"
assert fourth.result["clarification_required"] is False
assert fourth.result["missing_slots"] == []
assert "当前操作已完成,单据已经推送给 陈硕 进行审核,请耐心等待" in fourth.result["answer"]
assert "当前状态:陈硕审核中" in fourth.result["answer"]
assert fourth.result["suggested_actions"] == []
application_claims = [
claim
for claim in db.query(ExpenseClaim).all()
if claim.claim_no.startswith("APP-20260525-")
]
assert len(application_claims) == 1
assert application_claims[0].status == "submitted"
assert application_claims[0].approval_stage == "直属领导审批"
assert fourth.result["draft_payload"]["claim_no"] == application_claims[0].claim_no
def test_orchestrator_application_submit_bypasses_generic_operation_block(
monkeypatch,
) -> None:
monkeypatch.setattr(
"app.services.runtime_chat.RuntimeChatService.complete",
lambda *_args, **_kwargs: None,
)
session_factory = build_session_factory()
initial_message = (
"发生时间2026-05-25\n"
"地点:上海\n"
"事由:支持上海国网服务器部署\n"
"天数3天"
)
context_json = {
"session_type": "application",
"entry_source": "application",
"name": "申请员工",
"manager_name": "陈硕",
}
with session_factory() as db:
service = OrchestratorService(db)
first = service.run(
OrchestratorRequest(
source="user_message",
user_id="application-approval-required@example.com",
message=initial_message,
context_json=context_json,
)
)
service.run(
OrchestratorRequest(
source="user_message",
user_id="application-approval-required@example.com",
conversation_id=first.conversation_id,
message="飞机",
context_json=context_json,
)
)
preview = service.run(
OrchestratorRequest(
source="user_message",
user_id="application-approval-required@example.com",
conversation_id=first.conversation_id,
message="预计总费用12000元",
context_json=context_json,
)
)
def approval_required_parse_for_run(self, request, run_id): # noqa: ANN001
return OntologyParseResult(
scenario="expense",
intent="operate",
entities=[],
permission=OntologyPermission(
level="approval_required",
allowed=False,
reason="操作类请求需要人工审批确认。",
),
confidence=0.95,
missing_slots=[],
ambiguity=[],
clarification_required=False,
clarification_question=None,
run_id=run_id,
)
monkeypatch.setattr(
"app.services.ontology.SemanticOntologyService.parse_for_run",
approval_required_parse_for_run,
)
submitted = service.run(
OrchestratorRequest(
source="user_message",
user_id="application-approval-required@example.com",
conversation_id=first.conversation_id,
message="确认提交",
context_json=context_json,
)
)
assert preview.status == "blocked"
assert submitted.status == "succeeded"
assert submitted.requires_confirmation is False
assert "操作类请求需要人工审批确认" not in submitted.result["answer"]
assert "当前仅返回确认摘要" not in submitted.result["answer"]
assert "当前操作已完成,单据已经推送给 陈硕 进行审核,请耐心等待" in submitted.result["answer"]
assert submitted.result["draft_payload"]["status"] == "submitted"

View File

@@ -364,6 +364,70 @@ def test_approve_claim_endpoint_routes_direct_manager_claim_to_finance_review()
assert "manager-approve-api@example.com" not in approval_events[0]["message"]
def test_approve_application_endpoint_completes_after_direct_manager_review() -> None:
client, session_factory = build_client()
with session_factory() as db:
manager = Employee(
id="mgr-application-approve-1",
employee_no="E21002",
name="李经理",
email="manager-application-approve-api@example.com",
)
employee = Employee(
id="emp-application-approve-1",
employee_no="E11002",
name="张三",
email="zhangsan-application-approve-api@example.com",
manager=manager,
)
claim = ExpenseClaim(
id="claim-application-approve-1",
claim_no="APP-20260525-API001",
employee_id=employee.id,
employee_name="张三",
department_id="dept-1",
department_name="交付部",
project_code=None,
expense_type="travel_application",
reason="支撑国网服务器上线部署",
location="上海",
amount=Decimal("12000.00"),
currency="CNY",
invoice_count=0,
occurred_at=datetime(2026, 5, 25, tzinfo=UTC),
submitted_at=datetime(2026, 5, 25, 10, 0, tzinfo=UTC),
status="submitted",
approval_stage="直属领导审批",
risk_flags_json=[],
)
db.add_all([manager, employee, claim])
db.commit()
response = client.post(
"/api/v1/reimbursements/claims/claim-application-approve-1/approve",
json={"opinion": "业务必要,同意申请。"},
headers={
"X-Auth-Username": "manager-application-approve-api@example.com",
"X-Auth-Name": "manager-application-approve-api@example.com",
"X-Auth-Role-Codes": "manager",
},
)
assert response.status_code == 200
payload = response.json()
assert payload["status"] == "approved"
assert payload["approval_stage"] == "审批完成"
assert any(
item["source"] == "manual_approval"
and item["event_type"] == "expense_application_approval"
and item["opinion"] == "业务必要,同意申请。"
and item["operator"] == "李经理"
and item["next_status"] == "approved"
and item["next_approval_stage"] == "审批完成"
for item in payload["risk_flags_json"]
)
def test_claim_item_pdf_attachment_preview_returns_generated_image(monkeypatch, tmp_path) -> None:
preview_bytes = b"fake-preview-png"
preview_data_url = f"data:image/png;base64,{base64.b64encode(preview_bytes).decode('ascii')}"

View File

@@ -29,6 +29,41 @@ def build_session_factory() -> sessionmaker[Session]:
return sessionmaker(bind=engine, autoflush=False, autocommit=False)
def build_application_user_agent_response(
db: Session,
message: str,
*,
history: list[dict[str, object]] | None = None,
context_overrides: dict[str, object] | None = None,
):
context_json = {
"session_type": "application",
"entry_source": "application",
"attachment_count": 0,
}
if context_overrides:
context_json.update(context_overrides)
if history is not None:
context_json["conversation_history"] = history
ontology = SemanticOntologyService(db).parse(
OntologyParseRequest(
query=message,
user_id="pytest",
context_json=context_json,
)
)
return UserAgentService(db).respond(
UserAgentRequest(
run_id=ontology.run_id,
user_id="pytest",
message=message,
ontology=ontology,
context_json=context_json,
tool_payload={"clarification_required": ontology.clarification_required},
)
)
def test_user_agent_query_returns_readable_answer_and_actions() -> None:
session_factory = build_session_factory()
with session_factory() as db:
@@ -137,6 +172,216 @@ def test_user_agent_knowledge_prompt_enforces_knowledge_boundary() -> None:
assert '"knowledge_answer_evidence": []' in messages[1]["content"]
def test_user_agent_application_context_uses_application_language() -> None:
session_factory = build_session_factory()
message = (
"发生时间2026-05-25\n"
"地点:上海\n"
"事由:支持上海国网服务器部署\n"
"天数3天"
)
context_json = {
"session_type": "application",
"entry_source": "application",
"attachment_count": 0,
}
with session_factory() as db:
ontology = SemanticOntologyService(db).parse(
OntologyParseRequest(
query=message,
user_id="pytest",
context_json=context_json,
)
)
response = UserAgentService(db).respond(
UserAgentRequest(
run_id=ontology.run_id,
user_id="pytest",
message=message,
ontology=ontology,
context_json=context_json,
tool_payload={"clarification_required": True},
)
)
assert "费用申请" in response.answer
assert "| 字段 | 内容 |" in response.answer
assert "| 发生时间 | 2026-05-25 至 2026-05-28 |" in response.answer
assert "支持上海国网服务器部署" in response.answer
assert "当前还需要补充:出行方式、预计金额/预算" in response.answer
assert "请先在下面选择报销场景" not in response.answer
assert response.review_payload is None
assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"]
assert response.suggested_actions[0].payload["prompt_prefill"] == "出行方式:\n预计总费用:"
def test_user_agent_application_infers_natural_reason_and_expands_single_date() -> None:
session_factory = build_session_factory()
message = "发生时间2026-05-25\n去上海出差3天支撑上海国网服务器部署"
with session_factory() as db:
response = build_application_user_agent_response(db, message)
assert "| 发生时间 | 2026-05-25 至 2026-05-28 |" in response.answer
assert "| 地点 | 上海 |" in response.answer
assert "| 事由 | 支撑上海国网服务器部署 |" in response.answer
assert "当前还需要先补充:申请事由" not in response.answer
assert "当前还需要补充:出行方式、预计金额/预算" in response.answer
assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"]
def test_user_agent_application_uses_selected_time_and_natural_language_fields() -> None:
session_factory = build_session_factory()
message = "出差上海,支撑国网服务器上线部署"
context_json = {
"session_type": "application",
"entry_source": "application",
"business_time_context": {
"mode": "single",
"start_date": "2026-05-25",
"end_date": "2026-05-25",
"display_value": "2026-05-25",
},
}
with session_factory() as db:
ontology = SemanticOntologyService(db).parse(
OntologyParseRequest(
query=message,
user_id="pytest",
context_json=context_json,
)
)
response = UserAgentService(db).respond(
UserAgentRequest(
run_id=ontology.run_id,
user_id="pytest",
message=message,
ontology=ontology,
context_json=context_json,
tool_payload={"clarification_required": True},
)
)
assert "| 发生时间 | 2026-05-25 |" in response.answer
assert "| 地点 | 上海 |" in response.answer
assert "| 事由 | 支撑国网服务器上线部署 |" in response.answer
assert "当前还需要补充:出行方式、预计金额/预算" in response.answer
assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"]
assert response.suggested_actions[0].action_type == "prefill_composer"
assert response.suggested_actions[0].payload["prompt_prefill"] == "出行方式:\n预计总费用:"
def test_user_agent_application_asks_amount_after_transport_choice() -> None:
session_factory = build_session_factory()
initial_message = (
"发生时间2026-05-25\n"
"地点:上海\n"
"事由:支持上海国网服务器部署\n"
"天数3天"
)
with session_factory() as db:
response = build_application_user_agent_response(
db,
"飞机",
history=[{"role": "user", "content": initial_message}],
)
assert "| 出行方式 | 飞机 |" in response.answer
assert "当前还需要补充:预计金额/预算" in response.answer
assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"]
assert response.suggested_actions[0].action_type == "prefill_composer"
assert response.suggested_actions[0].payload["prompt_prefill"] == "预计总费用:"
def test_user_agent_application_missing_base_actions_prefill_composer() -> None:
session_factory = build_session_factory()
with session_factory() as db:
response = build_application_user_agent_response(
db,
"地点:上海\n事由:支撑国网服务器部署\n天数3天",
)
assert "当前还需要补充:发生时间、出行方式、预计金额/预算" in response.answer
assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"]
assert response.suggested_actions[0].action_type == "prefill_composer"
assert response.suggested_actions[0].payload["prompt_prefill"] == "申请时间段:\n出行方式:\n预计总费用:"
def test_user_agent_application_builds_preview_when_amount_is_ready() -> None:
session_factory = build_session_factory()
initial_message = (
"发生时间2026-05-25\n"
"地点:上海\n"
"事由:支持上海国网服务器部署\n"
"天数3天"
)
with session_factory() as db:
response = build_application_user_agent_response(
db,
"预计总费用12000元",
history=[
{"role": "user", "content": initial_message},
{"role": "user", "content": "飞机"},
],
)
assert "这是模拟的费用申请结果" in response.answer
assert "| 字段 | 内容 |" in response.answer
assert "| 事由 | 支持上海国网服务器部署 |" in response.answer
assert "| 出行方式 | 飞机 |" in response.answer
assert "| 预计总费用 | 12000元 |" in response.answer
assert "请核对上述信息无误" in response.answer
assert "[确认](#application-submit)" in response.answer
assert response.requires_confirmation is True
assert response.suggested_actions == []
def test_user_agent_application_submit_enters_leader_review() -> None:
session_factory = build_session_factory()
initial_message = (
"发生时间2026-05-25\n"
"地点:上海\n"
"事由:支持上海国网服务器部署\n"
"天数3天"
)
preview_answer = (
"这是模拟的费用申请结果,请核对:\n"
"| 字段 | 内容 |\n"
"| --- | --- |\n"
"| 申请类型 | 差旅费用申请 |\n"
"| 发生时间 | 2026-05-25 |\n"
"| 地点 | 上海 |\n"
"| 事由 | 支持上海国网服务器部署 |\n"
"| 天数 | 3天 |\n"
"| 出行方式 | 飞机 |\n"
"| 预计总费用 | 12000元 |\n\n"
"请核对上述信息无误,确认无误后 [确认](#application-submit) 提交至审批流程。"
)
with session_factory() as db:
response = build_application_user_agent_response(
db,
"确认提交",
context_overrides={"manager_name": "陈硕"},
history=[
{"role": "user", "content": initial_message},
{"role": "user", "content": "飞机"},
{"role": "user", "content": "预计总费用12000元"},
{"role": "assistant", "content": preview_answer},
],
)
assert "当前操作已完成,单据已经推送给 陈硕 进行审核,请耐心等待" in response.answer
assert "当前状态:陈硕审核中" in response.answer
assert "预算占用参考" in response.answer
assert "APP-20260525-" in response.answer
assert response.suggested_actions == []
claim = db.query(ExpenseClaim).filter(ExpenseClaim.claim_no.like("APP-20260525-%")).one()
assert claim.status == "submitted"
assert claim.approval_stage == "直属领导审批"
assert claim.expense_type == "travel_application"
assert claim.amount == Decimal("12000.00")
assert claim.employee_name == "pytest"
def test_user_agent_knowledge_fallback_is_honest_and_personalized() -> None:
session_factory = build_session_factory()
with session_factory() as db: