test_harness_core.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import json
  2. from datetime import UTC, datetime
  3. import pytest
  4. from app.commands.seed import build_seed_manifest
  5. from app.core.errors import AppError
  6. from app.domains.agent.tools import build_agent_tool_registry
  7. from app.domains.catalog.repository import InMemoryCatalogRepository
  8. from app.domains.catalog.service import ProductCatalogService
  9. from app.domains.enrollment.repository import InMemoryEnrollmentRepository
  10. from app.domains.enrollment.service import EnrollmentService
  11. from app.domains.identity.models import AdminUser, H5User
  12. from app.harness.policy import HarnessPolicyEngine, ToolPolicy
  13. from app.harness.registries import PersonaRegistry, PromptRegistry, SkillRegistry
  14. from app.harness.schemas import ProductRecommendationsBlock
  15. from app.harness.tooling import ToolExecutionContext
  16. NOW = datetime(2026, 7, 26, 8, tzinfo=UTC)
  17. def test_prompt_skill_and_persona_registries_form_a_closed_contract() -> None:
  18. prompts = PromptRegistry()
  19. skills = SkillRegistry()
  20. personas = PersonaRegistry()
  21. assert prompts.ids() == ("customer", "operation")
  22. assert skills.ids() == (
  23. "enrollment-guidance",
  24. "insurance-consultation",
  25. "operation-insights",
  26. )
  27. customer = personas.get("customer")
  28. operation = personas.get("operation")
  29. assert customer.prompt_id == "customer"
  30. assert "calculate_insurance_quote" in customer.tool_names
  31. assert operation.prompt_id == "operation"
  32. assert "get_attribution_performance" in operation.tool_names
  33. for skill_id in customer.skill_ids + operation.skill_ids:
  34. skill = skills.get(skill_id)
  35. assert skill.instructions
  36. assert skill.source.name == "SKILL.md"
  37. def test_operation_persona_cannot_authorize_a_draft_tool() -> None:
  38. policy = HarnessPolicyEngine()
  39. with pytest.raises(AppError) as error:
  40. policy.authorize(
  41. persona="operation",
  42. allowed_by_persona=("calculate_insurance_quote",),
  43. tool_name="calculate_insurance_quote",
  44. policy=ToolPolicy(personas=("operation",), effect="draft"),
  45. permissions=("*",),
  46. )
  47. assert error.value.code == "AGENT_OPERATION_READ_ONLY"
  48. def test_customer_tools_use_real_catalog_and_deterministic_quote_service() -> None:
  49. catalog_repository = InMemoryCatalogRepository()
  50. manifest = build_seed_manifest()
  51. for product in manifest.products:
  52. catalog_repository.save_product(product)
  53. for version in manifest.versions:
  54. catalog_repository.save_version(version)
  55. catalog = ProductCatalogService(catalog_repository, lambda: NOW)
  56. enrollment = EnrollmentService(
  57. InMemoryEnrollmentRepository(),
  58. catalog,
  59. lambda: NOW,
  60. "x" * 32,
  61. )
  62. registry = build_agent_tool_registry(catalog, enrollment)
  63. user = H5User(
  64. id="01H5USER000000000000000001",
  65. mobile="18800000001",
  66. mobile_masked="188****0001",
  67. display_name="体验用户",
  68. status="ACTIVE",
  69. created_at=NOW,
  70. )
  71. context = ToolExecutionContext(persona="customer", h5_user=user)
  72. tools = registry.build(
  73. names=("list_available_products", "calculate_insurance_quote"),
  74. context=context,
  75. policy_engine=HarnessPolicyEngine(),
  76. )
  77. by_name = {tool.name: tool for tool in tools}
  78. products = json.loads(by_name["list_available_products"].invoke({"category": "MEDICAL"}))
  79. quote = json.loads(
  80. by_name["calculate_insurance_quote"].invoke(
  81. {
  82. "product_code": "MED-SENIOR",
  83. "plan_code": "SENIOR_STANDARD",
  84. "age": 65,
  85. "region_code": "510100",
  86. "occupation_code": "GENERAL",
  87. "relationship": "PARENT",
  88. }
  89. )
  90. )
  91. assert products["data"]["total"] == 3
  92. assert quote["data"]["premium_cents"] == 19900
  93. assert quote["blocks"][0]["type"] == "quote"
  94. assert quote["actions"][0]["type"] == "open_enrollment"
  95. assert [execution.name for execution in context.executions] == [
  96. "list_available_products",
  97. "calculate_insurance_quote",
  98. ]
  99. def test_product_recommendation_contract_accepts_enriched_plan_data() -> None:
  100. catalog_repository = InMemoryCatalogRepository()
  101. manifest = build_seed_manifest()
  102. for product in manifest.products:
  103. catalog_repository.save_product(product)
  104. for version in manifest.versions:
  105. catalog_repository.save_version(version)
  106. catalog = ProductCatalogService(catalog_repository, lambda: NOW)
  107. enrollment = EnrollmentService(
  108. InMemoryEnrollmentRepository(),
  109. catalog,
  110. lambda: NOW,
  111. "x" * 32,
  112. )
  113. registry = build_agent_tool_registry(catalog, enrollment)
  114. context = ToolExecutionContext(
  115. persona="customer",
  116. h5_user=H5User(
  117. id="01H5USER000000000000000001",
  118. mobile="18800000001",
  119. mobile_masked="188****0001",
  120. display_name="体验用户",
  121. status="ACTIVE",
  122. created_at=NOW,
  123. ),
  124. )
  125. tool = registry.build(
  126. names=("list_available_products",),
  127. context=context,
  128. policy_engine=HarnessPolicyEngine(),
  129. )[0]
  130. products = json.loads(tool.invoke({"category": "MEDICAL"}))
  131. product = next(
  132. item
  133. for item in products["data"]["items"]
  134. if item["product_code"] == "MED-SENIOR"
  135. )
  136. recommendation = ProductRecommendationsBlock(
  137. items=[
  138. {
  139. "product_id": product["product_id"],
  140. "product_code": product["product_code"],
  141. "name": product["name"],
  142. "category": product["category"],
  143. "summary": product["summary"],
  144. "product_version_id": product["product_version_id"],
  145. "plans": product["plans"],
  146. }
  147. ],
  148. )
  149. plan = recommendation.items[0].plans[0]
  150. assert plan.premium_cents == 19900
  151. assert plan.coverage_amount_cents == 100_000_000
  152. assert (plan.min_age, plan.max_age) == (50, 75)
  153. def test_operation_tool_permissions_filter_unavailable_tools() -> None:
  154. catalog_repository = InMemoryCatalogRepository()
  155. catalog = ProductCatalogService(catalog_repository, lambda: NOW)
  156. enrollment = EnrollmentService(
  157. InMemoryEnrollmentRepository(),
  158. catalog,
  159. lambda: NOW,
  160. "x" * 32,
  161. )
  162. registry = build_agent_tool_registry(catalog, enrollment)
  163. admin = AdminUser(
  164. id="01ADMIN0000000000000000001",
  165. username="sales_a",
  166. password_hash="unused",
  167. display_name="业务员A",
  168. status="ACTIVE",
  169. roles=("SALESPERSON",),
  170. permissions=("dashboard:read", "attribution:read"),
  171. data_scope="SELF",
  172. )
  173. tools = registry.build(
  174. names=PersonaRegistry().get("operation").tool_names,
  175. context=ToolExecutionContext(persona="operation", admin_user=admin),
  176. policy_engine=HarnessPolicyEngine(),
  177. )
  178. assert {tool.name for tool in tools} == {
  179. "get_operation_overview",
  180. "get_attribution_performance",
  181. "list_available_products",
  182. }