# 搜索引擎测试流水线指南

本文档是测试套件的**权威入口**，涵盖目录约定、运行方式、回归锚点矩阵、以及手动
联调脚本的分工。任何与这里不一致的历史文档（例如提到 `tests/unit/` 或
`scripts/start_test_environment.sh`）都是过期信息，以本文为准。

## 1. 测试目录与分层

```
tests/
├── conftest.py              # 只做 sys.path 注入；不再维护全局 fixture
├── ci/                      # API/服务契约（FastAPI TestClient + 全 fake 依赖）
│   └── test_service_api_contracts.py
├── manual/                  # 需真实服务才能跑的联调脚本，pytest 默认不 collect
│   ├── test_build_docs_api.py
│   ├── test_cnclip_service.py
│   └── test_facet_api.py
└── test_*.py                # 子系统单测（全部自带 fake，无外部依赖）
```

关键约束（写在 `pytest.ini` 里，不要另起分支）：

- `testpaths = tests`，`norecursedirs = tests/manual`；
- `--strict-markers`：所有 marker 必须先在 `pytest.ini::markers` 登记；
- 测试**不得**依赖真实 ES / DeepL / LLM 服务。需要外部依赖的脚本请放 `tests/manual/`。

## 2. 运行方式

| 场景 | 命令 | 覆盖范围 |
|------|------|----------|
| CI 门禁（每次提交） | `./scripts/run_ci_tests.sh` | `tests/ci` + `contract` marker + `search ∧ regression` |
| 发版 / 大合并前 | `./scripts/run_regression_tests.sh` | 所有 `@pytest.mark.regression` |
| 子系统子集 | `SUBSYSTEM=search ./scripts/run_regression_tests.sh` | 指定子系统的 regression 锚点 |
| 全量（含非回归） | `python -m pytest tests/ -q` | 全部自动化用例 |
| 手动联调 | `python tests/manual/<script>.py` | 需提前起对应服务 |

## 3. Marker 体系与回归锚点矩阵

marker 定义见 `pytest.ini`。每个测试文件通过模块级 `pytestmark` 贴标，同时
属于 `regression` 的用例构成“**回归锚点集合**”。

| 子系统 marker | 关键文件（锚点） | 保护的行为 |
|---------------|------------------|------------|
| `contract`    | `tests/ci/test_service_api_contracts.py` | Search / Indexer / Embedding / Reranker / Translation 的 HTTP 契约 |
| `search`      | `test_search_rerank_window.py`, `test_es_query_builder.py`, `test_es_query_builder_text_recall_languages.py` | Searcher 主路径、排序 / 召回、keywords 副 combined_fields、多语种 |
| `query`       | `test_query_parser_mixed_language.py`, `test_tokenization.py` | 中英混合解析、HanLP 分词、language detect |
| `intent`      | `test_style_intent.py`, `test_product_title_exclusion.py`, `test_sku_intent_selector.py` | 风格意图、商品标题排除、SKU 选型 |
| `rerank`      | `test_rerank_client.py`, `test_rerank_query_text.py`, `test_rerank_provider_topn.py`, `test_reranker_server_topn.py`, `test_reranker_dashscope_backend.py`, `test_reranker_qwen3_gguf_backend.py` | 粗排 / 精排 / topN / 后端切换 |
| `embedding`   | `test_embedding_pipeline.py`, `test_embedding_service_limits.py`, `test_embedding_service_priority.py`, `test_cache_keys.py` | 文本/图像向量客户端、inflight limiter、优先级队列、缓存 key |
| `translation` | `test_translation_deepl_backend.py`, `test_translation_llm_backend.py`, `test_translation_local_backends.py`, `test_translator_failure_semantics.py` | DeepL / LLM / 本地回退、失败语义 |
| `indexer`     | `test_product_enrich_partial_mode.py`, `test_process_products_batching.py`, `test_llm_enrichment_batch_fill.py` | LLM Partial Mode、batch 拆分、空结果补位 |
| `suggestion`  | `test_suggestions.py` | 建议索引构建 |
| `eval`        | `test_eval_metrics.py`（regression） + `test_search_evaluation_datasets.py` / `test_eval_framework_clients.py`（非 regression） | NDCG / ERR 指标、数据集加载、评估客户端 |

> 任何新写的子系统单测，都应该在顶部加 `pytestmark = [pytest.mark.<子系统>, pytest.mark.regression]`。
> 不贴 `regression` 的测试默认**不会**被 `run_regression_tests.sh` 选中，请谨慎决定。

## 4. 当前覆盖缺口（跟踪中）

以下场景目前没有被 `regression` 锚点覆盖，优先级从高到低：

1. **`api/routes/search.py` 的请求参数映射**：`QueryParser.parse(...)` 透传是否完整（目前只有 `tests/ci` 间接覆盖）。
2. **`indexer/document_transformer.py` 的端到端转换**：从 MySQL 行到 ES doc 的 snapshot 对比。
3. **`config/loader.py` 加载多租户配置**：含继承 / override 的合并规则。
4. **`search/searcher.py::_build_function_score`**：function_score 装配。
5. **Facet 聚合 / disjunctive 过滤**。
6. **图像搜索主路径**（`search/image_searcher.py`）。

补齐时记得同步贴 `regression` + 对应子系统 marker，并在本表删除条目。

## 5. 手动联调：索引文档构建流水线

除自动化测试外，联调/问题排查时建议走一遍“**MySQL → ES doc**”链路，确保字段与 mapping
与查询逻辑对齐。

```bash
cd /home/tw/saas-search
./scripts/stop.sh                  # 停掉已有进程（可选）
./scripts/start_indexer.sh         # 启动 indexer 服务，默认端口 6004

curl -X POST "http://127.0.0.1:6004/indexer/build-docs-from-db" \
  -H "Content-Type: application/json" \
  -d '{ "tenant_id": "170", "spu_ids": ["223167"] }'
```

返回中 `docs[0]` 即当前代码构造的 ES doc（与 `mappings/search_products.json` 对齐）。
与真实 ES 数据对比的查询参考 `docs/常用查询 - ES.md`；若字段不一致，按以下路径定位：

- `indexer/document_transformer.py` — 文档构造逻辑
- `indexer/incremental_service.py` — 增量查库逻辑
- `logs/indexer.log` — 索引日志

## 6. 编写测试的约束（与 `开发原则` 对齐）

- **fail fast**：测试输入不合法时应直接抛错，不用 `if ... return`；不要用 `try/except` 吃掉异常再 `assert not exception`。
- **不做兼容双轨**：用例对准当前实现，不为历史行为保留“旧 assert”。若确有外部兼容性（例如 API 上标注 Deprecated 的字段），在 `tests/ci` 里单独写**契约**用例并注明 Deprecated。
- **外部依赖全 fake**：凡是依赖 HTTP / Redis / ES / LLM 的测试必须注入 fake stub，否则归入 `tests/manual/`。
- **一处真相**：共享 fixture 如果超过 2 个文件使用，放 `tests/conftest.py`；只给 1 个文件用就放在该文件内。避免再次出现全库无人引用的 dead fixture。
