test: apply evidence-based live checks without hiding regressions

This commit is contained in:
2026-09-09 02:40:13 -04:00
parent 16fa0ca474
commit 73008ad7d5
9 changed files with 2350 additions and 1359 deletions
+308
View File
@@ -0,0 +1,308 @@
"""真实测试的有限证据判定;不读取环境、不请求网络、不记录原始正文。"""
import hashlib
import json
import re
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal
from uuid import uuid4
from polygateway.errors import RequestRejectedError
from polygateway.types import ThinkingObservation
@dataclass(frozen=True)
class HttpEvidence:
"""一次 HTTP 的内存证据,禁止直接序列化。"""
call_id: str
request_checks: tuple[tuple[str, bool], ...]
status_code: int
error_body: bytes | None
raw_identity: tuple[bool, str | None]
@dataclass(frozen=True)
class AttemptEvidence:
"""一次 transport 尝试,可以没有 HTTP。"""
call_id: str
http: tuple[HttpEvidence, ...]
error: Exception | None
@dataclass(frozen=True)
class LiveVerdict:
"""测试命题的结论,不等同于 pytest 退出码。"""
status: Literal["PASS", "FAIL", "UNCOVERED"]
reason: str
def strict_json(body: bytes) -> Any:
"""独立解析完整 UTF-8 JSON,拒绝重复键及非标准常量。"""
def pairs(items: list[tuple[str, Any]]) -> dict[str, Any]:
"""重复键不允许被后值掩盖。"""
result = {}
for key, value in items:
if key in result:
raise ValueError("重复 JSON 键")
result[key] = value
return result
def invalid(value: str) -> None:
"""拒绝非标准数值常量。"""
raise ValueError("非法 JSON 常量")
return json.loads(body.decode("utf-8"), object_pairs_hook=pairs, parse_constant=invalid)
def messages_digest(messages: Any) -> str:
"""只在内存比较提示词摘要,不写提示词。"""
return hashlib.sha256(
json.dumps(messages, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
def request_is_valid(event: HttpEvidence) -> bool:
"""完整且无重复的显式检查才能作为请求资格。"""
checks = dict(event.request_checks)
common = {"method", "origin", "path", "model", "authorization", "control", "messages_digest"}
return (
len(checks) == len(event.request_checks)
and set(checks) in (common | {"stream"}, common | {"input_shape"})
and all(value is True for value in checks.values())
)
def _single_error(error: Exception, attempts: Sequence[AttemptEvidence]) -> HttpEvidence | None:
"""仅接受可与最终异常精确配对的独立单次错误。"""
if not isinstance(error, RequestRejectedError) or len(attempts) != 1:
return None
attempt = attempts[0]
if attempt.error is not error or len(attempt.http) != 1:
return None
event = attempt.http[0]
if event.call_id != attempt.call_id or not request_is_valid(event):
return None
if event.status_code != error.status_code:
return None
return event
def error_machine_type(event: HttpEvidence) -> str | None:
"""完整错误体中的唯一机器字段;不检查正文子串。"""
body = event.error_body
if body is None or not 0 < len(body) <= 65536:
return None
try:
data = strict_json(body)
except (ValueError, UnicodeError):
return None
if not isinstance(data, dict) or not isinstance(data.get("error"), dict):
return None
value = data["error"].get("type")
return value if isinstance(value, str) else None
def classify_live_failure(error: Exception, attempts: Sequence[AttemptEvidence]) -> LiveVerdict:
"""默认失败;唯一自动外因是完整证据支持的 404 model_not_found。"""
event = _single_error(error, attempts)
if (
event is not None
and event.status_code == 404
and error_machine_type(event) == "model_not_found"
):
return LiveVerdict("UNCOVERED", "端点回报该请求型号不可用")
return LiveVerdict("FAIL", "无满足窄外因契约的完整独立证据")
def assess_expected_rejection(
error: Exception, attempts: Sequence[AttemptEvidence], *, status_code: int, machine_type: str
) -> LiveVerdict:
"""预先声明的拒绝命题,不把一般 400 当不支持档位。"""
event = _single_error(error, attempts)
if (
event is not None
and event.status_code == status_code
and error_machine_type(event) == machine_type
):
return LiveVerdict("PASS", "符合预声明的拒绝类型、状态和机器字段")
return LiveVerdict("FAIL", "不符合预声明拒绝证据")
def assess_model_identity(
*,
requested: str,
aliases: frozenset[str],
reported: str | None,
raw_identity: tuple[bool, str | None],
request_valid: bool,
) -> LiveVerdict:
"""公共身份异常只有原始独立证据才能归因上游。"""
if not request_valid:
return LiveVerdict("FAIL", "实发请求校验不完整或不符")
allowed = {requested, *aliases}
captured, raw = raw_identity
if captured and raw != reported:
return LiveVerdict("FAIL", "公共身份与独立原始身份不一致")
if reported in allowed:
return LiveVerdict("PASS", "身份合格")
if not captured:
return LiveVerdict("FAIL", "身份来源无法区分")
return LiveVerdict("UNCOVERED", "独立原始响应身份缺失或不属于显式别名")
def assess_thinking_coverage(
observations: Sequence[ThinkingObservation],
*,
planned_rounds: int,
proposition: Literal["enabled", "disabled", "cannot_disable"],
) -> LiveVerdict:
"""按独立命题裁定;缺轮不减分母,UNKNOWN 不证明关闭。"""
if planned_rounds < 1 or len(observations) != planned_rounds:
return LiveVerdict("FAIL", "计划轮次不完整")
if any(not isinstance(item, ThinkingObservation) for item in observations):
return LiveVerdict("FAIL", "观测类型不符")
observed = observations.count(ThinkingObservation.OBSERVED)
unknown = observations.count(ThinkingObservation.UNKNOWN)
if proposition == "enabled":
if observed > planned_rounds / 2:
return LiveVerdict("PASS", "完整轮次多数观测到推理")
return LiveVerdict("UNCOVERED" if unknown else "FAIL", "开启证据未达多数")
if proposition == "disabled":
if observed:
return LiveVerdict("FAIL", "观测到推理,证伪关闭声明")
return LiveVerdict(
"UNCOVERED" if unknown else "PASS",
"UNKNOWN 不证明关闭" if unknown else "每轮明确 ABSENT",
)
if proposition == "cannot_disable":
if observed:
return LiveVerdict("PASS", "本条件下仍推理;不外推所有私有参数")
return LiveVerdict("UNCOVERED" if unknown else "FAIL", "缺少仍推理的证据")
raise ValueError("未知测试命题")
def summarize_verdicts(verdicts: Sequence[LiveVerdict], *, planned_rounds: int) -> dict[str, int]:
"""保留失败、未覆盖与缺轮的独立计数。"""
if planned_rounds < len(verdicts):
raise ValueError("实际轮次超出计划")
return {
**{
status: sum(v.status == status for v in verdicts)
for status in ("PASS", "FAIL", "UNCOVERED")
},
"missing": planned_rounds - len(verdicts),
}
_SAFE_FIELDS = frozenset(
{
"provider",
"requested_model",
"reported_model",
"stream",
"requested_effort",
"applied_effort",
"session_id",
"parent_call_id",
"attempts",
"status",
"reason",
"completed_rounds",
"planned_rounds",
"thinking_observation",
"prompt_tokens",
"completion_tokens",
"reasoning_tokens",
"thinking_chars",
"counts",
"proposition",
"error_type",
"error_status",
"evidence_notes",
}
)
_ATTEMPT_FIELDS = frozenset({"call_id", "error_type", "http"})
_HTTP_FIELDS = frozenset(
{"status_code", "request_checks", "identity_captured", "error_body_complete"}
)
def _validate_safe(fields: Mapping[str, Any]) -> None:
"""拒绝原始异常和证据对象,嵌套字段也有白名单。"""
if set(fields) - _SAFE_FIELDS:
raise ValueError("报告含非白名单字段")
for attempt in fields.get("attempts", []):
if not isinstance(attempt, dict) or set(attempt) != _ATTEMPT_FIELDS:
raise ValueError("非法 attempt 报告")
for event in attempt["http"]:
if not isinstance(event, dict) or set(event) != _HTTP_FIELDS:
raise ValueError("非法 HTTP 报告")
if not isinstance(event["request_checks"], dict) or any(
type(v) is not bool for v in event["request_checks"].values()
):
raise ValueError("请求校验报告只允许布尔值")
# 不提供 default=str:原始异常、bytes、dataclass 均必须失败。
json.dumps(fields, ensure_ascii=False, allow_nan=False)
def write_live_round(
output_dir: Path,
*,
run_id: str,
matrix_id: str,
round_index: int,
safe_fields: Mapping[str, Any],
) -> Path:
"""仅接收已脱敏字段;独占文件写入失败必须冒泡。"""
_validate_safe(safe_fields)
if round_index < 0 or not re.fullmatch(r"[a-zA-Z0-9_-]+", run_id + matrix_id):
raise ValueError("报告路径标识非法")
directory = output_dir / run_id
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{matrix_id}-{round_index}-{uuid4().hex}.md"
text = json.dumps(dict(safe_fields), ensure_ascii=False, indent=2, allow_nan=False)
with path.open("x", encoding="utf-8") as handle:
handle.write(f"# {matrix_id} · 轮次 {round_index}\n\n```json\n{text}\n```\n")
return path
def safe_attempts(attempts: Sequence[AttemptEvidence]) -> list[dict[str, Any]]:
"""只导出事实布尔值、异常类和状态;不落盘任何上游正文。"""
return [
{
"call_id": attempt.call_id,
"error_type": type(attempt.error).__name__ if attempt.error else None,
"http": [
{
"status_code": event.status_code,
"request_checks": dict(event.request_checks),
"identity_captured": event.raw_identity[0],
"error_body_complete": event.error_body is not None,
}
for event in attempt.http
],
}
for attempt in attempts
]
def combine_live_verdicts(verdicts: Sequence[LiveVerdict]) -> LiveVerdict:
"""部分失败优先于未覆盖;部分未覆盖不得汇总全 PASS。"""
if any(verdict.status == "FAIL" for verdict in verdicts):
return LiveVerdict("FAIL", "至少一个必需命题失败")
if not verdicts or any(verdict.status == "UNCOVERED" for verdict in verdicts):
return LiveVerdict("UNCOVERED", "至少一个必需命题未覆盖")
return LiveVerdict("PASS", "所有必需命题通过")
def qualify_live_rounds(verdicts: Sequence[LiveVerdict], *, planned_rounds: int) -> LiveVerdict:
"""汇总请求/身份资格,缺轮绝不缩小分母。"""
if planned_rounds < 1 or len(verdicts) != planned_rounds:
return LiveVerdict("FAIL", "计划轮次缺失")
return combine_live_verdicts(verdicts)