test_shaanxi_gateway.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. """取数网关协议、多业务身份、文件下载及旧SDK协议兼容性。"""
  2. import asyncio
  3. from dataclasses import replace
  4. import hashlib
  5. import json
  6. import httpx
  7. import pytest
  8. from db import business_store as store, db_conn
  9. from extensions import registry as extensions
  10. from extensions.registry import ExtensionRegistry, GatewayBinding
  11. from extensions.main_fan.schemas import MainFanInput
  12. from extensions.shaanxi.client import GatewayClient, GatewayError, GatewayStopped
  13. from extensions.shaanxi.settings import GatewaySettings
  14. from runtime.business_models import JudgmentResult, RemoteFile
  15. from runtime.business_tasks import BusinessTaskController
  16. from runtime.engine import ExecutionResult
  17. def settings(**kwargs):
  18. return GatewaySettings("https://gateway.invalid", "gateway-secret", "https://files.invalid", **kwargs)
  19. def response(data=None, **kwargs):
  20. return httpx.Response(200, json={"code": 200, "success": True, "data": data}, **kwargs)
  21. @pytest.mark.asyncio
  22. async def test_concurrent_calls_keep_agent_identity_and_registration_contract():
  23. seen = []
  24. async def handler(request):
  25. seen.append((request.url.path, request.headers["X-Agent-Code"], json.loads(request.content) if request.content else None))
  26. assert request.headers["Authorization"] == "Bearer gateway-secret"
  27. await asyncio.sleep(0)
  28. return response([])
  29. client = GatewayClient(settings(), transport=httpx.MockTransport(handler))
  30. try:
  31. await asyncio.gather(client.query("agent-one", "monitor_platform", "alarms"),
  32. client.query("agent-two", "monitor_platform", "alarm_detail"))
  33. assert {item[1] for item in seen} == {"agent-one", "agent-two"}
  34. original = extensions.load_extensions().business("main_fan")
  35. profile = replace(original, gateway=GatewayBinding("fan-code", "fan-check", topics=()))
  36. await client.register(profile)
  37. assert seen[-1][2]["agentCode"] == "fan-code"
  38. assert seen[-1][2]["topics"] == []
  39. assert seen[-1][2]["enabled"] is True
  40. finally:
  41. await client.close()
  42. @pytest.mark.asyncio
  43. async def test_http_200_business_failure_and_nested_proxy_status_are_rejected():
  44. answers = [httpx.Response(200, json={"code": 500, "success": False}),
  45. response({"status_code": 500, "body": {}}),
  46. response({"status_code": 200, "body": {"code": 500}}),
  47. httpx.Response(500, json={"detail": "智能体已停止"})]
  48. client = GatewayClient(settings(), transport=httpx.MockTransport(lambda r: answers.pop(0)))
  49. try:
  50. with pytest.raises(GatewayError):
  51. await client.push("one", {"taskId": "x"})
  52. with pytest.raises(GatewayError) as error:
  53. await client.proxy("one", "/query")
  54. assert error.value.retryable
  55. with pytest.raises(GatewayError):
  56. await client.proxy("one", "/query")
  57. with pytest.raises(GatewayStopped):
  58. await client.token("one")
  59. finally:
  60. await client.close()
  61. @pytest.mark.asyncio
  62. async def test_download_uses_platform_headers_and_rotates_expired_token(tmp_path):
  63. tokens = ["expired", "fresh"]
  64. downloads = []
  65. def handler(request):
  66. if request.url.host == "gateway.invalid":
  67. assert request.headers["X-Agent-Code"] == "fan-code"
  68. return response({"access_token": tokens.pop(0), "client_id": "platform-client"})
  69. downloads.append(request.headers["Authorization"])
  70. assert request.headers["clientid"] == "platform-client"
  71. assert "X-Agent-Code" not in request.headers
  72. if downloads[-1] == "Bearer expired":
  73. return httpx.Response(401)
  74. return httpx.Response(200, content=b"%PDF-test", headers={"content-type": "application/pdf"})
  75. client = GatewayClient(settings(), transport=httpx.MockTransport(handler))
  76. try:
  77. source = RemoteFile(file_id="r1", version="v1", filename="report.pdf", path="/download", params={"id": "r1"})
  78. metadata = await client.download("fan-code", source, tmp_path / "report.pdf")
  79. assert downloads == ["Bearer expired", "Bearer fresh"]
  80. assert metadata["sha256"] == hashlib.sha256(b"%PDF-test").hexdigest()
  81. assert metadata["version"] == "v1"
  82. assert not (tmp_path / "report.pdf.part").exists()
  83. finally:
  84. await client.close()
  85. @pytest.mark.asyncio
  86. @pytest.mark.parametrize("failure", ["same_token", "too_large", "json"])
  87. async def test_bad_download_does_not_leave_a_file(tmp_path, failure):
  88. def handler(request):
  89. if request.url.host == "gateway.invalid":
  90. return response({"access_token": "old", "client_id": "c"})
  91. if failure == "same_token":
  92. return httpx.Response(401)
  93. if failure == "json":
  94. return httpx.Response(200, json={"code": 401})
  95. return httpx.Response(200, content=b"oversized")
  96. client = GatewayClient(settings(max_file_bytes=4), transport=httpx.MockTransport(handler))
  97. target = tmp_path / "report.pdf"
  98. try:
  99. with pytest.raises(GatewayError):
  100. await client.download("one", RemoteFile(file_id="r", version="v", filename="r.pdf", path="/file"), target)
  101. assert not target.exists()
  102. assert not target.with_suffix(".pdf.part").exists()
  103. finally:
  104. await client.close()
  105. @pytest.mark.asyncio
  106. async def test_sse_frames_and_stop_signal():
  107. frames = 'event: fan.task\nid: event-1\ndata: {"data":{"mineCode":"m"}}\n\n: keepalive\n\n'
  108. responses = [httpx.Response(200, text=frames, headers={"content-type": "text/event-stream"}), httpx.Response(403)]
  109. client = GatewayClient(settings(), transport=httpx.MockTransport(lambda r: responses.pop(0)))
  110. try:
  111. events = [item async for item in client.events("fan-code")]
  112. assert events[0] is None
  113. assert events[1].message_id == "event-1" and events[1].topic == "fan.task"
  114. with pytest.raises(GatewayStopped):
  115. async for _ in client.events("fan-code"):
  116. pass
  117. finally:
  118. await client.close()
  119. @pytest.mark.asyncio
  120. async def test_rate_limit_hint_is_preserved_and_incomplete_receipt_rejected():
  121. replies = [httpx.Response(429, headers={"Retry-After": "120"}), response({})]
  122. client = GatewayClient(settings(), transport=httpx.MockTransport(lambda r: replies.pop(0)))
  123. try:
  124. with pytest.raises(GatewayError) as limited:
  125. await client.push("fan-code", {"taskId": "one"})
  126. assert limited.value.retryable and limited.value.retry_after == 120
  127. with pytest.raises(GatewayError, match="回执"):
  128. await client.push("fan-code", {"taskId": "one"})
  129. finally:
  130. await client.close()
  131. @pytest.mark.asyncio
  132. async def test_downloaded_material_is_visible_to_business_in_its_own_workspace(monkeypatch, tmp_path):
  133. from agents import workspace
  134. from extensions.main_fan.tools import inspect_main_fan_materials
  135. base = extensions.load_extensions()
  136. original = base.business_agents["main_fan"]
  137. monkeypatch.setitem(base.business_agents, "main_fan", replace(original, gateway=GatewayBinding("fan-code", "fan-check")))
  138. monkeypatch.setattr(workspace.config, "ROOT", tmp_path)
  139. def handler(request):
  140. if request.url.host == "gateway.invalid":
  141. return response({"access_token": "file-token", "client_id": "client-id"})
  142. return httpx.Response(200, content=b"report", headers={"content-type": "application/pdf"})
  143. received = []
  144. async def executor(**kwargs):
  145. received.append(kwargs)
  146. value = inspect_main_fan_materials([{"path": "uploaded"}], "m", "矿")
  147. return ExecutionResult("run", kwargs["session_id"], "success", "", "", {}, [], [], value)
  148. client = GatewayClient(settings(), transport=httpx.MockTransport(handler))
  149. try:
  150. source = {"file_id": "f", "version": "1", "filename": "report.pdf", "path": "/download"}
  151. job, _ = store.enqueue("main_fan", "file-owner", {"mine_code": "m", "mine_name": "矿", "materials": []}, [source])
  152. await BusinessTaskController(client, executor=executor).run_job(store.claim("main_fan"))
  153. saved = store.get(job["id"])
  154. assert saved["status"] == "needs_materials"
  155. assert "/workspace/uploads/" in received[0]["message"]
  156. metadata = db_conn.query_one("SELECT * FROM business_files WHERE job_id = ?", (job["id"],))
  157. assert metadata["sha256"] == hashlib.sha256(b"report").hexdigest()
  158. assert metadata["local_path"].startswith(str(workspace.session_root("file-owner", saved["session_id"])))
  159. finally:
  160. await client.close()