Files
llm/scripts/check_1c_saved_state_strict_readiness.py
T

179 lines
7.9 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import sys
import urllib.request
from pathlib import Path
from typing import Any
DEFAULT_BASE_URL = "http://docker-gpu.cin.su:8011"
DEFAULT_TABLES = ("ConfigCASSave", "ConfigSave")
ALLOWED_TABLES = {"ConfigCASSave", "ConfigSave"}
def request_json(method: str, url: str, *, payload: dict[str, Any] | None, timeout: float) -> tuple[int, dict[str, Any] | str]:
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload is not None else None
request = urllib.request.Request(
url,
data=data,
method=method,
headers={"Content-Type": "application/json; charset=utf-8"},
)
with urllib.request.urlopen(request, timeout=timeout) as response:
raw = response.read().decode("utf-8")
return response.status, json.loads(raw) if raw else {}
def rpc(base_url: str, method: str, payload: dict[str, Any], timeout: float) -> dict[str, Any]:
status, response = request_json(
"POST",
base_url.rstrip("/") + "/rpc",
payload={"method": method, "payload": payload},
timeout=timeout,
)
if status != 200 or not isinstance(response, dict):
raise RuntimeError(f"{method} failed: status={status}, response={response!r}")
return response
def health_summary(base_url: str, base_id: str, timeout: float) -> dict[str, Any]:
status, response = request_json("GET", f"{base_url.rstrip('/')}/health?base_id={base_id}", payload=None, timeout=timeout)
if status != 200 or not isinstance(response, dict):
return {"status": "error", "http_status": status, "response": response}
live_sql = response.get("live_sql") if isinstance(response.get("live_sql"), dict) else {}
return {
"status": response.get("status"),
"contract_version": response.get("contract_version"),
"live_sql": {
"configured": live_sql.get("configured"),
"server": live_sql.get("server"),
"database": live_sql.get("database"),
},
}
def saved_state_row_counts(base_url: str, base_id: str, tables: list[str], timeout: float) -> dict[str, Any]:
selects = [f"SELECT '{table}' AS TableName, COUNT(*) AS RowsCount FROM {table}" for table in tables]
result = rpc(
base_url,
"query.run",
{
"base_id": base_id,
"diagnostic": True,
"query": "\nUNION ALL\n".join(selects),
"timeout_seconds": int(timeout),
},
timeout,
)
counts: dict[str, int] = {table: 0 for table in tables}
for row in result.get("rows") or []:
if not isinstance(row, dict):
continue
table = str(row.get("TableName") or "")
if table in counts and isinstance(row.get("RowsCount"), int):
counts[table] = int(row["RowsCount"])
return {
"status": result.get("status"),
"validation": result.get("validation"),
"counts": counts,
"raw_counts": result.get("counts"),
}
def search_saved_state(base_url: str, base_id: str, tables: list[str], timeout: float) -> dict[str, Any]:
common = {"base_id": base_id, "tables": tables, "limit": 3, "timeout_seconds": int(timeout)}
forms = rpc(base_url, "metadata.saved_state.forms.search", common, timeout)
modules = rpc(base_url, "metadata.saved_state.modules.search", {**common, "scan_limit": 100}, timeout)
return {
"forms": {
"status": forms.get("status"),
"counts": forms.get("counts"),
"sample": forms.get("forms") or [],
},
"modules": {
"status": modules.get("status"),
"counts": modules.get("counts"),
"sample": modules.get("modules") or [],
},
}
def build_report(base_url: str, base_id: str, tables: list[str], timeout: float, saved_state_table: str | None) -> dict[str, Any]:
report: dict[str, Any] = {
"schema": "onec_saved_state_strict_readiness.v1",
"base_url": base_url,
"base_id": base_id,
"saved_state_table": saved_state_table,
"tables": tables,
"ready": False,
"status": "error",
"checks": {},
"recommendations": [],
}
report["checks"]["health"] = health_summary(base_url, base_id, timeout)
report["checks"]["row_counts"] = saved_state_row_counts(base_url, base_id, tables, timeout)
report["checks"]["saved_state_search"] = search_saved_state(base_url, base_id, tables, timeout)
row_counts = (report["checks"]["row_counts"] or {}).get("counts") or {}
forms_counts = (((report["checks"]["saved_state_search"] or {}).get("forms") or {}).get("counts") or {})
modules_counts = (((report["checks"]["saved_state_search"] or {}).get("modules") or {}).get("counts") or {})
total_rows = sum(value for value in row_counts.values() if isinstance(value, int))
forms = forms_counts.get("forms") if isinstance(forms_counts.get("forms"), int) else 0
modules = modules_counts.get("modules") if isinstance(modules_counts.get("modules"), int) else 0
report["summary"] = {
"saved_state_rows": total_rows,
"forms": forms,
"modules": modules,
}
if forms > 0 and modules > 0:
report["ready"] = True
report["status"] = "ready"
report["recommendations"].append("Strict saved-state smoke can be attempted: form and module saved-state candidates are present in the selected save layer.")
elif total_rows == 0:
report["status"] = "blocked_no_saved_state_rows"
report["recommendations"].append(
f"No unactivated Configurator changes are present in {', '.join(tables)}. To prepare a strict write test, copy the target object from Config/ConfigCAS into the selected save layer using the approved saved-state workflow, then rerun readiness."
)
else:
report["status"] = "blocked_no_strict_candidates"
report["recommendations"].append(
"Saved-state rows exist, but the adapter did not find both form and module candidates needed by strict write-and-rollback smoke."
)
return report
def main() -> int:
parser = argparse.ArgumentParser(description="Read-only readiness check for strict 1C saved-state smoke tests.")
parser.add_argument("--base-url", default=DEFAULT_BASE_URL)
parser.add_argument("--base-id", default="upo_test")
parser.add_argument("--table", action="append", choices=sorted(ALLOWED_TABLES), help="Saved-state table to inspect. Repeatable.")
parser.add_argument("--saved-state-table", choices=sorted(ALLOWED_TABLES), help="Expected saved-state table for the strict smoke target.")
parser.add_argument("--timeout", type=float, default=30.0)
parser.add_argument("--report", type=Path)
parser.add_argument("--require-ready", action="store_true", help="Exit non-zero when strict saved-state smoke is not ready.")
parser.add_argument("--json", action="store_true", help="Print full JSON report.")
args = parser.parse_args()
if args.saved_state_table and args.table and any(table != args.saved_state_table for table in args.table):
parser.error("--saved-state-table must match --table when both are provided")
tables = args.table or ([args.saved_state_table] if args.saved_state_table else list(DEFAULT_TABLES))
report = build_report(args.base_url, args.base_id, tables, args.timeout, args.saved_state_table)
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")
if args.json or args.require_ready or not report["ready"]:
print(json.dumps(report, ensure_ascii=True, indent=2), file=sys.stderr if args.require_ready and not report["ready"] else sys.stdout)
else:
print(f"OK: strict saved-state smoke readiness passed for {args.base_id}.")
return 0 if report["ready"] or not args.require_ready else 1
if __name__ == "__main__":
raise SystemExit(main())