test_chat_uploads.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. """附件上传:多份资料一次传入、数量上限、与 1.5 单份字段的合并。
  2. 背景:风量研判至少要「配风计划 + 测风报表」两份文件,而 1.5 契约里只有单个 `file`
  3. 字段——所以加了 `files` 多附件字段,且两者必须合并处理。
  4. """
  5. from __future__ import annotations
  6. from io import BytesIO
  7. from typing import Any
  8. import pytest
  9. from fastapi import HTTPException
  10. from starlette.datastructures import UploadFile
  11. from api import chat_routes as cr
  12. from agents import workspace
  13. @pytest.fixture
  14. def isolated_sandbox(tmp_path, monkeypatch):
  15. """把会话沙箱挪到临时目录,避免测试往仓库 data/workspaces 里写文件。"""
  16. monkeypatch.setattr(workspace, "session_root", lambda user, session: tmp_path / user / session)
  17. return tmp_path
  18. def _upload(name: str, payload: bytes = b"data") -> UploadFile:
  19. return UploadFile(filename=name, file=BytesIO(payload))
  20. # ---- 多附件 ---------------------------------------------------------------
  21. @pytest.mark.asyncio
  22. async def test_save_uploads_keeps_all_attachments(isolated_sandbox):
  23. saved = await cr._save_uploads(
  24. [_upload("配风计划.pdf"), _upload("测风报表.xlsx"), _upload("监控.csv")], "u", "s")
  25. assert [name for name, _ in saved] == ["配风计划.pdf", "测风报表.xlsx", "监控.csv"]
  26. paths = [path for _, path in saved]
  27. assert all(path.startswith("/workspace/uploads/") for path in paths)
  28. assert len(set(paths)) == 3, "同名文件不能互相覆盖"
  29. for path in paths:
  30. real = isolated_sandbox / "u" / "s" / path.lstrip("/")
  31. assert real.is_file() and real.read_bytes() == b"data"
  32. @pytest.mark.asyncio
  33. async def test_save_uploads_rejects_too_many_files(isolated_sandbox, monkeypatch):
  34. monkeypatch.setattr(cr, "MAX_UPLOAD_FILES", 2)
  35. with pytest.raises(HTTPException) as excinfo:
  36. await cr._save_uploads([_upload(f"f{i}.txt") for i in range(3)], "u", "s")
  37. assert excinfo.value.status_code == 400
  38. assert excinfo.value.detail["code"] == "too_many_files"
  39. # 先卡数量再落盘:被拒时不应留下半截文件
  40. assert not list((isolated_sandbox / "u" / "s" / "workspace" / "uploads").glob("*"))
  41. @pytest.mark.asyncio
  42. async def test_save_uploads_accepts_empty_list(isolated_sandbox):
  43. assert await cr._save_uploads([], "u", "s") == []
  44. def test_inject_all_lists_every_upload():
  45. ref: dict[str, Any] = {"reference": None, "forced_skill": None}
  46. single = cr._inject_all("看一下", "u", "full", ref, [("a.xlsx", "/workspace/uploads/a.xlsx")])
  47. assert "「a.xlsx」" in single and "1 个文件" not in single
  48. multi = cr._inject_all("看一下", "u", "full", ref, [
  49. ("a.pdf", "/workspace/uploads/a.pdf"), ("b.xlsx", "/workspace/uploads/b.xlsx")])
  50. assert "2 个文件" in multi
  51. for token in ("a.pdf", "b.xlsx", "/workspace/uploads/a.pdf", "/workspace/uploads/b.xlsx"):
  52. assert token in multi
  53. assert "不要在文件系统里搜索别处的资料" in multi
  54. @pytest.mark.asyncio
  55. async def test_chat_merges_single_file_field_and_files_field(monkeypatch):
  56. """1.5 的 file 与 2.0 的 files 必须合并处理,不能只认一个。"""
  57. captured: dict[str, Any] = {}
  58. async def fake_pipeline(**kwargs):
  59. captured.update(kwargs)
  60. return "stub"
  61. monkeypatch.setattr(cr, "_run_chat_pipeline", fake_pipeline)
  62. result = await cr.chat(message="研判", session_id=None, thread_id=None, mode=None,
  63. file=_upload("单份.pdf"),
  64. files=[_upload("甲.xlsx"), _upload("乙.xlsx")],
  65. user_name=None, request=None, request_id=None)
  66. assert result == "stub"
  67. names = [item.filename for item in captured["attachments"]]
  68. assert names == ["单份.pdf", "甲.xlsx", "乙.xlsx"]
  69. assert captured["user_name"] == "default"
  70. @pytest.mark.asyncio
  71. async def test_chat_without_attachments_passes_empty_list(monkeypatch):
  72. captured: dict[str, Any] = {}
  73. async def fake_pipeline(**kwargs):
  74. captured.update(kwargs)
  75. return "stub"
  76. monkeypatch.setattr(cr, "_run_chat_pipeline", fake_pipeline)
  77. await cr.chat(message="随便聊聊", session_id=None, thread_id=None, mode=None,
  78. file=None, files=[], user_name=None, request=None, request_id=None)
  79. assert captured["attachments"] == []