范围(不含 design-story-foundation、docs/、humanization/README.md 等进行中改动):
1. 新增 harness/ 控制平面
- skill_harness.py 静态审计:32 个运行时 Skill 的 frontmatter/manifest/文档污染,当前 0 问题
- run_selected.py 选择性执行器:manifest 与磁盘一一对账、依赖阻断、
空跑与 skip-only 失败关闭、AST 测试形状门
- manifests/skills.json:32 个 Skill 的合同责任方与协作领域登记
- manifests/test-inventory.json:81 个测试资产登记
- specs/skill-testing.md 与 README.md:测试分层、证据边界与 harness 职责
2. 实现测试从 .claude/skills/*/scripts/ 迁至 tests/skills/<skill>/
- 71 个测试文件迁移并修复项目根与临时目录运行导入
- 数据库触发器测试宽泛异常收窄为 psycopg.errors.RaiseException
- 抽取离线大测试拆出真实 PG smoke(默认阻断,不计入离线通过)
- 抽取 presence 去重边界拆出独立测试:493 + 78 = 571 项检查不变
3. 运行时文档清理
- 13 个 SKILL.md 移除自测/离线验证段落、测试命令与测试文件事实源表述,
只保留运行时合同;业务运行合同、额度、授权与离线模式均保留
4. SoT 同步
- AGENTS.md:新增 Skill 领域索引(7 个合同责任方分组,覆盖 32 个运行时 Skill)
- 领域 07:测试入口改由 harness/manifests/ 登记,SKILL.md 不承载测试命令
- humanization 覆盖矩阵:活动测试路径同步迁移
验证证据: harness 自测 15 项 + runner 自测 13 项通过;静态审计 32 Skill / 0 问题;
73 个非数据库测试通过;8 个集成条目中 6 个 PostgreSQL 项被依赖门明确阻断;
py_compile 与 git diff --check 通过。未连接 PostgreSQL、网络、真实模型或额度。
已知边界: 真正 skill_behavior_eval 仍为 0,尚未验证任何 Skill 自然语言行为;
evaluate-frozen-replay 的 raw 存储边界冲突留待单独治理。
106 lines
5.1 KiB
Python
106 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
||
"""落库字段覆盖门禁回归:把「字段全覆盖」从文档纪律变成机械门禁。
|
||
|
||
门禁读 meta/schemas/<schema_type>.yaml:只对该型声明的「必填字段」失败关闭,
|
||
推荐字段缺失只报告不拦;确无依据的字段可在 payload['字段存疑'] 显式标注原因放行。
|
||
未标「字段覆盖门禁: 强制」的型(如中文键的老 schema)不溯及强拦。
|
||
"""
|
||
|
||
import pathlib
|
||
import sys
|
||
import unittest
|
||
|
||
|
||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||
SCRIPT_DIR = PROJECT_ROOT / ".claude" / "skills" / "plan-story" / "scripts"
|
||
sys.path.insert(0, str(SCRIPT_DIR))
|
||
|
||
from persist_planning import check_field_coverage, write_section, _load_schema # noqa: E402
|
||
|
||
REQUIRED = ["targetChapter", "sourceRef", "chapterGoal", "hardConstraints", "keyEvents",
|
||
"adjustableBeats", "declaredNewFacts", "mustAppearEntities", "chapterEndHook"]
|
||
|
||
|
||
def full_payload():
|
||
return {
|
||
"targetChapter": 3,
|
||
"sourceRef": {"sourceId": "outline:volume_1", "sourceVersion": "v1"},
|
||
"chapterGoal": "本章戏剧任务",
|
||
"hardConstraints": ["衔接上一章结尾"],
|
||
"keyEvents": [{"id": "e1", "order": 1, "event": "事件", "participants": ["林深"],
|
||
"trigger": "触发", "resultDirection": "结果方向"}],
|
||
"adjustableBeats": ["次序可调整"],
|
||
"declaredNewFacts": [{"factId": "f1", "text": "新事实",
|
||
"sourceRef": {"sourceId": "outline:volume_1", "sourceVersion": "v1"}}],
|
||
"mustAppearEntities": ["林深"],
|
||
"chapterEndHook": "章末钩子",
|
||
# 推荐字段
|
||
"foreshadowing": [{"action": "推进", "subject": "伏笔", "evidence": "unknown"}],
|
||
"entities": [{"name": "林深", "type": "character", "role": "主角"}],
|
||
"stateChanges": ["状态变化"],
|
||
"unknowns": [],
|
||
"assumptions": [],
|
||
}
|
||
|
||
|
||
class FieldCoverageTest(unittest.TestCase):
|
||
def test_schema_loads_with_required_and_enforce(self):
|
||
fields, required, enforce = _load_schema("fine_outline")
|
||
self.assertTrue(enforce)
|
||
self.assertEqual(required, REQUIRED)
|
||
self.assertIn("keyEvents", fields) # 块式字段被解析
|
||
self.assertIn("foreshadowing", fields) # 推荐字段被解析
|
||
|
||
def test_full_payload_covers_all(self):
|
||
r = check_field_coverage("fine_outline", full_payload())
|
||
self.assertTrue(r["enforced"])
|
||
self.assertEqual(r["missing_required"], [])
|
||
self.assertEqual(r["missing_recommended"], ["sourceRefs", "targetChars"])
|
||
|
||
def test_only_required_is_passable_recommended_reported_not_blocking(self):
|
||
payload = {k: v for k, v in full_payload().items() if k in REQUIRED}
|
||
r = check_field_coverage("fine_outline", payload)
|
||
self.assertEqual(r["missing_required"], []) # 不会被拦
|
||
self.assertEqual(len(r["missing_recommended"]), 7) # 只报告
|
||
|
||
def test_missing_required_is_caught(self):
|
||
for missing in REQUIRED:
|
||
payload = {k: v for k, v in full_payload().items() if k != missing}
|
||
r = check_field_coverage("fine_outline", payload)
|
||
self.assertIn(missing, r["missing_required"], f"缺 {missing} 应进 missing_required")
|
||
|
||
def test_excuse_hatch_releases_field(self):
|
||
payload = {k: v for k, v in full_payload().items() if k in REQUIRED}
|
||
payload["字段存疑"] = {"foreshadowing": "本章无伏笔动作", "entities": "出场已在 mustAppearEntities"}
|
||
r = check_field_coverage("fine_outline", payload)
|
||
self.assertNotIn("foreshadowing", r["missing_recommended"])
|
||
self.assertNotIn("entities", r["missing_recommended"])
|
||
# 必填也能用存疑放行(确无依据时显式声明,而非静默缺省)
|
||
payload2 = {k: v for k, v in payload.items() if k != "chapterEndHook"}
|
||
payload2["字段存疑"] = {"chapterEndHook": "本章无钩子,审查认可"}
|
||
self.assertEqual(check_field_coverage("fine_outline", payload2)["missing_required"], [])
|
||
|
||
def test_non_enforced_schema_not_blocked(self):
|
||
# outline 是中文键老 schema,未标强制:enforce=False,不溯及强拦。
|
||
_, _, enforce = _load_schema("outline")
|
||
self.assertFalse(enforce)
|
||
r = check_field_coverage("outline", {"title": "x"})
|
||
self.assertFalse(r["enforced"])
|
||
|
||
def test_unknown_schema_skipped(self):
|
||
r = check_field_coverage("no_such_type", {"anything": 1})
|
||
self.assertFalse(r["enforced"])
|
||
self.assertEqual(r["missing_required"], [])
|
||
|
||
def test_write_section_rejects_missing_required_before_db(self):
|
||
# 门禁在连库之前失败关闭:缺必填的 fine_outline 落库直接 ValueError。
|
||
payload = {k: v for k, v in full_payload().items() if k != "hardConstraints"}
|
||
with self.assertRaises(ValueError) as ctx:
|
||
write_section(12, section_type="fine_outline", schema_type="fine_outline",
|
||
target_chapter=99, payload=payload)
|
||
self.assertIn("字段覆盖门禁", str(ctx.exception))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|