test(governance): validate test debt metadata

This commit is contained in:
rookiestar28
2026-04-20 05:38:56 +08:00
parent 723215ac48
commit 71199efa07
14 changed files with 887 additions and 340 deletions
+10 -7
View File
@@ -383,33 +383,36 @@ cleanup_precommit_snapshots
# edits cannot hide until deep in the pre-push unit suite.
"$VENV_PY" scripts/check_openapi_sync.py
echo "[pre-push] 3/8 coverage governance check"
echo "[pre-push] 3/9 coverage governance check"
"$VENV_PY" scripts/verify_quality_governance.py
echo "[pre-push] 4/8 backend unit tests"
echo "[pre-push] 4/9 test debt governance check"
"$VENV_PY" scripts/verify_test_debt_governance.py
echo "[pre-push] 5/9 backend unit tests"
MOLTBOT_STATE_DIR="$ROOT_DIR/moltbot_state/_pre_push_unit" \
"$VENV_PY" scripts/run_unittests.py --start-dir tests --pattern "test_*.py" --enforce-skip-policy tests/skip_policy.json
if [ -n "${OPENCLAW_IMPL_RECORD_PATH:-}" ]; then
echo "[pre-push] 4.5/8 implementation record lint (strict)"
echo "[pre-push] 5.5/9 implementation record lint (strict)"
"$VENV_PY" scripts/lint_implementation_record.py --path "$OPENCLAW_IMPL_RECORD_PATH" --strict
fi
echo "[pre-push] 5/8 backend real E2E lanes (R122/R123)"
echo "[pre-push] 6/9 backend real E2E lanes (R122/R123)"
MOLTBOT_STATE_DIR="$ROOT_DIR/moltbot_state/_pre_push_backend_e2e_real" \
"$VENV_PY" scripts/run_unittests.py --module tests.test_r122_real_backend_lane --enforce-skip-policy tests/skip_policy.json --max-skipped 0
MOLTBOT_STATE_DIR="$ROOT_DIR/moltbot_state/_pre_push_backend_e2e_real" \
"$VENV_PY" scripts/run_unittests.py --module tests.test_r123_real_backend_model_list_lane --enforce-skip-policy tests/skip_policy.json --max-skipped 0
echo "[pre-push] 6/8 R121 retry partition contract"
echo "[pre-push] 7/9 R121 retry partition contract"
MOLTBOT_STATE_DIR="$ROOT_DIR/moltbot_state/_pre_push_retry_partition" \
"$VENV_PY" scripts/run_unittests.py --module tests.test_r121_retry_partition_contract --enforce-skip-policy tests/skip_policy.json --max-skipped 0
echo "[pre-push] 7/8 R118 adversarial gate (adaptive: smoke/extended)"
echo "[pre-push] 8/9 R118 adversarial gate (adaptive: smoke/extended)"
MOLTBOT_STATE_DIR="$ROOT_DIR/moltbot_state/_pre_push_adversarial" \
"$VENV_PY" scripts/run_adversarial_gate.py --profile auto --seed 42 --artifact-dir .tmp/adversarial
echo "[pre-push] 8/8 npm test (Playwright)"
echo "[pre-push] 9/9 npm test (Playwright)"
npm test
echo "[pre-push] PASS"
+13 -6
View File
@@ -7,7 +7,6 @@ from fnmatch import fnmatch
from pathlib import Path
from typing import Any, Iterable
REQUIRED_HOTSPOT_FAMILIES = (
"safe_io",
"security_boundary",
@@ -42,8 +41,10 @@ def _validate_hotspot_family(
seen_ids.add(family_id)
paths = family.get("paths")
if not isinstance(paths, list) or not paths or not all(
isinstance(path, str) and path.strip() for path in paths
if (
not isinstance(paths, list)
or not paths
or not all(isinstance(path, str) and path.strip() for path in paths)
):
failures.append(
f"coverage policy: hotspot family {family_id} must define a non-empty paths list"
@@ -93,7 +94,9 @@ def load_and_validate_policy(path: Path) -> tuple[dict[str, Any] | None, list[st
f"coverage policy: stage {stage_id} missing numeric min_fail_under"
)
continue
stages.append(CoverageStage(stage_id=stage_id, min_fail_under=float(min_fail_under)))
stages.append(
CoverageStage(stage_id=stage_id, min_fail_under=float(min_fail_under))
)
for previous, current in zip(stages, stages[1:]):
if current.min_fail_under <= previous.min_fail_under:
@@ -243,7 +246,9 @@ def summarize_coverage(
summary = files[file_path].get("summary", {})
covered_lines += int(summary.get("covered_lines", 0))
num_statements += int(summary.get("num_statements", 0))
percent = round((covered_lines / num_statements) * 100, 2) if num_statements else 0.0
percent = (
round((covered_lines / num_statements) * 100, 2) if num_statements else 0.0
)
hotspot_summary[family_id] = {
"matched_files": matched_files,
"missing_paths": missing_paths,
@@ -263,7 +268,9 @@ def summarize_coverage(
"current_stage_fail_under": current_stage_threshold(policy),
"next_stage": next_policy_stage["id"] if next_policy_stage else None,
"next_stage_fail_under": (
float(next_policy_stage["min_fail_under"]) if next_policy_stage else None
float(next_policy_stage["min_fail_under"])
if next_policy_stage
else None
),
},
"overall": {
+5 -1
View File
@@ -10,7 +10,11 @@ import json
import sys
from pathlib import Path
from quality_governance_common import load_and_validate_policy, read_json, summarize_coverage
from quality_governance_common import (
load_and_validate_policy,
read_json,
summarize_coverage,
)
def _render_text(summary: dict[str, object]) -> str:
+11 -8
View File
@@ -210,28 +210,31 @@ if precommit_changed_repo_state; then
fi
cleanup_precommit_snapshots
echo "[tests] 3/9 coverage governance check"
echo "[tests] 3/10 coverage governance check"
"$VENV_PY" scripts/verify_quality_governance.py
echo "[tests] 4/9 backend unit tests"
echo "[tests] 4/10 test debt governance check"
"$VENV_PY" scripts/verify_test_debt_governance.py
echo "[tests] 5/10 backend unit tests"
MOLTBOT_STATE_DIR="$ROOT_DIR/moltbot_state/_local_unit" "$VENV_PY" scripts/run_unittests.py --start-dir tests --pattern "test_*.py" --enforce-skip-policy tests/skip_policy.json
if [ -n "${OPENCLAW_IMPL_RECORD_PATH:-}" ]; then
echo "[tests] 4.5/9 implementation record lint (strict)"
echo "[tests] 5.5/10 implementation record lint (strict)"
# IMPORTANT: strict mode is opt-in via OPENCLAW_IMPL_RECORD_PATH to avoid retroactive legacy record failures.
"$VENV_PY" scripts/lint_implementation_record.py --path "$OPENCLAW_IMPL_RECORD_PATH" --strict
fi
echo "[tests] 5/9 backend real E2E lanes (R122/R123)"
echo "[tests] 6/10 backend real E2E lanes (R122/R123)"
MOLTBOT_STATE_DIR="$ROOT_DIR/moltbot_state/_local_backend_e2e_real" \
"$VENV_PY" scripts/run_unittests.py --module tests.test_r122_real_backend_lane --enforce-skip-policy tests/skip_policy.json --max-skipped 0
MOLTBOT_STATE_DIR="$ROOT_DIR/moltbot_state/_local_backend_e2e_real" \
"$VENV_PY" scripts/run_unittests.py --module tests.test_r123_real_backend_model_list_lane --enforce-skip-policy tests/skip_policy.json --max-skipped 0
echo "[tests] 6/9 R121 retry partition contract"
echo "[tests] 7/10 R121 retry partition contract"
"$VENV_PY" scripts/run_unittests.py --module tests.test_r121_retry_partition_contract --enforce-skip-policy tests/skip_policy.json --max-skipped 0
echo "[tests] 7/9 Slack integration gates (R124/R125/R117/F57)"
echo "[tests] 8/10 Slack integration gates (R124/R125/R117/F57)"
"$VENV_PY" scripts/run_unittests.py --module tests.test_r124_slack_ingress_contract --enforce-skip-policy tests/skip_policy.json --max-skipped 0
"$VENV_PY" scripts/run_unittests.py --module tests.test_r125_slack_real_backend_lane --enforce-skip-policy tests/skip_policy.json --max-skipped 0
"$VENV_PY" scripts/run_unittests.py --module tests.test_r117_observability_redaction_e2e --enforce-skip-policy tests/skip_policy.json --max-skipped 0
@@ -239,11 +242,11 @@ echo "[tests] 7/9 Slack integration gates (R124/R125/R117/F57)"
"$VENV_PY" scripts/run_unittests.py --module tests.test_f57_slack_transport_parity --enforce-skip-policy tests/skip_policy.json --max-skipped 0
"$VENV_PY" scripts/run_unittests.py --module tests.test_f57_slack_socket_mode_startup --enforce-skip-policy tests/skip_policy.json --max-skipped 0
echo "[tests] 8/9 R118 adversarial gate (adaptive: smoke/extended)"
echo "[tests] 9/10 R118 adversarial gate (adaptive: smoke/extended)"
MOLTBOT_STATE_DIR="$ROOT_DIR/moltbot_state/_local_adversarial" \
"$VENV_PY" scripts/run_adversarial_gate.py --profile auto --seed 42 --artifact-dir .tmp/adversarial
echo "[tests] 9/9 frontend E2E"
echo "[tests] 10/10 frontend E2E"
# IMPORTANT: full-gate acceptance must provision Playwright browsers itself; do
# not assume a warmed local browser cache when running on fresh WSL/Linux hosts.
OPENCLAW_PLAYWRIGHT_INSTALL=1 OPENCLAW_PLAYWRIGHT_BROWSERS=chromium npm test
+13 -8
View File
@@ -266,26 +266,31 @@ if ($LASTEXITCODE -ne 0) {
}
Assert-PreCommitDidNotMutateRepo -BeforeWorktree $preCommitWorktreeBefore -BeforeIndex $preCommitIndexBefore
Write-Host "[tests] 3/9 coverage governance check"
Write-Host "[tests] 3/10 coverage governance check"
Invoke-Checked "coverage governance check" {
& $venvPython scripts\verify_quality_governance.py
}
Write-Host "[tests] 4/9 backend unit tests"
Write-Host "[tests] 4/10 test debt governance check"
Invoke-Checked "test debt governance check" {
& $venvPython scripts\verify_test_debt_governance.py
}
Write-Host "[tests] 5/10 backend unit tests"
$env:MOLTBOT_STATE_DIR = "$root\moltbot_state\_local_unit"
Invoke-Checked "backend unit tests" {
& $venvPython scripts\run_unittests.py --start-dir tests --pattern "test_*.py" --enforce-skip-policy tests\skip_policy.json
}
if ($env:OPENCLAW_IMPL_RECORD_PATH) {
Write-Host "[tests] 4.5/9 implementation record lint (strict)"
Write-Host "[tests] 5.5/10 implementation record lint (strict)"
# IMPORTANT: strict mode is opt-in via OPENCLAW_IMPL_RECORD_PATH to avoid retroactive legacy record failures.
Invoke-Checked "implementation record lint" {
& $venvPython scripts\lint_implementation_record.py --path $env:OPENCLAW_IMPL_RECORD_PATH --strict
}
}
Write-Host "[tests] 5/9 backend real E2E lanes (R122/R123)"
Write-Host "[tests] 6/10 backend real E2E lanes (R122/R123)"
$env:MOLTBOT_STATE_DIR = "$root\moltbot_state\_local_backend_e2e_real"
Invoke-Checked "backend real E2E lane R122" {
& $venvPython scripts\run_unittests.py --module tests.test_r122_real_backend_lane --enforce-skip-policy tests\skip_policy.json --max-skipped 0
@@ -294,12 +299,12 @@ Invoke-Checked "backend real E2E lane R123" {
& $venvPython scripts\run_unittests.py --module tests.test_r123_real_backend_model_list_lane --enforce-skip-policy tests\skip_policy.json --max-skipped 0
}
Write-Host "[tests] 6/9 R121 retry partition contract"
Write-Host "[tests] 7/10 R121 retry partition contract"
Invoke-Checked "R121 retry partition contract" {
& $venvPython scripts\run_unittests.py --module tests.test_r121_retry_partition_contract --enforce-skip-policy tests\skip_policy.json --max-skipped 0
}
Write-Host "[tests] 7/9 Slack integration gates (R124/R125/R117/F57)"
Write-Host "[tests] 8/10 Slack integration gates (R124/R125/R117/F57)"
Invoke-Checked "Slack integration gates" {
& $venvPython scripts\run_unittests.py --module tests.test_r124_slack_ingress_contract --enforce-skip-policy tests\skip_policy.json --max-skipped 0
& $venvPython scripts\run_unittests.py --module tests.test_r125_slack_real_backend_lane --enforce-skip-policy tests\skip_policy.json --max-skipped 0
@@ -309,13 +314,13 @@ Invoke-Checked "Slack integration gates" {
& $venvPython scripts\run_unittests.py --module tests.test_f57_slack_socket_mode_startup --enforce-skip-policy tests\skip_policy.json --max-skipped 0
}
Write-Host "[tests] 8/9 R118 adversarial gate (adaptive: smoke/extended)"
Write-Host "[tests] 9/10 R118 adversarial gate (adaptive: smoke/extended)"
$env:MOLTBOT_STATE_DIR = "$root\moltbot_state\_local_adversarial"
Invoke-Checked "R118 adversarial adaptive" {
& $venvPython scripts\run_adversarial_gate.py --profile auto --seed 42 --artifact-dir .tmp\adversarial
}
Write-Host "[tests] 9/9 frontend E2E"
Write-Host "[tests] 10/10 frontend E2E"
$env:OPENCLAW_PLAYWRIGHT_INSTALL = "1"
$env:OPENCLAW_PLAYWRIGHT_BROWSERS = "chromium"
# IMPORTANT: full-gate acceptance must provision Playwright browsers itself; do
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
"""
R171: validate skip-policy and mutation-survivor debt metadata.
This script is intentionally stdlib-only so it can run early in local/full-test
gates before optional dependencies are installed.
"""
from __future__ import annotations
import argparse
import json
from datetime import date
from pathlib import Path, PurePosixPath
from typing import Any, Dict, Iterable, List, Optional, Tuple
def _read_json_object(path: Path) -> Dict[str, Any]:
payload = json.loads(path.read_text(encoding="utf-8-sig"))
if not isinstance(payload, dict):
raise ValueError(f"{path}: expected a JSON object")
return payload
def _normalize_repo_rel_path(path: str) -> str:
return PurePosixPath(path.replace("\\", "/")).as_posix().lstrip("./")
def _parse_review_after(value: Any, *, label: str, failures: List[str]) -> None:
if not isinstance(value, str) or not value.strip():
failures.append(f"{label}: missing review_after")
return
try:
review_after = date.fromisoformat(value)
except ValueError:
failures.append(f"{label}: invalid review_after '{value}'")
return
if review_after < date.today():
failures.append(f"{label}: review_after {value} is in the past")
def _validate_reason(value: Any, *, label: str, failures: List[str]) -> None:
if not isinstance(value, str) or not value.strip():
failures.append(f"{label}: missing non-empty reason")
def _resolve_test_module_path(repo_root: Path, module_name: str) -> Path:
return repo_root / Path(module_name.replace(".", "/")).with_suffix(".py")
def _validate_skip_policy(repo_root: Path, path: Path) -> List[str]:
failures: List[str] = []
payload = _read_json_object(path)
max_skipped = payload.get("max_skipped")
if not isinstance(max_skipped, int) or max_skipped < 0:
failures.append("skip policy: max_skipped must be a non-negative integer")
modules = payload.get("no_skip_modules", [])
if not isinstance(modules, list) or any(
not isinstance(item, str) or not item.strip() for item in modules
):
failures.append("skip policy: no_skip_modules must be a list of non-empty strings")
return failures
seen = set()
duplicates = set()
normalized_modules: List[str] = []
for module in modules:
normalized = module.strip()
normalized_modules.append(normalized)
if normalized in seen:
duplicates.add(normalized)
seen.add(normalized)
module_path = _resolve_test_module_path(repo_root, normalized)
if not module_path.is_file():
failures.append(
f"skip policy: module path does not exist for {normalized} -> {module_path.relative_to(repo_root)}"
)
if duplicates:
failures.append(
"skip policy: duplicate no-skip modules: " + ", ".join(sorted(duplicates))
)
metadata = payload.get("no_skip_module_metadata")
if not isinstance(metadata, dict):
failures.append(
"skip policy: no_skip_module_metadata must be an object keyed by module name"
)
return failures
metadata_keys = {str(key).strip() for key in metadata.keys()}
missing_metadata = [module for module in normalized_modules if module not in metadata_keys]
extra_metadata = sorted(
key for key in metadata_keys if key and key not in set(normalized_modules)
)
if missing_metadata:
failures.append(
"skip policy: missing metadata for no-skip modules: "
+ ", ".join(sorted(missing_metadata))
)
if extra_metadata:
failures.append(
"skip policy: stale metadata without matching no-skip module: "
+ ", ".join(extra_metadata)
)
for module_name in normalized_modules:
raw_meta = metadata.get(module_name)
label = f"skip policy metadata[{module_name}]"
if not isinstance(raw_meta, dict):
failures.append(f"{label}: metadata entry must be an object")
continue
_validate_reason(raw_meta.get("reason"), label=label, failures=failures)
_parse_review_after(
raw_meta.get("review_after"), label=label, failures=failures
)
return failures
def _validate_mutation_allowlist(repo_root: Path, path: Path) -> List[str]:
failures: List[str] = []
payload = _read_json_object(path)
entries = payload.get("entries", [])
if not isinstance(entries, list):
return ["mutation allowlist: entries must be a list"]
seen: set[Tuple[str, int]] = set()
duplicates: set[Tuple[str, int]] = set()
for index, raw_entry in enumerate(entries):
label = f"mutation allowlist entry[{index}]"
if not isinstance(raw_entry, dict):
failures.append(f"{label}: entry must be an object")
continue
file_path = _normalize_repo_rel_path(str(raw_entry.get("file", "")))
mutation_index = raw_entry.get("mutation_index")
if not file_path:
failures.append(f"{label}: missing file")
elif not (repo_root / Path(file_path)).is_file():
failures.append(f"{label}: file does not exist in repo: {file_path}")
if not isinstance(mutation_index, int) or mutation_index < 0:
failures.append(f"{label}: mutation_index must be a non-negative integer")
else:
key = (file_path, mutation_index)
if key in seen:
duplicates.add(key)
seen.add(key)
_validate_reason(raw_entry.get("reason"), label=label, failures=failures)
_parse_review_after(
raw_entry.get("review_after"), label=label, failures=failures
)
if duplicates:
failures.append(
"mutation allowlist: duplicate (file, mutation_index) entries: "
+ ", ".join(f"{file}@{mutation_index}" for file, mutation_index in sorted(duplicates))
)
return failures
def verify_test_debt_governance(
*,
repo_root: Path,
skip_policy_path: Path,
mutation_allowlist_path: Path,
) -> List[str]:
failures: List[str] = []
if not skip_policy_path.is_file():
failures.append(f"missing skip policy: {skip_policy_path}")
else:
try:
failures.extend(_validate_skip_policy(repo_root, skip_policy_path))
except Exception as exc:
failures.append(f"skip policy: failed to validate {skip_policy_path}: {exc}")
if not mutation_allowlist_path.is_file():
failures.append(f"missing mutation survivor allowlist: {mutation_allowlist_path}")
else:
try:
failures.extend(
_validate_mutation_allowlist(repo_root, mutation_allowlist_path)
)
except Exception as exc:
failures.append(
f"mutation allowlist: failed to validate {mutation_allowlist_path}: {exc}"
)
return failures
def main() -> int:
parser = argparse.ArgumentParser(
description="Validate skip-policy and mutation-survivor debt metadata."
)
parser.add_argument(
"--repo-root",
default=".",
help="Repository root used to resolve test modules and file paths.",
)
parser.add_argument(
"--skip-policy",
default="tests/skip_policy.json",
help="Path to tests/skip_policy.json",
)
parser.add_argument(
"--mutation-survivor-allowlist",
default="tests/mutation_survivor_allowlist.json",
help="Path to tests/mutation_survivor_allowlist.json",
)
args = parser.parse_args()
repo_root = Path(args.repo_root).resolve()
failures = verify_test_debt_governance(
repo_root=repo_root,
skip_policy_path=Path(args.skip_policy),
mutation_allowlist_path=Path(args.mutation_survivor_allowlist),
)
if failures:
for failure in failures:
print(f"TEST-DEBT-GOVERNANCE-FAIL: {failure}")
return 1
print(
"TEST-DEBT-GOVERNANCE-PASS: skip-policy and mutation-survivor debt metadata are current."
)
return 0
if __name__ == "__main__":
raise SystemExit(main())