fix: 批量匹配跳过画像待补录教师 + render-html 按行重建 + 金标准 fixtures 业务化(P0+P1)

- run_match 新增 skip_incomplete:批量缺省跳过画像不完整教师并记录 skipped,指定单教师仍 PROFILE_INCOMPLETE;汇总页出"画像待补录"行
- render-html 补 --report-id 行上下文(inputMapping $row.$pk),CLI 容错解析回退最近 10 份
- report_service 日志改容错 _log(),修复未 setup 时单跑测试崩溃的顺序依赖
- fixtures 替换为教师画像/赛道/期望结论金标准样例,新增 sample_tracks.json
- 新增 4 个批量跳过用例(314 项,除 3 个已知 jsonschema 环境失败外全绿)
- tests/README 增补本机无共享 runtime 的测试方式;TUTORIAL/ACTIONS.md 同步表述
This commit is contained in:
chenfuqiang
2026-09-24 18:01:41 +08:00
parent 7c1f40b0e6
commit 4a8613b27e
15 changed files with 267 additions and 47 deletions

View File

@@ -103,6 +103,20 @@ python tests/run_tests.py test_cli_smoke
- ``tests/.gitignore`` 忽略 ``__pycache__``、``.pytest_cache``、``*.pyc``、``desktop/artifacts/``、``integration/artifacts/``、``.env`` 等。
- 推荐运行前设置 ``PYTHONDONTWRITEBYTECODE=1`` 减少 ``.pyc``。
### 1.5 本机无共享 runtime 时怎么跑(本技能开发机约定)
宿主共享 runtime(``{JIANGCHANG_DATA_ROOT}/python-runtime/.venv``)只在宿主环境存在;纯开发机上 ``import jiangchang_skill_core`` 会直接 ``ModuleNotFoundError``(整套测试 60+ errors)。**不安装任何包**的跑法:
```powershell
$env:PYTHONPATH = "D:\jiangchang\huashang\jiangchang-platform-kit\jiangchang-platform-kit\src"
python tests\run_tests.py -v
```
已知环境性失败(不修、不装包):``test_actions_manifest.py`` 中 3 项依赖 ``jsonschema`` 的 schema 校验
(``test_schema_allows_all_placement_execution_profile_combinations`` / ``test_schema_allows_bind_label_column`` /
``test_schema_rejects_toolbar_without_bind``)会以 ``jsonschema is required`` 失败。**除这 3 项外须全部通过**;
接入宿主共享 runtime 后自动恢复。``requirements.txt`` 保持为空——平台库由宿主 runtime 提供,技能不声明平台依赖。
---
## 2. 可选:Service / Golden(``tests/samples/*.sample``)

View File

@@ -12,8 +12,9 @@
| 文件 | 用途 |
|------|------|
| `sample_request.json` | 通用请求体样例,供 ``tests/samples/test_golden_cases.py.sample`` 演示读取。 |
| `expected_response.json` | 与上配套的期望摘要字段(业务复制后按域替换)。 |
| `sample_request.json` | 教师画像金标准样例(脱敏,JSON 数组),可直接 `import-teachers --teachers-json` 消费;供 `tests/samples/test_golden_cases.py.sample` 演示读取。 |
| `sample_tracks.json` | 指南赛道金标准样例(脱敏,JSON 数组),可直接 `import-tracks --tracks-json` 消费;与上配套覆盖 H1/H2/S1~S3/T1 场景。 |
| `expected_response.json` | 与上两份配套的期望匹配结论(Top 赛道/分层/得分/剔除项,业务复制后按域替换)。 |
| `adapter_profiles.sample.yaml` | 五类运行方式(mock / simulator_api / simulator_rpa / real_api / real_rpa)的配置表达样例。 |
---

View File

@@ -1,6 +1,22 @@
{
"success": true,
"data": {
"status": "ok"
}
"season": "2027",
"teacher_count": 2,
"track_count": 3,
"expectations": [
{
"teacherName": "张教授",
"topTrack": "面上项目·光伏材料",
"tier": "强烈推荐",
"matchPct": 100,
"reasonsContains": ["命中需求标签", "优先资助方向命中"]
},
{
"teacherName": "王讲师",
"topTrack": "青年项目·农村经济",
"tier": "可尝试",
"matchPct": 70,
"excludedContains": ["面上项目·光伏材料", "重点项目·量子计算"]
}
],
"disclaimerContains": "仅供参考"
}

View File

