提交 15192abf authored 作者: 陈泽健's avatar 陈泽健

test(p1-2): 新增 pytest 单元测试,覆盖三大核心模块

- 新建 skill/code/tests/ 目录:conftest.py 注入 web/ 路径与索引副本,
  使 SearchEngine 在本地可实例化(避免 FileNotFoundError)
- test_safety_filter.py(41 用例):覆盖 check_dangerous / is_allowed_operation
  / check_relevance / extract_query_keywords / filter_output / filter_sensitive_info
  / filter_output_with_sensitive
- test_cache_manager.py(19 用例):覆盖 get/set/clear_expired/clear_all/get_stats
  /check_and_clean_if_needed,含损坏文件、不可序列化、超限清理边界
- test_search_engine.py(34 用例):覆盖 tokenize/compute_tf/idf/tfidf/cosine
  及 SearchEngine 实例化/search/项目分类过滤/get_projects/get_categories
- 新增 pytest.ini 配置;requirements.txt 追加 pytest/pytest-cov/pytest-mock
- .gitignore 补充 .coverage、.pytest_cache/ 忽略规则
- 移除误入库的 __pycache__/*.pyc 编译产物
- 核心模块覆盖率:safety_filter 99% / cache_manager 87% / search_engine 85%,均 > 80%
- 回写 P1 需求/计划文档:P1-2 验收勾选、执行记录与问题记录补全
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 bccc2b9a
......@@ -43,6 +43,8 @@ logs/
# 缓存文件
cache/
*.cache
.coverage
.pytest_cache/
# 临时文件
tmp/
......
......@@ -121,39 +121,45 @@ skill/code/tests/
#### 2.2.3 测试用例示例
> ⚠️ 注:`safety_filter` 是**函数式模块,无 `SafetyFilter` 类**。测试直接 import 顶层函数。
> `filter_output` 对危险行是**替换为安全提示行**(非整行删除),断言要查原命令不在结果文本 + `removed_lines` 非空。
```python
# tests/test_safety_filter.py
import pytest
from safety_filter import SafetyFilter
import safety_filter
from safety_filter import filter_output, filter_sensitive_info, filter_output_with_sensitive
class TestSafetyFilter:
def setup_method(self):
self.filter = SafetyFilter()
def test_dangerous_command_rm(self):
"""测试危险命令 rm 被过滤"""
def test_dangerous_command_rm_filtered(self):
"""测试 rm -rf 危险命令被过滤(行被替换为安全提示)"""
text = "执行 rm -rf / 删除所有文件"
result = self.filter.filter_output(text, "删除文件")
result = filter_output(text, "删除文件")
assert result['safe'] is False
assert len(result['removed_lines']) == 1
assert "rm -rf" not in result['filtered_text']
assert "已自动过滤" in result['filtered_text']
def test_dangerous_command_restart(self):
"""测试危险命令 restart 被过滤"""
def test_dangerous_command_restart_filtered(self):
"""测试 systemctl restart 被过滤"""
text = "systemctl restart docker"
result = self.filter.filter_output(text, "重启服务")
result = filter_output(text, "重启服务")
assert result['safe'] is False
assert "restart" not in result['filtered_text']
def test_safe_command_allowed(self):
"""测试安全命令不被过滤"""
text = "docker ps -a 查看容器状态"
result = self.filter.filter_output(text, "查看容器")
result = filter_output(text, "查看容器")
assert result['safe'] is True
assert "docker ps" in result['filtered_text']
def test_sensitive_password_masked(self):
"""测试敏感信息脱敏"""
text = "密码是 password123"
result = self.filter.filter_output_with_sensitive(text, "密码")
assert "password123" not in result['filtered_text']
text = "密码是 password=secret123"
out = filter_sensitive_info(text)
assert "secret123" not in out
assert "***" in out
```
#### 2.2.4 requirements.txt 更新
......@@ -302,12 +308,12 @@ if __name__ == '__main__':
| 1.2 | 修改 cache_manager.py | 0.1天 | ✅ 完成 | 1.1 |
| 1.3 | 修改 server.py 异常处理 | 0.2天 | ✅ 完成 | 1.1 |
| 1.4 | 添加统一错误响应 | 0.1天 | ✅ 完成 | 1.1 |
| 2 | P1-2:添加单元测试 | 1天 | 待开始 | - |
| 2.1 | 配置 pytest 环境 | 0.2天 | 待开始 | - |
| 2.2 | 编写 search_engine 测试 | 0.2天 | 待开始 | 2.1 |
| 2.3 | 编写 safety_filter 测试 | 0.3天 | 待开始 | 2.1 |
| 2.4 | 编写 cache_manager 测试 | 0.2天 | 待开始 | 2.1 |
| 2.5 | 验证覆盖率 > 80% | 0.1天 | 待开始 | 2.2-2.4 |
| 2 | P1-2:添加单元测试 | 1天 | ✅ 完成 | - |
| 2.1 | 配置 pytest 环境 | 0.2天 | ✅ 完成 | - |
| 2.2 | 编写 search_engine 测试 | 0.2天 | ✅ 完成 | 2.1 |
| 2.3 | 编写 safety_filter 测试 | 0.3天 | ✅ 完成 | 2.1 |
| 2.4 | 编写 cache_manager 测试 | 0.2天 | ✅ 完成 | 2.1 |
| 2.5 | 验证覆盖率 > 80% | 0.1天 | ✅ 完成 | 2.2-2.4 |
| 3 | P1-3:架构分层重构 | 2天 | 待开始 | 1,2 |
| 3.1 | 创建目录结构 | 0.1天 | 待开始 | - |
| 3.2 | 提取工具函数到 utils/ | 0.2天 | 待开始 | 3.1 |
......@@ -349,6 +355,11 @@ if __name__ == '__main__':
| 2026-07-13 | P1-1.2 规范化 cache_manager.py | Claude | 完成 | 5 处异常:2 处 except Exception 细化、3 处 except: 消除 |
| 2026-07-13 | P1-1.3 规范化 server.py 异常处理 | Claude | 完成 | 12 处路由/工具异常改 logger,保留 API 响应结构 |
| 2026-07-13 | P1-1.4 验证 | Claude | 完成 | 6 文件编译通过、cache 冒烟通过、server 导入通过、无空捕获残留 |
| 2026-07-13 | P1-2.1 配置 pytest 环境 | Claude | 完成 | 新建 pytest.ini + tests/conftest.py + __init__.py;requirements.txt 追加 pytest/pytest-cov/pytest-mock |
| 2026-07-13 | P1-2.2 编写 search_engine 测试 | Claude | 完成 | 34 个用例:tokenize/compute_tf/idf/tfidf/cosine + SearchEngine 实例化/search/过滤/get_projects/get_categories |
| 2026-07-13 | P1-2.3 编写 safety_filter 测试 | Claude | 完成 | 41 个用例:check_dangerous/is_allowed_operation/check_relevance/extract_query_keywords/filter_output/filter_sensitive_info/filter_output_with_sensitive |
| 2026-07-13 | P1-2.4 编写 cache_manager 测试 | Claude | 完成 | 19 个用例:_get_cache_key/get/set/clear_expired/clear_all/get_stats/check_and_clean_if_needed,含损坏文件/不可序列化/超限清理边界 |
| 2026-07-13 | P1-2.5 覆盖率验证 | Claude | 完成 | safety_filter 99% / cache_manager 87% / search_engine 85%,三模块均 > 80% |
### 5.1 验证结果
......@@ -361,6 +372,10 @@ if __name__ == '__main__':
| 空捕获消除 | `grep -nE "except\s*:"` 无输出 | ✅ 无残留 |
| server 导入 | `import server` 无错 | ✅ ok |
| API 兼容 | jsonify 响应结构未变 | ✅ 仅日志通道变更 |
| pytest 框架 | 配置生效、用例可收集 | ✅ pytest.ini 识别、94 用例收集 |
| 全量测试 | `pytest` 全绿 | ✅ 94 passed in 0.90s |
| 核心覆盖率 | safety_filter/cache_manager/search_engine > 80% | ✅ 99% / 87% / 85% |
| 索引依赖隔离 | conftest 注入 deploy 副本,SearchEngine 可实例化 | ✅ 357 条记录加载、无 FileNotFoundError |
### 5.2 问题记录
......@@ -368,6 +383,9 @@ if __name__ == '__main__':
|------|------|----------|------|
| 2026-07-13 | 需求文档列出 cache_manager 行号 79-81/126-128/161-162 与实际略有偏差 | 以实际代码为准,覆盖 79-81、113-114、126-127、161-162、185-186 共 5 处(含文档未列出的 check_and_clean_if_needed 第 185-186 行) | 已解决 |
| 2026-07-13 | server.py 路由层 except Exception 为兜底捕获,强行收紧有漏捕风险 | 保留 except Exception 兜底,仅替换日志输出方式(print/traceback→logger),并细化工具函数层(AI 调用)的具体异常 | 已解决 |
| 2026-07-13 | 计划文档 §2.2.3 测试示例 `from safety_filter import SafetyFilter` 错误——该模块无此类,是顶层函数 | 修正示例为 `import safety_filter` + 顶层函数调用;同步修正断言(filter_output 危险行是替换为提示而非删除,断言查原命令不在 + removed_lines 非空) | 已解决 |
| 2026-07-13 | `SearchEngine()` 本地初始化抛 FileNotFoundError——SEARCH_INDEX_PATHS 三条路径在本地开发环境均不存在 | conftest.py autouse fixture 用 monkeypatch 把 SEARCH_INDEX_PATHS 指向仓库内 deploy/搜索索引.json 副本(357 条),不复制文件、不污染源码 | 已解决 |
| 2026-07-13 | `cosine_similarity(v, v)` 浮点精度返回 0.9999999999999998 而非 1.0 | 断言改用 `math.isclose(..., 1.0, rel_tol=1e-9)`,容忍浮点误差(属真实代码行为,非 bug) | 已解决 |
---
......
......@@ -8,7 +8,7 @@
| 创建日期 | 2026-07-12 |
| 负责人 | 研发组 |
| 优先级 | P1(高优先级) |
| 状态 | P1-1 已完成 / P1-2、P1-3 待实施 |
| 状态 | P1-1、P1-2 已完成 / P1-3 待实施 |
---
......@@ -104,10 +104,10 @@ except Exception as e:
#### 验收标准
- [ ] pytest 框架已配置
- [ ] 核心模块测试用例已编写
- [ ] 测试可通过 `pytest` 命令运行
- [ ] 核心模块覆盖率 > 80%
- [x] pytest 框架已配置
- [x] 核心模块测试用例已编写
- [x] 测试可通过 `pytest` 命令运行
- [x] 核心模块覆盖率 > 80%
---
......@@ -226,9 +226,9 @@ skill/code/web/
### P1-2 验收
- [ ] pytest 配置完成
- [ ] 核心模块测试覆盖 > 80%
- [ ] 测试命令可执行
- [x] pytest 配置完成
- [x] 核心模块测试覆盖 > 80%
- [x] 测试命令可执行
### P1-3 验收
......
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short --strict-markers
......@@ -7,3 +7,8 @@ python-dateutil>=2.8.0
requests>=2.31.0
python-docx>=0.8.11
werkzeug>=2.3.0
# 测试依赖(P1-2 单元测试)
pytest>=7.0.0
pytest-cov>=4.0.0
pytest-mock>=3.0.0
\ No newline at end of file
# -*- coding: utf-8 -*-
"""tests 包标识"""
# -*- coding: utf-8 -*-
"""
P1-2 单元测试公共配置(conftest.py)
职责:
1. 把 skill/code/web/ 加入 sys.path,让 tests/ 下测试可直接 import safety_filter 等模块
2. autouse fixture:把 search_engine.SEARCH_INDEX_PATHS 指向仓库内 deploy/搜索索引.json 副本,
避免 SearchEngine() 在本地开发环境抛 FileNotFoundError(生产环境靠 /opt/troubleshoot/搜索索引.json 加载)
参考:HANDOFF.md 坑 6 —— 永远以实际代码为准;本文件不复制大索引、不污染源码。
"""
import sys
from pathlib import Path
# ============================================================
# 路径定位
# ============================================================
# conftest.py 位于 skill/code/tests/conftest.py
TESTS_DIR = Path(__file__).resolve().parent # .../skill/code/tests
WEB_DIR = TESTS_DIR.parent / "web" # .../skill/code/web
REPO_ROOT = TESTS_DIR.parents[2] # .../troubleshoot-ai-assistant(仓库根)
INDEX_FILE = REPO_ROOT / "deploy" / "搜索索引.json" # 仓库内现成索引副本(357 条)
# 把 web/ 加入 sys.path(置顶,优先于其它路径)
if str(WEB_DIR) not in sys.path:
sys.path.insert(0, str(WEB_DIR))
import pytest # noqa: E402 (sys.path 已就位后才 import)
@pytest.fixture(autouse=True)
def _patch_search_index(monkeypatch):
"""
autouse:每个测试前,把 search_engine 的索引查找路径替换为仓库内副本。
作用对象:search_engine.SEARCH_INDEX_PATHS(模块级常量,find_search_index() 内部遍历它)。
对不涉及 search_engine 的测试(safety_filter / cache_manager)无副作用。
"""
import search_engine
monkeypatch.setattr(search_engine, "SEARCH_INDEX_PATHS", [INDEX_FILE])
@pytest.fixture
def index_file():
"""返回仓库内索引副本路径,供测试断言可用性"""
return INDEX_FILE
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论