diff --git a/server/tests/test_orchestrator_service.py b/server/tests/test_orchestrator_service.py index c035798..1daafae 100644 --- a/server/tests/test_orchestrator_service.py +++ b/server/tests/test_orchestrator_service.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Generator from datetime import UTC, datetime, timedelta +from decimal import Decimal from fastapi.testclient import TestClient from sqlalchemy import create_engine, func, select @@ -70,6 +71,106 @@ def test_orchestrator_routes_user_query_to_user_agent() -> None: assert run_detail["tool_calls"][0]["tool_type"] == "database" +def test_orchestrator_scopes_my_expense_query_to_current_user() -> None: + client, session_factory = build_client() + user_id = "zhaoliu@example.com" + + with session_factory() as db: + employee = Employee( + employee_no="E9001", + name="赵六", + email=user_id, + ) + db.add(employee) + db.flush() + db.add_all( + [ + ExpenseClaim( + claim_no="EXP-TEST-001", + employee_id=employee.id, + employee_name="赵六", + department_name="测试部", + project_code="PRJ-TEST-01", + expense_type="travel", + reason="上海客户拜访", + location="上海", + amount=Decimal("120.00"), + currency="CNY", + invoice_count=1, + occurred_at=datetime(2026, 5, 10, 9, 0, tzinfo=UTC), + submitted_at=datetime(2026, 5, 10, 18, 0, tzinfo=UTC), + status="submitted", + approval_stage="finance_review", + risk_flags_json=[], + ), + ExpenseClaim( + claim_no="EXP-TEST-002", + employee_name=user_id, + department_name="测试部", + project_code="PRJ-TEST-02", + expense_type="meal", + reason="客户午餐", + location="杭州", + amount=Decimal("300.00"), + currency="CNY", + invoice_count=1, + occurred_at=datetime(2026, 5, 11, 12, 0, tzinfo=UTC), + submitted_at=datetime(2026, 5, 11, 13, 0, tzinfo=UTC), + status="draft", + approval_stage=None, + risk_flags_json=[], + ), + ExpenseClaim( + claim_no="EXP-TEST-003", + employee_name="张三", + department_name="财务部", + project_code="PRJ-OTHER-01", + expense_type="hotel", + reason="外地出差住宿", + location="深圳", + amount=Decimal("999.00"), + currency="CNY", + invoice_count=1, + occurred_at=datetime(2026, 5, 12, 8, 30, tzinfo=UTC), + submitted_at=datetime(2026, 5, 12, 9, 30, tzinfo=UTC), + status="approved", + approval_stage="completed", + risk_flags_json=[], + ), + ] + ) + db.commit() + + response = client.post( + "/api/v1/orchestrator/run", + json={ + "source": "user_message", + "user_id": user_id, + "message": "请查询我的报销单", + "context_json": { + "role_codes": ["employee"], + "name": "赵六", + }, + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["selected_agent"] == "user_agent" + assert payload["status"] == "succeeded" + assert "查到你的报销单共 2 笔" in payload["result"]["answer"] + assert "EXP-TEST-001" in payload["result"]["answer"] + assert "EXP-TEST-002" in payload["result"]["answer"] + assert "EXP-TEST-003" not in payload["result"]["answer"] + + run_detail = client.get(f"/api/v1/agent-runs/{payload['run_id']}").json() + tool_response = run_detail["tool_calls"][0]["response_json"] + assert tool_response["record_count"] == 2 + assert tool_response["total_amount"] == 420.0 + assert tool_response["scoped_to_current_user"] is True + assert tool_response["scope_label"] == "你的报销单" + + def test_orchestrator_routes_schedule_to_hermes() -> None: client, session_factory = build_client() @@ -640,6 +741,109 @@ def test_orchestrator_can_delete_all_user_conversations() -> None: assert other_count == 1 +def test_orchestrator_can_delete_current_user_single_conversation() -> None: + client, session_factory = build_client() + + first_response = client.post( + "/api/v1/orchestrator/run", + json={ + "source": "user_message", + "user_id": "single_delete_user", + "message": "查一下本周报销金额", + "context_json": {"role_codes": ["finance"]}, + }, + ) + second_response = client.post( + "/api/v1/orchestrator/run", + json={ + "source": "user_message", + "user_id": "single_delete_user", + "message": "帮我生成差旅报销草稿", + "context_json": {"role_codes": ["finance"]}, + }, + ) + + assert first_response.status_code == 200 + assert second_response.status_code == 200 + + first_conversation_id = first_response.json()["conversation_id"] + second_conversation_id = second_response.json()["conversation_id"] + + delete_response = client.delete( + f"/api/v1/orchestrator/conversations/{first_conversation_id}", + params={"user_id": "single_delete_user"}, + ) + + assert delete_response.status_code == 200 + assert delete_response.json()["deleted_count"] == 1 + + with session_factory() as db: + remaining_ids = list( + db.scalars( + select(AgentConversation.conversation_id) + .where(AgentConversation.user_id == "single_delete_user") + .order_by(AgentConversation.created_at.asc()) + ).all() + ) + assert first_conversation_id not in remaining_ids + assert second_conversation_id in remaining_ids + + +def test_orchestrator_can_delete_user_conversations_by_session_type() -> None: + client, session_factory = build_client() + + expense_response = client.post( + "/api/v1/orchestrator/run", + json={ + "source": "user_message", + "user_id": "typed_delete_user", + "message": "帮我生成差旅报销草稿", + "context_json": { + "role_codes": ["finance"], + "session_type": "expense", + }, + }, + ) + knowledge_response = client.post( + "/api/v1/orchestrator/run", + json={ + "source": "user_message", + "user_id": "typed_delete_user", + "message": "发票抬头不一致还能报销吗", + "context_json": { + "role_codes": ["finance"], + "session_type": "knowledge", + }, + }, + ) + + assert expense_response.status_code == 200 + assert knowledge_response.status_code == 200 + + delete_response = client.delete( + "/api/v1/orchestrator/conversations", + params={ + "user_id": "typed_delete_user", + "session_type": "knowledge", + }, + ) + + assert delete_response.status_code == 200 + assert delete_response.json()["deleted_count"] == 1 + + with session_factory() as db: + remaining = list( + db.scalars( + select(AgentConversation) + .where(AgentConversation.user_id == "typed_delete_user") + .order_by(AgentConversation.created_at.asc()) + ).all() + ) + assert len(remaining) == 1 + remaining_session_type = str((remaining[0].state_json or {}).get("session_type") or "").strip() or "expense" + assert remaining_session_type == "expense" + + def test_orchestrator_tool_failure_is_logged_and_degraded() -> None: client, _ = build_client()