gapfill G5: wire optional ?current_main_head drift check into GET /audit-scorecard (caller-supplied only, no git/subprocess; absent param means no staleness verdict, verdict never rewrites overall); endpoint tests for passed/absent/empty

This commit is contained in:
host committed 2026-11-04 09:08:47 +00:00
1 parent b4270dc117
commit 145aa2eb5f
2 files changed
+136 -2

No files matched your search

+34 -2
View File
@@ -2730,7 +2730,7 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
elif path == "/metrics-report" and method == "GET":
self._metrics_report(user_id)
elif path == "/audit-scorecard" and method == "GET":
self._audit_scorecard(user_id)
self._audit_scorecard(user_id, query)
elif path == "/models" and method == "GET":
self._models_overview(user_id)
elif path == "/relay/models" and method == "GET":
@@ -8697,9 +8697,21 @@ pid 须为整数、address 须为字符串,否则 400(注册表里的
conn.close()
metrics = _compute_metrics(records, costs)
self._send_json(200, _build_metrics_report(metrics))
def _audit_scorecard(self, user_id):
def _audit_scorecard(self, user_id, query=None):
"""只读 7 维审计评分卡报告端点。
可选漂移判定(gapfill G5,机制 item 5 端点快照接线):
调用方可在查询串里传 ``?current_main_head=<头>``,端点
即用 cloud/audit_scorecard.py 的 check_baseline_stale()
对返回的卡做一次漂移判定,结果放在响应顶层的
``baseline_staleness`` 里(stale / usable_as_pass_basis
等字段原样返回)。该判定绝不改写卡本身的 overall /
overall_pass(旧值保留只为追溯),也绝不挡住查询:
未传参数时默认不做判定,响应与以前完全一致;传了
参数也永远返回 200 + 报告,只是多一个判定字段。
生产代码绝不读 git、不起 subprocess 查版本:main 头
只能由调用方传入,端点本身不探测版本控制状态。
权限:与 /metrics 同款,任意项目成员即可读(不要求 owner),
但本端点不统计任何项目数据、不写库、不写账本、不改链头、
不触发推回或重审——它只返回评分卡模块的一张历史示例卡。
@@ -8751,6 +8763,26 @@ pid 须为整数、address 须为字符串,否则 400(注册表里的
"untested_items": [],
"product_gate_passed": False},
}
# gapfill G5:可选漂移判定。未传参数时不做判定、不标
# stale,响应与以前一致;传了参数才附加判定字段,且
# 判定结果绝不改写报告的 overall / overall_pass。
heads = (query or {}).get("current_main_head") or []
current = heads[0] if heads else None
if current:
try:
from cloud.audit_scorecard import (
check_baseline_stale)
report["baseline_staleness"] = check_baseline_stale(
report, current)
except ImportError: # pragma: no cover - 模块不可用时诚实说明
report["baseline_staleness"] = {
"checked": False,
"reason": "audit_scorecard_module_unavailable",
"stale": True,
"usable_as_pass_basis": False,
"overall_usable_as_pass_basis": False,
"overall_must_not_be_used_as_pass_basis": True,
}
self._send_json(200, report)
@@ -0,0 +1,102 @@
"""gapfill G5:机制 item 5 端点快照接线定向测试。
接线方式:GET /audit-scorecard 可选查询串
``?current_main_head=<头>``,端点用 check_baseline_stale()
对返回的卡做一次漂移判定,结果放在响应顶层
``baseline_staleness`` 里。锁定点:
- 未传参数时默认不做判定:响应里没有 baseline_staleness,
其余字段与以前完全一致,查询绝不被挡住(永远 200)。
- 传了参数时附加判定字段,但判定绝不改写卡本身的
overall / overall_pass(示例卡 baseline_head 为 None,
故任何传入的头都判 stale=True、原因 baseline_head_missing,
这是失败关闭,不是把旧分改写)。
- 空串参数按未传入处理:不做判定、不标 stale。
- 端点本身不读 git、不起 subprocess 查版本:main 头只能
由调用方传入(本文件只测 HTTP 入参一路,不测任何
版本控制探测——check_baseline_stale 本身已在机制基线
测试里钉住“只认调用方传入参数”)。
只加测试;生产改动在 cloud/server.py 的 _audit_scorecard。
"""
import http.client
import json
import os
import sys
import tempfile
import threading
import unittest
from pathlib import Path
try:
from cloud.auth import AuthStore
from cloud.server import create_server
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.auth import AuthStore
from cloud.server import create_server
class BaselineSnapshotEndpointTestCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
self.auth = AuthStore(self.db_path)
self.owner_id, self.owner_key = self.auth.create_user("owner")
self.server = create_server(self.db_path, "127.0.0.1", 0)
self.port = self.server.server_address[1]
self.thread = threading.Thread(
target=self.server.serve_forever, daemon=True)
self.thread.start()
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.thread.join(timeout=5)
try:
self.auth.conn.close()
except Exception:
pass
self.tmp.cleanup()
def _req(self, path):
conn = http.client.HTTPConnection(
"127.0.0.1", self.port, timeout=10)
try:
conn.request("GET", path,
headers={"X-API-Key": self.owner_key})
resp = conn.getresponse()
data = resp.read()
finally:
conn.close()
return resp.status, json.loads(data.decode("utf-8"))
def test_no_param_means_no_staleness_field(self):
status, report = self._req("/audit-scorecard")
self.assertEqual(status, 200)
self.assertEqual(report["overall"], "not_pass")
self.assertNotIn("baseline_staleness", report)
def test_param_attaches_staleness_without_rewriting_overall(self):
status, report = self._req(
"/audit-scorecard?current_main_head=82d0e84")
self.assertEqual(status, 200)
# 示例卡无冻结基线:显式要求判定时失败关闭为 stale,
# 但卡本身的 overall / overall_pass 原样不动。
self.assertEqual(report["overall"], "not_pass")
self.assertFalse(report["overall_pass"])
stale = report["baseline_staleness"]
self.assertTrue(stale["stale"])
self.assertFalse(stale["usable_as_pass_basis"])
self.assertTrue(stale["overall_must_not_be_used_as_pass_basis"])
self.assertIn("baseline_head_missing", stale["reason"])
self.assertEqual(stale["current_main_head"], "82d0e84")
def test_empty_param_means_no_staleness_field(self):
status, report = self._req("/audit-scorecard?current_main_head=")
self.assertEqual(status, 200)
self.assertNotIn("baseline_staleness", report)
if __name__ == "__main__":
unittest.main()