aps-agent/tests/golden/test_m3_knowledge.py

121 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================
# M3 知识与偏好黄金测试(moduleId: golden-m3, 发版门禁 §14.3)
# 固化不变式:
# K1 检索必带出处(assetId/title/version)且未命中返回空(诚实原则 §8.1)
# K2 报告数字全部可溯源到快照字段(§9.10 硬规则 1)
# K3 偏好个性化:采用权重 > 试排权重;无信号回退默认(§8.3)
# K4 知识资产同名重复入库版本递增(§8.1 版本化)
# ============================================================
from __future__ import annotations # 前向类型引用
import re # 报告数字抽取
from server.knowledge.assets import KnowledgeStore # 资产库(临时路径实例化)
from server.knowledge.preferences import PreferenceStore # 偏好仓
from server.knowledge.retrieval import search # 混合检索
from server.aps_domain.reports import build_daily_report, build_version_diff_report # 报告
from server.state.seed import seed_world # 种子世界
from server.engines import get_engine # 引擎(造版本用)
from server.engines.base import EngineParams # 引擎入参
from server.timeutil import add_minutes, fmt_date, today0 # 日期工具
def _make_version(world, strategy="DELIVERY_FIRST"):
"""在世界里真实跑一版排产(报告测试的前置数据)。"""
counters = {} # 独立发号器
def next_id(kind):
counters[kind] = counters.get(kind, 0) + 1
return counters[kind] + 1000
params = EngineParams(orderIds=[], engineType="RULE", strategyTemplate=strategy,
planningHorizonDays=14, startDate=fmt_date(add_minutes(today0(), 24 * 60)))
return get_engine("RULE").solve(world, params, next_id)
# ---------------- K1:检索带出处 + 诚实未命中 ----------------
def test_search_returns_provenance(tmp_path):
"""命中必带 assetId/title/version;无关问题返回空列表(不编造)。"""
kb = KnowledgeStore(path=str(tmp_path / "kn.json")) # 临时库(自动播种)
hits = search(kb.assets, "换线有什么规定") # SOP 问题
assert hits, "种子 SOP 应能命中" # 有命中
top = hits[0] # 最相关
assert top["title"] == "换线标准SOP" # 命中正确资产
for key in ("assetId", "title", "version", "snippet"): # 出处字段齐全
assert top[key], f"命中缺出处字段 {key}"
miss = search(kb.assets, "quantum blockchain xyzzy") # 无关问题(无重叠词)
assert miss == [], "无关问题必须诚实返回空" # 未命中为空
# ---------------- K2:报告数字可溯源 ----------------
def test_daily_report_numbers_from_snapshot():
"""日报正文中的核心数字必须与冻结快照字段一致。"""
world = seed_world() # 种子世界
_make_version(world) # 真实排一版
report = build_daily_report(world) # 生成日报
snap = report["snapshot"] # 冻结快照
assert snap is not None # 有快照
md = report["markdown"] # 报告正文
assert f"**{snap['poCount']}** 个" in md # 订单数可溯源
assert f"**{snap['woCount']}** 个" in md # 工单数可溯源
assert f"**{snap['conflictCount']}** 项" in md # 冲突数可溯源
assert snap["versionNo"] in md # 版本号出现在出处行
def test_version_diff_report_needs_two_versions():
"""不足两版时诚实拒绝;两版后 KPI 表出现新旧版本号。"""
world = seed_world() # 种子世界
r0 = build_version_diff_report(world) # 无版本
assert r0["reportId"] is None # 拒绝生成
_make_version(world, "DELIVERY_FIRST") # 第一版
_make_version(world, "CAPACITY_BALANCE") # 第二版
r = build_version_diff_report(world) # 生成对比
assert r["reportId"] # 成功
assert r["snapshot"]["new"]["versionNo"] in r["markdown"] # 新版号在正文
assert r["snapshot"]["old"]["versionNo"] in r["markdown"] # 旧版号在正文
# ---------------- K3:偏好个性化 ----------------
def test_preference_weighting(tmp_path):
"""采用(2分)> 试排(1分);无信号回退系统默认。"""
ps = PreferenceStore(path=str(tmp_path / "pref.json")) # 临时仓
default, scores = ps.preferred_strategy() # 无样本
assert default == "COMPREHENSIVE" and scores == {} # 回退默认
ps.record("DELIVERY_FIRST", source="schedule.run") # 试排交期 ×2 = 2分
ps.record("DELIVERY_FIRST", source="schedule.run")
ps.record("CAPACITY_BALANCE", source="scenario.apply") # 采用均衡 ×1 = 2分…
ps.record("CAPACITY_BALANCE", source="scenario.apply") # ×2 = 4分(采用权重更高)
best, scores = ps.preferred_strategy() # 求偏好
assert best == "CAPACITY_BALANCE", f"采用权重应更高,got {scores}" # 采用胜出
assert scores["DELIVERY_FIRST"] == 2 and scores["CAPACITY_BALANCE"] == 4 # 得分可解释
# ---------------- K4:资产版本化 ----------------
def test_asset_versioning(tmp_path):
"""同名资产重复入库版本号递增(v1 → v2)。"""
kb = KnowledgeStore(path=str(tmp_path / "kn.json")) # 临时库
a1 = kb.add("sop", "测试SOP", "第一版内容") # 首次入库
assert a1["version"] == "v1" # v1
a2 = kb.add("sop", "测试SOP", "第二版内容") # 同名再入
assert a2["version"] == "v2" # 版本递增
assert kb.find_by_title("测试SOP") is not None # 标题可检索
def test_hybrid_search_units_with_chunks(tmp_path):
"""带 chunks 的资产按单元检索,仍强制出处。"""
kb = KnowledgeStore(path=str(tmp_path / "kn.json"))
kb.assets = []
kb._write()
kb.add_with_chunks(
kind="sop", title="夜班换线禁令", content="摘要",
chunks=[{"chunkId": "c0", "text": "每日十六点后禁止跨产品族换线作业。", "seq": 0}],
tags=["换线", "夜班"], approved=True,
)
hits = search(kb.assets, "夜班换线")
# search() 走整篇;再用 hybrid on units
from server.knowledge.retrieval import hybrid_search
units = kb.iter_search_units()
hits2 = hybrid_search(units, "禁止跨产品族换线", top_k=2)
assert hits2
for key in ("assetId", "title", "version", "snippet"):
assert hits2[0][key]
assert hits2[0].get("chunkId") == "c0"