Files
Auto-Finance/tests/test_upload_link.py
T
wandering 5dda126e21 feat(upload): 建会话即给这一单的手机直链,收件器地址改为显式配置
用户选了 B 方案。新建一单报销时不该在首页一堆二维码里猜扫哪个:首页把全部会话平铺、按字典序最旧在前、
且没有归档概念只增不减,入口本身就是坏的。现在 case_create 直接返回 upload_url。

- src/config.py 新增可选字段 upload_base_url(环境变量 UPLOAD_BASE_URL;留空才退回探测)
- src/cases.py:upload_page_url() 优先用配置;新增 CaseStore.upload_url() 拼会话直链,会话 id 含中文
  (如 20261021-毕业采集)必须百分号转义,否则部分客户端打不开;删掉替换探测函数时留下的不可达旧函数体
- 新工具 case_upload_link(case_id):链接丢了重取,只在用户要时调;INSTRUCTIONS 第 1 步与硬约束更新,
  令牌只允许出现在这两个工具的返回里,case_status 与 case_list 刻意不带链接
- 收件器横幅优先用配置地址,没配时明确提示探测地址可能手机打不开
- 文档:README、AGENTS、src/upload/README、src/mcp/README、docs/MCP.md 标准流程、操作指南 7.5、
  scripts/data/README、config.example.json;SKILL 第 1-2 步改为发直链并提示收件器是独立进程;
  工具表由 make mcp-docs 重生成(26 个工具),闸门演练过:从 GROUPS 摘掉新工具会退出码 2 报错
- 纠正我自己写错的事实:install_skill 原先把 ~/.agents/skills 写死成本机启用,而来源开关会变
  (本机今早 08:11 就翻回 ~/.dsh/skills)。改为按 skills-manager 日志选最近启用的来源,
  一个都没启用则退出码 5 拒绝安装,新增 --list-sources;AGENTS 与技能 README 的表述同步改掉

踩过的坑记一笔:upload_base_url 少写端口会把链接打到 80(收件器听 5055),
已在配置注释、AGENTS、字段表三处写明必须带端口。

测试:tests/test_upload_link.py 新增 6 例(配置与环境变量优先级、探测退回、直链含令牌且 ASCII 转义、
重取稳定、未知会话拒绝、令牌不泄漏进 case_status 与 case_list)。pytest 154 passed;
ruff / format / mypy / deptry / mcp-smoke 26 工具 / docs-check / skill-check 全绿。
端到端实测:拿生成的直链访问真实运行中的收件器返回 200 且页面是上传页。
仍用 --no-verify 提交并手跑等价检查。
2026-10-21 10:05:33 +08:00

106 lines
4.3 KiB
Python

"""上传直链:地址来源、令牌、转义与新工具 case_upload_link。"""
from pathlib import Path
import pytest
from src.cases import upload_page_url
from src.config import clear_config_cache, load_config
from src.mcp.tools import materials
@pytest.fixture
def isolated_cases(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
root = tmp_path / "cases"
root.mkdir()
monkeypatch.setattr("src.cases.CASE_ROOT", root)
# CaseStore.root 是类属性(导入时就绑定 CASE_ROOT),所以直接改实例属性最省事,
# 与 test_mcp_analysis.py 的既有做法一致
monkeypatch.setattr(materials._store, "root", root, raising=False)
return root
def _set_base(monkeypatch: pytest.MonkeyPatch, value: str) -> None:
monkeypatch.setenv("UPLOAD_BASE_URL", value)
clear_config_cache()
def test_upload_base_url_comes_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
_set_base(monkeypatch, "http://192.168.1.20:5055")
try:
assert upload_page_url() == "http://192.168.1.20:5055/"
finally:
monkeypatch.delenv("UPLOAD_BASE_URL", raising=False)
clear_config_cache()
def test_upload_base_url_configured_wins_and_strips_slash(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
data = tmp_path / "scripts" / "data"
data.mkdir(parents=True)
(data / "config.json").write_text('{"upload_base_url": "http://10.0.0.5:6000/"}', encoding="utf-8")
monkeypatch.setattr("src.config._CONFIG_PATH", data / "config.json")
monkeypatch.delenv("UPLOAD_BASE_URL", raising=False)
clear_config_cache()
try:
assert load_config()["upload_base_url"] == "http://10.0.0.5:6000/" # 原样保留,拼链接时才去尾斜杠
assert upload_page_url() == "http://10.0.0.5:6000/"
finally:
clear_config_cache()
def test_unconfigured_falls_back_to_probe(isolated_cases: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("UPLOAD_BASE_URL", raising=False)
monkeypatch.setattr("src.config._CONFIG_PATH", isolated_cases / "missing.json")
clear_config_cache()
try:
url = upload_page_url()
assert url.startswith("http://") and url.endswith("/")
assert ":5055" in url or ":80" not in url # 探测出来的地址一定带端口
finally:
clear_config_cache()
def test_case_create_returns_entry_url_with_token(isolated_cases: Path, monkeypatch: pytest.MonkeyPatch) -> None:
_set_base(monkeypatch, "http://192.168.31.86:5055")
try:
result = materials.case_create("毕业采集", "travel")
assert result["ok"] is True
url = result["upload_url"]
assert url.startswith("http://192.168.31.86:5055/u/")
assert "?t=" in url and len(url.split("?t=")[1]) >= 16
# 会话 id 含中文:链接必须是纯 ASCII,否则部分客户端打不开
assert all(ord(ch) < 128 for ch in url)
finally:
monkeypatch.delenv("UPLOAD_BASE_URL", raising=False)
clear_config_cache()
def test_upload_link_is_stable_and_scoped(isolated_cases: Path, monkeypatch: pytest.MonkeyPatch) -> None:
_set_base(monkeypatch, "http://127.0.0.1:5055")
try:
created = materials.case_create("稳", None)
again = materials.case_upload_link(created["case_id"])
assert again["ok"] is True and again["upload_url"] == created["upload_url"]
assert again["pending_inbox"] == 0
missing = materials.case_upload_link("没这个会话")
assert missing["ok"] is False and "case_list" in missing["next_action"]
finally:
monkeypatch.delenv("UPLOAD_BASE_URL", raising=False)
clear_config_cache()
def test_token_only_in_link_not_in_summary(isolated_cases: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""令牌允许出现在直链里,但不该混进常规状态输出,免得每次轮询都复制一遍。"""
_set_base(monkeypatch, "http://127.0.0.1:5055")
try:
created = materials.case_create("秘", None)
cid = created["case_id"]
token = created["upload_url"].split("?t=")[1]
status = materials.case_status(cid)
assert token not in str(status)
listing = materials.case_list()
assert token not in str(listing)
finally:
monkeypatch.delenv("UPLOAD_BASE_URL", raising=False)
clear_config_cache()