Files
call-center/scripts/uat_manual_prepare.py
T

330 lines
11 KiB
Python

from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
import shutil
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_EVIDENCE_ROOT = ROOT / "docs" / "uat" / "evidence"
DEFAULT_TEMPLATE_ROOT = ROOT / "docs" / "uat"
def utc_now() -> datetime:
return datetime.now(timezone.utc)
def now_compact() -> str:
return utc_now().strftime("%Y%m%d_%H%M%S")
def iso_now() -> str:
return utc_now().isoformat()
def find_latest_preflight(evidence_root: Path) -> Path:
candidates = sorted(
evidence_root.glob("preflight_*.md"),
key=lambda path: path.stat().st_mtime,
reverse=True,
)
if not candidates:
raise FileNotFoundError("No preflight report found in docs/uat/evidence")
return candidates[0]
def find_latest_dry_run_dir(evidence_root: Path) -> Path:
candidates = sorted(
[path for path in evidence_root.glob("dry_run_*") if path.is_dir()],
key=lambda path: path.stat().st_mtime,
reverse=True,
)
if not candidates:
raise FileNotFoundError("No dry-run directory found in docs/uat/evidence")
return candidates[0]
def _replace_line(content: str, prefix: str, value: str) -> str:
replacement = f"{prefix} {value}".rstrip()
return content.replace(prefix, replacement, 1)
def render_session_protocol(
template_text: str,
*,
session_id: str,
environment_url: str,
build_version: str,
deployment_date: str,
cluster_namespace: str,
participants: dict[str, str],
preflight_name: str,
defect_log_name: str,
) -> str:
content = template_text
today = utc_now().date().isoformat()
content = _replace_line(content, "- Session ID:", session_id)
content = _replace_line(content, "- Date:", today)
content = _replace_line(content, "- Start time:", "")
content = _replace_line(content, "- End time:", "")
content = _replace_line(content, "- Environment URL:", environment_url)
content = _replace_line(content, "- Build/version:", build_version)
content = _replace_line(content, "- Deployment date:", deployment_date)
content = _replace_line(content, "- Cluster/namespace:", cluster_namespace)
field_map = {
"- Operators:": participants["operators"],
"- Supervisor:": participants["supervisor"],
"- Analyst:": participants["analyst"],
"- Admin:": participants["admin"],
"- Business owner:": participants["business_owner"],
"- IT owner:": participants["it_owner"],
}
for prefix, value in field_map.items():
content = _replace_line(content, prefix, value)
content = content.replace(
"- [ ] Preflight report attached (`docs/uat/evidence/preflight_*.md`)",
f"- [x] Preflight report attached (`attachments/{preflight_name}`)",
1,
)
content = content.replace(
"- [ ] Defect log prepared from `docs/uat/defect-log-template.csv`",
f"- [x] Defect log prepared from `{defect_log_name}`",
1,
)
return content
def render_signoff_sheet(
template_text: str,
*,
session_id: str,
environment_url: str,
build_version: str,
) -> str:
lines = template_text.splitlines()
if not lines:
return template_text
header = [
lines[0],
"",
f"- Session ID: {session_id}",
f"- Environment URL: {environment_url}",
f"- Build/version: {build_version}",
f"- Prepared at (UTC): {iso_now()}",
"",
]
return "\n".join(header + lines[1:])
def build_manual_readme(
*,
session_id: str,
environment_url: str,
preflight_name: str,
dry_run_name: str,
) -> str:
lines = [
"# Manual UAT Session Bundle",
"",
f"- Session ID: {session_id}",
f"- Environment URL: {environment_url}",
f"- Prepared at (UTC): {iso_now()}",
"",
"## Included Files",
"",
"- `session-protocol.md`",
"- `scenario-checklist.md`",
"- `defect-log.csv`",
"- `signoff-sheet.md`",
"- `manifest.json`",
"",
"## Attached Evidence",
"",
f"- `attachments/{preflight_name}`",
f"- `attachments/{dry_run_name}/`",
"",
"## Next Step",
"",
"Run the live manual UAT on the pilot gateway, record any defects, and use the",
"`scripts/finalize_mvp_pilot.py` command only after signatures are captured and",
"all open P1/P2 defects are closed.",
]
return "\n".join(lines)
def prepare_manual_bundle(
*,
session_id: str,
environment_url: str,
build_version: str,
deployment_date: str,
cluster_namespace: str,
participants: dict[str, str],
output_root: Path,
template_root: Path,
preflight_report: Path,
dry_run_dir: Path,
overwrite: bool = False,
) -> Path:
session_dir = output_root / f"manual_{session_id}"
if session_dir.exists():
if not overwrite:
raise FileExistsError(f"Session directory already exists: {session_dir}")
shutil.rmtree(session_dir)
attachments_dir = session_dir / "attachments"
attachments_dir.mkdir(parents=True, exist_ok=True)
copied_preflight = attachments_dir / preflight_report.name
shutil.copy2(preflight_report, copied_preflight)
copied_dry_run = attachments_dir / dry_run_dir.name
shutil.copytree(dry_run_dir, copied_dry_run)
session_template = (template_root / "session-template.md").read_text(encoding="utf-8")
signoff_template = (template_root / "signoff-template.md").read_text(encoding="utf-8")
checklist_template = (template_root / "scenario-checklist.md").read_text(encoding="utf-8")
defect_template = (template_root / "defect-log-template.csv").read_text(encoding="utf-8")
rendered_session = render_session_protocol(
session_template,
session_id=session_id,
environment_url=environment_url,
build_version=build_version,
deployment_date=deployment_date,
cluster_namespace=cluster_namespace,
participants=participants,
preflight_name=preflight_report.name,
defect_log_name="defect-log.csv",
)
rendered_signoff = render_signoff_sheet(
signoff_template,
session_id=session_id,
environment_url=environment_url,
build_version=build_version,
)
(session_dir / "session-protocol.md").write_text(rendered_session, encoding="utf-8")
(session_dir / "signoff-sheet.md").write_text(rendered_signoff, encoding="utf-8")
(session_dir / "scenario-checklist.md").write_text(checklist_template, encoding="utf-8")
(session_dir / "defect-log.csv").write_text(defect_template, encoding="utf-8")
manifest: dict[str, Any] = {
"prepared_at": iso_now(),
"session_id": session_id,
"environment_url": environment_url,
"build_version": build_version,
"deployment_date": deployment_date,
"cluster_namespace": cluster_namespace,
"participants": participants,
"artifacts": {
"preflight_report": f"attachments/{preflight_report.name}",
"dry_run_bundle": f"attachments/{dry_run_dir.name}",
"session_protocol": "session-protocol.md",
"scenario_checklist": "scenario-checklist.md",
"defect_log": "defect-log.csv",
"signoff_sheet": "signoff-sheet.md",
},
}
(session_dir / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
(session_dir / "README.md").write_text(
build_manual_readme(
session_id=session_id,
environment_url=environment_url,
preflight_name=preflight_report.name,
dry_run_name=dry_run_dir.name,
),
encoding="utf-8",
)
return session_dir
def main() -> None:
parser = argparse.ArgumentParser(description="Prepare a manual UAT bundle from the latest preflight and dry-run")
parser.add_argument("--session-id", default=f"UAT-MANUAL-{now_compact()}", help="Manual UAT session id")
parser.add_argument(
"--environment-url",
default="http://<pilot-gateway>:8080",
help="Pilot gateway URL recorded in the session pack",
)
parser.add_argument("--build-version", default="v1.0.0-mvp", help="Build/version under test")
parser.add_argument(
"--deployment-date",
default=utc_now().date().isoformat(),
help="Deployment date recorded in the session pack",
)
parser.add_argument(
"--cluster-namespace",
default="pilot/production-like",
help="Cluster or namespace label recorded in the session pack",
)
parser.add_argument("--operators", default="TBD", help="Operator participants")
parser.add_argument("--supervisor", default="TBD", help="Supervisor participant")
parser.add_argument("--analyst", default="TBD", help="Analyst participant")
parser.add_argument("--admin", default="TBD", help="Admin participant")
parser.add_argument("--business-owner", default="TBD", help="Business owner")
parser.add_argument("--it-owner", default="TBD", help="IT owner")
parser.add_argument(
"--preflight-report",
default="",
help="Optional explicit preflight report path (default: latest preflight report)",
)
parser.add_argument(
"--dry-run-dir",
default="",
help="Optional explicit dry-run directory path (default: latest dry-run evidence folder)",
)
parser.add_argument(
"--output-root",
default="",
help="Output root directory (default: docs/uat/evidence)",
)
parser.add_argument("--overwrite", action="store_true", help="Overwrite the session folder if it already exists")
args = parser.parse_args()
output_root = Path(args.output_root) if args.output_root else DEFAULT_EVIDENCE_ROOT
output_root = output_root if output_root.is_absolute() else (ROOT / output_root).resolve()
preflight_report = Path(args.preflight_report) if args.preflight_report else find_latest_preflight(DEFAULT_EVIDENCE_ROOT)
if not preflight_report.is_absolute():
preflight_report = (ROOT / preflight_report).resolve()
dry_run_dir = Path(args.dry_run_dir) if args.dry_run_dir else find_latest_dry_run_dir(DEFAULT_EVIDENCE_ROOT)
if not dry_run_dir.is_absolute():
dry_run_dir = (ROOT / dry_run_dir).resolve()
participants = {
"operators": args.operators,
"supervisor": args.supervisor,
"analyst": args.analyst,
"admin": args.admin,
"business_owner": args.business_owner,
"it_owner": args.it_owner,
}
session_dir = prepare_manual_bundle(
session_id=args.session_id,
environment_url=args.environment_url,
build_version=args.build_version,
deployment_date=args.deployment_date,
cluster_namespace=args.cluster_namespace,
participants=participants,
output_root=output_root,
template_root=DEFAULT_TEMPLATE_ROOT,
preflight_report=preflight_report,
dry_run_dir=dry_run_dir,
overwrite=args.overwrite,
)
print(f"Manual UAT bundle prepared: {session_dir}")
if __name__ == "__main__":
main()