Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,552 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
sys.path.insert(0, str(ROOT / "plugins" / "1c" / "connector"))
|
||||
sys.path.insert(0, str(ROOT / "plugins" / "1c" / "mcp"))
|
||||
|
||||
import adapter_1c_mcp as adapter_mcp # noqa: E402
|
||||
import adapter_1c_server as adapter_server # noqa: E402
|
||||
import smoke_1c_mcp_selector_chain as selector_chain_smoke # noqa: E402
|
||||
|
||||
|
||||
FORBIDDEN_CONCRETE_SELECTOR_VALUES = (
|
||||
"ПечатьЭтикеток",
|
||||
"УОП_Печать",
|
||||
"ЦенаСоСкидкой",
|
||||
"fs_Отчеты",
|
||||
)
|
||||
PRODUCTION_SELECTOR_CONTRACT_ROOTS = (
|
||||
ROOT / "plugins" / "1c",
|
||||
ROOT / "docs",
|
||||
)
|
||||
PRODUCTION_SELECTOR_CONTRACT_SUFFIXES = {
|
||||
".css",
|
||||
".html",
|
||||
".js",
|
||||
".json",
|
||||
".md",
|
||||
".ps1",
|
||||
".py",
|
||||
".toml",
|
||||
".txt",
|
||||
".yaml",
|
||||
".yml",
|
||||
}
|
||||
SYNTHETIC_SELECTOR_GUID = "00000000-0000-4000-8000-000000000001"
|
||||
PRODUCTION_SELECTOR_CONTRACT_EXCLUDED_PARTS = {
|
||||
"__pycache__",
|
||||
".pytest_cache",
|
||||
"reports",
|
||||
"node_modules",
|
||||
".git",
|
||||
}
|
||||
|
||||
|
||||
def find_tool(name: str) -> dict[str, Any] | None:
|
||||
for tool in adapter_mcp.TOOLS:
|
||||
if tool.get("name") == name:
|
||||
return tool
|
||||
return None
|
||||
|
||||
|
||||
def iter_production_selector_contract_files() -> list[Path]:
|
||||
files: list[Path] = []
|
||||
for root in PRODUCTION_SELECTOR_CONTRACT_ROOTS:
|
||||
if not root.exists():
|
||||
continue
|
||||
for path in root.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
try:
|
||||
relative_parts = set(path.relative_to(ROOT).parts)
|
||||
except ValueError:
|
||||
relative_parts = set(path.parts)
|
||||
if relative_parts.intersection(PRODUCTION_SELECTOR_CONTRACT_EXCLUDED_PARTS):
|
||||
continue
|
||||
if path.suffix.lower() not in PRODUCTION_SELECTOR_CONTRACT_SUFFIXES:
|
||||
continue
|
||||
files.append(path)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def contract_checks(issues: list[dict[str, Any]], *, onec_request_present: bool) -> dict[str, bool]:
|
||||
return {
|
||||
"generic_onec_request_schema": onec_request_present
|
||||
and not any(
|
||||
issue["code"].startswith("mcp_onec_request")
|
||||
or issue["code"] in {"mcp_payload_not_open", "mcp_payload_selector_guidance_missing"}
|
||||
for issue in issues
|
||||
),
|
||||
"mcp_tool_selector_guidance": not any(issue["code"] in {"mcp_tool_selector_guidance_missing", "mcp_examples_public_ref_placeholder_missing"} for issue in issues),
|
||||
"all_adapter_methods_forward_over_rpc": not any(issue["code"].startswith("mcp_method") or issue["code"] == "mcp_rpc_body_method_mismatch" for issue in issues),
|
||||
"no_unified_shadowing_adapter_methods": not any(issue["code"] == "mcp_unified_shadows_adapter_methods" for issue in issues),
|
||||
"adapter_help_selector_guidance": not any(issue["code"] == "adapter_help_selector_guidance_missing" for issue in issues),
|
||||
"adapter_selector_argument_type_validation": not any(issue["code"] == "adapter_selector_argument_type_not_validated" for issue in issues),
|
||||
"adapter_selector_normalizer_type_validation": not any(issue["code"] == "adapter_selector_normalizer_type_not_validated" for issue in issues),
|
||||
"adapter_selector_required_message": not any(
|
||||
issue["code"] in {"adapter_selector_required_message_incomplete", "adapter_selector_message_constant_incomplete"}
|
||||
for issue in issues
|
||||
),
|
||||
"adapter_selector_presence_aliases": not any(issue["code"] == "adapter_selector_presence_alias_mismatch" for issue in issues),
|
||||
"adapter_selector_capability_descriptor": not any(
|
||||
issue["code"] in {"adapter_selector_capability_descriptor_mismatch", "adapter_help_selector_capabilities_missing"}
|
||||
for issue in issues
|
||||
),
|
||||
"adapter_definition_read_selector_public_ref": not any(issue["code"] == "adapter_definition_read_selector_public_ref_missing" for issue in issues),
|
||||
"adapter_module_read_selector_public_ref": not any(issue["code"] == "adapter_module_read_selector_public_ref_missing" for issue in issues),
|
||||
"adapter_parse_ordinal_unpacked": not any(issue["code"] == "adapter_parse_ordinal_not_unpacked" for issue in issues),
|
||||
"adapter_contract_version": not any(
|
||||
issue["code"] in {"adapter_contract_version_missing", "mcp_contract_version_mismatch", "adapter_help_contract_version_missing"}
|
||||
for issue in issues
|
||||
),
|
||||
"mcp_policy_blocks_missing_base_id": not any(issue["code"] == "mcp_policy_missing_base_id_not_blocked" for issue in issues),
|
||||
"mcp_policy_blocks_diagnostic_fallback": not any(issue["code"] == "mcp_policy_diagnostic_fallback_not_blocked" for issue in issues),
|
||||
"mcp_policy_allows_explicit_diagnostics": not any(issue["code"] == "mcp_policy_explicit_diagnostic_not_forwarded" for issue in issues),
|
||||
"mcp_selector_schema_template": not any(
|
||||
issue["code"] in {"mcp_selector_schema_properties_mismatch", "mcp_schema_requires_object_type_name_selector"}
|
||||
for issue in issues
|
||||
),
|
||||
"selector_chain_live_coverage_schema": not any(issue["code"].startswith("selector_chain_coverage_") for issue in issues),
|
||||
"selector_chain_strict_composition_gate": not any(issue["code"].startswith("selector_chain_strict_") for issue in issues),
|
||||
"selector_chain_working_state": not any(issue["code"].startswith("selector_chain_working_state_") for issue in issues),
|
||||
"no_concrete_selector_values_in_production": not any(issue["code"] == "production_concrete_selector_value_present" for issue in issues),
|
||||
}
|
||||
|
||||
|
||||
def check_contract() -> dict[str, Any]:
|
||||
issues: list[dict[str, Any]] = []
|
||||
methods = [str(row.get("name") or "") for row in adapter_server.METHODS if row.get("name")]
|
||||
method_set = set(methods)
|
||||
selector_guidance_terms = tuple(getattr(adapter_server, "OBJECT_SELECTOR_GUIDANCE_TERMS", ("ref", "kind/name/guid", "object_type/object_name/object_guid")))
|
||||
adapter_contract_version = str(getattr(adapter_server, "ADAPTER_CONTRACT_VERSION", ""))
|
||||
mcp_contract_version = str(getattr(adapter_mcp, "MCP_CONTRACT_VERSION", ""))
|
||||
if not adapter_contract_version:
|
||||
issues.append({"code": "adapter_contract_version_missing"})
|
||||
if adapter_contract_version != mcp_contract_version:
|
||||
issues.append({"code": "mcp_contract_version_mismatch", "adapter": adapter_contract_version, "mcp": mcp_contract_version})
|
||||
duplicate_methods = sorted({name for name in methods if methods.count(name) > 1})
|
||||
if duplicate_methods:
|
||||
issues.append({"code": "adapter_methods_not_unique", "methods": duplicate_methods})
|
||||
shadowed_methods = sorted(method_set.intersection(getattr(adapter_mcp, "UNIFIED_METHODS", set())))
|
||||
if shadowed_methods:
|
||||
issues.append({"code": "mcp_unified_shadows_adapter_methods", "methods": shadowed_methods})
|
||||
mcp_selector_schema_args = set(getattr(adapter_mcp, "OBJECT_SELECTOR_SCHEMA_PROPERTIES", {}))
|
||||
adapter_selector_args = set(getattr(adapter_server, "OBJECT_SELECTOR_ARGUMENTS", []))
|
||||
if mcp_selector_schema_args != adapter_selector_args:
|
||||
issues.append(
|
||||
{
|
||||
"code": "mcp_selector_schema_properties_mismatch",
|
||||
"mcp_arguments": sorted(mcp_selector_schema_args),
|
||||
"adapter_arguments": sorted(adapter_selector_args),
|
||||
}
|
||||
)
|
||||
selector_capability_methods = set(getattr(adapter_server, "OBJECT_SELECTOR_METHOD_CAPABILITIES", {}))
|
||||
selector_alias_methods = set(getattr(adapter_server, "OBJECT_SELECTOR_ALIAS_METHODS", set()))
|
||||
if selector_capability_methods != selector_alias_methods:
|
||||
issues.append(
|
||||
{
|
||||
"code": "adapter_selector_capability_descriptor_mismatch",
|
||||
"capability_methods": sorted(selector_capability_methods),
|
||||
"alias_methods": sorted(selector_alias_methods),
|
||||
}
|
||||
)
|
||||
mcp_source = Path(adapter_mcp.__file__).read_text(encoding="utf-8")
|
||||
adapter_source = Path(adapter_server.__file__).read_text(encoding="utf-8")
|
||||
for line_number, line in enumerate(adapter_source.splitlines(), start=1):
|
||||
if "parse_ordinal(" not in line or "=" not in line:
|
||||
continue
|
||||
left_side = line.split("=", 1)[0].strip()
|
||||
if "," not in left_side and not left_side.startswith(("return ", "raise ")):
|
||||
issues.append(
|
||||
{
|
||||
"code": "adapter_parse_ordinal_not_unpacked",
|
||||
"line": line_number,
|
||||
"source": line.strip(),
|
||||
}
|
||||
)
|
||||
if '"required": ["base_id", "object_type", "object_name"]' in mcp_source:
|
||||
issues.append({"code": "mcp_schema_requires_object_type_name_selector"})
|
||||
normalizer_check = adapter_server.normalize_object_selector_aliases({"kind": 123}, "contract.selector")
|
||||
if not isinstance(normalizer_check, dict) or normalizer_check.get("status") != "invalid_argument" or normalizer_check.get("argument") != "kind":
|
||||
issues.append(
|
||||
{
|
||||
"code": "adapter_selector_normalizer_type_not_validated",
|
||||
"result": normalizer_check,
|
||||
}
|
||||
)
|
||||
required_message = str(getattr(adapter_server, "OBJECT_SELECTOR_REQUIRED_MESSAGE", ""))
|
||||
for term in (*selector_guidance_terms, "ordinal"):
|
||||
if term not in required_message:
|
||||
issues.append(
|
||||
{
|
||||
"code": "adapter_selector_required_message_incomplete",
|
||||
"term": term,
|
||||
"message": required_message,
|
||||
}
|
||||
)
|
||||
selector_message_constants = {
|
||||
"OBJECT_SELECTOR_REQUIRED_MESSAGE_NO_ORDINAL": selector_guidance_terms,
|
||||
"OBJECT_SELECTOR_GLOBAL_REQUIRED_MESSAGE": (*selector_guidance_terms, "areas metadata/extensions"),
|
||||
"OBJECT_SELECTOR_OR_MODULE_REQUIRED_MESSAGE": (*selector_guidance_terms, "module_ref", "module_id"),
|
||||
"MODULE_READ_SELECTOR_OR_MODULE_ID_MESSAGE": (*selector_guidance_terms, "ordinal", "module_id"),
|
||||
}
|
||||
for constant_name, required_terms in selector_message_constants.items():
|
||||
message = str(getattr(adapter_server, constant_name, ""))
|
||||
for term in required_terms:
|
||||
if term not in message:
|
||||
issues.append(
|
||||
{
|
||||
"code": "adapter_selector_message_constant_incomplete",
|
||||
"constant": constant_name,
|
||||
"term": term,
|
||||
"message": message,
|
||||
}
|
||||
)
|
||||
selector_presence_cases = [
|
||||
({"ref": "Document.ObjectName"}, True),
|
||||
({"object_name": "ObjectName"}, True),
|
||||
({"object_guid": SYNTHETIC_SELECTOR_GUID}, True),
|
||||
({"kind": "Document"}, False),
|
||||
({"object_type": "Document"}, False),
|
||||
]
|
||||
for payload, expected in selector_presence_cases:
|
||||
actual = adapter_server.has_object_selector(payload)
|
||||
if actual is not expected:
|
||||
issues.append(
|
||||
{
|
||||
"code": "adapter_selector_presence_alias_mismatch",
|
||||
"payload": payload,
|
||||
"expected": expected,
|
||||
"actual": actual,
|
||||
}
|
||||
)
|
||||
read_selector = adapter_server.definition_read_selector(
|
||||
"contract_base",
|
||||
{
|
||||
"kind": "DataProcessor",
|
||||
"name": "ObjectName",
|
||||
"guid": SYNTHETIC_SELECTOR_GUID,
|
||||
},
|
||||
method="metadata.object.get",
|
||||
)
|
||||
if read_selector.get("ref") != "DataProcessor.ObjectName":
|
||||
issues.append(
|
||||
{
|
||||
"code": "adapter_definition_read_selector_public_ref_missing",
|
||||
"selector": read_selector,
|
||||
}
|
||||
)
|
||||
module_read_selector = adapter_server.enrich_selector_with_object_ref(
|
||||
{
|
||||
"base_id": "contract_base",
|
||||
"method": "modules.read",
|
||||
"kind": "DataProcessor",
|
||||
"guid": SYNTHETIC_SELECTOR_GUID,
|
||||
"module_ordinal": 1,
|
||||
},
|
||||
{
|
||||
"kind": "DataProcessor",
|
||||
"name": "ObjectName",
|
||||
"guid": SYNTHETIC_SELECTOR_GUID,
|
||||
},
|
||||
)
|
||||
if module_read_selector.get("ref") != "DataProcessor.ObjectName" or module_read_selector.get("name") != "ObjectName":
|
||||
issues.append(
|
||||
{
|
||||
"code": "adapter_module_read_selector_public_ref_missing",
|
||||
"selector": module_read_selector,
|
||||
}
|
||||
)
|
||||
synthetic_live_steps = [
|
||||
{
|
||||
"name": "metadata.resolve_overrides",
|
||||
"status": "not_found",
|
||||
"write_plan_evidence": True,
|
||||
"next_method": adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD,
|
||||
},
|
||||
{
|
||||
"name": adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD,
|
||||
"status": "ok",
|
||||
"modules": 0,
|
||||
"write_plan_target": False,
|
||||
},
|
||||
{
|
||||
"name": adapter_server.METADATA_WRITE_PLAN_METHOD,
|
||||
"status": "skipped_no_saved_state_target",
|
||||
"from_write_plan_target": False,
|
||||
},
|
||||
]
|
||||
live_coverage = selector_chain_smoke.live_coverage_from_steps(synthetic_live_steps)
|
||||
expected_coverage = {
|
||||
"resolve_overrides": {
|
||||
"attempted": True,
|
||||
"status": "not_found",
|
||||
"write_plan_evidence": True,
|
||||
"next_method": adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD,
|
||||
},
|
||||
"saved_state_resolution": {
|
||||
"attempted": True,
|
||||
"status": "ok",
|
||||
"modules": 0,
|
||||
"write_plan_target": False,
|
||||
},
|
||||
"write_plan_composition": {
|
||||
"attempted": True,
|
||||
"status": "skipped_no_saved_state_target",
|
||||
"composed": False,
|
||||
"from_write_plan_target": False,
|
||||
},
|
||||
}
|
||||
for section, expected in expected_coverage.items():
|
||||
actual = live_coverage.get(section)
|
||||
if actual != expected:
|
||||
issues.append(
|
||||
{
|
||||
"code": "selector_chain_coverage_section_mismatch",
|
||||
"section": section,
|
||||
"expected": expected,
|
||||
"actual": actual,
|
||||
}
|
||||
)
|
||||
if live_coverage.get("skips") != [{"step": adapter_server.METADATA_WRITE_PLAN_METHOD, "status": "skipped_no_saved_state_target"}]:
|
||||
issues.append({"code": "selector_chain_coverage_skips_mismatch", "actual": live_coverage.get("skips")})
|
||||
strict_skip_issue = selector_chain_smoke.live_write_plan_composition_required_issue(live_coverage)
|
||||
if not isinstance(strict_skip_issue, dict) or strict_skip_issue.get("code") != "live_write_plan_composition_required":
|
||||
issues.append({"code": "selector_chain_strict_skip_not_blocked", "actual": strict_skip_issue})
|
||||
composed_coverage = selector_chain_smoke.live_coverage_from_steps(
|
||||
[
|
||||
{
|
||||
"name": "metadata.resolve_overrides",
|
||||
"status": "ok",
|
||||
"write_plan_evidence": True,
|
||||
"next_method": adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD,
|
||||
},
|
||||
{
|
||||
"name": adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD,
|
||||
"status": "ok",
|
||||
"modules": 1,
|
||||
"write_plan_target": True,
|
||||
},
|
||||
{
|
||||
"name": adapter_server.METADATA_WRITE_PLAN_METHOD,
|
||||
"status": "planned",
|
||||
"allowed": True,
|
||||
"from_write_plan_target": True,
|
||||
},
|
||||
]
|
||||
)
|
||||
strict_composed_issue = selector_chain_smoke.live_write_plan_composition_required_issue(composed_coverage)
|
||||
if strict_composed_issue is not None:
|
||||
issues.append({"code": "selector_chain_strict_composed_blocked", "actual": strict_composed_issue})
|
||||
selector_chain_report = selector_chain_smoke.build_report()
|
||||
if selector_chain_report.get("passed") is not True:
|
||||
issues.append({"code": "selector_chain_working_state_smoke_failed", "report": selector_chain_report})
|
||||
for chain in selector_chain_report.get("chains") or []:
|
||||
if not isinstance(chain, dict):
|
||||
continue
|
||||
for step in chain.get("steps") or []:
|
||||
if not isinstance(step, dict) or step.get("method") not in selector_chain_smoke.WORKING_STATE_METHODS:
|
||||
continue
|
||||
payload = step.get("payload") if isinstance(step.get("payload"), dict) else {}
|
||||
if payload.get("source_state") != "working" and payload.get("state") != "working":
|
||||
issues.append(
|
||||
{
|
||||
"code": "selector_chain_working_state_missing",
|
||||
"chain": chain.get("name"),
|
||||
"method": step.get("method"),
|
||||
}
|
||||
)
|
||||
production_selector_files = iter_production_selector_contract_files()
|
||||
for path in production_selector_files:
|
||||
if not path.exists():
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
for value in FORBIDDEN_CONCRETE_SELECTOR_VALUES:
|
||||
if value in text:
|
||||
issues.append(
|
||||
{
|
||||
"code": "production_concrete_selector_value_present",
|
||||
"path": str(path.relative_to(ROOT)),
|
||||
"value": value,
|
||||
}
|
||||
)
|
||||
public_methods = {
|
||||
str(row.get("name") or ""): row
|
||||
for row in adapter_server.call_method("help.methods", {}).get("methods", [])
|
||||
if isinstance(row, dict) and row.get("name")
|
||||
}
|
||||
help_result = adapter_server.call_method("help.methods", {})
|
||||
if help_result.get("contract_version") != adapter_contract_version:
|
||||
issues.append(
|
||||
{
|
||||
"code": "adapter_help_contract_version_missing",
|
||||
"expected": adapter_contract_version,
|
||||
"actual": help_result.get("contract_version"),
|
||||
}
|
||||
)
|
||||
for method in sorted(getattr(adapter_server, "OBJECT_SELECTOR_ALIAS_METHODS", set())):
|
||||
capabilities = (public_methods.get(method) or {}).get("selector_capabilities")
|
||||
if not isinstance(capabilities, dict) or not capabilities.get("accepts_ref") or not capabilities.get("accepts_object_aliases"):
|
||||
issues.append(
|
||||
{
|
||||
"code": "adapter_help_selector_capabilities_missing",
|
||||
"method": method,
|
||||
"selector_capabilities": capabilities,
|
||||
}
|
||||
)
|
||||
description = str((public_methods.get(method) or {}).get("description") or "")
|
||||
for term in selector_guidance_terms:
|
||||
if term not in description:
|
||||
issues.append(
|
||||
{
|
||||
"code": "adapter_help_selector_guidance_missing",
|
||||
"method": method,
|
||||
"term": term,
|
||||
"description": description,
|
||||
}
|
||||
)
|
||||
for argument in getattr(adapter_server, "OBJECT_SELECTOR_ARGUMENTS", ["ref"]):
|
||||
selector_check = adapter_server.validate_adapter_job_payload(method, {"base_id": "contract_base", argument: 123})
|
||||
if (
|
||||
not isinstance(selector_check, dict)
|
||||
or selector_check.get("status") != "invalid_argument"
|
||||
or selector_check.get("argument") != argument
|
||||
):
|
||||
issues.append(
|
||||
{
|
||||
"code": "adapter_selector_argument_type_not_validated",
|
||||
"method": method,
|
||||
"argument": argument,
|
||||
"result": selector_check,
|
||||
}
|
||||
)
|
||||
|
||||
onec_request = find_tool("onec_request")
|
||||
if not onec_request:
|
||||
issues.append({"code": "mcp_onec_request_missing"})
|
||||
else:
|
||||
schema = onec_request.get("inputSchema") or {}
|
||||
tool_description = str(onec_request.get("description") or "")
|
||||
for term in selector_guidance_terms:
|
||||
if term not in tool_description:
|
||||
issues.append(
|
||||
{
|
||||
"code": "mcp_tool_selector_guidance_missing",
|
||||
"term": term,
|
||||
"description": tool_description,
|
||||
}
|
||||
)
|
||||
examples_json = json.dumps(schema.get("examples") or [], ensure_ascii=False)
|
||||
if "<metadata-kind>.<metadata-object-name>" not in examples_json:
|
||||
issues.append({"code": "mcp_examples_public_ref_placeholder_missing"})
|
||||
if schema.get("required") != ["method"]:
|
||||
issues.append({"code": "mcp_onec_request_required_not_generic", "required": schema.get("required")})
|
||||
if "oneOf" in schema:
|
||||
issues.append({"code": "mcp_onec_request_enumerates_adapter_methods"})
|
||||
payload_schema = (schema.get("properties") or {}).get("payload") or {}
|
||||
if payload_schema.get("additionalProperties") is not True:
|
||||
issues.append({"code": "mcp_payload_not_open", "payload_schema": payload_schema})
|
||||
payload_description = str(payload_schema.get("description") or "")
|
||||
for term in selector_guidance_terms:
|
||||
if term not in payload_description:
|
||||
issues.append(
|
||||
{
|
||||
"code": "mcp_payload_selector_guidance_missing",
|
||||
"term": term,
|
||||
"description": payload_description,
|
||||
}
|
||||
)
|
||||
|
||||
calls: list[tuple[str, str, Any]] = []
|
||||
|
||||
def fake_http_json(method: str, path: str, body: Any | None = None, *, timeout: float | None = None) -> dict[str, Any]:
|
||||
calls.append((method, path, body))
|
||||
return {"status": "ok", "method": body.get("method") if isinstance(body, dict) else "health"}
|
||||
|
||||
original_http_json = adapter_mcp.http_json
|
||||
adapter_mcp.http_json = fake_http_json
|
||||
try:
|
||||
calls.clear()
|
||||
missing_base_id_result = adapter_mcp.run_or_enqueue_adapter_method("metadata.write.plan", {})
|
||||
if calls or not isinstance(missing_base_id_result, dict) or missing_base_id_result.get("schema") != "adapter_1c_mcp_policy.v1" or missing_base_id_result.get("reason") != "base_id_required":
|
||||
issues.append(
|
||||
{
|
||||
"code": "mcp_policy_missing_base_id_not_blocked",
|
||||
"result": missing_base_id_result,
|
||||
"calls": calls,
|
||||
}
|
||||
)
|
||||
|
||||
calls.clear()
|
||||
diagnostic_fallback_result = adapter_mcp.run_or_enqueue_adapter_method("storage.files.list", {"base_id": "contract_base"})
|
||||
if calls or not isinstance(diagnostic_fallback_result, dict) or diagnostic_fallback_result.get("schema") != "adapter_1c_mcp_policy.v1" or diagnostic_fallback_result.get("reason") != "diagnostic_method":
|
||||
issues.append(
|
||||
{
|
||||
"code": "mcp_policy_diagnostic_fallback_not_blocked",
|
||||
"result": diagnostic_fallback_result,
|
||||
"calls": calls,
|
||||
}
|
||||
)
|
||||
|
||||
calls.clear()
|
||||
adapter_mcp.run_or_enqueue_adapter_method("storage.files.list", {"base_id": "contract_base", "diagnostic": True})
|
||||
if not calls:
|
||||
issues.append({"code": "mcp_policy_explicit_diagnostic_not_forwarded"})
|
||||
|
||||
for method in methods:
|
||||
calls.clear()
|
||||
adapter_mcp.call_adapter_method(method, {"base_id": "contract_base"})
|
||||
if method == "health":
|
||||
expected = ("GET", "/health?base_id=contract_base")
|
||||
if not calls or calls[0][0:2] != expected:
|
||||
issues.append({"code": "mcp_health_not_get_health", "method": method, "calls": calls})
|
||||
continue
|
||||
if not calls:
|
||||
issues.append({"code": "mcp_method_not_forwarded", "method": method})
|
||||
continue
|
||||
http_method, path, body = calls[0]
|
||||
if http_method != "POST" or path != "/rpc":
|
||||
issues.append({"code": "mcp_method_not_rpc", "method": method, "calls": calls})
|
||||
continue
|
||||
if not isinstance(body, dict) or body.get("method") != method:
|
||||
issues.append({"code": "mcp_rpc_body_method_mismatch", "method": method, "body": body})
|
||||
finally:
|
||||
adapter_mcp.http_json = original_http_json
|
||||
|
||||
return {
|
||||
"schema": "onec_mcp_adapter_contract_check.v1",
|
||||
"passed": not issues,
|
||||
"adapter_methods": len(methods),
|
||||
"production_selector_files": len(production_selector_files),
|
||||
"mcp_tools": [tool.get("name") for tool in adapter_mcp.TOOLS],
|
||||
"checks": contract_checks(issues, onec_request_present=onec_request is not None),
|
||||
"issues": issues,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Check that adapter-1c MCP stays a generic proxy for REST adapter methods.")
|
||||
parser.add_argument("--json", action="store_true", help="Print JSON report.")
|
||||
args = parser.parse_args()
|
||||
|
||||
report = check_contract()
|
||||
if args.json:
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
elif report["passed"]:
|
||||
print(f"OK: MCP generic proxy contract passed for {report['adapter_methods']} adapter methods.")
|
||||
else:
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2), file=sys.stderr)
|
||||
return 0 if report["passed"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user