← Files Zuora Coding AgentARCHIVED FILE
references/uat-test/execution/scripts/uat_verification.py
13.2 KB · Oct 2, 2026 · 00:30 UTC
#!/usr/bin/env python3
"""Read/write per-feature .uat-verification.json manifests."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
_EXECUTION_DIR = SCRIPT_DIR.parent
sys.path.insert(0, str(_EXECUTION_DIR))
sys.path.insert(0, str(_EXECUTION_DIR / "tests"))
from tests.test_utils.uat_root import find_git_root, find_uat_root # noqa: E402
TR_ENTRY_KEY_RE = re.compile(r"^TR\d+$", re.I)
UI_STATUS_CHOICES = ("passed", "failed", "skipped")
def _resolve_uat_root(args) -> Path:
if getattr(args, "uat_root", None):
return Path(args.uat_root).resolve()
if getattr(args, "git_root", None):
return find_uat_root(Path(args.git_root).resolve())
if getattr(args, "repo_root", None):
return Path(args.repo_root).resolve()
return find_uat_root(find_git_root())
def manifest_path(scenario_dir: Path) -> Path:
return scenario_dir / ".uat-verification.json"
def artifact_paths(uat_root: Path, feature: str, tr_number: int) -> list[Path]:
paths: list[Path] = []
plan_dirs = list((uat_root / "design" / "testplan").glob(f"*{feature}*_Test_Plan"))
if plan_dirs:
plan_file = plan_dirs[0] / f"tr{tr_number}_test_design.md"
if plan_file.is_file():
paths.append(plan_file)
scenario_dirs = list((uat_root / "execution" / "tests" / "test_scenarios").glob("test_*"))
scenario = None
norm = feature.lower()
for d in scenario_dirs:
core = d.name[5:]
if norm == core or norm.startswith(core) or core.startswith(norm):
scenario = d
break
if scenario:
for pattern in (f"*_tr{tr_number}_api.py", f"ui_steps_tr{tr_number}.md"):
for p in scenario.glob(pattern):
paths.append(p)
return paths
def compute_artifact_revision(uat_root: Path, feature: str, tr_number: int) -> str:
paths = artifact_paths(uat_root, feature, tr_number)
h = hashlib.sha256()
for p in sorted(paths, key=lambda x: str(x)):
h.update(str(p).encode())
h.update(p.read_bytes())
return h.hexdigest()[:12] if paths else ""
def load_manifest(scenario_dir: Path) -> dict:
path = manifest_path(scenario_dir)
if not path.is_file():
return {}
return json.loads(path.read_text(encoding="utf-8"))
def save_manifest(scenario_dir: Path, data: dict) -> None:
scenario_dir.mkdir(parents=True, exist_ok=True)
manifest_path(scenario_dir).write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
def tr_key(tr_number: int) -> str:
return f"TR{tr_number}"
def find_scenario_dir(uat_root: Path, feature: str) -> Path | None:
scenarios_root = uat_root / "execution" / "tests" / "test_scenarios"
norm = feature.lower()
for d in sorted(scenarios_root.glob("test_*")):
if not d.is_dir():
continue
core = d.name[5:]
if norm == core or norm.startswith(core) or core.startswith(norm):
return d
return None
def list_tr_numbers(uat_root: Path, feature: str, tr_filter: list[int] | None = None) -> list[int]:
trs: set[int] = set()
plan_dirs = list((uat_root / "design" / "testplan").glob(f"*{feature}*_Test_Plan"))
if plan_dirs:
for plan_file in plan_dirs[0].glob("tr*_test_design.md"):
match = re.search(r"tr(\d+)_test_design\.md$", plan_file.name, re.I)
if match:
trs.add(int(match.group(1)))
if not trs:
scenario = find_scenario_dir(uat_root, feature)
if scenario:
for api_file in scenario.glob("*_tr*_api.py"):
match = re.search(r"_tr(\d+)_api\.py$", api_file.name, re.I)
if match:
trs.add(int(match.group(1)))
result = sorted(trs)
if tr_filter is not None:
allowed = set(tr_filter)
result = [tr for tr in result if tr in allowed]
return result
def validate_manifest(data: dict) -> list[str]:
errors: list[str] = []
if not data:
errors.append("manifest is empty")
return errors
for key, entry in data.items():
if not TR_ENTRY_KEY_RE.match(str(key)):
errors.append(f"non-canonical top-level key: {key!r}")
continue
if not isinstance(entry, dict):
errors.append(f"{key} entry must be an object")
continue
if "verified" not in entry or not isinstance(entry["verified"], bool):
errors.append(f"{key} missing boolean verified field")
ui = entry.get("ui")
if ui is not None:
if not isinstance(ui, dict):
errors.append(f"{key}.ui must be an object")
elif ui.get("status") not in UI_STATUS_CHOICES:
errors.append(f"{key}.ui.status must be one of {UI_STATUS_CHOICES}")
return errors
def is_legacy_manifest(data: dict) -> bool:
if not data:
return False
return any(not TR_ENTRY_KEY_RE.match(str(key)) for key in data)
def _clear_tr(scenario_dir: Path, tr_number: int) -> dict:
data = load_manifest(scenario_dir)
key = tr_key(tr_number)
previous = data.get(key, {})
entry = {"verified": False}
if isinstance(previous, dict) and previous.get("ui"):
entry["ui"] = previous["ui"]
data[key] = entry
save_manifest(scenario_dir, data)
return data[key]
def _add_root_args(p):
p.add_argument("--git-root", help="Git project root (resolves UAT root)")
p.add_argument("--uat-root", help="UAT workspace root override")
p.add_argument("--repo-root", help="Deprecated: UAT workspace root")
def cmd_revision(args):
uat = _resolve_uat_root(args)
print(compute_artifact_revision(uat, args.feature, int(args.tr)))
def cmd_get(args):
data = load_manifest(Path(args.scenario_dir))
print(json.dumps(data.get(tr_key(int(args.tr)), {})))
def cmd_set(args):
scenario = Path(args.scenario_dir)
data = load_manifest(scenario)
key = tr_key(int(args.tr))
entry: dict = {"verified": args.verified.lower() == "true"}
if entry["verified"] and args.feature:
uat = _resolve_uat_root(args)
entry["verified_at"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
entry["artifact_revision"] = compute_artifact_revision(uat, args.feature, int(args.tr))
elif not entry["verified"] and args.reason:
entry["reason"] = args.reason
previous = data.get(key, {})
if isinstance(previous, dict) and previous.get("ui"):
entry["ui"] = previous["ui"]
data[key] = entry
save_manifest(scenario, data)
print(json.dumps(entry))
def cmd_clear(args):
scenario = Path(args.scenario_dir)
entry = _clear_tr(scenario, int(args.tr))
print(json.dumps(entry))
def cmd_finalize_generate(args):
uat = _resolve_uat_root(args)
scenario = find_scenario_dir(uat, args.feature)
if scenario is None:
print(json.dumps({"feature": args.feature, "verification": {}, "error": "scenario_dir_not_found"}))
sys.exit(1)
tr_filter = [int(tr) for tr in args.tr] if args.tr else None
tr_numbers = list_tr_numbers(uat, args.feature, tr_filter)
verification: dict[str, dict] = {}
if args.verify.lower() == "false":
for tr_number in tr_numbers:
verification[tr_key(tr_number)] = _clear_tr(scenario, tr_number)
manifest = load_manifest(scenario)
validation_errors = validate_manifest(manifest)
print(
json.dumps(
{
"feature": args.feature,
"verification": verification,
"manifest_path": str(manifest_path(scenario)),
"validation_errors": validation_errors,
}
)
)
def cmd_should_verify(args):
data = load_manifest(Path(args.scenario_dir))
entry = data.get(tr_key(int(args.tr)), {})
if not entry.get("verified"):
sys.exit(0)
if not args.feature:
sys.exit(1)
uat = _resolve_uat_root(args)
current = compute_artifact_revision(uat, args.feature, int(args.tr))
stored = entry.get("artifact_revision", "")
sys.exit(0 if current != stored else 1)
def cmd_validate(args):
scenario = Path(args.scenario_dir)
if not manifest_path(scenario).is_file():
print(json.dumps({"valid": False, "errors": ["manifest_missing"], "legacy": False}))
sys.exit(1)
data = load_manifest(scenario)
errors = validate_manifest(data)
payload = {
"valid": not errors,
"errors": errors,
"legacy": is_legacy_manifest(data),
"manifest_path": str(manifest_path(scenario)),
}
print(json.dumps(payload))
sys.exit(0 if payload["valid"] else 1)
def cmd_ensure_manifest(args):
uat = _resolve_uat_root(args)
scenario = find_scenario_dir(uat, args.feature)
if scenario is None:
print(json.dumps({"feature": args.feature, "error": "scenario_dir_not_found"}))
sys.exit(1)
tr_filter = [int(tr) for tr in args.tr] if args.tr else None
tr_numbers = list_tr_numbers(uat, args.feature, tr_filter)
path = manifest_path(scenario)
recreated = False
backup_path = None
added: dict[str, dict] = {}
if path.is_file():
data = load_manifest(scenario)
errors = validate_manifest(data)
if errors or is_legacy_manifest(data):
backup_path = path.with_suffix(".json.bak")
backup_path.write_text(path.read_text(encoding="utf-8"), encoding="utf-8")
save_manifest(scenario, {})
recreated = True
data = {}
for tr_number in tr_numbers:
key = tr_key(tr_number)
if key not in data:
added[key] = _clear_tr(scenario, tr_number)
else:
recreated = True
for tr_number in tr_numbers:
key = tr_key(tr_number)
added[key] = _clear_tr(scenario, tr_number)
validation_errors = validate_manifest(load_manifest(scenario))
print(
json.dumps(
{
"feature": args.feature,
"manifest_path": str(path),
"recreated": recreated,
"backup_path": str(backup_path) if backup_path else None,
"added": added,
"validation_errors": validation_errors,
}
)
)
if validation_errors:
sys.exit(1)
def cmd_record_ui_result(args):
scenario = Path(args.scenario_dir)
data = load_manifest(scenario)
key = tr_key(int(args.tr))
entry = data.get(key, {"verified": False})
if not isinstance(entry, dict):
entry = {"verified": False}
ui_entry: dict = {
"status": args.status,
"recorded_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
}
if args.evidence:
ui_entry["evidence"] = args.evidence
if args.summary:
ui_entry["summary"] = args.summary
if args.reason:
ui_entry["reason"] = args.reason
entry["ui"] = ui_entry
data[key] = entry
save_manifest(scenario, data)
print(json.dumps({"tr": key, "ui": ui_entry}))
def main():
parser = argparse.ArgumentParser(description="UAT verification manifest helpers")
sub = parser.add_subparsers(dest="command", required=True)
p = sub.add_parser("revision")
_add_root_args(p)
p.add_argument("--feature", required=True)
p.add_argument("--tr", required=True)
p.set_defaults(func=cmd_revision)
p = sub.add_parser("get")
p.add_argument("--scenario-dir", required=True)
p.add_argument("--tr", required=True)
p.set_defaults(func=cmd_get)
p = sub.add_parser("set")
_add_root_args(p)
p.add_argument("--scenario-dir", required=True)
p.add_argument("--tr", required=True)
p.add_argument("--verified", required=True, choices=["true", "false"])
p.add_argument("--feature")
p.add_argument("--reason")
p.set_defaults(func=cmd_set)
p = sub.add_parser("clear")
p.add_argument("--scenario-dir", required=True)
p.add_argument("--tr", required=True)
p.set_defaults(func=cmd_clear)
p = sub.add_parser("should-verify")
_add_root_args(p)
p.add_argument("--scenario-dir", required=True)
p.add_argument("--tr", required=True)
p.add_argument("--feature")
p.set_defaults(func=cmd_should_verify)
p = sub.add_parser("finalize-generate")
_add_root_args(p)
p.add_argument("--feature", required=True)
p.add_argument("--verify", required=True, choices=["true", "false"])
p.add_argument("--tr", action="append")
p.set_defaults(func=cmd_finalize_generate)
p = sub.add_parser("validate")
p.add_argument("--scenario-dir", required=True)
p.set_defaults(func=cmd_validate)
p = sub.add_parser("ensure-manifest")
_add_root_args(p)
p.add_argument("--feature", required=True)
p.add_argument("--tr", action="append")
p.set_defaults(func=cmd_ensure_manifest)
p = sub.add_parser("record-ui-result")
p.add_argument("--scenario-dir", required=True)
p.add_argument("--tr", required=True)
p.add_argument("--status", required=True, choices=list(UI_STATUS_CHOICES))
p.add_argument("--evidence")
p.add_argument("--summary")
p.add_argument("--reason")
p.set_defaults(func=cmd_record_ui_result)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()
SHA-256: 51e19b32aafa3e73da9b890bb559fd2e5055b1fdab0fc5aedb40d40a84994d3e