| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105 |
- """附件上传:多份资料一次传入、数量上限、与 1.5 单份字段的合并。
- 背景:风量研判至少要「配风计划 + 测风报表」两份文件,而 1.5 契约里只有单个 `file`
- 字段——所以加了 `files` 多附件字段,且两者必须合并处理。
- """
- from __future__ import annotations
- from io import BytesIO
- from typing import Any
- import pytest
- from fastapi import HTTPException
- from starlette.datastructures import UploadFile
- from api import chat_routes as cr
- from agents import workspace
- @pytest.fixture
- def isolated_sandbox(tmp_path, monkeypatch):
- """把会话沙箱挪到临时目录,避免测试往仓库 data/workspaces 里写文件。"""
- monkeypatch.setattr(workspace, "session_root", lambda user, session: tmp_path / user / session)
- return tmp_path
- def _upload(name: str, payload: bytes = b"data") -> UploadFile:
- return UploadFile(filename=name, file=BytesIO(payload))
- # ---- 多附件 ---------------------------------------------------------------
- @pytest.mark.asyncio
- async def test_save_uploads_keeps_all_attachments(isolated_sandbox):
- saved = await cr._save_uploads(
- [_upload("配风计划.pdf"), _upload("测风报表.xlsx"), _upload("监控.csv")], "u", "s")
- assert [name for name, _ in saved] == ["配风计划.pdf", "测风报表.xlsx", "监控.csv"]
- paths = [path for _, path in saved]
- assert all(path.startswith("/workspace/uploads/") for path in paths)
- assert len(set(paths)) == 3, "同名文件不能互相覆盖"
- for path in paths:
- real = isolated_sandbox / "u" / "s" / path.lstrip("/")
- assert real.is_file() and real.read_bytes() == b"data"
- @pytest.mark.asyncio
- async def test_save_uploads_rejects_too_many_files(isolated_sandbox, monkeypatch):
- monkeypatch.setattr(cr, "MAX_UPLOAD_FILES", 2)
- with pytest.raises(HTTPException) as excinfo:
- await cr._save_uploads([_upload(f"f{i}.txt") for i in range(3)], "u", "s")
- assert excinfo.value.status_code == 400
- assert excinfo.value.detail["code"] == "too_many_files"
- # 先卡数量再落盘:被拒时不应留下半截文件
- assert not list((isolated_sandbox / "u" / "s" / "workspace" / "uploads").glob("*"))
- @pytest.mark.asyncio
- async def test_save_uploads_accepts_empty_list(isolated_sandbox):
- assert await cr._save_uploads([], "u", "s") == []
- def test_inject_all_lists_every_upload():
- ref: dict[str, Any] = {"reference": None, "forced_skill": None}
- single = cr._inject_all("看一下", "u", "full", ref, [("a.xlsx", "/workspace/uploads/a.xlsx")])
- assert "「a.xlsx」" in single and "1 个文件" not in single
- multi = cr._inject_all("看一下", "u", "full", ref, [
- ("a.pdf", "/workspace/uploads/a.pdf"), ("b.xlsx", "/workspace/uploads/b.xlsx")])
- assert "2 个文件" in multi
- for token in ("a.pdf", "b.xlsx", "/workspace/uploads/a.pdf", "/workspace/uploads/b.xlsx"):
- assert token in multi
- assert "不要在文件系统里搜索别处的资料" in multi
- @pytest.mark.asyncio
- async def test_chat_merges_single_file_field_and_files_field(monkeypatch):
- """1.5 的 file 与 2.0 的 files 必须合并处理,不能只认一个。"""
- captured: dict[str, Any] = {}
- async def fake_pipeline(**kwargs):
- captured.update(kwargs)
- return "stub"
- monkeypatch.setattr(cr, "_run_chat_pipeline", fake_pipeline)
- result = await cr.chat(message="研判", session_id=None, thread_id=None, mode=None,
- file=_upload("单份.pdf"),
- files=[_upload("甲.xlsx"), _upload("乙.xlsx")],
- user_name=None, request=None, request_id=None)
- assert result == "stub"
- names = [item.filename for item in captured["attachments"]]
- assert names == ["单份.pdf", "甲.xlsx", "乙.xlsx"]
- assert captured["user_name"] == "default"
- @pytest.mark.asyncio
- async def test_chat_without_attachments_passes_empty_list(monkeypatch):
- captured: dict[str, Any] = {}
- async def fake_pipeline(**kwargs):
- captured.update(kwargs)
- return "stub"
- monkeypatch.setattr(cr, "_run_chat_pipeline", fake_pipeline)
- await cr.chat(message="随便聊聊", session_id=None, thread_id=None, mode=None,
- file=None, files=[], user_name=None, request=None, request_id=None)
- assert captured["attachments"] == []
|