Require name-first extension preflight smoke
This commit is contained in:
@@ -776,7 +776,12 @@ Use `-SavedStateTable ConfigSave` or `-SavedStateTable ConfigCASSave` to choose
|
|||||||
which save-layer table is used by the copy plan and saved-state write smokes.
|
which save-layer table is used by the copy plan and saved-state write smokes.
|
||||||
Add `-RequireSelectorChainWritePlanComposition` when the selected base/object
|
Add `-RequireSelectorChainWritePlanComposition` when the selected base/object
|
||||||
must have a saved-state stream that lets the selector-chain smoke compose a
|
must have a saved-state stream that lets the selector-chain smoke compose a
|
||||||
concrete read-only `metadata.write.plan`.
|
concrete read-only `metadata.write.plan`. The same strict mode also requires
|
||||||
|
the write-preflight smoke to discover an extension form by public
|
||||||
|
`extension/ref/form/member` names, compose an allowed plan, and prove that the
|
||||||
|
repository and support gates use the same resolved `extension:<GUID>` layer.
|
||||||
|
Run that check directly with
|
||||||
|
`scripts/smoke_1c_write_preflight.py --require-name-first-extension-form`.
|
||||||
|
|
||||||
To exercise the MCP proxy itself, switch transport and URL:
|
To exercise the MCP proxy itself, switch transport and URL:
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
@@ -15,6 +16,10 @@ ROOT = Path(__file__).resolve().parents[1]
|
|||||||
DEFAULT_REPORTS_ROOT = ROOT / "reports" / "1c-sql"
|
DEFAULT_REPORTS_ROOT = ROOT / "reports" / "1c-sql"
|
||||||
SAVED_STATE_TABLES = {"ConfigSave", "ConfigCASSave"}
|
SAVED_STATE_TABLES = {"ConfigSave", "ConfigCASSave"}
|
||||||
SAVED_STATE_SOURCE_BY_TARGET = {"ConfigSave": "Config", "ConfigCASSave": "ConfigCAS"}
|
SAVED_STATE_SOURCE_BY_TARGET = {"ConfigSave": "Config", "ConfigCASSave": "ConfigCAS"}
|
||||||
|
EXTENSION_GUID_LAYER_RE = re.compile(
|
||||||
|
r"^extension:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def safe_path_segment(value: str) -> str:
|
def safe_path_segment(value: str) -> str:
|
||||||
@@ -396,7 +401,7 @@ def validate_write_preflight(
|
|||||||
failures.append({"code": "write_preflight_not_ok", "label": label, "path": str(path), "status": report.get("status")})
|
failures.append({"code": "write_preflight_not_ok", "label": label, "path": str(path), "status": report.get("status")})
|
||||||
if report.get("failures"):
|
if report.get("failures"):
|
||||||
failures.append({"code": "write_preflight_failures_present", "label": label, "path": str(path), "failures": report.get("failures")})
|
failures.append({"code": "write_preflight_failures_present", "label": label, "path": str(path), "failures": report.get("failures")})
|
||||||
for check in ("method_exposed", "effective_path_preflight", "concrete_saved_state_preflight"):
|
for check in ("method_exposed", "effective_path_preflight", "concrete_saved_state_preflight", "name_first_extension_form_preflight"):
|
||||||
if check not in checks:
|
if check not in checks:
|
||||||
failures.append({"code": "write_preflight_check_missing", "label": label, "check": check, "path": str(path)})
|
failures.append({"code": "write_preflight_check_missing", "label": label, "check": check, "path": str(path)})
|
||||||
expect_check(checks, failures, label, path, "method_exposed", {"status": "ok"}, failure_code="write_preflight_check_field_unexpected")
|
expect_check(checks, failures, label, path, "method_exposed", {"status": "ok"}, failure_code="write_preflight_check_field_unexpected")
|
||||||
@@ -420,6 +425,51 @@ def validate_write_preflight(
|
|||||||
{"schema": "onec_metadata_write_preflight.v1", "writer": "metadata.module.write_apply"},
|
{"schema": "onec_metadata_write_preflight.v1", "writer": "metadata.module.write_apply"},
|
||||||
failure_code="write_preflight_check_field_unexpected",
|
failure_code="write_preflight_check_field_unexpected",
|
||||||
)
|
)
|
||||||
|
name_first_form = checks.get("name_first_extension_form_preflight") if isinstance(checks.get("name_first_extension_form_preflight"), dict) else {}
|
||||||
|
require_name_first_form = bool(
|
||||||
|
(report.get("requirements") or {}).get("name_first_extension_form")
|
||||||
|
if isinstance(report.get("requirements"), dict)
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
if name_first_form.get("status") == "skipped_no_public_extension_form_target":
|
||||||
|
if require_name_first_form:
|
||||||
|
failures.append({
|
||||||
|
"code": "write_preflight_name_first_extension_form_required",
|
||||||
|
"label": label,
|
||||||
|
"path": str(path),
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
expect_check(
|
||||||
|
checks,
|
||||||
|
failures,
|
||||||
|
label,
|
||||||
|
path,
|
||||||
|
"name_first_extension_form_preflight",
|
||||||
|
{
|
||||||
|
"schema": "onec_metadata_write_preflight.v1",
|
||||||
|
"plan_status": "planned",
|
||||||
|
"plan_allowed": True,
|
||||||
|
"name_first": True,
|
||||||
|
},
|
||||||
|
failure_code="write_preflight_check_field_unexpected",
|
||||||
|
)
|
||||||
|
repository_layer = str(name_first_form.get("repository_layer_id") or "")
|
||||||
|
support_layer = str(name_first_form.get("support_layer_id") or "")
|
||||||
|
if not EXTENSION_GUID_LAYER_RE.fullmatch(repository_layer):
|
||||||
|
failures.append({
|
||||||
|
"code": "write_preflight_extension_layer_unresolved",
|
||||||
|
"label": label,
|
||||||
|
"path": str(path),
|
||||||
|
"repository_layer_id": repository_layer or None,
|
||||||
|
})
|
||||||
|
if repository_layer != support_layer:
|
||||||
|
failures.append({
|
||||||
|
"code": "write_preflight_extension_layer_mismatch",
|
||||||
|
"label": label,
|
||||||
|
"path": str(path),
|
||||||
|
"repository_layer_id": repository_layer or None,
|
||||||
|
"support_layer_id": support_layer or None,
|
||||||
|
})
|
||||||
if require_mcp_initialize:
|
if require_mcp_initialize:
|
||||||
if "mcp.initialize" not in checks:
|
if "mcp.initialize" not in checks:
|
||||||
failures.append({"code": "write_preflight_check_missing", "label": label, "check": "mcp.initialize", "path": str(path)})
|
failures.append({"code": "write_preflight_check_missing", "label": label, "check": "mcp.initialize", "path": str(path)})
|
||||||
@@ -1572,6 +1622,17 @@ def write_self_test_reports(report_dir: Path, *, base_id: str, composed: bool, s
|
|||||||
"freshness": "live_sql_verified",
|
"freshness": "live_sql_verified",
|
||||||
"writer": "metadata.module.write_apply",
|
"writer": "metadata.module.write_apply",
|
||||||
},
|
},
|
||||||
|
"name_first_extension_form_preflight": {
|
||||||
|
"schema": "onec_metadata_write_preflight.v1",
|
||||||
|
"status": "ready",
|
||||||
|
"allowed": True,
|
||||||
|
"plan_status": "planned",
|
||||||
|
"plan_allowed": True,
|
||||||
|
"repository_layer_id": "extension:11111111-1111-1111-1111-111111111111",
|
||||||
|
"support_layer_id": "extension:11111111-1111-1111-1111-111111111111",
|
||||||
|
"name_first": True,
|
||||||
|
"extension": "test2",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"failures": [],
|
"failures": [],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -23,6 +24,10 @@ PREFLIGHT_CLASSIFICATION_STATUSES = {
|
|||||||
"blocked_by_support_rule",
|
"blocked_by_support_rule",
|
||||||
"blocked_support_unknown",
|
"blocked_support_unknown",
|
||||||
}
|
}
|
||||||
|
EXTENSION_GUID_LAYER_RE = re.compile(
|
||||||
|
r"^extension:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_json(url: str, payload: dict[str, Any], *, timeout: float, headers: dict[str, str] | None = None) -> tuple[dict[str, str], dict[str, Any]]:
|
def post_json(url: str, payload: dict[str, Any], *, timeout: float, headers: dict[str, str] | None = None) -> tuple[dict[str, str], dict[str, Any]]:
|
||||||
@@ -155,7 +160,61 @@ def first_saved_module_target(endpoint_url: str, base_id: str, timeout: float, *
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_smoke(endpoint_url: str, base_id: str, timeout: float, *, transport: str) -> dict[str, Any]:
|
def first_public_extension_form_candidate(result: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
|
candidates: list[dict[str, Any]] = []
|
||||||
|
for form in result.get("forms") or []:
|
||||||
|
if not isinstance(form, dict):
|
||||||
|
continue
|
||||||
|
for match in form.get("matches") or []:
|
||||||
|
if not isinstance(match, dict):
|
||||||
|
continue
|
||||||
|
selector = match.get("selector") if isinstance(match.get("selector"), dict) else {}
|
||||||
|
if not selector.get("extension") or not selector.get("ref") or not selector.get("form"):
|
||||||
|
continue
|
||||||
|
if not any(selector.get(key) for key in ("command", "element", "attribute")):
|
||||||
|
continue
|
||||||
|
if any(key in selector for key in ("table", "file_name", "guid", "form_guid", "module_ref")):
|
||||||
|
continue
|
||||||
|
for prop in match.get("writable_properties") or []:
|
||||||
|
if not isinstance(prop, dict) or prop.get("value_type") != "string":
|
||||||
|
continue
|
||||||
|
property_name = str(prop.get("presentation") or prop.get("property") or "").strip()
|
||||||
|
if not property_name:
|
||||||
|
continue
|
||||||
|
normalized_property = str(prop.get("property") or "").strip().casefold()
|
||||||
|
candidates.append(
|
||||||
|
{
|
||||||
|
"priority": 0 if normalized_property == "title" else (2 if normalized_property in {"id", "name"} else 1),
|
||||||
|
"target": {
|
||||||
|
"kind": "form",
|
||||||
|
**{
|
||||||
|
key: value
|
||||||
|
for key, value in selector.items()
|
||||||
|
if key in {"extension", "ref", "form", "command", "element", "attribute"}
|
||||||
|
and value not in (None, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"property": property_name,
|
||||||
|
"value": f"{str(prop.get('value') or '')} [NAME-FIRST PREFLIGHT]",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
candidate = sorted(candidates, key=lambda item: int(item.get("priority") or 0))[0]
|
||||||
|
candidate.pop("priority", None)
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
|
def run_smoke(
|
||||||
|
endpoint_url: str,
|
||||||
|
base_id: str,
|
||||||
|
timeout: float,
|
||||||
|
*,
|
||||||
|
transport: str,
|
||||||
|
require_name_first_extension_form: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
failures: list[str] = []
|
failures: list[str] = []
|
||||||
checks: dict[str, Any] = {}
|
checks: dict[str, Any] = {}
|
||||||
session_id: str | None = None
|
session_id: str | None = None
|
||||||
@@ -233,12 +292,79 @@ def run_smoke(endpoint_url: str, base_id: str, timeout: float, *, transport: str
|
|||||||
else:
|
else:
|
||||||
checks["concrete_saved_state_preflight"] = {"status": "skipped_no_saved_module_target"}
|
checks["concrete_saved_state_preflight"] = {"status": "skipped_no_saved_module_target"}
|
||||||
|
|
||||||
|
forms = rpc_call(
|
||||||
|
endpoint_url,
|
||||||
|
"metadata.saved_state.forms.search",
|
||||||
|
{
|
||||||
|
"base_id": base_id,
|
||||||
|
"limit": 20,
|
||||||
|
"scan_limit": 1000,
|
||||||
|
"timeout_seconds": int(timeout),
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
transport=transport,
|
||||||
|
session_id=session_id,
|
||||||
|
)
|
||||||
|
form_candidate = first_public_extension_form_candidate(forms)
|
||||||
|
if not form_candidate:
|
||||||
|
checks["name_first_extension_form_preflight"] = {
|
||||||
|
"status": "skipped_no_public_extension_form_target",
|
||||||
|
"search_status": forms.get("status"),
|
||||||
|
"forms": int((forms.get("counts") or {}).get("forms") or 0),
|
||||||
|
}
|
||||||
|
if require_name_first_extension_form:
|
||||||
|
failures.append("name-first extension form preflight target is required but was not discovered")
|
||||||
|
else:
|
||||||
|
form_preflight = rpc_call(
|
||||||
|
endpoint_url,
|
||||||
|
"metadata.write.preflight",
|
||||||
|
{
|
||||||
|
"base_id": base_id,
|
||||||
|
"target": form_candidate["target"],
|
||||||
|
"edits": [form_candidate["edit"]],
|
||||||
|
"resolve_origin": False,
|
||||||
|
"timeout_seconds": int(timeout),
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
transport=transport,
|
||||||
|
session_id=session_id,
|
||||||
|
)
|
||||||
|
plan = form_preflight.get("plan") if isinstance(form_preflight.get("plan"), dict) else {}
|
||||||
|
repository = form_preflight.get("repository") if isinstance(form_preflight.get("repository"), dict) else {}
|
||||||
|
support = form_preflight.get("support") if isinstance(form_preflight.get("support"), dict) else {}
|
||||||
|
repository_layer = str(repository.get("layer_id") or "")
|
||||||
|
support_layer = str(support.get("layer_id") or "")
|
||||||
|
name_first = not any(
|
||||||
|
key in form_candidate["target"]
|
||||||
|
for key in ("table", "file_name", "guid", "form_guid", "module_ref", "extension_guid")
|
||||||
|
)
|
||||||
|
checks["name_first_extension_form_preflight"] = {
|
||||||
|
"schema": form_preflight.get("schema"),
|
||||||
|
"status": form_preflight.get("status"),
|
||||||
|
"allowed": form_preflight.get("allowed"),
|
||||||
|
"plan_status": plan.get("status"),
|
||||||
|
"plan_allowed": plan.get("allowed"),
|
||||||
|
"repository_layer_id": repository_layer or None,
|
||||||
|
"support_layer_id": support_layer or None,
|
||||||
|
"name_first": name_first,
|
||||||
|
"extension": form_candidate["target"].get("extension"),
|
||||||
|
}
|
||||||
|
require(form_preflight.get("schema") == "onec_metadata_write_preflight.v1", "name-first extension form preflight must return expected schema", failures)
|
||||||
|
require(classified_preflight_status(form_preflight.get("status")), "name-first extension form preflight must classify readiness or a safety gate", failures)
|
||||||
|
require(plan.get("status") == "planned" and plan.get("allowed") is True, "name-first extension form preflight plan must be allowed and planned", failures)
|
||||||
|
require(name_first, "extension form preflight input must remain name-first", failures)
|
||||||
|
require(bool(EXTENSION_GUID_LAYER_RE.fullmatch(repository_layer)), "repository gate must use a resolved extension GUID layer", failures)
|
||||||
|
require(repository_layer == support_layer, "repository and support gates must use the same extension layer", failures)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"schema": "onec_write_preflight_smoke.v1",
|
"schema": "onec_write_preflight_smoke.v1",
|
||||||
"status": "ok" if not failures else "failed",
|
"status": "ok" if not failures else "failed",
|
||||||
"endpoint_url": endpoint_url,
|
"endpoint_url": endpoint_url,
|
||||||
"transport": transport,
|
"transport": transport,
|
||||||
"base_id": base_id,
|
"base_id": base_id,
|
||||||
|
"requirements": {
|
||||||
|
"name_first_extension_form": require_name_first_extension_form,
|
||||||
|
},
|
||||||
"checks": checks,
|
"checks": checks,
|
||||||
"failures": failures,
|
"failures": failures,
|
||||||
**({"mcp_initialize": initialize_result} if initialize_result and failures else {}),
|
**({"mcp_initialize": initialize_result} if initialize_result and failures else {}),
|
||||||
@@ -253,11 +379,18 @@ def main() -> int:
|
|||||||
parser.add_argument("--base-id", default="upo_test")
|
parser.add_argument("--base-id", default="upo_test")
|
||||||
parser.add_argument("--timeout", type=float, default=30.0)
|
parser.add_argument("--timeout", type=float, default=30.0)
|
||||||
parser.add_argument("--report", type=Path)
|
parser.add_argument("--report", type=Path)
|
||||||
|
parser.add_argument("--require-name-first-extension-form", action="store_true")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
endpoint_url = args.mcp_url if args.transport == "mcp" else args.base_url
|
endpoint_url = args.mcp_url if args.transport == "mcp" else args.base_url
|
||||||
try:
|
try:
|
||||||
report = run_smoke(endpoint_url, args.base_id, args.timeout, transport=args.transport)
|
report = run_smoke(
|
||||||
|
endpoint_url,
|
||||||
|
args.base_id,
|
||||||
|
args.timeout,
|
||||||
|
transport=args.transport,
|
||||||
|
require_name_first_extension_form=args.require_name_first_extension_form,
|
||||||
|
)
|
||||||
except (TimeoutError, URLError, OSError) as exc:
|
except (TimeoutError, URLError, OSError) as exc:
|
||||||
report = {
|
report = {
|
||||||
"schema": "onec_write_preflight_smoke.v1",
|
"schema": "onec_write_preflight_smoke.v1",
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ function Assert-WritePreflightReport {
|
|||||||
if ($report.failures -and $report.failures.Count -gt 0) {
|
if ($report.failures -and $report.failures.Count -gt 0) {
|
||||||
throw "$Label report contains failures: $Path"
|
throw "$Label report contains failures: $Path"
|
||||||
}
|
}
|
||||||
foreach ($check in @("method_exposed", "effective_path_preflight", "concrete_saved_state_preflight")) {
|
foreach ($check in @("method_exposed", "effective_path_preflight", "concrete_saved_state_preflight", "name_first_extension_form_preflight")) {
|
||||||
if ($report.checks.PSObject.Properties.Name -notcontains $check) {
|
if ($report.checks.PSObject.Properties.Name -notcontains $check) {
|
||||||
throw "$Label report is missing check '$check': $Path"
|
throw "$Label report is missing check '$check': $Path"
|
||||||
}
|
}
|
||||||
@@ -486,6 +486,9 @@ try {
|
|||||||
"--report",
|
"--report",
|
||||||
$writePreflightReport
|
$writePreflightReport
|
||||||
)
|
)
|
||||||
|
if ($RequireSelectorChainWritePlanComposition) {
|
||||||
|
$writePreflightCommand += "--require-name-first-extension-form"
|
||||||
|
}
|
||||||
Invoke-CheckedCommand -Label "REST adapter write-preflight smoke ($currentBaseId)" -Command $writePreflightCommand
|
Invoke-CheckedCommand -Label "REST adapter write-preflight smoke ($currentBaseId)" -Command $writePreflightCommand
|
||||||
Assert-WritePreflightReport -Label "REST adapter write-preflight smoke ($currentBaseId)" -Path $writePreflightReport
|
Assert-WritePreflightReport -Label "REST adapter write-preflight smoke ($currentBaseId)" -Path $writePreflightReport
|
||||||
}
|
}
|
||||||
@@ -782,6 +785,9 @@ try {
|
|||||||
"--report",
|
"--report",
|
||||||
$mcpWritePreflightReport
|
$mcpWritePreflightReport
|
||||||
)
|
)
|
||||||
|
if ($RequireSelectorChainWritePlanComposition) {
|
||||||
|
$mcpWritePreflightCommand += "--require-name-first-extension-form"
|
||||||
|
}
|
||||||
Invoke-CheckedCommand -Label "MCP proxy write-preflight smoke ($currentBaseId)" -Command $mcpWritePreflightCommand
|
Invoke-CheckedCommand -Label "MCP proxy write-preflight smoke ($currentBaseId)" -Command $mcpWritePreflightCommand
|
||||||
Assert-WritePreflightReport -Label "MCP proxy write-preflight smoke ($currentBaseId)" -Path $mcpWritePreflightReport
|
Assert-WritePreflightReport -Label "MCP proxy write-preflight smoke ($currentBaseId)" -Path $mcpWritePreflightReport
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,3 +61,126 @@ def test_saved_module_target_prefers_bounded_stream(monkeypatch) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result == bounded_stream
|
assert result == bounded_stream
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_extension_form_candidate_is_name_first_and_prefers_title() -> None:
|
||||||
|
result = smoke.first_public_extension_form_candidate(
|
||||||
|
{
|
||||||
|
"forms": [
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
{
|
||||||
|
"selector": {
|
||||||
|
"extension": "test2",
|
||||||
|
"ref": "Catalog.test2",
|
||||||
|
"form": "t_Форма",
|
||||||
|
"command": "ЗаменаДомена",
|
||||||
|
},
|
||||||
|
"writable_properties": [
|
||||||
|
{
|
||||||
|
"property": "name",
|
||||||
|
"presentation": "Имя",
|
||||||
|
"value": "ЗаменаДомена",
|
||||||
|
"value_type": "string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"property": "title",
|
||||||
|
"presentation": "Заголовок",
|
||||||
|
"value": "Замена домена",
|
||||||
|
"value_type": "string",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"target": {
|
||||||
|
"kind": "form",
|
||||||
|
"extension": "test2",
|
||||||
|
"ref": "Catalog.test2",
|
||||||
|
"form": "t_Форма",
|
||||||
|
"command": "ЗаменаДомена",
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"property": "Заголовок",
|
||||||
|
"value": "Замена домена [NAME-FIRST PREFLIGHT]",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_name_first_extension_form_preflight_rejects_base_gate_layer(monkeypatch) -> None:
|
||||||
|
def fake_rpc_call(
|
||||||
|
_endpoint_url: str,
|
||||||
|
method: str,
|
||||||
|
payload: dict[str, Any],
|
||||||
|
_timeout: float,
|
||||||
|
*,
|
||||||
|
transport: str,
|
||||||
|
session_id: str | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
assert transport == "rest"
|
||||||
|
assert session_id is None
|
||||||
|
if method == "help.methods":
|
||||||
|
return {"methods": [{"name": "metadata.write.preflight"}]}
|
||||||
|
if method == "metadata.saved_state.modules.search":
|
||||||
|
return {"status": "ok", "modules": []}
|
||||||
|
if method == "metadata.saved_state.forms.search":
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"counts": {"forms": 1},
|
||||||
|
"forms": [
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
{
|
||||||
|
"selector": {
|
||||||
|
"extension": "test2",
|
||||||
|
"ref": "Catalog.test2",
|
||||||
|
"form": "t_Форма",
|
||||||
|
"command": "ЗаменаДомена",
|
||||||
|
},
|
||||||
|
"writable_properties": [
|
||||||
|
{
|
||||||
|
"property": "title",
|
||||||
|
"presentation": "Заголовок",
|
||||||
|
"value": "Замена домена",
|
||||||
|
"value_type": "string",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
if method == "metadata.write.preflight" and (payload.get("target") or {}).get("extension"):
|
||||||
|
return {
|
||||||
|
"schema": "onec_metadata_write_preflight.v1",
|
||||||
|
"status": "ready",
|
||||||
|
"allowed": True,
|
||||||
|
"plan": {"status": "planned", "allowed": True},
|
||||||
|
"repository": {"layer_id": "base"},
|
||||||
|
"support": {"layer_id": "base"},
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"schema": "onec_metadata_write_preflight.v1",
|
||||||
|
"status": "blocked",
|
||||||
|
"allowed": False,
|
||||||
|
"plan": {"allowed": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(smoke, "rpc_call", fake_rpc_call)
|
||||||
|
|
||||||
|
report = smoke.run_smoke(
|
||||||
|
"http://adapter",
|
||||||
|
"upo_test",
|
||||||
|
30,
|
||||||
|
transport="rest",
|
||||||
|
require_name_first_extension_form=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert report["status"] == "failed"
|
||||||
|
assert report["checks"]["name_first_extension_form_preflight"]["name_first"] is True
|
||||||
|
assert any("resolved extension GUID layer" in failure for failure in report["failures"])
|
||||||
|
|||||||
Reference in New Issue
Block a user