@@ -1,11 +1,22 @@
{
"request_id": "req_test_001",
"tenant_id": "tenant_demo",
"actor": "tester",
"parameters": {
"input_text": "demo input",
"options": {
"dry_run": true
}
[
{
"teacherName": "张教授",
"department": "化材学院",
"title": "教授",
"researchTags": "钙钛矿,光伏,半导体,界面调控,稳定性",
"recentPapers": "钙钛矿电池界面钝化研究",
"projectHistory": "主持省自然科学基金",
"sciQ1Count": 3,
"corePaperCount": 8
},
{
"teacherName": "王讲师",
"department": "经管学院",
"title": "讲师",
"researchTags": "乡村振兴,农村经济",
"recentPapers": "县域乡村产业发展研究",
"projectHistory": "参与市社科基金",
"sciQ1Count": 0,
"corePaperCount": 1
}
}
]

38
tests/fixtures/sample_tracks.json vendored Normal file
View File

@@ -0,0 +1,38 @@
[
{
"year": 2027,
"agency": "数理学部",
"programName": "面上项目",
"trackName": "光伏材料",
"trackTags": "钙钛矿,光伏,半导体",
"priorityKeywords": "界面调控,稳定性",
"minTitle": "副高及以上",
"minSciQ1": 0,
"quota": 10,
"sourceDoc": "2027年度申报指南(样例)"
},
{
"year": 2027,
"agency": "管理学部",
"programName": "青年项目",
"trackName": "农村经济",
"trackTags": "乡村振兴,农村经济",
"priorityKeywords": "数字经济",
"minTitle": "",
"minSciQ1": 0,
"quota": 8,
"sourceDoc": "2027年度申报指南(样例)"
},
{
"year": 2027,
"agency": "信息学部",
"programName": "重点项目",
"trackName": "量子计算",
"trackTags": "量子计算,量子信息",
"priorityKeywords": "",
"minTitle": "正高及以上",
"minSciQ1": 5,
"quota": 3,
"sourceDoc": "2027年度申报指南(样例)"
}
]

View File

@@ -40,7 +40,7 @@
- **计算类**:费用、税费、分摊、账单字段。
- **校验类**:合规、字段一致性、状态机规则。
推荐配合根目录下的 **`tests/fixtures/sample_request.json`** 与 **`tests/fixtures/expected_response.json`**(复制技能后按域替换字段,保持脱敏)。
推荐配合根目录下的 **`tests/fixtures/sample_request.json`**、**`tests/fixtures/sample_tracks.json`** 与 **`tests/fixtures/expected_response.json`**(复制技能后按域替换字段,保持脱敏)。
### 5. `test_logging_contract.py.sample`

View File

@@ -26,11 +26,14 @@ class TestGoldenCasesSample(unittest.TestCase):
req = json.load(f)
with open(exp_path, encoding="utf-8") as f:
exp = json.load(f)
self.assertEqual(req["request_id"], "req_test_001")
self.assertTrue(req["parameters"]["options"]["dry_run"])
# 示意:out = run(req); 然后 assert out["success"] == exp["success"] 等关键字段
self.assertTrue(exp["success"])
self.assertEqual(exp["data"]["status"], "ok")
# 本技能:sample_request = 教师画像数组(import-teachers 可直接消费)
self.assertIsInstance(req, list)
self.assertEqual(req[0]["teacherName"], "张教授")
# 示意:run_match(req, tracks, exp["season"]) 后断言关键字段
self.assertEqual(exp["season"], "2027")
top = exp["expectations"][0]
self.assertEqual(top["tier"], "强烈推荐")
self.assertEqual(top["matchPct"], 100)
if __name__ == "__main__":

View File

@@ -167,5 +167,48 @@ class TestKernelAndPersistence(MatchRuleTestBase):
self.assertEqual(payload["tracks_by_year"].get("2027"), 4)
class TestBatchSkipIncomplete(MatchRuleTestBase):
"""批量缺省跳过画像不完整教师(E1),指定单教师保持严格失败。"""
def test_batch_skips_incomplete_teacher(self) -> None:
from service.match_service import run_match
incomplete = _teacher(teacherName="王讲师", researchTags="", recentPapers="")
payload = run_match([_teacher(), incomplete], _tracks(), "2027")
self.assertEqual(payload["ok"], True)
self.assertEqual(payload["count"], 1)
self.assertEqual(len(payload["skipped"]), 1)
skipped = payload["skipped"][0]
self.assertEqual(skipped["teacher_name"], "王讲师")
self.assertIn("E1", skipped["reason"])
# 正常教师仍有报告
self.assertEqual(len(payload["reports"]), 1)
self.assertEqual(payload["reports"][0]["teacher_name"], "张教授")
def test_batch_brief_marks_skipped_teacher(self) -> None:
from service.match_service import run_match
incomplete = _teacher(teacherName="王讲师", researchTags="", recentPapers="")
payload = run_match([_teacher(), incomplete], _tracks(), "2027")
with open(payload["brief_file"], encoding="utf-8") as f:
html_text = f.read()
self.assertIn("画像待补录", html_text)
self.assertIn("王讲师", html_text)
def test_all_skipped_still_returns_success_payload(self) -> None:
from service.match_service import run_match
payload = run_match([_teacher(researchTags="", recentPapers="")], _tracks(), "2027")
self.assertEqual(payload["ok"], True)
self.assertEqual(payload["count"], 0)
self.assertEqual(len(payload["skipped"]), 1)
def test_strict_mode_raises_for_incomplete(self) -> None:
from service.match_service import run_match
with self.assertRaises(ValueError):
run_match([_teacher(researchTags="", recentPapers="")], _tracks(), "2027", skip_incomplete=False)
if __name__ == "__main__":
unittest.main()