Preserve extension names in module preflight
This commit is contained in:
@@ -777,14 +777,18 @@ 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`. The same strict mode also requires
|
concrete read-only `metadata.write.plan`. The same strict mode also requires
|
||||||
the write-preflight smoke to discover an extension form by public
|
the write-preflight smoke to discover both an extension form and an extension
|
||||||
`extension/ref/form/member` names, compose an allowed plan, and prove that the
|
module by public names (`extension/ref/form/member` and
|
||||||
repository and support gates use the same resolved `extension:<GUID>` layer.
|
`extension/ref/form/module/stream_ordinal`), compose allowed plans, and prove
|
||||||
It then submits the same public selector with a deliberately different GUID
|
that the repository and support gates use the same resolved
|
||||||
and requires a read-only `blocked / extension_selector_conflict` result, so a
|
`extension:<GUID>` layer. Module search restores the public extension name from
|
||||||
legacy permissive repository profile cannot authorize a mismatched layer.
|
`ConfigCASSave`; it never returns the storage GUID as the caller-facing
|
||||||
Run that check directly with
|
selector. The smoke then submits each public selector with a deliberately
|
||||||
`scripts/smoke_1c_write_preflight.py --require-name-first-extension-form`.
|
different GUID and requires a read-only
|
||||||
|
`blocked / extension_selector_conflict` result, so a legacy permissive
|
||||||
|
repository profile cannot authorize a mismatched layer. Run both checks
|
||||||
|
directly with
|
||||||
|
`scripts/smoke_1c_write_preflight.py --require-name-first-extension-form --require-name-first-extension-module`.
|
||||||
|
|
||||||
To exercise the MCP proxy itself, switch transport and URL:
|
To exercise the MCP proxy itself, switch transport and URL:
|
||||||
|
|
||||||
|
|||||||
@@ -41912,7 +41912,11 @@ def metadata_saved_state_modules_owner_guid_from_selector(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def public_saved_state_modules_search_row(row: dict[str, Any]) -> dict[str, Any] | None:
|
def public_saved_state_modules_search_row(
|
||||||
|
row: dict[str, Any],
|
||||||
|
*,
|
||||||
|
requested_extension: str | None = None,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
owner = row.get("owner") if isinstance(row.get("owner"), dict) else {}
|
owner = row.get("owner") if isinstance(row.get("owner"), dict) else {}
|
||||||
form = row.get("form") if isinstance(row.get("form"), dict) else {}
|
form = row.get("form") if isinstance(row.get("form"), dict) else {}
|
||||||
module = row.get("module") if isinstance(row.get("module"), dict) else {}
|
module = row.get("module") if isinstance(row.get("module"), dict) else {}
|
||||||
@@ -41936,6 +41940,7 @@ def public_saved_state_modules_search_row(row: dict[str, Any]) -> dict[str, Any]
|
|||||||
}
|
}
|
||||||
qualified_name = str(row.get("qualified_name") or row.get("display_name") or "").strip()
|
qualified_name = str(row.get("qualified_name") or row.get("display_name") or "").strip()
|
||||||
selector = {
|
selector = {
|
||||||
|
**({"extension": requested_extension} if requested_extension else {}),
|
||||||
**({"ref": owner_ref} if owner_ref else {}),
|
**({"ref": owner_ref} if owner_ref else {}),
|
||||||
**({"form": form_public.get("name")} if form_public.get("name") else {}),
|
**({"form": form_public.get("name")} if form_public.get("name") else {}),
|
||||||
**({"module": module_public.get("name")} if module_public.get("name") else {}),
|
**({"module": module_public.get("name")} if module_public.get("name") else {}),
|
||||||
@@ -41960,6 +41965,7 @@ def public_saved_state_modules_search_row(row: dict[str, Any]) -> dict[str, Any]
|
|||||||
stream_qualified_name = str(stream.get("qualified_name") or stream.get("display_name") or qualified_name).strip()
|
stream_qualified_name = str(stream.get("qualified_name") or stream.get("display_name") or qualified_name).strip()
|
||||||
write_plan_target = {
|
write_plan_target = {
|
||||||
"kind": "module",
|
"kind": "module",
|
||||||
|
**({"extension": requested_extension} if requested_extension else {}),
|
||||||
**({"ref": stream_owner_ref} if stream_owner_ref else {}),
|
**({"ref": stream_owner_ref} if stream_owner_ref else {}),
|
||||||
**({"form": stream_form.get("name")} if stream_form.get("name") else {}),
|
**({"form": stream_form.get("name")} if stream_form.get("name") else {}),
|
||||||
**({"module": stream_module.get("name")} if stream_module.get("name") else {}),
|
**({"module": stream_module.get("name")} if stream_module.get("name") else {}),
|
||||||
@@ -42173,11 +42179,28 @@ def metadata_saved_state_modules_search(payload: dict[str, Any]) -> dict[str, An
|
|||||||
break
|
break
|
||||||
if len(modules) >= int(limit or 50):
|
if len(modules) >= int(limit or 50):
|
||||||
break
|
break
|
||||||
public_modules = modules if include_storage else [
|
public_modules = modules if include_storage else []
|
||||||
public_row
|
if not include_storage:
|
||||||
for row in modules
|
extension_names_by_guid: dict[str, str] = {}
|
||||||
if (public_row := public_saved_state_modules_search_row(row)) is not None
|
if not extension_filter and any(row.get("table") == "ConfigCASSave" for row in modules):
|
||||||
]
|
extension_names_by_guid = {
|
||||||
|
str(guid).strip().lower(): str(item.get("name") or "").strip()
|
||||||
|
for guid, item in extension_map_by_guid(base_id).items()
|
||||||
|
if str(item.get("name") or "").strip()
|
||||||
|
}
|
||||||
|
for row in modules:
|
||||||
|
row_extension = extension_filter or None
|
||||||
|
if not row_extension and row.get("table") == "ConfigCASSave":
|
||||||
|
row_file_name = str(row.get("file_name") or "")
|
||||||
|
extension_guid_from_file = row_file_name.split("__", 1)[0].strip().lower() if "__" in row_file_name else ""
|
||||||
|
if extension_guid_from_file:
|
||||||
|
row_extension = extension_names_by_guid.get(extension_guid_from_file)
|
||||||
|
public_row = public_saved_state_modules_search_row(
|
||||||
|
row,
|
||||||
|
requested_extension=row_extension,
|
||||||
|
)
|
||||||
|
if public_row is not None:
|
||||||
|
public_modules.append(public_row)
|
||||||
result = {
|
result = {
|
||||||
"schema": "onec_saved_state_module_search.v1",
|
"schema": "onec_saved_state_module_search.v1",
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
|
|||||||
@@ -401,7 +401,15 @@ 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", "name_first_extension_form_preflight", "conflicting_extension_selector_preflight"):
|
for check in (
|
||||||
|
"method_exposed",
|
||||||
|
"effective_path_preflight",
|
||||||
|
"concrete_saved_state_preflight",
|
||||||
|
"name_first_extension_form_preflight",
|
||||||
|
"conflicting_extension_selector_preflight",
|
||||||
|
"name_first_extension_module_preflight",
|
||||||
|
"conflicting_extension_module_selector_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")
|
||||||
@@ -495,6 +503,80 @@ def validate_write_preflight(
|
|||||||
},
|
},
|
||||||
failure_code="write_preflight_check_field_unexpected",
|
failure_code="write_preflight_check_field_unexpected",
|
||||||
)
|
)
|
||||||
|
name_first_module = checks.get("name_first_extension_module_preflight") if isinstance(checks.get("name_first_extension_module_preflight"), dict) else {}
|
||||||
|
require_name_first_module = bool(
|
||||||
|
(report.get("requirements") or {}).get("name_first_extension_module")
|
||||||
|
if isinstance(report.get("requirements"), dict)
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
if name_first_module.get("status") == "skipped_no_public_extension_module_target":
|
||||||
|
if require_name_first_module:
|
||||||
|
failures.append({
|
||||||
|
"code": "write_preflight_name_first_extension_module_required",
|
||||||
|
"label": label,
|
||||||
|
"path": str(path),
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
expect_check(
|
||||||
|
checks,
|
||||||
|
failures,
|
||||||
|
label,
|
||||||
|
path,
|
||||||
|
"name_first_extension_module_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_module.get("repository_layer_id") or "")
|
||||||
|
support_layer = str(name_first_module.get("support_layer_id") or "")
|
||||||
|
if not EXTENSION_GUID_LAYER_RE.fullmatch(repository_layer):
|
||||||
|
failures.append({
|
||||||
|
"code": "write_preflight_extension_module_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_module_layer_mismatch",
|
||||||
|
"label": label,
|
||||||
|
"path": str(path),
|
||||||
|
"repository_layer_id": repository_layer or None,
|
||||||
|
"support_layer_id": support_layer or None,
|
||||||
|
})
|
||||||
|
conflicting_extension_module = (
|
||||||
|
checks.get("conflicting_extension_module_selector_preflight")
|
||||||
|
if isinstance(checks.get("conflicting_extension_module_selector_preflight"), dict)
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
if conflicting_extension_module.get("status") == "skipped_no_public_extension_module_target":
|
||||||
|
if require_name_first_module:
|
||||||
|
failures.append({
|
||||||
|
"code": "write_preflight_extension_module_conflict_check_required",
|
||||||
|
"label": label,
|
||||||
|
"path": str(path),
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
expect_check(
|
||||||
|
checks,
|
||||||
|
failures,
|
||||||
|
label,
|
||||||
|
path,
|
||||||
|
"conflicting_extension_module_selector_preflight",
|
||||||
|
{
|
||||||
|
"schema": "onec_metadata_write_preflight.v1",
|
||||||
|
"status": "blocked",
|
||||||
|
"allowed": False,
|
||||||
|
"resolution_status": "conflict",
|
||||||
|
"error": "extension_selector_conflict",
|
||||||
|
"layer_id": "extension:unresolved",
|
||||||
|
},
|
||||||
|
failure_code="write_preflight_check_field_unexpected",
|
||||||
|
)
|
||||||
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)})
|
||||||
@@ -1666,6 +1748,25 @@ def write_self_test_reports(report_dir: Path, *, base_id: str, composed: bool, s
|
|||||||
"error": "extension_selector_conflict",
|
"error": "extension_selector_conflict",
|
||||||
"layer_id": "extension:unresolved",
|
"layer_id": "extension:unresolved",
|
||||||
},
|
},
|
||||||
|
"name_first_extension_module_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",
|
||||||
|
},
|
||||||
|
"conflicting_extension_module_selector_preflight": {
|
||||||
|
"schema": "onec_metadata_write_preflight.v1",
|
||||||
|
"status": "blocked",
|
||||||
|
"allowed": False,
|
||||||
|
"resolution_status": "conflict",
|
||||||
|
"error": "extension_selector_conflict",
|
||||||
|
"layer_id": "extension:unresolved",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"failures": [],
|
"failures": [],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,6 +207,57 @@ def first_public_extension_form_candidate(result: dict[str, Any]) -> dict[str, A
|
|||||||
return candidate
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
|
def first_public_extension_module_candidate(result: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
|
public_keys = {
|
||||||
|
"extension",
|
||||||
|
"ref",
|
||||||
|
"form",
|
||||||
|
"module",
|
||||||
|
"qualified_name",
|
||||||
|
"stream_ordinal",
|
||||||
|
}
|
||||||
|
opaque_keys = {
|
||||||
|
"table",
|
||||||
|
"file_name",
|
||||||
|
"guid",
|
||||||
|
"form_guid",
|
||||||
|
"module_guid",
|
||||||
|
"object_guid",
|
||||||
|
"module_ref",
|
||||||
|
"extension_guid",
|
||||||
|
}
|
||||||
|
for module in result.get("modules") or []:
|
||||||
|
if not isinstance(module, dict):
|
||||||
|
continue
|
||||||
|
for stream in module.get("streams") or []:
|
||||||
|
if not isinstance(stream, dict):
|
||||||
|
continue
|
||||||
|
target = stream.get("write_plan_target") if isinstance(stream.get("write_plan_target"), dict) else {}
|
||||||
|
preview = str(stream.get("preview") or "").strip()
|
||||||
|
control_fragment = next((line.strip() for line in preview.splitlines() if line.strip()), "")
|
||||||
|
if not target.get("extension") or not target.get("ref") or not target.get("module"):
|
||||||
|
continue
|
||||||
|
if not preview or not control_fragment or any(key in target for key in opaque_keys):
|
||||||
|
continue
|
||||||
|
public_target = {
|
||||||
|
"kind": "module",
|
||||||
|
**{
|
||||||
|
key: value
|
||||||
|
for key, value in target.items()
|
||||||
|
if key in public_keys and value not in (None, "")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"target": public_target,
|
||||||
|
"intent": {
|
||||||
|
"operation": "replace_with_control",
|
||||||
|
"control_fragment": control_fragment,
|
||||||
|
"new": preview,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def different_extension_guid(layer_id: str) -> str | None:
|
def different_extension_guid(layer_id: str) -> str | None:
|
||||||
if not EXTENSION_GUID_LAYER_RE.fullmatch(str(layer_id or "")):
|
if not EXTENSION_GUID_LAYER_RE.fullmatch(str(layer_id or "")):
|
||||||
return None
|
return None
|
||||||
@@ -222,6 +273,7 @@ def run_smoke(
|
|||||||
*,
|
*,
|
||||||
transport: str,
|
transport: str,
|
||||||
require_name_first_extension_form: bool = False,
|
require_name_first_extension_form: bool = False,
|
||||||
|
require_name_first_extension_module: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
failures: list[str] = []
|
failures: list[str] = []
|
||||||
checks: dict[str, Any] = {}
|
checks: dict[str, Any] = {}
|
||||||
@@ -410,6 +462,117 @@ def run_smoke(
|
|||||||
require(layer_resolution.get("status") == "conflict", "conflicting extension selector must report conflict resolution status", failures)
|
require(layer_resolution.get("status") == "conflict", "conflicting extension selector must report conflict resolution status", failures)
|
||||||
require(layer_diagnostics.get("error") == "extension_selector_conflict", "conflicting extension selector must report extension_selector_conflict", failures)
|
require(layer_diagnostics.get("error") == "extension_selector_conflict", "conflicting extension selector must report extension_selector_conflict", failures)
|
||||||
|
|
||||||
|
modules = rpc_call(
|
||||||
|
endpoint_url,
|
||||||
|
"metadata.saved_state.modules.search",
|
||||||
|
{
|
||||||
|
"base_id": base_id,
|
||||||
|
"tables": ["ConfigCASSave"],
|
||||||
|
"limit": 20,
|
||||||
|
"scan_limit": 1000,
|
||||||
|
"timeout_seconds": int(timeout),
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
transport=transport,
|
||||||
|
session_id=session_id,
|
||||||
|
)
|
||||||
|
module_candidate = first_public_extension_module_candidate(modules)
|
||||||
|
if not module_candidate:
|
||||||
|
checks["name_first_extension_module_preflight"] = {
|
||||||
|
"status": "skipped_no_public_extension_module_target",
|
||||||
|
"search_status": modules.get("status"),
|
||||||
|
"modules": int((modules.get("counts") or {}).get("modules") or 0),
|
||||||
|
}
|
||||||
|
checks["conflicting_extension_module_selector_preflight"] = {
|
||||||
|
"status": "skipped_no_public_extension_module_target",
|
||||||
|
}
|
||||||
|
if require_name_first_extension_module:
|
||||||
|
failures.append("name-first extension module preflight target is required but was not discovered")
|
||||||
|
else:
|
||||||
|
module_preflight = rpc_call(
|
||||||
|
endpoint_url,
|
||||||
|
"metadata.write.preflight",
|
||||||
|
{
|
||||||
|
"base_id": base_id,
|
||||||
|
"target": module_candidate["target"],
|
||||||
|
"intent": module_candidate["intent"],
|
||||||
|
"resolve_origin": False,
|
||||||
|
"timeout_seconds": int(timeout),
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
transport=transport,
|
||||||
|
session_id=session_id,
|
||||||
|
)
|
||||||
|
plan = module_preflight.get("plan") if isinstance(module_preflight.get("plan"), dict) else {}
|
||||||
|
repository = module_preflight.get("repository") if isinstance(module_preflight.get("repository"), dict) else {}
|
||||||
|
support = module_preflight.get("support") if isinstance(module_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 module_candidate["target"]
|
||||||
|
for key in ("table", "file_name", "guid", "form_guid", "module_guid", "object_guid", "module_ref", "extension_guid")
|
||||||
|
)
|
||||||
|
checks["name_first_extension_module_preflight"] = {
|
||||||
|
"schema": module_preflight.get("schema"),
|
||||||
|
"status": module_preflight.get("status"),
|
||||||
|
"allowed": module_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": module_candidate["target"].get("extension"),
|
||||||
|
}
|
||||||
|
require(module_preflight.get("schema") == "onec_metadata_write_preflight.v1", "name-first extension module preflight must return expected schema", failures)
|
||||||
|
require(classified_preflight_status(module_preflight.get("status")), "name-first extension module preflight must classify readiness or a safety gate", failures)
|
||||||
|
require(plan.get("status") == "planned" and plan.get("allowed") is True, "name-first extension module preflight plan must be allowed and planned", failures)
|
||||||
|
require(name_first, "extension module preflight input must remain name-first", failures)
|
||||||
|
require(bool(EXTENSION_GUID_LAYER_RE.fullmatch(repository_layer)), "module repository gate must use a resolved extension GUID layer", failures)
|
||||||
|
require(repository_layer == support_layer, "module repository and support gates must use the same extension layer", failures)
|
||||||
|
conflicting_guid = different_extension_guid(repository_layer)
|
||||||
|
if not conflicting_guid:
|
||||||
|
checks["conflicting_extension_module_selector_preflight"] = {
|
||||||
|
"status": "skipped_no_resolved_extension_layer",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
conflicting = rpc_call(
|
||||||
|
endpoint_url,
|
||||||
|
"metadata.write.preflight",
|
||||||
|
{
|
||||||
|
"base_id": base_id,
|
||||||
|
"extension_guid": conflicting_guid,
|
||||||
|
"target": module_candidate["target"],
|
||||||
|
"intent": module_candidate["intent"],
|
||||||
|
"resolve_origin": False,
|
||||||
|
"timeout_seconds": int(timeout),
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
transport=transport,
|
||||||
|
session_id=session_id,
|
||||||
|
)
|
||||||
|
layer_resolution = (
|
||||||
|
conflicting.get("development_layer_resolution")
|
||||||
|
if isinstance(conflicting.get("development_layer_resolution"), dict)
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
layer_diagnostics = (
|
||||||
|
layer_resolution.get("diagnostics")
|
||||||
|
if isinstance(layer_resolution.get("diagnostics"), dict)
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
checks["conflicting_extension_module_selector_preflight"] = {
|
||||||
|
"schema": conflicting.get("schema"),
|
||||||
|
"status": conflicting.get("status"),
|
||||||
|
"allowed": conflicting.get("allowed"),
|
||||||
|
"resolution_status": layer_resolution.get("status"),
|
||||||
|
"error": layer_diagnostics.get("error"),
|
||||||
|
"layer_id": layer_resolution.get("layer_id"),
|
||||||
|
}
|
||||||
|
require(conflicting.get("schema") == "onec_metadata_write_preflight.v1", "conflicting extension module selector preflight must return expected schema", failures)
|
||||||
|
require(conflicting.get("status") == "blocked" and conflicting.get("allowed") is False, "conflicting extension module name/GUID must block preflight", failures)
|
||||||
|
require(layer_resolution.get("status") == "conflict", "conflicting extension module selector must report conflict resolution status", failures)
|
||||||
|
require(layer_diagnostics.get("error") == "extension_selector_conflict", "conflicting extension module selector must report extension_selector_conflict", 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",
|
||||||
@@ -418,6 +581,7 @@ def run_smoke(
|
|||||||
"base_id": base_id,
|
"base_id": base_id,
|
||||||
"requirements": {
|
"requirements": {
|
||||||
"name_first_extension_form": require_name_first_extension_form,
|
"name_first_extension_form": require_name_first_extension_form,
|
||||||
|
"name_first_extension_module": require_name_first_extension_module,
|
||||||
},
|
},
|
||||||
"checks": checks,
|
"checks": checks,
|
||||||
"failures": failures,
|
"failures": failures,
|
||||||
@@ -434,6 +598,7 @@ def main() -> int:
|
|||||||
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")
|
parser.add_argument("--require-name-first-extension-form", action="store_true")
|
||||||
|
parser.add_argument("--require-name-first-extension-module", 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
|
||||||
@@ -444,6 +609,7 @@ def main() -> int:
|
|||||||
args.timeout,
|
args.timeout,
|
||||||
transport=args.transport,
|
transport=args.transport,
|
||||||
require_name_first_extension_form=args.require_name_first_extension_form,
|
require_name_first_extension_form=args.require_name_first_extension_form,
|
||||||
|
require_name_first_extension_module=args.require_name_first_extension_module,
|
||||||
)
|
)
|
||||||
except (TimeoutError, URLError, OSError) as exc:
|
except (TimeoutError, URLError, OSError) as exc:
|
||||||
report = {
|
report = {
|
||||||
|
|||||||
@@ -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", "name_first_extension_form_preflight", "conflicting_extension_selector_preflight")) {
|
foreach ($check in @("method_exposed", "effective_path_preflight", "concrete_saved_state_preflight", "name_first_extension_form_preflight", "conflicting_extension_selector_preflight", "name_first_extension_module_preflight", "conflicting_extension_module_selector_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"
|
||||||
}
|
}
|
||||||
@@ -488,6 +488,7 @@ try {
|
|||||||
)
|
)
|
||||||
if ($RequireSelectorChainWritePlanComposition) {
|
if ($RequireSelectorChainWritePlanComposition) {
|
||||||
$writePreflightCommand += "--require-name-first-extension-form"
|
$writePreflightCommand += "--require-name-first-extension-form"
|
||||||
|
$writePreflightCommand += "--require-name-first-extension-module"
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -787,6 +788,7 @@ try {
|
|||||||
)
|
)
|
||||||
if ($RequireSelectorChainWritePlanComposition) {
|
if ($RequireSelectorChainWritePlanComposition) {
|
||||||
$mcpWritePreflightCommand += "--require-name-first-extension-form"
|
$mcpWritePreflightCommand += "--require-name-first-extension-form"
|
||||||
|
$mcpWritePreflightCommand += "--require-name-first-extension-module"
|
||||||
}
|
}
|
||||||
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
|
||||||
|
|||||||
@@ -11668,6 +11668,64 @@ def test_saved_state_modules_search_scopes_extension_to_cassave_prefix(monkeypat
|
|||||||
assert seen_rows == [f"{extension_guid}__object-guid.0"]
|
assert seen_rows == [f"{extension_guid}__object-guid.0"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("requested_extension", [None, "test2"])
|
||||||
|
def test_saved_state_modules_search_preserves_public_extension_name(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
requested_extension: str | None,
|
||||||
|
) -> None:
|
||||||
|
extension_guid = "FB26CF42-7609-11F1-828F-005056B0D483"
|
||||||
|
file_name = f"{extension_guid.lower()}__form-guid.0"
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
adapter_server,
|
||||||
|
"extension_map_by_guid",
|
||||||
|
lambda base_id: {extension_guid: {"guid": extension_guid, "name": "test2"}},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
adapter_server,
|
||||||
|
"storage_files_list",
|
||||||
|
lambda payload: {
|
||||||
|
"status": "ok",
|
||||||
|
"files": [{"FileName": file_name, "PartCount": 1, "Bytes": 1}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
adapter_server,
|
||||||
|
"saved_state_module_search_row",
|
||||||
|
lambda **kwargs: {
|
||||||
|
"table": "ConfigCASSave",
|
||||||
|
"file_name": file_name,
|
||||||
|
"owner": {"status": "resolved", "kind": "Catalog", "name": "test2"},
|
||||||
|
"form": {"name": "t_Форма"},
|
||||||
|
"module": {"kind": "form_module", "name": "Модуль формы"},
|
||||||
|
"qualified_name": "test2.t_Форма.Модуль формы",
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"preview": "Процедура Команда()\nКонецПроцедуры",
|
||||||
|
"match": {"query": None, "in_text": False},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"base_id": "upo_test",
|
||||||
|
"tables": ["ConfigCASSave"],
|
||||||
|
"limit": 5,
|
||||||
|
}
|
||||||
|
if requested_extension:
|
||||||
|
payload["extension"] = requested_extension
|
||||||
|
result = adapter_server.metadata_saved_state_modules_search(payload)
|
||||||
|
|
||||||
|
assert result["status"] == "ok"
|
||||||
|
assert result["counts"]["modules"] == 1
|
||||||
|
module = result["modules"][0]
|
||||||
|
assert module["selector"]["extension"] == "test2"
|
||||||
|
assert module["streams"][0]["write_plan_target"]["extension"] == "test2"
|
||||||
|
assert "extension_guid" not in str(module)
|
||||||
|
assert extension_guid.lower() not in str(module).lower()
|
||||||
|
|
||||||
|
|
||||||
def test_saved_state_forms_search_scopes_extension_to_cassave_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_saved_state_forms_search_scopes_extension_to_cassave_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
|
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
|
||||||
seen_file_list_payloads = []
|
seen_file_list_payloads = []
|
||||||
@@ -25065,12 +25123,14 @@ def test_public_saved_state_module_search_row_is_name_first() -> None:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
|
requested_extension="test2",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert public is not None
|
assert public is not None
|
||||||
assert public["qualified_name"] == "test2.t_Форма.Модуль формы"
|
assert public["qualified_name"] == "test2.t_Форма.Модуль формы"
|
||||||
assert public["selector"] == {
|
assert public["selector"] == {
|
||||||
|
"extension": "test2",
|
||||||
"ref": "Catalog.test2",
|
"ref": "Catalog.test2",
|
||||||
"form": "t_Форма",
|
"form": "t_Форма",
|
||||||
"module": "Модуль формы",
|
"module": "Модуль формы",
|
||||||
@@ -25079,6 +25139,7 @@ def test_public_saved_state_module_search_row_is_name_first() -> None:
|
|||||||
assert public["streams"][0]["match"] == {"query": "Команда", "in_text": True, "in_name": False}
|
assert public["streams"][0]["match"] == {"query": "Команда", "in_text": True, "in_name": False}
|
||||||
assert public["streams"][0]["write_plan_target"] == {
|
assert public["streams"][0]["write_plan_target"] == {
|
||||||
"kind": "module",
|
"kind": "module",
|
||||||
|
"extension": "test2",
|
||||||
"ref": "Catalog.test2",
|
"ref": "Catalog.test2",
|
||||||
"form": "t_Форма",
|
"form": "t_Форма",
|
||||||
"module": "Модуль формы",
|
"module": "Модуль формы",
|
||||||
|
|||||||
@@ -112,6 +112,72 @@ def test_public_extension_form_candidate_is_name_first_and_prefers_title() -> No
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_extension_module_candidate_is_name_first() -> None:
|
||||||
|
result = smoke.first_public_extension_module_candidate(
|
||||||
|
{
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"preview": "\n&НаКлиенте\nПроцедура Команда()\nКонецПроцедуры",
|
||||||
|
"write_plan_target": {
|
||||||
|
"kind": "module",
|
||||||
|
"extension": "test2",
|
||||||
|
"ref": "Catalog.test2",
|
||||||
|
"form": "t_Форма",
|
||||||
|
"module": "Модуль формы",
|
||||||
|
"qualified_name": "test2.t_Форма.Модуль формы",
|
||||||
|
"stream_ordinal": 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"target": {
|
||||||
|
"kind": "module",
|
||||||
|
"extension": "test2",
|
||||||
|
"ref": "Catalog.test2",
|
||||||
|
"form": "t_Форма",
|
||||||
|
"module": "Модуль формы",
|
||||||
|
"qualified_name": "test2.t_Форма.Модуль формы",
|
||||||
|
"stream_ordinal": 1,
|
||||||
|
},
|
||||||
|
"intent": {
|
||||||
|
"operation": "replace_with_control",
|
||||||
|
"control_fragment": "&НаКлиенте",
|
||||||
|
"new": "&НаКлиенте\nПроцедура Команда()\nКонецПроцедуры",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_extension_module_candidate_rejects_storage_handle() -> None:
|
||||||
|
result = smoke.first_public_extension_module_candidate(
|
||||||
|
{
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"preview": "Процедура Команда()\nКонецПроцедуры",
|
||||||
|
"write_plan_target": {
|
||||||
|
"extension": "test2",
|
||||||
|
"ref": "Catalog.test2",
|
||||||
|
"module": "Модуль формы",
|
||||||
|
"module_ref": "ConfigCASSave:opaque.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
def test_different_extension_guid_changes_only_last_hex_digit() -> None:
|
def test_different_extension_guid_changes_only_last_hex_digit() -> None:
|
||||||
assert (
|
assert (
|
||||||
smoke.different_extension_guid(
|
smoke.different_extension_guid(
|
||||||
|
|||||||
Reference in New Issue
Block a user