feat: enhance Milkdown editor and file system functionality

- Normalize line endings in Markdown export for DOCX files.
- Improve selection serialization to Markdown with better handling of empty documents.
- Add a new `updateFile` function to the file system for updating file properties.
- Introduce video transcoding capabilities using FFmpeg, supporting various video formats.
- Update AGENTS.md for clearer plugin structure and responsibilities.
- Add scoped styles for TreeNodeItem component to improve UI consistency.
- Implement cross-origin isolation headers in Vite configuration for enhanced security.
- Remove obsolete test_cross.py file.
This commit is contained in:
2026-05-01 20:55:02 +08:00
parent 52ade88840
commit 70152c61b1
43 changed files with 3911 additions and 1373 deletions
+52 -35
View File
@@ -1,45 +1,62 @@
OVERVIEW: pytest 测试套件,覆盖率要求 90%
STRUCTURE
- test_*.py - 各模块测试
- run_tests.py - 测试执行脚本(unit/integration/all
- simulate_macos.py - macOS 环境模拟
- TESTING_GUIDE.md - 测试指南文档
# Backend Tests 测试指引
WHERE TO LOOK
表格
本文件适用于 backend/tests/ 下的测试和测试脚本。
| Area | Path |
|---|---|
| 单元测试 | backend/tests/ |
| 集成测试 | backend/tests/ |
| 测试执行脚本 | backend/tests/run_tests.py |
| macOS 模拟 | backend/tests/simulate_macos.py |
| 测试指南 | backend/tests/TESTING_GUIDE.md |
## 测试入口
运行命令:
- pytest - 运行所有测试
- python backend/tests/run_tests.py unit - 单元测试
- python backend/tests/run_tests.py integration - 集成测试
- pytest.ini 指定默认测试目录为 backend/tests,并设置后端覆盖率门槛为 90%。
- run_tests.py 提供 unit、integration、simulate、all 几种快捷入口。
- 默认优先使用 pytest 跑窄测试;只有在需要脚本封装参数时再用 run_tests.py。
测试命名约定:test_*.py、Test* 类、test_* 函数
## 测试分布
ANTI-PATTERNS:删除测试以通过覆盖率
- test_main_endpoints.py:主 API 路由行为
- test_main_cancel.py:补全取消和任务生命周期
- test_prompt.py、test_prompt_extended.pyPrompt 上下文与规则
- test_llm.py、test_llm_extended.pyLLM 包装层
- test_geoip.pyGeoIP 逻辑
- test_tts_asr_*.pyTTS 相关与历史 TTS/ASR 面
- simulate_macos.py:历史模拟脚本
- quick_verify.py、verify_cross.py、play_audio.py:人工验证或辅助脚本
验证
- 保证测试覆盖率≥90% 时,报告合格
- 使用 CI 运行 pytest,确保通过率
## 常用命令
注意事项
- 不要重复父目录内容
- 不要超过 60 行
- pytest
- pytest backend/tests/test_main_endpoints.py -v
- pytest backend/tests/test_main_cancel.py -v
- pytest backend/tests/test_prompt.py -v
- pytest backend/tests/test_llm.py -v
- python backend/tests/run_tests.py unit
- python backend/tests/run_tests.py integration --url http://localhost:8001 --key your-secret-key-here
测试应尽量独立,不要依赖全局状态
- 运行单元测试时应使用 unit 标签
- 运行集成测试时应使用 integration 标签
## 测试原则
区分环境
- unit 测试尽量快速、稳定
- integration 测试应覆盖接口和数据库交互
- 优先跑与改动直接对应的窄测试,不要动不动全量跑。
- 单元测试尽量 mock 掉外部依赖,不要直连真实 Ollama。
- 涉及 main.py 时,优先用 monkeypatch 或 fake 对象替代:
- call_ollama
- call_vlm_ocr
- MarkItDown
- GeoIP 查询
- TTS 模型加载
- 测试要保持确定性,不依赖全局状态、环境顺序或人工输入。
维护
- 如扩展新模块,优先增加 test_*.py 文件并在其中添加对应的测试类和方法
## 容易误判的点
- 覆盖率门槛是针对多个 backend 模块一起算的,改核心文件时,即使单测通过也可能因为覆盖率不够失败。
- htmlcov、.pytest_cache、api_performance_report.md 属于生成产物,不是需要维护的源码。
- 这一目录里有一批 TTS/ASR 测试和说明明显继承自旧实现;当它们与当前 backend/tts_asr.py 冲突时,不要默认代码错了,先确认目标产品面。
- integration 脚本通常假设本地服务在 http://localhost:8001,且默认 API Key 还是占位值。
## 改动定位建议
- 路由返回值不对:先看 test_main_endpoints.py 和 test_main_cancel.py
- Prompt 规则不对:先看 test_prompt.py 和 test_prompt_extended.py
- Ollama 调用包装不对:先看 test_llm.py 和 test_llm_extended.py
- TTS 面变化:先确认当前 backend/tts_asr.py 是不是仍然以旧文档描述为目标,再决定修测试还是修实现
## 维护原则
- 新增后端行为时,优先给对应模块补测试,不要只依赖全量回归。
- 如果变更的是历史 TTS/ASR 面,先把“当前规范是什么”确定下来,再批量修测试。
- 如果覆盖率策略变化,记得同步这个文件,而不是只改 pytest.ini。
-37
View File
@@ -1,37 +0,0 @@
import asyncio
import base64
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from backend.tts_asr import _tts_sync_with_retry
async def play_audio():
print("生成测试音频中,请稍候...")
test_text = "这是一段用以测试新语音模型音质的中文合成音频。"
try:
audio_bytes, sr = await _tts_sync_with_retry(test_text, rate=1.0)
# 保存到本地文件
wav_path = os.path.join(os.path.dirname(__file__), "test_audio.wav")
with open(wav_path, "wb") as f:
f.write(audio_bytes)
print(f"音频已生成并保存到: {wav_path}")
print("正在尝试在 macOS 上播放...")
# Mac OS 的播放命令
os.system(f"afplay '{wav_path}'")
print("播放完成。")
except Exception as e:
import traceback
traceback.print_exc()
print(f"音频生成失败: {str(e)}")
if __name__ == "__main__":
asyncio.run(play_audio())
-1
View File
@@ -12,7 +12,6 @@ macOS环境模拟测试工具
"""
import argparse
import importlib
import os
import platform
import sys
Binary file not shown.
-1
View File
@@ -1,5 +1,4 @@
import sys
import os
import types
import pathlib
import pytest
+8 -4
View File
@@ -1,7 +1,7 @@
import os
import sys
import base64
import asyncio
import types
import pytest
from unittest.mock import MagicMock
from fastapi.testclient import TestClient
@@ -11,6 +11,10 @@ BACKEND_DIR = os.path.abspath(os.path.join(CURRENT_DIR, ".."))
if BACKEND_DIR not in sys.path:
sys.path.insert(0, BACKEND_DIR)
fake_tts_asr = types.ModuleType("tts_asr")
fake_tts_asr.register_tts_asr_routes = lambda app: None
sys.modules.setdefault("tts_asr", fake_tts_asr)
import main # type: ignore
API_KEY = main.API_KEY
@@ -61,13 +65,13 @@ def test_sanitize_markdown_strips_img_tag():
assert "<img" not in main._sanitize_converted_markdown("<img src='x.png'/>")
def test_sanitize_markdown_collapse_newlines():
assert main._sanitize_converted_markdown("a\n\n\nb\n\n\n\nc") == "a\n\nb\n\nc"
def test_sanitize_markdown_preserves_extra_newlines():
assert main._sanitize_converted_markdown("a\n\n\nb\n\n\n\nc") == "a\n\n\nb\n\n\n\nc"
def test_sanitize_markdown_normalize_crlf():
result = main._sanitize_converted_markdown("line1\r\nline2\r\n")
assert "line1\nline2" in result
assert result == "line1\nline2\n"
assert "\r" not in result
+7
View File
@@ -28,6 +28,13 @@ def test_prompt_builds_system_and_user():
assert "MERMAID_CONTEXT" in user_prompt
assert "PREFIX_ENDS_WITH_NEWLINE" in user_prompt
assert "SUFFIX_STARTS_WITH_NEWLINE" in user_prompt
assert "actual line breaks" in system_prompt
assert "start OUTPUT on a new line" in system_prompt
assert "Use real line breaks instead of spelled-out escape sequences" in user_prompt
assert "make the first character of OUTPUT a real newline" in user_prompt
assert "make the last character of OUTPUT a real newline" in user_prompt
assert "start output with \\n" not in user_prompt
assert "Use single \\n" not in system_prompt
def test_cursor_in_fence_detection():
-1
View File
@@ -1,5 +1,4 @@
import sys
import os
import re
from pathlib import Path
@@ -15,9 +15,7 @@ TTS/ASR模块集成测试
python backend/tests/test_tts_asr_integration.py --test config
"""
import asyncio
import base64
import json
import os
import sys
import time
+1 -2
View File
@@ -12,8 +12,7 @@ TTS/ASR模块单元测试
import os
import sys
import unittest
from unittest.mock import Mock, MagicMock, patch
import tempfile
from unittest.mock import patch
import numpy as np
# 确保可以导入backend和tts_asr模块
-56
View File
@@ -1,56 +0,0 @@
import asyncio
import base64
import os
import sys
# 确保能找到backend模块
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
from backend.tts_asr import _tts_sync_with_retry, _load_asr_pipeline_with_retry, _get_asr_pipeline
async def verify_tts_asr_cross():
print("====================================")
print(" 开始严格交叉验证: TTS 生成 -> ASR 解析")
print("====================================")
test_text = "苹果设备支持离线大模型运算"
print(f"\n[1] 正在调用 TTS 引擎 (suno/bark-small)...")
print(f"目标文本: '{test_text}'")
try:
# TTS生成
audio_bytes, sr = await _tts_sync_with_retry(test_text, rate=1.0)
print(f"-> TTS 成功生成音频数据,大小: {len(audio_bytes)} Bytes, 采样率: {sr}Hz")
except Exception as e:
print(f"-> TTS 失败: {str(e)}")
sys.exit(1)
print("\n[2] 正在调用 ASR 引擎 (Whisper)...")
try:
loaded = await _load_asr_pipeline_with_retry()
if not loaded:
print("-> ASR 核心加载失败!")
sys.exit(1)
print("-> ASR 加载成功,开始解析音频...")
# 将生成的wav bytes传递给ASR进行语音识别
asr_pipeline = _get_asr_pipeline()
result = asr_pipeline(audio_bytes, generate_kwargs={"task": "transcribe"})
recognized_text = result.get('text', '')
print(f"-> ASR 识别结果: '{recognized_text.strip()}'")
if len(recognized_text.strip()) > 0:
print("\n结论: ✅ 验证成功!TTS和ASR模块功能链路闭环完成。")
else:
print("\n结论: ❌ ASR输出为空字符,闭环失败。")
sys.exit(1)
except Exception as e:
import traceback
traceback.print_exc()
print(f"-> ASR 分析阶段失败: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(verify_tts_asr_cross())