#!/usr/bin/env python3 """Controlled public smoke for extension ConfigCASSave preparation. The smoke uses no storage coordinates. It creates one extension saved-state copy, verifies readback, rolls it back by opaque receipt, then proves that the same public selector is immediately ready for another prepare. """ from __future__ import annotations import argparse import json import sys import time from pathlib import Path from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen def rpc(base_url: str, method: str, payload: dict, timeout: float) -> dict: request = Request( base_url.rstrip("/") + "/rpc", data=json.dumps({"method": method, "payload": payload}, ensure_ascii=False).encode("utf-8"), method="POST", headers={"Content-Type": "application/json; charset=utf-8"}, ) try: with urlopen(request, timeout=timeout) as response: result = json.loads(response.read().decode("utf-8")) except (HTTPError, URLError) as exc: raise AssertionError(f"{method} transport failure: {exc}") from exc if not isinstance(result, dict): raise AssertionError(f"{method} returned a non-object response") return result def require(condition: bool, message: str) -> None: if not condition: raise AssertionError(message) def main() -> int: parser = argparse.ArgumentParser(description="Public extension saved-state prepare/rollback smoke.") parser.add_argument("--base-url", default="http://docker.cin.su:8011") parser.add_argument("--base-id", default="upo_test") parser.add_argument("--extension", default="фс_ДоработкиОбщее") parser.add_argument("--ref", default="Catalog.Номенклатура") parser.add_argument("--timeout", type=float, default=120.0) parser.add_argument("--report", type=Path) parser.add_argument("--apply", action="store_true", help="Perform the controlled SQL prepare and rollback.") args = parser.parse_args() report: dict = { "schema": "onec_extension_saved_state_prepare_smoke.v1", "base_url": args.base_url, "base_id": args.base_id, "extension": args.extension, "ref": args.ref, "status": "pending", "passed": False, } target = { "base_id": args.base_id, "extension": args.extension, "ref": args.ref, "layer": "extension_saved_state", } try: initial = rpc(args.base_url, "metadata.saved_state.prepare", target | {"mode": "plan"}, args.timeout) report["initial_plan"] = {"status": initial.get("status"), "counts": initial.get("counts")} require(initial.get("status") == "plan_ready", f"initial plan must be plan_ready, got {initial.get('status')}") if not args.apply: report["status"] = "plan_ready" report["passed"] = True else: applied = rpc( args.base_url, "metadata.saved_state.prepare", target | {"mode": "apply_and_verify", "allow_sql_saved_state_prepare": True}, args.timeout, ) report["prepare"] = {"status": applied.get("status"), "counts": applied.get("counts"), "verification": applied.get("verification")} require(applied.get("status") == "verified" and applied.get("applied") is True, "prepare must be verified") receipt_id = str(applied.get("prepare_receipt_id") or "") require(receipt_id, "prepare response must include an opaque receipt") rolled_back = rpc( args.base_url, "metadata.saved_state.ensure.rollback", {"base_id": args.base_id, "prepare_receipt_id": receipt_id, "allow_sql_saved_state_rollback": True}, args.timeout, ) report["rollback"] = {"status": rolled_back.get("status"), "counts": rolled_back.get("counts")} require(rolled_back.get("status") == "rolled_back" and rolled_back.get("applied") is True, "prepare rollback must succeed") started = time.monotonic() final_plan = rpc(args.base_url, "metadata.saved_state.prepare", target | {"mode": "plan"}, args.timeout) elapsed_ms = round((time.monotonic() - started) * 1000, 1) report["post_rollback_plan"] = {"status": final_plan.get("status"), "counts": final_plan.get("counts"), "elapsed_ms": elapsed_ms} require(final_plan.get("status") == "plan_ready", f"post-rollback plan must be plan_ready, got {final_plan.get('status')}") require(int((final_plan.get("counts") or {}).get("existing_saved_records") or 0) == 0, "rollback must leave no saved records") report["status"] = "verified_and_rolled_back" report["passed"] = True except Exception as exc: report["status"] = "failed" report["error"] = str(exc) if args.report: args.report.parent.mkdir(parents=True, exist_ok=True) args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps(report, ensure_ascii=False, indent=2)) return 0 if report["passed"] else 1 if __name__ == "__main__": sys.exit(main())