Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_REST_ADAPTER_URL = "http://docker-gpu.cin.su:8011"
|
||||
DEFAULT_MCP_URL = "http://docker.cin.su:8021"
|
||||
SAVED_STATE_TABLES = ("ConfigSave", "ConfigCASSave")
|
||||
|
||||
|
||||
def duplicate_values(values: list[str]) -> list[str]:
|
||||
seen: set[str] = set()
|
||||
duplicates: set[str] = set()
|
||||
for value in values:
|
||||
if value in seen:
|
||||
duplicates.add(value)
|
||||
seen.add(value)
|
||||
return sorted(duplicates)
|
||||
|
||||
|
||||
def trim_output(value: str, max_chars: int) -> tuple[str, bool]:
|
||||
if max_chars <= 0 or len(value) <= max_chars:
|
||||
return value, False
|
||||
return value[-max_chars:], True
|
||||
|
||||
|
||||
def parse_json_output(value: str) -> dict[str, Any] | None:
|
||||
text = value.strip()
|
||||
if not text or not text.startswith("{"):
|
||||
return None
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
|
||||
|
||||
def parsed_summary(parsed: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
if not isinstance(parsed, dict):
|
||||
return None
|
||||
result: dict[str, Any] = {"schema": parsed.get("schema")}
|
||||
for key in ("passed", "status", "failures", "issues"):
|
||||
if key in parsed:
|
||||
value = parsed.get(key)
|
||||
if isinstance(value, list):
|
||||
result[f"{key}_count"] = len(value)
|
||||
else:
|
||||
result[key] = value
|
||||
checks = parsed.get("checks")
|
||||
if isinstance(checks, dict):
|
||||
result["checks_count"] = len(checks)
|
||||
if all(isinstance(value, bool) for value in checks.values()):
|
||||
result["checks_failed"] = sorted(key for key, value in checks.items() if value is not True)
|
||||
counts = parsed.get("counts")
|
||||
if isinstance(counts, dict):
|
||||
result["counts"] = counts
|
||||
bases = parsed.get("bases")
|
||||
if isinstance(bases, dict):
|
||||
result["bases_count"] = len(bases)
|
||||
result["bases"] = {
|
||||
str(base_id): compact_base_summary(base)
|
||||
for base_id, base in bases.items()
|
||||
if isinstance(base, dict)
|
||||
}
|
||||
strict_codes = parsed.get("strict_skip_failure_codes")
|
||||
if isinstance(strict_codes, list):
|
||||
result["strict_skip_failure_codes"] = strict_codes
|
||||
coverage_codes = parsed.get("coverage_failure_codes")
|
||||
if isinstance(coverage_codes, list):
|
||||
result["coverage_failure_codes"] = coverage_codes
|
||||
consistency_codes = parsed.get("consistency_failure_codes")
|
||||
if isinstance(consistency_codes, list):
|
||||
result["consistency_failure_codes"] = consistency_codes
|
||||
safety_codes = parsed.get("safety_failure_codes")
|
||||
if isinstance(safety_codes, list):
|
||||
result["safety_failure_codes"] = safety_codes
|
||||
rollback_safety_codes = parsed.get("rollback_safety_failure_codes")
|
||||
if isinstance(rollback_safety_codes, list):
|
||||
result["rollback_safety_failure_codes"] = rollback_safety_codes
|
||||
saved_state_diff_codes = parsed.get("saved_state_diff_failure_codes")
|
||||
if isinstance(saved_state_diff_codes, list):
|
||||
result["saved_state_diff_failure_codes"] = saved_state_diff_codes
|
||||
schema_codes = parsed.get("schema_failure_codes")
|
||||
if isinstance(schema_codes, list):
|
||||
result["schema_failure_codes"] = schema_codes
|
||||
identity_codes = parsed.get("identity_failure_codes")
|
||||
if isinstance(identity_codes, list):
|
||||
result["identity_failure_codes"] = identity_codes
|
||||
endpoint_codes = parsed.get("endpoint_failure_codes")
|
||||
if isinstance(endpoint_codes, list):
|
||||
result["endpoint_failure_codes"] = endpoint_codes
|
||||
duplicate_codes = parsed.get("duplicate_failure_codes")
|
||||
if isinstance(duplicate_codes, list):
|
||||
result["duplicate_failure_codes"] = duplicate_codes
|
||||
saved_state_codes = parsed.get("saved_state_failure_codes")
|
||||
if isinstance(saved_state_codes, list):
|
||||
result["saved_state_failure_codes"] = saved_state_codes
|
||||
saved_state_strict_readiness_codes = parsed.get("saved_state_strict_readiness_failure_codes")
|
||||
if isinstance(saved_state_strict_readiness_codes, list):
|
||||
result["saved_state_strict_readiness_failure_codes"] = saved_state_strict_readiness_codes
|
||||
saved_state_copy_plan_codes = parsed.get("saved_state_copy_plan_failure_codes")
|
||||
if isinstance(saved_state_copy_plan_codes, list):
|
||||
result["saved_state_copy_plan_failure_codes"] = saved_state_copy_plan_codes
|
||||
saved_state_table_codes = parsed.get("saved_state_table_failure_codes")
|
||||
if isinstance(saved_state_table_codes, list):
|
||||
result["saved_state_table_failure_codes"] = saved_state_table_codes
|
||||
staleness_codes = parsed.get("staleness_failure_codes")
|
||||
if isinstance(staleness_codes, list):
|
||||
result["staleness_failure_codes"] = staleness_codes
|
||||
return result
|
||||
|
||||
|
||||
def compact_base_summary(base: dict[str, Any]) -> dict[str, Any]:
|
||||
reports = base.get("reports") if isinstance(base.get("reports"), dict) else {}
|
||||
summary: dict[str, Any] = {"passed": base.get("passed")}
|
||||
selector_chain: dict[str, Any] = {}
|
||||
write_plan_safety: dict[str, Any] = {}
|
||||
write_rollback_safety: dict[str, Any] = {}
|
||||
saved_state_diff: dict[str, Any] = {}
|
||||
saved_state: dict[str, Any] = {}
|
||||
|
||||
for name, report in reports.items():
|
||||
if not isinstance(report, dict):
|
||||
continue
|
||||
if name.startswith("selector_chain_"):
|
||||
transport = name.removeprefix("selector_chain_")
|
||||
selector_chain[transport] = {
|
||||
"passed": report.get("passed"),
|
||||
"base_id": report.get("base_id"),
|
||||
"transport": report.get("transport"),
|
||||
"endpoint_url": report.get("endpoint_url"),
|
||||
"resolve_overrides_status": report.get("resolve_overrides_status"),
|
||||
"write_plan_evidence": report.get("write_plan_evidence"),
|
||||
"next_method": report.get("next_method"),
|
||||
"saved_state_status": report.get("saved_state_status"),
|
||||
"saved_state_modules": report.get("saved_state_modules"),
|
||||
"write_plan_target": report.get("write_plan_target"),
|
||||
"composition_status": report.get("composition_status"),
|
||||
"composed": report.get("composed"),
|
||||
}
|
||||
elif name.startswith("write_plan_safety_"):
|
||||
transport = name.removeprefix("write_plan_safety_")
|
||||
write_plan_safety[transport] = {
|
||||
"status": report.get("status"),
|
||||
"checks": report.get("checks"),
|
||||
"base_id": report.get("base_id"),
|
||||
"transport": report.get("transport"),
|
||||
"endpoint_url": report.get("endpoint_url"),
|
||||
}
|
||||
elif name.startswith("write_rollback_safety_"):
|
||||
transport = name.removeprefix("write_rollback_safety_")
|
||||
write_rollback_safety[transport] = {
|
||||
"status": report.get("status"),
|
||||
"checks": report.get("checks"),
|
||||
"base_id": report.get("base_id"),
|
||||
"transport": report.get("transport"),
|
||||
"endpoint_url": report.get("endpoint_url"),
|
||||
}
|
||||
elif name.startswith("saved_state_diff_"):
|
||||
transport = name.removeprefix("saved_state_diff_")
|
||||
saved_state_diff[transport] = {
|
||||
"status": report.get("status"),
|
||||
"checks": report.get("checks"),
|
||||
"base_id": report.get("base_id"),
|
||||
"transport": report.get("transport"),
|
||||
"endpoint_url": report.get("endpoint_url"),
|
||||
"saved_state_table": report.get("saved_state_table"),
|
||||
"diff_status": report.get("diff_status"),
|
||||
"needs_prepare": report.get("needs_prepare"),
|
||||
}
|
||||
elif name == "saved_state_form_write":
|
||||
saved_state["form"] = {
|
||||
"passed": report.get("passed"),
|
||||
"status": report.get("status"),
|
||||
"base_id": report.get("base_id"),
|
||||
"table": report.get("table"),
|
||||
"routes": report.get("routes"),
|
||||
"preflight_status": report.get("preflight_status"),
|
||||
"preflight_counts": report.get("preflight_counts"),
|
||||
}
|
||||
elif name == "saved_state_module_write":
|
||||
saved_state["module"] = {
|
||||
"status": report.get("status"),
|
||||
"base_id": report.get("base_id"),
|
||||
"table": report.get("table"),
|
||||
"module_ref": report.get("module_ref"),
|
||||
"write_plan_allowed": report.get("write_plan_allowed"),
|
||||
"preflight_status": report.get("preflight_status"),
|
||||
"preflight_counts": report.get("preflight_counts"),
|
||||
}
|
||||
elif name == "saved_state_strict_readiness":
|
||||
saved_state["strict_readiness"] = {
|
||||
"status": report.get("status"),
|
||||
"ready": report.get("ready"),
|
||||
"base_id": report.get("base_id"),
|
||||
"table": report.get("table"),
|
||||
"tables": report.get("tables"),
|
||||
"saved_state_rows": report.get("saved_state_rows"),
|
||||
"forms": report.get("forms"),
|
||||
"modules": report.get("modules"),
|
||||
}
|
||||
elif name == "saved_state_copy_plan":
|
||||
source_family = report.get("source_family") if isinstance(report.get("source_family"), dict) else {}
|
||||
saved_state["copy_plan"] = {
|
||||
"status": report.get("status"),
|
||||
"ready_to_copy": report.get("ready_to_copy"),
|
||||
"base_id": report.get("base_id"),
|
||||
"target_table": report.get("target_table"),
|
||||
"source_family": {
|
||||
"expected_source_table": source_family.get("expected_source_table"),
|
||||
"source_tables": source_family.get("source_tables"),
|
||||
"valid": source_family.get("valid"),
|
||||
},
|
||||
"source_rows": report.get("source_rows"),
|
||||
"found_source_storage_rows": report.get("found_source_storage_rows"),
|
||||
"target_collision_status": report.get("target_collision_status"),
|
||||
"target_collision_rows": report.get("target_collision_rows"),
|
||||
}
|
||||
elif name in {"saved_state_prepare_sql", "saved_state_cleanup_sql"}:
|
||||
key = "prepare_sql" if name == "saved_state_prepare_sql" else "cleanup_sql"
|
||||
saved_state[key] = {
|
||||
"status": report.get("status"),
|
||||
"base_id": report.get("base_id"),
|
||||
"table": report.get("table"),
|
||||
"source_table": report.get("source_table"),
|
||||
"read_only": report.get("read_only"),
|
||||
"sql_write_performed": report.get("sql_write_performed"),
|
||||
}
|
||||
|
||||
if selector_chain:
|
||||
summary["selector_chain"] = selector_chain
|
||||
if write_plan_safety:
|
||||
summary["write_plan_safety"] = write_plan_safety
|
||||
if write_rollback_safety:
|
||||
summary["write_rollback_safety"] = write_rollback_safety
|
||||
if saved_state_diff:
|
||||
summary["saved_state_diff"] = saved_state_diff
|
||||
if saved_state:
|
||||
summary["saved_state"] = saved_state
|
||||
return summary
|
||||
|
||||
|
||||
def run(command: list[str], *, stream: bool, max_output_chars: int) -> dict[str, Any]:
|
||||
label = " ".join(command)
|
||||
if stream:
|
||||
print(f"\n== {label}", flush=True)
|
||||
result = subprocess.run(command, cwd=ROOT, text=True, check=False)
|
||||
return {"label": label, "command": command, "returncode": result.returncode}
|
||||
result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, check=False)
|
||||
parsed = parse_json_output(result.stdout)
|
||||
stdout, stdout_truncated = trim_output(result.stdout, max_output_chars)
|
||||
stderr, stderr_truncated = trim_output(result.stderr, max_output_chars)
|
||||
report = {
|
||||
"label": label,
|
||||
"command": command,
|
||||
"returncode": result.returncode,
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
"stdout_truncated": stdout_truncated,
|
||||
"stderr_truncated": stderr_truncated,
|
||||
}
|
||||
summary = parsed_summary(parsed)
|
||||
if summary is not None:
|
||||
report["parsed"] = summary
|
||||
return report
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Run offline/static checks for the 1C adapter verification stack.")
|
||||
parser.add_argument("--base-id", nargs="+", default=["upo_test"], help="Base id(s) whose persisted verify reports should be checked.")
|
||||
parser.add_argument("--skip-persisted-reports", action="store_true", help="Skip checking reports/1c-sql/<base-id> artifacts.")
|
||||
parser.add_argument("--require-saved-state-write-smoke", action="store_true", help="Require persisted saved-state smoke reports to contain real write checks.")
|
||||
parser.add_argument("--require-selector-chain-write-plan-composition", action="store_true", help="Require persisted selector-chain reports to contain composed metadata.write.plan coverage.")
|
||||
parser.add_argument("--rest-adapter-url", default=DEFAULT_REST_ADAPTER_URL, help="Expected REST adapter endpoint_url in persisted reports.")
|
||||
parser.add_argument("--mcp-url", default=DEFAULT_MCP_URL, help="Expected MCP proxy endpoint_url in persisted reports.")
|
||||
parser.add_argument("--saved-state-table", choices=SAVED_STATE_TABLES, default="ConfigSave", help="Expected saved-state target table in persisted copy-plan/form/module reports.")
|
||||
parser.add_argument("--max-report-age-seconds", type=int, help="Fail if any checked persisted report file is older than this many seconds.")
|
||||
parser.add_argument("--report", type=Path, help="Optional JSON report path.")
|
||||
parser.add_argument("--json", action="store_true", help="Print JSON summary without streaming child command output.")
|
||||
parser.add_argument("--max-output-chars", type=int, default=4000, help="Maximum stdout/stderr characters retained per child command in JSON mode.")
|
||||
args = parser.parse_args()
|
||||
|
||||
duplicate_base_ids = duplicate_values(args.base_id)
|
||||
if duplicate_base_ids:
|
||||
report = {
|
||||
"schema": "onec_adapter_verification_stack_check.v1",
|
||||
"passed": False,
|
||||
"base_id": args.base_id[0] if len(args.base_id) == 1 else None,
|
||||
"base_ids": args.base_id,
|
||||
"rest_adapter_url": args.rest_adapter_url,
|
||||
"mcp_url": args.mcp_url,
|
||||
"saved_state_table": args.saved_state_table,
|
||||
"checks": [],
|
||||
"failures": [{"code": "duplicate_base_id", "base_ids": duplicate_base_ids}],
|
||||
}
|
||||
if args.report:
|
||||
report_path = args.report if args.report.is_absolute() else ROOT / args.report
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
if args.json:
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print("Duplicate --base-id value(s): " + ", ".join(duplicate_base_ids), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
commands = [
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"py_compile",
|
||||
"scripts/check_1c_adapter_verification_stack.py",
|
||||
"scripts/check_1c_verify_reports.py",
|
||||
"scripts/check_powershell_scripts.py",
|
||||
"scripts/smoke_1c_mcp_selector_chain.py",
|
||||
"scripts/smoke_1c_write_plan_safety.py",
|
||||
"scripts/smoke_1c_write_preflight.py",
|
||||
"scripts/smoke_1c_write_rollback_safety.py",
|
||||
"scripts/smoke_1c_saved_state_diff.py",
|
||||
"scripts/smoke_1c_saved_state_changes.py",
|
||||
"scripts/smoke_1c_saved_state_write_routes.py",
|
||||
"scripts/smoke_1c_saved_state_module_write.py",
|
||||
"scripts/check_1c_saved_state_strict_readiness.py",
|
||||
"scripts/plan_1c_saved_state_copy.py",
|
||||
"scripts/prepare_1c_saved_state_copy_sql.py",
|
||||
"scripts/prepare_1c_saved_state_cleanup_sql.py",
|
||||
"scripts/verify_1c_saved_state_copy.py",
|
||||
],
|
||||
[sys.executable, "scripts/check_powershell_scripts.py"],
|
||||
[sys.executable, "scripts/check_1c_mcp_adapter_contract.py", "--json"],
|
||||
[sys.executable, "scripts/check_1c_extension_action_contract.py", "--print"],
|
||||
[sys.executable, "scripts/check_1c_write_plan_contract.py", "--print"],
|
||||
[sys.executable, "scripts/smoke_1c_mcp_selector_chain.py", "--json", "--no-report"],
|
||||
[sys.executable, "scripts/check_1c_verify_reports.py", "--self-test", "--json"],
|
||||
]
|
||||
if not args.skip_persisted_reports:
|
||||
report_command = [
|
||||
sys.executable,
|
||||
"scripts/check_1c_verify_reports.py",
|
||||
"--base-id",
|
||||
*args.base_id,
|
||||
"--rest-adapter-url",
|
||||
args.rest_adapter_url,
|
||||
"--mcp-url",
|
||||
args.mcp_url,
|
||||
"--saved-state-table",
|
||||
args.saved_state_table,
|
||||
"--json",
|
||||
]
|
||||
if args.require_saved_state_write_smoke:
|
||||
report_command.append("--require-saved-state-write-smoke")
|
||||
if args.require_selector_chain_write_plan_composition:
|
||||
report_command.append("--require-selector-chain-write-plan-composition")
|
||||
if args.max_report_age_seconds is not None:
|
||||
report_command.extend(["--max-report-age-seconds", str(args.max_report_age_seconds)])
|
||||
commands.append(report_command)
|
||||
|
||||
results: list[dict[str, Any]] = []
|
||||
failures: list[str] = []
|
||||
for command in commands:
|
||||
result = run(command, stream=not args.json, max_output_chars=args.max_output_chars)
|
||||
results.append(result)
|
||||
if result["returncode"] != 0:
|
||||
failures.append(str(result["label"]))
|
||||
|
||||
report = {
|
||||
"schema": "onec_adapter_verification_stack_check.v1",
|
||||
"passed": not failures,
|
||||
"base_id": args.base_id[0] if len(args.base_id) == 1 else None,
|
||||
"base_ids": args.base_id,
|
||||
"rest_adapter_url": args.rest_adapter_url,
|
||||
"mcp_url": args.mcp_url,
|
||||
"saved_state_table": args.saved_state_table,
|
||||
"checks": results,
|
||||
"failures": failures,
|
||||
}
|
||||
if args.report:
|
||||
report_path = args.report if args.report.is_absolute() else ROOT / args.report
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
return 0 if report["passed"] else 1
|
||||
|
||||
if failures:
|
||||
print("\n1C adapter verification stack check failed:", file=sys.stderr)
|
||||
for failure in failures:
|
||||
print(f"- {failure}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("\n1C adapter verification stack checks passed.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user