v7 mech 8: MCDT acceptance/key-fact nodes as scorecard input model only
(cherry picked from commit 93e491b6a1b83fcdbbacf3493cdd2fd76418d0b6)
This commit is contained in:
1 parent
83135c283d
commit
b7e2f29868
2 files changed
+504
No files matched your search
@@ -0,0 +1,235 @@
|
||||
"""v7 机制第 8 项:MCDT 结果指标(评分卡输入模型)定向测试。
|
||||
|
||||
钉住的规则(对应任务 (a)(b)(c)):
|
||||
(a) 某维无对应验收节点时该维输入为 unknown,不得用过程指标填补。
|
||||
(b) 过程指标(test_count、coverage)只作为证据字段透传,绝不直接
|
||||
进入得分输入:只改 test_count/coverage 而节点状态不变时,每维
|
||||
的得分输入不变;只改节点状态(pending->pass 或 pass->fail)时,
|
||||
映射输出变化。
|
||||
(c) 节点状态为 fail 时对应维不得为 pass;节点状态 unknown 时对应维
|
||||
为 unknown。
|
||||
|
||||
诚实边界:此前 v6 头全仓 grep MCDT / 关键事实节点 / 验收节点为
|
||||
0 命中,本项只是评分卡输入模型,不是产品功能;测试不声称 MCDT
|
||||
已接入生产评分、门禁或真实审计循环。阈值一律仍是
|
||||
test_default_unapproved,除非调用方显式传入全 user_approved。
|
||||
"""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from cloud.audit_scorecard import DIMENSION_IDS as SCORECARD_DIMS
|
||||
from cloud.mcdt_nodes import (
|
||||
AcceptanceNode,
|
||||
KeyFactNode,
|
||||
DIMENSION_IDS,
|
||||
nodes_to_dimension_inputs,
|
||||
)
|
||||
except ImportError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from cloud.audit_scorecard import DIMENSION_IDS as SCORECARD_DIMS
|
||||
from cloud.mcdt_nodes import (
|
||||
AcceptanceNode,
|
||||
KeyFactNode,
|
||||
DIMENSION_IDS,
|
||||
nodes_to_dimension_inputs,
|
||||
)
|
||||
|
||||
|
||||
def _acc(node_id, dim, status, **extra):
|
||||
node = {
|
||||
"id": node_id,
|
||||
"title": "t-%s" % node_id,
|
||||
"acceptance_criteria": ["criterion 1"],
|
||||
"status": status,
|
||||
"evidence_refs": ["ev-1"],
|
||||
"dimension": dim,
|
||||
}
|
||||
node.update(extra)
|
||||
return node
|
||||
|
||||
|
||||
def _fact(node_id, dim, status):
|
||||
return {
|
||||
"id": node_id,
|
||||
"fact": "fact-%s" % node_id,
|
||||
"status": status,
|
||||
"source": "src-1",
|
||||
"dimension": dim,
|
||||
}
|
||||
|
||||
|
||||
def _walk(obj):
|
||||
if isinstance(obj, dict):
|
||||
for key, value in obj.items():
|
||||
yield key
|
||||
yield from _walk(value)
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
for value in obj:
|
||||
yield from _walk(value)
|
||||
|
||||
|
||||
class McdtNodesTestCase(unittest.TestCase):
|
||||
def test_dimension_ids_match_scorecard(self):
|
||||
self.assertEqual(DIMENSION_IDS, SCORECARD_DIMS)
|
||||
self.assertEqual(len(DIMENSION_IDS), 7)
|
||||
|
||||
def test_no_nodes_all_dimensions_unknown(self):
|
||||
out = nodes_to_dimension_inputs([], [])
|
||||
for dim in DIMENSION_IDS:
|
||||
entry = out["dimensions"][dim]
|
||||
self.assertEqual(entry["status"], "unknown")
|
||||
self.assertIn("no_acceptance_node_mapped",
|
||||
entry["missing_items"])
|
||||
self.assertEqual(entry["score_input"]["node_status"], "unknown")
|
||||
|
||||
def test_only_one_dimension_mapped_others_unknown(self):
|
||||
out = nodes_to_dimension_inputs(
|
||||
[_acc("a1", "delivery_quality", "pass")], [])
|
||||
self.assertEqual(out["dimensions"]["delivery_quality"]["status"],
|
||||
"pass")
|
||||
for dim in DIMENSION_IDS:
|
||||
if dim != "delivery_quality":
|
||||
self.assertEqual(out["dimensions"][dim]["status"], "unknown")
|
||||
|
||||
def test_process_metrics_do_not_change_dimension_inputs(self):
|
||||
nodes = [_acc("a1", "delivery_quality", "pass"),
|
||||
_acc("a2", "gate_audit", "pending")]
|
||||
base = nodes_to_dimension_inputs(
|
||||
nodes, [], process_metrics={"test_count": 100,
|
||||
"coverage": 0.5})
|
||||
changed = nodes_to_dimension_inputs(
|
||||
nodes, [], process_metrics={"test_count": 10000,
|
||||
"coverage": 0.99})
|
||||
self.assertEqual(base["dimensions"], changed["dimensions"])
|
||||
# 透传只在顶层,不在任何一维里。
|
||||
self.assertEqual(changed["process_evidence"]["test_count"], 10000)
|
||||
self.assertEqual(changed["process_evidence"]["coverage"], 0.99)
|
||||
for entry in changed["dimensions"].values():
|
||||
keys = set(_walk(entry))
|
||||
self.assertNotIn("test_count", keys)
|
||||
self.assertNotIn("coverage", keys)
|
||||
|
||||
def test_node_carried_process_keys_are_ignored(self):
|
||||
plain = nodes_to_dimension_inputs(
|
||||
[_acc("a1", "delivery_quality", "pass")], [])
|
||||
noisy = nodes_to_dimension_inputs(
|
||||
[_acc("a1", "delivery_quality", "pass",
|
||||
test_count=99999, coverage=0.01)], [])
|
||||
self.assertEqual(plain["dimensions"], noisy["dimensions"])
|
||||
|
||||
def test_pending_to_pass_changes_output(self):
|
||||
before = nodes_to_dimension_inputs(
|
||||
[_acc("a1", "delivery_quality", "pending")], [])
|
||||
after = nodes_to_dimension_inputs(
|
||||
[_acc("a1", "delivery_quality", "pass")], [])
|
||||
self.assertEqual(
|
||||
before["dimensions"]["delivery_quality"]["status"], "unknown")
|
||||
self.assertEqual(
|
||||
after["dimensions"]["delivery_quality"]["status"], "pass")
|
||||
self.assertNotEqual(before["dimensions"], after["dimensions"])
|
||||
|
||||
def test_pass_to_fail_changes_output_and_is_not_pass(self):
|
||||
before = nodes_to_dimension_inputs(
|
||||
[_acc("a1", "arch_contract", "pass")], [])
|
||||
after = nodes_to_dimension_inputs(
|
||||
[_acc("a1", "arch_contract", "fail")], [])
|
||||
self.assertEqual(before["dimensions"]["arch_contract"]["status"],
|
||||
"pass")
|
||||
self.assertEqual(after["dimensions"]["arch_contract"]["status"],
|
||||
"fail")
|
||||
self.assertNotEqual(before["dimensions"], after["dimensions"])
|
||||
|
||||
def test_fail_node_blocks_pass_even_with_other_pass_nodes(self):
|
||||
out = nodes_to_dimension_inputs(
|
||||
[_acc("a1", "gate_audit", "pass"),
|
||||
_acc("a2", "gate_audit", "fail")], [])
|
||||
self.assertEqual(out["dimensions"]["gate_audit"]["status"], "fail")
|
||||
|
||||
def test_unknown_node_means_dimension_unknown(self):
|
||||
out = nodes_to_dimension_inputs(
|
||||
[_acc("a1", "delivery_quality", "unknown")], [])
|
||||
self.assertEqual(out["dimensions"]["delivery_quality"]["status"],
|
||||
"unknown")
|
||||
out2 = nodes_to_dimension_inputs(
|
||||
[_acc("a1", "delivery_quality", "pass"),
|
||||
_acc("a2", "delivery_quality", "unknown")], [])
|
||||
self.assertEqual(out2["dimensions"]["delivery_quality"]["status"],
|
||||
"unknown")
|
||||
|
||||
def test_key_fact_fail_blocks_pass(self):
|
||||
out = nodes_to_dimension_inputs(
|
||||
[_acc("a1", "delivery_quality", "pass")],
|
||||
[_fact("f1", "delivery_quality", "fail")])
|
||||
self.assertEqual(out["dimensions"]["delivery_quality"]["status"],
|
||||
"fail")
|
||||
|
||||
def test_key_fact_unknown_blocks_pass(self):
|
||||
out = nodes_to_dimension_inputs(
|
||||
[_acc("a1", "delivery_quality", "pass")],
|
||||
[_fact("f1", "delivery_quality", "unknown")])
|
||||
self.assertEqual(out["dimensions"]["delivery_quality"]["status"],
|
||||
"unknown")
|
||||
|
||||
def test_key_fact_alone_cannot_pass_dimension(self):
|
||||
out = nodes_to_dimension_inputs(
|
||||
[], [_fact("f1", "delivery_quality", "pass")])
|
||||
self.assertEqual(out["dimensions"]["delivery_quality"]["status"],
|
||||
"unknown")
|
||||
|
||||
def test_unmapped_nodes_do_not_affect_any_dimension(self):
|
||||
out = nodes_to_dimension_inputs(
|
||||
[_acc("a1", None, "pass")],
|
||||
[_fact("f1", "bogus_dim", "pass")])
|
||||
for dim in DIMENSION_IDS:
|
||||
self.assertEqual(out["dimensions"][dim]["status"], "unknown")
|
||||
self.assertIn("a1", out["unmapped_acceptance_node_ids"])
|
||||
self.assertIn("f1", out["unmapped_key_fact_node_ids"])
|
||||
|
||||
def test_invalid_status_is_unknown_not_pass(self):
|
||||
out = nodes_to_dimension_inputs(
|
||||
[_acc("a1", "delivery_quality", "PASS")], [])
|
||||
self.assertEqual(out["dimensions"]["delivery_quality"]["status"],
|
||||
"unknown")
|
||||
|
||||
def test_dataclass_nodes_work(self):
|
||||
out = nodes_to_dimension_inputs(
|
||||
[AcceptanceNode(id="a1", title="t", acceptance_criteria=["c"],
|
||||
status="pass", evidence_refs=["e"],
|
||||
dimension="arch_contract")],
|
||||
[KeyFactNode(id="f1", fact="f", status="pass", source="s",
|
||||
dimension="arch_contract")])
|
||||
self.assertEqual(out["dimensions"]["arch_contract"]["status"],
|
||||
"pass")
|
||||
|
||||
def test_thresholds_default_unapproved_and_do_not_change_status(self):
|
||||
nodes = [_acc("a1", "delivery_quality", "pass")]
|
||||
default = nodes_to_dimension_inputs(nodes, [])
|
||||
self.assertEqual(default["thresholds_source"],
|
||||
"test_default_unapproved")
|
||||
approved = nodes_to_dimension_inputs(
|
||||
nodes, [],
|
||||
thresholds={"x": {"value": 1, "source": "user_approved"}})
|
||||
self.assertEqual(approved["thresholds_source"], "user_approved")
|
||||
self.assertEqual(default["dimensions"], approved["dimensions"])
|
||||
# 未知维传了已拍板阈值也不能变 pass。
|
||||
unknown_approved = nodes_to_dimension_inputs(
|
||||
[], [],
|
||||
thresholds={"x": {"value": 1, "source": "user_approved"}})
|
||||
self.assertEqual(
|
||||
unknown_approved["dimensions"]["delivery_quality"]["status"],
|
||||
"unknown")
|
||||
|
||||
def test_test_coverage_not_filled_by_coverage_metric(self):
|
||||
out = nodes_to_dimension_inputs(
|
||||
[], [], process_metrics={"test_count": 5000, "coverage": 0.99})
|
||||
self.assertEqual(out["dimensions"]["test_coverage"]["status"],
|
||||
"unknown")
|
||||
self.assertIn("no_acceptance_node_mapped",
|
||||
out["dimensions"]["test_coverage"]["missing_items"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user