from __future__ import annotations
import sys
import base64
import hashlib
import json
import sqlite3
import time
from pathlib import Path
from typing import Any
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "plugins" / "1c"))
from parser.payload import ( # noqa: E402
append_brace_text_child,
compress_payload,
decode_payload_lossless,
encode_brace_tree,
encode_payload_lossless,
get_tree_path,
patch_brace_text_path,
parse_brace_text,
serialize_brace_tree,
set_tree_path,
try_decode,
try_decompress,
)
from parser.config_object import find_identity # noqa: E402
from parser.config_semantic import decode_config_semantic # noqa: E402
from parser.cas_payload import classify_payload, replace_stream_block, stream_header # noqa: E402
from parser.bsl_validation import replace_routine_text, routine_blocks, text_sha1, validate_bsl_text # noqa: E402
from parser.form_payload import command_handler_links, data_path_reference_from_node, decode_form_payload, dynamic_list_fields, dynamic_list_settings, embedded_table_addition_row, enrich_form_common_semantic, enrich_item_data_paths, enrich_item_reference_semantics, event_handlers, event_handlers_from_node, form_common_semantic, section_record_semantic_properties # noqa: E402
from parser.form_xml import decode_form_xml # noqa: E402
from parser.dbnames import parse_dbnames_bytes, parse_dbnames_version_bytes # noqa: E402
sys.path.insert(0, str(ROOT / "plugins" / "1c" / "connector"))
import adapter_1c_server as adapter_server # noqa: E402
from adapter_1c_server import call_method, filter_related_children_by_identity, limit_object_commands_result, module_text_response, payload_public_preview, payload_public_properties, payload_public_undecoded_evidence, public_form_profile, public_form_row, public_metadata_item, public_module_row, public_semantic_profile, search_modules, strip_storage_traces # noqa: E402
from adapter_1c_server import validate_metadata_object_full_payload # noqa: E402
sys.path.insert(0, str(ROOT / "plugins" / "1c" / "mcp"))
import adapter_1c_mcp as adapter_mcp # noqa: E402
from adapter_1c_mcp import validate_metadata_object_full_sections # noqa: E402
import scripts.check_1c_mcp_adapter_contract as mcp_adapter_contract # noqa: E402
import scripts.smoke_1c_mcp_selector_chain as mcp_selector_chain_smoke # noqa: E402
import scripts.export_1c_access_role_audit as access_role_audit_export_script # noqa: E402
import scripts.compare_1c_access_role_audit as access_role_audit_compare_script # noqa: E402
import scripts.audit_1c_access_role as access_role_audit_script # noqa: E402
import scripts.compare_1c_form_sql_xml as form_sql_xml_compare_script # noqa: E402
def test_dbnames_version_parser_accepts_utf8_bom_scalar_version() -> None:
version = "E27A62F5-8C82-4132-BD16-281F9E47DEFC"
parsed = parse_dbnames_version_bytes(f"{{0,{version}}}".encode("utf-8-sig"))
assert parsed["marker"] == 0
assert parsed["version"] == version.lower()
assert parsed["encoding"] == "utf-8-sig"
assert parsed["compression"] == "none"
@pytest.mark.parametrize(
("payload", "message"),
[
(b"", "expected root list"),
(b"{0,{version}}", "expected scalar marker and version"),
(b"{0,not-a-guid}", "version must be a GUID string"),
],
)
def test_dbnames_version_parser_rejects_invalid_shape(payload: bytes, message: str) -> None:
with pytest.raises(ValueError, match=message):
parse_dbnames_version_bytes(payload)
def test_dbnames_records_parser_keeps_existing_contract() -> None:
parsed = parse_dbnames_bytes(b'{12700,{1,{11111111-1111-1111-1111-111111111111,"Role",7}}}')
assert parsed["declared_count"] == 1
assert len(parsed["records"]) == 1
assert parsed["records"][0].storage_role == "Role"
def test_dbnames_sequence_role_maps_to_public_sequence_kind() -> None:
parsed = parse_dbnames_bytes(b'{12700,{1,{11111111-1111-1111-1111-111111111111,"Sequence",7}}}')
record = parsed["records"][0]
assert adapter_server.DBNAMES_ROLE_KIND[record.storage_role] == "Sequence"
assert adapter_server.PUBLIC_KIND["Sequence"] == "sequence"
assert adapter_server.ONEC_CODE_ROOT_BY_KIND["Sequence"] == "Последовательности"
def test_dbnames_summary_routes_version_to_separate_parser(monkeypatch: pytest.MonkeyPatch) -> None:
payloads = {
"DBNames": b'{12700,{1,{11111111-1111-1111-1111-111111111111,"Role",7}}}',
"DBNamesVersion": b"\xef\xbb\xbf{0,e27a62f5-8c82-4132-bd16-281f9e47defc}",
}
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda _payload: {"status": "ok", "files": [{"FileName": name} for name in payloads]},
)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda _base_id, _table, file_name, **_kwargs: (payloads[file_name], {}, None),
)
result = adapter_server.metadata_dbnames_summary({"base_id": "upo_test", "diagnostic": True})
assert result["status"] == "ok"
assert result["counts"] == {"files": 2, "records": 1}
summaries = {item["file_name"]: item for item in result["dbnames"]}
assert summaries["DBNames"]["records"] == 1
assert summaries["DBNamesVersion"]["marker"] == 0
assert summaries["DBNamesVersion"]["version"] == "e27a62f5-8c82-4132-bd16-281f9e47defc"
def test_live_dbnames_records_ignores_dbnames_version(monkeypatch: pytest.MonkeyPatch) -> None:
seen: list[str] = []
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda _payload: {"status": "ok", "files": [{"FileName": "DBNames"}, {"FileName": "DBNamesVersion"}]},
)
def read_file(_base_id: str, _table: str, file_name: str, **_kwargs: Any) -> tuple[bytes, dict[str, Any], None]:
seen.append(file_name)
return b'{12700,{1,{11111111-1111-1111-1111-111111111111,"Role",7}}}', {}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", read_file)
records, error = adapter_server.live_dbnames_records("upo_test")
assert error is None
assert len(records or []) == 1
assert seen == ["DBNames"]
@pytest.mark.parametrize("extension_guid", [None, ""])
def test_mcp_code_search_omits_empty_extension_guid(monkeypatch: pytest.MonkeyPatch, extension_guid: Any) -> None:
captured: dict[str, Any] = {}
def call_adapter(method: str, payload: dict[str, Any], **_kwargs: Any) -> dict[str, Any]:
captured.update(payload)
return {"schema": "onec_code_search.v1", "method": method, "status": "ok", "items": []}
monkeypatch.setattr(adapter_mcp, "call_adapter_method", call_adapter)
result = adapter_mcp.run_or_enqueue_adapter_method(
"code.search",
{"base_id": "upo_test", "query": "Найти", "extension_guid": extension_guid, "_mcp_sync": True},
)
assert result["status"] == "ok"
assert "extension_guid" not in captured
def test_mcp_code_search_preserves_valid_extension_guid(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "e27a62f5-8c82-4132-bd16-281f9e47defc"
captured: dict[str, Any] = {}
def call_adapter(method: str, payload: dict[str, Any], **_kwargs: Any) -> dict[str, Any]:
captured.update(payload)
return {"schema": "onec_code_search.v1", "method": method, "status": "ok", "items": []}
monkeypatch.setattr(adapter_mcp, "call_adapter_method", call_adapter)
adapter_mcp.run_or_enqueue_adapter_method(
"code.search",
{"base_id": "upo_test", "query": "Найти", "extension_guid": extension_guid, "_mcp_sync": True},
)
assert captured["extension_guid"] == extension_guid
def test_code_search_internal_modules_request_omits_empty_extension_selectors(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
monkeypatch.setattr(adapter_server, "code_saved_state_common_form_module_search", lambda *_args, **_kwargs: None)
def search(payload: dict[str, Any]) -> dict[str, Any]:
captured.update(payload)
return {"status": "ok", "matches": [], "counts": {"matches": 0}}
monkeypatch.setattr(adapter_server, "search_modules", search)
result = adapter_server.code_search({"base_id": "upo_test", "query": "Найти", "state": "active"})
assert result["status"] == "not_found"
assert "extension" not in captured
assert "extension_guid" not in captured
def test_object_full_uses_fast_form_listing_by_default() -> None:
assert adapter_server.adapter_full_forms_method({}) == "metadata.object.forms"
assert adapter_server.adapter_full_forms_method({"include_form_details": False}) == "metadata.object.forms"
def test_object_full_uses_form_details_only_when_requested() -> None:
assert adapter_server.adapter_full_forms_method({"include_form_details": True}) == "metadata.object.form.details"
assert adapter_server.adapter_full_forms_method({"include_form_module_text": True}) == "metadata.object.form.details"
def test_common_form_kind_is_first_class_selector() -> None:
assert adapter_server.canonical_kind("Общая форма") == "CommonForm"
assert adapter_server.canonical_kind("ОбщиеФормы") == "CommonForm"
assert adapter_server.canonical_kind("CommonForm") == "CommonForm"
assert adapter_server.PUBLIC_KIND["CommonForm"] == "common_form"
assert adapter_server.RU_KIND["CommonForm"] == "ОбщаяФорма"
assert "form_details" in adapter_server.KIND_CAPABILITIES["CommonForm"]
def test_every_public_1c_kind_name_and_code_root_resolves_to_internal_kind() -> None:
for kind, kind_ru in adapter_server.RU_KIND.items():
assert adapter_server.known_canonical_kind(kind) == kind
assert adapter_server.known_canonical_kind(kind_ru) == kind
for code_root, kind in adapter_server.ONEC_CODE_ROOT_KIND.items():
assert adapter_server.known_canonical_kind(code_root) == kind
def test_runtime_method_registry_is_unique_and_covers_contract_metadata() -> None:
registry = adapter_server.adapter_method_registry()
diagnostics = adapter_server.adapter_method_registry_diagnostics()
response = adapter_server.call_method("help.methods", {})
assert diagnostics["status"] == "ok"
assert diagnostics["duplicates"] == []
assert diagnostics["input_schemas_without_method"] == []
assert diagnostics["selector_contracts_without_method"] == []
assert diagnostics["http_routes_without_method"] == []
assert diagnostics["counts"]["specialized_http_routes"] == 68
assert diagnostics["counts"]["methods_with_input_schema"] == 151
assert len(registry) == len(adapter_server.METHODS) == 151
assert all(row["input_schema"]["type"] == "object" for row in registry.values())
assert registry["code.search"]["http_routes"] == [{"verb": "POST", "path": "/code/search"}]
assert registry["modules.read"]["http_routes"] == [{"verb": "GET", "path": "/modules/read"}]
assert response["count"] == 151
assert response["registry"] == diagnostics
def test_http_handler_dispatches_specialized_routes_from_registry(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, dict[str, Any]]] = []
monkeypatch.setenv("ONEC_ADAPTER_SERVICE_TOKEN", "test-route-token")
def fake_call_method(method: str, payload: dict[str, Any] | None) -> dict[str, Any]:
calls.append((method, dict(payload or {})))
return {"status": "ok", "method": method}
monkeypatch.setattr(adapter_server, "call_method", fake_call_method)
server = adapter_server.ThreadingHTTPServer(("127.0.0.1", 0), adapter_server.Handler)
thread = adapter_server.threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
base_url = f"http://127.0.0.1:{server.server_address[1]}"
def request(path: str, body: dict[str, Any] | None = None) -> dict[str, Any]:
data = json.dumps(body).encode("utf-8") if body is not None else None
req = adapter_server.urllib.request.Request(
base_url + path,
data=data,
method="POST" if body is not None else "GET",
headers={
"Authorization": "Bearer test-route-token",
**({"Content-Type": "application/json"} if body is not None else {}),
},
)
with adapter_server.urllib.request.urlopen(req, timeout=5) as response:
return json.loads(response.read().decode("utf-8"))
try:
assert request("/methods?method=code.search")["method"] == "help.methods"
assert request("/code/search", {"base_id": "upo_test", "query": "Найти"})["method"] == "code.search"
assert request("/rpc", {"method": "metadata.kinds", "payload": {"base_id": "upo_test"}})["method"] == "metadata.kinds"
finally:
server.shutdown()
server.server_close()
thread.join(timeout=2)
assert calls == [
("help.methods", {"method": "code.search"}),
("code.search", {"base_id": "upo_test", "query": "Найти"}),
("metadata.kinds", {"base_id": "upo_test"}),
]
@pytest.mark.parametrize(
("ref", "kind", "name"),
[
("Справочники.Номенклатура", "Catalog", "Номенклатура"),
("СправочникСсылка.Номенклатура", "Catalog", "Номенклатура"),
("ДокументОбъект.Заказ", "Document", "Заказ"),
("РегистрыСведений.КурсыВалют", "InformationRegister", "КурсыВалют"),
("РегистрСведенийНаборЗаписей.КурсыВалют", "InformationRegister", "КурсыВалют"),
("Метаданные.HTTPСервисы.Интеграция", "HTTPService", "Интеграция"),
("cfg:ОбщиеМодули.Интеграция", "CommonModule", "Интеграция"),
],
)
def test_code_style_and_type_qualified_object_refs_resolve(ref: str, kind: str, name: str) -> None:
assert adapter_server.parse_object_query(None, ref) == (kind, name)
normalized = adapter_server.normalize_object_ref_payload({"ref": ref}, "test")
assert normalized["kind"] == kind
assert normalized["name"] == name
def test_nested_1c_path_keeps_object_identity_and_member_path() -> None:
parsed = adapter_server.parse_1c_object_path("Документы.Заказ.Формы.ФормаДокумента.Элементы.КнопкаЗаписать")
assert parsed["kind"] == "Document"
assert parsed["name"] == "Заказ"
assert parsed["member_path"] == ["Формы", "ФормаДокумента", "Элементы", "КнопкаЗаписать"]
assert parsed["metadata_ref"] == "Документ.Заказ"
assert parsed["code_ref"] == "Документы.Заказ"
def test_data_and_access_surfaces_share_code_style_object_resolution() -> None:
data_payload = adapter_server.data_schema_selector_payload(
{"base_id": "upo_test", "object_ref": "РегистрыСведений.КурсыВалют"}
)
normalized_data = adapter_server.normalize_object_selector_aliases(data_payload, "data.schema")
normalized_access = adapter_server.normalize_object_selector_aliases(
{"base_id": "upo_test", "ref": "Справочники.Номенклатура"},
"access.object.roles",
)
assert normalized_data["kind"] == "InformationRegister"
assert normalized_data["name"] == "КурсыВалют"
assert normalized_access["kind"] == "Catalog"
assert normalized_access["name"] == "Номенклатура"
assert "access.object.roles" in adapter_server.OBJECT_SELECTOR_ALIAS_METHODS
assert "access.object.subjects" in adapter_server.OBJECT_SELECTOR_ALIAS_METHODS
def test_data_methods_publish_name_first_selector_contracts() -> None:
methods = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}
assert adapter_server.DATA_OBJECT_SELECTOR_METHODS <= adapter_server.OBJECT_SELECTOR_ALIAS_METHODS
for method in adapter_server.DATA_OBJECT_SELECTOR_METHODS:
capabilities = methods[method]["selector_capabilities"]
properties = methods[method]["input_schema"]["properties"]
assert capabilities["requires_object_selector"] is True
assert capabilities["accepts_object_ref"] is True
assert capabilities["accepts_ordinal"] is False
assert properties["object_ref"]["type"] == "string"
assert properties["object_name"]["type"] == "string"
assert "Справочники.Номенклатура" in properties["object_ref"]["description"]
def test_data_selector_keeps_record_ref_separate_from_russian_object_ref() -> None:
record_ref = "A" * 32
normalized = adapter_server.normalize_data_object_selector_aliases(
{
"base_id": "upo_test",
"object_ref": "Справочники.Номенклатура",
"ref": record_ref,
},
"data.get",
)
assert normalized["kind"] == "Catalog"
assert normalized["name"] == "Номенклатура"
assert normalized["object_ref"] == "Справочники.Номенклатура"
assert normalized["ref"] == record_ref
assert "guid" not in normalized
def test_data_get_dispatch_normalizes_object_name_without_consuming_record_ref(
monkeypatch: pytest.MonkeyPatch,
) -> None:
record_ref = "B" * 32
captured: dict[str, Any] = {}
def fake_data_read(payload: dict[str, Any], *, count_only: bool = False, method: str | None = None) -> dict[str, Any]:
captured.update(payload)
return {"schema": "onec_data_result.v1", "status": "ok", "rows": [], "method": method}
monkeypatch.setattr(adapter_server, "data_read", fake_data_read)
result = adapter_server.call_method(
"data.get",
{
"base_id": "upo_test",
"object_ref": "Справочники.Номенклатура",
"record_ref": record_ref,
},
)
assert result["status"] == "ok"
assert captured["kind"] == "Catalog"
assert captured["name"] == "Номенклатура"
assert captured["record_ref"] == record_ref
def test_data_methods_reject_unsupported_ordinal_selector() -> None:
result = adapter_server.call_method(
"data.schema",
{"base_id": "upo_test", "kind": "Catalog", "ordinal": 1},
)
assert result["status"] == "invalid_argument"
assert result["argument"] == "ordinal"
def test_write_plan_path_accepts_code_roots_and_rejects_unknown_roots() -> None:
parsed = adapter_server.metadata_write_plan_path_parts("ОбщиеМодули.Интеграция.Отправить")
unknown = adapter_server.metadata_write_plan_path_parts("НеизвестныеОбъекты.Объект.Поле")
assert parsed["is_full_path"] is True
assert parsed["kind"] == "CommonModule"
assert parsed["name"] == "Интеграция"
assert parsed["path_kind"] == "module_routine"
assert parsed["canonical_path"] == "ОбщийМодуль.Интеграция.Отправить"
assert parsed["code_path"] == "ОбщиеМодули.Интеграция.Отправить"
assert unknown["is_full_path"] is False
assert unknown["reason"] == "unknown_object_kind"
def test_root_kind_routes_match_xml_exported_configuration() -> None:
assert adapter_server.ROOT_APPLICATION_CLASS_KIND["1c57eabe-7349-44b3-b1de-ebfeab67b47d"] == "CommandGroup"
assert adapter_server.ROOT_APPLICATION_CLASS_KIND["36a8e346-9aaa-4af9-bdbd-83be3c177977"] == "DocumentNumerator"
assert adapter_server.ROOT_COLLECTION_KIND["0fffc09c-8f4c-47cc-b41c-8d5c5a221d79"] == "HTTPService"
assert adapter_server.ROOT_COLLECTION_KIND["8657032e-7740-4e1d-a3ba-5dd6e8afb78f"] == "WebService"
assert adapter_server.ROOT_COLLECTION_KIND["cc9df798-7c94-4616-97d2-7aa0b7bc515e"] == "XDTOPackage"
assert adapter_server.ROOT_COLLECTION_KIND["5274d9fc-9c3a-4a71-8f5e-a0db8ab23de5"] == "ExternalDataSource"
assert adapter_server.ROOT_COLLECTION_KIND["bf3420b0-f6f9-41a0-b83a-fe9d4ab0b65d"] == "IntegrationService"
assert adapter_server.ROOT_COLLECTION_KIND["a7641777-7813-45c6-96ef-9d51587a6ac6"] == "Interface"
assert "DocumentNumerator" in adapter_server.TOP_LEVEL_METADATA_KINDS
assert "IntegrationService" in adapter_server.TOP_LEVEL_METADATA_KINDS
assert "Configuration" in adapter_server.TOP_LEVEL_METADATA_KINDS
assert adapter_server.canonical_kind("FilterCriterion") == "SelectionCriterion"
assert adapter_server.public_metadata_row({"kind": "DocumentNumerator", "name": "Нумератор", "guid": "x"})["ref"] == "DocumentNumerator.Нумератор"
assert adapter_server.metadata_cache_public_row({"kind": "IntegrationService", "name": "Обмен", "guid": "x"})["ref"] == "IntegrationService.Обмен"
def test_every_kind_specific_sql_decoder_is_advertised_by_capabilities() -> None:
for kind in adapter_server.SPECIAL_PROPERTY_KINDS:
capabilities = adapter_server.KIND_CAPABILITIES[kind]
assert "properties" in capabilities, kind
assert "special_details" in capabilities, kind
@pytest.mark.parametrize(
("capability", "category", "direct_kinds"),
[
("commands", "Command", {"CommandGroup"}),
("forms", "Form", set()),
("templates", "Template", {"CommonTemplate"}),
],
)
def test_advertised_related_capability_has_a_live_collection_rule(
capability: str,
category: str,
direct_kinds: set[str],
) -> None:
advertised = {kind for kind, capabilities in adapter_server.KIND_CAPABILITIES.items() if capability in capabilities}
implemented = {
kind
for kind, rules in adapter_server.RELATED_SECTION_RULES.items()
if any(rule.get("category") == category for rule in rules)
} | direct_kinds
assert advertised == implemented
def test_corrected_related_capabilities_cover_calculation_register_without_false_legacy_claims() -> None:
assert {"forms", "templates", "commands"} <= set(adapter_server.KIND_CAPABILITIES["CalculationRegister"])
assert "commands" in adapter_server.KIND_CAPABILITIES["CommandGroup"]
assert "commands" not in adapter_server.KIND_CAPABILITIES["Interface"]
assert "templates" not in adapter_server.KIND_CAPABILITIES["ChartOfAccounts"]
assert "templates" not in adapter_server.KIND_CAPABILITIES["ChartOfCalculationTypes"]
def test_related_selectors_follow_corrected_capabilities_and_keep_public_names() -> None:
calculation = adapter_server.object_related_selectors(
"upo_test",
{
"kind": "CalculationRegister",
"name": "Начисления",
"guid": "11111111-1111-1111-1111-111111111111",
"ref": "CalculationRegister.Начисления",
},
)
interface = adapter_server.object_related_selectors(
"upo_test",
{
"kind": "Interface",
"name": "Основной",
"guid": "22222222-2222-2222-2222-222222222222",
"ref": "Interface.Основной",
},
)
assert calculation["forms"]["ref"] == "CalculationRegister.Начисления"
assert calculation["templates"]["name"] == "Начисления"
assert calculation["commands"]["method"] == "metadata.object.commands"
command_group = adapter_server.object_related_selectors(
"upo_test",
{
"kind": "CommandGroup",
"name": "Сервис",
"guid": "33333333-3333-3333-3333-333333333333",
"ref": "CommandGroup.Сервис",
},
)
assert command_group["commands"]["ref"] == "CommandGroup.Сервис"
assert set(interface) == {"card", "full"}
def test_command_group_commands_follow_reverse_common_command_relation(monkeypatch: pytest.MonkeyPatch) -> None:
group_guid = "11111111-1111-1111-1111-111111111111"
first_guid = "22222222-2222-2222-2222-222222222222"
second_guid = "33333333-3333-3333-3333-333333333333"
def command_tree(command_group_guid: str) -> dict[str, Any]:
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
root = seq()
def put(path: tuple[int, ...], value: dict[str, Any]) -> None:
node = root
for index in path[:-1]:
items = node.setdefault("items", [])
while len(items) <= index:
items.append(seq())
node = items[index]
items = node.setdefault("items", [])
while len(items) <= path[-1]:
items.append(seq())
items[path[-1]] = value
put((1, 1, 2, 7, 1), atom(command_group_guid))
return root
payloads = {
first_guid: serialize_brace_tree(command_tree(group_guid)).encode("utf-8"),
second_guid: serialize_brace_tree(command_tree("44444444-4444-4444-4444-444444444444")).encode("utf-8"),
}
monkeypatch.setattr(
adapter_server,
"list_objects",
lambda *_args, **_kwargs: {
"status": "ok",
"objects": [
{"kind": "CommonCommand", "guid": first_guid, "name": "Первая", "synonym": "Первая команда", "ref": "CommonCommand.Первая"},
{"kind": "CommonCommand", "guid": second_guid, "name": "Вторая", "synonym": "Вторая команда", "ref": "CommonCommand.Вторая"},
],
},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *_args, **_kwargs: (payloads, {"database": "test"}, None),
)
result = adapter_server.metadata_command_group_commands(
base_id="upo_test",
object_card={"kind": "CommandGroup", "guid": group_guid, "name": "Сервис", "ref": "CommandGroup.Сервис"},
requested_command=None,
include_storage=False,
table="Config",
max_commands=200,
timeout_seconds=30,
)
assert result["status"] == "ok"
assert result["counts"]["scanned_common_commands"] == 2
assert result["counts"]["matched_common_commands"] == 1
assert result["commands"][0]["ref"] == "CommonCommand.Первая"
assert result["commands"][0]["module"]["read_selector"]["ref"] == "CommonCommand.Первая"
assert result["capabilities"]["reverse_relation"] == "CommonCommand.group"
assert all("storage" not in command for command in result["commands"])
def test_metadata_kinds_combines_sequence_dbnames_and_interface_root_discovery(monkeypatch: pytest.MonkeyPatch) -> None:
sequence_guid = "11111111-1111-1111-1111-111111111111"
interface_guid = "22222222-2222-2222-2222-222222222222"
class SequenceRecord:
guid = sequence_guid
storage_role = "Sequence"
monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *_args, **_kwargs: ([SequenceRecord()], None))
monkeypatch.setattr(
adapter_server,
"live_base_root_metadata_index",
lambda *_args, **_kwargs: (
[
{
"guid": interface_guid,
"kind": "Interface",
"kind_ru": "Интерфейс",
"public_kind": "interface",
"source": "base",
}
],
[],
),
)
result = adapter_server.get_kinds("upo_test")
assert result["status"] == "ok"
assert result["internal_kind_counts"]["Sequence"] == 1
assert result["internal_kind_counts"]["Interface"] == 1
assert {item["kind"]: item["count"] for item in result["kinds"]}["sequence"] == 1
assert {item["kind"]: item["count"] for item in result["kinds"]}["interface"] == 1
def test_metadata_cache_rebuild_merges_root_only_objects(monkeypatch: pytest.MonkeyPatch) -> None:
root_guid = "11111111-1111-1111-1111-111111111111"
stored: list[dict[str, Any]] = []
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda _base_id: ({"server": "sql", "database": "base"}, None))
monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *_args, **_kwargs: ([], None))
monkeypatch.setattr(adapter_server, "metadata_guid_index_migrate_legacy_types", lambda _config: 0)
monkeypatch.setattr(
adapter_server,
"live_base_root_metadata_index",
lambda *_args, **_kwargs: ([{"guid": root_guid, "kind": "SettingsStorage", "kind_ru": "ХранилищеНастроек", "public_kind": "settings_storage", "source": "base"}], []),
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *_args, **_kwargs: ({root_guid: b"payload"}, [], None),
)
monkeypatch.setattr(
adapter_server,
"config_identity_from_bytes",
lambda _data: {"guid": root_guid, "name": "Настройки", "synonyms": {"ru": "Настройки"}},
)
monkeypatch.setattr(adapter_server, "metadata_cache_upsert", lambda _config, row: stored.append(dict(row)))
monkeypatch.setattr(adapter_server, "generated_type_records_from_bytes", lambda *_args, **_kwargs: [])
monkeypatch.setattr(adapter_server, "live_config_file_name_page_after", lambda *_args, **_kwargs: [])
result = adapter_server.metadata_cache_rebuild_base("upo_test")
assert result["status"] == "ok"
assert result["counts"]["candidates"] == 1
assert result["counts"]["updated"] == 1
assert stored[0]["kind"] == "SettingsStorage"
assert stored[0]["name"] == "Настройки"
def test_style_item_sql_details_decodes_color_font_and_border() -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
identity = seq(atom(3), seq(atom(1), atom(0), atom("11111111-1111-1111-1111-111111111111")), atom("Test"))
color_tree = seq(atom(1), seq(atom(3), atom(0), seq(atom("#"), atom("9cd510c7-abfc-11d4-9434-004095e12fc7"), atom(2), seq(atom(3), atom(0), seq(atom(4625920)))), identity), atom(0))
color = adapter_server.style_item_sql_details(color_tree)
assert color["value_type"] == "Color"
assert color["value"]["value"] == "#009646"
font_payload = seq(atom(7), atom(0), atom(575), atom(100), atom(0), atom(0), atom(0), atom(700), atom(1), atom(0), atom(0), atom(0), atom(0), atom(0), atom(0), atom(0), atom("Arial"), atom(1), atom(100))
font_tree = seq(atom(1), seq(atom(3), atom(1), seq(atom("#"), atom("9cd510c8-abfc-11d4-9434-004095e12fc7"), atom(1), font_payload, atom(0)), identity), atom(0))
font = adapter_server.style_item_sql_details(font_tree)
assert font["value_type"] == "Font"
assert font["value"] == {"kind": "absolute", "face_name": "Arial", "height": 10.0, "bold": True, "italic": True, "underline": False, "strikeout": False, "scale": 100}
border_payload = seq(atom(3), atom(0), seq(atom(0)), atom(1), atom(1), atom(0), atom("48312c09-257f-4b29-b280-284dd89efc1e"))
border_tree = seq(atom(1), seq(atom(3), atom(2), seq(atom("#"), atom("4d10ca00-111a-4d43-9c96-92cd773716de"), atom(1), border_payload, atom(0)), identity), atom(0))
border = adapter_server.style_item_sql_details(border_tree)
assert border["value_type"] == "Border"
assert border["value"] == {"style": "Single", "width": 1}
def test_language_and_common_picture_sql_properties() -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
identity = seq(atom(3), seq(atom(1), atom(0), atom("11111111-1111-1111-1111-111111111111")), atom("Test"))
assert adapter_server.language_sql_details(seq(atom(1), seq(atom(0), identity, atom("ru")), atom(0))) == {"language_code": "ru"}
picture = adapter_server.common_picture_sql_details(seq(atom(1), seq(atom(4), identity, atom(1), atom(0)), atom(0)), None)
assert picture["availability_for_choice"] is True
assert picture["availability_for_appearance"] is False
assert picture["binary"]["status"] == "source_missing"
@pytest.mark.parametrize(
("content", "media_type"),
[
(b"\x89PNG\r\n\x1a\nrest", "image/png"),
(b"", "image/svg+xml"),
(b"\xef\xbb\xbf", "image/svg+xml"),
(b"PK\x03\x04picture-package", "application/zip"),
(b"\x00\x00\x01\x00\x01\x00icon", "image/x-icon"),
],
)
def test_binary_part_summary_recognizes_picture_formats(content: bytes, media_type: str) -> None:
encoded = base64.b64encode(content).decode("ascii")
stored = compress_payload(f'{{1,{{#base64:{encoded}}}}}'.encode("utf-8-sig"), "raw_deflate")
result = adapter_server.binary_part_summary(stored)
assert result["status"] == "ok"
assert result["media_type"] == media_type
assert result["bytes"] == len(content)
def test_xdto_package_xml_details_decodes_types_properties_and_enums() -> None:
xml = b'''
OpenClosed
'''
result = adapter_server.xdto_package_xml_details(compress_payload(xml, "raw_deflate"))
assert result["status"] == "ok"
assert result["namespace"] == "urn:test"
assert result["imports"] == ["urn:base"]
assert result["counts"] == {"types": 2, "object_types": 1, "value_types": 1, "properties": 3, "enumerations": 2, "inline_type_definitions": 1, "imports": 1}
assert result["types"][0]["enumerations"][0]["value"] == "Open"
assert result["types"][1]["properties"][1]["type_definition"]["properties"][0]["name"] == "Value"
def test_ws_reference_sql_details_decodes_wsdl_and_xsd_streams() -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
tree = seq(atom(1), seq(atom(2), seq(atom("https://example.test/service?wsdl"), atom(0)), atom("identity"), atom("11111111-1111-1111-1111-111111111111"), atom("22222222-2222-2222-2222-222222222222")), atom(0))
wsdl = b''''''
xsd = b''''''
payload = b"prefix" + stream_header(len(wsdl)) + wsdl + stream_header(len(xsd)) + xsd
result = adapter_server.ws_reference_sql_details(tree, compress_payload(payload, "raw_deflate"))
assert result["status"] == "ok"
assert result["location_url"] == "https://example.test/service?wsdl"
assert result["counts"] == {"streams": 2, "xml_streams": 2, "wsdl_definitions": 1, "schemas": 1, "operations": 1, "services": 1, "ports": 1}
assert result["definitions"][0]["port_types"][0]["operations"][0]["soap_action"] == "urn:test/Ping"
assert result["schemas"][0]["types"][0]["members"][0]["name"] == "Value"
def test_sql_only_decoders_cover_document_numerator_and_integration_channels() -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
identity = seq(atom(3), seq(atom(1), atom(0), atom("00000000-0000-0000-0000-000000000001")), atom("Test"), seq(atom(1), atom("ru"), atom("Тест")))
numerator_tree = seq(atom(1), seq(atom(3), identity, atom(1), atom(11), atom(1), atom(1), atom(1)), atom(0))
numerator = adapter_server.document_numerator_sql_details(numerator_tree, include_storage=False)
assert numerator["number_type"] == "String"
assert numerator["number_length"] == 11
assert numerator["number_allowed_length"] == "Variable"
assert numerator["number_periodicity"] == "Year"
assert numerator["check_unique"] is True
assert all("storage" not in item for item in numerator["properties"])
chart_header = [atom(35), identity]
while len(chart_header) <= 57:
chart_header.append(atom(0))
chart_header[24] = atom(1)
chart_header[25] = atom(5)
chart_header[26] = atom(1)
chart_header[27] = atom(0)
chart_header[30] = atom(100)
chart_header[32] = atom("11111111-1111-1111-1111-111111111111")
chart_header[33] = atom("22222222-2222-2222-2222-222222222222")
chart_header[34] = atom("00000000-0000-0000-0000-000000000000")
chart_header[48] = seq(atom(1), atom("ru"), atom("Вид расчета"))
chart_header[49] = seq(atom(0))
chart_header[50] = seq(atom(1), atom("ru"), atom("Виды расчета"))
chart_header[57] = atom(0)
chart = adapter_server.chart_of_calculation_types_sql_details(seq(atom(1), seq(*chart_header)), include_storage=False)
assert chart["use_standard_commands"] is True
assert chart["code_length"] == 5
assert chart["code_type"] == "String"
assert chart["code_allowed_length"] == "Variable"
assert chart["description_length"] == 100
assert chart["default_object_form"]["guid"] == "11111111-1111-1111-1111-111111111111"
assert chart["default_list_form"]["guid"] == "22222222-2222-2222-2222-222222222222"
assert chart["default_choice_form"] is None
assert chart["object_presentation"] == {"ru": "Вид расчета"}
assert chart["list_presentation"] == {"ru": "Виды расчета"}
assert chart["action_period_use"] is False
assert len(chart["properties"]) == 17
assert all("storage" not in item for item in chart["properties"])
register_header = [atom(21)]
while len(register_header) <= 31:
register_header.append(atom(0))
register_header[16] = atom(2)
register_header[17] = atom(0)
register_header[18] = atom(0)
register_header[19] = atom("00000000-0000-0000-0000-000000000000")
register_header[20] = atom("00000000-0000-0000-0000-000000000000")
register_header[22] = atom("33333333-3333-3333-3333-333333333333")
register_header[24] = atom(1)
register_header[25] = atom(0)
register_header[26] = atom(1)
register_header[27] = atom(0)
register_header[28] = seq(atom(1), atom("ru"), atom("Расчеты"))
register_header[30] = seq(atom(0))
register_header[31] = seq(atom(0))
register = adapter_server.calculation_register_sql_details(seq(atom(1), seq(*register_header)), include_storage=False)
assert register["periodicity"] == "Month"
assert register["action_period"] is False
assert register["base_period"] is False
assert register["chart_of_calculation_types"]["guid"] == "33333333-3333-3333-3333-333333333333"
assert register["use_standard_commands"] is True
assert register["include_help_in_contents"] is False
assert register["data_lock_control_mode"] == "Managed"
assert register["full_text_search"] == "DontUse"
assert register["list_presentation"] == {"ru": "Расчеты"}
assert len(register["properties"]) == 13
collections = {item["path"]: item for item in register["child_collections"]}
assert collections["3"]["role"] == "Attribute"
assert collections["4"]["role"] == "Recalculation"
assert collections["5"]["role"] == "Template"
assert collections["6"]["role"] == "Resource"
assert collections["7"]["role"] == "Form"
assert collections["8"]["role"] == "Command"
assert collections["9"]["role"] == "Dimension"
assert all(item["status"] == "classified" for item in collections.values())
assert all("possible_roles" not in item for item in collections.values())
assert collections["3"]["collection_ordinal"] == 1
assert collections["9"]["collection_ordinal"] == 7
semantic_tree = seq(atom(1), *(seq(atom(f"marker-{index}"), atom(0)) for index in range(1, 10)))
semantic = decode_config_semantic(semantic_tree, kind="CalculationRegister", include_generic=False)
assert [(section["path"], section["category"]) for section in semantic["sections"]] == [
("3", "Attribute"),
("4", "Recalculation"),
("6", "Resource"),
("9", "Dimension"),
]
channel_identity = seq(atom(3), seq(atom(1), atom(0), atom("00000000-0000-0000-0000-000000000002")), atom("input"), seq(atom(1), atom("ru"), atom("Вход")))
channel_record = seq(atom(1), channel_identity, atom("type-id"), atom("value-id"), atom("external.input"), atom("ProcessInput"), atom(1), atom(0))
integration_tree = seq(atom(1), seq(), atom(1), seq(atom("class-id"), atom(1), seq(channel_record, atom(0))))
integration = adapter_server.integration_service_sql_details(integration_tree, include_storage=False)
assert integration["counts"] == {"channels": 1, "declared_channels": 1}
assert integration["channels"][0]["name"] == "input"
assert integration["channels"][0]["synonyms"] == {"ru": "Вход"}
assert integration["channels"][0]["message_direction"] == "Receive"
assert integration["channels"][0]["transactioned"] is False
assert "storage" not in integration["channels"][0]
configuration_tree = seq()
def put(path: tuple[int, ...], value: dict[str, Any]) -> None:
node = configuration_tree
for index in path[:-1]:
items = node.setdefault("items", [])
while len(items) <= index:
items.append(seq())
node = items[index]
items = node.setdefault("items", [])
while len(items) <= path[-1]:
items.append(seq())
items[path[-1]] = value
put((3, 1, 1, 1, 1, 2), atom("TestConfiguration"))
put((3, 1, 1, 1, 1, 3), seq(atom(1), atom("ru"), atom("Тестовая конфигурация")))
put((3, 1, 1, 4), seq(atom(1), atom("ru"), atom("Подробно")))
put((3, 1, 1, 5), seq(atom(1), atom("ru"), atom("Кратко")))
put((3, 1, 1, 6), seq(atom(1), atom("ru"), atom("Copyright")))
put((3, 1, 1, 7), seq(atom(1), atom("ru"), atom("https://vendor.example")))
put((3, 1, 1, 8), seq(atom(1), atom("ru"), atom("https://configuration.example")))
put((3, 1, 1, 14), atom("Vendor"))
put((3, 1, 1, 15), atom("1.2.3"))
put((3, 1, 1, 16), atom("https://updates.example"))
configuration = adapter_server.configuration_sql_details(configuration_tree, include_storage=False)
assert configuration["name"] == "TestConfiguration"
assert configuration["synonyms"] == {"ru": "Тестовая конфигурация"}
assert configuration["vendor"] == "Vendor"
assert configuration["version"] == "1.2.3"
assert configuration["counts"] == {"decoded_properties": 10, "declared_properties": 10}
assert all("storage" not in item for item in configuration["properties"])
schedule_tree = seq(
atom("00010101000000"), atom("00010101000000"), atom("00010101000001"), atom("00010101000000"), atom("00010101000000"),
atom(0), atom(0), atom(0), atom(7), atom(1), atom(2), atom(3), atom(4), atom(5), atom(6), atom(7),
atom(0), atom(0), atom(12), atom(1), atom(2), atom(3), atom(4), atom(5), atom(6), atom(7), atom(8), atom(9), atom(10), atom(11), atom(12),
atom(1), atom(1), atom(0),
)
schedule = adapter_server.scheduled_job_sql_schedule(schedule_tree, include_storage=False)
assert schedule["status"] == "ok"
assert schedule["begin_date"] == "0001-01-01"
assert schedule["begin_time"] == "00:00:01"
assert schedule["week_days"] == [1, 2, 3, 4, 5, 6, 7]
assert schedule["months"] == list(range(1, 13))
assert schedule["weeks_period"] == 1
assert schedule["days_repeat_period"] == 1
assert "storage" not in schedule
command_group_tree = seq(
atom(1),
seq(
atom(3),
seq(atom(4), atom(1), seq(atom(0), atom("00000000-0000-0000-0000-000000000003")), atom(""), atom(-1), atom(-1), atom(0), atom(0), atom("")),
atom(8),
atom(1),
seq(atom(1), atom("ru"), atom("Подсказка")),
),
atom(0),
)
command_group = adapter_server.command_group_sql_details(command_group_tree, include_storage=False)
assert command_group["category"] == "FormCommandBar"
assert command_group["representation"] == "Picture"
assert command_group["tool_tip"] == {"ru": "Подсказка"}
assert command_group["picture"]["guid"] == "00000000-0000-0000-0000-000000000003"
assert command_group["load_transparent"] is False
assert "storage" not in command_group
def test_scheduled_job_schedule_write_edits_use_named_fields_and_preserve_collection_shape() -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
tree = seq(
atom("00010101000000"), atom("00010101000000"), atom("00010101000001"), atom("00010101000000"), atom("00010101000000"),
atom(0), atom(0), atom(0), atom(2), atom(1), atom(5),
atom(0), atom(0), atom(2), atom(1), atom(12),
atom(1), atom(1),
)
result = adapter_server.scheduled_job_schedule_write_edits(
tree,
{
"begin_time": "08:30:00",
"completion_interval": 15,
"week_days": [2, 6],
"months": [2, 11],
},
)
assert result["status"] == "ok"
assert result["requested"] == {
"begin_time": "08:30:00",
"completion_interval": 15,
"week_days": [2, 6],
"months": [2, 11],
}
edits = {edit["path"]: edit for edit in result["edits"]}
assert edits["2"]["value"] == "00010101083000"
assert edits["5"]["value"] == "15"
assert [edits[path]["value"] for path in ("9", "10")] == ["2", "6"]
assert [edits[path]["value"] for path in ("14", "15")] == ["2", "11"]
assert all("expected_old" in edit for edit in result["edits"])
def test_scheduled_job_schedule_write_rebuilds_tree_for_collection_resize() -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
tree = {
"type": "list",
"items": [
atom("00010101000000"), atom("00010101000000"), atom("00010101000000"), atom("00010101000000"), atom("00010101000000"),
atom(0), atom(0), atom(0), atom(2), atom(1), atom(5),
atom(0), atom(0), atom(1), atom(1),
atom(1), atom(1),
],
}
result = adapter_server.scheduled_job_schedule_write_edits(
tree,
{"begin_time": "09:30:00", "week_days": [1, 2, 3], "months": [1, 6, 12]},
)
assert result["status"] == "ok"
assert result["counts"]["resized_collections"] == 2
assert len(result["edits"]) == 1
edit = result["edits"][0]
assert edit["resized_collections"] == ["months", "week_days"]
assert edit["old_counts"] == {"week_days": 2, "months": 1}
assert edit["new_counts"] == {"week_days": 3, "months": 3}
rebuilt = adapter_server.scheduled_job_sql_schedule(edit["replace_root"], include_storage=False)
assert rebuilt["begin_time"] == "09:30:00"
assert rebuilt["week_days"] == [1, 2, 3]
assert rebuilt["months"] == [1, 6, 12]
def test_metadata_scheduled_job_schedule_write_resolves_public_1c_ref_to_configsave(monkeypatch: pytest.MonkeyPatch) -> None:
guid = "11111111-1111-1111-1111-111111111111"
atom = lambda value: {"type": "atom", "value": str(value)}
tree = {
"type": "list",
"items": [
atom("00010101000000"), atom("00010101000000"), atom("00010101000000"), atom("00010101000000"), atom("00010101000000"),
atom(0), atom(0), atom(0), atom(1), atom(1),
atom(0), atom(0), atom(1), atom(1),
atom(1), atom(1),
],
}
captured: dict[str, Any] = {}
monkeypatch.setattr(
adapter_server,
"get_object",
lambda kind, selector, **_kwargs: {
"status": "ok",
"object": {"kind": kind, "name": "ОбменДанными", "guid": guid, "ref": "ScheduledJob.ОбменДанными"},
},
)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *_args, **_kwargs: (b"schedule", {"database": "test"}, None),
)
monkeypatch.setattr(adapter_server, "parse_config_tree_from_bytes", lambda _data: tree)
def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
captured.update(payload)
return {
"schema": "onec_change_proposal.v1",
"status": "accepted_for_review",
"source": payload["source"],
"original": {"sha1": "old"},
"encoded": {"sha1": "new"},
}
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)
read_only = adapter_server.metadata_scheduled_job_schedule_write(
{
"base_id": "upo_test",
"target": {"kind": "schedule", "ref": "РегламентныеЗадания.ОбменДанными"},
"schedule": {"begin_time": "09:15:00"},
}
)
assert read_only["status"] == "blocked"
assert read_only["next_call"]["method"] == "metadata.write"
assert read_only["next_call"]["params"]["target"]["ref"] == "ScheduledJob.ОбменДанными"
assert read_only["next_call"]["params"]["allow_saved_state_write"] is True
assert "payload" not in read_only["next_call"]
result = adapter_server.metadata_scheduled_job_schedule_write(
{
"base_id": "upo_test",
"target": {"kind": "schedule", "ref": "РегламентныеЗадания.ОбменДанными"},
"schedule": {"begin_time": "09:15:00"},
"allow_saved_state_write": True,
"execution_mode": "plan",
}
)
assert result["status"] == "planned"
assert result["object"]["ref"] == "ScheduledJob.ОбменДанными"
assert captured["source"]["table"] == "ConfigSave"
assert captured["source"]["file_name"] == f"{guid}.0"
assert captured["edits"] == [{"path": "2", "value": "00010101091500", "node_type": "atom", "expected_old": "00010101000000"}]
assert captured["include_payload"] is False
def test_metadata_write_routes_schedule_target_without_changing_existing_form_module_routes(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
monkeypatch.setattr(adapter_server, "normalize_repository_write_context", lambda payload, _method: (dict(payload), None))
monkeypatch.setattr(adapter_server, "resolve_write_gate_context", lambda payload: payload)
monkeypatch.setattr(adapter_server, "repository_apply_gate", lambda *_args, **_kwargs: None)
monkeypatch.setattr(adapter_server, "metadata_write_save_first_payload", lambda payload, _mode: payload)
monkeypatch.setattr(
adapter_server,
"metadata_scheduled_job_schedule_write",
lambda payload: captured.update(payload) or {"schema": "onec_scheduled_job_schedule_write.v1", "status": "planned"},
)
result = adapter_server.call_method(
"metadata.write",
{
"base_id": "upo_test",
"target": {"kind": "schedule", "ref": "РегламентныеЗадания.ОбменДанными"},
"schedule": {"begin_time": "09:15:00"},
"execution_mode": "plan",
}
)
assert result["status"] == "planned"
assert result["routed_method"] == "metadata.write:scheduled_job_schedule"
assert result["target_kind"] == "schedule"
assert captured["target"]["ref"] == "РегламентныеЗадания.ОбменДанными"
def test_metadata_write_preflight_routes_named_scheduled_job_read_only(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(adapter_server, "normalize_repository_write_context", lambda payload, _method: (dict(payload), None))
monkeypatch.setattr(adapter_server, "resolve_write_gate_context", lambda payload: payload)
monkeypatch.setattr(
adapter_server,
"metadata_scheduled_job_schedule_write",
lambda _payload: {
"schema": "onec_scheduled_job_schedule_write.v1",
"status": "planned",
"object": {
"kind": "ScheduledJob",
"name": "ОбменДанными",
"guid": "11111111-1111-1111-1111-111111111111",
"ref": "ScheduledJob.ОбменДанными",
},
"source": {"kind": "live_metadata"},
"proposal": {"original": {"sha1": "abc"}},
},
)
monkeypatch.setattr(adapter_server.repository_control, "write_gate", lambda _payload: {"status": "ok", "allowed": True})
monkeypatch.setattr(adapter_server, "live_sql_support_gate", lambda _payload: {"status": "ok", "allowed": True})
result = adapter_server.metadata_write_preflight(
{
"base_id": "upo_test",
"target": {"kind": "schedule", "ref": "РегламентныеЗадания.ОбменДанными"},
"schedule": {"begin_time": "09:15:00"},
}
)
assert result["status"] == "ready"
assert result["allowed"] is True
assert result["target"]["ref"] == "ScheduledJob.ОбменДанными"
assert result["route"]["writer"] == "metadata.write:scheduled_job_schedule"
assert result["guards"]["expected_sha1"] == "abc"
assert result["diagnostics"]["read_only"] is True
def test_service_module_roles_and_full_stream_selection(monkeypatch: pytest.MonkeyPatch) -> None:
guid = "10000000-0000-0000-0000-000000000001"
monkeypatch.setattr(
adapter_server,
"get_object",
lambda *args, **kwargs: {
"status": "ok",
"object": {"guid": guid, "kind": "HTTPService", "name": "Api"},
},
)
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(
adapter_server,
"metadata_object_parts",
lambda payload: {
"status": "ok",
"source": {"kind": "live_metadata"},
"object": {"guid": guid, "kind": "HTTPService", "name": "Api"},
"parts": [
{
"table": "Config",
"part_id": f"{guid}.0",
"suffix": ".0",
"classification": {
"role": "bsl_module_payload",
"stream_blocks": [
{"has_bsl_marker": True, "bytes": 512, "sha1": "short"},
{"has_bsl_marker": True, "bytes": 4096, "sha1": "full"},
],
},
}
],
"counts": {"parts": 1},
},
)
result = adapter_server.metadata_object_modules(
{"base_id": "upo_test", "kind": "HTTPService", "name": "Api", "refresh_cache": True}
)
assert result["status"] == "ok"
assert result["counts"]["modules"] == 1
assert result["modules"][0]["kind"] == "http_service_module"
assert result["modules"][0]["name"] == "Модуль HTTP-сервиса"
assert result["modules"][0]["qualified_name"] == "Api.Модуль HTTP-сервиса"
assert adapter_server.public_module_role(owner_kind="WebService")["kind"] == "web_service_module"
assert adapter_server.public_module_role(owner_kind="IntegrationService")["kind"] == "integration_service_module"
def test_common_module_selects_only_largest_canonical_bsl_stream(monkeypatch: pytest.MonkeyPatch) -> None:
guid = "10000000-0000-0000-0000-000000000009"
monkeypatch.setattr(
adapter_server,
"get_object",
lambda *args, **kwargs: {
"status": "ok",
"object": {"guid": guid, "kind": "CommonModule", "name": "ОбщийКод"},
},
)
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(
adapter_server,
"metadata_object_parts",
lambda payload: {
"status": "ok",
"source": {"kind": "live_metadata"},
"object": {"guid": guid, "kind": "CommonModule", "name": "ОбщийКод"},
"parts": [
{
"table": "Config",
"part_id": f"{guid}.0",
"suffix": ".0",
"classification": {
"role": "bsl_module_payload",
"stream_blocks": [
{"has_bsl_marker": True, "bytes": 1024, "sha1": "fragment"},
{"has_bsl_marker": True, "bytes": 8192, "sha1": "canonical"},
],
},
}
],
"counts": {"parts": 1},
},
)
result = adapter_server.metadata_object_modules(
{
"base_id": "upo_test",
"ref": "CommonModule.ОбщийКод",
"refresh_cache": True,
"include_storage": True,
}
)
assert result["status"] == "ok"
assert result["counts"]["modules"] == 1
assert result["modules"][0]["kind"] == "common_module"
assert result["modules"][0]["name"] == "ОбщийКод"
assert result["modules"][0]["qualified_name"] == "ОбщийКод"
assert result["modules"][0]["display_name"] == "ОбщийКод"
assert result["modules"][0]["storage_name"] == f"{guid}.0#stream:1"
assert result["modules"][0]["sha1"] == "canonical"
def test_configuration_parts_use_identity_guid_sql_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
root_guid = "10000000-0000-0000-0000-000000000001"
identity_guid = "10000000-0000-0000-0000-000000000002"
seen: dict[str, Any] = {}
monkeypatch.setattr(
adapter_server,
"resolve_object_guid",
lambda *args, **kwargs: (
root_guid,
"Configuration",
{
"guid": root_guid,
"kind": "Configuration",
"name": "Тестовая",
"identity": {"guid": identity_guid, "name": "Тестовая"},
},
None,
),
)
def fake_files(payload: dict[str, Any]) -> dict[str, Any]:
seen["prefix"] = payload["prefix"]
return {"status": "ok", "files": [{"FileName": f"{identity_guid}.6"}]}
monkeypatch.setattr(adapter_server, "storage_files_list", fake_files)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: (
{root_guid: b"root", f"{identity_guid}.6": b"module"},
{"database": "upo_test"},
None,
),
)
import parser.cas_payload as cas_payload
monkeypatch.setattr(cas_payload, "classify_payload", lambda data, **kwargs: {"role": "metadata_payload", "stream_blocks": []})
result = adapter_server.metadata_object_parts(
{"base_id": "upo_test", "kind": "Configuration", "guid": root_guid, "include_storage": True}
)
assert seen["prefix"] == identity_guid
assert [part["part_id"] for part in result["parts"]] == [root_guid, f"{identity_guid}.6"]
assert result["parts"][1]["suffix"] == ".6"
def test_configuration_modules_include_empty_external_connection_module(monkeypatch: pytest.MonkeyPatch) -> None:
root_guid = "10000000-0000-0000-0000-000000000001"
identity_guid = "10000000-0000-0000-0000-000000000002"
monkeypatch.setattr(
adapter_server,
"get_object",
lambda *args, **kwargs: {
"status": "ok",
"object": {
"guid": root_guid,
"kind": "Configuration",
"name": "Тестовая",
"identity": {"guid": identity_guid, "name": "Тестовая"},
},
},
)
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
def part(suffix: str, *, marker: bool) -> dict[str, Any]:
streams = [{"has_bsl_marker": False, "bytes": 1} for _ in range(5)]
streams[4] = {"has_bsl_marker": marker, "bytes": 100 if marker else 0, "sha1": suffix}
return {
"table": "Config",
"part_id": f"{identity_guid}.{suffix}",
"suffix": f".{suffix}",
"classification": {"role": "bsl_module_payload" if marker else "stream_container", "stream_blocks": streams},
}
monkeypatch.setattr(
adapter_server,
"metadata_object_parts",
lambda payload: {
"status": "ok",
"source": {"kind": "live_metadata"},
"object": {"guid": root_guid, "kind": "Configuration", "name": "Тестовая"},
"parts": [part("0", marker=True), part("5", marker=False), part("6", marker=True), part("7", marker=True)],
"counts": {"parts": 4},
},
)
result = adapter_server.metadata_object_modules(
{"base_id": "upo_test", "kind": "Configuration", "name": "Тестовая", "refresh_cache": True}
)
assert [module["kind"] for module in result["modules"]] == [
"ordinary_application_module",
"external_connection_module",
"managed_application_module",
"session_module",
]
assert all(module["module_ordinal"] >= 1 for module in result["modules"])
def test_metadata_object_properties_wraps_kind_specific_sql_decoder(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_object_special_details",
lambda payload: {
"schema": "onec_object_special_details.v1",
"status": "ok",
"base_id": payload["base_id"],
"source": {"kind": "live_metadata"},
"object": {"kind": "Configuration", "name": "Основная"},
"details": {"vendor": "Test"},
"counts": {"properties": 1},
},
)
result = adapter_server.metadata_object_properties({"base_id": "upo_test", "ref": "Configuration.Основная"})
assert result["schema"] == "onec_metadata_object_properties.v1"
assert result["method"] == "metadata.object.properties"
assert result["decoder"] == "kind_specific_sql"
assert result["properties"] == {"vendor": "Test"}
assert "details" not in result
def test_public_module_method_handler_returns_ready_routine_selector() -> None:
handler = adapter_server.public_module_method_handler(
"upo_test",
{
"guid": "00000000-0000-0000-0000-000000000001",
"kind": "CommonModule",
"name": "РегламентныеОперации",
"ref": "CommonModule.РегламентныеОперации",
"status": "ok",
},
"ВыполнитьЗадание",
)
assert handler["ref"] == "CommonModule.РегламентныеОперации.ВыполнитьЗадание"
assert handler["read_selector"] == {
"method": "modules.read",
"base_id": "upo_test",
"ref": "CommonModule.РегламентныеОперации",
"routine_name": "ВыполнитьЗадание",
"state": "working",
}
assert "read_selector" not in adapter_server.public_module_method_handler("upo_test", None, "ВыполнитьЗадание")
def test_public_pattern_value_type_preserves_union_references(monkeypatch: pytest.MonkeyPatch) -> None:
first_guid = "00000000-0000-0000-0000-000000000101"
second_guid = "00000000-0000-0000-0000-000000000102"
type_node = {
"type": "list",
"items": [
{"type": "string", "value": "Pattern"},
{"type": "list", "items": [{"type": "string", "value": "#"}, {"type": "atom", "value": first_guid}]},
{"type": "list", "items": [{"type": "string", "value": "#"}, {"type": "atom", "value": second_guid}]},
],
}
monkeypatch.setattr(
adapter_server,
"resolve_type_guids",
lambda *args, **kwargs: {
first_guid: {"status": "ok", "kind": "Catalog", "name": "Пользователи", "generated_category": "Ref"},
second_guid: {"status": "ok", "kind": "Catalog", "name": "ВнешниеПользователи", "generated_category": "Ref"},
},
)
value_type = adapter_server.public_pattern_value_type("upo_test", type_node)
assert value_type["kind"] == "union"
assert value_type["count"] == 2
assert [item["presentation"] for item in value_type["types"]] == [
"СправочникСсылка.Пользователи",
"СправочникСсылка.ВнешниеПользователи",
]
def test_public_pattern_value_type_decodes_external_binary_data(monkeypatch: pytest.MonkeyPatch) -> None:
type_node = {
"type": "list",
"items": [
{"type": "string", "value": "Pattern"},
{"type": "list", "items": [{"type": "string", "value": "R"}]},
],
}
monkeypatch.setattr(adapter_server, "resolve_type_guids", lambda *args, **kwargs: {})
value_type = adapter_server.public_pattern_value_type("upo_test", type_node)
assert value_type == {"kind": "binary", "presentation": "ДвоичныеДанные"}
def test_builtin_session_parameter_type_overrides_stale_unresolved_cache(monkeypatch: pytest.MonkeyPatch) -> None:
type_guid = "220455ea-6c85-4513-996f-bbe79ed07774"
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "test"}, None))
monkeypatch.setattr(
adapter_server,
"metadata_guid_index_lookup_types",
lambda config, guids: {type_guid: {"guid": type_guid, "status": "generated_type_unresolved"}},
)
monkeypatch.setattr(adapter_server, "metadata_type_cache_lookup_many", lambda config, guids: {})
monkeypatch.setattr(adapter_server, "metadata_type_cache_upsert", lambda *args, **kwargs: None)
resolved = adapter_server.resolve_type_guids("upo_test", {type_guid})
assert resolved[type_guid]["status"] == "ok"
assert resolved[type_guid]["presentation"] == "ФиксированноеСоответствие"
assert resolved[type_guid]["bsl_type"] == "FixedMap"
def test_settings_storage_suffix_zero_is_public_manager_module() -> None:
role = adapter_server.public_module_role(owner_kind="SettingsStorage", suffix="8")
assert role == {"kind": "manager_module", "name": "Модуль менеджера", "suffix": "8"}
def test_standard_common_command_groups_have_public_names() -> None:
assert adapter_server.STANDARD_COMMAND_GROUP_GUIDS["77ea1b8f-dd79-4717-9dba-5628e7f348cf"] == "NavigationPanelOrdinary"
assert adapter_server.STANDARD_COMMAND_GROUP_GUIDS["aabb34e1-98c1-4bd0-bf7f-243f95437b44"] == "ActionsPanelTools"
def test_sql_only_decoders_cover_event_subscription_and_services(monkeypatch: pytest.MonkeyPatch) -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
string = lambda value: {"type": "string", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
def identity(guid: str, name: str, synonym: str) -> dict[str, Any]:
return seq(atom(3), seq(atom(1), atom(0), atom(guid)), string(name), seq(atom(1), string("ru"), string(synonym)), string(""), atom(0), atom(0), atom("00000000-0000-0000-0000-000000000000"), atom(0))
monkeypatch.setattr(
adapter_server,
"public_metadata_guid_reference",
lambda base_id, guid: {"guid": guid, "kind": "Document", "name": "Заказ", "ref": "Document.Заказ", "status": "ok"},
)
subscription_tree = seq(
atom(1),
seq(
atom(1),
identity("00000000-0000-0000-0000-000000000001", "Подписка", "Подписка"),
seq(atom("Pattern"), seq(atom("#"), atom("00000000-0000-0000-0000-000000000002"))),
atom("BeforeWrite_ПередЗаписью"),
atom("00000000-0000-0000-0000-000000000003"),
string("Обработать"),
),
atom(0),
)
subscription = adapter_server.event_subscription_sql_details("upo_test", subscription_tree)
assert subscription["event"] == "BeforeWrite"
assert subscription["event_code"] == "BeforeWrite_ПередЗаписью"
assert subscription["sources"][0]["ref"] == "Document.Заказ"
assert subscription["handler"]["method"] == "Обработать"
assert subscription["handler"]["read_selector"] == {
"method": "modules.read",
"base_id": "upo_test",
"ref": "Document.Заказ",
"routine_name": "Обработать",
"state": "working",
}
operation_identity = identity("00000000-0000-0000-0000-000000000004", "execute", "execute")
parameter_identity = identity("00000000-0000-0000-0000-000000000005", "request", "request")
parameter = seq(atom(0), parameter_identity, seq(atom(0), string("urn:test"), string("Request")), atom(0), atom(0))
operation = seq(atom(1), operation_identity, seq(atom(0), string("urn:test"), string("Response")), atom(0), atom(0), string("Execute"), atom(1))
web_tree = seq(
atom(1),
seq(
atom(4), string("urn:test"), identity("00000000-0000-0000-0000-000000000006", "Service", "Service"),
seq(atom(0), atom(1), seq(atom("#"), atom("00000000-0000-0000-0000-000000000007"), seq(atom(1), atom("00000000-0000-0000-0000-000000000008")))),
string("service.1cws"), seq(atom(0)), atom(0), atom(20),
),
atom(1),
seq(atom("collection"), atom(1), seq(operation, atom(1), seq(atom("parameters"), atom(1), seq(parameter, atom(0))))),
)
web = adapter_server.web_service_sql_details("upo_test", web_tree)
assert web["namespace"] == "urn:test"
assert web["descriptor_file_name"] == "service.1cws"
assert web["reuse_sessions"] == "DontUse"
assert web["operations"][0]["procedure_name"] == "Execute"
assert web["operations"][0]["data_lock_control_mode"] == "Managed"
assert web["operations"][0]["parameters"][0]["transfer_direction"] == "In"
method_identity = identity("00000000-0000-0000-0000-000000000009", "POST", "POST")
template_identity = identity("00000000-0000-0000-0000-000000000010", "Notify", "Notify")
method = seq(atom(0), string("NotifyPOST"), atom(11), method_identity)
template = seq(atom(0), string("/notify"), template_identity)
http_tree = seq(
atom(1),
seq(atom(2), string("api"), identity("00000000-0000-0000-0000-000000000011", "Http", "Http"), atom(2), atom(20)),
atom(1),
seq(atom("collection"), atom(1), seq(template, atom(1), seq(atom("methods"), atom(1), seq(method)))),
)
http = adapter_server.http_service_sql_details(http_tree)
assert http["root_url"] == "api"
assert http["reuse_sessions"] == "AutoUse"
assert http["url_templates"][0]["template"] == "/notify"
assert http["url_templates"][0]["methods"][0]["http_method"] == "POST"
assert http["url_templates"][0]["methods"][0]["handler"] == "NotifyPOST"
def test_external_data_source_sql_decoder_reads_tables_fields_types_and_keys(monkeypatch: pytest.MonkeyPatch) -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
string = lambda value: {"type": "string", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
source_guid = "10000000-0000-0000-0000-000000000001"
table_guid = "10000000-0000-0000-0000-000000000002"
field_guid = "10000000-0000-0000-0000-000000000003"
def identity(guid: str, name: str, synonym: str) -> dict[str, Any]:
return seq(atom(1), seq(atom(0), atom(0), atom(guid)), string(name), seq(atom(1), string("ru"), string(synonym)), string(""), atom(0), atom(0))
source_tree = seq(
atom(1),
seq(atom(2), seq(atom(0), identity(source_guid, "Источник", "Источник")), atom(0), atom(0), atom(0), atom(0), atom(0), atom(0), atom(0), atom(0)),
atom(3),
seq(atom("cubes"), atom(0)),
seq(atom("functions"), atom(0)),
seq(atom("tables"), atom(1), atom(table_guid)),
)
header = [atom(0) for _ in range(19)]
header[0] = atom(11)
header[1] = seq(atom(0), identity(table_guid, "Продажи", "Продажи"))
header[16] = atom(0)
header[17] = string("dbo.SALES")
header[18] = seq(atom(0), atom(1), seq(string("#"), atom("157fa490-4ce9-11d4-9415-008048da11f9"), seq(atom(1), atom(field_guid))))
definition = seq(atom(2), identity(field_guid, "Код", "Код"), seq(string("Pattern"), seq(string("N"), atom(10), atom(0), atom(0))))
field_properties = seq(atom(27), definition)
field = seq(seq(atom(1), field_properties, string("CODE"), atom(1), atom(0)), atom(0))
table_tree = seq(
atom(1),
seq(*header),
atom(4),
seq(atom(0), atom(0)),
seq(atom(0), atom(0)),
seq(atom(0), atom(0)),
seq(atom("fields"), atom(1), field),
)
monkeypatch.setattr(adapter_server, "read_storage_files_bytes", lambda *args, **kwargs: ({table_guid: b"table"}, {}, None))
monkeypatch.setattr(adapter_server, "parse_config_tree_from_bytes", lambda data: table_tree)
monkeypatch.setattr(adapter_server, "resolve_type_guids", lambda *args, **kwargs: {})
decoded = adapter_server.external_data_source_sql_details("upo_test", source_tree)
assert decoded["counts"] == {
"tables": 1,
"declared_tables": 1,
"fields": 1,
"typed_fields": 1,
"cubes": 0,
"declared_cubes": 0,
"functions": 0,
"declared_functions": 0,
"missing_child_payloads": 0,
}
table = decoded["tables"][0]
assert table["ref"] == "ExternalDataSource.Источник.Table.Продажи"
assert table["name_in_data_source"] == "dbo.SALES"
assert table["key_fields"][0]["identity"]["name"] == "Код"
assert table["fields"][0]["value_type"]["presentation"] == "Число(10, 0)"
assert table["fields"][0]["ref"] == "ExternalDataSource.Источник.Table.Продажи.Field.Код"
def test_templates_read_resolves_top_level_common_template_by_public_ref(monkeypatch: pytest.MonkeyPatch) -> None:
guid = "10000000-0000-0000-0000-000000000001"
monkeypatch.setattr(
adapter_server,
"resolve_object_guid",
lambda *args, **kwargs: (guid, "CommonTemplate", {"guid": guid, "kind": "CommonTemplate", "name": "Печать"}, None),
)
monkeypatch.setattr(
adapter_server,
"read_template_by_guid",
lambda payload, analyze=False: {
"schema": "onec_templates_analyze.v1" if analyze else "onec_templates_read.v1",
"status": "ok",
"object": {"guid": payload["guid"], "kind": "Template"},
"templates": [{"guid": payload["guid"], "name": "Печать"}],
"counts": {"templates": 1, "parts": 2},
},
)
result = adapter_server.templates_read({"base_id": "upo_test", "ref": "CommonTemplate.Печать", "view": "summary"})
assert result["status"] == "ok"
assert result["counts"] == {"templates": 1, "parts": 2}
assert result["templates"][0]["name"] == "Печать"
def test_defined_type_selection_criterion_and_enum_sql_decoders(monkeypatch: pytest.MonkeyPatch) -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
string = lambda value: {"type": "string", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
def identity(guid: str, name: str, synonym: str, comment: str = "") -> dict[str, Any]:
return seq(atom(3), seq(atom(1), atom(0), atom(guid)), string(name), seq(atom(1), string("ru"), string(synonym)), string(comment), atom(0), atom(0), atom("00000000-0000-0000-0000-000000000000"), atom(0))
type_guid = "10000000-0000-0000-0000-000000000010"
content_guid = "10000000-0000-0000-0000-000000000020"
monkeypatch.setattr(
adapter_server,
"public_pattern_value_type",
lambda *args, **kwargs: {
"kind": "union",
"presentation": "СправочникСсылка.А | ДокументСсылка.Б",
"types": [
{"kind": "reference", "presentation": "СправочникСсылка.А"},
{"kind": "reference", "presentation": "ДокументСсылка.Б"},
],
"count": 2,
},
)
monkeypatch.setattr(
adapter_server,
"public_metadata_guid_references",
lambda *args, **kwargs: {
content_guid: {
"guid": content_guid,
"kind": "Attribute",
"name": "Заказ",
"ref": "Document.Оплата.Attribute.Заказ",
"status": "ok",
}
},
)
defined_tree = seq(
atom(1),
seq(atom(0), atom(type_guid), atom(type_guid), identity("10000000-0000-0000-0000-000000000001", "СоставнойТип", "Составной тип", "Комментарий"), seq(string("Pattern"), seq(string("#"), atom(type_guid)))),
atom(0),
)
defined = adapter_server.defined_type_sql_details("upo_test", defined_tree)
assert defined["value_type"]["kind"] == "union"
assert defined["counts"] == {"types": 2, "resolved_types": 2}
criterion_tree = seq(
atom(1),
seq(
atom(14), atom(0), atom(0), atom(0), atom(0),
seq(atom(2), identity("10000000-0000-0000-0000-000000000002", "ПоЗаказу", "По заказу"), seq(string("Pattern"), seq(string("#"), atom(type_guid)))),
seq(atom(0), atom(1), seq(string("#"), atom("157fa490-4ce9-11d4-9415-008048da11f9"), seq(atom(1), atom(content_guid)))),
atom(0), atom("00000000-0000-0000-0000-000000000000"), atom("00000000-0000-0000-0000-000000000000"), seq(atom(0)), seq(atom(0)), seq(atom(1), string("ru"), string("Документы по заказу")),
),
atom(2), seq(atom(0), atom(0)), seq(atom(0), atom(0)),
)
criterion = adapter_server.selection_criterion_sql_details("upo_test", criterion_tree)
assert criterion["use_standard_commands"] is False
assert criterion["list_presentation"] == {"ru": "Документы по заказу"}
assert criterion["content"][0]["ref"] == "Document.Оплата.Attribute.Заказ"
assert criterion["counts"]["resolved_content"] == 1
enum_name = "Состояния"
first_value = seq(seq(atom(0), identity("10000000-0000-0000-0000-000000000003", "Новый", "Новый")), atom(0))
second_value = seq(seq(atom(0), identity("10000000-0000-0000-0000-000000000004", "Закрыт", "Закрыт")), atom(0))
enum_body = [atom(0) for _ in range(21)]
enum_body[5] = seq(atom(0), identity("10000000-0000-0000-0000-000000000005", enum_name, enum_name))
enum_body[6] = atom(0)
enum_body[11] = atom(2)
enum_body[12] = atom(1)
enum_tree = seq(atom(1), seq(*enum_body), atom(4), seq(atom(0), atom(0)), seq(atom(0), atom(0)), seq(atom(0), atom(0)), seq(atom("values"), atom(2), first_value, second_value))
enum = adapter_server.enum_sql_details(enum_tree)
assert enum["quick_choice"] is True
assert enum["choice_mode"] == "BothWays"
assert [item["ref"] for item in enum["values"]] == ["Enum.Состояния.Value.Новый", "Enum.Состояния.Value.Закрыт"]
def test_generated_type_decoder_finds_late_manager_pair() -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
guids = [f"00000000-0000-0000-0000-{index:012d}" for index in range(1, 13)]
tree = seq(
atom(1),
seq(
atom(40),
atom(guids[0]), atom(guids[1]), atom(guids[2]), atom(guids[3]),
atom(guids[4]), atom(guids[5]), atom(guids[6]), atom(guids[7]),
seq(atom(0)), atom(1), atom(2), atom(3),
atom(guids[8]), atom(guids[9]),
atom(0), atom(0),
atom(guids[10]), atom(guids[11]),
),
)
records = adapter_server.generated_type_records_from_tree(
tree,
kind="Document",
name="Заказ",
guid="10000000-0000-0000-0000-000000000001",
)
assert [record["category"] for record in records] == ["Object", "Ref", "Selection", "List", "Manager"]
assert records[-1]["type_guid"] == guids[10]
assert records[-1]["presentation"] == "cfg:DocumentManager.Заказ"
def test_generated_type_decoder_skips_identity_and_maps_constant_and_business_process_pairs() -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
identity = seq(atom(3), seq(atom(1), atom(0), atom("10000000-0000-0000-0000-000000000001")), atom("Объект"), seq(atom(0)))
guids = [f"20000000-0000-0000-0000-{index:012d}" for index in range(1, 17)]
constant_tree = seq(
atom(1),
seq(atom(16), identity, atom(guids[0]), atom(guids[1]), atom(guids[2]), atom(guids[3]), atom(1), atom(0), seq(atom(0)), atom(0), atom(guids[4]), atom(guids[5])),
)
constant_records = adapter_server.generated_type_records_from_tree(
constant_tree,
kind="Constant",
name="Флаг",
guid="10000000-0000-0000-0000-000000000002",
)
assert [record["category"] for record in constant_records[:2]] == ["Manager", "ValueManager"]
assert constant_records[1]["type_guid"] == guids[2]
business_process_tree = seq(
atom(1),
seq(
atom(30), identity, atom(1),
*[atom(value) for value in guids[:12]],
seq(atom(0)), atom(1), atom(0), atom(11), atom(1), atom(1), atom(1), atom(guids[12]), atom(guids[13]),
),
)
business_records = adapter_server.generated_type_records_from_tree(
business_process_tree,
kind="BusinessProcess",
name="Заявка",
guid="10000000-0000-0000-0000-000000000003",
)
assert [record["category"] for record in business_records] == [
"Object", "Ref", "Selection", "List", "RoutePointRef", "RoutePoint", "Manager"
]
assert business_records[4]["name"] == "BusinessProcessRoutePointRef.Заявка"
def test_role_rights_decoder_covers_flags_rights_rls_and_templates(monkeypatch: pytest.MonkeyPatch) -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
string = lambda value: {"type": "string", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
object_guid = "10000000-0000-0000-0000-000000000001"
read_guid = "1c87578f-9e09-4ec0-a991-5629c87b1588"
update_guid = "287b74b8-3a66-4a76-ba27-4f1f6a93770e"
restriction = seq(read_guid, seq(atom(1), seq(atom(1), string("ГДЕ ИСТИНА"), atom(0))))
rights = seq(atom(1), atom(2), atom(read_guid), atom(1), atom(update_guid), atom(-1), atom(1), restriction)
record = seq(seq(atom(1), atom(object_guid), atom(0), atom(0)), rights)
tree = seq(
atom(10),
seq(atom(1), record),
seq(atom(1), seq(string("ДляОбъекта"), string("ГДЕ Ссылка = &Ссылка"))),
atom(1), atom(1), atom(0), atom("4294967295"),
)
monkeypatch.setattr(
adapter_server,
"public_metadata_guid_references",
lambda *args, **kwargs: {
object_guid: {
"guid": object_guid,
"kind": "Document",
"name": "Заказ",
"ref": "Document.Заказ",
"status": "ok",
}
},
)
decoded = adapter_server.role_rights_sql_details("upo_test", tree)
assert decoded["set_for_new_objects"] is True
assert decoded["set_for_attributes_by_default"] is True
assert decoded["independent_rights_of_child_objects"] is False
assert decoded["objects"][0]["object"]["ref"] == "Document.Заказ"
assert decoded["objects"][0]["rights"][0]["name"] == "Read"
assert decoded["objects"][0]["rights"][0]["state"] == "allowed"
assert decoded["objects"][0]["rights"][0]["restrictions"][0]["condition"] == "ГДЕ ИСТИНА"
assert decoded["objects"][0]["rights"][1]["name"] == "Update"
assert decoded["objects"][0]["rights"][1]["state"] == "denied"
assert decoded["restriction_templates"][0]["name"] == "ДляОбъекта"
assert decoded["counts"]["restrictions"] == 1
def test_nested_command_identity_and_category_are_decoded_from_config_tree() -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
command_guid = "10000000-0000-0000-0000-000000000001"
command_class = "078a6af8-d22c-4248-9c33-7e90075a3d2c"
identity = seq(
seq(atom(1), atom(0), atom(command_guid)),
atom("Выполнить"),
seq(atom(1), atom("ru"), atom("Выполнить действие")),
)
tree = seq(seq(atom(2), atom(command_guid), atom(command_class)), identity)
identities = adapter_server.config_tree_identity_records(tree)
categories = adapter_server.config_tree_nested_categories(tree, {command_guid})
assert identities[command_guid]["name"] == "Выполнить"
assert identities[command_guid]["synonyms"] == {"ru": "Выполнить действие"}
assert categories == {command_guid: "Command"}
def test_nested_identity_index_accepts_multilingual_and_empty_synonyms() -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
string = lambda value: {"type": "string", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
multilingual_guid = "10000000-0000-0000-0000-000000000001"
empty_guid = "10000000-0000-0000-0000-000000000002"
tree = seq(
seq(atom(1), atom(0), atom(multilingual_guid)),
string("Многоязычный"),
seq(atom(2), string("ru"), string("Русский"), string("en"), string("English")),
seq(
seq(atom(1), atom(0), atom(empty_guid)),
string("БезСинонима"),
seq(atom(0)),
),
)
identities = adapter_server.config_tree_identity_records(tree)
assert identities[multilingual_guid]["synonyms"] == {"ru": "Русский", "en": "English"}
assert identities[empty_guid]["name"] == "БезСинонима"
assert identities[empty_guid]["synonyms"] == {}
def test_config_identity_accepts_multilingual_and_empty_synonyms() -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
string = lambda value: {"type": "string", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
empty_guid = "10000000-0000-0000-0000-000000000002"
multilingual_guid = "10000000-0000-0000-0000-000000000003"
empty = find_identity(seq(seq(atom(1), atom(0), atom(empty_guid)), string("БезСинонима"), seq(atom(0))))
multilingual = find_identity(
seq(
seq(atom(1), atom(0), atom(multilingual_guid)),
string("Многоязычный"),
seq(atom(2), string("ru"), string("Русский"), string("en"), string("English")),
)
)
assert empty is not None
assert empty.name == "БезСинонима"
assert empty.synonyms == {}
assert multilingual is not None
assert multilingual.synonyms == {"ru": "Русский", "en": "English"}
def test_integration_channel_reference_maps_dbnames_order_to_sql_config_channels(monkeypatch: pytest.MonkeyPatch) -> None:
from types import SimpleNamespace
service_guid = "10000000-0000-0000-0000-000000000001"
first_guid = "10000000-0000-0000-0000-000000000002"
second_guid = "10000000-0000-0000-0000-000000000003"
records = [
SimpleNamespace(guid=service_guid, storage_role="IntegServiceSettings", sql_number=10, source="DBNames", index=1),
SimpleNamespace(guid=first_guid, storage_role="IntegChannelInQueue", sql_number=11, source="DBNames", index=2),
SimpleNamespace(guid=second_guid, storage_role="IntegChannelInQueue", sql_number=12, source="DBNames", index=3),
]
monkeypatch.setattr(adapter_server, "read_storage_files_bytes", lambda *args, **kwargs: ({service_guid: b"payload"}, {}, None))
monkeypatch.setattr(adapter_server, "parse_config_tree_from_bytes", lambda data: {})
monkeypatch.setattr(adapter_server, "config_identity_from_bytes", lambda data: {"guid": service_guid, "name": "Обмен"})
monkeypatch.setattr(
adapter_server,
"integration_service_sql_details",
lambda tree, include_storage=False: {
"channels": [
{"name": "ВходОбычный", "message_direction": "Receive"},
{"name": "ВходСрочный", "message_direction": "Receive"},
]
},
)
monkeypatch.setattr(adapter_server, "cache_nested_metadata_reference", lambda *args, **kwargs: None)
resolved = adapter_server.integration_channel_guid_references(
"upo_test",
[first_guid, second_guid],
dbnames_records=records,
)
assert resolved[first_guid]["ref"] == "IntegrationService.Обмен.Channel.ВходОбычный"
assert resolved[second_guid]["name"] == "ВходСрочный"
assert resolved[second_guid]["owner"]["ref"] == "IntegrationService.Обмен"
def test_metadata_object_properties_falls_back_to_generic_sql_semantics(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(adapter_server, "metadata_object_special_details", lambda payload: {"status": "unsupported"})
monkeypatch.setattr(
adapter_server,
"get_object",
lambda *args, **kwargs: {
"schema": "onec_metadata_object.v1",
"status": "ok",
"base_id": kwargs["base_id"],
"source": {"kind": "live_metadata"},
"object": {"kind": "Document", "name": "Заказ"},
"semantic": {"sections": [{"kind": "attributes"}]},
"counts": {"matches": 1},
},
)
result = adapter_server.metadata_object_properties({"base_id": "upo_test", "kind": "Document", "name": "Заказ"})
assert result["status"] == "ok"
assert result["decoder"] == "generic_semantic_sql"
assert result["properties"] == {"sections": [{"kind": "attributes"}]}
def test_metadata_object_properties_is_public_and_rejects_xml_runtime_source() -> None:
help_result = call_method("help.methods", {"method": "metadata.object.properties"})
blocked = call_method(
"metadata.object.properties",
{"base_id": "upo_test", "kind": "Document", "name": "X", "xml_path": "Z:\\offline.xml"},
)
assert help_result["count"] == 1
assert blocked["status"] == "invalid_argument"
assert blocked["argument"] == "payload.xml_path"
def test_form_owner_cache_resolves_common_form_by_name(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
config = {"server": "sql.example", "database": "upo_test"}
adapter_server.metadata_form_owner_cache_upsert(
config,
base_id="upo_test",
extension={"guid": "fb26cf42-7609-11f1-828f-005056b0d483", "name": "test2"},
owner_kind="CommonForm",
owner_name="t_Форма",
form_name="t_Форма",
table="ConfigCAS",
file_name="adb1ca009585150335d928b709e79a3c52fdb3a2",
form_guid="adb1ca009585150335d928b709e79a3c52fdb3a2",
bsl_offset=1375,
)
cached = adapter_server.metadata_form_owner_cache_lookup(
config,
owner_kind="CommonForm",
form_name="t_Форма",
extension="test2",
)
assert cached is not None
assert cached["form"]["name"] == "t_Форма"
assert cached["source"] == {"table": "ConfigCAS", "file_name": "adb1ca009585150335d928b709e79a3c52fdb3a2"}
assert cached["module_ref"] == "ConfigCAS:adb1ca009585150335d928b709e79a3c52fdb3a2"
assert cached["bsl_offset"] == 1375
def test_form_owner_cache_resolves_form_module_ref(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
config = {"server": "sql.example", "database": "upo_test"}
file_name = "adb1ca009585150335d928b709e79a3c52fdb3a2"
adapter_server.metadata_form_owner_cache_upsert(
config,
base_id="upo_test",
extension={"name": "test2"},
owner_kind="CommonForm",
form_name="t_Форма",
table="ConfigCAS",
file_name=file_name,
bsl_offset=1375,
)
cached = adapter_server.metadata_form_owner_cache_lookup(
config,
module_ref=f"ConfigCAS:{file_name}#form_module",
)
assert cached is not None
assert cached["owner"]["kind"] == "CommonForm"
assert cached["form"]["name"] == "t_Форма"
assert cached["bsl_offset"] == 1375
def test_module_owner_cache_prune_resolves_name_and_supports_dry_run(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
config = {"server": "sql.example", "database": "upo_test"}
owner_guid = "11111111-1111-1111-1111-111111111111"
other_owner_guid = "22222222-2222-2222-2222-222222222222"
first_ref = "Config:first.0#stream:1"
second_ref = "Config:second.0#stream:2"
other_ref = "Config:other.0#stream:3"
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_modules_owner_guid_from_selector",
lambda base_id, payload, timeout_seconds: (
owner_guid,
{
"status": "resolved",
"method": "metadata.objects.list",
"owner_guid": owner_guid,
"object": {
"kind": "CommonModule",
"name": "ОбщегоНазначения",
"guid": owner_guid,
"ref": "CommonModule.ОбщегоНазначения",
},
},
),
)
adapter_server.metadata_module_owner_cache_upsert(
config,
first_ref,
{"kind": "CommonModule", "name": "ОбщегоНазначения", "guid": owner_guid},
)
adapter_server.metadata_module_owner_cache_upsert(
config,
second_ref,
{"kind": "CommonModule", "name": "ОбщегоНазначения", "guid": owner_guid},
)
adapter_server.metadata_module_owner_cache_upsert(
config,
other_ref,
{"kind": "CommonModule", "name": "ДругойМодуль", "guid": other_owner_guid},
)
dry_run = adapter_server.metadata_module_owner_cache_prune(
{
"base_id": "upo_test",
"object_type": "CommonModule",
"object_name": "ОбщегоНазначения",
"dry_run": True,
}
)
assert dry_run["status"] == "ok"
assert dry_run["dry_run"] is True
assert dry_run["counts"] == {"matched": 2}
assert dry_run["resolved_by"] == "public_object_selector"
assert dry_run["query"]["owner"]["ref"] == "CommonModule.ОбщегоНазначения"
assert "guid" not in dry_run["query"]["owner"]
assert "owner_guid" not in dry_run["query"]
assert adapter_server.metadata_module_owner_cache_lookup(config, first_ref) is not None
applied = adapter_server.metadata_module_owner_cache_prune(
{
"base_id": "upo_test",
"ref": "CommonModule.ОбщегоНазначения",
"dry_run": False,
}
)
assert applied["counts"] == {"matched": 2, "deleted": 2}
assert adapter_server.metadata_module_owner_cache_lookup(config, first_ref) is None
assert adapter_server.metadata_module_owner_cache_lookup(config, second_ref) is None
assert adapter_server.metadata_module_owner_cache_lookup(config, other_ref) is not None
def test_module_owner_cache_prune_rejects_unresolved_name(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_modules_owner_guid_from_selector",
lambda base_id, payload, timeout_seconds: (
None,
{
"status": "not_found",
"selector": {"object_type": "CommonModule", "object_name": "НетТакого"},
},
),
)
result = adapter_server.metadata_module_owner_cache_prune(
{
"base_id": "upo_test",
"ref": "CommonModule.НетТакого",
"dry_run": True,
}
)
assert result["status"] == "not_found"
assert result["error"] == "owner_not_resolved"
assert result["selector"] == {
"ref": "CommonModule.НетТакого",
"kind": "CommonModule",
"name": "НетТакого",
}
def test_code_index_verify_reports_cache_hit_verified(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
config = {"server": "sql.example", "database": "upo_test"}
text = "Процедура Проверка()\nКонецПроцедуры"
data = b"payload"
module_ref = "ConfigCAS:file1"
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (data, config, None))
monkeypatch.setattr(adapter_server, "extract_code_index_text_from_payload", lambda *args, **kwargs: (text, {"status": "ok"}))
adapter_server.code_index_upsert(
config,
base_id="upo_test",
table="ConfigCAS",
file_name="file1",
module_ref=module_ref,
data=data,
text=text,
verified=True,
)
result = adapter_server.metadata_code_index_verify({"base_id": "upo_test", "module_ref": module_ref})
assert result["status"] == "ok"
assert result["freshness"]["status"] == "cache_hit_verified"
assert result["freshness"]["verified_against_sql"] is True
def test_code_index_verify_reports_cache_hit_stale(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
config = {"server": "sql.example", "database": "upo_test"}
module_ref = "ConfigCAS:file1"
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
adapter_server.code_index_upsert(
config,
base_id="upo_test",
table="ConfigCAS",
file_name="file1",
module_ref=module_ref,
data=b"old",
text="Процедура Старая()\nКонецПроцедуры",
verified=True,
)
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (b"new", config, None))
monkeypatch.setattr(adapter_server, "extract_code_index_text_from_payload", lambda *args, **kwargs: ("Процедура Новая()\nКонецПроцедуры", {"status": "ok"}))
result = adapter_server.metadata_code_index_verify({"base_id": "upo_test", "module_ref": module_ref})
assert result["status"] == "stale"
assert result["freshness"]["status"] == "cache_hit_stale"
assert result["freshness"]["current_payload_sha1"] != result["freshness"]["payload_sha1"]
def test_code_vector_search_revalidates_candidates(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
config = {"server": "sql.example", "database": "upo_test"}
text = "Процедура ЗаменаДомена()\nКонецПроцедуры"
data = b"payload"
module_ref = "ConfigCAS:file1"
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (data, config, None))
monkeypatch.setattr(adapter_server, "extract_code_index_text_from_payload", lambda *args, **kwargs: (text, {"status": "ok"}))
upserted = adapter_server.code_index_upsert(
config,
base_id="upo_test",
table="ConfigCAS",
file_name="file1",
module_ref=module_ref,
data=data,
text=text,
owner={"owner_kind": "CommonForm", "owner_name": "t_Форма", "form_name": "t_Форма"},
verified=True,
)
adapter_server.code_vector_upsert_chunks(config, {**upserted, "module_ref": module_ref, "text": text, "routines": upserted["routines"]})
result = adapter_server.metadata_code_vector_search({"base_id": "upo_test", "query": "домен", "verify": True})
assert result["status"] == "ok"
assert result["source"]["authoritative"] is False
assert result["matches"][0]["freshness"]["status"] == "cache_hit_verified"
assert result["matches"][0]["owner"]["name"] == "t_Форма"
def test_extension_common_form_read_selector_points_to_form_decode() -> None:
selectors = adapter_server.extension_object_read_selectors(
"upo_test",
"CommonForm",
{"name": "t_Форма", "guid": "11111111-2222-3333-4444-555555555555"},
{"table": "ConfigCAS", "file_name": "11111111-2222-3333-4444-555555555555"},
)
assert selectors["form_decode"] == {
"method": "metadata.form.decode",
"base_id": "upo_test",
"kind": "CommonForm",
"guid": "11111111-2222-3333-4444-555555555555",
"name": "t_Форма",
"table": "ConfigCAS",
"file_name": "11111111-2222-3333-4444-555555555555",
}
assert "forms" not in selectors
def test_form_command_button_write_blocks_without_sql_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_forms_search",
lambda payload: {
"schema": "onec_saved_state_form_search.v1",
"status": "ok",
"base_id": payload["base_id"],
"forms": [],
"counts": {"forms": 0},
},
)
result = adapter_server.metadata_form_command_button_write(
{
"base_id": "upo_test",
"extension": "test2",
"object_type": "CommonForm",
"object_name": "t_Форма",
"command_name": "РасчетС",
}
)
assert result["status"] == "blocked"
assert result["error"] == "saved_state_or_xml_form_target_required"
assert result["target"]["kind"] == "CommonForm"
assert result["operation"]["command"]["name"] == "РасчетС"
assert "SQL storage only" in result["diagnostics"]["source_boundary"]
assert any(item.get("workflow") == "xml_analysis_learning" for item in result["next_resolution"])
assert not any(item.get("workflow") == "extension_xml_staging" for item in result["next_resolution"])
rpc_followups = [item for item in result["next_resolution"] if item.get("method")]
assert rpc_followups
assert all("params" in item and "payload" not in item for item in rpc_followups)
def test_form_command_button_write_selects_saved_form_by_owner_without_writing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
owner_guid = "11111111-1111-1111-1111-111111111111"
form_guid = "22222222-2222-2222-2222-222222222222"
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_forms_search",
lambda payload: {
"status": "ok",
"forms": [
{
"name": "ФормаОбмена",
"owner": {"kind": "DataProcessor", "name": "ПервыйОбмен"},
"source": {"table": "ConfigSave", "file_name": "first-form"},
},
{
"name": "ФормаОбмена",
"owner": {"kind": "DataProcessor", "name": "ВторойОбмен"},
"source": {"table": "ConfigSave", "file_name": "second-form"},
},
],
"counts": {"forms": 2},
},
)
result = adapter_server.call_method(
"metadata.form.command_button.write",
{
"base_id": "upo_test",
"ref": "Обработки.ВторойОбмен",
"object_guid": owner_guid,
"form_name": "ФормаОбмена",
"form_guid": form_guid,
"command_name": "Выполнить",
"handler_name": "ОбработатьВыполнение",
"include_storage": True,
},
)
assert result["status"] == "blocked"
assert result["counts"]["has_concrete_saved_state"] is True
assert result["target"]["object"] == "ВторойОбмен"
assert result["target"]["object_guid"] == owner_guid
assert result["target"]["form"] == "ФормаОбмена"
assert result["target"]["form_guid"] == form_guid
assert result["operation"]["command"]["action"] == "ОбработатьВыполнение"
assert result["saved_state_target"]["file_name"] == "second-form"
assert result["diagnostics"]["message"].startswith("A saved-state form target exists")
def test_form_command_button_write_help_exposes_guards_and_child_selectors() -> None:
methods = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}
method = methods["metadata.form.command_button.write"]
assert method["selector_capabilities"]["read_only"] is False
assert "allow_saved_state_write" in method["selector_capabilities"]["write_guards"]
assert "repository_apply_gate" in method["selector_capabilities"]["write_guards"]
assert method["selector_capabilities"]["child_selector_arguments"] == [
"command_name",
"button_name",
"handler_name",
]
assert method["input_schema"]["properties"]["form_guid"]["description"].endswith(
"distinct from the owner GUID."
)
def test_form_command_button_write_reports_saved_state_target_as_plan(monkeypatch: pytest.MonkeyPatch) -> None:
command_node = '{11,{1,409b9a53-7f7e-4178-86c1-33176c7c7a7a},"ЗаменаДомена",{1,1,{"ru","Замена домена"}},{1,1,{"ru","Замена домена"}},{0,{0,{"B",1},0}},{0,0,0},{4,0,{0},"",-1,-1,1,0,""},"ЗаменаДомена",3,0,0,{0,0},1,0,1,0,0,2,0,0}'
button_node = '{34,{1,02023637-7868-4a5f-8576-835a76e0c9ba},0,1,{0,{0,{"B",1},0}},1,"ЗаменаДомена",{1,0},1,{1,409b9a53-7f7e-4178-86c1-33176c7c7a7a},{0},3,0,0,0,2,2,0,0,0,{4,4,{0},4},{4,4,{0},4},{4,4,{0},4},{8,3,0,1,100},{0,0,0},0,{4,0,{0},"",-1,-1,1,0,""},1,{"Pattern"},"",2,0,1,{12,{2,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,0,"ЗаменаДоменаРасширеннаяПодсказка",{1,0},{1,0},1,0,0,2,2,{4,4,{0},4},{8,3,0,1,100},{0,0,0},1,{5,0,0,3,0,{0,1,0},{4,4,{0},4},{4,4,{0},4},{3,0,{0},0,1,0,48312c09-257f-4b29-b280-284dd89efc1e}},0,1,2,{1,{1,0},0},0,0,1,0,0,1,0,3,3,0,0},{"U"},1,0,0,1,0,0,0,3,3,3,0,0,1,0,0,0,1,0,0,{4,0,{0},"",-1,-1,1,0,""},0,0,0,1,""}'
form_text = "{4,{" + ",".join(["0"] * 25 + [button_node]) + '},"",{0,0},{0,0},' + "{0,1," + command_node + "},{0,0},{0,0},0,0}"
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_forms_search",
lambda payload: {
"schema": "onec_saved_state_form_search.v1",
"status": "ok",
"base_id": payload["base_id"],
"forms": [
{
"name": "t_Форма",
"source": {"table": "ConfigCASSave", "file_name": "form-file"},
},
{
"name": "tt_Форма3",
"source": {"table": "ConfigCASSave", "file_name": "other-form-file"},
},
],
"counts": {"forms": 2},
},
)
monkeypatch.setattr(
adapter_server,
"metadata_form_decode",
lambda payload: {
"schema": "onec_form_decode.v1",
"status": "ok",
"source": {"table": payload["table"], "file_name": payload["file_name"]},
"form": {"file_name": payload["file_name"]},
"profile": {
"commands": [{"path": "5.2", "name": "ЗаменаДомена", "title": "Замена домена", "guids_sample": ["409b9a53-7f7e-4178-86c1-33176c7c7a7a"]}],
"items": [{"path": "1.25", "name": "ЗаменаДомена", "title": "", "marker": "34"}],
"command_links": [{"command": "ЗаменаДомена", "handler": "ЗаменаДомена", "command_guid": "409b9a53-7f7e-4178-86c1-33176c7c7a7a"}],
"button_command_links": [{"button": "ЗаменаДомена", "command_guid": "409b9a53-7f7e-4178-86c1-33176c7c7a7a"}],
},
},
)
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (form_text.encode("utf-8-sig"), {"database": "upo_test"}, None))
monkeypatch.setattr(adapter_server, "metadata_write_apply_plan_gate", lambda *args, **kwargs: ({"allowed": True}, None))
result = adapter_server.metadata_form_command_button_write(
{
"base_id": "upo_test",
"object_type": "CommonForm",
"object_name": "t_Форма",
"command_name": "РасчетС",
"allow_saved_state_write": True,
}
)
assert result["status"] == "planned"
assert result["saved_state_target"] == {"table": "ConfigCASSave", "form": "t_Форма"}
assert result["saved_state_search"]["counts"]["forms"] == 2
assert result["saved_state_search"]["selected_form"]["name"] == "t_Форма"
assert "forms" not in result["saved_state_search"]
assert result["counts"]["has_concrete_saved_state"] is True
assert result["proposal"]["status"] == "accepted_for_review"
assert [edit["mode"] for edit in result["proposal"]["edits"]] == [
"structural_append_child_preserve_format",
"structural_append_child_preserve_format",
]
assert result["structural"]["command_append"]["parent_path"] == "5"
assert result["structural"]["button_append"]["parent_path"] == "1"
assert result["handler_result"]["source"] == {"table": "ConfigCASSave", "file_name": "form-file", "module_path": "2"}
assert result["handler_result"]["proposal"]["edits"][0]["path"] == "2"
assert result["handler_result"]["status"] == "planned"
def test_form_command_button_write_existing_workflow_returns_semantic_verify_and_refresh(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_forms_search",
lambda payload: {
"schema": "onec_saved_state_form_search.v1",
"status": "ok",
"base_id": payload["base_id"],
"forms": [{"name": "t_Форма", "source": {"table": "ConfigCASSave", "file_name": "form-file"}}],
"counts": {"forms": 1},
},
)
monkeypatch.setattr(
adapter_server,
"metadata_form_decode",
lambda payload: {
"schema": "onec_form_decode.v1",
"status": "ok",
"source": {"table": payload["table"], "file_name": payload["file_name"]},
"form": {"file_name": payload["file_name"]},
"profile": {
"commands": [{"path": "5.3", "name": "РасчетС", "title": "РасчетС", "guids_sample": ["74959ac4-7d60-4b92-a21f-cc44c409ed91"]}],
"items": [{"path": "1.66", "name": "РасчетС", "title": "РасчетС", "marker": "34"}],
"module": {"routines_sample": [{"kind": "Процедура", "name": "РасчетС"}]},
"command_links": [{"command": "РасчетС", "handler": "РасчетС", "command_guid": "74959ac4-7d60-4b92-a21f-cc44c409ed91"}],
"button_command_links": [{"button": "РасчетС", "command": "РасчетС", "command_guid": "74959ac4-7d60-4b92-a21f-cc44c409ed91"}],
"counts": {"commands_total": 2, "items_total": 10, "command_links": 2, "button_command_links": 2},
},
},
)
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (b'{"ok"}', {"database": "upo_test"}, None))
monkeypatch.setattr(
adapter_server,
"form_embedded_module_handler_write_apply",
lambda *args, **kwargs: {"schema": "onec_form_embedded_module_write.v1", "status": "applied", "applied": True},
)
monkeypatch.setattr(
adapter_server,
"code_index_refresh_form_embedded_module",
lambda **kwargs: {"schema": "onec_code_index_refresh_form_module.v1", "status": "updated", "routine_count": 1},
)
result = adapter_server.metadata_form_command_button_write(
{
"base_id": "upo_test",
"extension": "test2",
"object_type": "CommonForm",
"object_name": "t_Форма",
"command_name": "РасчетС",
"allow_saved_state_write": True,
"allow_sql_saved_state_apply": True,
"mode": "apply",
}
)
assert result["status"] == "verified"
assert result["idempotency"]["status"] == "already_exists"
assert result["semantic_verify"]["verified"] is True
assert result["semantic_verify"]["command"]["path"] == "5.3"
assert result["semantic_verify"]["button"]["path"] == "1.66"
assert result["code_index_refresh"]["status"] == "updated"
def test_metadata_form_decode_working_selector_prefers_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_saved_state_forms_search(payload: dict[str, Any]) -> dict[str, Any]:
seen["search_payload"] = payload
return {
"schema": "onec_saved_state_form_search.v1",
"status": "ok",
"base_id": payload["base_id"],
"forms": [
{
"name": "t_Форма",
"form": {"name": "t_Форма", "guid": "form-guid"},
"source": {"kind": "live_metadata"},
"file": {"FileName": "extension-guid__form-guid.0"},
}
],
"counts": {"forms": 1},
}
def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, Any], None]:
seen["read"] = {"base_id": base_id, "table": table, "file_name": file_name}
return b"{}", {"database": base_id}, None
monkeypatch.setattr(adapter_server, "metadata_saved_state_forms_search", fake_saved_state_forms_search)
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
monkeypatch.setattr(
adapter_server,
"parse_config_tree_from_bytes",
lambda data: parse_brace_text('{4,{0},"",{0,0},{0,0},{0,0},{0,0},{0,0},0,0}'),
)
result = adapter_server.metadata_form_decode(
{
"base_id": "upo_test",
"extension": "test2",
"object_type": "CommonForm",
"form": "t_Форма",
"source_state": "working",
"include_storage": True,
}
)
assert result["status"] == "ok"
assert seen["search_payload"]["extension"] == "test2"
assert seen["search_payload"]["tables"] == ["ConfigCASSave"]
assert seen["read"] == {"base_id": "upo_test", "table": "ConfigCASSave", "file_name": "extension-guid__form-guid.0"}
assert result["source"]["table"] == "ConfigCASSave"
assert result["source"]["file_name"] == "extension-guid__form-guid.0"
def test_form_command_button_verify_resolves_saved_state_by_public_selector(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_saved_state_forms_search(payload: dict[str, Any]) -> dict[str, Any]:
seen["search_payload"] = payload
return {
"schema": "onec_saved_state_form_search.v1",
"status": "ok",
"base_id": payload["base_id"],
"forms": [
{
"name": "t_Форма",
"form": {"name": "t_Форма", "guid": "form-guid"},
"source": {"table": "ConfigCASSave", "file_name": "extension-guid__form-guid.0"},
},
{
"name": "tt_Форма3",
"form": {"name": "tt_Форма3", "guid": "other-form-guid"},
"source": {"table": "ConfigCASSave", "file_name": "extension-guid__other-form-guid.0"},
},
],
"counts": {"forms": 2},
}
def fake_form_decode(payload: dict[str, Any]) -> dict[str, Any]:
seen["decode_payload"] = payload
return {
"schema": "onec_form_decode.v1",
"status": "ok",
"source": {"table": payload["table"], "file_name": payload["file_name"]},
"profile": {
"commands": [{"path": "5.3", "name": "РасчетС", "title": "РасчетС"}],
"items": [{"path": "1.66", "name": "РасчетС", "title": "РасчетС", "type_name": "Кнопка"}],
"module": {"routines_sample": [{"kind": "Процедура", "name": "РасчетС"}]},
"command_links": [{"command": "РасчетС", "handler": "РасчетС"}],
"button_command_links": [{"button": "РасчетС", "command": "РасчетС"}],
"counts": {"commands_total": 2, "items_total": 10},
},
}
monkeypatch.setattr(adapter_server, "metadata_saved_state_forms_search", fake_saved_state_forms_search)
monkeypatch.setattr(
adapter_server,
"metadata_form_decode",
fake_form_decode,
)
result = adapter_server.metadata_form_command_button_verify(
{
"base_id": "upo_test",
"extension": "test2",
"object_type": "CommonForm",
"form": "t_Форма",
"command_name": "РасчетС",
}
)
assert result["status"] == "ok"
assert result["verified"] is True
assert result["command"]["path"] == "5.3"
assert result["command"]["form_path"] == "5.3"
assert result["button"]["path"] == "1.66"
assert result["button"]["form_path"] == "1.66"
assert result["saved_state_search"]["counts"]["forms"] == 2
assert result["saved_state_search"]["selected_form"]["name"] == "t_Форма"
assert "forms" not in result["saved_state_search"]
assert seen["search_payload"]["extension"] == "test2"
assert seen["search_payload"]["include_storage"] is True
assert seen["decode_payload"]["table"] == "ConfigCASSave"
assert seen["decode_payload"]["file_name"] == "extension-guid__form-guid.0"
public_result = adapter_server.call_method(
"metadata.form.command_button.verify",
{
"base_id": "upo_test",
"extension": "test2",
"object_type": "CommonForm",
"form": "t_Форма",
"command_name": "РасчетС",
"include_storage": False,
},
)
assert public_result["status"] == "ok"
assert public_result["command"]["form_path"] == "5.3"
assert public_result["button"]["form_path"] == "1.66"
assert "path" not in public_result["command"]
assert public_result["saved_state_search"]["selected_form"]["name"] == "t_Форма"
assert "forms" not in public_result["saved_state_search"]
def test_form_command_button_verify_disambiguates_same_form_name_by_owner(
monkeypatch: pytest.MonkeyPatch,
) -> None:
seen: dict[str, Any] = {}
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_forms_search",
lambda payload: {
"status": "ok",
"forms": [
{
"name": "ФормаОбмена",
"owner": {"kind": "DataProcessor", "name": "ПервыйОбмен"},
"source": {"table": "ConfigSave", "file_name": "first-form"},
},
{
"name": "ФормаОбмена",
"owner": {"kind": "DataProcessor", "name": "ВторойОбмен"},
"source": {"table": "ConfigSave", "file_name": "second-form"},
},
],
"counts": {"forms": 2},
},
)
def decode_form(payload: dict[str, Any]) -> dict[str, Any]:
seen["file_name"] = payload["file_name"]
return {
"status": "ok",
"profile": {
"commands": [{"path": "5.3", "name": "Выполнить"}],
"items": [{"path": "1.2", "name": "Выполнить", "type_name": "Кнопка"}],
"module": {"routines_sample": [{"name": "Выполнить"}]},
"command_links": [{"command": "Выполнить", "handler": "Выполнить"}],
"button_command_links": [{"button": "Выполнить", "command": "Выполнить"}],
},
}
monkeypatch.setattr(adapter_server, "metadata_form_decode", decode_form)
result = adapter_server.call_method(
"metadata.form.command_button.verify",
{
"base_id": "upo_test",
"ref": "Обработки.ВторойОбмен",
"form_name": "ФормаОбмена",
"command_name": "Выполнить",
},
)
assert result["status"] == "ok"
assert result["verified"] is True
assert seen["file_name"] == "second-form"
assert result["query"]["owner"] == {"kind": "DataProcessor", "name": "ВторойОбмен"}
assert result["query"]["form"] == "ФормаОбмена"
def test_form_command_button_verify_help_exposes_child_name_selectors() -> None:
methods = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}
method = methods["metadata.form.command_button.verify"]
assert method["selector_capabilities"]["read_only"] is True
assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name"]
assert method["selector_capabilities"]["child_selector_arguments"] == [
"command_name",
"button_name",
"handler_name",
]
assert method["input_schema"]["properties"]["command_name"]["type"] == "string"
def test_form_write_target_verify_reports_writable_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_resolve(payload: dict[str, Any]) -> dict[str, Any]:
return {
"schema": "onec_form_write_target_resolution.v1",
"status": "ok",
"base_id": payload["base_id"],
"source": {"table": payload["table"], "file_name": "form-file"},
"form": {"name": "t_Форма"},
"target": {"section": "commands", "name": "РасчетС"},
"effective_target": {"section": "commands", "name": "РасчетС"},
"write_target": {"section": "commands", "name": "РасчетС", "path": "5.3.3.2.1", "status": "ok"},
"property": {"status": "ok", "path": "5.3.3.2.1", "old": "РасчетС", "new": "Расчет"},
"counts": {"matches": 1},
"search": {
"schema": "onec_saved_state_form_search.v1",
"status": "ok",
"base_id": payload["base_id"],
"forms": [{"name": "t_Форма", "source": {"table": "ConfigCASSave", "file_name": "form-file"}}],
"counts": {"forms": 1},
},
}
monkeypatch.setattr(adapter_server, "metadata_form_write_target_resolve", fake_resolve)
result = adapter_server.call_method(
"metadata.form.write_target.verify",
{
"base_id": "upo_test",
"extension": "test2",
"form": "t_Форма",
"command": "РасчетС",
"property": "title",
"value": "Расчет",
},
)
assert result["status"] == "ok"
assert result["verified"] is True
assert result["writable_now"] is True
assert result["needs_prepare"] is False
assert result["source"] == {
"kind": "saved_state",
"layer": "extension_saved_state",
}
assert result["write_target"]["form_path"] == "5.3.3.2.1"
assert "path" not in result["write_target"]
assert result["saved_state_search"]["selected_form"]["name"] == "t_Форма"
def test_form_write_target_verify_reports_prepare_needed(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_form_write_target_resolve",
lambda payload: {
"schema": "onec_form_write_target_resolution.v1",
"status": "not_found",
"base_id": payload["base_id"],
"diagnostics": {"message": "Saved-state form was not found."},
"search": {"schema": "onec_saved_state_form_search.v1", "status": "ok", "base_id": payload["base_id"], "forms": [], "counts": {"forms": 0}},
},
)
result = adapter_server.call_method(
"metadata.form.write_target.verify",
{
"base_id": "upo_test",
"extension": "test2",
"object_type": "CommonForm",
"form": "t_Форма",
"command": "РасчетС",
"property": "title",
},
)
assert result["status"] == "needs_prepare"
assert result["verified"] is False
assert result["writable_now"] is False
assert result["needs_prepare"] is True
assert result["next_resolution"]["method"] == "metadata.saved_state.prepare"
assert result["next_resolution"]["params"] == {
"base_id": "upo_test",
"layer": "extension_saved_state",
"ref": "CommonForm.t_Форма",
"kind": "CommonForm",
"name": "t_Форма",
"extension": "test2",
"mode": "plan",
}
assert result["saved_state_search"]["counts"]["forms"] == 0
def test_form_write_target_resolve_prefers_exact_form_among_similar_saved_forms(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_forms_search",
lambda payload: {
"schema": "onec_saved_state_form_search.v1",
"status": "ok",
"base_id": payload["base_id"],
"forms": [
{"name": "t_Форма", "form": {"name": "t_Форма"}, "source": {"table": "ConfigCASSave", "file_name": "form-file"}},
{"name": "tt_Форма3", "form": {"name": "tt_Форма3"}, "source": {"table": "ConfigCASSave", "file_name": "other-form-file"}},
],
"counts": {"forms": 2},
},
)
def fake_form_decode(payload: dict[str, Any]) -> dict[str, Any]:
seen["decode_payload"] = payload
return {
"schema": "onec_form_decode.v1",
"status": "ok",
"source": {"table": payload["table"], "file_name": payload["file_name"]},
"form": {"name": "t_Форма"},
"profile": {
"commands": [{"path": "5.3", "name": "РасчетС", "title": "РасчетС", "_profile_section": "commands"}],
"items": [],
"attributes": [],
"tables": [],
"command_bars": [],
},
}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_form_decode)
result = adapter_server.metadata_form_write_target_resolve(
{
"base_id": "upo_test",
"extension": "test2",
"form": "t_Форма",
"command": "РасчетС",
"property": "title",
}
)
assert result["status"] == "ok"
assert seen["decode_payload"]["file_name"] == "form-file"
assert result["target"]["name"] == "РасчетС"
def test_form_write_target_resolve_disambiguates_same_form_name_by_owner(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
first_owner_guid = "11111111-1111-1111-1111-111111111111"
second_owner_guid = "22222222-2222-2222-2222-222222222222"
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_forms_search",
lambda payload: {
"schema": "onec_saved_state_form_search.v1",
"status": "ok",
"base_id": payload["base_id"],
"forms": [
{
"name": "ФормаОбмена",
"form": {"name": "ФормаОбмена"},
"owner": {"kind": "DataProcessor", "name": "ПервыйОбмен", "guid": first_owner_guid},
"source": {"table": "ConfigCASSave", "file_name": "first-form"},
},
{
"name": "ФормаОбмена",
"form": {"name": "ФормаОбмена"},
"owner": {"kind": "DataProcessor", "name": "ВторойОбмен", "guid": second_owner_guid},
"source": {"table": "ConfigCASSave", "file_name": "second-form"},
},
],
"counts": {"forms": 2},
},
)
def fake_form_decode(payload: dict[str, Any]) -> dict[str, Any]:
seen["decode_payload"] = payload
return {
"schema": "onec_form_decode.v1",
"status": "ok",
"source": {"table": payload["table"], "file_name": payload["file_name"]},
"form": {"name": "ФормаОбмена"},
"profile": {
"commands": [{"path": "5.3", "name": "РасчетС", "title": "РасчетС", "_profile_section": "commands"}],
"items": [],
"attributes": [],
"tables": [],
"command_bars": [],
},
}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_form_decode)
result = adapter_server.call_method(
"metadata.form.write_target.resolve",
{
"base_id": "upo_test",
"ref": "Обработки.ВторойОбмен",
"object_guid": second_owner_guid,
"form_name": "ФормаОбмена",
"command": "РасчетС",
"property": "title",
},
)
assert result["status"] == "ok"
assert seen["decode_payload"]["file_name"] == "second-form"
assert "guid" not in seen["decode_payload"]
assert result["target"]["name"] == "РасчетС"
def test_form_write_target_help_separates_owner_form_and_child_selectors() -> None:
methods = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}
for method_name in ("metadata.form.write_target.resolve", "metadata.form.write_target.verify"):
method = methods[method_name]
assert method["selector_capabilities"]["accepts_ordinal"] is False
assert method["selector_capabilities"]["read_only"] is True
assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name", "form_guid"]
assert method["selector_capabilities"]["child_selector_arguments"] == [
"element",
"element_name",
"command",
"attribute",
]
properties = method["input_schema"]["properties"]
assert properties["object_guid"]["description"].endswith("owner GUID.")
assert properties["form_guid"]["description"].endswith("owner GUID.")
def test_saved_state_forms_search_names_saved_only_extension_form(monkeypatch: pytest.MonkeyPatch) -> None:
form_file = "ext-guid__caee4437-14b6-436f-98e3-360c4577f20d.0"
descriptor_file = "ext-guid__caee4437-14b6-436f-98e3-360c4577f20d"
seen_reads: list[tuple[str, str, str]] = []
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {
"status": "ok",
"files": [{"FileName": form_file, "PartCount": 1, "Bytes": 100}],
},
)
monkeypatch.setattr(
adapter_server,
"metadata_form_decode",
lambda payload: {
"schema": "onec_form_decode.v1",
"status": "ok",
"source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
"form": {"guid": "caee4437-14b6-436f-98e3-360c4577f20d", "file_name": payload["file_name"]},
"profile": {"items": []},
"counts": {"items": 0},
},
)
def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, **kwargs: Any) -> tuple[bytes | None, dict[str, Any], None]:
seen_reads.append((base_id, table, file_name))
assert file_name == descriptor_file
return b"descriptor", {"database": base_id}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
monkeypatch.setattr(
adapter_server,
"config_identity_from_bytes",
lambda data: {
"guid": "caee4437-14b6-436f-98e3-360c4577f20d",
"name": "new_SaveOnlyForm",
"synonyms": {"ru": "Новая форма"},
},
)
result = adapter_server.metadata_saved_state_forms_search(
{
"base_id": "upo_test",
"tables": ["ConfigCASSave"],
"form": "new_SaveOnlyForm",
"limit": 10,
"include_storage": True,
}
)
assert result["status"] == "ok"
assert result["counts"]["forms"] == 1
row = result["forms"][0]
assert row["name"] == "new_SaveOnlyForm"
assert row["synonym"] == "Новая форма"
assert row["form"]["name"] == "new_SaveOnlyForm"
assert row["form"]["synonym"] == "Новая форма"
assert row["form"]["identity"]["source"] == "saved_state_descriptor"
assert row["form"]["identity"]["descriptor_file_name"] == descriptor_file
assert seen_reads == [("upo_test", "ConfigCASSave", descriptor_file)]
def test_saved_state_forms_search_disambiguates_same_form_name_by_owner(monkeypatch: pytest.MonkeyPatch) -> None:
owner_guids = {
"first-form.0": "11111111-1111-1111-1111-111111111111",
"second-form.0": "22222222-2222-2222-2222-222222222222",
}
owner_names = {
"first-form.0": "ПервыйОбмен",
"second-form.0": "ВторойОбмен",
}
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {
"status": "ok",
"files": [{"FileName": file_name} for file_name in owner_guids],
},
)
def fake_row(**kwargs: Any) -> dict[str, Any]:
file_name = kwargs["file_name"]
return {
"table": kwargs["table"],
"file_name": file_name,
"name": "ФормаОбмена",
"form": {"name": "ФормаОбмена", "guid": f"{file_name}-guid"},
"owner": {
"kind": "DataProcessor",
"name": owner_names[file_name],
"guid": owner_guids[file_name],
},
"source": {"table": kwargs["table"], "file_name": file_name},
"matches": [],
}
monkeypatch.setattr(adapter_server, "saved_state_form_search_row", fake_row)
result = adapter_server.call_method(
"metadata.saved_state.forms.search",
{
"base_id": "upo_test",
"tables": ["ConfigCASSave"],
"ref": "Обработки.ВторойОбмен",
"object_guid": owner_guids["second-form.0"],
"form_name": "ФормаОбмена",
"include_storage": True,
},
)
assert result["status"] == "ok"
assert result["counts"]["forms"] == 1
assert result["forms"][0]["file_name"] == "second-form.0"
assert result["query"]["owner"] == {
"kind": "DataProcessor",
"name": "ВторойОбмен",
"guid": owner_guids["second-form.0"],
}
assert result["query"]["form"] == "ФормаОбмена"
def test_saved_state_forms_search_default_is_name_first(monkeypatch: pytest.MonkeyPatch) -> None:
file_name = "extension-guid__form-guid.0"
monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda *args, **kwargs: ("extension-guid", None))
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {"status": "ok", "files": [{"FileName": file_name, "PartCount": 1, "Bytes": 100}]},
)
monkeypatch.setattr(
adapter_server,
"saved_state_form_search_row",
lambda **kwargs: {
"table": "ConfigCASSave",
"file_name": file_name,
"name": "t_Форма",
"synonym": "T форма",
"owner": {
"status": "resolved",
"kind": "Catalog",
"name": "test2",
"guid": "owner-guid",
"source": "saved_state_descriptor",
},
"form": {
"name": "t_Форма",
"synonym": "T форма",
"guid": "form-guid",
"file_name": file_name,
"identity": {"guid": "form-guid", "descriptor_file_name": "extension-guid__form-guid"},
},
"source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": file_name},
"counts": {"items": 1, "commands": 1},
"matches": [
{
"name": "ЗаменаДомена",
"id": "1",
"title": "Замена домена",
"path": "5.2",
"marker": "11",
"section": "commands",
"writable_properties": [
{
"property": "title",
"presentation": "Заголовок",
"path": "5.2.3.2.1",
"value": "Замена домена",
"value_type": "string",
"verification": "source_aware_readback",
}
],
}
],
},
)
result = adapter_server.metadata_saved_state_forms_search(
{
"base_id": "upo_test",
"tables": ["ConfigCASSave"],
"extension": "test2",
"ref": "Catalog.test2",
"form": "t_Форма",
"command": "ЗаменаДомена",
}
)
assert result["source"] == {"kind": "saved_state"}
assert result["query"]["owner"]["ref"] == "Catalog.test2"
assert result["counts"] == {"forms": 1, "limit": 50}
form = result["forms"][0]
assert form["qualified_name"] == "test2.t_Форма"
assert form["selector"] == {
"extension": "test2",
"ref": "Catalog.test2",
"form": "t_Форма",
"qualified_name": "test2.t_Форма",
}
assert form["matches"][0]["selector"] == {
"extension": "test2",
"ref": "Catalog.test2",
"form": "t_Форма",
"qualified_name": "test2.t_Форма",
"command": "ЗаменаДомена",
}
assert form["matches"][0]["writable_properties"] == [
{
"property": "title",
"presentation": "Заголовок",
"value": "Замена домена",
"value_type": "string",
"verification": "source_aware_readback",
}
]
for storage_marker in (
"ConfigCASSave",
"extension-guid",
"form-guid",
"owner-guid",
"file_name",
"descriptor_file_name",
"path",
"marker",
"live_sql",
):
assert storage_marker not in str(result)
def test_saved_state_forms_search_infers_public_extension_name_without_filter(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
file_name = f"{extension_guid}__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}]},
)
monkeypatch.setattr(
adapter_server,
"saved_state_form_search_row",
lambda **kwargs: {
"table": "ConfigCASSave",
"file_name": file_name,
"name": "t_Форма",
"owner": {"status": "resolved", "kind": "Catalog", "name": "test2"},
"form": {"name": "t_Форма"},
"source": {"table": "ConfigCASSave", "file_name": file_name},
"matches": [
{
"name": "ЗаменаДомена",
"section": "commands",
"writable_properties": [
{
"property": "title",
"presentation": "Заголовок",
"value": "Замена домена",
"value_type": "string",
}
],
}
],
},
)
result = adapter_server.metadata_saved_state_forms_search(
{
"base_id": "upo_test",
"tables": ["ConfigCASSave"],
"limit": 1,
}
)
form = result["forms"][0]
assert form["selector"]["extension"] == "test2"
assert form["matches"][0]["selector"] == {
"extension": "test2",
"ref": "Catalog.test2",
"form": "t_Форма",
"qualified_name": "test2.t_Форма",
"command": "ЗаменаДомена",
}
assert extension_guid not in str(result)
assert "ConfigCASSave" not in str(result["forms"])
def test_saved_state_form_search_resolves_owner_for_internal_name_first_write(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_form_decode",
lambda payload: {
"status": "ok",
"source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
"form": {"file_name": payload["file_name"]},
"profile": {"items": [], "commands": []},
"counts": {"items": 0, "commands": 0},
},
)
monkeypatch.setattr(
adapter_server,
"saved_state_form_descriptor_identity",
lambda **kwargs: {"name": "t_Форма", "synonym": "T форма", "guid": "form-guid"},
)
monkeypatch.setattr(
adapter_server,
"saved_state_related_descriptor_identity",
lambda **kwargs: {
"kind": "Catalog",
"name": "test2",
"synonym": "Test2",
"guid": "owner-guid",
},
)
row = adapter_server.saved_state_form_search_row(
base_id="upo_test",
table="ConfigCASSave",
file_name="extension-guid__form-guid.0",
payload={"include_storage": True, "form": "t_Форма"},
timeout_seconds=30,
)
assert row is not None
assert row["owner"]["kind"] == "Catalog"
assert row["owner"]["name"] == "test2"
def test_saved_state_forms_search_help_exposes_owner_and_form_selectors() -> None:
method = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}["metadata.saved_state.forms.search"]
assert method["selector_capabilities"]["accepts_ordinal"] is False
assert method["selector_capabilities"]["read_only"] is True
assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name", "form_guid"]
assert method["selector_capabilities"]["child_selector_arguments"] == [
"element",
"element_name",
"command",
"attribute",
]
assert method["input_schema"]["properties"]["object_guid"]["type"] == "string"
assert method["input_schema"]["properties"]["form_guid"]["type"] == "string"
def test_access_graph_builds_effective_permissions_with_sources() -> None:
access = {
"users": [{"id": "u1", "name": "Иванов", "groups": ["g1"]}],
"groups": [{"id": "g1", "name": "Менеджеры продаж", "profiles": ["p1"]}],
"profiles": [{"id": "p1", "name": "Продажи", "roles": ["r1"]}],
"roles": [
{
"id": "r1",
"name": "ДобавлениеИзменениеЗаказовКлиентов",
"permissions": [{"object": "Document.ЗаказКлиента", "actions": ["read", "create"]}],
}
],
"data_restrictions": [{"subject_type": "group", "subject_id": "g1", "dimension": "Организация", "values": ["Ромашка ООО"]}],
}
result = call_method("access.graph.build", {"base_id": "upo_test", "access": access})
assert result["status"] == "ok"
assert {key: result["counts"][key] for key in ("users", "groups", "profiles", "roles", "data_restrictions", "effective_users")} == {
"users": 1,
"groups": 1,
"profiles": 1,
"roles": 1,
"data_restrictions": 1,
"effective_users": 1,
}
assert result["counts"]["effective_permissions_total"] == 2
effective = result["effective_users"][0]
assert effective["user"]["name"] == "Иванов"
assert effective["groups"] == ["g1"]
assert {item["action"] for item in effective["permissions"]} == {"read", "create"}
source = effective["permissions"][0]["sources"][0]
assert source["role"] == "r1"
assert source["chains"][0] == {"type": "group_profile_role", "group": "g1", "profile": "p1"}
assert effective["data_restrictions"][0]["dimension"] == "Организация"
def test_access_user_explain_filters_permissions() -> None:
access = {
"users": [{"id": "u1", "name": "Иванов", "groups": ["g1"]}],
"groups": [{"id": "g1", "profiles": ["p1"]}],
"profiles": [{"id": "p1", "roles": ["r1"]}],
"roles": [{"id": "r1", "permissions": [{"object": "Catalog.Контрагенты", "actions": ["read", "update"]}]}],
}
result = call_method(
"access.user.explain",
{"access": access, "user": "Иванов", "object": "Catalog.Контрагенты", "action": "read"},
)
assert result["status"] == "ok"
assert result["user"]["id"] == "u1"
assert len(result["permissions"]) == 1
assert result["permissions"][0]["object"] == "Catalog.Контрагенты"
assert result["permissions"][0]["action"] == "read"
assert result["permissions"][0]["sources"][0]["chains"][0]["profile"] == "p1"
def test_access_user_explain_normalizes_action_filter() -> None:
access = {
"users": [{"id": "u1", "name": "Иванов", "roles": ["r1"]}],
"roles": [{"id": "r1", "permissions": [{"object": "Catalog.Контрагенты", "actions": ["Просмотр", "Изменение"]}]}],
}
result = call_method(
"access.user.explain",
{"access": access, "user": "Иванов", "object": "Catalog.Контрагенты", "action": "read"},
)
assert result["status"] == "ok"
assert [permission["action"] for permission in result["permissions"]] == ["Просмотр"]
def test_access_user_explain_resolves_object_filter_from_name(monkeypatch: pytest.MonkeyPatch) -> None:
object_guid = "38cea62d-22bb-11ed-9fae-0050569db20c"
monkeypatch.setattr(
adapter_server,
"resolve_object_guid",
lambda payload, base_id, **kwargs: (
object_guid,
"InformationRegister",
{
"guid": object_guid,
"kind": "InformationRegister",
"kind_ru": "РегистрСведений",
"name": "УОП_АктуальныеСпецификации",
"synonym": "Актуальные рецептуры",
},
None,
),
)
access = {
"users": [{"id": "u1", "name": "Иванов", "roles": ["r1"]}],
"roles": [
{
"id": "r1",
"permissions": [
{
"object": "000000C4:AAA",
"object_name": "Актуальные рецептуры (Регистр сведений)",
"actions": ["Добавление", "Изменение"],
}
],
}
],
}
result = call_method(
"access.user.explain",
{
"base_id": "upo_test",
"access": access,
"user": "Иванов",
"object": "РегистрСведений.УОП_АктуальныеСпецификации",
"action": "write",
},
)
assert result["status"] == "ok"
assert [permission["object_name"] for permission in result["permissions"]] == ["Актуальные рецептуры (Регистр сведений)"]
def test_access_user_explain_reports_available_object_actions_when_requested_action_missing() -> None:
access = {
"users": [{"id": "u1", "name": "Иванов", "roles": ["r1"]}],
"roles": [
{
"id": "r1",
"permissions": [
{
"object": "000000C4:AAA",
"object_name": "Актуальные рецептуры (Регистр сведений)",
"actions": ["Чтение", "ЧтениеБезОграничения"],
}
],
}
],
}
result = call_method(
"access.user.explain",
{
"access": access,
"user": "Иванов",
"object": "Актуальные рецептуры",
"action": "write",
},
)
assert result["status"] == "ok"
assert result["permissions"] == []
assert result["available_actions_for_object"] == {"read": True, "insert": False, "update": False, "delete": False}
assert "other rights" in result["summary"]["hint"]
def test_access_user_explain_not_found_returns_nearest_users() -> None:
access = {
"users": [{"id": "u1", "name": "Lyubov.IVANOVA", "roles": []}, {"id": "u2", "name": "Ivan.PETROV", "roles": []}],
}
result = call_method("access.user.explain", {"access": access, "user": "Lyubov.EREMINA"})
assert result["status"] == "not_found"
assert result["nearest_users"][0]["name"] == "Lyubov.IVANOVA"
assert result["diagnostics"]["hint"].startswith("Use access.users.search")
def test_access_users_search_returns_matches_and_nearest(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"access_bsp_extractor_plan",
lambda base_id: (
{"users": "users_sql"},
{"users": {"id": "id", "name": "name", "active": "active", "marked": "marked", "user_type": "user_type"}},
{"preset": "bsp"},
),
)
monkeypatch.setattr(
adapter_server,
"access_rows_from_query",
lambda base_id, query, *, limit, timeout_seconds: (
[
{"id": "u1", "name": "Lyubov.IVANOVA", "active": True, "marked": False, "user_type": "user"},
{"id": "u2", "name": "Ivan.PETROV", "active": True, "marked": False, "user_type": "user"},
],
None,
False,
),
)
result = adapter_server.access_users_search({"base_id": "upo_test", "query": "Lyubov", "limit": 5})
assert result["status"] == "ok"
assert [user["name"] for user in result["users"]] == ["Lyubov.IVANOVA"]
assert result["counts"]["scanned"] == 2
assert result["user_kind"] == "bsp_catalog_user"
assert result["terminology"]["authoritative_for_platform_roles"] is False
assert result["terminology"]["default_user_method"] == "infobase.users.search"
def test_infobase_users_search_is_default_configurator_user_source(monkeypatch: pytest.MonkeyPatch) -> None:
users = [
adapter_server.infobase_user_public(
{
"id": "8fca349abe4d635d430f0c6c4e749dcd",
"name": "Администратор 1С",
"full_name": "Администратор 1С",
"changed": "2026-01-01T00:00:00",
"role_set_id": "4",
"visible_in_login_list": 1,
"standard_authentication_enabled": 1,
"os_authentication_configured": 0,
"email_configured": 0,
"platform_administrator": 1,
"protected_data_bytes": 495,
}
),
adapter_server.infobase_user_public(
{
"id": "a74272fec8cf262a405a568d7871fb07",
"name": "Аналитик",
"full_name": "Аналитик",
"role_set_id": "363",
"visible_in_login_list": 0,
"standard_authentication_enabled": 0,
"os_authentication_configured": 0,
"email_configured": 0,
"platform_administrator": 0,
"protected_data_bytes": 703,
}
),
]
monkeypatch.setattr(adapter_server, "infobase_users_read", lambda *args, **kwargs: (users, None))
result = call_method("infobase.users.search", {"base_id": "upo_test", "query": "Аналитик"})
assert result["status"] == "ok"
assert result["terminology"]["default_user_meaning"] == "infobase_user"
assert result["terminology"]["bsp_user_is_separate"] is True
assert result["source"]["table"] == "dbo.v8users"
assert result["users"][0]["name"] == "Аналитик"
assert result["users"][0]["role_set_id"] == "363"
assert result["users"][0]["role_assignment"]["exact_role_names_status"] == "runtime_required"
assert result["capabilities"]["password_hashes_exposed"] is False
assert "protected_data" not in result["users"][0]
def test_infobase_user_get_does_not_substitute_bsp_roles(monkeypatch: pytest.MonkeyPatch) -> None:
user = adapter_server.infobase_user_public(
{
"id": "8fca349abe4d635d430f0c6c4e749dcd",
"name": "Администратор 1С",
"full_name": "Администратор 1С",
"role_set_id": "4",
"platform_administrator": 1,
"protected_data_bytes": 495,
}
)
monkeypatch.setattr(adapter_server, "infobase_users_read", lambda *args, **kwargs: ([user], None))
result = call_method("infobase.user.get", {"base_id": "upo_test", "user": "Администратор 1С"})
assert result["status"] == "ok"
assert result["user"]["user_kind"] == "infobase_user"
assert result["user"]["platform_administrator"] is True
assert result["user"]["role_assignment"]["exact_role_names"] is None
assert result["bsp_correlation"]["authoritative_for_platform_roles"] is False
def test_infobase_user_password_capabilities_block_anonymous_mutations(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("ONEC_ADAPTER_SERVICE_TOKEN", raising=False)
monkeypatch.delenv("ONEC_INFOBASE_USER_ADMIN_ALLOW_UNAUTHENTICATED", raising=False)
monkeypatch.setattr(
adapter_server,
"infobase_user_admin_config_for_base",
lambda base_id: ({"url": "https://runtime.example", "token": "bridge-token"}, None),
)
result = call_method("infobase.user.password.capabilities", {"base_id": "upo_test"})
assert result["status"] == "blocked"
assert result["operations"] == {"status": False, "set": False, "clear": False}
assert result["blocker"]["status"] == "service_auth_required"
assert result["security"]["direct_sql_password_write"] is False
@pytest.mark.parametrize(
"standard_authentication_enabled,empty_hashes,expected_state,can_login_empty",
[(1, True, "empty", True), (1, False, "set", False), (0, True, "standard_authentication_disabled", False)],
)
def test_infobase_user_password_status_is_safe(
monkeypatch: pytest.MonkeyPatch,
standard_authentication_enabled: int,
empty_hashes: bool,
expected_state: str,
can_login_empty: bool,
) -> None:
platform_id = "b58c9366f69fa7f54b687267775eb46c"
hash_value = adapter_server.INFOBASE_EMPTY_PASSWORD_SHA1_BASE64 if empty_hashes else base64.b64encode(hashlib.sha1(b"set").digest()).decode("ascii")
monkeypatch.setattr(
adapter_server,
"infobase_user_get",
lambda payload: {
"status": "ok",
"user": {"id": platform_id.upper(), "name": "Администратор 1С УПО 3", "platform_administrator": True},
},
)
monkeypatch.setattr(
adapter_server,
"infobase_user_password_data_decode",
lambda data: {"password_pair": (12, 13), "values": ([None] * 12) + [hash_value, hash_value]},
)
class FakeCursor:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def execute(self, query: str, params: tuple[object, ...]) -> None:
pass
def fetchone(self) -> dict[str, object]:
return {
"Name": "Администратор 1С УПО 3",
"standard_authentication_enabled": standard_authentication_enabled,
"Data": b"protected",
}
class FakeConnection:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def cursor(self, *, as_dict: bool = False) -> FakeCursor:
assert as_dict is True
return FakeCursor()
def close(self) -> None:
pass
monkeypatch.setattr(
adapter_server,
"connect_live_sql",
lambda base_id, method, *, timeout_seconds: (FakeConnection(), {"database": "upo"}, None),
)
result = call_method("infobase.user.password.status", {"base_id": "upo", "user": "Администратор 1С УПО 3"})
assert result["status"] == "ok"
assert result["password_state"] == expected_state
assert result["can_login_with_empty_password"] is can_login_empty
assert result["security"] == {"password_hashes_exposed": False, "protected_data_exposed": False}
assert hash_value not in json.dumps(result, ensure_ascii=False)
def test_infobase_user_password_set_requires_service_auth(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("ONEC_ADAPTER_SERVICE_TOKEN", raising=False)
monkeypatch.delenv("ONEC_INFOBASE_USER_ADMIN_ALLOW_UNAUTHENTICATED", raising=False)
result = call_method(
"infobase.user.password.set",
{
"base_id": "upo_test",
"user": "Аналитик",
"confirm_user_id": "a74272fec8cf262a405a568d7871fb07",
"new_password": "temporary-test-password",
"allow_password_change": True,
},
)
assert result["status"] == "blocked"
assert result["error"] == "service_auth_required"
assert "temporary-test-password" not in json.dumps(result, ensure_ascii=False)
def test_infobase_user_password_set_uses_sql_without_echoing_secret(monkeypatch: pytest.MonkeyPatch) -> None:
platform_id = "a74272fec8cf262a405a568d7871fb07"
captured: dict[str, object] = {}
monkeypatch.delenv("ONEC_ADAPTER_SERVICE_TOKEN", raising=False)
monkeypatch.setenv("ONEC_INFOBASE_USER_ADMIN_ALLOW_UNAUTHENTICATED", "true")
monkeypatch.setattr(
adapter_server,
"infobase_user_get",
lambda payload: {
"status": "ok",
"user": {"id": platform_id.upper(), "name": "Аналитик", "platform_administrator": False},
},
)
def fake_sql_set(base_id: str, user: dict[str, object], *, new_password: str, request_id: str, timeout_seconds: int):
captured.update(
{"base_id": base_id, "user": user, "new_password": new_password, "request_id": request_id, "timeout_seconds": timeout_seconds}
)
return {
"status": "ok",
"applied": True,
"verification": {"only_current_password_hash_pair_changed": True},
}
monkeypatch.setattr(adapter_server, "infobase_user_password_set_sql", fake_sql_set)
result = call_method(
"infobase.user.password.set",
{
"base_id": "upo_test",
"user": "Аналитик",
"confirm_user_id": platform_id,
"new_password": "temporary-test-password",
"allow_password_change": True,
"request_id": "request-1",
},
)
assert result["status"] == "ok"
assert result["applied"] is True
assert captured["base_id"] == "upo_test"
assert captured["new_password"] == "temporary-test-password"
assert "temporary-test-password" not in json.dumps(result, ensure_ascii=False)
assert result["verification"]["only_current_password_hash_pair_changed"] is True
def test_infobase_user_password_clear_has_no_password_and_admin_extra_gate(monkeypatch: pytest.MonkeyPatch) -> None:
platform_id = "8fca349abe4d635d430f0c6c4e749dcd"
captured: dict[str, object] = {}
monkeypatch.setenv("ONEC_ADAPTER_SERVICE_TOKEN", "adapter-service-token")
monkeypatch.setattr(
adapter_server,
"infobase_user_get",
lambda payload: {
"status": "ok",
"user": {"id": platform_id, "name": "Администратор 1С", "platform_administrator": True},
},
)
blocked = call_method(
"infobase.user.password.clear",
{
"base_id": "upo_test",
"user": "Администратор 1С",
"confirm_user_id": platform_id,
"allow_password_clear": True,
},
)
assert blocked["status"] == "invalid_argument"
assert blocked["argument"] == "allow_administrator_password_change"
def fake_sql_clear(base_id: str, user: dict[str, object], *, request_id: str, timeout_seconds: int):
captured.update({"base_id": base_id, "user": user, "request_id": request_id, "timeout_seconds": timeout_seconds})
return {"status": "ok", "operation": "clear", "applied": True}
monkeypatch.setattr(adapter_server, "infobase_user_password_clear_sql", fake_sql_clear)
result = call_method(
"infobase.user.password.clear",
{
"base_id": "upo_test",
"user": "Администратор 1С",
"confirm_user_id": platform_id,
"allow_password_clear": True,
"allow_administrator_password_change": True,
},
)
assert result["status"] == "ok"
assert captured["base_id"] == "upo_test"
assert captured["user"]["id"] == platform_id
assert "new_password" not in captured
@pytest.mark.parametrize("field_count,pair_start", [(31, 11), (34, 11), (33, 12), (38, 12), (40, 12), (43, 12)])
def test_infobase_user_password_data_clear_preserves_container(field_count: int, pair_start: int) -> None:
old_hash = base64.b64encode(hashlib.sha1(b"old-test-password").digest()).decode("ascii")
fields = ["0"] * field_count
fields[7] = "1"
fields[pair_start] = json.dumps(old_hash)
fields[pair_start + 1] = json.dumps(old_hash)
text = "{" + ",".join(fields) + "}"
key = bytes(range(1, 21))
plain = b"\xef\xbb\xbf" + text.encode("utf-8") + b"\x00"
data = bytes([len(key)]) + key + bytes(value ^ key[index % len(key)] for index, value in enumerate(plain))
new_data, details = adapter_server.infobase_user_password_data_clear(data)
decoded = adapter_server.infobase_user_password_data_decode(new_data)
assert len(new_data) == len(data)
assert details["already_clear"] is False
assert details["password_pair"] == [pair_start, pair_start + 1]
assert decoded["values"][pair_start : pair_start + 2] == [
adapter_server.INFOBASE_EMPTY_PASSWORD_SHA1_BASE64,
adapter_server.INFOBASE_EMPTY_PASSWORD_SHA1_BASE64,
]
assert decoded["values"][:pair_start] == adapter_server.infobase_user_password_data_decode(data)["values"][:pair_start]
def test_infobase_user_password_data_clear_is_idempotent() -> None:
empty_hash = adapter_server.INFOBASE_EMPTY_PASSWORD_SHA1_BASE64
fields = ["0"] * 38
fields[7] = "1"
fields[12] = json.dumps(empty_hash)
fields[13] = json.dumps(empty_hash)
text = "{" + ",".join(fields) + "}"
key = b"test-password-key"
plain = b"\xef\xbb\xbf" + text.encode("utf-8") + b"\x00"
data = bytes([len(key)]) + key + bytes(value ^ key[index % len(key)] for index, value in enumerate(plain))
new_data, details = adapter_server.infobase_user_password_data_clear(data)
assert new_data == data
assert details["already_clear"] is True
def test_infobase_user_password_data_set_writes_normal_and_uppercase_hashes() -> None:
old_hash = base64.b64encode(hashlib.sha1(b"old").digest()).decode("ascii")
fields = ["0"] * 39
fields[7] = "1"
fields[12] = json.dumps(old_hash)
fields[13] = json.dumps(old_hash)
text = "{" + ",".join(fields) + "}"
key = b"set-password-key"
plain = b"\xef\xbb\xbf" + text.encode("utf-8") + b"\x00"
data = bytes([len(key)]) + key + bytes(value ^ key[index % len(key)] for index, value in enumerate(plain))
new_data, details = adapter_server.infobase_user_password_data_set(data, "TestПароль")
decoded = adapter_server.infobase_user_password_data_decode(new_data)
expected = adapter_server.infobase_user_password_hash_pair("TestПароль")
assert details["already_applied"] is False
assert tuple(decoded["values"][12:14]) == expected
assert expected[0] != expected[1]
assert len(new_data) == len(data)
def test_infobase_user_password_clear_sql_commits_verified_single_row(monkeypatch: pytest.MonkeyPatch) -> None:
platform_id = "a74272fec8cf262a405a568d7871fb07"
old_hash = base64.b64encode(hashlib.sha1(b"old-test-password").digest()).decode("ascii")
fields = ["0"] * 38
fields[7] = "1"
fields[12] = json.dumps(old_hash)
fields[13] = json.dumps(old_hash)
text = "{" + ",".join(fields) + "}"
key = b"transaction-test-key"
plain = b"\xef\xbb\xbf" + text.encode("utf-8") + b"\x00"
old_data = bytes([len(key)]) + key + bytes(value ^ key[index % len(key)] for index, value in enumerate(plain))
class FakeCursor:
def __init__(self) -> None:
self.mode = ""
self.rowcount = -1
self.new_data = b""
def execute(self, query: str, params: tuple[object, ...]) -> None:
normalized = " ".join(query.split()).casefold()
if normalized.startswith("select id"):
self.mode = "original"
elif normalized.startswith("update dbo.v8users"):
self.mode = "update"
self.new_data = bytes(params[0])
self.rowcount = 1
elif normalized.startswith("select data"):
self.mode = "readback"
def fetchone(self) -> dict[str, object]:
if self.mode == "original":
return {
"ID": bytes.fromhex(platform_id),
"Name": "Аналитик",
"standard_authentication_enabled": 1,
"Data": old_data,
}
return {"Data": self.new_data}
class FakeConnection:
def __init__(self) -> None:
self.cursor_instance = FakeCursor()
self.committed = False
self.rolled_back = False
self.closed = False
def cursor(self, *, as_dict: bool = False) -> FakeCursor:
assert as_dict is True
return self.cursor_instance
def commit(self) -> None:
self.committed = True
def rollback(self) -> None:
self.rolled_back = True
def close(self) -> None:
self.closed = True
connection = FakeConnection()
monkeypatch.setattr(
adapter_server,
"connect_live_sql",
lambda base_id, method, *, timeout_seconds: (connection, {"database": "upo_test"}, None),
)
result = adapter_server.infobase_user_password_clear_sql(
"upo_test",
{"id": platform_id.upper(), "name": "Аналитик"},
request_id="request-1",
timeout_seconds=30,
)
assert result["status"] == "ok"
assert result["applied"] is True
assert result["source"]["kind"] == "live_sql"
assert result["verification"]["only_current_password_hash_pair_changed"] is True
assert connection.committed is True
assert connection.rolled_back is False
assert connection.closed is True
def test_infobase_user_password_write_history_never_stores_clear_text_secret(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
platform_id = "b58c9366f69fa7f54b687267775eb46c"
secret = "must-never-enter-history"
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
monkeypatch.setenv("ONEC_INFOBASE_USER_ADMIN_ALLOW_UNAUTHENTICATED", "true")
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "sql.example", "database": base_id}, None))
monkeypatch.setattr(
adapter_server,
"infobase_user_password_change",
lambda payload, *, operation: {
"schema": "onec_infobase_user_password_change.v1",
"status": "ok",
"base_id": payload["base_id"],
"operation": operation,
"applied": True,
"target": {"id": platform_id.upper(), "name": "Администратор 1С УПО 3"},
"source": {"kind": "live_sql", "table": "dbo.v8users", "column": "Data"},
},
)
result = adapter_server.call_method(
"infobase.user.password.set",
{
"base_id": "upo",
"user": "Администратор 1С УПО 3",
"confirm_user_id": platform_id,
"new_password": secret,
"allow_password_change": True,
"allow_administrator_password_change": True,
},
)
assert result["operation_id"]
history = adapter_server.call_method("metadata.write.history", {"base_id": "upo", "operation_id": result["operation_id"]})
serialized = json.dumps(history, ensure_ascii=False)
assert secret not in serialized
assert history["operations"][0]["method"] == "infobase.user.password.set"
assert history["operations"][0]["target_summary"]["target"] == {
"id": platform_id.upper(),
"name": "Администратор 1С УПО 3",
}
assert history["operations"][0]["target_summary"]["transport"] == "sql_dbo_v8users_data"
def test_access_user_explain_can_use_live_bsp_extract(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_extract(payload: dict[str, Any]) -> dict[str, Any]:
assert payload["preset"] == "bsp"
assert payload["limit"] == 20000
assert payload["max_effective_permissions_per_user"] == 20000
return {
"status": "ok",
"counts": {"effective_users": 1},
"diagnostics": {"extractors": {"users": {"rows": 1}}},
"graph": {
"source": {"kind": "access_snapshot"},
"effective_users": [
{
"user": {"id": "0000015C:u1", "name": "Иванов", "active": True, "service": False, "administrator": False},
"groups": ["g1"],
"roles": [{"id": "r1", "name": "Роль", "sources": []}],
"permissions": [{"object": "Catalog.Контрагенты", "object_name": "Контрагенты", "action": "Просмотр", "sources": []}],
"access_keys": {"counts": {"group_keys": 2, "user_keys": 1, "total": 3}, "samples": {"group_keys": [], "user_keys": []}},
"data_restrictions": [],
}
],
},
}
monkeypatch.setattr(adapter_server, "access_snapshot_extract", fake_extract)
result = call_method("access.user.explain", {"base_id": "upo_test", "user": "u1", "object": "Контрагенты"})
assert result["status"] == "ok"
assert result["source"] == {"kind": "live_sql", "preset": "bsp", "extraction": "access.snapshot.extract"}
assert result["user"]["name"] == "Иванов"
assert result["counts"]["access_keys"] == 3
assert result["permissions"][0]["object_name"] == "Контрагенты"
assert result["extraction_counts"] == {"effective_users": 1}
def test_access_snapshot_from_extractor_rows_builds_graph() -> None:
rows_by_area = {
"users": [{"UserRef": "u1", "UserName": "Иванов"}],
"groups": [{"GroupRef": "g1", "GroupName": "Менеджеры"}],
"profiles": [{"ProfileRef": "p1", "ProfileName": "Продажи"}],
"roles": [{"RoleRef": "r1", "RoleName": "ЧтениеКонтрагентов"}],
"group_users": [{"GroupRef": "g1", "UserRef": "u1"}],
"group_profiles": [{"GroupRef": "g1", "ProfileRef": "p1"}],
"profile_roles": [{"ProfileRef": "p1", "RoleRef": "r1"}],
"role_permissions": [{"RoleRef": "r1", "ObjectRef": "Catalog.Контрагенты", "Action": "read"}],
}
mappings = {
"users": {"id": "UserRef", "name": "UserName"},
"groups": {"id": "GroupRef", "name": "GroupName"},
"profiles": {"id": "ProfileRef", "name": "ProfileName"},
"roles": {"id": "RoleRef", "name": "RoleName"},
"group_users": {"group": "GroupRef", "user": "UserRef"},
"group_profiles": {"group": "GroupRef", "profile": "ProfileRef"},
"profile_roles": {"profile": "ProfileRef", "role": "RoleRef"},
"role_permissions": {"role": "RoleRef", "object": "ObjectRef", "action": "Action"},
}
access = adapter_server.access_snapshot_from_extractor_rows(rows_by_area, mappings)
graph = adapter_server.build_access_graph_from_snapshot(access, base_id="upo_test")
assert access["groups"][0]["users"] == ["u1"]
assert access["profiles"][0]["roles"] == ["r1"]
assert graph["effective_users"][0]["permissions"][0]["object"] == "Catalog.Контрагенты"
assert graph["effective_users"][0]["permissions"][0]["sources"][0]["chains"][0]["group"] == "g1"
def test_access_snapshot_expands_user_group_members() -> None:
rows_by_area = {
"users": [{"id": "u1", "name": "Иванов"}, {"id": "ug1", "name": "Отдел продаж"}],
"groups": [{"id": "g1", "name": "Доступ продажи"}],
"profiles": [{"id": "p1", "name": "Продажи"}],
"roles": [{"id": "r1", "name": "Чтение"}],
"group_users": [{"group": "g1", "user": "ug1"}],
"user_group_members": [{"group": "ug1", "user": "u1"}],
"group_profiles": [{"group": "g1", "profile": "p1"}],
"profile_roles": [{"profile": "p1", "role": "r1"}],
"role_permissions": [{"role": "r1", "object": "Catalog.Контрагенты", "action": "read"}],
}
access = adapter_server.access_snapshot_from_extractor_rows(rows_by_area, {})
graph = adapter_server.build_access_graph_from_snapshot(access, base_id="upo_test")
users = {item["user"]["id"]: item for item in graph["effective_users"]}
assert "u1" in users
assert "g1" in users["u1"]["groups"]
assert users["u1"]["permissions"][0]["object"] == "Catalog.Контрагенты"
def test_access_role_permission_actions_split_into_effective_permissions() -> None:
rows_by_area = {
"users": [{"id": "u1", "name": "Иванов"}],
"groups": [{"id": "g1", "name": "Доступ продажи"}],
"profiles": [{"id": "p1", "name": "Продажи"}],
"roles": [{"id": "r1", "name": "Роль"}],
"group_users": [{"group": "g1", "user": "u1"}],
"group_profiles": [{"group": "g1", "profile": "p1"}],
"profile_roles": [{"profile": "p1", "role": "r1"}],
"role_permissions": [{"role": "r1", "object": "Document.ЗаказКлиента", "actions": "Добавление,Изменение,", "source_fields": "_Fld27940,_Fld27941,"}],
}
access = adapter_server.access_snapshot_from_extractor_rows(rows_by_area, {})
graph = adapter_server.build_access_graph_from_snapshot(access, base_id="upo_test")
permissions = graph["effective_users"][0]["permissions"]
assert {permission["action"] for permission in permissions} == {"Добавление", "Изменение"}
assert permissions[0]["sources"][0]["role"] == "r1"
def test_access_graph_limits_effective_permissions_per_user() -> None:
access = {
"users": [{"id": "u1", "name": "Иванов", "roles": ["r1"]}],
"roles": [
{
"id": "r1",
"permissions": [
{"object": "Catalog.A", "action": "read"},
{"object": "Catalog.B", "action": "read"},
{"object": "Catalog.C", "action": "read"},
],
}
],
}
graph = adapter_server.build_access_graph_from_snapshot(access, base_id="upo_test", max_permissions_per_user=2)
user = graph["effective_users"][0]
assert len(user["permissions"]) == 2
assert user["permission_counts"] == {"total": 3, "returned": 2, "truncated": True}
assert graph["counts"]["effective_permissions_total"] == 3
assert graph["counts"]["effective_permissions_returned"] == 2
assert graph["counts"]["effective_users_permissions_truncated"] == 1
def test_access_graph_enriches_resolved_access_identifiers(monkeypatch: pytest.MonkeyPatch) -> None:
role_id = "000000C4:9FAE0050569DB20C11ED22BB38CEA4C6"
object_id = "000000C4:9FAE0050569DB20C11ED22BB38CEA62D"
def fake_resolve(config: dict[str, str] | None, value: str) -> dict[str, Any] | None:
if value == role_id:
return {"guid": "38cea4c6-22bb-11ed-9fae-0050569db20c", "kind": "Role", "name": "МенеджерПродаж"}
if value == object_id:
return {
"guid": "38cea62d-22bb-11ed-9fae-0050569db20c",
"kind": "Catalog",
"name": "Контрагенты",
"full_name": "Справочник.Контрагенты",
}
return None
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "s", "database": "d"}, None))
monkeypatch.setattr(adapter_server, "access_identifier_payload_from_index", fake_resolve)
graph = adapter_server.build_access_graph_from_snapshot(
{
"users": [{"id": "u1", "name": "User"}],
"roles": [{"id": role_id, "permissions": [{"object": object_id, "action": "Просмотр"}]}],
"assignments": [{"subject": "u1", "roles": [role_id]}],
},
base_id="upo_test",
resolve_identifiers=True,
)
role = graph["effective_users"][0]["roles"][0]
permission = graph["effective_users"][0]["permissions"][0]
assert role["name"] == "МенеджерПродаж"
assert permission["object_name"] == "Контрагенты"
assert permission["object_full_name"] == "Справочник.Контрагенты"
assert graph["diagnostics"]["identifier_resolution"]["roles"]["resolved"] == 1
assert graph["diagnostics"]["identifier_resolution"]["objects"]["resolved"] == 1
def test_access_schema_candidate_score_finds_access_tables() -> None:
score, reasons = adapter_server.access_schema_candidate_score(
"_Reference.ПрофилиГруппДоступа",
["_IDRRef", "_Description", "Роль", "ПравоДоступа"],
)
assert score > 0
assert {"profiles", "roles", "permissions"}.issubset(set(reasons))
def test_access_bsp_extractor_plan_builds_expected_vt_tables(monkeypatch: pytest.MonkeyPatch) -> None:
def profile(sql_number: int, sections: list[dict[str, Any]] | None = None) -> dict[str, Any]:
return {
"status": "ok",
"object": {"storage": {"dbnames": [{"sql_number": sql_number}]}},
"tabular_sections": sections or [],
}
fake_profiles = {
("Catalog", "Пользователи"): profile(348),
("Catalog", "ВнешниеПользователи"): profile(141),
("Catalog", "ГруппыДоступа"): profile(
146,
[{"name": "Пользователи", "columns": [{"name": "Пользователь", "storage_routes": [{"sql_number": 14130}]}]}],
),
("Catalog", "ГруппыПользователей"): profile(
149,
[{"name": "Состав", "columns": [{"name": "Пользователь", "storage_routes": [{"sql_number": 14151}]}]}],
),
("Catalog", "ПрофилиГруппДоступа"): profile(
376,
[{"name": "Роли", "columns": [{"name": "Роль", "storage_routes": [{"sql_number": 19417}]}]}],
),
("Catalog", "ИдентификаторыОбъектовМетаданных"): profile(196),
("Catalog", "КлючиДоступа"): profile(226),
("Catalog", "НаборыГруппДоступа"): profile(259),
("InformationRegister", "ПраваРолей"): profile(27937),
("InformationRegister", "КлючиДоступаГруппДоступа"): profile(25996),
("InformationRegister", "КлючиДоступаПользователей"): profile(26025),
("InformationRegister", "КлючиДоступаКОбъектам"): profile(26003),
("InformationRegister", "КлючиДоступаНаборовГруппДоступа"): profile(26018),
}
monkeypatch.setattr(adapter_server, "metadata_object_attributes", lambda payload: fake_profiles[(payload["kind"], payload["name"])])
def fake_existing_tables(base_id: str, table_prefix: str, timeout_seconds: int = 30) -> list[str]:
if table_prefix == "_Reference348":
return ["_Reference348", "_Reference348X1"]
if table_prefix == "_Reference196":
return ["_Reference196X1"]
return [table_prefix]
monkeypatch.setattr(adapter_server, "access_sql_existing_tables", fake_existing_tables)
queries, mappings, diagnostics = adapter_server.access_bsp_extractor_plan("upo_test")
assert queries is not None
assert mappings is not None
tables = diagnostics["metadata"]["tables"]
assert tables["group_users"] == "_Reference146_VT14128"
assert tables["profile_roles"] == "_Reference376_VT19415"
assert tables["user_group_members"] == "_Reference149_VT14149"
assert tables["role_permissions"] == "_InfoRg27937"
assert tables["access_group_keys"] == "_InfoRg25996"
assert tables["access_user_keys"] == "_InfoRg26025"
assert tables["access_object_keys"] == "_InfoRg26003"
assert "FROM dbo.[_Reference376_VT19415]" in queries["profile_roles"]
assert "FROM dbo.[_Reference348X1]" in queries["users"]
assert "AS marked" in queries["users"]
assert "AS user_type" in queries["users"]
assert "AS service" in queries["users"]
assert mappings["users"]["marked"] == "marked"
assert mappings["users"]["user_type"] == "user_type"
assert "FROM dbo.[_InfoRg27937]" in queries["role_permissions"]
assert "LEFT JOIN (SELECT _IDRRef, _Description FROM dbo.[_Reference196X1]" in queries["role_permissions"]
assert "FROM dbo.[_InfoRg25996]" in queries["access_group_keys"]
assert "AS group_ref" in queries["access_group_keys"]
assert "AS [user]" in queries["access_user_keys"]
assert "AS object_sql_number" in queries["access_object_keys"]
assert "AS object_id" in queries["access_object_keys"]
assert mappings["role_permissions"]["source_fields"] == "source_fields"
assert mappings["role_permissions"]["object_name"] == "object_name"
assert mappings["access_group_keys"]["access_key"] == "access_key"
assert mappings["access_user_keys"]["user"] == "user"
assert "Добавление" in queries["role_permissions"]
assert tables["metadata_identifiers"] == ["_Reference196X1"]
assert diagnostics["metadata"]["permission_action_labels"]["mapping"]["_Fld27940"] == "Добавление"
def test_access_snapshot_extract_defaults_to_bsp_preset(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"access_bsp_extractor_plan",
lambda base_id: (
{"users": "SELECT N'u1' AS id, N'Иванов' AS name"},
{"users": {"id": "id", "name": "name"}},
{"preset": "bsp"},
),
)
monkeypatch.setattr(
adapter_server,
"access_rows_from_query",
lambda base_id, query, *, limit, timeout_seconds: ([{"id": "u1", "name": "Иванов"}], None, False),
)
monkeypatch.setattr(
adapter_server,
"build_access_graph_from_snapshot",
lambda access, **kwargs: {"counts": {"users": len(access.get("users") or [])}, "effective_users": []},
)
result = adapter_server.access_snapshot_extract({"base_id": "upo", "limit": 10})
assert result["status"] == "ok"
assert result["preset"] == "bsp"
assert result["counts"]["users"] == 1
def test_access_keys_query_filters_group_keys(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
monkeypatch.setattr(
adapter_server,
"access_bsp_extractor_plan",
lambda base_id: (
{"access_group_keys": "SELECT N'g1' AS [group], N'Продажи' AS group_name, N'k1' AS access_key"},
{"access_group_keys": {"group": "group", "group_name": "group_name", "access_key": "access_key"}},
{"preset": "bsp"},
),
)
def fake_rows(base_id: str, query: str, *, limit: int, timeout_seconds: int) -> tuple[list[dict[str, Any]], None, bool]:
captured["query"] = query
captured["limit"] = limit
return [{"group": "g1", "group_name": "Продажи", "access_key": "k1"}], None, False
monkeypatch.setattr(adapter_server, "access_rows_from_query", fake_rows)
result = adapter_server.access_keys_query({"base_id": "upo_test", "kind": "group", "group": "g1", "limit": 10, "offset": 20})
assert result["status"] == "ok"
assert result["areas"]["access_group_keys"]["rows"] == [{"group": "g1", "group_name": "Продажи", "access_key": "k1"}]
assert "q.[group] = N'g1'" in captured["query"]
assert "OFFSET 20 ROWS FETCH NEXT 11 ROWS ONLY" in captured["query"]
assert captured["limit"] == 11
def test_access_object_record_filters_resolve_public_names_without_consuming_query_kind(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_schema(payload: dict[str, Any]) -> dict[str, Any]:
seen["schema_payload"] = payload
return {
"status": "ok",
"object": {
"kind": "Catalog",
"name": "Номенклатура",
"guid": "11111111-1111-1111-1111-111111111111",
},
"table": {"name": "_Reference106", "row_kind": "Catalog"},
}
monkeypatch.setattr(adapter_server, "data_object_schema", fake_schema)
normalized, resolution, error = adapter_server.normalize_access_object_record_filters(
{
"base_id": "upo_test",
"kind": "object",
"object_ref": "Справочники.Номенклатура",
"record_ref": "00112233-4455-6677-8899-aabbccddeeff",
},
"access.keys.query",
"upo_test",
timeout_seconds=60,
)
assert error is None
assert normalized["kind"] == "object"
assert normalized["object_sql_number"] == 106
assert normalized["object_id"] == "00112233445566778899AABBCCDDEEFF"
assert seen["schema_payload"]["object_ref"] == "Справочники.Номенклатура"
assert "kind" not in seen["schema_payload"]
assert resolution["metadata_object"]["ref"] == "Catalog.Номенклатура"
assert resolution["data_record"]["record_ref"] == "00112233445566778899AABBCCDDEEFF"
def test_access_keys_query_maps_object_ref_and_record_ref_to_bsp_filters(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
record_ref = "00112233445566778899AABBCCDDEEFF"
monkeypatch.setattr(
adapter_server,
"data_object_schema",
lambda payload: {
"status": "ok",
"object": {"kind": "Catalog", "name": "Номенклатура"},
"table": {"name": "_Reference106"},
},
)
monkeypatch.setattr(
adapter_server,
"access_bsp_extractor_plan",
lambda base_id: (
{
"access_object_keys": (
"SELECT N'x' AS object, N'0000006A' AS object_type_code, "
"106 AS object_sql_number, N'x' AS object_id, N'k1' AS access_key"
)
},
{
"access_object_keys": {
"object": "object",
"object_type_code": "object_type_code",
"object_sql_number": "object_sql_number",
"object_id": "object_id",
"access_key": "access_key",
}
},
{"preset": "bsp"},
),
)
def fake_rows(base_id: str, query: str, *, limit: int, timeout_seconds: int) -> tuple[list[dict[str, Any]], None, bool]:
captured["query"] = query
return [], None, False
monkeypatch.setattr(adapter_server, "access_rows_from_query", fake_rows)
result = adapter_server.access_keys_query(
{
"base_id": "upo_test",
"kind": "object",
"object_ref": "Справочники.Номенклатура",
"record_ref": record_ref,
}
)
assert result["status"] == "ok"
assert "q.[object_sql_number] = 106" in captured["query"]
assert f"q.[object_id] = N'{record_ref}'" in captured["query"]
assert result["selector_resolution"]["metadata_object"]["name"] == "Номенклатура"
assert result["areas"]["access_object_keys"]["filters"]["object_ref"] == "Справочники.Номенклатура"
assert result["areas"]["access_object_keys"]["filters"]["record_ref"] == record_ref
def test_access_object_record_filters_reject_conflicting_legacy_id(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"data_object_schema",
lambda payload: {
"status": "ok",
"object": {"kind": "Document", "name": "ЗаказКлиента"},
"table": {"name": "_Document422"},
},
)
_, _, error = adapter_server.normalize_access_object_record_filters(
{
"object_ref": "Документы.ЗаказКлиента",
"object_sql_number": 421,
},
"access.object_keys.resolve",
"upo_test",
timeout_seconds=60,
)
assert error is not None
assert error["status"] == "invalid_argument"
assert error["argument"] == "object_sql_number"
def test_access_key_help_and_mcp_schemas_distinguish_object_and_record_refs() -> None:
methods = {row["name"]: row for row in adapter_server.call_method("help.methods", {}).get("methods") or []}
mcp_tools = {row["name"]: row for row in adapter_mcp.TOOLS}
for method in ("access.keys.query", "access.object_keys.resolve", "access.object.explain"):
properties = methods[method]["input_schema"]["properties"]
assert "object_ref" in properties
assert "record_ref" in properties
assert "not a 1C metadata kind" in methods["access.keys.query"]["input_schema"]["properties"]["kind"]["description"]
for tool in ("access_keys_query", "access_object_keys_resolve", "access_object_explain"):
properties = mcp_tools[tool]["inputSchema"]["properties"]
assert "object_ref" in properties
assert "record_ref" in properties
def test_access_object_roles_resolves_name_to_guid_and_filters_roles(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"resolve_object_guid",
lambda payload, base_id, **kwargs: (
"38cea62d-22bb-11ed-9fae-0050569db20c",
"InformationRegister",
{
"guid": "38cea62d-22bb-11ed-9fae-0050569db20c",
"kind": "InformationRegister",
"kind_ru": "Регистр сведений",
"name": "УОП_АктуальныеСпецификации",
"synonym": "Актуальные рецептуры",
},
None,
),
)
monkeypatch.setattr(adapter_server, "access_object_roles_fast_permissions", lambda *args, **kwargs: {"status": "error"})
monkeypatch.setattr(
adapter_server,
"access_snapshot_extract",
lambda payload: {
"status": "ok",
"counts": {"roles": 3},
"diagnostics": {"preset": "bsp"},
"access": {
"roles": [
{
"id": "r1",
"name": "Чтение рецептур",
"permissions": [
{
"object": "000000C4:AAA",
"action": "Просмотр",
"object_name": "Актуальные рецептуры (Регистр сведений)",
}
],
},
{
"id": "r2",
"name": "Изменение рецептур",
"permissions": [
{
"object": "000000C4:BBB",
"actions": "Добавление,ИзменениеБезОграничения,",
"object_name": "Актуальные рецептуры (Регистр сведений)",
}
],
},
{
"id": "r3",
"name": "Другая роль",
"permissions": [
{
"object": "000000C4:CCC",
"action": "Просмотр",
"object_name": "Другой регистр (Регистр сведений)",
"object_resolution": {
"guid": "11111111-1111-1111-1111-111111111111",
"kind": "InformationRegister",
"kind_ru": "Регистр сведений",
"name": "ДругойРегистр",
"full_name": "InformationRegister.ДругойРегистр",
},
}
],
},
]
},
},
)
result = adapter_server.access_object_roles({"base_id": "upo_test", "ref": "РегистрСведений.УОП_АктуальныеСпецификации"})
assert result["status"] == "ok"
assert result["object"]["guid"] == "38cea62d-22bb-11ed-9fae-0050569db20c"
assert [item["id"] for item in result["roles"]] == ["r2", "r1"]
assert result["roles"][0]["rights"] == {"read": False, "insert": True, "update": True, "delete": False}
assert result["roles"][0]["rights_detail"]["insert"] == {"allowed": True, "unrestricted": False}
assert result["roles"][0]["rights_detail"]["update"] == {"allowed": True, "unrestricted": True}
assert result["roles"][1]["rights"] == {"read": True, "insert": False, "update": False, "delete": False}
assert result["roles"][1]["rights_detail"]["read"] == {"allowed": True, "unrestricted": False}
def test_access_object_roles_filters_action(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"resolve_object_guid",
lambda payload, base_id, **kwargs: (
"38cea62d-22bb-11ed-9fae-0050569db20c",
"InformationRegister",
{"guid": "38cea62d-22bb-11ed-9fae-0050569db20c", "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации"},
None,
),
)
monkeypatch.setattr(adapter_server, "access_object_roles_fast_permissions", lambda *args, **kwargs: {"status": "error"})
monkeypatch.setattr(
adapter_server,
"access_snapshot_extract",
lambda payload: {
"status": "ok",
"counts": {"roles": 2},
"diagnostics": {"preset": "bsp"},
"access": {
"roles": [
{
"id": "r1",
"name": "Чтение рецептур",
"permissions": [
{
"object": "000000C4:AAA",
"action": "Просмотр",
"object_resolution": {"guid": "38cea62d-22bb-11ed-9fae-0050569db20c", "kind_ru": "РегистрСведений", "name": "УОП_АктуальныеСпецификации"},
}
],
},
{
"id": "r2",
"name": "Изменение рецептур",
"permissions": [
{
"object": "000000C4:BBB",
"action": "Изменение",
"object_resolution": {"guid": "38cea62d-22bb-11ed-9fae-0050569db20c", "kind_ru": "РегистрСведений", "name": "УОП_АктуальныеСпецификации"},
}
],
},
]
},
},
)
result = adapter_server.access_object_roles(
{"base_id": "upo_test", "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации", "action": "read"}
)
assert result["status"] == "ok"
assert [item["id"] for item in result["roles"]] == ["r1"]
assert result["roles"][0]["rights"]["read"] is True
def test_access_object_roles_fast_permissions_filters_sql_rows(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, object] = {}
monkeypatch.setattr(
adapter_server,
"access_bsp_extractor_plan",
lambda base_id: (
{"role_permissions": "SELECT role, object, role_name, object_name, actions, source_fields FROM rights"},
{"role_permissions": {"role": "role", "object": "object", "role_name": "role_name", "object_name": "object_name", "actions": "actions", "source_fields": "source_fields"}},
{"preset": "bsp"},
),
)
def fake_rows(base_id: str, query: str, *, limit: int, timeout_seconds: int) -> tuple[list[dict[str, object]], None, bool]:
captured["query"] = query
return (
[
{
"role": "r1",
"object": "000000C4:AAA",
"role_name": "УПО. Добавление изменение актуальных спецификаций (Роль)",
"object_name": "Актуальные рецептуры (Регистр сведений)",
"actions": "Добавление,ИзменениеБезОграничения,",
"source_fields": "_Fld27940,_Fld27943,",
},
{
"role": "r2",
"object": "000000C4:BBB",
"role_name": "Другая роль",
"object_name": "Другой регистр (Регистр сведений)",
"actions": "ИзменениеБезОграничения,",
"source_fields": "_Fld27943,",
},
],
None,
False,
)
monkeypatch.setattr(adapter_server, "access_rows_from_query", fake_rows)
result = adapter_server.access_object_roles_fast_permissions(
"upo_test",
object_payload={"name": "УОП_АктуальныеСпецификации", "synonym": "Актуальные рецептуры"},
object_selector="InformationRegister.УОП_АктуальныеСпецификации",
object_guid="38cea62d-22bb-11ed-9fae-0050569db20c",
action_filter_keys={"insert", "update"},
timeout_seconds=30,
limit=10,
)
assert result["status"] == "ok"
assert "Актуальные рецептуры" in str(captured["query"])
assert [item["name"] for item in result["roles"]] == ["УПО. Добавление изменение актуальных спецификаций (Роль)"]
assert result["roles"][0]["rights"] == {"read": False, "insert": True, "update": True, "delete": False}
def test_access_object_roles_fast_permissions_does_not_treat_read_flags_as_write(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"access_bsp_extractor_plan",
lambda base_id: (
{"role_permissions": "SELECT role, object, role_name, object_name, actions, source_fields FROM rights"},
{"role_permissions": {"role": "role", "object": "object", "role_name": "role_name", "object_name": "object_name", "actions": "actions", "source_fields": "source_fields"}},
{"preset": "bsp"},
),
)
def fake_rows(base_id: str, query: str, *, limit: int, timeout_seconds: int) -> tuple[list[dict[str, object]], None, bool]:
return (
[
{
"role": "r1",
"object": "000000C4:AAA",
"role_name": "УПО. Чтение актуальных спецификаций (Роль)",
"object_name": "Актуальные рецептуры (Регистр сведений)",
"actions": "Чтение,ЧтениеБезОграничения,",
"source_fields": "_Fld27942,_Fld27945,",
}
],
None,
False,
)
monkeypatch.setattr(adapter_server, "access_rows_from_query", fake_rows)
write_result = adapter_server.access_object_roles_fast_permissions(
"upo_test",
object_payload={"name": "УОП_АктуальныеСпецификации", "synonym": "Актуальные рецептуры"},
object_selector="InformationRegister.УОП_АктуальныеСпецификации",
object_guid="38cea62d-22bb-11ed-9fae-0050569db20c",
action_filter_keys={"insert", "update"},
timeout_seconds=30,
limit=10,
)
read_result = adapter_server.access_object_roles_fast_permissions(
"upo_test",
object_payload={"name": "УОП_АктуальныеСпецификации", "synonym": "Актуальные рецептуры"},
object_selector="InformationRegister.УОП_АктуальныеСпецификации",
object_guid="38cea62d-22bb-11ed-9fae-0050569db20c",
action_filter_keys={"read"},
timeout_seconds=30,
limit=10,
)
assert write_result["status"] == "not_found"
assert read_result["status"] == "ok"
assert read_result["roles"][0]["rights"] == {"read": True, "insert": False, "update": False, "delete": False}
assert read_result["roles"][0]["rights_detail"]["read"] == {"allowed": True, "unrestricted": True}
def test_access_object_subjects_fast_chain_returns_profiles_groups_users(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"access_bsp_extractor_plan",
lambda base_id: (
{
"profiles": "profiles",
"profile_roles": "profile_roles",
"group_profiles": "group_profiles",
"groups": "groups",
"group_users": "group_users",
"user_group_members": "user_group_members",
"users": "users",
},
{
"profiles": {"id": "id", "name": "name"},
"profile_roles": {"profile": "profile", "role": "role"},
"group_profiles": {"group": "group", "profile": "profile"},
"groups": {"id": "id", "name": "name", "roles": "roles"},
"group_users": {"group": "group", "user": "user"},
"user_group_members": {"group": "group", "user": "user"},
"users": {"id": "id", "name": "name", "active": "active", "marked": "marked", "user_type": "user_type", "roles": "roles"},
},
{"preset": "bsp"},
),
)
rows_by_query = {
"profiles": [{"id": "p1", "name": "Технологи"}],
"profile_roles": [{"profile": "p1", "role": "r1"}],
"group_profiles": [{"group": "g1", "profile": "p1"}],
"groups": [{"id": "g1", "name": "Технолог"}, {"id": "g2", "name": "Прямая группа", "roles": ["r1"]}],
"group_users": [{"group": "g1", "user": "u1"}, {"group": "g2", "user": "u2"}],
"user_group_members": [],
"users": [
{"id": "u1", "name": "Ivan.PETROV", "active": True, "marked": False, "user_type": "user"},
{"id": "u2", "name": "Anna.SIDOROVA", "active": True, "marked": False, "user_type": "user"},
{"id": "u3", "name": "Direct.USER", "active": True, "marked": False, "user_type": "user", "roles": ["r1"]},
],
}
monkeypatch.setattr(
adapter_server,
"access_rows_from_query",
lambda base_id, query, *, limit, timeout_seconds: (rows_by_query[str(query)], None, False),
)
result = adapter_server.access_object_subjects_fast_chain(
"upo_test",
matched_roles=[{"id": "r1", "name": "УПО. Добавление изменение актуальных спецификаций (Роль)"}],
timeout_seconds=30,
limit=2,
scope_subject_limit=10,
)
assert result["status"] == "ok"
assert result["counts"] == {"roles": 1, "profiles": 1, "groups": 2, "users": 3}
assert [profile["name"] for profile in result["profiles"]] == ["Технологи"]
assert [group["name"] for group in result["groups"]] == ["Прямая группа", "Технолог"]
assert [user["name"] for user in result["users"]] == ["Anna.SIDOROVA", "Direct.USER"]
assert [user["name"] for user in result["_scope_users"]] == ["Anna.SIDOROVA", "Direct.USER", "Ivan.PETROV"]
assert result["users"][0]["role_sources"][0]["type"] == "group_role"
assert result["users"][1]["role_sources"][0]["type"] == "direct_user_role"
assert result["_scope_users"][2]["role_sources"][0]["profile_name"] == "Технологи"
def test_access_subject_access_key_scope_filters_subject_keys(monkeypatch: pytest.MonkeyPatch) -> None:
captured_queries: list[str] = []
monkeypatch.setattr(
adapter_server,
"access_bsp_extractor_plan",
lambda base_id: (
{"access_group_keys": "group_keys", "access_user_keys": "user_keys"},
{
"access_group_keys": {"group": "group", "group_ref": "group_ref", "group_name": "group_name", "access_key": "access_key"},
"access_user_keys": {"user_set": "user_set", "user_set_name": "user_set_name", "user": "user", "access_key": "access_key"},
},
{"preset": "bsp"},
),
)
def fake_rows(base_id: str, query: str, *, limit: int, timeout_seconds: int) -> tuple[list[dict[str, object]], None, bool]:
captured_queries.append(query)
if query.startswith("SELECT * FROM (group_keys)"):
return ([{"group": "g1", "group_ref": "00000092:g1", "group_name": "Технолог", "access_key": "kg1"}], None, False)
if query.startswith("SELECT * FROM (user_keys)"):
return ([{"user": "0000008D:u1", "user_set": "s1", "user_set_name": "Ivan.PETROV", "access_key": "ku1"}], None, False)
return ([], None, False)
monkeypatch.setattr(adapter_server, "access_rows_from_query", fake_rows)
result = adapter_server.access_subject_access_key_scope(
"upo_test",
groups=[{"id": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "name": "Технолог"}],
users=[{"id": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "name": "Ivan.PETROV"}],
total_groups=3,
total_users=5,
timeout_seconds=30,
)
assert result["status"] == "ok"
assert result["counts"]["group_key_rows"] == 1
assert result["counts"]["user_key_rows"] == 1
assert result["counts"]["subject_access_keys"] == 2
assert result["coverage"] == {"groups_checked": 1, "groups_total": 3, "users_checked": 1, "users_total": 5, "complete": False}
assert "not metadata-object role permissions" in result["note"]
assert any("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" in query for query in captured_queries)
assert any("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" in query for query in captured_queries)
def test_access_object_subjects_include_access_key_scope_switch(monkeypatch: pytest.MonkeyPatch) -> None:
scope_calls = 0
scope_kwargs: dict[str, object] = {}
object_guid = "38cea62d-22bb-11ed-9fae-0050569db20c"
monkeypatch.setattr(
adapter_server,
"resolve_object_guid",
lambda payload, base_id, **kwargs: (
object_guid,
"InformationRegister",
{"guid": object_guid, "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации"},
None,
),
)
monkeypatch.setattr(
adapter_server,
"access_object_roles_fast_permissions",
lambda *args, **kwargs: {
"status": "ok",
"roles": [{"id": "r1", "name": "УПО. Добавление изменение актуальных спецификаций (Роль)", "permissions": []}],
"counts": {"roles": 1, "permissions": 1},
},
)
monkeypatch.setattr(
adapter_server,
"access_object_subjects_fast_chain",
lambda *args, **kwargs: {
"status": "ok",
"roles": [{"id": "r1", "name": "УПО. Добавление изменение актуальных спецификаций (Роль)", "permissions": []}],
"profiles": [{"id": "p1", "name": "Технологи"}],
"groups": [{"id": "g1", "name": "Технолог"}],
"users": [{"id": "u1", "name": "Ivan.PETROV"}],
"_scope_groups": [{"id": "g1", "name": "Технолог"}, {"id": "g2", "name": "Производство"}],
"_scope_users": [{"id": "u1", "name": "Ivan.PETROV"}, {"id": "u2", "name": "Anna.SIDOROVA"}],
"counts": {"roles": 1, "profiles": 1, "groups": 2, "users": 2},
},
)
def fake_scope(*args: object, **kwargs: object) -> dict[str, object]:
nonlocal scope_calls
scope_calls += 1
scope_kwargs.update(kwargs)
return {"status": "ok", "counts": {"subject_access_keys": 2}, "coverage": {"complete": True}, "truncated": False}
monkeypatch.setattr(adapter_server, "access_subject_access_key_scope", fake_scope)
fast = adapter_server.access_object_subjects(
{"base_id": "upo_test", "ref": "РегистрСведений.УОП_АктуальныеСпецификации", "action": "write"}
)
extended = adapter_server.access_object_subjects(
{
"base_id": "upo_test",
"ref": "РегистрСведений.УОП_АктуальныеСпецификации",
"action": "write",
"limit": 1,
"include_access_key_scope": True,
"access_key_scope_limit": 123,
"access_key_scope_subject_limit": 10,
}
)
assert "access_key_scope" not in fast
assert extended["access_key_scope"]["counts"]["subject_access_keys"] == 2
assert scope_calls == 1
assert [group["id"] for group in scope_kwargs["groups"]] == ["g1", "g2"]
assert [user["id"] for user in scope_kwargs["users"]] == ["u1", "u2"]
assert scope_kwargs["total_groups"] == 2
assert scope_kwargs["total_users"] == 2
assert scope_kwargs["limit"] == 123
def test_access_object_subjects_returns_roles_profiles_groups_users(monkeypatch: pytest.MonkeyPatch) -> None:
object_guid = "38cea62d-22bb-11ed-9fae-0050569db20c"
monkeypatch.setattr(
adapter_server,
"resolve_object_guid",
lambda payload, base_id, **kwargs: (
object_guid,
"InformationRegister",
{"guid": object_guid, "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации"},
None,
),
)
monkeypatch.setattr(adapter_server, "access_object_roles_fast_permissions", lambda *args, **kwargs: {"status": "error"})
monkeypatch.setattr(
adapter_server,
"access_snapshot_extract",
lambda payload: {
"status": "ok",
"counts": {"roles": 2, "effective_users": 2},
"diagnostics": {"preset": "bsp"},
"access": {
"roles": [
{
"id": "r1",
"name": "Чтение рецептур",
"permissions": [
{
"object": "000000C4:AAA",
"action": "Просмотр",
"object_resolution": {"guid": object_guid, "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации"},
}
],
},
{
"id": "r2",
"name": "Изменение рецептур",
"permissions": [
{
"object": "000000C4:AAA",
"action": "ИзменениеБезОграничения",
"object_resolution": {"guid": object_guid, "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации"},
}
],
},
],
"profiles": [{"id": "p1", "name": "Технологи", "roles": ["r2"]}],
"groups": [{"id": "g1", "name": "Технолог", "profiles": ["p1"], "users": ["u1"]}],
},
"graph": {
"effective_users": [
{
"user": {"id": "u1", "name": "Ivan.PETROV", "active": True},
"groups": ["g1"],
"roles": [{"id": "r2", "name": "Изменение рецептур"}],
"permissions": [
{
"object": "000000C4:AAA",
"action": "ИзменениеБезОграничения",
"object_resolution": {"guid": object_guid, "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации"},
"sources": [{"role": "r2", "role_name": "Изменение рецептур", "chains": [{"type": "group_profile_role", "group": "g1", "profile": "p1"}]}],
}
],
"data_restrictions": [],
},
{
"user": {"id": "u2", "name": "Anna.SIDOROVA", "active": True},
"groups": [],
"roles": [{"id": "r1", "name": "Чтение рецептур"}],
"permissions": [
{
"object": "000000C4:AAA",
"action": "Просмотр",
"object_resolution": {"guid": object_guid, "kind": "InformationRegister", "name": "УОП_АктуальныеСпецификации"},
"sources": [{"role": "r1", "role_name": "Чтение рецептур", "chains": [{"type": "direct_user_role", "user": "u2"}]}],
}
],
"data_restrictions": [],
},
]
},
},
)
result = adapter_server.access_object_subjects(
{"base_id": "upo_test", "ref": "РегистрСведений.УОП_АктуальныеСпецификации", "action": "write"}
)
assert result["status"] == "ok"
assert result["object"]["guid"] == object_guid
assert [role["id"] for role in result["roles"]] == ["r2"]
assert result["profiles"] == [{"id": "p1", "name": "Технологи", "roles": ["r2"]}]
assert result["groups"] == [{"id": "g1", "name": "Технолог", "profiles": ["p1"], "users": ["u1"]}]
assert [item["user"]["id"] for item in result["users"]] == ["u1"]
assert result["users"][0]["rights"] == {"read": False, "insert": False, "update": True, "delete": False}
assert result["users"][0]["rights_detail"]["update"] == {"allowed": True, "unrestricted": True}
def test_access_rls_discover_returns_candidates_with_fields(monkeypatch: pytest.MonkeyPatch) -> None:
listed_calls: list[tuple[str, str | None]] = []
def fake_list_objects(
kind: str | None = None,
*,
base_id: str,
limit: int,
offset: int,
include_storage: bool = False,
table: str = "Config",
name_filter: str | None = None,
**kwargs: Any,
) -> dict[str, Any]:
listed_calls.append((str(kind), name_filter))
if kind == "InformationRegister" and name_filter == "Доступ":
return {
"status": "ok",
"objects": [
{
"guid": "11111111-1111-1111-1111-111111111111",
"kind": "InformationRegister",
"kind_ru": "РегистрСведений",
"name": "КлючиДоступаКОбъектам",
"storage": {"dbnames": [{"sql_number": 26003}]},
}
],
}
return {"status": "ok", "objects": []}
monkeypatch.setattr(adapter_server, "list_objects", fake_list_objects)
monkeypatch.setattr(
adapter_server,
"metadata_object_attributes",
lambda payload: {
"status": "ok",
"dimensions": [{"name": "Объект", "storage_routes": [{"sql_number": 26004}]}],
"resources": [{"name": "КлючДоступа", "storage_routes": [{"sql_number": 26005}]}],
"attributes": [],
"tabular_sections": [],
},
)
result = adapter_server.access_rls_discover({"base_id": "upo_test", "terms": ["Доступ"], "limit": 10})
assert result["status"] == "ok"
assert listed_calls
assert result["counts"]["candidates"] == 1
candidate = result["candidates"][0]
assert candidate["name"] == "КлючиДоступаКОбъектам"
assert "known_bsp_access_keys" in candidate["reasons"]
assert candidate["fields"]["dimensions"][0]["name"] == "Объект"
assert candidate["fields"]["resources"][0]["storage_routes"][0]["sql_number"] == 26005
def test_access_object_keys_resolve_enriches_rows(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"access_bsp_extractor_plan",
lambda base_id: (
{
"access_object_keys": (
"SELECT N'0000006A:o1' AS object, N'0000006A' AS object_type_code, "
"106 AS object_sql_number, N'o1' AS object_id, N'k1' AS access_key"
)
},
{"access_object_keys": {"object": "object", "object_type_code": "object_type_code", "object_sql_number": "object_sql_number", "object_id": "object_id", "access_key": "access_key"}},
{"preset": "bsp"},
),
)
monkeypatch.setattr(
adapter_server,
"access_rows_from_query",
lambda base_id, query, limit, timeout_seconds: ([{"object": "0000006A:o1", "object_type_code": "0000006A", "object_sql_number": 106, "object_id": "o1", "access_key": "k1"}], None, False),
)
monkeypatch.setattr(
adapter_server,
"access_resolve_object_key_records",
lambda base_id, rows, timeout_seconds=60, max_records=200: {
"rows": [{**rows[0], "object_presentation": "Заказ 1", "object_record": {"presentation": "Заказ 1"}}],
"diagnostics": {"resolved": 1, "requested": 1},
},
)
result = adapter_server.access_object_keys_resolve({"base_id": "upo_test", "limit": 10})
row = result["areas"]["access_object_keys"]["rows"][0]
assert row["object_presentation"] == "Заказ 1"
assert result["areas"]["access_object_keys"]["record_resolution"] == {"resolved": 1, "requested": 1}
def test_access_object_explain_resolves_subjects(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"access_bsp_extractor_plan",
lambda base_id: (
{
"access_object_keys": "object_sql",
"access_group_keys": "group_key_sql",
"access_user_keys": "user_key_sql",
"access_set_keys": "set_key_sql",
"users": "users_sql",
"groups": "groups_sql",
"group_users": "group_users_sql",
"user_group_members": "user_group_members_sql",
},
{"access_object_keys": {"access_key": "access_key"}, "access_group_keys": {"group": "group"}, "access_user_keys": {"user": "user"}, "access_set_keys": {"access_set": "access_set"}},
{"preset": "bsp"},
),
)
def fake_key_area(
base_id: str,
area: str,
query: str,
mapping: dict[str, str],
payload: dict[str, Any],
*,
limit: int,
offset: int,
timeout_seconds: int,
) -> dict[str, Any]:
rows_by_area = {
"access_object_keys": [{"object": "0000006A:o1", "object_sql_number": 106, "object_id": "o1", "access_key": "k1"}],
"access_group_keys": [{"group": "g1", "group_name": "Продажи", "access_key": "k1"}],
"access_user_keys": [{"user": "u2", "user_set": "s2", "user_set_name": "Петров (Пользователь)", "access_key": "k1"}],
"access_set_keys": [{"access_set": "s1", "access_set_name": "Набор", "access_key": "k1"}],
}
return {"area": area, "rows": rows_by_area[area], "counts": {"rows": len(rows_by_area[area]), "limit": limit, "offset": offset, "truncated": False}}
monkeypatch.setattr(adapter_server, "access_key_query_run_area", fake_key_area)
monkeypatch.setattr(
adapter_server,
"access_resolve_object_key_records",
lambda base_id, rows, timeout_seconds=60, max_records=200: {
"rows": [{**rows[0], "object_presentation": "Заказ 1"}],
"diagnostics": {"resolved": 1},
},
)
monkeypatch.setattr(
adapter_server,
"access_rows_from_query",
lambda base_id, query, limit, timeout_seconds: ([], None, False),
)
monkeypatch.setattr(
adapter_server,
"access_snapshot_from_extractor_rows",
lambda rows_by_area, mappings: {
"groups": [{"id": "g1", "name": "Продажи", "users": ["u1"]}],
"users": [{"id": "u1", "name": "Иванов"}],
},
)
monkeypatch.setattr(adapter_server, "access_resolve_user_names", lambda base_id, refs, diagnostics=None, timeout_seconds=60, max_refs=20000: {"U1": "Иванов из справочника"})
result = adapter_server.access_object_explain({"base_id": "upo_test", "object_id": "o1"})
assert result["status"] == "ok"
assert result["object_keys"][0]["object_presentation"] == "Заказ 1"
assert result["access_keys"] == ["K1"]
assert result["groups"][0]["group_name"] == "Продажи"
assert {user["name"] for user in result["users"]} == {"Иванов из справочника", "Петров (Пользователь)"}
assert result["counts"]["users"] == 2
def test_access_role_profiles_returns_profiles_and_groups(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"access_bsp_extractor_plan",
lambda base_id: (
{
"roles": "roles_sql",
"profiles": "profiles_sql",
"profile_roles": "profile_roles_sql",
"group_profiles": "group_profiles_sql",
"groups": "groups_sql",
},
{
"roles": {"id": "id", "name": "name"},
"profiles": {"id": "id", "name": "name"},
"profile_roles": {"profile": "profile", "role": "role"},
"group_profiles": {"group": "group", "profile": "profile"},
"groups": {"id": "id", "name": "name"},
},
{"preset": "bsp"},
),
)
rows_by_query = {
"roles_sql": [{"id": "r1", "name": "МенеджерПродаж"}],
"profiles_sql": [{"id": "p1", "name": "Продажи"}, {"id": "p2", "name": "Склад"}],
"profile_roles_sql": [{"profile": "p1", "role": "r1"}],
"group_profiles_sql": [{"group": "g1", "profile": "p1"}, {"group": "g2", "profile": "p2"}],
"groups_sql": [{"id": "g1", "name": "Отдел продаж"}, {"id": "g2", "name": "Склад"}],
}
monkeypatch.setattr(
adapter_server,
"access_rows_from_query",
lambda base_id, query, limit, timeout_seconds: (rows_by_query[query], None, False),
)
result = adapter_server.access_role_profiles({"base_id": "upo_test", "role": "Менеджер"})
assert result["status"] == "ok"
assert result["roles"][0]["name"] == "МенеджерПродаж"
assert result["profiles"] == [{"id": "p1", "name": "Продажи", "roles": ["r1"]}]
assert result["groups"] == [{"id": "g1", "name": "Отдел продаж", "profiles": ["p1"]}]
assert result["counts"] == {"roles": 1, "profiles": 1, "groups": 1}
def test_access_role_users_fuzzy_role_phrase_returns_users(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"access_bsp_extractor_plan",
lambda base_id: (
{
"roles": "roles_sql",
"profiles": "profiles_sql",
"profile_roles": "profile_roles_sql",
"group_profiles": "group_profiles_sql",
"groups": "groups_sql",
"group_users": "group_users_sql",
"user_group_members": "user_group_members_sql",
"users": "users_sql",
},
{
"roles": {"id": "id", "name": "name"},
"profiles": {"id": "id", "name": "name"},
"profile_roles": {"profile": "profile", "role": "role"},
"group_profiles": {"group": "group", "profile": "profile"},
"groups": {"id": "id", "name": "name"},
"group_users": {"group": "group", "user": "user"},
"user_group_members": {"group": "group", "user": "user"},
"users": {"id": "id", "name": "name", "active": "active"},
},
{"preset": "bsp"},
),
)
rows_by_query = {
"roles_sql": [
{"id": "r1", "name": "УПО. Добавление и изменение номенклатуры поставщиков (Роль)"},
{"id": "r2", "name": "УПО. Чтение номенклатуры поставщиков (Роль)"},
],
"profiles_sql": [{"id": "p1", "name": "УПО. Закупки"}],
"profile_roles_sql": [{"profile": "p1", "role": "r1"}],
"group_profiles_sql": [{"group": "g1", "profile": "p1"}],
"groups_sql": [{"id": "g1", "name": "Закупщик"}],
"group_users_sql": [{"group": "g1", "user": "0000015C:11111111111111111111111111111111"}],
"user_group_members_sql": [],
"users_sql": [{"id": "0000015C:11111111111111111111111111111111", "name": "Ivan.PETROV", "active": True}],
}
monkeypatch.setattr(
adapter_server,
"access_rows_from_query",
lambda base_id, query, limit, timeout_seconds: (rows_by_query[query], None, False),
)
monkeypatch.setattr(adapter_server, "access_resolve_user_names", lambda *args, **kwargs: {"U1": "Ivan.PETROV"})
result = adapter_server.access_role_users({"base_id": "upo_test", "role": "запись изменение номенклатура поставщиков"})
assert result["status"] == "ok"
assert result["roles"][0]["name"] == "УПО. Добавление и изменение номенклатуры поставщиков (Роль)"
assert result["profiles"][0]["name"] == "УПО. Закупки"
assert result["groups"][0]["name"] == "Закупщик"
assert result["users"][0] == {
"id": "0000015C:11111111111111111111111111111111",
"name": "Ivan.PETROV",
"active": True,
"groups": [{"id": "g1", "name": "Закупщик"}],
"role_sources": [{"type": "group_profile_role", "group": "g1", "profile": "p1", "role": "r1", "role_name": "УПО. Добавление и изменение номенклатуры поставщиков (Роль)"}],
}
assert "номенклатур" in result["diagnostics"]["role_match"]["tokens"]
assert "поставщик" in result["diagnostics"]["role_match"]["tokens"]
def test_access_role_users_includes_direct_user_and_group_roles(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"access_bsp_extractor_plan",
lambda base_id: (
{
"roles": "roles_sql",
"profiles": "profiles_sql",
"profile_roles": "profile_roles_sql",
"group_profiles": "group_profiles_sql",
"groups": "groups_sql",
"group_users": "group_users_sql",
"user_group_members": "user_group_members_sql",
"users": "users_sql",
},
{
"roles": {"id": "id", "name": "name"},
"profiles": {"id": "id", "name": "name"},
"profile_roles": {"profile": "profile", "role": "role"},
"group_profiles": {"group": "group", "profile": "profile"},
"groups": {"id": "id", "name": "name", "roles": "roles"},
"group_users": {"group": "group", "user": "user"},
"user_group_members": {"group": "group", "user": "user"},
"users": {"id": "id", "name": "name", "active": "active", "roles": "roles"},
},
{"preset": "bsp"},
),
)
rows_by_query = {
"roles_sql": [{"id": "r1", "name": "Прямая роль"}],
"profiles_sql": [],
"profile_roles_sql": [],
"group_profiles_sql": [],
"groups_sql": [{"id": "g1", "name": "Прямая группа", "roles": ["r1"]}],
"group_users_sql": [{"group": "g1", "user": "u2"}],
"user_group_members_sql": [],
"users_sql": [
{"id": "u1", "name": "Direct.USER", "active": True, "roles": ["r1"]},
{"id": "u2", "name": "Group.USER", "active": True, "roles": []},
],
}
monkeypatch.setattr(
adapter_server,
"access_rows_from_query",
lambda base_id, query, limit, timeout_seconds: (rows_by_query[query], None, False),
)
monkeypatch.setattr(adapter_server, "access_resolve_user_names", lambda *args, **kwargs: {})
result = adapter_server.access_role_users({"base_id": "upo_test", "role": "Прямая роль"})
assert result["status"] == "ok"
users = {user["id"]: user for user in result["users"]}
assert users["u1"]["role_sources"] == [{"type": "direct_user_role", "user": "u1", "role": "r1", "role_name": "Прямая роль"}]
assert users["u2"]["role_sources"] == [{"type": "group_role", "group": "g1", "role": "r1", "role_name": "Прямая роль"}]
assert users["u2"]["groups"] == [{"id": "g1", "name": "Прямая группа"}]
def test_access_role_audit_export_returns_flat_rows_and_csv(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"access_role_chain",
lambda base_id, role_selector, timeout_seconds=120: {
"status": "ok",
"base_id": base_id,
"query": {"role": role_selector},
"roles": [{"id": "r1", "name": "УПО. Добавление и изменение номенклатуры поставщиков (Роль)"}],
"profiles": [{"id": "p1", "name": "УПО. Закупки", "roles": ["r1"]}],
"groups": [{"id": "g1", "name": "Закупщик", "profiles": ["p1"]}],
"users": [{"id": "0000015C:11111111111111111111111111111111", "name": "Ivan.PETROV", "active": True, "groups": [{"id": "g1", "name": "Закупщик"}]}],
"alternatives": [],
"counts": {"roles": 1, "profiles": 1, "groups": 1, "users": 1},
"diagnostics": {"role_match": {"best_score": 100}, "bsp": {"metadata": {"sql_numbers": {"users": 348, "external_users": 141, "user_groups": 149}}}},
},
)
result = adapter_server.access_role_audit_export({"base_id": "upo_test", "role": "запись изменение номенклатура поставщиков", "format": "csv"})
assert result["status"] == "ok"
assert result["format"] == "csv"
assert result["rows"][0]["matched_role_name"] == "УПО. Добавление и изменение номенклатуры поставщиков (Роль)"
assert result["rows"][0]["profile_name"] == "УПО. Закупки"
assert result["rows"][0]["group_name"] == "Закупщик"
assert result["rows"][0]["user_name"] == "Ivan.PETROV"
assert result["rows"][0]["user_type"] == "user"
assert result["rows"][0]["user_active"] is True
assert result["rows"][0]["user_marked"] is False
assert result["rows"][0]["user_groups_count"] == 1
assert result["rows"][0]["access_path"] == "УПО. Добавление и изменение номенклатуры поставщиков (Роль) -> УПО. Закупки -> Закупщик -> Ivan.PETROV"
assert "matched_role_name,profile_id" in result["csv"]
assert "access_path" in result["csv"]
assert "Ivan.PETROV" in result["csv"]
def test_access_role_audit_export_includes_direct_role_sources(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"access_role_chain",
lambda base_id, role_selector, timeout_seconds=120: {
"status": "ok",
"base_id": base_id,
"query": {"role": role_selector},
"roles": [{"id": "r1", "name": "Прямая роль"}],
"profiles": [],
"groups": [{"id": "g1", "name": "Прямая группа"}],
"users": [
{
"id": "u1",
"name": "Direct.USER",
"active": True,
"marked": True,
"user_type": "external_user",
"groups": [],
"role_sources": [{"type": "direct_user_role", "user": "u1", "role": "r1", "role_name": "Прямая роль"}],
},
{
"id": "u2",
"name": "Group.USER",
"active": True,
"groups": [{"id": "g1", "name": "Прямая группа"}],
"role_sources": [{"type": "group_role", "group": "g1", "role": "r1", "role_name": "Прямая роль"}],
},
],
"alternatives": [],
"counts": {"roles": 1, "profiles": 0, "groups": 1, "users": 2},
"diagnostics": {},
},
)
result = adapter_server.access_role_audit_export({"base_id": "upo_test", "role": "Прямая роль"})
assert result["status"] == "ok"
assert [row["user_name"] for row in result["rows"]] == ["Direct.USER", "Group.USER"]
assert result["rows"][0]["profile_name"] == ""
assert result["rows"][0]["user_type"] == "external_user"
assert result["rows"][0]["user_marked"] is True
assert result["rows"][0]["access_path"] == "Прямая роль -> Direct.USER"
assert result["rows"][1]["group_name"] == "Прямая группа"
assert result["rows"][1]["access_path"] == "Прямая роль -> Прямая группа -> Group.USER"
def test_access_role_audit_analyze_returns_findings(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"access_role_audit_export",
lambda payload: {
"schema": "onec_access_role_audit_export.v1",
"status": "ok",
"base_id": payload["base_id"],
"source": {"kind": "live_sql", "preset": "bsp"},
"query": {"role": payload["role"]},
"rows": [
{
"matched_role_name": "Администратор системы (Роль)",
"profile_name": "Администратор",
"group_name": "Администраторы",
"user_id": "u1",
"user_name": "Ivan.PETROV",
"user_type": "external_user",
"user_active": True,
"user_marked": False,
"access_path": "Администратор системы (Роль) -> Администратор -> Администраторы -> Ivan.PETROV",
}
],
"roles": [{"id": "r1", "name": "Администратор системы (Роль)"}],
"profiles": [{"id": "p1", "name": "Администратор"}],
"groups": [{"id": "g1", "name": "Администраторы"}],
"users": [{"id": "u1", "name": "Ivan.PETROV"}],
"alternatives": [{"id": "r1", "name": "Администратор системы (Роль)"}, {"id": "r2", "name": "Администратор"}],
"counts": {"roles": 1, "profiles": 1, "groups": 1, "users": 1, "rows": 1},
"diagnostics": {},
},
)
result = adapter_server.access_role_audit_analyze({"base_id": "upo_test", "role": "Администратор", "user_threshold": 1})
assert result["status"] == "ok"
assert result["risk_level"] == "high"
codes = {finding["code"] for finding in result["findings"]}
assert {"many_users", "fuzzy_role_alternatives", "broad_access_group", "external_users"}.issubset(codes)
def test_access_graph_adds_user_access_key_counts() -> None:
graph = adapter_server.build_access_graph_from_snapshot(
{
"users": [{"id": "0000015C:u1", "name": "User"}],
"groups": [{"id": "g1", "name": "Продажи", "users": ["0000015C:u1"]}],
"access_keys": {
"access_group_keys": [
{"group": "g1", "group_name": "Продажи", "access_key": "kg1"},
{"group_ref": "00000092:g2", "group_name": "Склад", "access_key": "kg2"},
],
"access_user_keys": [
{"user": "0000015C:u1", "user_set": "s1", "access_key": "ku1"},
{"user": "0000015C:u2", "user_set": "s2", "access_key": "ku2"},
],
"access_object_keys": [
{"object": "Document.Заказ", "object_type_code": "0000006A", "object_sql_number": 106, "object_id": "o1", "access_key": "kg1"},
{"object": "Document.Чужой", "access_key": "other"},
],
},
},
base_id="upo_test",
)
access_keys = graph["effective_users"][0]["access_keys"]
assert access_keys["counts"] == {"group_keys": 1, "user_keys": 1, "object_keys": 1, "object_key_types": 1, "total": 2}
assert access_keys["samples"]["group_keys"][0]["access_key"] == "kg1"
assert access_keys["samples"]["user_keys"][0]["access_key"] == "ku1"
assert access_keys["samples"]["object_keys"][0]["object"] == "Document.Заказ"
assert access_keys["samples"]["object_keys"][0]["object_sql_number"] == 106
def test_access_graph_uses_access_user_key_name_for_placeholder_user() -> None:
graph = adapter_server.build_access_graph_from_snapshot(
{
"users": [{"id": "0000015C:u1", "name": "0000015C:u1"}],
"access_keys": {
"access_user_keys": [
{"user": "0000015C:u1", "user_set_name": "Иванов (Пользователь)", "user_set": "s1", "access_key": "ku1"},
],
},
},
base_id="upo_test",
)
assert graph["effective_users"][0]["user"]["name"] == "Иванов (Пользователь)"
def test_access_user_explain_has_summary_and_can_resolve_object_key_samples(monkeypatch: pytest.MonkeyPatch) -> None:
access = {
"users": [{"id": "u1", "name": "Иванов", "groups": ["g1"]}],
"groups": [{"id": "g1", "profiles": ["p1"]}],
"profiles": [{"id": "p1", "roles": ["r1"]}],
"roles": [{"id": "r1", "permissions": [{"object": "Catalog.Контрагенты", "object_name": "Контрагенты", "actions": ["Просмотр"]}]}],
"access_keys": {
"access_group_keys": [{"group": "g1", "access_key": "k1"}],
"access_object_keys": [{"object": "0000006A:o1", "object_type_code": "0000006A", "object_sql_number": 106, "object_id": "o1", "access_key": "k1"}],
},
}
monkeypatch.setattr(
adapter_server,
"access_resolve_object_key_records",
lambda base_id, rows, timeout_seconds=60, max_records=200: {
"rows": [{**rows[0], "object_presentation": "Объект 1"}],
"diagnostics": {"resolved": 1},
},
)
result = call_method("access.user.explain", {"base_id": "upo_test", "access": access, "user": "u1", "resolve_records": True})
assert result["summary"]["permission_objects"] == ["Контрагенты"]
assert "Иванов" in result["summary"]["text"]
assert result["access_keys"]["samples"]["object_keys"][0]["object_presentation"] == "Объект 1"
def test_related_child_filter_accepts_guid_selector() -> None:
guid = "e5afdaab-b6e1-452e-9b22-d40207bc7413"
items = [
{
"category": "Template",
"status": "ok",
"guid": guid,
"identity": {"name": "РасширеннаяИнформацияПоСДИЗ", "synonyms": {"ru": "Расширенная информация по СДИЗ"}},
},
{
"category": "Template",
"status": "ok",
"guid": "11111111-1111-1111-1111-111111111111",
"identity": {"name": "ДругойМакет"},
},
]
matches = filter_related_children_by_identity(items, "Template", guid)
assert len(matches) == 1
assert matches[0][0]["guid"] == guid
assert matches[0][1] == "guid_exact"
def test_public_template_summary_exposes_known_platform_types() -> None:
summary = adapter_server.public_template_summary(
[
{
"role": "content",
"features": {"tabular_document": True},
}
]
)
assert summary["platform_type"] == "Табличный документ"
assert summary["format"] == "ТабличныйДокумент"
assert summary["template_type_candidates"][0]["id"] == "tabular_document"
assert summary["template_type_candidates"][0]["confidence"] == "high"
assert "Текстовый документ" in {item["name"] for item in summary["known_platform_types"]}
assert "Внешняя компонента" in {item["name"] for item in summary["known_platform_types"]}
assert {item["name"] for item in summary["known_platform_types"]} == {
"Табличный документ",
"Текстовый документ",
"Двоичные данные",
"Active document",
"HTML документ",
"Географическая схема",
"Графическая схема",
"Схема компоновки данных",
"Макет оформления компоновки данных",
"Внешняя компонента",
}
def test_public_template_summary_uses_declared_template_type_alias() -> None:
summary = adapter_server.public_template_summary(
[
{
"role": "metadata_payload",
"template_type": "DataCompositionAppearanceTemplate",
},
{
"role": "content",
"features": {},
},
]
)
assert summary["features"]["declared_platform_type_id"] == "data_composition_appearance_template"
assert summary["features"]["declared_platform_xml_type"] == "DataCompositionAppearanceTemplate"
assert summary["platform_type"] == "Макет оформления компоновки данных"
assert summary["template_type_candidates"][0]["id"] == "data_composition_appearance_template"
assert summary["template_type_candidates"][0]["confidence"] == "high"
def test_moxel_template_artifact_kind_tracks_coordinate_hint_schema() -> None:
assert adapter_server.MOXEL_TEMPLATE_ARTIFACT_KIND == "template_part_moxel_v11"
def test_template_structure_semantic_text_includes_coordinate_hints() -> None:
text = adapter_server.template_structure_semantic_text(
{
"format": "MOXCEL",
"capacity_dimensions": {"rows": 75, "columns": 26},
"used_dimensions": {"rows": 75, "columns": 26},
"cell_coordinate_hints": [
{
"text": "ПредставлениеПодотчетногоЛица",
"one_based": {"row": 22, "column": 7},
}
],
}
)
assert "cell_hint R22C7: ПредставлениеПодотчетногоЛица" in text
def test_limit_object_commands_result_applies_total_limit() -> None:
object_commands = [{"name": "ОбъектнаяКоманда"}]
form_commands = [{"name": "ФормаКоманда1"}, {"name": "ФормаКоманда2"}]
limited_object, limited_form, commands, counts = limit_object_commands_result(object_commands, form_commands, 2)
assert commands == [{"name": "ОбъектнаяКоманда"}, {"name": "ФормаКоманда1"}]
assert limited_object == [{"name": "ОбъектнаяКоманда"}]
assert limited_form == [{"name": "ФормаКоманда1"}]
assert counts == {"commands_total": 3, "commands_truncated": True, "max_commands": 2}
def test_definition_find_validation_rejects_bad_query_and_areas() -> None:
result = adapter_server.validate_metadata_definition_find_payload(
{"base_id": "upo_test", "kind": "Document", "name": "АвансовыйОтчет", "query": ["Реквизит"]}
)
assert result is not None
assert result["status"] == "invalid_argument"
assert result["argument"] == "query"
result = adapter_server.validate_metadata_definition_find_payload(
{"base_id": "upo_test", "kind": "Document", "name": "АвансовыйОтчет", "query": "Реквизит", "areas": ["unknown"]}
)
assert result is not None
assert result["status"] == "invalid_argument"
assert result["argument"] == "areas"
result = adapter_server.validate_metadata_definition_find_payload(
{"base_id": "upo_test", "query": "Реквизит", "areas": ["metadata"], "use_cache": "true"}
)
assert result is not None
assert result["status"] == "invalid_argument"
assert result["argument"] == "use_cache"
def test_definition_match_public_location_and_origin() -> None:
object_card = {"kind": "Document", "kind_ru": "Документ", "name": "Заказ", "guid": "object-guid", "source": "base"}
match = adapter_server.definition_match(
query="Количество",
area="object",
kind_ru="Реквизит табличной части",
location={"presentation": "Документ.Заказ.ТабличныеЧасти.Товары.Количество"},
item={"name": "Количество", "type": {"presentation": "Число(15,3)"}},
object_card=object_card,
read_selector={"method": "metadata.object.attributes"},
)
assert match is not None
assert match["match_by"] == "name_exact"
assert match["origin"] == {"source": "configuration", "presentation": "Конфигурация", "extension": None, "status": "ok"}
assert match["type"]["presentation"] == "Число(15,3)"
assert match["location"]["presentation"] == "Документ.Заказ.ТабличныеЧасти.Товары.Количество"
def test_definition_match_finds_form_module_routine(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"get_object",
lambda *args, **kwargs: {
"status": "ok",
"source": {"kind": "live_metadata"},
"object": {
"guid": "cfcf9913-41db-4fb7-974f-add97b7144ec",
"kind": "Document",
"kind_ru": "Документ",
"name": "УОП_ПланированиеМеню",
"synonym": "Планирование производства (Документ)",
"source": "base",
},
},
)
monkeypatch.setattr(
adapter_server,
"metadata_object_form_details",
lambda payload: {
"status": "ok",
"forms": [
{
"guid": "ccc6b506-1dce-4a4c-b3c7-3b5cd4252cb5",
"name": "ФормаДокумента",
"synonyms": {"ru": "Форма документа"},
"attributes": [],
"elements": [],
"commands": [],
"events": [],
"module": {
"routine_count": 1,
"routines_sample": [
{"kind": "Процедура", "name": "ЗаполнитьПроизводствоНаСервере"},
],
},
}
],
},
)
result = adapter_server.metadata_definition_find(
{
"base_id": "upo_test",
"kind": "Document",
"name": "УОП_ПланированиеМеню",
"form": "ФормаДокумента",
"query": "ЗаполнитьПроизводствоНаСервере",
"areas": ["form"],
}
)
assert result["status"] == "ok"
assert result["matches"][0]["kind"] == "Процедура формы"
assert result["matches"][0]["location"]["section"] == "Модуль формы"
assert result["matches"][0]["form"]["name"] == "ФормаДокумента"
def test_metadata_object_form_details_supports_common_form(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {
"status": "ok",
"source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
"form": {"kind": "CommonForm", "name": "t_Форма", "guid": "form-guid"},
"profile": {
"items": [{"name": "Кнопка1", "section": "items"}],
"attributes": [],
"commands": [{"name": "ЗаменаДомена", "action": "ЗаменаДомена", "section": "commands"}],
"events": [],
"module": {"routine_count": 1, "routines_sample": [{"kind": "Процедура", "name": "ЗаменаДомена"}]},
},
}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
result = adapter_server.metadata_object_form_details(
{
"base_id": "upo_test",
"extension": "test2",
"object_type": "CommonForm",
"object_name": "t_Форма",
"table": "ConfigCASSave",
}
)
assert result["status"] == "ok"
assert result["object"] == {"kind": "CommonForm", "name": "t_Форма", "guid": "form-guid"}
assert result["forms"][0]["commands"][0]["name"] == "ЗаменаДомена"
assert result["forms"][0]["module"]["routines_sample"][0]["name"] == "ЗаменаДомена"
assert seen["payload"]["object_type"] == "CommonForm"
assert seen["payload"]["object_name"] == "t_Форма"
assert seen["payload"]["form"] == "t_Форма"
assert seen["payload"]["include_storage"] is False
def test_form_object_data_paths_are_resolved_from_metadata_identity_guids() -> None:
order_guid = "21e8237e-5d20-4726-8712-3d43bfc9993e"
author_guid = "977014d5-3fac-4bdf-9d20-81b437053042"
profile = {
"items": [
{
"name": "ЗапасыЗаказ",
"path_to_data": "Объект.Запасы.Заказ",
"guids_sample": [order_guid],
"semantic": {"groups": {"Основные": [{"name": "ПутьКДанным", "value": "Объект.Запасы.Заказ"}]}},
},
{
"name": "АвторДокумента",
"path_to_data": "Объект.АвторДокумента",
"guids_sample": [author_guid],
"semantic": {"groups": {"Основные": [{"name": "ПутьКДанным", "value": "Объект.АвторДокумента"}]}},
},
{
"name": "ЗапасыЗаказИтог",
"path_to_data": "Объект.Запасы.TotalЗаказ",
"guids_sample": [order_guid],
"semantic": {"groups": {"Основные": [{"name": "ПутьКДанным", "value": "Объект.Запасы.TotalЗаказ"}]}},
},
]
}
object_fields = {
"attributes": [{"name": "Автор", "identity": {"guid": author_guid}}],
"tabular_sections": [
{"name": "Запасы", "columns": [{"name": "ЗаказПокупателя", "identity": {"guid": order_guid}}]}
],
}
assert adapter_server.enrich_form_profile_object_data_paths(profile, object_fields) == 3
assert profile["items"][0]["path_to_data"] == "Объект.Запасы.ЗаказПокупателя"
assert profile["items"][1]["path_to_data"] == "Объект.Автор"
assert profile["items"][2]["path_to_data"] == "Объект.Запасы.TotalЗаказПокупателя"
for item in profile["items"]:
prop = item["semantic"]["groups"]["Основные"][0]
assert prop["value"] == item["path_to_data"]
assert prop["source"] == "object_metadata_identity_guid"
def test_preferred_form_payload_file_name_selects_dot_zero_body() -> None:
guid = "f24e223f-bd30-4d19-b0a2-d1b63eeed65e"
selected = adapter_server.preferred_form_payload_file_name(
guid,
[guid, f"{guid}.1", f"{guid}.0"],
)
assert selected == f"{guid}.0"
assert adapter_server.preferred_form_payload_file_name(guid, [guid]) == ""
def test_form_command_reference_exposes_metadata_guid() -> None:
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
guid = "64a59186-ea83-44d7-b433-bf93778db6f9"
node = seq(atom(31), seq(), atom(0), atom(0), atom(0), atom("Кнопка"), seq(), atom(1), seq(atom(0), atom(guid)))
from parser.form_payload import form_command_reference
reference = form_command_reference(node, "1.2")
assert reference == {"guid": guid, "code": "0", "path": "1.2.8", "scope": "metadata", "status": "identity_pending"}
def test_form_command_reference_resolves_known_standard_command() -> None:
from parser.form_payload import form_command_reference
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
node = seq(atom(31), seq(), atom(0), atom(0), atom(0), atom("Справка"), seq(), atom(1), seq(atom(0), atom("39bb0fe9-771d-4dd5-8a6e-2d16984523af")))
reference = form_command_reference(node, "1.2")
assert reference is not None
assert reference["command_name"] == "Form.StandardCommand.Help"
assert reference["scope"] == "standard"
assert reference["status"] == "ok"
def test_form_command_reference_resolves_local_form_command_id() -> None:
from parser.form_payload import form_command_reference
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
node = seq(atom(31), seq(), atom(0), atom(0), atom(0), atom("Кнопка"), seq(), atom(1), seq(atom(39), atom("409b9a53-7f7e-4178-86c1-33176c7c7a7a")))
reference = form_command_reference(node, "1.2")
assert reference is not None
assert reference["command_id"] == "39"
assert reference["scope"] == "form"
assert reference["match_by"] == "form_command_id"
def test_enrich_item_data_paths_resolves_command_button_standard_ref() -> None:
from parser.form_payload import enrich_item_data_paths
item = {
"marker": "31",
"name": "СвязанныеДокументы",
"data_path_attribute_id": "1",
"data_path_field_id": "-5",
"semantic": {"groups": {}},
}
attributes = [{"id": "1", "name": "Объект"}]
enrich_item_data_paths([item], attributes)
assert item["path_to_data"] == "Объект.Ref"
prop = item["semantic"]["groups"]["Основные"][-1]
assert prop == {"name": "ПутьКДанным", "value": "Объект.Ref", "source": "standard_data_field_reference", "status": "ok"}
def test_command_button_dynamic_field_uses_current_data_path() -> None:
from parser.form_payload import enrich_item_data_paths
item = {"marker": "31", "name": "Выполнить", "data_path_attribute_id": "1", "data_path_field_id": "132", "semantic": {"groups": {}}}
attribute = {
"id": "1",
"name": "Список",
"dynamic_list_settings": {"status": "ok"},
"dynamic_list_fields": [{"id": "132", "name": "Ссылка", "data_name": "Ref", "path_to_data": "Список.Ref"}],
}
enrich_item_data_paths([item], [attribute])
assert item["path_to_data"] == "Items.Список.CurrentData.Ref"
def test_enrich_item_data_paths_distinguishes_document_number_from_catalog_code() -> None:
from parser.form_payload import enrich_item_data_paths
item = {
"marker": "37",
"name": "Номер",
"data_path_attribute_id": "1",
"data_path_field_id": "-2",
"semantic": {"groups": {}},
}
enrich_item_data_paths([item], [{"id": "1", "name": "Объект"}])
assert item["path_to_data"] == "Объект.Number"
def test_enrich_item_data_paths_distinguishes_object_date_from_description() -> None:
from parser.form_payload import enrich_item_data_paths
item = {
"marker": "37",
"name": "Дата",
"data_path_attribute_id": "1",
"data_path_field_id": "-3",
"semantic": {"groups": {}},
}
enrich_item_data_paths([item], [{"id": "1", "name": "Объект"}])
assert item["path_to_data"] == "Объект.Date"
def test_element_event_guids_decode_to_public_event_names() -> None:
from parser.form_payload import event_handlers_from_node
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
node = seq(
atom(2),
atom("fe115cc8-9e33-4684-a166-bd5136fe7a9f"),
atom("ПолеПриИзменении"),
atom("f72043b8-2d79-414e-bc4e-3972fe9dbca1"),
atom("ПолеОбработкаВыбора"),
)
events = event_handlers_from_node(node, "1.2", owner="Поле")
assert [(event["event_name"], event["handler"]) for event in events] == [
("OnChange", "ПолеПриИзменении"),
("ChoiceProcessing", "ПолеОбработкаВыбора"),
]
@pytest.mark.parametrize(
("guid", "expected"),
[
("ac5a9c5a-5f1d-4fc5-b88c-a187038c16d1", "Opening"),
("70636369-514c-4662-977e-1c3976c9756c", "Tuning"),
("650da4af-3233-4ce0-a1ae-23f87a226eee", "DetailProcessing"),
("509eca20-d6e4-4fef-a0f8-3a6b44c64178", "URLProcessing"),
("3c3da18f-fc18-4f77-8c2d-96c25bec40a5", "Selection"),
("1952a54f-35ad-4928-902f-df212ab38ca3", "OnSaveDataInSettingsAtServer"),
("93dfba16-26db-46f8-acb5-4f92f50c855f", "NavigationProcessing"),
],
)
def test_specialized_element_event_guids_decode_to_public_names(guid: str, expected: str) -> None:
from parser.form_payload import event_handlers_from_node
node = parse_brace_text('{1,"' + guid + '","Обработчик"}')
events = event_handlers_from_node(node, "1.2", owner="Поле")
assert events[0]["event_name"] == expected
def test_form_xml_event_profile_preserves_handler(tmp_path: Path) -> None:
form_xml = tmp_path / "Form.xml"
form_xml.write_text('
', encoding="utf-8")
profile = decode_form_xml(form_xml)
assert profile["form"]["kind_ru"] == "Форма"
event = next(item for item in profile["items"] if item["name"] == "OnChange")
assert event["kind_ru"] == "Событие"
event = next(item for item in profile["items"] if item["kind"] == "Event")
properties = [prop for group in event["semantic"]["groups"].values() for prop in group]
assert next(prop for prop in properties if prop["name"] == "Обработчик")["value"] == "ПолеПриИзменении"
def test_form_common_semantic_decodes_paired_show_command_bar_probe() -> None:
false_semantic = form_common_semantic(
[
{"index": 17, "presentation": "Параметр 17", "value": "0", "value_kind": "boolean_or_number"},
{"index": 56, "presentation": "Параметр 56", "value": "0", "value_kind": "boolean_or_number"},
]
)
true_semantic = form_common_semantic(
[
{"index": 17, "presentation": "Параметр 17", "value": "2", "value_kind": "number"},
{"index": 56, "presentation": "Параметр 56", "value": "1", "value_kind": "boolean_or_number"},
]
)
bottom_semantic = form_common_semantic(
[
{"index": 17, "presentation": "Параметр 17", "value": "3", "value_kind": "number"},
{"index": 56, "presentation": "Параметр 56", "value": "1", "value_kind": "boolean_or_number"},
]
)
false_property = false_semantic["groups"]["Основные"][0]
true_property = true_semantic["groups"]["Основные"][0]
assert false_property["name"] == "ОтображатьКоманднуюПанель"
assert false_property["value"] is False
assert false_property["parameter_indices"] == [17, 56]
assert false_property["write_shape"] == "paired_scalar"
assert true_property["value"] is True
assert true_property["parameter_values"] == ["2", "1"]
assert bottom_semantic["groups"]["Основные"][0]["value"] is True
assert true_semantic["coverage"] == {"mapped": 2, "unmapped": 0, "total": 2, "status": "ok"}
enrich_form_common_semantic(
true_semantic,
[{"id": "-1", "name": "ФормаКоманднаяПанель", "type_name": "Командная панель"}],
)
assert true_semantic["groups"]["Основные"][-1] == {
"name": "АвтоКоманднаяПанель",
"value": "ФормаКоманднаяПанель",
"source": "form_item_reference:id=-1",
"status": "ok",
}
def test_form_common_semantic_decodes_command_bar_location_probe() -> None:
def decode(primary: str, companion: str) -> dict[str, object]:
semantic = form_common_semantic(
[
{"index": 17, "presentation": "Параметр 17", "value": primary, "value_kind": "number"},
{"index": 56, "presentation": "Параметр 56", "value": companion, "value_kind": "boolean_or_number"},
]
)
return next(prop for prop in semantic["groups"]["Основные"] if prop["name"] == "ПоложениеКоманднойПанели")
assert decode("0", "0")["value"] == "None"
assert decode("2", "1")["value"] == "Top"
bottom = decode("3", "1")
assert bottom == {
"name": "ПоложениеКоманднойПанели",
"value": "Bottom",
"source": "controlled_designer_form_command_bar_location",
"status": "ok",
"parameter_indices": [17, 56],
"parameter_values": ["3", "1"],
"write_shape": "paired_scalar_shared",
}
def test_form_common_semantic_decodes_document_typed_root_properties() -> None:
semantic = form_common_semantic(
[
{
"index": 20,
"typed_kind": "enum",
"typed_guid": "adeb08a0-415c-11d6-b9d1-0050bae0a95d",
"typed_value": "3",
},
{
"index": 22,
"typed_kind": "enum",
"typed_guid": "20d89b09-bd04-4304-a8c7-4d07fac6338a",
"typed_value": "3",
},
{"index": 24, "typed_kind": "boolean", "typed_value": "1"},
]
)
props = {item["name"]: item["value"] for items in semantic["groups"].values() for item in items}
assert props["AutoTime"] == "CurrentOrLast"
assert props["UsePostingMode"] == "Auto"
assert props["RepostOnWrite"] is True
def test_form_common_semantic_decodes_paired_window_opening_mode_probe() -> None:
dont_block = form_common_semantic(
[
{"index": 2, "presentation": "Параметр 2", "value": "0", "value_kind": "boolean_or_number"},
{"index": 54, "presentation": "Параметр 54", "value": "0", "value_kind": "boolean_or_number"},
]
)
lock_owner = form_common_semantic(
[
{"index": 2, "presentation": "Параметр 2", "value": "1", "value_kind": "boolean_or_number"},
{"index": 54, "presentation": "Параметр 54", "value": "1", "value_kind": "boolean_or_number"},
]
)
lock_whole_interface = form_common_semantic(
[
{"index": 2, "presentation": "Параметр 2", "value": "2", "value_kind": "number"},
{"index": 54, "presentation": "Параметр 54", "value": "2", "value_kind": "number"},
]
)
dont_block_property = dont_block["groups"]["Основные"][0]
lock_owner_property = lock_owner["groups"]["Основные"][0]
assert dont_block_property == {
"name": "РежимОткрытияОкна",
"value": "DontBlock",
"source": "controlled_designer_form_window_opening_mode",
"status": "ok",
"parameter_indices": [2, 54],
"parameter_values": ["0", "0"],
"write_shape": "paired_scalar",
}
assert lock_owner_property["value"] == "LockOwner"
assert lock_owner_property["parameter_values"] == ["1", "1"]
assert lock_whole_interface["groups"]["Основные"][0]["value"] == "LockWholeInterface"
assert lock_whole_interface["groups"]["Основные"][0]["parameter_values"] == ["2", "2"]
assert lock_owner["coverage"] == {"mapped": 2, "unmapped": 0, "total": 2, "status": "ok"}
def test_form_common_semantic_decodes_auto_save_data_in_settings_probe() -> None:
use = form_common_semantic(
[{"index": 7, "presentation": "Параметр 7", "value": "1", "value_kind": "boolean_or_number"}]
)
dont_use = form_common_semantic(
[{"index": 7, "presentation": "Параметр 7", "value": "0", "value_kind": "boolean_or_number"}]
)
use_property = use["groups"]["Основные"][0]
dont_use_property = dont_use["groups"]["Основные"][0]
assert use_property == {
"name": "АвтоСохранениеДанныхВНастройках",
"value": "Use",
"source": "controlled_designer_form_auto_save_data_in_settings",
"status": "ok",
"parameter_indices": [7],
"parameter_values": ["1"],
"write_shape": "scalar_enum",
}
assert dont_use_property["value"] == "DontUse"
assert dont_use["coverage"] == {"mapped": 1, "unmapped": 0, "total": 1, "status": "ok"}
def test_form_common_semantic_decodes_composite_root_group_probe() -> None:
def parameters(values: str | tuple[str, str, str, str]) -> list[dict[str, object]]:
resolved_values = (values, values, values, values) if isinstance(values, str) else values
return [
{"index": index, "presentation": f"Параметр {index}", "value": value, "value_kind": "boolean_or_number"}
for index, value in zip((11, 40, 47, 57), resolved_values, strict=True)
]
vertical = form_common_semantic(parameters("0"))
horizontal = form_common_semantic(parameters("1"))
always_horizontal = form_common_semantic(parameters(("1", "1", "3", "3")))
horizontal_if_possible = form_common_semantic(parameters(("1", "2", "2", "2")))
vertical_property = vertical["groups"]["Основные"][0]
horizontal_property = horizontal["groups"]["Основные"][0]
assert vertical_property == {
"name": "Группировка",
"value": "Vertical",
"source": "controlled_designer_form_root_group",
"status": "ok",
"parameter_indices": [11, 40, 47, 57],
"parameter_values": ["0", "0", "0", "0"],
"write_shape": "composite_scalar",
}
assert horizontal_property["value"] == "Horizontal"
assert horizontal_property["parameter_values"] == ["1", "1", "1", "1"]
assert always_horizontal["groups"]["Основные"][0]["value"] == "AlwaysHorizontal"
assert horizontal_if_possible["groups"]["Основные"][0]["value"] == "HorizontalIfPossible"
assert horizontal["coverage"] == {"mapped": 4, "unmapped": 0, "total": 4, "status": "ok"}
def test_form_common_semantic_decodes_compact_base_form_root_layout() -> None:
semantic = form_common_semantic(
[
{"index": 0, "presentation": "Версия/тип формы", "value": "49", "value_kind": "number"},
{"index": 2, "presentation": "Параметр 2", "value": "0", "value_kind": "boolean_or_number"},
{"index": 11, "presentation": "Параметр 11", "value": "0", "value_kind": "boolean_or_number"},
]
)
properties = semantic["groups"]["Основные"]
window = next(prop for prop in properties if prop["name"] == "РежимОткрытияОкна")
group = next(prop for prop in properties if prop["name"] == "Группировка")
assert window["value"] == "DontBlock"
assert window["parameter_values"] == ["0", ""]
assert group["value"] == "Vertical"
assert group["parameter_values"] == ["0", "", "", ""]
def test_form_common_semantic_decodes_versioned_root_visibility_scroll_and_title() -> None:
dp = form_common_semantic(
[
{"index": 0, "presentation": "Версия/тип формы", "value": "50", "value_kind": "number"},
{"index": 9, "presentation": "Параметр 9", "value": "1", "value_kind": "boolean_or_number"},
{"index": 17, "presentation": "Параметр 17", "value": "0", "value_kind": "boolean_or_number"},
]
)
register = form_common_semantic(
[
{"index": 0, "presentation": "Версия/тип формы", "value": "49", "value_kind": "number"},
{"index": 36, "presentation": "Параметр 36", "value": "2", "value_kind": "number"},
]
)
report = form_common_semantic(
[
{"index": 0, "presentation": "Версия/тип формы", "value": "50", "value_kind": "number"},
{"index": 9, "presentation": "Параметр 9", "value": "0", "value_kind": "boolean_or_number"},
]
)
dp_props = {p["name"]: p["value"] for group in dp["groups"].values() for p in group}
register_props = {p["name"]: p["value"] for group in register["groups"].values() for p in group}
report_props = {p["name"]: p["value"] for group in report["groups"].values() for p in group}
assert dp_props["ОтображатьКоманднуюПанель"] is False
assert dp_props["ПоложениеКоманднойПанели"] == "None"
assert register_props["VerticalScroll"] == "useIfNecessary"
assert report_props["AutoTitle"] is False
def test_form_common_semantic_decodes_typed_use_for_folders_and_items() -> None:
from parser.form_payload import form_common_parameters
values = ["0"] * 21
values[0] = "49"
values[20] = '{"#",59ef2b80-c86b-11d5-a3c1-0050bae0a776,0}'
tree = parse_brace_text("{4,{" + ",".join(values) + "}}")
parameters = form_common_parameters(tree)
semantic = form_common_semantic(parameters)
prop = next(prop for prop in semantic["groups"]["Основные"] if prop["name"] == "UseForFoldersAndItems")
assert prop["value"] == "Items"
assert prop["parameter_index"] == 20
assert prop["write_shape"] == "typed_enum_read_only"
def test_form_container_type_codes_have_public_names() -> None:
from parser.form_payload import form_item_public_type_name, item_type_name
assert item_type_name("22", "0") == "Командная панель"
assert item_type_name("22", "1") == "Подменю"
assert item_type_name("22", "2") == "Группа колонок"
assert item_type_name("22", "3") == "Страницы"
assert item_type_name("22", "6") == "Группа кнопок"
assert item_type_name("37", "3") == "Поле флажка"
assert item_type_name("37", "4") == "PictureField"
assert item_type_name("37", "5") == "Поле переключателя"
assert item_type_name("37", "6") == "Поле табличного документа"
assert form_item_public_type_name("12", "0", "Остаток") == "Декорация надписи"
assert form_item_public_type_name("12", "1", "Картинка") == "Декорация картинки"
assert form_item_public_type_name("12", "0", "ПолеРасширеннаяПодсказка") == "Расширенная подсказка"
def test_command_button_representation_decodes_all_enum_values() -> None:
from parser.form_payload import button_representation_presentation
assert [button_representation_presentation(str(code)) for code in range(4)] == [
"Text",
"Picture",
"PictureAndText",
"Авто",
]
def test_input_field_title_location_decodes_confirmed_enum_values_only() -> None:
from parser.form_payload import title_location_presentation
assert title_location_presentation("0") == "None"
assert title_location_presentation("1") == "Auto"
assert title_location_presentation("2") == "Left"
assert title_location_presentation("3") == "Top"
assert title_location_presentation("4") == "Right"
assert title_location_presentation("5") is None
def test_container_type_implies_confirmed_representation() -> None:
from parser.form_payload import enrich_container_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
node = seq(atom(22), atom(0), atom(0), atom(0), atom(0), atom(6), atom("Группа"), atom(0), atom(0), atom(0), atom(1))
row = {"marker": "22", "type_name": "Группа кнопок", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_container_specific_semantics(row, node)
assert row["semantic"]["groups"]["Оформление"][-1] == {
"name": "Отображение",
"value": "Compact",
"source": "form_payload_container_type",
"status": "ok",
}
def test_group_layout_exposes_confirmed_stretch_flags() -> None:
from parser.form_payload import enrich_container_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(30)]
values[0] = atom(22)
values[5] = atom(5)
values[6] = atom("Группа")
values[10] = atom(1)
values[14] = atom(1)
values[15] = atom(0)
node = seq(*values)
row = {"marker": "22", "type_name": "Группа", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_container_specific_semantics(row, node)
props = {prop["name"]: prop["value"] for prop in row["semantic"]["groups"]["Расположение"]}
assert props["РастягиватьПоГоризонтали"] is True
assert props["РастягиватьПоВертикали"] is False
def test_page_type_exposes_confirmed_layout_defaults() -> None:
from parser.form_payload import enrich_container_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
node = seq(atom(22), atom(0), atom(0), atom(0), atom(0), atom(4), atom("Страница"), atom(0), atom(0), atom(0), atom(1))
row = {"marker": "22", "type_name": "Страница", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_container_specific_semantics(row, node)
assert row["semantic"]["groups"]["Расположение"][-1]["value"] == "Vertical"
assert row["semantic"]["groups"]["Использование"][-1]["value"] is False
@pytest.mark.parametrize(
("code", "expected"),
[("0", "None"), ("1", "TabsOnTop")],
)
def test_pages_type_decodes_confirmed_representation(code: str, expected: str) -> None:
from parser.form_payload import enrich_container_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(21)]
values[0] = atom(22)
values[5] = atom(3)
values[6] = atom("Страницы")
values[10] = atom(1)
values[20] = seq(atom(4), atom(code), seq(), atom(2), atom(0), atom(code))
row = {"marker": "22", "type_name": "Страницы", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_container_specific_semantics(row, seq(*values))
assert row["semantic"]["groups"]["Оформление"][-1] == {
"name": "PagesRepresentation",
"value": expected,
"source": "form_payload_pages_layout",
"status": "ok",
"parameter_index": 20,
}
def test_pages_type_does_not_guess_representation_when_repeated_codes_disagree() -> None:
from parser.form_payload import enrich_container_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(21)]
values[0] = atom(22)
values[5] = atom(3)
values[6] = atom("Страницы")
values[20] = seq(atom(4), atom(1), seq(), atom(2), atom(0), atom(0))
row = {"marker": "22", "type_name": "Страницы", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_container_specific_semantics(row, seq(*values))
assert "Оформление" not in row["semantic"]["groups"]
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_default_item(code: str, expected: bool) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(39)]
values[0] = atom(37)
values[5] = atom(1)
values[6] = atom("Дата")
values[13] = atom(1)
values[16] = atom(code)
row = {"marker": "37", "type_name": "Поле надписи", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Основные"] if item["name"] == "АктивизироватьПоУмолчанию")
assert prop == {
"name": "АктивизироватьПоУмолчанию",
"value": expected,
"source": "form_payload_input_field",
"status": "ok",
"parameter_index": 16,
}
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_choice_list_button(code: str, expected: bool) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
values[6] = atom("Поле")
values[7] = atom(0)
values[13] = atom(1)
specific = [atom(0) for _ in range(56)]
specific[0] = atom(36)
specific[11] = atom(code)
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "ChoiceListButton")
assert prop == {
"name": "ChoiceListButton",
"value": expected,
"source": "form_payload_input_field",
"status": "ok",
"parameter_index": 11,
}
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True), ("2", "Авто")])
def test_input_field_decodes_confirmed_open_button(code: str, expected: bool | str) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
values[6] = atom("Поле")
specific = [atom(0) for _ in range(56)]
specific[0] = atom(36)
specific[15] = atom(code)
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "OpenButton")
assert prop["value"] == expected
assert prop["parameter_index"] == 15
def test_input_field_open_button_is_independent_from_quick_choice() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
specific = [atom(0) for _ in range(56)]
specific[0] = atom(36)
specific[15] = atom(1)
specific[23] = atom(0)
specific[32] = atom(0)
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
props = {item["name"]: item["value"] for item in row["semantic"]["groups"]["Использование"]}
assert props["OpenButton"] is True
assert props["БыстрыйВыбор"] is False
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True), ("2", "Авто")])
def test_input_field_decodes_confirmed_quick_choice(code: str, expected: bool | str) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
specific = [atom(0) for _ in range(56)]
specific[0] = atom(36)
specific[23] = atom(code)
specific[32] = atom("0" if code == "1" else "1")
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "БыстрыйВыбор")
assert prop["value"] == expected
assert prop["parameter_index"] == 23
def test_input_field_decodes_quick_choice_independently_from_choose_type() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
specific = [atom(0) for _ in range(56)]
specific[0] = atom(36)
specific[23] = atom(1)
specific[32] = atom(0)
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
props = {item["name"]: item for item in row["semantic"]["groups"]["Использование"]}
assert props["БыстрыйВыбор"]["value"] is True
assert props["БыстрыйВыбор"]["parameter_index"] == 23
assert props["ВыбиратьТип"]["value"] is False
assert props["ВыбиратьТип"]["parameter_index"] == 32
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_create_button(code: str, expected: bool) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
values[6] = atom("Поле")
specific = [atom(0) for _ in range(56)]
specific[0] = atom(36)
specific[45] = atom(code)
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "CreateButton")
assert prop == {
"name": "CreateButton",
"value": expected,
"source": "form_payload_input_field",
"status": "ok",
"parameter_index": 45,
}
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True), ("2", "Авто")])
def test_input_field_decodes_confirmed_clear_button(code: str, expected: bool | str) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(56)]
specific[0] = atom(36)
specific[13] = atom(code)
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "КнопкаОчистки")
assert prop["value"] == expected
assert prop["parameter_index"] == 13
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_text_edit(code: str, expected: bool) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(56)]
specific[0] = atom(36)
specific[41] = atom(code)
specific[55] = atom("0" if code == "1" else "1")
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "РедактированиеТекста")
assert prop["value"] is expected
assert prop["parameter_index"] == 41
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_choose_type_from_index_32_not_54(code: str, expected: bool) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(56)]
specific[0] = atom(36)
specific[32] = atom(code)
specific[54] = atom("0" if code == "1" else "1")
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "ВыбиратьТип")
assert prop["value"] is expected
assert prop["parameter_index"] == 32
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_show_in_header(code: str, expected: bool) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
values[20] = atom(code)
specific = [atom(0) for _ in range(56)]
specific[0] = atom(36)
specific[20] = atom("0" if code == "1" else "1")
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Расположение"] if item["name"] == "ShowInHeader")
assert prop["value"] is expected
assert prop["parameter_index"] == 20
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_visibility_from_index_43(code: str, expected: bool) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(44)]
values[0] = atom(37)
values[5] = atom(2)
values[13] = atom("0" if code == "1" else "1")
values[43] = atom(code)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Основные"] if item["name"] == "Видимость")
assert prop["value"] is expected
assert prop["parameter_index"] == 43
@pytest.mark.parametrize(("index", "name", "group"), [(21, "ShowInFooter", "Расположение"), (22, "CellHyperlink", "Использование")])
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_direct_boolean_properties(
index: int,
name: str,
group: str,
code: str,
expected: bool,
) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(44)]
values[0] = atom(37)
values[5] = atom(2)
values[index] = atom(code)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"][group] if item["name"] == name)
assert prop["value"] is expected
assert prop["parameter_index"] == index
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_read_only(code: str, expected: bool) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(44)]
values[0] = atom(37)
values[5] = atom(2)
values[14] = atom(code)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Основные"] if item["name"] == "ТолькоПросмотр")
assert prop["value"] is expected
assert prop["parameter_index"] == 14
@pytest.mark.parametrize(("code", "expected"), [("1", True), ("2", False)])
def test_input_field_decodes_confirmed_skip_on_input(code: str, expected: bool) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(44)]
values[0] = atom(37)
values[5] = atom(2)
values[15] = atom(code)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == "ПропускатьПриВводе")
assert prop["value"] is expected
assert prop["parameter_index"] == 15
@pytest.mark.parametrize(
("index", "name", "group", "code", "expected"),
[
(8, "TitleHeight", "Расположение", "0", "0"),
(8, "TitleHeight", "Расположение", "2", "2"),
(23, "HorizontalAlign", "Расположение", "0", "Left"),
(23, "HorizontalAlign", "Расположение", "1", "Center"),
(23, "HorizontalAlign", "Расположение", "2", "Right"),
(23, "HorizontalAlign", "Расположение", "3", "Auto"),
(26, "РежимРедактирования", "Использование", "0", "Directly"),
(26, "РежимРедактирования", "Использование", "1", "Auto"),
(26, "РежимРедактирования", "Использование", "2", "EnterOnInput"),
(53, "GroupHorizontalAlign", "Расположение", "2", "Right"),
(53, "GroupHorizontalAlign", "Расположение", "3", "Auto"),
],
)
def test_input_field_decodes_confirmed_direct_scalar_and_enum_properties(
index: int,
name: str,
group: str,
code: str,
expected: str,
) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(54)]
values[0] = atom(37)
values[5] = atom(2)
values[index] = atom(code)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"][group] if item["name"] == name)
assert prop["value"] == expected
assert prop["parameter_index"] == index
def test_input_field_derives_auto_edit_mode_from_enter_on_input() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(59)]
values[0] = atom(37)
values[26] = atom(2)
row = {"marker": "37", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
usage = {item["name"]: item for item in row["semantic"]["groups"]["Использование"]}
assert usage["РежимРедактирования"]["value"] == "EnterOnInput"
assert usage["AutoEditMode"]["value"] is True
assert usage["AutoEditMode"]["parameter_index"] == 26
@pytest.mark.parametrize(
("code", "expected"),
[("0", "None"), ("1", "Auto"), ("2", "Left"), ("3", "Top"), ("4", "Right")],
)
def test_input_field_decodes_all_confirmed_title_locations(code: str, expected: str) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(44)]
values[0] = atom(37)
values[5] = atom(2)
values[7] = atom(code)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Основные"] if item["name"] == "ПоложениеЗаголовка")
assert prop["value"] == expected
assert prop["parameter_index"] == 7
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_input_field_decodes_confirmed_enabled(code: str, expected: bool) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(54)]
values[0] = atom(37)
values[5] = atom(2)
values[13] = atom(code)
values[43] = atom("1")
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Основные"] if item["name"] == "Доступность")
assert prop["value"] is expected
assert prop["parameter_index"] == 13
@pytest.mark.parametrize(("code", "expected"), [("0", "Auto"), ("3", "Button")])
def test_input_field_decodes_confirmed_tooltip_representation(code: str, expected: str) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(54)]
values[0] = atom(37)
values[5] = atom(2)
values[50] = atom(code)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Оформление"] if item["name"] == "ToolTipRepresentation")
assert prop["value"] == expected
assert prop["parameter_index"] == 50
@pytest.mark.parametrize(
("index", "name", "group", "code", "expected"),
[
(4, "HorizontalStretch", "Расположение", "0", False),
(4, "HorizontalStretch", "Расположение", "1", True),
(4, "HorizontalStretch", "Расположение", "2", "Авто"),
(6, "Wrap", "Расположение", "0", False),
(6, "Wrap", "Расположение", "1", True),
(19, "ListChoiceMode", "Использование", "0", False),
(19, "ListChoiceMode", "Использование", "1", True),
(28, "AutoChoiceIncomplete", "Использование", "1", True),
(28, "AutoChoiceIncomplete", "Использование", "2", "Авто"),
(31, "MarkRequiredComplete", "Использование", "0", False),
(31, "MarkRequiredComplete", "Использование", "1", True),
(31, "MarkRequiredComplete", "Использование", "2", "Авто"),
(31, "AutoMarkIncomplete", "Использование", "0", False),
(31, "AutoMarkIncomplete", "Использование", "1", True),
(31, "AutoMarkIncomplete", "Использование", "2", "Авто"),
(52, "ExtendedEdit", "Использование", "0", False),
(52, "ExtendedEdit", "Использование", "1", True),
(65, "ExtendedEditMultipleValues", "Использование", "0", False),
(65, "ExtendedEditMultipleValues", "Использование", "1", True),
],
)
def test_input_field_decodes_confirmed_nested_usage_and_layout_properties(
index: int,
name: str,
group: str,
code: str,
expected: bool | str,
) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(66)]
specific[0] = atom(36)
specific[index] = atom(code)
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"][group] if item["name"] == name)
assert prop["value"] == expected
assert prop["parameter_index"] == index
@pytest.mark.parametrize(
("index", "name", "code", "expected"),
[
(2, "Width", "0", "0"),
(2, "Width", "42", "42"),
(3, "Height", "0", "0"),
(3, "Height", "2", "2"),
(5, "VerticalStretch", "0", False),
(5, "VerticalStretch", "1", True),
(5, "VerticalStretch", "2", "Авто"),
(21, "ChoiceListHeight", "0", "0"),
(21, "ChoiceListHeight", "5", "5"),
(52, "AutoMaxHeight", "0", False),
(52, "AutoMaxHeight", "1", True),
(53, "MaxHeight", "0", "0"),
(53, "MaxHeight", "3", "3"),
],
)
def test_input_field_decodes_confirmed_nested_size_properties(
index: int,
name: str,
code: str,
expected: bool | str,
) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(66)]
specific[0] = atom(36)
specific[index] = atom(code)
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Расположение"] if item["name"] == name)
assert prop["value"] == expected
assert prop["parameter_index"] == index
@pytest.mark.parametrize(
("index", "name", "code", "expected"),
[
(8, "MultiLine", "1", True),
(8, "MultiLine", "2", False),
(14, "SpinButton", "0", False),
(14, "SpinButton", "1", True),
(14, "SpinButton", "2", "Авто"),
(24, "ChoiceFoldersAndItems", "0", "Items"),
(24, "ChoiceFoldersAndItems", "3", "Auto"),
(43, "EditTextUpdate", "0", "Auto"),
(43, "EditTextUpdate", "2", "OnValueChange"),
(46, "ChoiceButtonRepresentation", "0", "Auto"),
(46, "ChoiceButtonRepresentation", "2", "ShowInDropListAndInInputField"),
(48, "ChoiceHistoryOnInput", "0", "Auto"),
(48, "ChoiceHistoryOnInput", "1", "DontUse"),
],
)
def test_input_field_decodes_confirmed_nested_editing_properties(
index: int,
name: str,
code: str,
expected: bool | str,
) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(66)]
specific[0] = atom(36)
specific[index] = atom(code)
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(item for item in row["semantic"]["groups"]["Использование"] if item["name"] == name)
assert prop["value"] == expected
assert prop["parameter_index"] == index
def test_input_field_decodes_structured_style_properties() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
style_guid = "da164c9a-4836-4c07-ace9-1c2e2382eceb"
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(66)]
specific[0] = atom(36)
specific[37] = seq(atom(3), atom(3), seq(atom(-21)))
specific[38] = seq(atom(3), atom(3), seq(atom(0), atom(style_guid)))
specific[39] = seq(atom(3), atom(3), seq(atom(-1)))
specific[40] = seq(atom(7), atom(3), atom(0), atom(1), atom(100))
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
props = {item["name"]: item for item in row["semantic"]["groups"]["Оформление"]}
assert props["TextColor"]["value"] == "style:ButtonTextColor"
assert props["TextColor"]["parameter_index"] == 37
assert props["BackColor"]["value"] == {"kind": "StyleItem", "guid": style_guid, "status": "identity_pending"}
assert props["BackColor"]["parameter_index"] == 38
assert props["BorderColor"]["value"] == "style:FormBackColor"
assert props["BorderColor"]["parameter_index"] == 39
assert props["Font"]["value"] == "Авто"
assert props["Font"]["parameter_index"] == 40
def test_input_field_decodes_standard_and_custom_font_references() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
style_guid = "fa2a9ef2-00a1-44f4-a82c-6c7288dd62dc"
def decode(font):
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(66)]
specific[0] = atom(36)
specific[40] = font
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
return next(prop["value"] for prop in row["semantic"]["groups"]["Оформление"] if prop["name"] == "Font")
assert decode(seq(atom(7), atom(2), atom(0), seq(atom(-31)), atom(1), atom(100))) == "style:NormalTextFont"
assert decode(seq(atom(7), atom(2), atom(0), seq(atom(-32)), atom(1), atom(100))) == "style:LargeTextFont"
assert decode(seq(atom(7), atom(2), atom(0), seq(atom(0), atom(style_guid)), atom(1), atom(100))) == {
"kind": "StyleItem",
"guid": style_guid,
"status": "identity_pending",
}
def test_input_field_decodes_localized_formats_and_input_hint() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
localized = lambda value: seq(atom(1), atom(1), seq(atom("ru"), atom(value)))
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(66)]
specific[0] = atom(36)
specific[29] = localized("ЧДЦ=2; ЧН=0,00")
specific[30] = localized("ДФ=dd.MM.yyyy")
specific[44] = localized("Введите дату")
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
formatting = {prop["name"]: prop for prop in row["semantic"]["groups"]["Форматирование"]}
assert formatting["Format"]["value"] == "ЧДЦ=2; ЧН=0,00"
assert formatting["Format"]["parameter_index"] == 29
assert formatting["EditFormat"]["value"] == "ДФ=dd.MM.yyyy"
assert formatting["EditFormat"]["parameter_index"] == 30
hint = next(prop for prop in row["semantic"]["groups"]["Оформление"] if prop["name"] == "InputHint")
assert hint["value"] == "Введите дату"
assert hint["parameter_index"] == 44
@pytest.mark.parametrize(("code", "expected"), [("1", True), ("2", False)])
def test_input_field_decodes_password_mode(code: str, expected: bool) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(66)]
specific[0] = atom(36)
specific[7] = atom(code)
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(prop for prop in row["semantic"]["groups"]["Использование"] if prop["name"] == "PasswordMode")
assert prop["value"] is expected
assert prop["parameter_index"] == 7
def test_input_field_decodes_choice_parameter_link_names() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(66)]
specific[0] = atom(36)
specific[64] = seq(
atom(5007),
atom(2),
atom("Отбор.Банк"),
atom(1),
seq(atom(161)),
atom(0),
atom(""),
atom(""),
atom("Отбор.Владелец"),
atom(2),
seq(atom(1)),
seq(atom(0), atom("ba7dcb3b-b8b9-4d44-ab5f-56f7a228f10d")),
atom(0),
atom(""),
atom(""),
)
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(
prop
for prop in row["semantic"]["groups"]["Использование"]
if prop["name"] == "ChoiceParameterLinks"
)
assert prop["value"] == {
"count": 2,
"links": [{"name": "Отбор.Банк"}, {"name": "Отбор.Владелец"}],
}
assert prop["parameter_index"] == 64
def test_input_field_does_not_expose_empty_or_malformed_choice_parameter_links() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
for links in (seq(atom(5007), atom(0)), seq(atom(5007), atom(2), atom("Отбор.Банк"))):
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(66)]
specific[0] = atom(36)
specific[64] = links
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
assert all(
prop["name"] != "ChoiceParameterLinks"
for props in row["semantic"]["groups"].values()
for prop in props
)
def test_input_field_decodes_mask_and_numeric_choice_list() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
def choice(value: int, presentation: str) -> dict:
return seq(
atom("#"),
atom("0e704aa2-07bd-48b9-8223-a0212c4d5fc2"),
seq(
atom(0),
atom(1),
seq(atom("N"), atom(value)),
atom("00000000-0000-0000-0000-000000000000"),
atom("00000000-0000-0000-0000-000000000000"),
seq(atom(1), atom(1), seq(atom("ru"), atom(presentation))),
),
)
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(66)]
specific[0] = atom(36)
specific[1] = seq(
atom(3),
atom(3),
atom(""),
choice(2, "Не выполненные"),
atom(""),
choice(1, "Выполненные"),
atom(""),
choice(0, "Все задачи"),
)
specific[18] = atom("999-999")
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
usage = {prop["name"]: prop for prop in row["semantic"]["groups"]["Использование"]}
assert usage["Mask"]["value"] == "999-999"
assert usage["Mask"]["parameter_index"] == 18
assert usage["ChoiceList"]["value"] == {
"count": 3,
"items": [
{"presentation": "Не выполненные", "value": {"kind": "Number", "value": "2"}},
{"presentation": "Выполненные", "value": {"kind": "Number", "value": "1"}},
{"presentation": "Все задачи", "value": {"kind": "Number", "value": "0"}},
],
}
assert usage["ChoiceList"]["parameter_index"] == 1
def test_input_field_does_not_guess_unsupported_choice_list_value_type() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(66)]
specific[0] = atom(36)
specific[1] = seq(atom(3), atom(1), atom(""), seq(atom("#"), atom("guid"), seq(atom(0), atom(1), seq(atom("R"), atom("ref")))))
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
assert all(
prop["name"] != "ChoiceList"
for props in row["semantic"]["groups"].values()
for prop in props
)
def test_input_field_decodes_pending_enum_choice_list_reference() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
type_guid = "e36f8d8f-9d89-433b-bf81-6511e4141522"
value_guid = "2d31cb6b-c0f4-4554-b5f0-b7343aa22a99"
encoded_item = seq(
atom("#"),
atom("0e704aa2-07bd-48b9-8223-a0212c4d5fc2"),
seq(atom(0), atom(0), seq(atom("U")), atom(type_guid), atom(value_guid), seq(atom(1), atom(0))),
)
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(5)
specific = [atom(0) for _ in range(12)]
specific[0] = atom(8)
specific[1] = seq(atom(3), atom(1), atom(""), encoded_item)
values[39] = seq(*specific)
row = {"marker": "37", "type_name": "Поле ввода", "semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(prop for prop in row["semantic"]["groups"]["Использование"] if prop["name"] == "ChoiceList")
assert prop["value"]["items"][0]["value"] == {
"kind": "EnumValue",
"type_guid": type_guid,
"value_guid": value_guid,
"status": "identity_pending",
}
@pytest.mark.parametrize(("code", "expected"), [("0", False), ("1", True)])
def test_label_field_decodes_confirmed_hiperlink_flag(code: str, expected: bool) -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(1)
specific = [atom(0) for _ in range(20)]
specific[0] = atom(11)
specific[7] = atom(code)
values[39] = seq(*specific)
row = {
"marker": "37",
"type_code": "1",
"type_name": "Поле надписи",
"semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}},
}
enrich_input_field_specific_semantics(row, seq(*values))
prop = next(prop for prop in row["semantic"]["groups"]["Использование"] if prop["name"] == "Hiperlink")
assert prop["value"] is expected
assert prop["parameter_index"] == 7
def test_report_label_and_chart_fields_decode_layout_properties() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
label_values = ["0"] * 40
label_values[0] = "37"
label_values[5] = "1"
label_values[39] = "{11,50,5,2,0,2,0,0}"
label_row = {"marker": "37", "type_code": "1", "semantic": {"groups": {}}}
enrich_input_field_specific_semantics(label_row, parse_brace_text("{" + ",".join(label_values) + "}"))
chart_values = ["0"] * 40
chart_values[0] = "37"
chart_values[5] = "11"
chart_values[39] = "{1,50,8,1,1}"
chart_row = {"marker": "37", "type_code": "11", "semantic": {"groups": {}}}
enrich_input_field_specific_semantics(chart_row, parse_brace_text("{" + ",".join(chart_values) + "}"))
label_props = {prop["name"]: prop["value"] for group in label_row["semantic"]["groups"].values() for prop in group}
chart_props = {prop["name"]: prop["value"] for group in chart_row["semantic"]["groups"].values() for prop in group}
assert label_props["Ширина"] == "50"
assert label_props["Высота"] == "5"
assert label_props["ВертикальноеПоложениеВГруппе"] == "Top"
assert label_props["РастягиватьПоВертикали"] is False
assert chart_props["Высота"] == "8"
def test_graphical_schema_field_decodes_size_and_edit_mode() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
values = ["0"] * 40
values[0] = "37"
values[5] = "14"
values[39] = "{3,80,25,0,0}"
row = {"marker": "37", "type_code": "14", "semantic": {"groups": {}}}
enrich_input_field_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
assert props["Ширина"] == "80"
assert props["Высота"] == "25"
assert props["Edit"] is False
def test_report_styles_decode_tooltip_background_and_web_green() -> None:
from parser.form_payload import style_value_presentation
assert style_value_presentation(parse_brace_text("{3,3,{-23}}")) == "style:ToolTipBackColor"
assert style_value_presentation(parse_brace_text("{3,2,{27}}")) == "web:DarkGreen"
def test_input_field_type_link_is_resolved_to_public_current_data_path() -> None:
from parser.form_payload import enrich_input_field_specific_semantics, enrich_item_data_paths
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(40)]
values[0] = atom(37)
values[5] = atom(2)
specific = [atom(0) for _ in range(66)]
specific[0] = atom(36)
specific[42] = seq(
atom(3),
atom(2),
seq(atom(1), atom("02023637-7868-4a5f-8576-835a76e0c9ba")),
seq(atom(1)),
atom(0),
)
values[39] = seq(*specific)
row = {
"marker": "37",
"type_code": "2",
"type_name": "Поле ввода",
"data_path_attribute_id": "1",
"data_path_field_id": "2",
"parameters": [],
"semantic": {"groups": {}, "unmapped_parameters": [{"index": 42}], "coverage": {"mapped": 0, "unmapped": 1}},
}
enrich_input_field_specific_semantics(row, seq(*values))
enrich_item_data_paths(
[row],
[
{
"id": "1",
"name": "ДеревоНастроек",
"dynamic_list_fields": [
{"id": "1", "name": "Настройка", "data_name": "Настройка"},
{"id": "2", "name": "Значение", "data_name": "Значение"},
],
}
],
)
prop = next(prop for prop in row["semantic"]["groups"]["Использование"] if prop["name"] == "TypeLink")
assert prop["value"] == {
"path_to_data": "Items.ДеревоНастроек.CurrentData.Настройка",
"link_item": 0,
}
assert prop["parameter_index"] == 42
assert "_type_link_reference" not in row
def test_footer_data_path_reference_and_object_tabular_path_are_resolved() -> None:
from parser.form_payload import enrich_item_data_paths, footer_data_path_reference
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
owner_guid = "82363dd2-24bb-4e24-a05a-df6e01759f8f"
field_guid = "75518f7c-ee35-4bd7-a107-acda05e715c6"
primary = seq(atom(3), seq(atom(1)), seq(atom(0), atom(owner_guid)), seq(atom(0), atom(field_guid)))
footer = seq(atom(3), seq(atom(1)), seq(atom(0), atom(owner_guid)), seq(atom(101000000), atom(field_guid)))
assert footer_data_path_reference(primary, footer) == {"aggregate": "Total", "field_guid": field_guid}
table = {"marker": "55", "name": "ПозицииЧека", "path": "1", "parameters": [], "semantic": {"groups": {}}}
field = {
"marker": "37",
"name": "ПозицииЧекаКоличество",
"path": "1.2",
"data_path_attribute_id": "0",
"parameters": [],
"_footer_data_path_reference": {"aggregate": "Total", "field_guid": field_guid},
"semantic": {"groups": {}, "unmapped_parameters": [{"index": 12}], "coverage": {"mapped": 0, "unmapped": 1}},
}
enrich_item_data_paths([table, field], [{"id": "1", "name": "Объект"}])
assert field["path_to_data"] == "Объект.ПозицииЧека.Количество"
main = {prop["name"]: prop for prop in field["semantic"]["groups"]["Основные"]}
assert main["FooterDataPath"]["value"] == "Объект.ПозицииЧека.TotalКоличество"
assert main["FooterDataPath"]["parameter_index"] == 12
assert "_footer_data_path_reference" not in field
aggregate_primary = seq(atom(3), seq(atom(1)), seq(atom(0), atom(owner_guid)), seq(atom(101000000), atom(field_guid)))
assert footer_data_path_reference(aggregate_primary, seq(atom(0))) == {
"aggregate": "Total",
"field_guid": field_guid,
"primary_aggregate": True,
}
total = {
"marker": "37",
"name": "ПозицииЧекаКоличествоИтог",
"path": "1.3",
"guids_sample": [field_guid],
"_footer_data_path_reference": {
"aggregate": "Total",
"field_guid": field_guid,
"primary_aggregate": True,
},
"semantic": {"groups": {}, "unmapped_parameters": [{"index": 12}], "coverage": {"mapped": 0, "unmapped": 1}},
}
field["guids_sample"] = [field_guid]
table["guids_sample"] = [field_guid]
enrich_item_data_paths([table, field, total], [{"id": "1", "name": "Объект"}])
assert total["path_to_data"] == "Объект.ПозицииЧека.TotalКоличество"
assert next(prop for prop in total["semantic"]["groups"]["Основные"] if prop["name"] == "ПутьКДанным")["value"] == total["path_to_data"]
def test_form_additional_columns_are_decoded_from_attribute_section_tail() -> None:
from parser.form_payload import additional_column_items
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
owner_guid = "3e998dcc-3dfd-4562-bbb0-7684217acc79"
owner_reference = seq(atom(2), seq(atom(1)), seq(atom(0), atom(owner_guid)))
group = seq(
atom(0),
owner_reference,
atom(2),
seq(atom(5), atom(1), atom(0), atom("Пометка")),
seq(atom(5), atom(2), atom(0), atom("ИспользоватьХарактеристики")),
)
tree = seq(atom(0), atom(0), atom(0), seq(atom(4), atom(1), seq(atom(9)), group))
owner = {
"marker": "55",
"name": "Запасы",
"path_to_data": "Объект.Запасы",
"guids_sample": [owner_guid],
}
rows, total, truncated = additional_column_items(tree, [owner])
assert total == 2
assert truncated is False
assert [(row["id"], row["name"], row["path_to_data"]) for row in rows] == [
("1", "Пометка", "Объект.Запасы.Пометка"),
("2", "ИспользоватьХарактеристики", "Объект.Запасы.ИспользоватьХарактеристики"),
]
def test_legacy_marker_35_input_field_is_included_with_existing_semantics() -> None:
from parser.form_payload import item_records
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(57)]
values[0] = atom(35)
values[1] = seq(atom(70), atom("02023637-7868-4a5f-8576-835a76e0c9ba"))
values[5] = atom(2)
values[6] = atom("ПроксиПароль")
values[9] = seq(atom(1), atom(1), seq(atom("ru"), atom("Пароль")))
values[11] = seq(atom(1), seq(atom(13)))
values[13] = atom(1)
values[14] = atom(0)
values[15] = atom(2)
values[43] = atom(1)
specific = [atom(0) for _ in range(66)]
specific[0] = atom(32)
specific[2] = atom(25)
specific[7] = atom(1)
specific[40] = seq(atom(7), atom(3), atom(0), atom(1), atom(100))
values[39] = seq(*specific)
items, total, truncated = item_records(seq(atom(0), seq(*values)), limit=10, include_parameters=True)
assert total == 1
assert truncated is False
assert len(items) == 1
row = items[0]
assert row["marker"] == "35"
assert row["marker_name"] == "InputField"
assert row["name"] == "ПроксиПароль"
assert row["title"] == "Пароль"
assert row["type_name"] == "Поле ввода"
assert row["data_path_attribute_id"] == "13"
usage = {prop["name"]: prop["value"] for prop in row["semantic"]["groups"]["Использование"]}
assert usage["PasswordMode"] is True
layout = {prop["name"]: prop["value"] for prop in row["semantic"]["groups"]["Расположение"]}
assert layout["Width"] == "25"
appearance = {prop["name"]: prop["value"] for prop in row["semantic"]["groups"]["Оформление"]}
assert appearance["Font"] == "Авто"
def test_shifted_legacy_marker_35_input_field_layout_is_decoded() -> None:
from parser.form_payload import item_records
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(58)]
values[0] = atom(35)
values[1] = seq(atom(1), atom("02023637-7868-4a5f-8576-835a76e0c9ba"))
values[5] = atom("")
values[6] = atom(2)
values[7] = atom("Период")
values[10] = seq(atom(1), atom(1), seq(atom("ru"), atom("Период")))
values[12] = seq(atom(1), seq(atom(13)))
values[14] = atom(1)
values[15] = atom(0)
values[16] = atom(2)
values[44] = atom(1)
specific = [atom(0) for _ in range(66)]
specific[0] = atom(32)
specific[2] = atom(25)
specific[15] = atom(1)
values[40] = seq(*specific)
items, total, truncated = item_records(seq(atom(0), seq(*values)), limit=10, include_parameters=True)
assert total == 1
assert truncated is False
row = items[0]
assert row["name"] == "Период"
assert row["name_path"].endswith(".7")
assert row["type_code"] == "2"
assert row["type_name"] == "Поле ввода"
assert row["data_path_attribute_id"] == "13"
main = {prop["name"]: prop["value"] for prop in row["semantic"]["groups"]["Основные"]}
usage = {prop["name"]: prop["value"] for prop in row["semantic"]["groups"]["Использование"]}
assert main["Имя"] == "Период"
assert main["Доступность"] is True
assert usage["OpenButton"] is True
def test_register_record_legacy_field_data_paths_use_public_item_names() -> None:
from parser.form_payload import enrich_item_data_paths
attributes = [{"id": "1", "name": "Запись"}]
items = [
{"marker": "35", "id": "1", "name": "Период", "path": "1.1", "data_path_attribute_id": "1", "data_path_field_id": "-2", "parameters": [], "semantic": {"groups": {}}},
{"marker": "35", "id": "3", "name": "СтруктурнаяЕдиница", "path": "1.3", "data_path_attribute_id": "1", "data_path_field_id": "0", "parameters": [], "semantic": {"groups": {}}},
]
enrich_item_data_paths(items, attributes)
assert items[0]["path_to_data"] == "Запись.Period"
assert items[1]["path_to_data"] == "Запись.СтруктурнаяЕдиница"
def test_enrich_form_style_references_resolves_public_style_name(monkeypatch: pytest.MonkeyPatch) -> None:
guid = "da164c9a-4836-4c07-ace9-1c2e2382eceb"
prop = {"name": "BackColor", "value": {"kind": "StyleItem", "guid": guid, "status": "identity_pending"}}
profile = {"items": [{"semantic": {"groups": {"Оформление": [prop]}}}]}
monkeypatch.setattr(
adapter_server,
"metadata_cache_lookup_guid",
lambda base_id, wanted_guid: {"kind": "StyleItem", "name": "ФонУправляющегоПоля", "guid": wanted_guid, "match_by": "cache"},
)
adapter_server.enrich_form_style_references("upo_test", profile)
assert prop["value"] == "style:ФонУправляющегоПоля"
assert prop["style_reference"] == {
"kind": "StyleItem",
"guid": guid,
"name": "ФонУправляющегоПоля",
"ref": "StyleItem.ФонУправляющегоПоля",
"status": "ok",
"resolved_by": "cache",
}
def test_enrich_form_choice_list_references_resolves_public_enum_value(monkeypatch: pytest.MonkeyPatch) -> None:
type_guid = "e36f8d8f-9d89-433b-bf81-6511e4141522"
value_guid = "2d31cb6b-c0f4-4554-b5f0-b7343aa22a99"
owner_guid = "4cddc788-29df-4050-8d5e-7c0af0587d3a"
item = {
"presentation": "",
"value": {
"kind": "EnumValue",
"type_guid": type_guid,
"value_guid": value_guid,
"status": "identity_pending",
},
}
profile = {
"items": [
{
"semantic": {
"groups": {
"Использование": [
{"name": "ChoiceList", "value": {"count": 1, "items": [item]}}
]
}
}
}
]
}
monkeypatch.setattr(
adapter_server,
"resolve_type_guids",
lambda *_args, **_kwargs: {
type_guid: {"kind": "Enum", "name": "Варианты", "owner_guid": owner_guid, "status": "ok"}
},
)
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *_args, **_kwargs: (b"enum", {}, None))
monkeypatch.setattr(
adapter_server,
"decode_config_object_full",
lambda *_args, **_kwargs: {
"semantic": {
"sections": [
{
"category": "EnumValue",
"records": [
{
"identity": {
"guid": value_guid,
"name": "Сложение",
"synonyms": {"ru": "Сложение"},
}
}
],
}
]
}
},
)
adapter_server.enrich_form_choice_list_references("upo_test", profile)
assert item == {
"presentation": "Сложение",
"value": {
"kind": "EnumValue",
"ref": "Enum.Варианты.EnumValue.Сложение",
"name": "Сложение",
"status": "ok",
},
}
def test_enrich_form_command_references_adds_public_common_command(monkeypatch: pytest.MonkeyPatch) -> None:
guid = "64a59186-ea83-44d7-b433-bf93778db6f9"
profile = {"items": [{"name": "Кнопка", "command_reference": {"guid": guid}, "semantic": {"groups": {}}}]}
monkeypatch.setattr(
adapter_server,
"metadata_cache_lookup_guid",
lambda base_id, wanted_guid: {"kind": "CommonCommand", "name": "СвязанныеДокументы", "guid": wanted_guid},
)
adapter_server.enrich_form_command_references("upo_test", profile)
row = profile["items"][0]
assert row["command_name"] == "CommonCommand.СвязанныеДокументы"
assert row["command_reference"]["status"] == "ok"
assert row["semantic"]["groups"]["Основные"][-1]["name"] == "ИмяКоманды"
def test_enrich_form_command_references_reads_direct_sql_guid_when_cache_is_empty(monkeypatch: pytest.MonkeyPatch) -> None:
guid = "64a59186-ea83-44d7-b433-bf93778db6f9"
profile = {"items": [{"name": "Кнопка", "command_reference": {"guid": guid}, "semantic": {"groups": {}}}]}
seen: dict[str, Any] = {}
monkeypatch.setattr(adapter_server, "metadata_cache_lookup_guid", lambda base_id, wanted_guid: None)
def fake_read(base_id: str, table: str, file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, Any], None]:
seen.update({"base_id": base_id, "table": table, "file_name": file_name})
return b"command", {}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)
monkeypatch.setattr(
adapter_server,
"config_identity_from_bytes",
lambda data: {"guid": guid, "name": "СвязанныеДокументы", "synonyms": {"ru": "Связанные документы"}},
)
adapter_server.enrich_form_command_references("upo_test", profile)
assert seen == {"base_id": "upo_test", "table": "Config", "file_name": guid}
assert profile["items"][0]["command_name"] == "CommonCommand.СвязанныеДокументы"
def test_form_public_sections_promotes_command_action_from_semantic() -> None:
sections = adapter_server.form_public_sections(
{
"commands": [
{
"name": "ЗаменаДомена",
"semantic": {
"groups": {
"Основные": [
{"name": "Action", "value": "ЗаменаДомена"},
]
}
},
}
],
"command_links": [],
}
)
assert sections["commands"][0]["action"] == "ЗаменаДомена"
assert sections["commands"][0]["handler"] == "ЗаменаДомена"
def test_public_metadata_item_exposes_extension_origin_from_dbnames_route() -> None:
item = public_metadata_item(
{
"likely_name": "фс_ЗдоровоеПитания",
"type": {"kind": "boolean", "presentation": "Булево"},
"storage_routes": [
{
"source": "DBNames-Ext-f96a0c45-3eff-11f1-8287-005056b0d483",
"storage_role": "Fld",
}
],
},
{},
extensions_by_guid={
"f96a0c45-3eff-11f1-8287-005056b0d483": {
"name": "фс_ДоработкиОбщее",
"guid": "f96a0c45-3eff-11f1-8287-005056b0d483",
"active": True,
}
},
)
assert item["origin"]["source"] == "extension"
assert item["origin"]["extension"]["name"] == "фс_ДоработкиОбщее"
assert "storage_routes" not in item
def test_definition_find_allows_extension_only_without_object_selector() -> None:
result = adapter_server.validate_metadata_definition_find_payload(
{"base_id": "upo_test", "query": "фс_ДатаМобайлСписокДляВыгрузки", "areas": ["extensions"]}
)
assert result is None
def test_definition_find_allows_metadata_without_object_selector() -> None:
result = adapter_server.validate_metadata_definition_find_payload(
{"base_id": "upo_test", "query": "АвансовыйОтчет", "areas": ["metadata"]}
)
assert result is None
result = adapter_server.validate_metadata_definition_find_payload(
{
"base_id": "upo_test",
"query": "Реквизит",
"areas": ["object"],
"object_type": "Обработка",
"object_name": "ТестоваяОбработка",
}
)
assert result is None
result = adapter_server.validate_metadata_definition_find_payload(
{"base_id": "upo_test", "query": "ФормаДокумента", "areas": ["form"]}
)
assert result is not None
assert result["status"] == "invalid_argument"
assert result["argument"] == "name"
def test_configuration_definition_matches_from_metadata_cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
adapter_server.metadata_cache_upsert(
config,
{
"kind": "Document",
"kind_ru": "Документ",
"public_kind": "document",
"name": "АвансовыйОтчет",
"synonym": "Авансовый отчет",
"guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
"source": "base",
},
)
matches, diagnostics = adapter_server.metadata_configuration_definition_matches(
base_id="upo_test",
query="Авансовый отчет",
max_matches=5,
use_cache=True,
)
assert diagnostics == [{"area": "metadata", "source": "metadata_cache", "cache": "hit", "matches": 1}]
assert len(matches) == 1
assert matches[0]["area"] == "metadata"
assert matches[0]["kind"] == "Документ"
assert matches[0]["match_by"] == "synonym_exact"
assert matches[0]["origin"]["source"] == "configuration"
assert matches[0]["read_selector"]["method"] == "metadata.object.get"
def test_extension_definition_matches_resolve_owner_extension(monkeypatch: pytest.MonkeyPatch) -> None:
class Record:
guid = "d7c766c3-3ad6-41f5-92c0-c871981ad269"
source = "DBNames-Ext-f96a0c45-3eff-11f1-8287-005056b0d483"
storage_role = "InfoRg"
sql_number = 62970
payload_text = '{1,{3,{1,0,d7c766c3-3ad6-41f5-92c0-c871981ad269},"фс_ДатаМобайлСписокДляВыгрузки",{1,"ru","DataMobile Список для выгрузки"}}}'
payload = payload_text.encode("utf-8")
monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *args, **kwargs: ([Record()], None))
monkeypatch.setattr(
adapter_server,
"extension_map_by_guid",
lambda base_id: {
"f96a0c45-3eff-11f1-8287-005056b0d483": {
"name": "фс_ДоработкиОбщее",
"guid": "f96a0c45-3eff-11f1-8287-005056b0d483",
"active": True,
}
},
)
monkeypatch.setattr(adapter_server, "storage_files_list", lambda payload: {"status": "ok", "files": [{"FileName": "cas"}]})
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (payload, {}, None))
monkeypatch.setattr(adapter_server, "read_storage_files_bytes", lambda *args, **kwargs: ({"cas": payload}, {}, None))
monkeypatch.setattr(
adapter_server,
"payload_text_from_bytes",
lambda data: {"text": payload_text},
)
monkeypatch.setattr(
adapter_server,
"config_identity_from_bytes",
lambda data: {
"guid": "d7c766c3-3ad6-41f5-92c0-c871981ad269",
"name": "фс_ДатаМобайлСписокДляВыгрузки",
"synonyms": {"ru": "DataMobile Список для выгрузки"},
},
)
matches, diagnostics = adapter_server.metadata_extension_definition_matches(
base_id="upo_test",
query="фс_ДатаМобайлСписокДляВыгрузки",
max_files=10,
max_matches=5,
timeout_seconds=30,
)
assert diagnostics[-1]["indexed_extension_guids"] == 1
assert len(matches) == 1
assert matches[0]["origin"]["extension"]["name"] == "фс_ДоработкиОбщее"
assert matches[0]["kind"] == "РегистрСведений"
def test_extension_definition_cache_lookup_returns_public_match(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
adapter_server.metadata_extension_definition_cache_upsert(
config,
base_id="upo_test",
identity={
"guid": "d7c766c3-3ad6-41f5-92c0-c871981ad269",
"name": "фс_ДатаМобайлСписокДляВыгрузки",
"synonyms": {"ru": "DataMobile Список для выгрузки"},
},
source_item={
"extension": {
"guid": "f96a0c45-3eff-11f1-8287-005056b0d483",
"name": "фс_ДоработкиОбщее",
"active": True,
},
"storage_role": "InfoRg",
},
file_name="cas",
)
matches, marker = adapter_server.metadata_extension_definition_cache_lookup(
"upo_test",
query="фс_ДатаМобайлСписокДляВыгрузки",
max_matches=5,
)
assert marker is None
assert len(matches) == 1
assert matches[0]["match_by"] == "name_exact"
assert matches[0]["origin"]["source"] == "extension"
assert matches[0]["origin"]["extension"]["name"] == "фс_ДоработкиОбщее"
assert matches[0]["location"]["presentation"] == "Расширение.фс_ДоработкиОбщее.РегистрСведений.фс_ДатаМобайлСписокДляВыгрузки"
contains_matches, _ = adapter_server.metadata_extension_definition_cache_lookup(
"upo_test",
query="ДатаМобайл",
max_matches=5,
)
assert len(contains_matches) == 1
assert contains_matches[0]["match_by"] == "name_contains"
def test_extension_definition_cache_complete_marker_returns_fast_not_found(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
adapter_server.metadata_guid_index_upsert(
config,
{
"guid": adapter_server.EXTENSION_DEFINITION_CACHE_MARKER_GUID,
"guid_role": adapter_server.EXTENSION_DEFINITION_CACHE_MARKER_ROLE,
"kind": "ExtensionDefinitionCache",
"kind_ru": "Индекс определений расширений",
"name": "Индекс определений расширений",
"presentation": "Индекс определений расширений",
"payload": {"status": "complete", "indexed_definitions": 1},
},
)
def fail_live_scan(*args, **kwargs):
raise AssertionError("live scan must not run when complete cache marker exists")
monkeypatch.setattr(adapter_server, "live_dbnames_records", fail_live_scan)
matches, diagnostics = adapter_server.metadata_extension_definition_matches(
base_id="upo_test",
query="НетТакогоИмени",
max_files=10,
max_matches=5,
timeout_seconds=30,
use_cache=True,
)
assert matches == []
assert diagnostics == [{"area": "extensions", "cache": "hit_empty", "indexed_definitions": 1}]
def test_payload_decoder_prefers_cyrillic_cp1251_over_false_utf16() -> None:
text = '{1,"\u0444\u0441_\u041f\u0424_MXL_\u041c\u0435\u043d\u044e\u0417\u0430\u041f\u0435\u0440\u0438\u043e\u0434"}'
decoded, encoding = try_decode(text.encode("cp1251"))
assert decoded == text
assert encoding == "cp1251"
def test_identity_matching_repairs_latin1_cp1251_mojibake() -> None:
name = "\u0444\u0441_\u041f\u0424_MXL_\u041c\u0435\u043d\u044e\u0417\u0430\u041f\u0435\u0440\u0438\u043e\u0434"
mojibake_name = name.encode("cp1251").decode("latin1")
assert adapter_server.best_text_variant(mojibake_name) == name
assert adapter_server.extension_definition_identity_match_by({"name": mojibake_name, "synonyms": {}}, name) == "name_exact"
def test_extension_objects_find_uses_manifest_descriptor_route(monkeypatch: pytest.MonkeyPatch) -> None:
descriptor_key = "a" * 40
part_key = "b" * 40
object_base_id = "7c6dc711-02f5-4e6a-9f37-7dd57d8942b3"
guid = "11111111-2222-3333-4444-555555555555"
name = "\u0444\u0441_\u041f\u0424_MXL_\u041c\u0435\u043d\u044e\u0417\u0430\u041f\u0435\u0440\u0438\u043e\u0434"
mojibake_name = name.encode("cp1251").decode("latin1")
descriptor_payload = f'{{1,{{3,{{1,0,{guid}}},"{mojibake_name}",{{1,"ru","Test template"}}}}}}'.encode("latin1")
monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", lambda *args, **kwargs: ({}, None))
monkeypatch.setattr(
adapter_server,
"live_extension_manifests",
lambda *args, **kwargs: (
[
{
"status": "ok",
"root_cas_key": "c" * 40,
"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"},
"entries": [
{"object_id": object_base_id, "object_base_id": object_base_id, "suffix": "", "cas_key": descriptor_key},
{"object_id": f"{object_base_id}.0", "object_base_id": object_base_id, "suffix": ".0", "cas_key": part_key},
],
}
],
[],
),
)
monkeypatch.setattr(adapter_server, "read_storage_files_bytes", lambda *args, **kwargs: ({descriptor_key: descriptor_payload}, {}, None))
result = adapter_server.extension_objects_find(
{
"base_id": "upo_test",
"kind": "Template",
"query": name,
"include_storage": True,
"limit": 1,
}
)
assert result["status"] == "ok"
assert result["objects"][0]["name"] == name
assert result["objects"][0]["kind"] == "Template"
assert result["objects"][0]["route"]["route_type"] == "extension_manifest_cas"
assert result["objects"][0]["route"]["file_name"] == descriptor_key
assert result["objects"][0]["route"]["manifest_entries"] == 2
assert result["counts"]["manifest_matches"] == 1
def test_extension_objects_find_uses_validated_source_cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
descriptor_key = "a" * 40
part_key = "b" * 40
object_base_id = "7c6dc711-02f5-4e6a-9f37-7dd57d8942b3"
name = "\u0444\u0441_\u041f\u0424_MXL_\u041c\u0435\u043d\u044e\u0417\u0430\u041f\u0435\u0440\u0438\u043e\u0434"
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", lambda *args, **kwargs: ({}, None))
monkeypatch.setattr(
adapter_server,
"live_extension_manifests",
lambda *args, **kwargs: (
[
{
"root_cas_key": "c" * 40,
"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"},
"entries": [
{"object_id": object_base_id, "object_base_id": object_base_id, "suffix": "", "cas_key": descriptor_key},
{"object_id": f"{object_base_id}.0", "object_base_id": object_base_id, "suffix": ".0", "cas_key": part_key},
],
}
],
[],
),
)
route = {
"route_type": "extension_manifest_cas",
"table": "ConfigCAS",
"file_name": descriptor_key,
"manifest_entries": 2,
"manifest_entry": {
"object_id": object_base_id,
"object_base_id": object_base_id,
"suffix": "",
"cas_key": descriptor_key,
"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"},
"root_cas_key": "c" * 40,
},
}
adapter_server.extension_route_cache_upsert(
config,
{
"kind": "Template",
"kind_ru": "Макет",
"name": name,
"guid": descriptor_key,
"origin": {"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"}},
"route": route,
"manifest_entries": [],
},
descriptor_payload_sha1="sha",
)
result = adapter_server.extension_objects_find({"base_id": "upo_test", "kind": "Template", "query": name, "limit": 1})
assert result["status"] == "ok"
assert result["objects"][0]["match_by"] == "source_cache"
assert result["objects"][0]["freshness"]["status"] == "fresh"
assert result["counts"]["source_cache_validated"] == 1
assert result["counts"]["manifest_matches"] == 0
def test_extension_objects_find_skips_deep_scan_for_extension_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "99999999-9999-9999-9999-999999999999"
descriptor_key = "a" * 40
match = {
"kind": "CommonForm",
"kind_ru": "ОбщаяФорма",
"name": "TestForm",
"guid": "11111111-2222-3333-4444-555555555555",
"match_by": "manifest_descriptor",
"origin": {"source": "extension", "extension": {"name": "test2", "guid": extension_guid}, "status": "ok"},
"route": {"route_type": "extension_manifest_cas", "table": "ConfigCAS", "file_name": descriptor_key},
"read_selectors": {},
}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda base_id, extension, method: (extension_guid, None))
monkeypatch.setattr(adapter_server, "extension_route_cache_lookup", lambda *args, **kwargs: [])
monkeypatch.setattr(
adapter_server,
"extension_manifest_object_matches",
lambda *args, **kwargs: ([match], [], {"manifest_count": 1, "descriptor_entries": 1, "descriptor_payloads_read": 1}),
)
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_forms_search",
lambda *args, **kwargs: {"status": "not_found", "forms": [], "counts": {"scanned": 0}},
)
def fail_storage_scan(payload: dict[str, Any]) -> dict[str, Any]:
raise AssertionError("ConfigCAS scan must be explicit for extension-scoped lookups")
monkeypatch.setattr(adapter_server, "storage_files_list", fail_storage_scan)
result = adapter_server.extension_objects_find({"base_id": "upo_test", "extension": "test2", "limit": 50})
assert result["status"] == "ok"
assert result["objects"] == [match]
assert result["counts"]["scanned_payloads"] == 0
assert result["counts"]["configcas_scan_skipped"] is True
assert result["query"]["full_scan"] is False
assert result["diagnostics"][0]["reason"] == "full_scan_disabled_for_extension"
def test_extension_objects_find_defaults_to_working_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
saved_guid = "caee4437-14b6-436f-98e3-360c4577f20d"
saved_file = f"{extension_guid}__{saved_guid}.0"
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda base_id, extension, method: (extension_guid, None))
monkeypatch.setattr(adapter_server, "extension_route_cache_lookup", lambda *args, **kwargs: [])
monkeypatch.setattr(
adapter_server,
"extension_manifest_object_matches",
lambda *args, **kwargs: ([], [], {"manifest_count": 0, "descriptor_entries": 0, "descriptor_payloads_read": 0}),
)
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_forms_search",
lambda *args, **kwargs: {
"status": "ok",
"forms": [
{
"table": "ConfigCASSave",
"file_name": saved_file,
"name": "tt_Форма6",
"form": {
"identity": {
"source": "saved_state_descriptor",
"name": "tt_Форма6",
"synonym": "Форма 6",
"guid": saved_guid,
"descriptor_file_name": saved_file[:-2],
}
},
}
],
"counts": {"scanned": 2},
},
)
result = adapter_server.extension_objects_find({"base_id": "upo_test", "extension": "test2", "limit": 50})
assert result["status"] == "ok"
assert result["query"]["state"] == "working"
assert result["objects"][0]["name"] == "tt_Форма6"
assert result["objects"][0]["activation_state"] == "saved_only"
assert result["objects"][0]["route"]["table"] == "ConfigCASSave"
assert result["objects"][0]["read_selectors"]["form_decode"]["table"] == "ConfigCASSave"
assert result["counts"]["saved_state_matches"] == 1
assert result["counts"]["activation_state"] == {"saved_only": 1}
def test_extension_objects_find_decodes_saved_calculation_register(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
file_name = f"{extension_guid}__{object_guid}"
descriptor = '{1,{21,"tt_CalcRegister1"},0,0,0,0,0,0,0,0}'.encode("utf-8")
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda base_id, extension, method: (extension_guid, None))
monkeypatch.setattr(adapter_server, "extension_route_cache_lookup", lambda *args, **kwargs: [])
monkeypatch.setattr(
adapter_server,
"extension_manifest_object_matches",
lambda *args, **kwargs: ([], [], {"manifest_count": 0, "descriptor_entries": 0, "descriptor_payloads_read": 0}),
)
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {"status": "ok", "files": [{"FileName": file_name}]},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({file_name: descriptor}, {"database": "upo_test"}, None),
)
result = adapter_server.extension_objects_find(
{
"base_id": "upo_test",
"extension": "test2",
"kind": "CalculationRegister",
"query": "tt_CalcRegister1",
"limit": 20,
}
)
assert result["status"] == "ok"
assert result["objects"][0]["kind"] == "CalculationRegister"
assert result["objects"][0]["name"] == "tt_CalcRegister1"
assert result["objects"][0]["guid"] == object_guid
assert result["objects"][0]["activation_state"] == "saved_only"
assert result["objects"][0]["route"]["route_type"] == "saved_state_metadata"
assert result["objects"][0]["route"]["table"] == "ConfigCASSave"
assert result["objects"][0]["read_selectors"]["card"]["file_name"] == file_name
assert result["objects"][0]["read_selectors"]["card"]["extension_guid"] == extension_guid
assert result["counts"]["saved_state_scanned"] == 1
def test_metadata_object_get_reconstructs_saved_descriptor_file_name(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
file_name = f"{extension_guid}__{object_guid}"
descriptor = '{1,{21,"tt_CalcRegister1"},0,0,0,0,0,0,0,0}'.encode("utf-8")
read_calls: list[tuple[str, str, str]] = []
def fake_read(base_id: str, table: str, requested_file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, str], None]:
read_calls.append((base_id, table, requested_file_name))
return descriptor, {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)
result = adapter_server.get_object(
"CalculationRegister",
object_guid,
base_id="upo_test",
table="ConfigCASSave",
extension_guid=extension_guid,
include_semantic=False,
)
assert result["status"] == "ok"
assert result["source"] == {"kind": "live_sql", "table": "ConfigCASSave"}
assert result["object"]["kind"] == "CalculationRegister"
assert result["object"]["name"] == "tt_CalcRegister1"
assert result["object"]["guid"] == object_guid
assert result["counts"]["scanned"] == 1
assert read_calls == [("upo_test", "ConfigCASSave", file_name)]
def test_metadata_object_get_dispatch_prefers_guid_for_saved_extension_selector(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
captured: dict[str, Any] = {}
def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
captured.update({"kind": kind, "name": name, **kwargs})
return {"status": "ok"}
monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
result = adapter_server.call_method_impl(
"metadata.object.get",
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"name": "tt_CalcRegister1",
"table": "ConfigCASSave",
"extension_guid": extension_guid,
},
)
assert result["status"] == "ok"
assert captured["name"] == object_guid
assert captured["extension_guid"] == extension_guid
assert captured["table"] == "ConfigCASSave"
def test_metadata_object_properties_reads_saved_calculation_register_selector(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
chart_guid = "b69453f1-42f1-449c-b051-185324b13824"
file_name = f"{extension_guid}__{object_guid}"
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
header = [atom(21)]
while len(header) <= 31:
header.append(atom(0))
header[16] = atom(2)
header[17] = atom(0)
header[18] = atom(0)
header[22] = atom(chart_guid)
header[24] = atom(1)
header[25] = atom(0)
header[26] = atom(1)
header[27] = atom(0)
tree = seq(atom(1), seq(*header))
read_calls: list[tuple[str, str, str]] = []
def fake_read(base_id: str, table: str, requested_file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, str], None]:
read_calls.append((base_id, table, requested_file_name))
return (b"chart" if requested_file_name.endswith(chart_guid) else b"descriptor"), {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)
monkeypatch.setattr(adapter_server, "parse_config_tree_from_bytes", lambda data: tree)
monkeypatch.setattr(
adapter_server,
"config_identity_from_bytes",
lambda data: {"guid": chart_guid, "name": "tt_Chart1"} if data == b"chart" else {"guid": object_guid, "name": "tt_CalcRegister1"},
)
result = adapter_server.metadata_object_properties(
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"name": "tt_CalcRegister1",
"table": "ConfigCASSave",
"extension_guid": extension_guid,
}
)
assert result["status"] == "ok"
assert result["decoder"] == "kind_specific_sql"
assert result["object"]["guid"] == object_guid
assert result["properties"]["periodicity"] == "Month"
assert result["properties"]["chart_of_calculation_types"]["guid"] == chart_guid
assert result["properties"]["chart_of_calculation_types"]["name"] == "tt_Chart1"
assert result["properties"]["chart_of_calculation_types"]["status"] == "ok"
assert read_calls == [
("upo_test", "ConfigCASSave", file_name),
("upo_test", "ConfigCASSave", f"{extension_guid}__{chart_guid}"),
]
def test_metadata_object_attributes_reads_saved_extension_register_fields(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
calls: list[dict[str, Any]] = []
object_card = {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid}
def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
calls.append({"kind": kind, "name": name, **kwargs})
result: dict[str, Any] = {"status": "ok", "object": object_card}
if kwargs.get("include_semantic", True):
result["semantic"] = {
"sections": [
{"category": "Attribute", "records": [{"likely_name": "Реквизит1", "type": {"kind": "string", "presentation": "Строка(10)"}}]},
{"category": "Resource", "records": [{"likely_name": "Ресурс1", "type": {"kind": "number", "presentation": "Число(10, 0)"}}]},
{"category": "Dimension", "records": [{"likely_name": "Измерение1", "type": {"kind": "string", "presentation": "Строка(10)"}}]},
]
}
return result
monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(adapter_server, "extension_map_by_guid", lambda base_id: {})
result = adapter_server.metadata_object_attributes(
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"table": "ConfigCASSave",
"extension_guid": extension_guid,
"only": "register_fields",
}
)
assert result["status"] == "ok"
assert [item["name"] for item in result["attributes"]] == ["Реквизит1"]
assert [item["name"] for item in result["resources"]] == ["Ресурс1"]
assert [item["name"] for item in result["dimensions"]] == ["Измерение1"]
assert result["query"]["extension_guid"] == extension_guid
assert len(calls) == 2
assert all(call["extension_guid"] == extension_guid for call in calls)
assert calls[0]["table"] == "ConfigCASSave"
def test_metadata_object_related_reads_saved_extension_child_with_internal_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
child_guid = "10000000-0000-0000-0000-000000000004"
object_file_name = f"{extension_guid}__{object_guid}"
child_file_name = f"{extension_guid}__{child_guid}"
atom = lambda value: {"type": "atom", "value": str(value)}
string = lambda value: {"type": "string", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
identity = seq(seq(atom(1), atom(0), atom(child_guid)), string("Перерасчет1"), seq(atom(0)))
root_items = [atom(1), *(seq(atom(f"marker-{index}"), atom(0)) for index in range(1, 10))]
root_items[4] = seq(atom("recalculation-marker"), atom(1), identity)
tree = seq(*root_items)
get_calls: list[dict[str, Any]] = []
read_calls: list[str] = []
def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
get_calls.append({"kind": kind, "name": name, **kwargs})
return {
"status": "ok",
"object": {
"kind": "CalculationRegister",
"name": "tt_РегистрРасчета1",
"guid": object_guid,
"storage": {"table": "ConfigCASSave", "file_name": object_file_name},
},
}
def fake_read(base_id: str, table: str, file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, str], None]:
read_calls.append(file_name)
return (b'{1}' if file_name == child_file_name else b'{0}'), {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)
monkeypatch.setattr(adapter_server, "parse_config_tree_from_bytes", lambda data: tree)
monkeypatch.setattr(adapter_server, "config_identity_from_bytes", lambda data: {"guid": child_guid, "name": "Перерасчет1"})
result = adapter_server.metadata_object_related(
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"table": "ConfigCASSave",
"extension_guid": extension_guid,
}
)
assert result["status"] == "ok"
assert result["related"][0]["category"] == "Recalculation"
assert result["related"][0]["identity"]["name"] == "Перерасчет1"
assert get_calls[0]["extension_guid"] == extension_guid
assert read_calls == [object_file_name, child_file_name]
def test_metadata_object_related_saved_command_uses_identity_and_module_suffix(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
command_guid = "a9b2fc38-86b6-4586-8aca-29b92bdd86de"
command_class_guid = "078a6af8-d22c-4248-9c33-7e90075a3d2c"
object_file_name = f"{extension_guid}__{object_guid}"
command_file_name = f"{extension_guid}__{command_guid}"
atom = lambda value: {"type": "atom", "value": str(value)}
string = lambda value: {"type": "string", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
identity = seq(seq(atom(1), atom(0), atom(command_guid)), string("Команда1"), seq(atom(0)))
command_record = seq(identity, atom(command_class_guid))
root_items = [atom(1), *(seq(atom(f"marker-{index}"), atom(0)) for index in range(1, 10))]
root_items[8] = seq(atom("command-marker"), atom(1), command_record)
tree = seq(*root_items)
read_calls: list[str] = []
monkeypatch.setattr(
adapter_server,
"get_object",
lambda *args, **kwargs: {
"status": "ok",
"object": {
"kind": "CalculationRegister",
"name": "tt_РегистрРасчета1",
"guid": object_guid,
"storage": {"table": "ConfigCASSave", "file_name": object_file_name},
},
},
)
def fake_read(base_id: str, table: str, file_name: str, **kwargs: Any) -> tuple[Any, Any, Any]:
read_calls.append(file_name)
if file_name == object_file_name:
return b"object", {"database": "upo_test"}, None
if file_name == f"{command_file_name}.2":
return b'{1,"module"}', {"database": "upo_test"}, None
return None, {"database": "upo_test"}, {"status": "source_missing", "diagnostics": {"message": "missing"}}
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)
monkeypatch.setattr(adapter_server, "parse_config_tree_from_bytes", lambda data: tree)
monkeypatch.setattr(adapter_server, "config_identity_from_bytes", lambda data: None)
result = adapter_server.metadata_object_related(
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"table": "ConfigCASSave",
"extension_guid": extension_guid,
}
)
assert result["status"] == "ok"
assert result["counts"]["related"] == 1
assert result["related"][0]["category"] == "Command"
assert result["related"][0]["guid"] == command_guid
assert result["related"][0]["identity"]["name"] == "Команда1"
assert command_class_guid not in [item.get("guid") for item in result["related"]]
assert read_calls == [object_file_name, command_file_name, f"{command_file_name}.2"]
def test_metadata_object_forms_reads_saved_form_payload_route(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
form_guid = "a329697c-32b1-4601-8121-6703516be515"
form_file_name = f"{extension_guid}__{form_guid}.0"
get_calls: list[dict[str, Any]] = []
read_calls: list[str] = []
def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
get_calls.append(kwargs)
return {
"status": "ok",
"object": {
"kind": "CalculationRegister",
"name": "tt_РегистрРасчета1",
"guid": object_guid,
"storage": {"table": "ConfigCASSave", "file_name": f"{extension_guid}__{object_guid}"},
},
}
monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
monkeypatch.setattr(
adapter_server,
"metadata_object_related",
lambda payload: {
"status": "ok",
"object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
"source": {"kind": "live_metadata"},
"related": [{"category": "Form", "guid": form_guid, "status": "ok", "identity": {"guid": form_guid, "name": "ФормаСписка", "synonyms": {}}}],
"counts": {"related": 1, "by_category": {"Form": 1}},
},
)
def fake_read(base_id: str, table: str, file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, str], None]:
read_calls.append(file_name)
return b'{1,"form"}', {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)
result = adapter_server.metadata_object_forms(
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"table": "ConfigCASSave",
"extension_guid": extension_guid,
"include_storage": True,
}
)
assert result["status"] == "ok"
assert result["forms"][0]["name"] == "ФормаСписка"
assert result["forms"][0]["source"]["file_name"] == form_file_name
assert get_calls[0]["extension_guid"] == extension_guid
assert read_calls == [form_file_name]
def test_metadata_object_commands_forwards_saved_extension_selector(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
command_guid = "a9b2fc38-86b6-4586-8aca-29b92bdd86de"
get_calls: list[dict[str, Any]] = []
def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
get_calls.append(kwargs)
return {"status": "ok", "object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid}}
monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(
adapter_server,
"metadata_object_related",
lambda payload: {
"status": "ok",
"related": [{"category": "Command", "guid": command_guid, "status": "ok", "identity": {"guid": command_guid, "name": "Команда1", "synonyms": {}}}],
"counts": {"related": 1, "by_category": {"Command": 1}},
},
)
result = adapter_server.metadata_object_commands(
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"table": "ConfigCASSave",
"extension_guid": extension_guid,
"include_form_commands": False,
}
)
assert result["status"] == "ok"
assert [item["name"] for item in result["object_commands"]] == ["Команда1"]
assert result["object_commands"][0]["module"] == {"kind": "command_module", "name": "Модуль команды"}
assert result["object_commands"][0]["read_selector"] == {
"method": "modules.read",
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"module_ref": f"ConfigCASSave:{extension_guid}__{command_guid}.2",
}
assert get_calls[0]["extension_guid"] == extension_guid
def test_metadata_object_modules_forwards_saved_extension_selector(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
get_calls: list[dict[str, Any]] = []
def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
get_calls.append(kwargs)
return {"status": "ok", "object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid}}
monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(
adapter_server,
"metadata_object_parts",
lambda payload: {
"status": "ok",
"object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
"source": {"kind": "live_metadata"},
"parts": [],
"counts": {"parts": 0, "roles": {}},
},
)
result = adapter_server.metadata_object_modules(
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"table": "ConfigCASSave",
"extension_guid": extension_guid,
}
)
assert result["status"] == "ok"
assert result["modules"] == []
assert get_calls[0]["extension_guid"] == extension_guid
def test_metadata_object_commands_enriches_cached_saved_command_with_read_selector(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
command_guid = "a9b2fc38-86b6-4586-8aca-29b92bdd86de"
monkeypatch.setattr(
adapter_server,
"get_object",
lambda *args, **kwargs: {"status": "ok", "object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid}},
)
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
monkeypatch.setattr(
adapter_server,
"metadata_guid_index_lookup_payload",
lambda config, guid, role: {
"status": "ok",
"object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
"object_commands": [{"guid": command_guid, "name": "Команда1", "status": "ok", "scope": "object"}],
"form_commands": [],
"counts": {"commands": 1, "object_commands": 1, "form_commands": 0},
},
)
monkeypatch.setattr(
adapter_server,
"metadata_object_related",
lambda payload: (_ for _ in ()).throw(AssertionError("cached commands must not trigger a related-object scan")),
)
result = adapter_server.metadata_object_commands(
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"table": "ConfigCASSave",
"extension_guid": extension_guid,
"include_form_commands": False,
}
)
assert result["status"] == "ok"
assert result["cache"]["status"] == "hit"
assert result["object_commands"][0]["read_selector"]["module_ref"] == f"ConfigCASSave:{extension_guid}__{command_guid}.2"
def test_metadata_definition_find_reads_saved_command_routine(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
command_guid = "a9b2fc38-86b6-4586-8aca-29b92bdd86de"
module_ref = f"ConfigCASSave:{extension_guid}__{command_guid}.2"
get_calls: list[dict[str, Any]] = []
def fake_get_object(kind: str | None, name: str, **kwargs: Any) -> dict[str, Any]:
get_calls.append(kwargs)
return {
"status": "ok",
"object": {
"kind": "CalculationRegister",
"kind_ru": "РегистрРасчета",
"name": "tt_РегистрРасчета1",
"guid": object_guid,
},
}
monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
monkeypatch.setattr(
adapter_server,
"metadata_object_modules",
lambda payload: {"status": "ok", "object": {}, "modules": []},
)
monkeypatch.setattr(
adapter_server,
"metadata_object_commands",
lambda payload: {
"status": "ok",
"object_commands": [
{
"guid": command_guid,
"name": "Команда1",
"read_selector": {
"method": "modules.read",
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"module_ref": module_ref,
},
}
],
},
)
monkeypatch.setattr(
adapter_server,
"read_module",
lambda payload: {
"status": "ok",
"routines": [{"name": "ОбработкаКоманды", "kind": "Процедура"}],
},
)
result = adapter_server.metadata_definition_find(
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"table": "ConfigCASSave",
"extension_guid": extension_guid,
"query": "ОбработкаКоманды",
"areas": ["modules"],
}
)
assert result["status"] == "ok"
assert result["counts"]["matches"] == 1
assert result["matches"][0]["name"] == "ОбработкаКоманды"
assert result["matches"][0]["location"]["command"] == "Команда1"
assert result["matches"][0]["origin"]["source"] == "extension"
assert result["matches"][0]["read_selector"]["module_ref"] == module_ref
assert result["matches"][0]["read_selector"]["routine_name"] == "ОбработкаКоманды"
assert get_calls[0]["extension_guid"] == extension_guid
def test_metadata_definition_find_accepts_full_code_style_member_path(monkeypatch: pytest.MonkeyPatch) -> None:
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
get_calls: list[tuple[str | None, str]] = []
def fake_get_object(kind: str | None, name: str, **_kwargs: Any) -> dict[str, Any]:
get_calls.append((kind, name))
return {
"status": "ok",
"object": {
"kind": "Document",
"kind_ru": "Документ",
"name": "Заказ",
"guid": object_guid,
},
}
monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
monkeypatch.setattr(
adapter_server,
"metadata_object_attributes",
lambda _payload: {
"status": "ok",
"dimensions": [],
"resources": [],
"attributes": [{"name": "Автор", "guid": "11111111-1111-1111-1111-111111111111"}],
"tabular_sections": [],
},
)
result = adapter_server.metadata_definition_find(
{
"base_id": "upo_test",
"query": "Документы.Заказ.Реквизиты.Автор",
}
)
assert result["status"] == "ok"
assert get_calls == [("Document", "Заказ")]
assert result["counts"]["matches"] == 1
assert result["matches"][0]["area"] == "object"
assert result["matches"][0]["name"] == "Автор"
def test_metadata_object_parts_uses_saved_extension_storage_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
file_name = f"{extension_guid}__{object_guid}"
list_calls: list[dict[str, Any]] = []
def fake_list(payload: dict[str, Any]) -> dict[str, Any]:
list_calls.append(payload)
return {"status": "ok", "files": [{"FileName": file_name}]}
monkeypatch.setattr(
adapter_server,
"get_object",
lambda *args, **kwargs: {
"status": "ok",
"object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
},
)
monkeypatch.setattr(adapter_server, "storage_files_list", fake_list)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({file_name: b"{1}"}, {"database": "upo_test"}, None),
)
result = adapter_server.metadata_object_parts(
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"table": "ConfigCASSave",
"extension_guid": extension_guid,
}
)
assert result["status"] == "ok"
assert result["counts"]["parts"] == 1
assert result["object"]["name"] == "tt_РегистрРасчета1"
assert list_calls[0]["prefix"] == file_name
def test_metadata_object_decode_reads_saved_extension_descriptor(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
file_name = f"{extension_guid}__{object_guid}"
read_calls: list[str] = []
monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *args, **kwargs: ([], None))
monkeypatch.setattr(
adapter_server,
"get_object",
lambda *args, **kwargs: {
"status": "ok",
"object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
},
)
def fake_read(base_id: str, table: str, requested_file_name: str, **kwargs: Any) -> tuple[bytes, dict[str, str], None]:
read_calls.append(requested_file_name)
return b"{1}", {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read)
monkeypatch.setattr(
adapter_server,
"decode_config_object_full",
lambda *args, **kwargs: {"status": "ok", "root": {}, "semantic": {"sections": []}},
)
result = adapter_server.decode_metadata_object(
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"table": "ConfigCASSave",
"extension_guid": extension_guid,
}
)
assert result["status"] == "ok"
assert result["source"] == {"kind": "live_metadata"}
assert result["object"]["name"] == "tt_РегистрРасчета1"
assert read_calls == [file_name]
def test_metadata_object_full_commands_exposes_saved_command_module_handle(monkeypatch: pytest.MonkeyPatch) -> None:
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
module_ref = "ConfigCASSave:extension__command.2"
monkeypatch.setattr(
adapter_server,
"call_method_impl",
lambda method, payload: {
"status": "ok",
"object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
"object_commands": [
{
"name": "Команда1",
"read_selector": {
"method": "modules.read",
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"module_ref": module_ref,
},
}
],
},
)
result = adapter_server.adapter_full_commands_result(
{"base_id": "upo_test", "kind": "CalculationRegister", "guid": object_guid},
{"object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid}, "forms": []},
30,
)
assert result["status"] == "ok"
assert result["counts"]["command_modules"] == 1
assert result["modules"][0]["kind"] == "command_module"
assert result["modules"][0]["command"] == "Команда1"
assert result["modules"][0]["qualified_name"] == "tt_РегистрРасчета1.Команда.Команда1.Модуль команды"
assert result["modules"][0]["read_selector"]["module_ref"] == module_ref
def test_get_object_resolves_saved_extension_object_by_name(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
file_name = f"{extension_guid}__{object_guid}"
monkeypatch.setattr(
adapter_server,
"extension_objects_find",
lambda payload: {
"status": "ok",
"objects": [
{
"kind": "CalculationRegister",
"name": "tt_РегистрРасчета1",
"guid": object_guid,
"route": {"table": "ConfigCASSave", "file_name": file_name},
}
],
},
)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (b"{1}", {"database": "upo_test"}, None),
)
monkeypatch.setattr(
adapter_server,
"config_identity_from_bytes",
lambda data: {"guid": object_guid, "name": "tt_РегистрРасчета1", "synonyms": {}},
)
result = adapter_server.get_object(
"CalculationRegister",
"tt_РегистрРасчета1",
base_id="upo_test",
table="ConfigCASSave",
extension_guid=extension_guid,
include_semantic=False,
)
assert result["status"] == "ok"
assert result["object"]["name"] == "tt_РегистрРасчета1"
assert result["object"]["guid"] == object_guid
rpc_result = adapter_server.call_method(
"metadata.object.get",
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"name": "tt_РегистрРасчета1",
"table": "ConfigCASSave",
"extension_guid": extension_guid,
},
)
assert rpc_result["status"] == "ok"
assert rpc_result["object"]["name"] == "tt_РегистрРасчета1"
def test_code_module_owner_cache_stream_ref_falls_back_to_base_ref(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
base_ref = f"ConfigCASSave:{extension_guid}__command.2"
stream_ref = f"{base_ref}#stream:4"
lookup_calls: list[str] = []
monkeypatch.setattr(adapter_server, "metadata_form_owner_cache_lookup", lambda *args, **kwargs: None)
def fake_lookup(config: dict[str, str], module_ref: str) -> dict[str, Any] | None:
lookup_calls.append(module_ref)
if module_ref == base_ref:
return {
"owner": {
"kind": "CalculationRegister",
"name": "tt_РегистрРасчета1",
"guid": "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5",
}
}
return None
monkeypatch.setattr(adapter_server, "metadata_module_owner_cache_lookup", fake_lookup)
result = adapter_server.code_module_owner_from_cache({"database": "upo_test"}, stream_ref)
assert lookup_calls == [stream_ref, base_ref]
assert result["owner_kind"] == "CalculationRegister"
assert result["owner_name"] == "tt_РегистрРасчета1"
assert result["extension_guid"] == extension_guid
def test_metadata_code_index_build_ignores_non_bsl_text_streams(monkeypatch: pytest.MonkeyPatch) -> None:
class FakeStreamClassifier:
pass
file_name = "extension__command.2"
indexed_refs: list[str] = []
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
monkeypatch.setattr(adapter_server, "storage_files_list", lambda payload: {"status": "ok", "files": [{"FileName": file_name}]})
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({file_name: b"payload"}, {"database": "upo_test"}, None),
)
monkeypatch.setattr(adapter_server, "code_module_owner_from_cache", lambda *args, **kwargs: {})
monkeypatch.setattr(
adapter_server,
"code_index_upsert",
lambda config, **kwargs: indexed_refs.append(kwargs["module_ref"])
or {"module_ref": kwargs["module_ref"], "routines": [], "routine_count": 0},
)
monkeypatch.setattr(adapter_server, "code_vector_upsert_chunks", lambda *args, **kwargs: 0)
monkeypatch.setattr(adapter_server, "code_index_prune_file_modules", lambda *args, **kwargs: 0)
monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
FakeStreamClassifier.classify_payload = staticmethod(
lambda data, include_text=True: {
"stream_blocks": [
{"text": "служебный descriptor", "has_bsl_marker": False},
{"text": "Процедура ОбработкаКоманды()\nКонецПроцедуры\n", "has_bsl_marker": True},
]
}
)
result = adapter_server.metadata_code_index_build(
{"base_id": "upo_test", "table": "ConfigCASSave", "prefix": file_name, "include_vectors": False}
)
assert result["status"] == "ok"
assert result["counts"]["indexed"] == 1
assert indexed_refs == [f"ConfigCASSave:{file_name}#stream:1"]
def test_metadata_code_index_build_routes_named_base_object_to_config(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, Any] = {}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
def get_object(kind: str, name: str, **kwargs: Any) -> dict[str, Any]:
captured.update({"kind": kind, "name": name, "table": kwargs.get("table")})
return {
"status": "ok",
"object": {
"kind": "CommonModule",
"name": "ОбменДанными",
"guid": "11111111-1111-1111-1111-111111111111",
"ref": "CommonModule.ОбменДанными",
},
}
monkeypatch.setattr(adapter_server, "get_object", get_object)
monkeypatch.setattr(adapter_server, "metadata_object_modules", lambda payload: {"status": "ok", "modules": []})
monkeypatch.setattr(adapter_server, "metadata_object_commands", lambda payload: {"status": "ok", "object_commands": []})
monkeypatch.setattr(adapter_server, "metadata_object_forms", lambda payload: {"status": "ok", "forms": []})
result = adapter_server.call_method(
"metadata.code_index.build",
{
"base_id": "upo_test",
"ref": "ОбщиеМодули.ОбменДанными",
"include_vectors": False,
},
)
assert result["status"] == "ok"
assert captured == {"kind": "CommonModule", "name": "ОбменДанными", "table": "Config"}
assert result["source"]["table"] == "Config"
assert result["query"]["name"] == "ОбменДанными"
def test_metadata_code_index_help_publishes_optional_name_scope() -> None:
methods = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}
method = methods["metadata.code_index.build"]
assert method["selector_capabilities"]["selector_purpose"] == "optional_index_scope"
assert method["selector_capabilities"]["allows_global_search"] is True
assert method["selector_capabilities"]["accepts_extension"] is True
assert method["input_schema"]["properties"]["object_name"]["type"] == "string"
def test_form_owner_index_keeps_owner_and_form_names_separate(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, Any] = {}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
def find_objects(payload: dict[str, Any]) -> dict[str, Any]:
captured["find"] = payload
return {
"status": "ok",
"objects": [
{
"kind": "DataProcessor",
"name": "ОбменДанными",
"guid": "11111111-1111-1111-1111-111111111111",
"route": {"table": "ConfigCAS", "file_name": "owner-descriptor"},
"origin": {"extension": {"name": "test", "guid": "22222222-2222-2222-2222-222222222222"}},
}
],
}
def index_entry(**kwargs: Any) -> tuple[dict[str, Any], None]:
captured["entry"] = kwargs
return (
{
"owner": {"kind": kwargs["owner_kind"], "name": kwargs["owner_name"]},
"form": {"name": kwargs["form_name"]},
"bsl_offset": None,
},
None,
)
monkeypatch.setattr(adapter_server, "extension_objects_find", find_objects)
monkeypatch.setattr(adapter_server, "manifest_related_entries_for_cas_key", lambda *args, **kwargs: ([], []))
monkeypatch.setattr(adapter_server, "form_owner_index_entry_from_sql_payload", index_entry)
result = adapter_server.call_method(
"metadata.form.owner_index.build",
{
"base_id": "upo_test",
"ref": "Обработки.ОбменДанными",
"form_name": "ФормаОбмена",
"extension": "test",
},
)
assert result["status"] == "ok"
assert captured["find"]["kind"] == "DataProcessor"
assert captured["find"]["query"] == "ОбменДанными"
assert captured["entry"]["owner_name"] == "ОбменДанными"
assert captured["entry"]["form_name"] == "ФормаОбмена"
assert result["query"]["owner"] == "ОбменДанными"
assert result["query"]["form"] == "ФормаОбмена"
def test_form_owner_index_help_publishes_owner_and_form_selectors() -> None:
methods = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}
method = methods["metadata.form.owner_index.build"]
assert method["selector_capabilities"]["selector_purpose"] == "optional_form_owner_scope"
assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name"]
assert method["input_schema"]["properties"]["form_name"]["type"] == "string"
assert "Обработки.ОбменДанными" in method["input_schema"]["properties"]["ref"]["description"]
def test_metadata_code_index_build_resolves_saved_command_files_from_object_name(monkeypatch: pytest.MonkeyPatch) -> None:
class FakeStreamClassifier:
pass
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
command_guid = "a9b2fc38-86b6-4586-8aca-29b92bdd86de"
file_name = f"{extension_guid}__{command_guid}.2"
form_file_name = f"{extension_guid}__form-guid.0"
form_module_ref = f"ConfigCASSave:{form_file_name}#form_module"
read_chunks: list[list[str]] = []
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
monkeypatch.setattr(
adapter_server,
"get_object",
lambda *args, **kwargs: {
"status": "ok",
"object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
},
)
monkeypatch.setattr(adapter_server, "metadata_object_modules", lambda payload: {"status": "ok", "modules": []})
monkeypatch.setattr(
adapter_server,
"metadata_object_commands",
lambda payload: {
"status": "ok",
"object_commands": [
{
"name": "Команда1",
"read_selector": {"method": "modules.read", "module_ref": f"ConfigCASSave:{file_name}"},
}
],
},
)
monkeypatch.setattr(
adapter_server,
"metadata_object_forms",
lambda payload: {
"status": "ok",
"forms": [{"name": "ФормаСписка", "source": {"table": "ConfigCASSave", "file_name": form_file_name}}],
},
)
monkeypatch.setattr(
adapter_server,
"metadata_form_decode",
lambda payload: {
"status": "ok",
"profile": {
"module": {
"text": "Процедура ПриОткрытии()\nКонецПроцедуры\n",
"read_selector": {"method": "modules.read", "module_ref": form_module_ref},
}
},
},
)
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: (_ for _ in ()).throw(AssertionError("object-scoped build must not scan storage by prefix")),
)
def fake_read(base_id: str, table: str, file_names: list[str], **kwargs: Any) -> tuple[dict[str, bytes], dict[str, str], None]:
read_chunks.append(file_names)
return {file_name: b"command", form_file_name: b"form"}, {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "read_storage_files_bytes", fake_read)
monkeypatch.setattr(adapter_server, "code_module_owner_from_cache", lambda *args, **kwargs: {})
monkeypatch.setattr(
adapter_server,
"code_index_upsert",
lambda config, **kwargs: {"module_ref": kwargs["module_ref"], "routines": [], "routine_count": 0},
)
monkeypatch.setattr(adapter_server, "code_vector_upsert_chunks", lambda *args, **kwargs: 0)
monkeypatch.setattr(adapter_server, "code_index_prune_file_modules", lambda *args, **kwargs: 0)
monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
FakeStreamClassifier.classify_payload = staticmethod(
lambda data, include_text=True: (
{"stream_blocks": [{"text": "Процедура ОбработкаКоманды()\nКонецПроцедуры\n", "has_bsl_marker": True}]}
if data == b"command"
else {"stream_blocks": []}
)
)
result = adapter_server.metadata_code_index_build(
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"name": "tt_РегистрРасчета1",
"extension_guid": extension_guid,
"include_vectors": False,
}
)
assert result["status"] == "ok"
assert result["query"]["table"] == "ConfigCASSave"
assert result["object"]["name"] == "tt_РегистрРасчета1"
assert result["counts"]["discovered_module_refs"] == 2
assert result["counts"]["forms_scanned"] == 1
assert result["counts"]["form_modules_discovered"] == 1
assert result["counts"]["empty_form_modules"] == 0
assert result["counts"]["form_module_errors"] == 0
assert result["counts"]["indexed"] == 2
assert read_chunks == [[file_name, form_file_name]]
def test_extension_objects_find_active_state_excludes_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id}, None))
monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda base_id, extension, method: (extension_guid, None))
monkeypatch.setattr(adapter_server, "extension_route_cache_lookup", lambda *args, **kwargs: [])
monkeypatch.setattr(
adapter_server,
"extension_manifest_object_matches",
lambda *args, **kwargs: ([], [], {"manifest_count": 0, "descriptor_entries": 0, "descriptor_payloads_read": 0}),
)
def fail_saved_state_search(payload: dict[str, Any]) -> dict[str, Any]:
raise AssertionError("state=active must not inspect ConfigCASSave")
monkeypatch.setattr(adapter_server, "metadata_saved_state_forms_search", fail_saved_state_search)
result = adapter_server.extension_objects_find({"base_id": "upo_test", "extension": "test2", "state": "active", "limit": 50})
assert result["status"] == "not_found"
assert result["query"]["state"] == "active"
assert result["counts"]["saved_state_matches"] == 0
def test_extension_cache_rebuild_warms_route_cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
descriptor_key = "a" * 40
part_key = "b" * 40
object_base_id = "7c6dc711-02f5-4e6a-9f37-7dd57d8942b3"
guid = "11111111-2222-3333-4444-555555555555"
name = "\u0444\u0441_\u041f\u0424_MXL_\u041c\u0435\u043d\u044e\u0417\u0430\u041f\u0435\u0440\u0438\u043e\u0434"
descriptor_payload = f'{{1,{{3,{{1,0,{guid}}},"{name}",{{1,"ru","Test template"}}}}}}'.encode("utf-8")
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", lambda *args, **kwargs: ({}, None))
monkeypatch.setattr(
adapter_server,
"live_extension_manifests",
lambda *args, **kwargs: (
[
{
"root_cas_key": "c" * 40,
"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"},
"entries": [
{"object_id": object_base_id, "object_base_id": object_base_id, "suffix": "", "cas_key": descriptor_key},
{"object_id": f"{object_base_id}.0", "object_base_id": object_base_id, "suffix": ".0", "cas_key": part_key},
],
}
],
[],
),
)
read_calls = {"count": 0}
def fake_read_files(*args: Any, **kwargs: Any) -> tuple[dict[str, bytes], dict[str, str], None]:
read_calls["count"] += 1
return {descriptor_key: descriptor_payload}, config, None
monkeypatch.setattr(adapter_server, "read_storage_files_bytes", fake_read_files)
rebuild = adapter_server.extension_cache_rebuild({"base_id": "upo_test", "kind": "Template", "max_items": 10})
assert rebuild["status"] == "ok"
assert rebuild["counts"]["cached_routes"] == 1
def fail_dbnames_lookup(*args: Any, **kwargs: Any) -> tuple[dict[str, list[dict[str, Any]]], None]:
raise AssertionError("DBNames lookup must not run for a validated source-cache hit")
monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", fail_dbnames_lookup)
found = adapter_server.extension_objects_find({"base_id": "upo_test", "kind": "Template", "query": name, "limit": 1})
assert found["status"] == "ok"
assert found["objects"][0]["match_by"] == "source_cache"
assert found["counts"]["source_cache_validated"] == 1
assert read_calls["count"] == 1
def test_extension_cache_validate_marks_missing_routes_stale(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
fresh_key = "a" * 40
stale_key = "b" * 40
object_base_id = "7c6dc711-02f5-4e6a-9f37-7dd57d8942b3"
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
manifest_calls = {"count": 0}
def fake_live_extension_manifests(*args: Any, **kwargs: Any) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
manifest_calls["count"] += 1
return (
[
{
"root_cas_key": "c" * 40,
"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"},
"entries": [
{"object_id": object_base_id, "object_base_id": object_base_id, "suffix": "", "cas_key": fresh_key},
],
}
],
[],
)
monkeypatch.setattr(adapter_server, "live_extension_manifests", fake_live_extension_manifests)
for key, name in ((fresh_key, "FreshTemplate"), (stale_key, "StaleTemplate")):
adapter_server.extension_route_cache_upsert(
config,
{
"kind": "Template",
"kind_ru": "Макет",
"name": name,
"guid": key,
"origin": {"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"}},
"route": {
"route_type": "extension_manifest_cas",
"table": "ConfigCAS",
"file_name": key,
"manifest_entry": {
"object_id": object_base_id,
"object_base_id": object_base_id,
"suffix": "",
"cas_key": key,
"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"},
"root_cas_key": "c" * 40,
},
},
"manifest_entries": [],
},
descriptor_payload_sha1="sha",
)
result = adapter_server.extension_cache_validate({"base_id": "upo_test", "include_entries": True})
assert result["status"] == "ok"
assert result["counts"] == {"checked": 2, "fresh": 1, "stale": 1}
assert manifest_calls["count"] == 1
stale_entry = next(item for item in result["entries"] if item["descriptor_cas_key"] == stale_key)
assert stale_entry["freshness"]["status"] == "stale"
def test_extension_cache_status_groups_freshness(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
fresh_key = "a" * 40
stale_key = "b" * 40
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
for key, name in ((fresh_key, "FreshTemplate"), (stale_key, "StaleTemplate")):
adapter_server.extension_route_cache_upsert(
config,
{
"kind": "Template",
"kind_ru": "Макет",
"name": name,
"guid": key,
"origin": {"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"}},
"route": {
"route_type": "extension_manifest_cas",
"table": "ConfigCAS",
"file_name": key,
"manifest_entry": {
"object_id": key,
"object_base_id": key,
"suffix": "",
"cas_key": key,
"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"},
},
},
"manifest_entries": [],
},
descriptor_payload_sha1="sha",
)
adapter_server.extension_route_cache_mark_stale(config, stale_key, "test_stale")
result = adapter_server.extension_cache_status({"base_id": "upo_test", "include_entries": True, "limit": 10})
assert result["status"] == "ok"
assert result["counts"]["total"] == 2
assert result["counts"]["usable"] == 1
assert result["counts"]["stale"] == 1
assert result["groups"][0]["counts"] == {"total": 2, "usable": 1, "stale": 1}
assert result["entries"][0]["freshness_status"] == "stale"
assert result["entries"][0]["stale_reason"] == "test_stale"
def test_extension_objects_find_reuses_recent_validated_cache_with_ttl(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
descriptor_key = "a" * 40
name = "\u0444\u0441_\u041f\u0424_MXL_\u041c\u0435\u043d\u044e\u0417\u0430\u041f\u0435\u0440\u0438\u043e\u0434"
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
adapter_server.extension_route_cache_upsert(
config,
{
"kind": "Template",
"kind_ru": "Макет",
"name": name,
"guid": descriptor_key,
"origin": {"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"}},
"route": {"route_type": "extension_manifest_cas", "table": "ConfigCAS", "file_name": descriptor_key},
"manifest_entries": [],
},
descriptor_payload_sha1="sha",
)
manifest_calls = {"count": 0}
def fake_live_extension_manifests(*args: Any, **kwargs: Any) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
manifest_calls["count"] += 1
return ([], [])
monkeypatch.setattr(adapter_server, "live_extension_manifests", fake_live_extension_manifests)
monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", lambda *args, **kwargs: ({}, None))
monkeypatch.setattr(adapter_server, "storage_files_list", lambda payload: {"status": "ok", "files": []})
recent = adapter_server.extension_objects_find({"base_id": "upo_test", "kind": "Template", "query": name, "limit": 1, "cache_ttl_seconds": 300})
assert recent["status"] == "ok"
assert recent["objects"][0]["freshness"]["validated_by"] == "recent_source_cache"
assert manifest_calls["count"] == 0
expired = adapter_server.extension_objects_find({"base_id": "upo_test", "kind": "Template", "query": name, "limit": 1, "cache_ttl_seconds": 0, "scan_limit": 1})
assert expired["status"] == "not_found"
assert expired["counts"]["source_cache_stale"] == 1
assert manifest_calls["count"] >= 1
def test_extension_objects_find_marks_stale_source_cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
descriptor_key = "a" * 40
name = "\u0444\u0441_\u041f\u0424_MXL_\u041c\u0435\u043d\u044e\u0417\u0430\u041f\u0435\u0440\u0438\u043e\u0434"
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", lambda *args, **kwargs: ({}, None))
monkeypatch.setattr(adapter_server, "live_extension_manifests", lambda *args, **kwargs: ([], []))
monkeypatch.setattr(adapter_server, "storage_files_list", lambda payload: {"status": "ok", "files": []})
adapter_server.extension_route_cache_upsert(
config,
{
"kind": "Template",
"kind_ru": "Макет",
"name": name,
"guid": descriptor_key,
"origin": {"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"}},
"route": {"route_type": "extension_manifest_cas", "table": "ConfigCAS", "file_name": descriptor_key},
"manifest_entries": [],
},
descriptor_payload_sha1="sha",
)
result = adapter_server.extension_objects_find({"base_id": "upo_test", "kind": "Template", "query": name, "limit": 1, "scan_limit": 1, "cache_ttl_seconds": 0})
assert result["status"] == "not_found"
assert result["counts"]["source_cache_candidates"] == 1
assert result["counts"]["source_cache_stale"] == 1
def test_template_part_structure_reuses_decoded_artifact_cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
payload = b"MOXCEL cached payload"
calls = {"decode": 0}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (payload, config, None))
def fake_extract(data: bytes) -> dict[str, Any]:
calls["decode"] += 1
return {
"format": "MOXCEL",
"dimensions": {"rows": 1, "columns": 1},
"named_areas": [{"name": "ОбластьШапка", "range": {"one_based": {"top": 1, "left": 1, "bottom": 1, "right": 1}}}],
"parameters": [{"name": "Период"}],
"cells": [{"row": 1, "column": 1, "text": "Текст"}],
"capabilities": {"decoded_binary": True, "named_areas": True},
}
monkeypatch.setattr(adapter_server, "extract_moxel_public_structure", fake_extract)
part = {
"part_id": "part-key",
"table": "ConfigCAS",
"classification": {"markers": ["MOXCEL"], "role": "template_payload"},
}
first = adapter_server.template_part_structure("upo_test", part, timeout_seconds=30)
second = adapter_server.template_part_structure("upo_test", part, timeout_seconds=30)
assert calls["decode"] == 1
assert first["artifact_cache"]["status"] == "miss_stored"
assert second["artifact_cache"]["status"] == "hit"
assert second["structure"]["named_areas"][0]["name"] == "ОбластьШапка"
def test_bounded_template_content_export_extracts_html_from_base64_block() -> None:
html = b"Template help"
encoded = base64.b64encode(html).decode("ascii")
stored = compress_payload(f'{{5,1,"ru",{{#base64:{encoded}}}}}'.encode("utf-8-sig"), "raw_deflate")
result = adapter_server.bounded_template_content_export(stored, max_content_bytes=4096)
assert result["status"] == "complete"
assert result["container"]["encoding"] == "base64"
assert result["container"]["truncated"] is False
assert result["counts"]["extracted_text"] == 1
assert result["extracted_text"][0]["media_type"] == "text/html"
assert "Template help" in result["extracted_text"][0]["text"]
def test_bounded_template_content_export_limits_binary_payload() -> None:
stored = compress_payload(b"\x00\x01\x02\x03\x04\x05", "raw_deflate")
result = adapter_server.bounded_template_content_export(stored, max_content_bytes=4)
assert result["status"] == "truncated"
assert result["container"]["bytes"] == 6
assert result["container"]["returned_bytes"] == 4
assert base64.b64decode(result["container"]["data_base64"]) == b"\x00\x01\x02\x03"
def test_template_part_structure_exports_non_moxel_content_only_when_requested(monkeypatch: pytest.MonkeyPatch) -> None:
payload = compress_payload(b"plain template content", "raw_deflate")
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (payload, {}, None))
part = {"part_id": "part-key", "table": "Config", "classification": {"markers": [], "role": "binary_or_unknown_payload"}}
without_content = adapter_server.template_part_structure("upo_test", part, timeout_seconds=30)
with_content = adapter_server.template_part_structure("upo_test", part, timeout_seconds=30, include_content=True, max_content_bytes=100)
assert "content_export" not in without_content
assert with_content["content_export"]["status"] == "complete"
assert base64.b64decode(with_content["content_export"]["container"]["data_base64"]) == b"plain template content"
def test_templates_read_content_export_arguments_are_strict_and_bounded() -> None:
selector = {"base_id": "upo_test", "route_ref": "ConfigCAS:" + ("a" * 40)}
assert adapter_server.validate_templates_read_payload({**selector, "include_content": True, "max_content_bytes": 4096}) is None
invalid_bool = adapter_server.validate_templates_read_payload({**selector, "include_content": "yes"})
too_large = adapter_server.validate_templates_read_payload({**selector, "max_content_bytes": adapter_server.TEMPLATE_CONTENT_MAX_BYTES + 1})
assert invalid_bool is not None and invalid_bool["argument"] == "include_content"
assert too_large is not None and too_large["argument"] == "max_content_bytes"
def test_templates_read_job_validation_uses_template_response_view_values() -> None:
payload = {"base_id": "upo_test", "route_ref": "ConfigCAS:" + ("a" * 40), "view": "full"}
assert adapter_server.validate_adapter_job_payload("templates.read", payload) is None
def test_templates_read_rpc_validation_uses_template_response_view_values(monkeypatch: pytest.MonkeyPatch) -> None:
descriptor_key = "a" * 40
monkeypatch.setattr(
adapter_server,
"read_template_by_route",
lambda payload, analyze=False: {"status": "ok", "view": payload.get("view"), "templates": [], "counts": {"templates": 0}},
)
result = adapter_server.call_method(
"templates.read",
{"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}", "view": "full"},
)
assert result["status"] == "ok"
assert result["view"] == "full"
def test_templates_read_does_not_forward_response_view_to_metadata_selector(monkeypatch: pytest.MonkeyPatch) -> None:
seen: list[dict[str, Any]] = []
def fake_details(payload: dict[str, Any]) -> dict[str, Any]:
seen.append(payload)
return {"status": "ok", "source": {"kind": "test"}, "object": {"kind": "Document", "name": "Doc"}, "templates": []}
monkeypatch.setattr(adapter_server, "metadata_object_template_details", fake_details)
result = adapter_server.templates_read({"base_id": "upo_test", "kind": "Document", "name": "Doc", "view": "full"})
assert result["status"] == "ok"
assert result["view"] == "full"
assert "view" not in seen[0]
def test_templates_areas_find_returns_named_area_coordinates_for_extension_template(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
template_name = "фс_ПФ_MXL_МенюЗаПериод"
descriptor_key = "a" * 40
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
def fake_extension_objects_find(payload: dict[str, Any]) -> dict[str, Any]:
assert payload["query"] == template_name
assert payload["kind"] == "Template"
return {
"status": "ok",
"objects": [
{
"kind": "Template",
"name": template_name,
"guid": descriptor_key,
"match_by": "source_cache",
"origin": {"extension": {"name": "фс_Отчеты", "guid": "99999999-9999-9999-9999-999999999999"}},
"route": {"route_type": "extension_manifest_cas", "table": "ConfigCAS", "file_name": descriptor_key},
"freshness": {"status": "fresh", "validated_by": "live_manifest", "validation_required": False},
}
],
}
def fake_read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]:
assert payload["file_name"] == descriptor_key
assert payload["sections"] == "named_areas,coverage,diagnostics"
return {
"status": "ok",
"templates": [
{
"name": template_name,
"guid": descriptor_key,
"structure": {
"format": "MOXCEL",
"dimensions": {"rows": 20, "columns": 8},
"capabilities": {"named_areas": True, "named_area_coordinates": True},
"named_areas": [
{
"name": "ОбластьШапка",
"occurrence": 1,
"source": "moxel_named_area",
"range": {
"address": "R1C1:R3C8",
"one_based": {"top": 1, "left": 1, "bottom": 3, "right": 8},
"zero_based": {"top": 0, "left": 0, "bottom": 2, "right": 7},
"width": 8,
"height": 3,
},
}
],
"area_cell_coverage": [
{
"name": "ОбластьШапка",
"occurrence": 1,
"cell_count": 1,
"parameter_count": 1,
"cells": [{"row": 1, "column": 1, "text": "Меню за период"}],
"parameters": [{"name": "Период", "row": 2, "column": 1, "source": "cell_parameter"}],
}
],
},
}
],
"counts": {"templates": 1, "parts": 1},
}
monkeypatch.setattr(adapter_server, "extension_objects_find", fake_extension_objects_find)
monkeypatch.setattr(adapter_server, "read_template_by_route", fake_read_template_by_route)
result = adapter_server.templates_areas_find(
{"base_id": "upo_test", "extension": "фс_Отчеты", "query": template_name, "max_areas": 50}
)
assert result["status"] == "ok"
assert result["source"]["authoritative"] is True
assert result["counts"]["templates"] == 1
assert result["counts"]["areas"] == 1
assert result["counts"]["areas_with_coordinates"] == 1
assert result["templates"][0]["object"]["name"] == template_name
assert result["templates"][0]["route"]["route_ref"] == f"ConfigCAS:{descriptor_key}"
assert result["templates"][0]["read_selector"]["route_ref"] == f"ConfigCAS:{descriptor_key}"
area = result["areas"][0]
assert area["name"] == "ОбластьШапка"
assert area["coordinates_available"] is True
assert area["range"]["one_based"] == {"top": 1, "left": 1, "bottom": 3, "right": 8}
assert area["coverage"]["parameters"][0]["name"] == "Период"
def test_templates_areas_find_retries_query_without_short_prefix(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
descriptor_key = "a" * 40
attempts: list[dict[str, Any]] = []
def fake_extension_objects_find(payload: dict[str, Any]) -> dict[str, Any]:
attempts.append(payload)
if payload.get("query") == "ПФ_MXL_МенюЗаПериод" and "kind" not in payload:
return {
"status": "ok",
"objects": [
{
"name": "ПФ_MXL_МенюЗаПериод",
"guid": descriptor_key,
"route": {"route_type": "configcas_payload", "table": "ConfigCAS", "file_name": descriptor_key},
}
],
}
return {"status": "not_found", "objects": [], "diagnostics": []}
monkeypatch.setattr(adapter_server, "extension_objects_find", fake_extension_objects_find)
monkeypatch.setattr(
adapter_server,
"read_template_by_route",
lambda payload, analyze=False: {
"status": "ok",
"templates": [
{
"name": "ПФ_MXL_МенюЗаПериод",
"guid": descriptor_key,
"structure": {
"format": "MOXCEL",
"capabilities": {"named_area_coordinates": True},
"named_areas": [
{
"name": "ОбластьШапка",
"occurrence": 1,
"range": {"one_based": {"top": 1, "left": 1, "bottom": 1, "right": 4}},
}
],
},
}
],
"counts": {"templates": 1, "parts": 1},
},
)
result = adapter_server.templates_areas_find({"base_id": "upo_test", "query": "фс_ПФ_MXL_МенюЗаПериод"})
assert result["status"] == "ok"
assert [attempt.get("query") for attempt in attempts] == [
"фс_ПФ_MXL_МенюЗаПериод",
"фс_ПФ_MXL_МенюЗаПериод",
"ПФ_MXL_МенюЗаПериод",
"ПФ_MXL_МенюЗаПериод",
]
assert result["areas"][0]["name"] == "ОбластьШапка"
assert result["templates"][0]["route"]["route_ref"] == f"ConfigCAS:{descriptor_key}"
def test_templates_areas_find_accepts_route_ref(monkeypatch: pytest.MonkeyPatch) -> None:
descriptor_key = "a" * 40
seen_payloads: list[dict[str, Any]] = []
def fake_read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]:
seen_payloads.append(payload)
return {
"status": "ok",
"templates": [
{
"name": "RouteRefTemplate",
"guid": descriptor_key,
"structure": {
"format": "MOXCEL",
"capabilities": {"named_area_coordinates": True},
"named_areas": [
{
"name": "ОбластьШапка",
"occurrence": 1,
"range": {"one_based": {"top": 1, "left": 1, "bottom": 2, "right": 4}},
}
],
},
}
],
"counts": {"templates": 1, "parts": 1},
}
monkeypatch.setattr(adapter_server, "read_template_by_route", fake_read_template_by_route)
result = adapter_server.templates_areas_find({"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}"})
assert result["status"] == "ok"
assert seen_payloads[0]["table"] == "ConfigCAS"
assert seen_payloads[0]["file_name"] == descriptor_key
assert result["templates"][0]["route"]["route_ref"] == f"ConfigCAS:{descriptor_key}"
assert result["areas"][0]["name"] == "ОбластьШапка"
def test_templates_areas_find_normalizes_public_template_ref(monkeypatch: pytest.MonkeyPatch) -> None:
descriptor_key = "a" * 40
seen: dict[str, Any] = {}
def fake_find(payload: dict[str, Any]) -> dict[str, Any]:
seen["find_payload"] = payload
return {
"status": "ok",
"objects": [
{
"object": {"kind": "Template", "name": "ПечатнаяФорма", "guid": descriptor_key},
"route": {"table": "ConfigCAS", "file_name": descriptor_key, "route_type": "cache"},
"match_by": "name",
}
],
}
monkeypatch.setattr(adapter_server, "extension_objects_find", fake_find)
monkeypatch.setattr(
adapter_server,
"read_template_by_route",
lambda payload, analyze=False: {
"status": "ok",
"templates": [
{
"name": "ПечатнаяФорма",
"guid": descriptor_key,
"structure": {
"format": "MOXCEL",
"named_areas": [
{
"name": "Шапка",
"occurrence": 1,
"range": {"one_based": {"top": 1, "left": 1, "bottom": 1, "right": 4}},
}
],
},
}
],
},
)
result = adapter_server.call_method(
"templates.areas.find",
{
"base_id": "upo_test",
"ref": "Макеты.ПечатнаяФорма",
"area_name": "Шапка",
},
)
assert result["status"] == "ok"
assert seen["find_payload"]["query"] == "ПечатнаяФорма"
assert seen["find_payload"]["kind"] == "Template"
assert result["query"]["query"] == "ПечатнаяФорма"
assert result["query"]["area_query"] == "Шапка"
assert result["areas"][0]["name"] == "Шапка"
def test_templates_areas_find_help_separates_template_and_area_selectors() -> None:
method = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}["templates.areas.find"]
assert method["selector_capabilities"]["accepts_ordinal"] is False
assert method["selector_capabilities"]["read_only"] is True
assert method["selector_capabilities"]["child_selector_arguments"] == [
"area_name",
"area_query",
"area_occurrence",
]
assert method["selector_capabilities"]["direct_route_arguments"] == ["route_ref", "table", "file_name"]
assert method["input_schema"]["properties"]["ref"]["type"] == "string"
assert method["input_schema"]["properties"]["area_name"]["type"] == "string"
def test_metadata_cache_lookup_normalizes_public_object_ref(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_lookup(base_id: str, kind: str, name: str) -> dict[str, Any]:
seen["lookup"] = {"base_id": base_id, "kind": kind, "name": name}
return {
"base_id": base_id,
"kind": kind,
"kind_ru": "Документ",
"name": name,
"guid": "document-guid",
"source": "base",
}
monkeypatch.setattr(adapter_server, "metadata_cache_lookup_row", fake_lookup)
result = adapter_server.call_method(
"metadata.cache.lookup",
{
"base_id": "upo_test",
"ref": "Документы.ЗаказКлиента",
},
)
assert result["status"] == "ok"
assert seen["lookup"] == {
"base_id": "upo_test",
"kind": "Document",
"name": "ЗаказКлиента",
}
assert result["object"]["name"] == "ЗаказКлиента"
def test_metadata_cache_lookup_help_marks_cache_as_non_authoritative() -> None:
method = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}["metadata.cache.lookup"]
assert method["selector_capabilities"]["accepts_ordinal"] is False
assert method["selector_capabilities"]["requires_object_selector"] is True
assert method["selector_capabilities"]["read_only"] is True
assert method["selector_capabilities"]["authoritative_source"] == "live_sql_not_cache"
assert method["input_schema"]["properties"]["ref"]["type"] == "string"
def test_templates_areas_find_filters_by_area_query(monkeypatch: pytest.MonkeyPatch) -> None:
descriptor_key = "a" * 40
seen_payloads: list[dict[str, Any]] = []
def fake_read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]:
seen_payloads.append(payload)
return {
"status": "ok",
"templates": [
{
"name": "RouteRefTemplate",
"guid": descriptor_key,
"structure": {
"format": "MOXCEL",
"capabilities": {"named_area_coordinates": True},
"named_areas": [
{
"name": "ОбластьШапка",
"occurrence": 1,
"range": {"one_based": {"top": 1, "left": 1, "bottom": 2, "right": 4}},
},
{
"name": "ОбластьТело",
"occurrence": 1,
"range": {"one_based": {"top": 3, "left": 1, "bottom": 5, "right": 4}},
},
],
},
}
],
"counts": {"templates": 1, "parts": 1},
}
monkeypatch.setattr(adapter_server, "read_template_by_route", fake_read_template_by_route)
result = adapter_server.templates_areas_find(
{"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}", "area_query": "Тело", "max_areas": 1}
)
assert result["status"] == "ok"
assert seen_payloads[0]["max_areas"] == 5000
assert result["query"]["area_query"] == "Тело"
assert result["counts"]["area_filter_applied"] is True
assert result["counts"]["decoded_named_areas"] == 2
assert result["counts"]["areas"] == 1
assert result["counts"]["matching_areas"] == 1
assert result["counts"]["areas_limited"] is False
assert result["areas"][0]["name"] == "ОбластьТело"
def test_templates_areas_find_reports_matching_areas_before_limit(monkeypatch: pytest.MonkeyPatch) -> None:
descriptor_key = "a" * 40
monkeypatch.setattr(
adapter_server,
"read_template_by_route",
lambda payload, analyze=False: {
"status": "ok",
"templates": [
{
"name": "RouteRefTemplate",
"guid": descriptor_key,
"structure": {
"format": "MOXCEL",
"capabilities": {"named_area_coordinates": True},
"named_areas": [
{"name": "ОбластьШапка", "occurrence": 1, "range": {"one_based": {"top": 1, "left": 1, "bottom": 2, "right": 4}}},
{"name": "ОбластьТело", "occurrence": 1, "range": {"one_based": {"top": 3, "left": 1, "bottom": 5, "right": 4}}},
{"name": "ОбластьПодвал", "occurrence": 1, "range": {"one_based": {"top": 6, "left": 1, "bottom": 6, "right": 4}}},
],
},
}
],
"counts": {"templates": 1, "parts": 1},
},
)
result = adapter_server.templates_areas_find(
{"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}", "area_query": "Область", "max_areas": 1}
)
assert result["status"] == "ok"
assert result["counts"]["decoded_named_areas"] == 3
assert result["counts"]["areas"] == 1
assert result["counts"]["areas_returned"] == 1
assert result["counts"]["matching_areas"] == 3
assert result["counts"]["areas_limited"] is True
assert result["templates"][0]["counts"]["matching_areas"] == 3
assert result["templates"][0]["counts"]["areas_limited"] is True
def test_templates_areas_find_can_skip_coverage(monkeypatch: pytest.MonkeyPatch) -> None:
descriptor_key = "a" * 40
seen_payloads: list[dict[str, Any]] = []
def fake_read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]:
seen_payloads.append(payload)
return {
"status": "ok",
"templates": [
{
"name": "RouteRefTemplate",
"guid": descriptor_key,
"structure": {
"format": "MOXCEL",
"capabilities": {"named_area_coordinates": True},
"named_areas": [
{"name": "Header", "occurrence": 1, "range": {"one_based": {"top": 1, "left": 1, "bottom": 2, "right": 4}}},
],
"area_cell_coverage": [
{
"name": "Header",
"occurrence": 1,
"cell_count": 2,
"parameter_count": 1,
"cells": [{"row": 1, "column": 1}],
"parameters": [{"name": "Param"}],
}
],
},
}
],
"counts": {"templates": 1, "parts": 1},
}
monkeypatch.setattr(adapter_server, "read_template_by_route", fake_read_template_by_route)
result = adapter_server.templates_areas_find(
{"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}", "include_coverage": False}
)
assert result["status"] == "ok"
assert seen_payloads[0]["sections"] == "named_areas,diagnostics"
assert seen_payloads[0]["max_coverage"] == 0
assert result["query"]["include_coverage"] is False
assert "coverage" not in result["areas"][0]
def test_templates_areas_find_area_name_defaults_to_exact_match(monkeypatch: pytest.MonkeyPatch) -> None:
descriptor_key = "a" * 40
monkeypatch.setattr(
adapter_server,
"read_template_by_route",
lambda payload, analyze=False: {
"status": "ok",
"templates": [
{
"name": "RouteRefTemplate",
"guid": descriptor_key,
"structure": {
"format": "MOXCEL",
"capabilities": {"named_area_coordinates": True},
"named_areas": [
{
"name": "ОбластьШапка",
"occurrence": 1,
"range": {"one_based": {"top": 1, "left": 1, "bottom": 2, "right": 4}},
},
{
"name": "ОбластьШапкаЦена",
"occurrence": 1,
"range": {"one_based": {"top": 3, "left": 1, "bottom": 3, "right": 4}},
},
],
},
}
],
"counts": {"templates": 1, "parts": 1},
},
)
result = adapter_server.templates_areas_find(
{"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}", "area_name": "ОбластьШапка"}
)
assert result["status"] == "ok"
assert result["query"]["area_match"] == "exact"
assert result["counts"]["decoded_named_areas"] == 2
assert result["counts"]["areas"] == 1
assert result["areas"][0]["name"] == "ОбластьШапка"
def test_templates_areas_find_filters_by_area_occurrence(monkeypatch: pytest.MonkeyPatch) -> None:
descriptor_key = "a" * 40
seen_payloads: list[dict[str, Any]] = []
def fake_read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]:
seen_payloads.append(payload)
return {
"status": "ok",
"templates": [
{
"name": "RouteRefTemplate",
"guid": descriptor_key,
"structure": {
"format": "MOXCEL",
"capabilities": {"named_area_coordinates": True},
"named_areas": [
{
"name": "ОбластьШапка",
"occurrence": 1,
"range": {"one_based": {"top": 1, "left": 1, "bottom": 2, "right": 4}},
},
{
"name": "ОбластьШапка",
"occurrence": 2,
"range": {"one_based": {"top": 1, "left": 1, "bottom": 3, "right": 4}},
},
{
"name": "ОбластьШапкаЦена",
"occurrence": 1,
"range": {"one_based": {"top": 4, "left": 1, "bottom": 4, "right": 4}},
},
],
},
}
],
"counts": {"templates": 1, "parts": 1},
}
monkeypatch.setattr(adapter_server, "read_template_by_route", fake_read_template_by_route)
result = adapter_server.templates_areas_find(
{
"base_id": "upo_test",
"route_ref": f"ConfigCAS:{descriptor_key}",
"area_name": "ОбластьШапка",
"area_occurrence": 2,
"max_areas": 1,
}
)
assert result["status"] == "ok"
assert seen_payloads[0]["max_areas"] == 5000
assert result["query"]["area_match"] == "exact"
assert result["query"]["area_occurrence"] == 2
assert result["counts"]["area_filter_applied"] is True
assert result["counts"]["decoded_named_areas"] == 3
assert result["counts"]["areas"] == 1
assert result["counts"]["matching_areas"] == 1
assert result["counts"]["areas_limited"] is False
assert result["areas"][0]["name"] == "ОбластьШапка"
assert result["areas"][0]["occurrence"] == 2
assert result["areas"][0]["range"]["one_based"] == {"top": 1, "left": 1, "bottom": 3, "right": 4}
def test_templates_areas_find_rejects_zero_occurrence() -> None:
result = adapter_server.validate_templates_areas_find_payload(
{"base_id": "upo_test", "route_ref": "ConfigCAS:" + ("a" * 40), "area_name": "Header", "occurrence": 0}
)
assert result is not None
assert result["status"] == "invalid_argument"
assert result["argument"] == "area_occurrence"
def test_templates_areas_find_accepts_occurrence_alias(monkeypatch: pytest.MonkeyPatch) -> None:
descriptor_key = "a" * 40
monkeypatch.setattr(
adapter_server,
"read_template_by_route",
lambda payload, analyze=False: {
"status": "ok",
"templates": [
{
"name": "RouteRefTemplate",
"guid": descriptor_key,
"structure": {
"format": "MOXCEL",
"named_areas": [
{"name": "Header", "occurrence": 1, "range": {"one_based": {"top": 1, "left": 1, "bottom": 1, "right": 4}}},
{"name": "Header", "occurrence": 2, "range": {"one_based": {"top": 2, "left": 1, "bottom": 2, "right": 4}}},
],
},
}
],
"counts": {"templates": 1, "parts": 1},
},
)
validation = adapter_server.validate_templates_areas_find_payload(
{"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}", "area_name": "Header", "occurrence": 2}
)
result = adapter_server.templates_areas_find(
{"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}", "area_name": "Header", "occurrence": 2}
)
assert validation is None
assert result["status"] == "ok"
assert result["query"]["area_occurrence"] == 2
assert result["counts"]["areas"] == 1
assert result["areas"][0]["occurrence"] == 2
def test_templates_read_accepts_route_ref(monkeypatch: pytest.MonkeyPatch) -> None:
descriptor_key = "a" * 40
seen_payloads: list[dict[str, Any]] = []
def fake_read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]:
seen_payloads.append(payload)
return {"status": "ok", "templates": [], "counts": {"templates": 0, "parts": 0}}
monkeypatch.setattr(adapter_server, "read_template_by_route", fake_read_template_by_route)
validation = adapter_server.validate_templates_read_payload({"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}"})
result = adapter_server.templates_read({"base_id": "upo_test", "route_ref": f"ConfigCAS:{descriptor_key}"})
assert validation is None
assert result["status"] == "ok"
assert seen_payloads[0]["table"] == "ConfigCAS"
assert seen_payloads[0]["file_name"] == descriptor_key
def test_semantic_cache_search_returns_candidate_only(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
adapter_server.semantic_document_cache_upsert(
config,
document_id="template_part:ConfigCAS:part-key:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
object_kind="Template",
object_guid="part-key",
object_name="ТестовыйМакет",
extension_guid="99999999-9999-9999-9999-999999999999",
source_route={"table": "ConfigCAS", "file_name": "part-key", "part_id": "part-key"},
content_sha1="a" * 40,
text="area: ОбластьШапка\nparameter: Период\ncell R1C1: Меню за период",
)
result = adapter_server.semantic_cache_search({"base_id": "upo_test", "query": "ОбластьШапка", "kind": "Template"})
assert result["status"] == "ok"
assert result["source"]["authoritative"] is False
assert result["matches"][0]["freshness"]["status"] == "candidate_only"
assert result["matches"][0]["freshness"]["validation_required"] is True
assert result["matches"][0]["read_selector"]["method"] == "templates.read"
assert result["matches"][0]["cache"]["authoritative_source"] == "decoded_artifact_cache"
def test_semantic_cache_search_can_validate_top_candidates(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
data = b"validated-template-payload"
adapter_server.semantic_document_cache_upsert(
config,
document_id="doc-validated",
object_kind="Template",
object_guid="validated-template",
object_name="ValidatedTemplate",
extension_guid=None,
source_route={"table": "ConfigCAS", "file_name": "validated-template"},
content_sha1=hashlib.sha1(data).hexdigest(),
text="area: ОбластьШапка\ncell R1C1: Меню за период",
)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda base_id, table, file_name, timeout_seconds=30: (data, None, None),
)
result = adapter_server.semantic_cache_search(
{
"base_id": "upo_test",
"query": "ОбластьШапка",
"kind": "Template",
"validate_candidates": True,
}
)
assert result["status"] == "ok"
assert result["query"]["validate_candidates"] is True
assert result["counts"]["validation"]["checked"] == 1
assert result["counts"]["validation"]["fresh"] == 1
assert result["matches"][0]["freshness"]["status"] == "fresh"
assert result["matches"][0]["freshness"]["validation_required"] is False
assert result["matches"][0]["validation"]["source"]["authoritative"] is True
def test_semantic_embedding_upsert_requires_content_precondition(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
document_id = "template_part:ConfigCAS:part-key:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
adapter_server.semantic_document_cache_upsert(
config,
document_id=document_id,
object_kind="Template",
object_guid="part-key",
object_name="ТестовыйМакет",
extension_guid=None,
source_route={"table": "ConfigCAS", "file_name": "part-key"},
content_sha1="a" * 40,
text="area: ОбластьШапка",
)
pending = adapter_server.semantic_cache_pending({"base_id": "upo_test"})
assert pending["documents"][0]["precondition"]["content_sha1"] == "a" * 40
stored = adapter_server.semantic_cache_embedding_upsert(
{
"base_id": "upo_test",
"document_id": document_id,
"content_sha1": "a" * 40,
"embedding_model": "test-embedding",
"embedding": [1.0, 0.0],
}
)
assert stored["status"] == "ok"
assert stored["dimensions"] == 2
vector_result = adapter_server.semantic_cache_search({"base_id": "upo_test", "query_embedding": [1.0, 0.0]})
assert vector_result["status"] == "ok"
assert vector_result["matches"][0]["match_by"] == "vector_embedding"
assert vector_result["matches"][0]["cache"]["embedding_model"] == "test-embedding"
rejected = adapter_server.semantic_cache_embedding_upsert(
{
"base_id": "upo_test",
"document_id": document_id,
"content_sha1": "b" * 40,
"embedding_model": "test-embedding",
"embedding": [0.0, 1.0],
}
)
assert rejected["status"] == "conflict"
assert rejected["error"] == "content_sha1_mismatch"
def test_semantic_cache_status_reports_vector_readiness(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
adapter_server.semantic_document_cache_upsert(
config,
document_id="doc-pending",
object_kind="Template",
object_guid="pending-template",
object_name="PendingTemplate",
extension_guid=None,
source_route={"table": "ConfigCAS", "file_name": "pending-template"},
content_sha1="a" * 40,
text="area: ОбластьШапка",
)
adapter_server.semantic_document_cache_upsert(
config,
document_id="doc-embedded",
object_kind="Template",
object_guid="embedded-template",
object_name="EmbeddedTemplate",
extension_guid=None,
source_route={"table": "ConfigCAS", "file_name": "embedded-template"},
content_sha1="b" * 40,
text="area: ОбластьТело",
)
stored = adapter_server.semantic_cache_embedding_upsert(
{
"base_id": "upo_test",
"document_id": "doc-embedded",
"content_sha1": "b" * 40,
"embedding_model": "test-embedding",
"embedding": [0.0, 1.0],
}
)
assert stored["status"] == "ok"
result = adapter_server.semantic_cache_status({"base_id": "upo_test", "kind": "Template", "include_entries": True})
assert result["status"] == "ok"
assert result["schema"] == "onec_semantic_cache_status.v1"
assert result["source"]["authoritative"] is False
assert result["counts"]["total"] == 2
assert result["counts"]["pending_embedding"] == 1
assert result["counts"]["embedded"] == 1
assert any(group["vector_status"] == "pending_embedding" and group["count"] == 1 for group in result["groups"])
assert any(
group["vector_status"] == "embedded"
and group["embedding_model"] == "test-embedding"
and group["count"] == 1
for group in result["groups"]
)
assert {entry["document_id"] for entry in result["entries"]} == {"doc-pending", "doc-embedded"}
def test_semantic_cache_validate_confirms_current_source_sha(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
data = b"current-template-payload"
document_id = "doc-fresh"
adapter_server.semantic_document_cache_upsert(
config,
document_id=document_id,
object_kind="Template",
object_guid="fresh-template",
object_name="FreshTemplate",
extension_guid=None,
source_route={"table": "ConfigCAS", "file_name": "fresh-template"},
content_sha1=hashlib.sha1(data).hexdigest(),
text="area: ОбластьШапка",
)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda base_id, table, file_name, timeout_seconds=30: (data, None, None),
)
result = adapter_server.semantic_cache_validate({"base_id": "upo_test", "document_id": document_id})
assert result["status"] == "ok"
assert result["source"]["authoritative"] is True
assert result["freshness"]["status"] == "fresh"
assert result["read_selector"]["method"] == "templates.read"
def test_semantic_cache_validate_marks_changed_payload_stale(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
document_id = "doc-stale"
adapter_server.semantic_document_cache_upsert(
config,
document_id=document_id,
object_kind="Template",
object_guid="stale-template",
object_name="StaleTemplate",
extension_guid=None,
source_route={"table": "ConfigCAS", "file_name": "stale-template"},
content_sha1=hashlib.sha1(b"old-template-payload").hexdigest(),
text="area: ОбластьШапка",
)
stored = adapter_server.semantic_cache_embedding_upsert(
{
"base_id": "upo_test",
"document_id": document_id,
"content_sha1": hashlib.sha1(b"old-template-payload").hexdigest(),
"embedding_model": "test-embedding",
"embedding": [1.0, 0.0],
}
)
assert stored["status"] == "ok"
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda base_id, table, file_name, timeout_seconds=30: (b"new-template-payload", None, None),
)
result = adapter_server.semantic_cache_validate({"base_id": "upo_test", "document_id": document_id})
status = adapter_server.semantic_cache_status({"base_id": "upo_test", "kind": "Template"})
assert result["status"] == "stale"
assert result["error"] == "content_changed"
assert result["freshness"]["validation_required"] is True
assert result["cache"]["cached_content_sha1"] == hashlib.sha1(b"old-template-payload").hexdigest()
assert result["cache"]["current_content_sha1"] == hashlib.sha1(b"new-template-payload").hexdigest()
assert status["counts"]["error"] == 1
assert status["counts"]["embedded"] == 0
def test_semantic_cache_validate_batch_summarizes_fresh_and_stale(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
payload_by_file = {
"fresh-template": b"fresh-template-payload",
"stale-template": b"new-stale-template-payload",
}
old_stale_sha1 = hashlib.sha1(b"old-stale-template-payload").hexdigest()
adapter_server.semantic_document_cache_upsert(
config,
document_id="doc-fresh",
object_kind="Template",
object_guid="fresh-template",
object_name="FreshTemplate",
extension_guid=None,
source_route={"table": "ConfigCAS", "file_name": "fresh-template"},
content_sha1=hashlib.sha1(payload_by_file["fresh-template"]).hexdigest(),
text="area: ОбластьШапка",
)
adapter_server.semantic_document_cache_upsert(
config,
document_id="doc-stale",
object_kind="Template",
object_guid="stale-template",
object_name="StaleTemplate",
extension_guid=None,
source_route={"table": "ConfigCAS", "file_name": "stale-template"},
content_sha1=old_stale_sha1,
text="area: ОбластьТело",
)
stored = adapter_server.semantic_cache_embedding_upsert(
{
"base_id": "upo_test",
"document_id": "doc-stale",
"content_sha1": old_stale_sha1,
"embedding_model": "test-embedding",
"embedding": [1.0, 0.0],
}
)
assert stored["status"] == "ok"
def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, timeout_seconds: int = 30) -> tuple[bytes | None, None, None]:
return payload_by_file.get(file_name), None, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.semantic_cache_validate_batch({"base_id": "upo_test", "kind": "Template"})
status = adapter_server.semantic_cache_status({"base_id": "upo_test", "kind": "Template"})
assert result["status"] == "ok"
assert result["schema"] == "onec_semantic_cache_validate_batch.v1"
assert result["counts"]["checked"] == 2
assert result["counts"]["fresh"] == 1
assert result["counts"]["stale"] == 1
assert {item["status"] for item in result["results"]} == {"ok", "stale"}
stale = next(item for item in result["results"] if item["status"] == "stale")
assert stale["error"] == "content_changed"
assert stale["cache"]["cached_content_sha1"] == old_stale_sha1
assert status["counts"]["error"] == 1
assert status["counts"]["embedded"] == 0
def test_semantic_cache_refresh_redecodes_changed_template_and_queues_embedding(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
old_sha1 = hashlib.sha1(b"old-template-payload").hexdigest()
new_payload = b"new-template-payload"
new_sha1 = hashlib.sha1(new_payload).hexdigest()
old_document_id = f"template_part:ConfigCAS:refresh-template:{old_sha1}"
adapter_server.semantic_document_cache_upsert(
config,
document_id=old_document_id,
object_kind="Template",
object_guid="refresh-template",
object_name="RefreshTemplate",
extension_guid=None,
source_route={"table": "ConfigCAS", "file_name": "refresh-template", "part_id": "refresh-template"},
content_sha1=old_sha1,
text="area: СтараяОбласть",
)
stored = adapter_server.semantic_cache_embedding_upsert(
{
"base_id": "upo_test",
"document_id": old_document_id,
"content_sha1": old_sha1,
"embedding_model": "test-embedding",
"embedding": [1.0, 0.0],
}
)
assert stored["status"] == "ok"
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda base_id, table, file_name, timeout_seconds=60: (new_payload, None, None),
)
monkeypatch.setattr(
adapter_server,
"extract_moxel_public_structure",
lambda data: {
"format": "MOXCEL",
"dimensions": {"rows": 2, "columns": 2},
"named_areas": [{"name": "ОбластьШапка", "range": {"address": "R1C1:R1C2"}}],
"parameters": [{"name": "Период"}],
"cells": [{"row": 1, "column": 1, "text": "Меню за период"}],
"diagnostics": [],
},
)
result = adapter_server.semantic_cache_refresh({"base_id": "upo_test", "document_id": old_document_id})
status = adapter_server.semantic_cache_status({"base_id": "upo_test", "kind": "Template", "include_entries": True})
pending = adapter_server.semantic_cache_pending({"base_id": "upo_test", "kind": "Template"})
assert result["status"] == "ok"
assert result["previous_document_id"] == old_document_id
assert result["document_id"] == f"template_part:ConfigCAS:refresh-template:{new_sha1}"
assert result["cache"]["content_changed"] is True
assert result["cache"]["vector_status"] == "pending_embedding"
assert result["precondition"]["content_sha1"] == new_sha1
assert status["counts"]["total"] == 1
assert status["counts"]["pending_embedding"] == 1
assert status["counts"]["embedded"] == 0
assert status["entries"][0]["document_id"] == result["document_id"]
assert pending["documents"][0]["document_id"] == result["document_id"]
assert pending["documents"][0]["precondition"]["content_sha1"] == new_sha1
def test_semantic_cache_rebuild_warms_from_extension_route_cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
descriptor_key = "a" * 40
part_key = "b" * 40
content_sha1 = "c" * 40
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
monkeypatch.setattr(adapter_server, "extension_filter_to_guid", lambda base_id, value, method: (None, None))
adapter_server.extension_route_cache_upsert(
config,
{
"kind": "Template",
"kind_ru": "Макет",
"name": "RouteTemplate",
"guid": descriptor_key,
"origin": {"extension": {"name": "TestExtension", "guid": "99999999-9999-9999-9999-999999999999"}},
"route": {"route_type": "extension_manifest_cas", "table": "ConfigCAS", "file_name": descriptor_key},
"manifest_entries": [],
},
descriptor_payload_sha1="sha",
)
calls: list[dict[str, Any]] = []
def fake_read_template_by_route(payload: dict[str, Any], *, analyze: bool = False) -> dict[str, Any]:
calls.append(payload)
adapter_server.semantic_document_cache_upsert(
config,
document_id=f"template_part:ConfigCAS:{part_key}:{content_sha1}",
object_kind="Template",
object_guid=part_key,
object_name="RouteTemplate",
extension_guid=None,
source_route={"table": "ConfigCAS", "file_name": part_key, "part_id": part_key},
content_sha1=content_sha1,
text="area: ОбластьШапка\ncell R1C1: Меню за период",
)
return {"status": "ok", "counts": {"templates": 1, "parts": 1}}
monkeypatch.setattr(adapter_server, "read_template_by_route", fake_read_template_by_route)
result = adapter_server.semantic_cache_rebuild({"base_id": "upo_test", "kind": "Template", "include_entries": True})
pending = adapter_server.semantic_cache_pending({"base_id": "upo_test", "kind": "Template"})
assert result["status"] == "ok"
assert result["source"]["authoritative"] is False
assert result["counts"]["routes_scanned"] == 1
assert result["counts"]["templates_read"] == 1
assert result["counts"]["parts_seen"] == 1
assert result["counts"]["semantic_documents_created"] == 1
assert result["counts"]["errors"] == 0
assert result["entries"][0]["descriptor_cas_key"] == descriptor_key
assert calls[0]["file_name"] == descriptor_key
assert pending["documents"][0]["document_id"] == f"template_part:ConfigCAS:{part_key}:{content_sha1}"
def test_modules_search_does_not_pass_empty_prefix_to_storage_list(monkeypatch: pytest.MonkeyPatch) -> None:
seen_payloads: list[dict] = []
def fake_storage_files_list(payload: dict) -> dict:
seen_payloads.append(payload)
assert "prefix" not in payload
return {"status": "ok", "files": [], "counts": {"files": 0}}
monkeypatch.setattr(adapter_server, "storage_files_list", fake_storage_files_list)
monkeypatch.setattr(adapter_server, "read_storage_files_bytes", lambda *args, **kwargs: ({}, {}, None))
result = adapter_server.search_modules({"base_id": "upo_test", "query": "НетТакогоТекста", "scan_limit": 10})
assert result["status"] == "ok"
assert result["counts"]["matches"] == 0
assert seen_payloads
def test_metadata_kinds_include_report_and_data_processor_roles(monkeypatch: pytest.MonkeyPatch) -> None:
class Record:
def __init__(self, guid: str, storage_role: str) -> None:
self.guid = guid
self.storage_role = storage_role
records = [
Record("11111111-1111-1111-1111-111111111111", "Report"),
Record("22222222-2222-2222-2222-222222222222", "DataProcessor"),
]
monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *args, **kwargs: (records, None))
result = adapter_server.get_kinds("upo_test")
assert result["status"] == "ok"
assert result["internal_kind_counts"]["Report"] == 1
assert result["internal_kind_counts"]["DataProcessor"] == 1
assert {"kind": "report", "count": 1} in result["kinds"]
assert {"kind": "processing", "count": 1} in result["kinds"]
def test_get_object_reads_extension_data_processor_from_configcas(monkeypatch: pytest.MonkeyPatch) -> None:
class Record:
guid = "22222222-2222-2222-2222-222222222222"
storage_role = "DataProcessor"
source = "DBNames-Ext-11111111-1111-1111-1111-111111111111"
sql_number = 1
index = 1
read_tables: list[str] = []
monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *args, **kwargs: ([Record()], None))
monkeypatch.setattr(adapter_server, "metadata_cache_lookup_row", lambda *args, **kwargs: None)
def fake_read_files(base_id: str, table: str, file_names: list[str], **kwargs: Any) -> tuple[dict[str, bytes], dict[str, str], None]:
read_tables.append(table)
if table == "ConfigCAS":
return {Record.guid: b"payload"}, {"database": "upo_test"}, None
return {}, {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "read_storage_files_bytes", fake_read_files)
monkeypatch.setattr(
adapter_server,
"config_identity_from_bytes",
lambda data: {"guid": Record.guid, "name": "ТестоваяОбработка", "synonyms": {"ru": "Тестовая обработка"}} if data else None,
)
result = adapter_server.get_object(
"Обработки",
"ТестоваяОбработка",
base_id="upo_test",
include_semantic=False,
include_storage=True,
)
assert result["status"] == "ok"
assert result["object"]["kind"] == "DataProcessor"
assert result["object"]["source"] == "extension"
assert result["object"]["storage"]["table"] == "ConfigCAS"
assert read_tables == ["ConfigCAS"]
def test_get_object_falls_back_to_extension_definition_for_data_processor(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *args, **kwargs: ([], None))
monkeypatch.setattr(adapter_server, "metadata_cache_lookup_row", lambda *args, **kwargs: None)
monkeypatch.setattr(
adapter_server,
"metadata_extension_definition_matches",
lambda **kwargs: (
[
{
"kind": "Определение расширения",
"guid": "22222222-2222-2222-2222-222222222222",
"name": "ТестоваяОбработка",
"synonym": "Тестовая обработка",
"source": {"table": "ConfigCAS", "file_name": "22222222-2222-2222-2222-222222222222"},
}
],
[],
),
)
result = adapter_server.get_object(
"Обработки",
"ТестоваяОбработка",
base_id="upo_test",
include_semantic=False,
include_storage=True,
)
assert result["status"] == "ok"
assert result["object"]["kind"] == "DataProcessor"
assert result["object"]["source"] == "extension"
assert result["object"]["storage"]["table"] == "ConfigCAS"
assert result["object"]["match_by"] == "extension_definition"
def test_extension_definition_match_accepts_configcas_identity_without_dbnames_source(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *args, **kwargs: ([], None))
monkeypatch.setattr(adapter_server, "extension_map_by_guid", lambda *args, **kwargs: {})
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda *args, **kwargs: (None, None))
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {"status": "ok", "files": [{"FileName": "cas-file"}]},
)
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (b"payload", {}, None))
monkeypatch.setattr(adapter_server, "read_storage_files_bytes", lambda *args, **kwargs: ({"cas-file": b"payload"}, {}, None))
monkeypatch.setattr(
adapter_server,
"payload_text_from_bytes",
lambda data: {"text": "935ca39d-dfc4-445d-b696-03484dc5b145"},
)
monkeypatch.setattr(
adapter_server,
"config_identity_from_bytes",
lambda data: {"guid": "935ca39d-dfc4-445d-b696-03484dc5b145", "name": "ТестоваяОбработка", "synonyms": {}},
)
matches, diagnostics = adapter_server.metadata_extension_definition_matches(
base_id="upo_test",
query="ТестоваяОбработка",
max_files=10,
max_matches=5,
timeout_seconds=30,
include_storage=True,
)
assert len(matches) == 1
assert matches[0]["name"] == "ТестоваяОбработка"
assert matches[0]["kind"] == "Определение расширения"
assert matches[0]["source"]["table"] == "ConfigCAS"
assert matches[0]["source"]["file_name"] == "cas-file"
assert any(item.get("scanned_payloads") == 1 for item in diagnostics)
def test_dynamic_list_settings_detect_query_text_and_flags() -> None:
node = {
"type": "list",
"items": [
{"type": "string", "value": "ОсновнаяТаблица"},
{"type": "string", "value": "Catalog.Номенклатура"},
{"type": "string", "value": "ПроизвольныйЗапрос"},
{"type": "string", "value": "B"},
{"type": "number", "value": "1"},
{"type": "string", "value": "ТекстЗапроса"},
{"type": "string", "value": "S"},
{"type": "string", "value": "ВЫБРАТЬ Номенклатура.Ссылка ИЗ Справочник.Номенклатура КАК Номенклатура"},
],
}
settings = dynamic_list_settings(node, owner_path="3.4")
assert settings == {
"main_table": "Catalog.Номенклатура",
"custom_query": True,
"query_text": "ВЫБРАТЬ Номенклатура.Ссылка ИЗ Справочник.Номенклатура КАК Номенклатура",
"paths": {"main_table": "3.4.1", "custom_query": "3.4.4", "query_text": "3.4.7"},
"sources": {"main_table": "key_value", "custom_query": "key_value", "query_text": "key_value"},
"status": "ok",
}
def test_definition_metadata_match_accepts_public_kind_reference(monkeypatch: pytest.MonkeyPatch) -> None:
scanned_kinds: list[str] = []
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "sql-host", "database": "upo_test"}, None))
def fake_list_objects(kind: str, **kwargs: Any) -> dict[str, Any]:
scanned_kinds.append(kind)
assert kind == "DataProcessor"
return {
"status": "ok",
"objects": [
{
"kind": "DataProcessor",
"kind_ru": "Обработка",
"public_kind": "processing",
"name": "ТестоваяОбработка",
"synonym": "Тестовая обработка",
"guid": "22222222-2222-2222-2222-222222222222",
"source": "base",
}
],
}
monkeypatch.setattr(adapter_server, "list_objects", fake_list_objects)
matches, diagnostics = adapter_server.metadata_configuration_definition_matches(
base_id="upo_test",
query="Обработка.ТестоваяОбработка",
max_matches=5,
use_cache=False,
)
assert scanned_kinds == ["DataProcessor"]
assert diagnostics == [{"area": "metadata", "source": "live_metadata", "cache": "not_used", "matches": 1}]
assert len(matches) == 1
assert matches[0]["kind"] == "Обработка"
assert matches[0]["name"] == "ТестоваяОбработка"
assert matches[0]["read_selector"]["kind"] == "DataProcessor"
assert matches[0]["read_selector"]["ref"] == "DataProcessor.ТестоваяОбработка"
assert matches[0]["related_selectors"]["modules"]["method"] == "metadata.object.modules"
assert matches[0]["related_selectors"]["code_search"]["method"] == "code.search"
assert matches[0]["related_selectors"]["modules_search"]["method"] == "modules.search"
assert matches[0]["related_selectors"]["card"]["method"] == "metadata.object.get"
assert matches[0]["related_selectors"]["full"]["method"] == "metadata.object.full"
assert "query" not in matches[0]["related_selectors"]["code_search"]
assert matches[0]["related_selectors"]["forms"]["kind"] == "DataProcessor"
assert matches[0]["related_selectors"]["forms"]["ref"] == "DataProcessor.ТестоваяОбработка"
def test_definition_metadata_live_match_does_not_require_cache_config(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"sql_config_for_base",
lambda base_id: pytest.fail("live metadata definition lookup must not require cache config"),
)
monkeypatch.setattr(
adapter_server,
"list_objects",
lambda kind, **kwargs: {
"status": "ok",
"objects": [
{
"kind": "DataProcessor",
"kind_ru": "Обработка",
"public_kind": "processing",
"name": "ТестоваяОбработка",
"synonym": "Тестовая обработка",
"guid": "22222222-2222-2222-2222-222222222222",
"source": "base",
}
],
},
)
matches, diagnostics = adapter_server.metadata_configuration_definition_matches(
base_id="upo_test",
query="Обработка.ТестоваяОбработка",
max_matches=5,
use_cache=False,
)
assert len(matches) == 1
assert matches[0]["name"] == "ТестоваяОбработка"
assert diagnostics == [{"area": "metadata", "source": "live_metadata", "cache": "not_used", "matches": 1}]
def test_definition_find_promotes_single_metadata_object_and_selectors(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "sql-host", "database": "upo_test"}, None))
monkeypatch.setattr(
adapter_server,
"list_objects",
lambda kind, **kwargs: {
"status": "ok",
"objects": [
{
"kind": "DataProcessor",
"kind_ru": "Обработка",
"public_kind": "processing",
"name": "ТестоваяОбработка",
"synonym": "Тестовая обработка",
"guid": "22222222-2222-2222-2222-222222222222",
"source": "base",
}
],
},
)
result = adapter_server.metadata_definition_find(
{
"base_id": "upo_test",
"query": "Обработка.ТестоваяОбработка",
"areas": ["metadata"],
"max_matches": 5,
}
)
assert result["status"] == "ok"
assert result["object"]["kind"] == "DataProcessor"
assert result["object"]["name"] == "ТестоваяОбработка"
assert result["related_selectors"]["modules"]["guid"] == "22222222-2222-2222-2222-222222222222"
assert result["related_selectors"]["modules"]["ref"] == "DataProcessor.ТестоваяОбработка"
assert result["related_selectors"]["form_details"]["method"] == "metadata.object.form.details"
assert result["related_selectors"]["modules_search"]["method"] == "modules.search"
assert result["related_selectors"]["card"]["method"] == "metadata.object.get"
def test_extension_object_read_selectors_include_form_routes_for_form_capable_kind() -> None:
selectors = adapter_server.extension_object_read_selectors(
"upo_test",
"DataProcessor",
{"name": "AnyProcessing", "guid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"},
{"table": "ConfigCAS", "file_name": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"},
)
assert selectors["forms"] == {
"method": "metadata.object.forms",
"base_id": "upo_test",
"kind": "DataProcessor",
"guid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"name": "AnyProcessing",
"table": "ConfigCAS",
}
assert selectors["form_details"]["method"] == "metadata.object.form.details"
assert selectors["form_details"]["guid"] == "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
assert selectors["form_details"]["table"] == "ConfigCAS"
def test_definition_find_promotes_resolved_guid_selector_without_metadata_match(monkeypatch: pytest.MonkeyPatch) -> None:
guid = "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
monkeypatch.setattr(
adapter_server,
"get_object",
lambda *args, **kwargs: {
"status": "ok",
"object": {
"kind": "Document",
"kind_ru": "Документ",
"name": "АвансовыйОтчет",
"synonym": "Авансовый отчет",
"guid": guid,
},
},
)
monkeypatch.setattr(
adapter_server,
"metadata_configuration_definition_matches",
lambda **kwargs: ([], [{"area": "metadata", "source": "live_metadata", "matches": 0}]),
)
result = adapter_server.metadata_definition_find(
{
"base_id": "upo_test",
"query": guid,
"guid": guid,
"areas": ["metadata"],
"exact_only": True,
"max_matches": 5,
}
)
assert result["status"] == "ok"
assert result["object"]["guid"] == guid
assert result["matches"][0]["area"] == "metadata"
assert result["matches"][0]["match_by"] == "guid_exact"
assert result["related_selectors"]["modules"]["method"] == "metadata.object.modules"
def test_definition_find_accepts_object_type_name_aliases(monkeypatch: pytest.MonkeyPatch) -> None:
seen_get_object: dict[str, Any] = {}
seen_attributes: list[dict[str, Any]] = []
def fake_get_object(kind: str | None, name_or_guid: str, **kwargs: Any) -> dict[str, Any]:
seen_get_object.update({"kind": kind, "name_or_guid": name_or_guid})
return {
"status": "ok",
"object": {
"kind": "DataProcessor",
"kind_ru": "Обработка",
"public_kind": "processing",
"name": "ТестоваяОбработка",
"synonym": "Тестовая обработка",
"guid": "22222222-2222-2222-2222-222222222222",
},
}
def fake_metadata_object_attributes(payload: dict[str, Any]) -> dict[str, Any]:
seen_attributes.append(payload)
return {
"status": "ok",
"dimensions": [],
"resources": [],
"attributes": [{"name": "ЦелевойРеквизит", "type": {"presentation": "Строка"}}],
"tabular_sections": [],
}
monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
monkeypatch.setattr(adapter_server, "metadata_object_attributes", fake_metadata_object_attributes)
result = adapter_server.call_method(
"metadata.definition.find",
{
"base_id": "upo_test",
"object_type": "Обработка",
"object_name": "ТестоваяОбработка",
"query": "ЦелевойРеквизит",
"areas": ["object"],
},
)
assert result["status"] == "ok"
assert seen_get_object == {"kind": "DataProcessor", "name_or_guid": "ТестоваяОбработка"}
assert seen_attributes[0]["kind"] == "DataProcessor"
assert seen_attributes[0]["name"] == "ТестоваяОбработка"
assert result["matches"][0]["object"]["kind"] == "DataProcessor"
def test_definition_find_direct_call_accepts_object_type_name_aliases(monkeypatch: pytest.MonkeyPatch) -> None:
seen_get_object: dict[str, Any] = {}
def fake_get_object(kind: str | None, name_or_guid: str, **kwargs: Any) -> dict[str, Any]:
seen_get_object.update({"kind": kind, "name_or_guid": name_or_guid})
return {
"status": "ok",
"object": {
"kind": "DataProcessor",
"kind_ru": "Обработка",
"public_kind": "processing",
"name": "ТестоваяОбработка",
"guid": "22222222-2222-2222-2222-222222222222",
},
}
monkeypatch.setattr(adapter_server, "get_object", fake_get_object)
monkeypatch.setattr(
adapter_server,
"metadata_object_attributes",
lambda payload: {
"status": "ok",
"dimensions": [],
"resources": [],
"attributes": [{"name": "ЦелевойРеквизит"}],
"tabular_sections": [],
},
)
result = adapter_server.metadata_definition_find(
{
"base_id": "upo_test",
"object_type": "Обработка",
"object_name": "ТестоваяОбработка",
"query": "ЦелевойРеквизит",
"areas": ["object"],
}
)
assert result["status"] == "ok"
assert seen_get_object == {"kind": "DataProcessor", "name_or_guid": "ТестоваяОбработка"}
def test_metadata_adapter_audit_reports_recognized_missing_and_unmapped(monkeypatch: pytest.MonkeyPatch) -> None:
class Record:
def __init__(self, guid: str, storage_role: str) -> None:
self.guid = guid
self.storage_role = storage_role
records = [
Record("11111111-1111-1111-1111-111111111111", "Document"),
Record("22222222-2222-2222-2222-222222222222", "DataProcessor"),
Record("33333333-3333-3333-3333-333333333333", "ExtDataSrcPrms"),
]
monkeypatch.setattr(adapter_server, "live_dbnames_records", lambda *args, **kwargs: (records, None))
compact = adapter_server.metadata_adapter_audit({"base_id": "upo_test"})
detailed = adapter_server.metadata_adapter_audit({"base_id": "upo_test", "include_missing": True, "include_unmapped": True})
assert compact["status"] == "ok"
assert compact["counts"]["recognized_kinds"] == 2
assert compact["recognized_kind_counts"] == {"DataProcessor": 1, "Document": 1}
assert compact["public_kind_counts"]["document"] == 1
assert compact["public_kind_counts"]["processing"] == 1
assert all(item["count"] > 0 for item in compact["metadata_kinds"])
assert compact["code_carriers"]["object_modules"]["module_roles"] == ["object_module", "manager_module", "command_module"]
assert compact["code_carriers"]["register_modules"]["module_roles"] == ["record_set_module", "register_module", "command_module"]
assert "CalculationRegister" in compact["code_carriers"]["register_modules"]["owner_kinds"]
assert compact["code_carriers"]["form_modules"]["module_roles"] == ["form_module"]
assert compact["code_carriers"]["scheduled_jobs"]["status"] == "supported_reference"
assert compact["code_carriers"]["web_http_service_modules"]["status"] == "supported_read"
assert compact["write_capabilities"]["bsl_modules"]["status"] == "supported_saved_state"
assert compact["write_capabilities"]["templates"]["status"] == "read_only"
assert detailed["counts"]["recognized_kinds"] == 2
assert detailed["counts"]["supported_kinds"] == len(adapter_server.TOP_LEVEL_METADATA_KINDS)
assert detailed["counts"]["missing_supported_kinds"] == len(adapter_server.TOP_LEVEL_METADATA_KINDS) - 2
assert any(
item["kind"] == "Report"
and item["presence_status"] == "supported_absent_in_selected_base"
for item in detailed["missing_supported_kinds"]
)
assert detailed["metadata_candidates"] == []
assert detailed["unmapped_source_roles"] == [
{"source_role": "ExtDataSrcPrms", "category": "auxiliary_storage", "records": 1}
]
def test_dbnames_platform_service_tables_are_not_reported_as_metadata_candidates() -> None:
system_roles = {
"Acoustic",
"Bots",
"Ecs",
"LangModel",
"MobileClientDataExchange",
"STTGrammar",
"STTGrammarChecksum",
"STTModels",
"STTModelsDesc",
"STTSettings",
"URLExternalData",
"WebSocketClients",
}
assert {adapter_server.classify_unmapped_source_role(role) for role in system_roles} == {
"platform_system_storage"
}
assert adapter_server.classify_unmapped_source_role("ExtDataSrcPrms") == "auxiliary_storage"
def test_modules_search_returns_public_read_selector_without_storage(monkeypatch: pytest.MonkeyPatch) -> None:
class FakeStreamClassifier:
pass
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {"status": "ok", "files": [{"FileName": "module-guid"}], "counts": {"files": 1}},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({"module-guid": b"payload"}, {"database": "upo_test"}, None),
)
monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": ""})
monkeypatch.setattr(adapter_server, "extract_bsl_text_from_container", lambda text, **kwargs: ("", {"status": "not_found"}))
monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
FakeStreamClassifier.classify_payload = staticmethod(
lambda data, include_text=True: {
"role": "bsl_module_payload",
"stream_blocks": [{"text": "Процедура Команда()\nЗапрос = Новый Запрос;\nКонецПроцедуры\n"}],
}
)
result = adapter_server.search_modules({"base_id": "upo_test", "query": "Новый Запрос", "scan_limit": 10, "state": "active"})
assert result["status"] == "ok"
assert result["counts"]["matches"] == 1
assert result["counts"]["complete"] is True
assert result["counts"]["scan_limit_hit"] is False
assert result["counts"]["owner_unresolved"] == 1
match = result["matches"][0]
assert match["owner"]["status"] == "unresolved"
assert match["read_selector"]["base_id"] == "upo_test"
assert match["read_selector"]["method"] == "modules.read"
assert match["read_selector"]["module_ref"] == "ConfigCAS:module-guid#stream:0"
assert match["read_selector"]["preview"] is True
assert "module_id" not in match
assert "table" not in match
assert "file_name" not in match
def test_modules_search_defaults_to_working_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
class FakeStreamClassifier:
pass
seen_tables: list[str] = []
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: seen_tables.append(str(payload.get("table"))) or {"status": "ok", "files": [{"FileName": "module-guid"}], "counts": {"files": 1}},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({"module-guid": b"payload"}, {"database": "upo_test"}, None),
)
monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": ""})
monkeypatch.setattr(adapter_server, "extract_bsl_text_from_container", lambda text, **kwargs: ("", {"status": "not_found"}))
monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
FakeStreamClassifier.classify_payload = staticmethod(
lambda data, include_text=True: {
"role": "bsl_module_payload",
"stream_blocks": [{"text": "Процедура SavedStateProbe()\nКонецПроцедуры\n"}],
}
)
result = adapter_server.search_modules({"base_id": "upo_test", "query": "SavedStateProbe", "scan_limit": 10})
assert result["status"] == "ok"
assert result["query"]["state"] == "working"
assert seen_tables[0] == "ConfigCASSave"
assert result["matches"][0]["read_selector"]["module_ref"] == "ConfigCASSave:module-guid#stream:0"
assert result["matches"][0]["activation_state"] == "saved_state"
def test_modules_search_repairs_saved_state_stream_and_reports_owner_role(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "extension-guid"
catalog_guid = "catalog-guid"
file_name = f"{extension_guid}__{catalog_guid}.3"
descriptor_file_name = f"{extension_guid}__{catalog_guid}"
good_text = "// Это модуль менеджера\r\nПроцедура ОбработкаКоманды(Команда)\r\nКонецПроцедуры\r\n"
mojibake_text = good_text.encode("utf-8").decode("cp1251")
module_text = mojibake_text.encode("utf-8-sig")
header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
stored_module = compress_payload(b"prefix" + header + module_text, "raw_deflate")
stored_descriptor = compress_payload('{"tt_Справочник1"}'.encode("utf-8-sig"), "raw_deflate")
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {"status": "ok", "files": [{"FileName": file_name}], "counts": {"files": 1}},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({file_name: stored_module}, {"database": "upo_test"}, None),
)
def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
assert base_id == "upo_test"
assert table == "ConfigCASSave"
if requested_file_name == descriptor_file_name:
return stored_descriptor, {"database": "upo_test"}, None
return stored_module, {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.search_modules(
{
"base_id": "upo_test",
"query": "Это модуль менеджера",
"scan_limit": 10,
"state": "save",
"scope": "modules",
}
)
assert result["status"] == "ok"
assert result["counts"]["matches"] == 1
match = result["matches"][0]
assert match["owner"]["name"] == "tt_Справочник1"
assert match["module"]["kind"] == "manager_module"
assert match["module"]["name"] == "Модуль менеджера"
assert match["activation_state"] == "saved_state"
assert match["read_selector"]["module_ref"] == f"ConfigCASSave:{file_name}#stream:0"
def test_modules_search_saved_state_form_container_reports_form_module(monkeypatch: pytest.MonkeyPatch) -> None:
file_name = "extension-guid__form-guid.0"
form_text = '{"meta","data","&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\nКонецПроцедуры\r\n"}'
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {"status": "ok", "files": [{"FileName": file_name}], "counts": {"files": 1}},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({file_name: b"payload"}, {"database": "upo_test"}, None),
)
monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": form_text})
monkeypatch.setattr(
adapter_server,
"saved_state_descriptor_identity",
lambda **kwargs: {"name": "t_Форма", "synonym": "T форма", "guid": "form-guid", "source": "saved_state_descriptor"},
)
monkeypatch.setattr(adapter_server, "saved_state_related_descriptor_identity", lambda **kwargs: None)
result = adapter_server.search_modules(
{
"base_id": "upo_test",
"query": "ЗаменаДомена",
"scan_limit": 10,
"state": "save",
"scope": "modules",
}
)
assert result["status"] == "ok"
match = result["matches"][0]
assert match["form"]["name"] == "t_Форма"
assert match["module"]["kind"] == "form_module"
assert match["module"]["name"] == "Модуль формы"
assert match["module"]["form"] == "t_Форма"
assert match["read_selector"]["module_ref"] == f"ConfigCASSave:{file_name}"
def test_module_roles_depend_on_owner_kind_and_suffix() -> None:
assert adapter_server.public_module_role(owner_kind="Catalog", suffix="0") == {
"kind": "object_module",
"name": "Модуль объекта",
"suffix": "0",
}
assert adapter_server.public_module_role(owner_kind="Document", suffix="3") == {
"kind": "manager_module",
"name": "Модуль менеджера",
"suffix": "3",
}
assert adapter_server.public_module_role(owner_kind="InformationRegister", suffix="0") == {
"kind": "record_set_module",
"name": "Модуль набора записей",
"suffix": "0",
}
assert adapter_server.public_module_role(owner_kind="AccumulationRegister", suffix="0") == {
"kind": "record_set_module",
"name": "Модуль набора записей",
"suffix": "0",
}
assert adapter_server.public_module_role(owner_kind="Constant", suffix="0") == {
"kind": "value_manager_module",
"name": "Модуль менеджера значения",
"suffix": "0",
}
assert adapter_server.public_module_role(owner_kind="Catalog", suffix="2") == {
"kind": "command_module",
"name": "Модуль команды",
"suffix": "2",
}
assert adapter_server.public_module_role(owner_kind="Document", suffix=".2") == {
"kind": "command_module",
"name": "Модуль команды",
"suffix": "2",
}
assert adapter_server.public_module_role(owner_kind="AccountingRegister", suffix=".6", ordinal=2) == {
"kind": "register_module",
"name": "Модуль регистра 2",
}
def test_public_module_row_uses_owner_kind_for_register_module() -> None:
module = {"module_id": "ConfigCAS:register-guid.0#stream:0", "suffix": "0"}
public = adapter_server.public_module_row(module, include_storage=False, ordinal=1, owner_kind="InformationRegister")
assert public["kind"] == "record_set_module"
assert public["name"] == "Модуль набора записей"
assert public["module_ordinal"] == 1
assert "module_id" not in public
def test_public_module_row_keeps_public_name_with_storage_details() -> None:
module = {
"module_id": "Config:register-guid.6#stream:4",
"name": "register-guid.6#stream:4",
}
public = adapter_server.public_module_row(module, include_storage=True, ordinal=2, owner_kind="AccountingRegister")
assert public["kind"] == "register_module"
assert public["name"] == "Модуль регистра 2"
assert public["storage_name"] == "register-guid.6#stream:4"
assert public["module_id"] == "Config:register-guid.6#stream:4"
def test_public_common_module_uses_1c_owner_name_not_storage_route() -> None:
module = {
"module_id": "Config:0422fa60-6cd4-410c-b2af-8792a6a9b199.0#stream:4",
"name": "0422fa60-6cd4-410c-b2af-8792a6a9b199.0#stream:4",
"storage_name": "0422fa60-6cd4-410c-b2af-8792a6a9b199.0#stream:4",
}
public = adapter_server.public_module_with_qualified_name(
module,
owner={"kind": "CommonModule", "name": "ОбщегоНазначенияБЭДПовтИсп"},
include_storage=False,
ordinal=1,
)
assert public["kind"] == "common_module"
assert public["name"] == "ОбщегоНазначенияБЭДПовтИсп"
assert public["qualified_name"] == "ОбщегоНазначенияБЭДПовтИсп"
assert public["display_name"] == "ОбщегоНазначенияБЭДПовтИсп"
assert "storage_name" not in public
assert "module_id" not in public
def test_public_common_module_without_owner_name_does_not_leak_storage_route() -> None:
public = adapter_server.public_module_row(
{
"module_id": "Config:0422fa60-6cd4-410c-b2af-8792a6a9b199.0#stream:4",
"name": "0422fa60-6cd4-410c-b2af-8792a6a9b199.0#stream:4",
},
include_storage=False,
ordinal=1,
owner_kind="CommonModule",
)
assert public["name"] == "Общий модуль"
assert "0422fa60" not in str(public)
def test_public_code_qualified_name_joins_owner_form_and_module() -> None:
assert adapter_server.public_code_qualified_name(
owner={"name": "test2"},
form={"name": "t_Форма"},
module={"name": "Модуль формы"},
) == "test2.t_Форма.Модуль формы"
assert adapter_server.public_code_qualified_name(
owner={"name": "tt_Справочник1"},
module={"name": "Модуль объекта"},
) == "tt_Справочник1.Модуль объекта"
def test_form_embedded_module_public_text_removes_container_tail() -> None:
text = "&НаКлиенте\nПроцедура Команда1(Команда)\nКонецПроцедуры\n\n///----\",\n{4,0,0,0,{#base64:AAAA}}"
public = adapter_server.form_embedded_module_public_text(text)
assert public == "&НаКлиенте\nПроцедура Команда1(Команда)\nКонецПроцедуры"
def test_merge_module_owner_context_marks_direct_form_module_and_qualified_name() -> None:
result = {"module": {"kind": "bsl_container_module", "name": "BSL module"}}
owner_context = {
"owner": {"name": "test2", "kind": "CommonForm"},
"form": {"name": "t_Форма"},
"module": {"module_name": "Модуль формы", "form": "t_Форма"},
}
merged = adapter_server.merge_module_owner_context(result, owner_context)
assert merged["module"]["kind"] == "form_module"
assert merged["module"]["name"] == "Модуль формы"
assert merged["qualified_name"] == "test2.t_Форма.Модуль формы"
def test_merge_module_owner_context_replaces_partial_owner_with_saved_form_context() -> None:
result = {"module": {"kind": "bsl_container_module", "name": "BSL module"}}
owner_context = {
"owner": {"status": "resolved", "kind": "CommonForm", "name": "test2"},
"form": {"name": "t_Форма"},
"module": {"kind": "form_module", "name": "Модуль формы"},
"qualified_name": "test2.t_Форма.Модуль формы",
}
merged = adapter_server.merge_module_owner_context(result, owner_context)
assert merged["owner"]["name"] == "test2"
assert merged["form"]["name"] == "t_Форма"
assert merged["qualified_name"] == "test2.t_Форма.Модуль формы"
def test_saved_state_bsl_module_role_uses_owner_kind_for_register() -> None:
role = adapter_server.saved_state_bsl_module_role("extension-guid__register-guid.0", owner_kind="InformationRegister")
assert role == {"kind": "record_set_module", "name": "Модуль набора записей", "suffix": "0"}
def test_saved_state_module_search_uses_cached_owner_kind_for_register(monkeypatch: pytest.MonkeyPatch) -> None:
module_text = "Процедура ПередЗаписью(Отказ)\r\nКонецПроцедуры\r\n".encode("utf-8-sig")
header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
stored_module = compress_payload(b"prefix" + header + module_text, "raw_deflate")
stored_descriptor = compress_payload('{"РегистрСведений1"}'.encode("utf-8-sig"), "raw_deflate")
module_file_name = "extension-guid__register-guid.0"
descriptor_file_name = "extension-guid__register-guid"
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {"status": "ok", "files": [{"FileName": module_file_name, "PartCount": 1, "Bytes": len(stored_module)}]},
)
monkeypatch.setattr(
adapter_server,
"metadata_cache_lookup_guid",
lambda base_id, guid: {"kind": "InformationRegister", "name": "РегистрСведений1", "guid": guid} if guid == "register-guid" else None,
)
def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
if requested_file_name == descriptor_file_name:
return stored_descriptor, {"database": "upo_test"}, None
return stored_module, {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.metadata_saved_state_modules_search(
{
"base_id": "upo_test",
"tables": ["ConfigCASSave"],
"query": "ПередЗаписью",
"limit": 10,
"include_storage": True,
}
)
module = result["modules"][0]
assert module["owner"]["kind"] == "InformationRegister"
assert module["module"] == {"kind": "record_set_module", "name": "Модуль набора записей", "suffix": "0"}
assert module["qualified_name"] == "РегистрСведений1.Модуль набора записей"
assert module["streams"][0]["module"] == {"kind": "record_set_module", "name": "Модуль набора записей", "suffix": "0"}
assert module["streams"][0]["qualified_name"] == "РегистрСведений1.Модуль набора записей"
def test_saved_state_modules_search_treats_empty_query_as_no_filter(monkeypatch: pytest.MonkeyPatch) -> None:
seen_payloads = []
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {
"status": "ok",
"files": [{"FileName": "extension-guid__object-guid.0", "PartCount": 1, "Bytes": 1}],
},
)
def fake_search_row(**kwargs):
seen_payloads.append(kwargs["payload"])
return {"module": {"kind": "object_module", "name": "Модуль объекта"}, "streams": []}
monkeypatch.setattr(adapter_server, "saved_state_module_search_row", fake_search_row)
result = adapter_server.metadata_saved_state_modules_search(
{"base_id": "upo_test", "tables": ["ConfigCASSave"], "query": "", "limit": 5, "include_storage": True}
)
assert result["status"] == "ok"
assert result["counts"]["modules"] == 1
assert result["query"]["query"] is None
assert seen_payloads[0]["query"] is None
def test_saved_state_modules_search_accepts_semantic_layer(monkeypatch: pytest.MonkeyPatch) -> None:
seen_tables: list[str] = []
def fake_storage_files_list(payload: dict[str, Any]) -> dict[str, Any]:
seen_tables.append(payload["table"])
return {"status": "ok", "files": []}
monkeypatch.setattr(adapter_server, "storage_files_list", fake_storage_files_list)
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_modules_owner_guid_from_selector",
lambda *_args, **_kwargs: ("owner-guid", {"status": "resolved"}),
)
result = adapter_server.metadata_saved_state_modules_search(
{
"base_id": "upo_test",
"layer": "base_saved_state",
"ref": "CommonModule.Интеграция",
"query": "Отправить",
}
)
assert result["status"] == "ok"
assert result["source"] == {"kind": "saved_state"}
assert result["query"]["layer"] == "base_saved_state"
assert seen_tables == ["ConfigSave"]
def test_saved_state_modules_search_rejects_layer_and_tables_together() -> None:
result = adapter_server.metadata_saved_state_modules_search(
{
"base_id": "upo_test",
"layer": "extension_saved_state",
"tables": ["ConfigCASSave"],
}
)
assert result["status"] == "invalid_argument"
assert result["argument"] == "tables"
def test_saved_state_modules_search_scopes_extension_to_cassave_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
seen_file_list_payloads = []
seen_rows = []
monkeypatch.setattr(
adapter_server,
"extension_map_by_guid",
lambda base_id: {extension_guid: {"guid": extension_guid, "name": "test2"}},
)
def fake_storage_files_list(payload):
seen_file_list_payloads.append(payload)
return {
"status": "ok",
"files": [
{"FileName": f"{extension_guid}__object-guid.0", "PartCount": 1, "Bytes": 1},
{"FileName": "base-object-guid.0", "PartCount": 1, "Bytes": 1},
],
}
monkeypatch.setattr(adapter_server, "storage_files_list", fake_storage_files_list)
def fake_search_row(**kwargs):
seen_rows.append(kwargs["file_name"])
return {"module": {"kind": "object_module", "name": "Модуль объекта"}, "streams": []}
monkeypatch.setattr(adapter_server, "saved_state_module_search_row", fake_search_row)
result = adapter_server.metadata_saved_state_modules_search(
{
"base_id": "upo_test",
"tables": ["ConfigCASSave", "ConfigSave"],
"extension": "test2",
"query": "",
"limit": 5,
"include_storage": True,
}
)
assert result["status"] == "ok"
assert result["source"]["tables"] == ["ConfigCASSave"]
assert result["query"]["extension"] == "test2"
assert seen_file_list_payloads == [
{"base_id": "upo_test", "table": "ConfigCASSave", "limit": 1000, "diagnostic": True, "timeout_seconds": 60, "prefix": f"{extension_guid}__"}
]
assert seen_rows == [f"{extension_guid}__object-guid.0"]
def test_saved_state_forms_search_scopes_extension_to_cassave_prefix(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
seen_file_list_payloads = []
seen_rows = []
monkeypatch.setattr(
adapter_server,
"extension_map_by_guid",
lambda base_id: {extension_guid: {"guid": extension_guid, "name": "test2"}},
)
def fake_storage_files_list(payload):
seen_file_list_payloads.append(payload)
return {
"status": "ok",
"files": [
{"FileName": f"{extension_guid}__form-guid.0", "PartCount": 1, "Bytes": 1},
{"FileName": "base-form-guid.0", "PartCount": 1, "Bytes": 1},
],
}
monkeypatch.setattr(adapter_server, "storage_files_list", fake_storage_files_list)
def fake_form_row(**kwargs):
seen_rows.append(kwargs["file_name"])
return {"name": "t_Форма", "form": {"name": "t_Форма"}}
monkeypatch.setattr(adapter_server, "saved_state_form_search_row", fake_form_row)
result = adapter_server.metadata_saved_state_forms_search(
{
"base_id": "upo_test",
"tables": ["ConfigCASSave", "ConfigSave"],
"extension": "test2",
"limit": 5,
"include_storage": True,
}
)
assert result["status"] == "ok"
assert result["source"]["tables"] == ["ConfigCASSave"]
assert result["query"]["extension"] == "test2"
assert seen_file_list_payloads == [
{"base_id": "upo_test", "table": "ConfigCASSave", "limit": 1000, "diagnostic": True, "timeout_seconds": 60, "prefix": f"{extension_guid}__"}
]
assert seen_rows == [f"{extension_guid}__form-guid.0"]
def test_extension_saved_state_common_form_has_public_name_and_state(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_forms_search",
lambda payload: {
"status": "ok",
"forms": [
{
"table": "ConfigCASSave",
"file_name": f"{extension_guid}__form-guid.0",
"form": {
"name": "t_Форма",
"identity": {"name": "t_Форма", "guid": "form-guid", "source": "saved_state_descriptor"},
},
}
],
"counts": {"scanned": 1},
},
)
matches, errors, stats = adapter_server.extension_saved_state_object_matches(
base_id="upo_test",
payload={"extension": "test2"},
query="",
kind_filter="CommonForm",
guid_filter="",
extension_guid=extension_guid,
active_guid_keys=set(),
limit=10,
scan_limit=100,
timeout_seconds=60,
include_storage=False,
)
assert errors == []
assert stats["saved_state_rows"] == 1
assert matches[0]["qualified_name"] == "test2.t_Форма"
assert matches[0]["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
def test_modules_search_marks_extension_saved_only_module(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
saved_file = f"{extension_guid}__caee4437-14b6-436f-98e3-360c4577f20d.0"
class FakeStreamClassifier:
pass
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(adapter_server, "extension_map_by_guid", lambda base_id: {extension_guid: {"name": "test2", "guid": extension_guid}})
monkeypatch.setattr(
adapter_server,
"extension_objects_find",
lambda payload: {"status": "ok", "objects": [{"guid": "77494708-43ea-4956-ac3c-199cfb035ad2"}]},
)
monkeypatch.setattr(
adapter_server,
"extension_definition_guid_sources",
lambda *args, **kwargs: (
{
"77494708-43ea-4956-ac3c-199cfb035ad2": [
{
"extension": {"name": "test2", "guid": extension_guid},
"storage_role": "DataProcessor",
}
]
},
None,
),
)
monkeypatch.setattr(
adapter_server,
"metadata_object_modules",
lambda *args, **kwargs: {"status": "ok", "object": {}, "modules": []},
)
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {"status": "ok", "files": [{"FileName": saved_file}], "counts": {"files": 1}},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({saved_file: b"payload"}, {"database": "upo_test"}, None),
)
monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": ""})
monkeypatch.setattr(adapter_server, "extract_bsl_text_from_container", lambda text, **kwargs: ("", {"status": "not_found"}))
monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
FakeStreamClassifier.classify_payload = staticmethod(
lambda data, include_text=True: {
"role": "bsl_module_payload",
"stream_blocks": [{"text": "Процедура SavedOnlyProbe()\nКонецПроцедуры\n"}],
}
)
result = adapter_server.search_modules({"base_id": "upo_test", "extension": "test2", "query": "SavedOnlyProbe", "scan_limit": 10})
assert result["status"] == "ok"
assert result["matches"][0]["read_selector"]["module_ref"] == f"ConfigCASSave:{saved_file}#stream:0"
assert result["matches"][0]["activation_state"] == "saved_only"
def test_modules_search_marks_extension_saved_override_module(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
saved_file = f"{extension_guid}__77494708-43ea-4956-ac3c-199cfb035ad2.0"
class FakeStreamClassifier:
pass
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(adapter_server, "extension_map_by_guid", lambda base_id: {extension_guid: {"name": "test2", "guid": extension_guid}})
monkeypatch.setattr(
adapter_server,
"extension_objects_find",
lambda payload: {"status": "ok", "objects": [{"guid": "77494708-43ea-4956-ac3c-199cfb035ad2"}]},
)
monkeypatch.setattr(adapter_server, "extension_definition_guid_sources", lambda *args, **kwargs: ({}, None))
monkeypatch.setattr(adapter_server, "metadata_object_modules", lambda *args, **kwargs: {"status": "ok", "object": {}, "modules": []})
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {"status": "ok", "files": [{"FileName": saved_file}], "counts": {"files": 1}},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({saved_file: b"payload"}, {"database": "upo_test"}, None),
)
monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": ""})
monkeypatch.setattr(adapter_server, "extract_bsl_text_from_container", lambda text, **kwargs: ("", {"status": "not_found"}))
monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
FakeStreamClassifier.classify_payload = staticmethod(
lambda data, include_text=True: {
"role": "bsl_module_payload",
"stream_blocks": [{"text": "Процедура SavedOverrideProbe()\nКонецПроцедуры\n"}],
}
)
result = adapter_server.search_modules({"base_id": "upo_test", "extension": "test2", "query": "SavedOverrideProbe", "scan_limit": 10})
assert result["status"] == "ok"
assert result["matches"][0]["activation_state"] == "saved_override"
def test_modules_search_accepts_object_type_name_aliases(monkeypatch: pytest.MonkeyPatch) -> None:
seen_payloads: list[dict[str, Any]] = []
def fake_metadata_object_modules(payload: dict[str, Any]) -> dict[str, Any]:
seen_payloads.append(payload)
return {
"status": "ok",
"object": {
"kind": "DataProcessor",
"name": "ТестоваяОбработка",
"synonym": "Тестовая обработка",
"guid": "22222222-2222-2222-2222-222222222222",
},
"modules": [{"name": "Модуль объекта", "module_id": "ConfigCAS:module-guid#stream:0"}],
}
class FakeStreamClassifier:
pass
monkeypatch.setattr(adapter_server, "metadata_object_modules", fake_metadata_object_modules)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (b"payload", {"database": "upo_test"}, None),
)
monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
FakeStreamClassifier.classify_payload = staticmethod(
lambda data, include_text=True: {
"role": "bsl_module_payload",
"stream_blocks": [{"text": "Процедура ЦелеваяПроцедура()\nКонецПроцедуры\n"}],
}
)
result = adapter_server.call_method(
"modules.search",
{
"base_id": "upo_test",
"object_type": "Обработка",
"object_name": "ТестоваяОбработка",
"query": "ЦелеваяПроцедура",
},
)
assert result["status"] == "ok"
assert seen_payloads[0]["kind"] == "DataProcessor"
assert seen_payloads[0]["name"] == "ТестоваяОбработка"
assert result["query"]["scope"] == "object_modules"
assert result["matches"][0]["owner"]["kind"] == "DataProcessor"
def test_modules_search_includes_saved_object_command_modules(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
object_guid = "a4dfe8f1-2ce8-43c9-8b41-3deb7238c5d5"
command_guid = "a9b2fc38-86b6-4586-8aca-29b92bdd86de"
module_ref = f"ConfigCASSave:{extension_guid}__{command_guid}.2"
monkeypatch.setattr(
adapter_server,
"metadata_object_modules",
lambda payload: {
"status": "ok",
"object": {"kind": "CalculationRegister", "name": "tt_РегистрРасчета1", "guid": object_guid},
"modules": [],
},
)
monkeypatch.setattr(
adapter_server,
"metadata_object_commands",
lambda payload: {
"status": "ok",
"object_commands": [
{
"guid": command_guid,
"name": "Команда1",
"status": "ok",
"read_selector": {
"method": "modules.read",
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"module_ref": module_ref,
},
}
],
},
)
monkeypatch.setattr(
adapter_server,
"read_module",
lambda payload: {
"status": "ok",
"text": "&НаКлиенте\nПроцедура ОбработкаКоманды()\nКонецПроцедуры\n",
"module": {"kind": "command_module", "name": "Модуль команды"},
},
)
result = adapter_server.search_modules(
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"guid": object_guid,
"table": "ConfigCASSave",
"extension_guid": extension_guid,
"query": "ОбработкаКоманды",
"state": "save",
}
)
assert result["status"] == "ok"
assert result["counts"]["command_modules"] == 1
assert result["counts"]["owner_resolved"] == 1
assert result["counts"]["complete"] is True
assert result["matches"][0]["module"]["kind"] == "command_module"
assert result["matches"][0]["module"]["command"] == "Команда1"
assert result["matches"][0]["read_selector"]["module_ref"] == module_ref
assert result["matches"][0]["activation_state"] == "saved_state"
def test_modules_search_direct_call_accepts_public_ref(monkeypatch: pytest.MonkeyPatch) -> None:
seen_payloads: list[dict[str, Any]] = []
def fake_metadata_object_modules(payload: dict[str, Any]) -> dict[str, Any]:
seen_payloads.append(payload)
return {
"status": "ok",
"object": {
"kind": "DataProcessor",
"name": "ТестоваяОбработка",
"guid": "22222222-2222-2222-2222-222222222222",
},
"modules": [],
}
monkeypatch.setattr(adapter_server, "metadata_object_modules", fake_metadata_object_modules)
result = adapter_server.search_modules(
{
"base_id": "upo_test",
"ref": "Обработка.ТестоваяОбработка",
"query": "ЦелеваяПроцедура",
}
)
assert result["status"] == "ok"
assert seen_payloads[0]["kind"] == "DataProcessor"
assert seen_payloads[0]["name"] == "ТестоваяОбработка"
assert result["query"]["scope"] == "object_modules"
def test_modules_search_owner_resolution_scans_all_module_capable_kinds(monkeypatch: pytest.MonkeyPatch) -> None:
module_ref = "ConfigCAS:module-guid#stream:0"
class FakeStreamClassifier:
pass
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {"status": "ok", "files": [{"FileName": "module-guid"}], "counts": {"files": 1}},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({"module-guid": b"payload"}, {"database": "upo_test"}, None),
)
monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": ""})
monkeypatch.setattr(adapter_server, "extract_bsl_text_from_container", lambda text, **kwargs: ("", {"status": "not_found"}))
monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
FakeStreamClassifier.classify_payload = staticmethod(
lambda data, include_text=True: {
"role": "bsl_module_payload",
"stream_blocks": [{"text": "Функция НайтиСтроку()\nВозврат 1;\nКонецФункции\n"}],
}
)
def fake_list_objects(kind: str, **kwargs: Any) -> dict[str, Any]:
if kind != "InformationRegister":
return {"status": "ok", "objects": []}
return {
"status": "ok",
"objects": [
{
"kind": "InformationRegister",
"name": "ТестовыйРегистр",
"synonym": "Тестовый регистр",
"guid": "33333333-3333-3333-3333-333333333333",
}
],
}
def fake_metadata_object_modules(payload: dict[str, Any]) -> dict[str, Any]:
if payload.get("kind") != "InformationRegister":
return {"status": "ok", "object": {}, "modules": []}
return {
"status": "ok",
"object": {
"kind": "InformationRegister",
"name": "ТестовыйРегистр",
"synonym": "Тестовый регистр",
"guid": "33333333-3333-3333-3333-333333333333",
},
"modules": [{"name": "Модуль менеджера", "module_id": module_ref}],
}
monkeypatch.setattr(adapter_server, "list_objects", fake_list_objects)
monkeypatch.setattr(adapter_server, "metadata_object_modules", fake_metadata_object_modules)
result = adapter_server.search_modules(
{
"base_id": "upo_test",
"query": "НайтиСтроку",
"scan_limit": 10,
"resolve_owners": True,
"owner_scan_limit": 40,
"state": "active",
}
)
assert result["status"] == "ok"
assert result["counts"]["owner_resolved"] == 1
assert result["counts"]["owner_indexed_module_refs"] == 1
assert result["diagnostics"]["owner_resolution"]["strategy"] == "live_metadata_scan"
assert result["diagnostics"]["owner_resolution"]["indexed_module_refs"] == 1
match = result["matches"][0]
assert match["owner"]["kind"] == "InformationRegister"
assert match["owner"]["name"] == "ТестовыйРегистр"
assert match["read_selector"]["kind"] == "InformationRegister"
assert match["read_selector"]["name"] == "ТестовыйРегистр"
assert match["read_selector"]["ref"] == "InformationRegister.ТестовыйРегистр"
assert match["read_selector"]["method"] == "modules.read"
assert match["read_selector"]["module_ordinal"] == 1
def test_modules_search_reports_owner_scan_limit_hit(monkeypatch: pytest.MonkeyPatch) -> None:
class FakeStreamClassifier:
pass
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {"status": "ok", "files": [{"FileName": "module-guid"}], "counts": {"files": 1}},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({"module-guid": b"payload"}, {"database": "upo_test"}, None),
)
monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": ""})
monkeypatch.setattr(adapter_server, "extract_bsl_text_from_container", lambda text, **kwargs: ("", {"status": "not_found"}))
monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
FakeStreamClassifier.classify_payload = staticmethod(
lambda data, include_text=True: {
"role": "bsl_module_payload",
"stream_blocks": [{"text": "Процедура НеНайденныйВладелец()\nКонецПроцедуры\n"}],
}
)
monkeypatch.setattr(
adapter_server,
"list_objects",
lambda kind, **kwargs: {
"status": "ok",
"objects": [
{
"kind": kind,
"name": "ПервыйОбъект",
"guid": "44444444-4444-4444-4444-444444444444",
}
],
},
)
monkeypatch.setattr(adapter_server, "metadata_object_modules", lambda payload: {"status": "ok", "object": {}, "modules": []})
result = adapter_server.search_modules(
{
"base_id": "upo_test",
"query": "НеНайденныйВладелец",
"resolve_owners": True,
"owner_scan_limit": 1,
"scan_limit": 10,
}
)
assert result["status"] == "ok"
assert result["counts"]["owner_unresolved"] == 1
assert result["counts"]["owner_scan_limit_hit"] is True
assert result["diagnostics"]["owner_resolution"]["owner_scan_limit_hit"] is True
assert result["diagnostics"]["owner_resolution"]["owner_objects_scanned"] == 1
def test_modules_search_public_call_preserves_read_selector_module_ref(monkeypatch: pytest.MonkeyPatch) -> None:
class FakeStreamClassifier:
pass
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {"status": "ok", "files": [{"FileName": "module-guid"}], "counts": {"files": 1}},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({"module-guid": b"payload"}, {"database": "upo_test"}, None),
)
monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "ok", "text": ""})
monkeypatch.setattr(adapter_server, "extract_bsl_text_from_container", lambda text, **kwargs: ("", {"status": "not_found"}))
monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
FakeStreamClassifier.classify_payload = staticmethod(
lambda data, include_text=True: {
"role": "bsl_module_payload",
"stream_blocks": [{"text": "Процедура ПубличныйСелектор()\nКонецПроцедуры\n"}],
}
)
result = adapter_server.call_method("modules.search", {"base_id": "upo_test", "query": "ПубличныйСелектор", "state": "active"})
assert result["status"] == "ok"
selector = result["matches"][0]["read_selector"]
assert selector["method"] == "modules.read"
assert selector["module_ref"] == "ConfigCAS:module-guid#stream:0"
assert "module_id" not in result["matches"][0]
assert "file_name" not in result["matches"][0]
def test_code_search_exposes_scan_and_owner_counts(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"search_modules",
lambda payload: {
"status": "partial",
"source": {"kind": "live_metadata"},
"matches": [
{
"snippet": {"text": "Запрос = Новый Запрос;", "offset": 0},
"owner": {"status": "unresolved", "kind": None, "name": None},
"origin": {"source": "cas_reference", "status": "owner_unresolved", "write_surface": "requires_owner_resolution"},
"module": {"name": "Модуль БСЛ"},
"read_selector": {"base_id": "upo_test", "module_ref": "ConfigCAS:module-guid#stream:0"},
}
],
"counts": {
"truncated": True,
"complete": False,
"scan_limit_hit": True,
"owner_resolved": 0,
"owner_unresolved": 1,
"owner_scan_limit_hit": True,
"owner_indexed_module_refs": 0,
},
"diagnostics": {"message": "limited"},
},
)
result = adapter_server.code_search({"base_id": "upo_test", "query": "Новый Запрос", "limit": 10})
assert result["status"] == "partial"
assert result["counts"]["truncated"] is True
assert result["counts"]["complete"] is False
assert result["counts"]["scan_limit_hit"] is True
assert result["counts"]["owner_resolved"] == 0
assert result["counts"]["owner_unresolved"] == 1
assert result["counts"]["owner_scan_limit_hit"] is True
assert result["counts"]["owner_indexed_module_refs"] == 0
assert result["items"][0]["read_selector"]["method"] == "code.read"
assert result["items"][0]["read_selector"]["module_ref"] == "ConfigCAS:module-guid#stream:0"
assert result["items"][0]["origin"]["source"] == "cas_reference"
assert result["items"][0]["origin"]["write_surface"] == "requires_owner_resolution"
def test_code_search_uses_effective_working_view_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
seen_payload: dict[str, Any] = {}
def fake_search_modules(payload: dict[str, Any]) -> dict[str, Any]:
seen_payload.update(payload)
return {"status": "not_found", "source": {"kind": "logical"}, "matches": [], "counts": {}, "diagnostics": {}}
monkeypatch.setattr(adapter_server, "search_modules", fake_search_modules)
result = adapter_server.code_search({"base_id": "upo_test", "query": "Тест"})
assert seen_payload["state"] == "working"
assert result["configuration_view"]["name"] == "effective_working"
assert "not claimed to be the currently applied runtime" in result["configuration_view"]["meaning"]
def test_code_search_accepts_runtime_applied_configuration_view(monkeypatch: pytest.MonkeyPatch) -> None:
seen_payload: dict[str, Any] = {}
def fake_search_modules(payload: dict[str, Any]) -> dict[str, Any]:
seen_payload.update(payload)
return {"status": "not_found", "source": {"kind": "logical"}, "matches": [], "counts": {}, "diagnostics": {}}
monkeypatch.setattr(adapter_server, "search_modules", fake_search_modules)
result = adapter_server.code_search(
{"base_id": "upo_test", "query": "Тест", "configuration_view": "runtime_applied"}
)
assert seen_payload["state"] == "active"
assert result["configuration_view"]["name"] == "runtime_applied"
def test_code_read_rejects_conflicting_configuration_view(monkeypatch: pytest.MonkeyPatch) -> None:
result = adapter_server.code_read(
{
"base_id": "upo_test",
"configuration_view": "runtime_applied",
"state": "working",
"module_id": "Config:test.0",
}
)
assert result["status"] == "invalid_argument"
assert result["argument"] == "configuration_view"
def test_code_search_accepts_module_ordinal_without_tuple_response(monkeypatch: pytest.MonkeyPatch) -> None:
seen_payload: dict[str, Any] = {}
def fake_search_modules(payload: dict[str, Any]) -> dict[str, Any]:
seen_payload.update(payload)
return {
"status": "ok",
"source": {"kind": "live_metadata"},
"matches": [
{
"snippet": {"text": "Процедура Проверка() Экспорт", "offset": 0},
"owner": {"status": "resolved", "kind": "Document", "name": "ТестовыйДокумент"},
"module": {"name": "Модуль объекта", "module_ordinal": 1},
"read_selector": {
"base_id": "upo_test",
"method": "modules.read",
"kind": "Document",
"name": "ТестовыйДокумент",
"module_ordinal": 1,
},
}
],
"counts": {"matches": 1, "complete": True, "scan_limit_hit": False},
"diagnostics": {},
}
monkeypatch.setattr(adapter_server, "search_modules", fake_search_modules)
result = adapter_server.code_search(
{
"base_id": "upo_test",
"kind": "Document",
"name": "ТестовыйДокумент",
"query": "Процедура",
"module_ordinal": 1,
}
)
assert isinstance(result, dict)
assert result["schema"] == "onec_code_search.v1"
assert result["status"] == "ok"
assert seen_payload["module_ordinal"] == 1
assert result["items"][0]["read_selector"]["method"] == "code.read"
def test_code_search_reads_saved_state_common_form_without_storage(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_modules_search",
lambda payload: {
"schema": "onec_saved_state_module_search.v1",
"status": "ok",
"source": {"kind": "live_sql", "tables": ["ConfigCASSave"]},
"modules": [
{
"table": "ConfigCASSave",
"file_name": "form.0",
"streams": [
{
"module_ref": "ConfigCASSave:form.0",
"module_path": "2",
"preview": "\r\n&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\n\t// Отредактировано адаптером\r\nКонецПроцедуры\r\n",
}
],
}
],
"counts": {"modules": 1},
},
)
result = adapter_server.code_search(
{
"base_id": "upo_test",
"object_type": "CommonForm",
"object_name": "t_Форма",
"query": "Отредактировано адаптером",
}
)
assert result["status"] == "ok"
assert result["source"]["kind"] == "saved_state"
assert result["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
assert result["items"][0]["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
assert result["items"][0]["read_selector"] == {
"method": "code.read",
"base_id": "upo_test",
"object_type": "CommonForm",
"object_name": "t_Форма",
"include_text": True,
"max_chars": 100000,
}
assert "storage" not in result["items"][0]
def test_code_search_hides_form_embedded_module_trailing_marker(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_modules_search",
lambda payload: {
"schema": "onec_saved_state_module_search.v1",
"status": "ok",
"source": {"kind": "live_sql", "tables": ["ConfigCASSave"]},
"modules": [
{
"table": "ConfigCASSave",
"file_name": "form.0",
"streams": [
{
"module_ref": "ConfigCASSave:form.0",
"module_path": "2",
"preview": "Процедура Проверка()\nКонецПроцедуры\n\n///----",
}
],
}
],
"counts": {"modules": 1},
},
)
result = adapter_server.code_search(
{
"base_id": "upo_test",
"object_type": "CommonForm",
"object_name": "t_Форма",
"query": "///----",
}
)
assert result["status"] == "not_found"
assert result["items"] == []
def test_code_read_hides_form_embedded_module_trailing_marker(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_modules_search",
lambda payload: {
"schema": "onec_saved_state_module_search.v1",
"status": "ok",
"modules": [{"streams": [{"preview": "&НаКлиенте\nПроцедура Проверка()\nКонецПроцедуры\n\n///----"}]}],
},
)
result = adapter_server.code_read(
{
"base_id": "upo_test",
"object_type": "CommonForm",
"object_name": "t_Форма",
"state": "working",
}
)
assert result["status"] == "ok"
assert "///----" not in result["text"]
assert result["text"].endswith("КонецПроцедуры")
def test_code_search_state_both_merges_saved_common_form_and_active(monkeypatch: pytest.MonkeyPatch) -> None:
seen_active_payload: dict[str, Any] = {}
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_modules_search",
lambda payload: {
"schema": "onec_saved_state_module_search.v1",
"status": "ok",
"source": {"kind": "live_sql", "tables": ["ConfigCASSave"]},
"modules": [
{
"table": "ConfigCASSave",
"file_name": "form.0",
"streams": [
{
"module_ref": "ConfigCASSave:form.0",
"module_path": "2",
"preview": "Процедура ЗаменаДомена(Команда)\n\t// saved\nКонецПроцедуры\n",
}
],
}
],
"counts": {"modules": 1},
},
)
def fake_search_modules(payload: dict[str, Any]) -> dict[str, Any]:
seen_active_payload.update(payload)
return {
"status": "ok",
"source": {"kind": "live_metadata"},
"matches": [
{
"snippet": {"text": "// active", "offset": 0},
"owner": {"status": "resolved", "kind": "CommonForm", "name": "t_Форма"},
"origin": {"source": "active"},
"module": {"name": "Модуль формы"},
"read_selector": {"base_id": "upo_test", "method": "modules.read", "kind": "CommonForm", "name": "t_Форма"},
}
],
"counts": {"complete": True, "scan_limit_hit": False},
}
monkeypatch.setattr(adapter_server, "search_modules", fake_search_modules)
result = adapter_server.code_search(
{
"base_id": "upo_test",
"object_type": "CommonForm",
"object_name": "t_Форма",
"query": "ЗаменаДомена",
"state": "both",
}
)
assert seen_active_payload["state"] == "active"
assert result["status"] == "ok"
assert result["current_state"] == {"source": "both", "activation_state": "mixed"}
assert result["counts"]["saved_matches"] == 1
assert result["counts"]["active_matches"] == 1
assert result["comparison"]["both_present"] is True
assert result["items"][0]["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
assert result["items"][1]["current_state"] == {"source": "active", "activation_state": "active"}
def test_code_read_reads_saved_state_common_form_routine(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_modules_search",
lambda payload: {
"schema": "onec_saved_state_module_search.v1",
"status": "ok",
"modules": [
{
"table": "ConfigCASSave",
"file_name": "form.0",
"streams": [
{
"module_ref": "ConfigCASSave:form.0",
"module_path": "2",
"preview": "\r\n&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\n\t// Отредактировано адаптером\r\nКонецПроцедуры\r\n",
}
],
}
],
},
)
result = adapter_server.code_read(
{
"base_id": "upo_test",
"object_type": "CommonForm",
"object_name": "t_Форма",
"routine_name": "ЗаменаДомена",
"include_line_numbers": True,
}
)
assert result["schema"] == "onec_code_read.v1"
assert result["status"] == "ok"
assert result["source"]["kind"] == "saved_state"
assert result["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
assert "Отредактировано адаптером" in result["text"]
assert result["line"] == 3
def test_code_read_state_active_skips_saved_state_common_form(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_modules_search",
lambda payload: (_ for _ in ()).throw(AssertionError("state=active must not read saved-state")),
)
monkeypatch.setattr(
adapter_server,
"read_module",
lambda payload: {"status": "ok", "text": "active text"},
)
result = adapter_server.code_read(
{
"base_id": "upo_test",
"object_type": "CommonForm",
"object_name": "t_Форма",
"state": "active",
}
)
assert result["status"] == "ok"
assert result["current_state"] == {"source": "active", "activation_state": "active"}
assert result["text"] == "active text"
def test_code_read_state_active_not_found_keeps_active_state(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_modules_search",
lambda payload: (_ for _ in ()).throw(AssertionError("state=active must not read saved-state")),
)
monkeypatch.setattr(
adapter_server,
"read_module",
lambda payload: {"schema": "onec_metadata_object.v1", "method": "modules.read", "status": "not_found", "error": "not_found"},
)
result = adapter_server.code_read(
{
"base_id": "upo_test",
"object_type": "CommonForm",
"object_name": "t_Форма",
"state": "active",
}
)
assert result["status"] == "not_found"
assert result["method"] == "code.read"
assert result["current_state"] == {"source": "active", "activation_state": "active"}
def test_code_read_state_save_does_not_fallback_to_active(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_modules_search",
lambda payload: {"schema": "onec_saved_state_module_search.v1", "status": "ok", "modules": []},
)
monkeypatch.setattr(
adapter_server,
"read_module",
lambda payload: (_ for _ in ()).throw(AssertionError("state=save must not read active fallback")),
)
result = adapter_server.code_read(
{
"base_id": "upo_test",
"object_type": "CommonForm",
"object_name": "t_Форма",
"state": "save",
}
)
assert result["status"] == "not_found"
assert result["error"] == "saved_state_code_not_found"
assert result["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
def test_code_read_state_both_returns_saved_and_active_layers(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_modules_search",
lambda payload: {
"schema": "onec_saved_state_module_search.v1",
"status": "ok",
"modules": [
{
"streams": [
{
"preview": "\r\nПроцедура ЗаменаДомена(Команда)\r\n\t// saved\r\nКонецПроцедуры\r\n",
}
],
}
],
},
)
def fake_read_module(payload: dict[str, object]) -> dict[str, object]:
assert payload["state"] == "active"
return {
"schema": "onec_code_read.v1",
"method": "code.read",
"status": "ok",
"text": "Процедура ЗаменаДомена(Команда)\n\t// active\nКонецПроцедуры\n",
"current_state": {"source": "active", "activation_state": "active"},
}
monkeypatch.setattr(adapter_server, "read_module", fake_read_module)
result = adapter_server.code_read(
{
"base_id": "upo_test",
"object_type": "CommonForm",
"object_name": "t_Форма",
"routine_name": "ЗаменаДомена",
"state": "both",
}
)
assert result["status"] == "ok"
assert result["current_state"] == {"source": "both", "activation_state": "mixed"}
assert result["comparison"]["both_present"] is True
assert result["comparison"]["differs"] is True
assert result["layers"][0]["source"] == "saved_state"
assert result["layers"][0]["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
assert result["layers"][1]["source"] == "active"
assert result["layers"][1]["current_state"] == {"source": "active", "activation_state": "active"}
assert result["text_source"] == "saved_state"
def test_code_read_state_both_treats_saved_module_ref_as_saved_layer(monkeypatch: pytest.MonkeyPatch) -> None:
seen_payloads: list[dict[str, Any]] = []
def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
seen_payloads.append(dict(payload))
assert payload["module_ref"] == "ConfigCASSave:object__module.0#stream:4"
return {
"schema": "onec_module_read.v1",
"status": "ok",
"text": "Процедура ИзSave()\nКонецПроцедуры\n",
"origin": {"source": "saved_state"},
}
monkeypatch.setattr(adapter_server, "read_module", fake_read_module)
result = adapter_server.code_read(
{
"base_id": "upo_test",
"module_ref": "ConfigCASSave:object__module.0#stream:4",
"state": "both",
}
)
assert len(seen_payloads) == 1
assert seen_payloads[0]["state"] == "save"
assert result["status"] == "ok"
assert result["layers"][0]["source"] == "saved_state"
assert result["layers"][0]["status"] == "ok"
assert result["layers"][1]["source"] == "active"
assert result["layers"][1]["status"] == "not_found"
assert result["layers"][1]["error"] == "active_selector_required"
assert result["text_source"] == "saved_state"
def test_code_read_state_both_removes_saved_module_ref_for_active_layer(monkeypatch: pytest.MonkeyPatch) -> None:
seen_payloads: list[dict[str, Any]] = []
def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
seen_payloads.append(dict(payload))
if payload.get("state") == "save":
return {"schema": "onec_module_read.v1", "status": "ok", "text": "Процедура ИзSave()\nКонецПроцедуры\n"}
assert payload["state"] == "active"
assert "module_ref" not in payload
assert "module_id" not in payload
assert payload["kind"] == "CommonModule"
assert payload["name"] == "ТестовыйМодуль"
return {"schema": "onec_module_read.v1", "status": "ok", "text": "Процедура ИзActive()\nКонецПроцедуры\n"}
monkeypatch.setattr(adapter_server, "read_module", fake_read_module)
result = adapter_server.code_read(
{
"base_id": "upo_test",
"module_ref": "ConfigCASSave:object__module.0#stream:4",
"object_type": "CommonModule",
"object_name": "ТестовыйМодуль",
"state": "both",
}
)
assert len(seen_payloads) == 2
assert result["comparison"]["both_present"] is True
assert result["comparison"]["differs"] is True
assert result["layers"][0]["text"].startswith("Процедура ИзSave")
assert result["layers"][1]["text"].startswith("Процедура ИзActive")
def test_code_search_preserves_public_ref_in_read_selector(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"search_modules",
lambda payload: {
"status": "ok",
"source": {"kind": "live_metadata"},
"matches": [
{
"snippet": {"text": "Команда();", "offset": 0},
"owner": {"status": "resolved", "kind": "DataProcessor", "name": "ТестоваяОбработка"},
"module": {"name": "Модуль объекта", "module_ordinal": 1},
"read_selector": {
"base_id": "upo_test",
"method": "modules.read",
"kind": "DataProcessor",
"name": "ТестоваяОбработка",
"ref": "DataProcessor.ТестоваяОбработка",
"module_ordinal": 1,
},
}
],
"counts": {"matches": 1, "complete": True, "scan_limit_hit": False},
"diagnostics": {},
},
)
result = adapter_server.code_search({"base_id": "upo_test", "query": "Команда", "limit": 10})
selector = result["items"][0]["read_selector"]
assert selector["method"] == "code.read"
assert selector["ref"] == "DataProcessor.ТестоваяОбработка"
assert selector["name"] == "ТестоваяОбработка"
assert selector["module_ordinal"] == 1
def test_code_search_accepts_public_ref_selector(monkeypatch: pytest.MonkeyPatch) -> None:
seen_payloads: list[dict[str, Any]] = []
def fake_search_modules(payload: dict[str, Any]) -> dict[str, Any]:
seen_payloads.append(payload)
return {
"status": "ok",
"source": {"kind": "live_metadata"},
"matches": [],
"counts": {"matches": 0, "complete": True, "scan_limit_hit": False},
}
monkeypatch.setattr(adapter_server, "search_modules", fake_search_modules)
result = adapter_server.call_method(
"code.search",
{"base_id": "upo_test", "query": "ЦелеваяПроцедура", "ref": "Обработка.ТестоваяОбработка"},
)
assert result["status"] == "not_found"
assert seen_payloads[0]["kind"] == "DataProcessor"
assert seen_payloads[0]["name"] == "ТестоваяОбработка"
def test_code_search_direct_call_accepts_public_ref_selector(monkeypatch: pytest.MonkeyPatch) -> None:
seen_payloads: list[dict[str, Any]] = []
def fake_search_modules(payload: dict[str, Any]) -> dict[str, Any]:
seen_payloads.append(payload)
return {
"status": "ok",
"source": {"kind": "live_metadata"},
"matches": [],
"counts": {"matches": 0, "complete": True, "scan_limit_hit": False},
}
monkeypatch.setattr(adapter_server, "search_modules", fake_search_modules)
result = adapter_server.code_search(
{"base_id": "upo_test", "query": "ЦелеваяПроцедура", "ref": "Обработка.ТестовыйОбъект"}
)
assert result["status"] == "not_found"
assert seen_payloads[0]["kind"] == "DataProcessor"
assert seen_payloads[0]["name"] == "ТестовыйОбъект"
def test_code_read_direct_call_accepts_public_ref_selector(monkeypatch: pytest.MonkeyPatch) -> None:
seen_payloads: list[dict[str, Any]] = []
def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
seen_payloads.append(payload)
return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры\n"}
monkeypatch.setattr(adapter_server, "read_module", fake_read_module)
result = adapter_server.code_read({"base_id": "upo_test", "ref": "Обработка.ТестовыйОбъект"})
assert result["status"] == "ok"
assert seen_payloads[0]["kind"] == "DataProcessor"
assert seen_payloads[0]["name"] == "ТестовыйОбъект"
def test_attributes_job_stops_after_card_not_found(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[str] = []
def fake_call_method_impl(method: str, payload: dict) -> dict:
calls.append(method)
return {
"schema": "onec_adapter_request_error.v1",
"method": method,
"status": "not_found",
"error": "not_found",
"diagnostics": {"message": "object was not found"},
"matches": [],
}
job_id = "test-attributes-not-found"
adapter_server.ADAPTER_JOBS[job_id] = {"status": "running", "created_at": adapter_server.adapter_now(), "updated_at": adapter_server.adapter_now()}
monkeypatch.setattr(adapter_server, "call_method_impl", fake_call_method_impl)
adapter_server.adapter_run_metadata_object_attributes_job(
job_id,
{"base_id": "upo_test", "kind": "Document", "name": "НеСуществующийДокумент", "only": "all"},
240,
)
job = adapter_server.ADAPTER_JOBS[job_id]
assert calls == ["metadata.object.get"]
assert job["status"] == "done"
assert job["result"]["status"] == "not_found"
assert job["result"]["sections"]["card"] == "failed"
assert job["result"]["sections"]["semantic"] == "not_started_due_to_card_failure"
assert job["progress"]["current_step"] == "done"
adapter_server.ADAPTER_JOBS.pop(job_id, None)
def test_adapter_job_start_returns_not_found_without_job_for_missing_object(monkeypatch: pytest.MonkeyPatch) -> None:
before_jobs = set(adapter_server.ADAPTER_JOBS)
def fake_call_method_impl(method: str, payload: dict) -> dict:
assert method == "metadata.object.get"
return {
"schema": "onec_metadata_object.v1",
"method": "metadata.object.get",
"status": "not_found",
"error": "not_found",
"object": None,
"matches": [],
"diagnostics": {"message": "object was not found"},
}
monkeypatch.setattr(adapter_server, "call_method_impl", fake_call_method_impl)
result = adapter_server.adapter_start_job(
{
"method": "metadata.object.attributes",
"payload": {"base_id": "upo_test", "kind": "Document", "name": "НеСуществующийДокумент", "only": "all"},
}
)
assert result["status"] == "not_found"
assert result["sections"]["card"] == "failed"
assert result["sections"]["semantic"] == "not_started_due_to_card_failure"
assert "job_id" not in result
assert set(adapter_server.ADAPTER_JOBS) == before_jobs
def test_attributes_cancel_marks_running_sections_cancelled() -> None:
now = adapter_server.adapter_now()
job_id = "test-attributes-cancel"
adapter_server.ADAPTER_JOBS[job_id] = {
"schema": "onec_adapter_job.v1",
"status": "running",
"method": "metadata.object.attributes",
"created_at": now,
"started_at": now,
"updated_at": now,
"partial_result": {
"schema": "onec_metadata_object_attributes.v1",
"status": "partial",
"sections": {
"card": "ok",
"semantic": "running",
"type_resolution": "pending",
"build_result": "pending",
},
"section_timings": {
"semantic": {"method": "metadata.object.attributes", "status": "running", "started_at": now},
},
"diagnostics": [],
},
}
result = adapter_server.adapter_cancel_job({"job_id": job_id})
assert result["status"] == "cancelled"
assert result["current_step"] == "cancelled"
assert result["diagnostics"]["termination"] == "hard_process"
assert result["partial_result"]["sections"]["semantic"] == "cancelled"
assert result["partial_result"]["sections"]["type_resolution"] == "cancelled"
assert result["progress"]["running_steps"] == []
adapter_server.ADAPTER_JOBS.pop(job_id, None)
def test_adapter_jobs_persist_in_local_sqlite_and_drop_credentials(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
state_db = tmp_path / "adapter-state.sqlite"
legacy_json = tmp_path / "adapter-jobs.json"
monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(state_db))
monkeypatch.setenv("ONEC_ADAPTER_JOB_STORE", str(legacy_json))
original_jobs = dict(adapter_server.ADAPTER_JOBS)
original_loaded = adapter_server.ADAPTER_JOB_STORE_LOADED
now = adapter_server.adapter_now()
try:
adapter_server.ADAPTER_JOBS.clear()
adapter_server.ADAPTER_JOB_STORE_LOADED = True
adapter_server.ADAPTER_JOBS.update(
{
"queued-job": {
"schema": "onec_adapter_job.v1",
"status": "queued",
"job_id": "queued-job",
"method": "metadata.object.full",
"created_at": now,
"updated_at": now,
},
"done-job": {
"schema": "onec_adapter_job.v1",
"status": "done",
"job_id": "done-job",
"method": "metadata.object.get",
"created_at": now,
"updated_at": now,
"result": {"status": "ok", "password": "must-not-be-stored", "password_state": "set"},
},
}
)
adapter_server.adapter_save_jobs_to_store()
adapter_server.ADAPTER_JOBS.clear()
adapter_server.ADAPTER_JOB_STORE_LOADED = False
adapter_server.adapter_load_jobs_from_store()
assert state_db.is_file()
assert not legacy_json.exists()
assert adapter_server.ADAPTER_JOBS["queued-job"]["status"] == "error"
assert adapter_server.ADAPTER_JOBS["queued-job"]["error"] == "adapter_restarted"
assert adapter_server.ADAPTER_JOBS["done-job"]["result"]["status"] == "ok"
assert adapter_server.ADAPTER_JOBS["done-job"]["result"]["password_state"] == "set"
assert "password" not in adapter_server.ADAPTER_JOBS["done-job"]["result"]
with sqlite3.connect(state_db) as conn:
assert conn.execute("SELECT COUNT(*) FROM adapter_jobs").fetchone()[0] == 2
finally:
adapter_server.ADAPTER_JOBS.clear()
adapter_server.ADAPTER_JOBS.update(original_jobs)
adapter_server.ADAPTER_JOB_STORE_LOADED = original_loaded
def test_adapter_background_job_runs_in_killable_process(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(tmp_path / "adapter-state.sqlite"))
monkeypatch.setenv("ONEC_ADAPTER_JOB_PROCESS_ISOLATION", "true")
monkeypatch.setenv("ONEC_ADAPTER_JOB_TIMEOUT_SECONDS", "20")
accepted = adapter_server.adapter_start_job({"method": "help.methods", "payload": {}, "timeout_seconds": 20})
assert accepted["status"] == "accepted"
assert accepted["timeout_seconds"] == 20
job_id = accepted["job_id"]
deadline = time.time() + 20
job: dict[str, Any] = {}
try:
while time.time() < deadline:
with adapter_server.ADAPTER_JOB_LOCK:
job = dict(adapter_server.ADAPTER_JOBS.get(job_id) or {})
if job.get("status") in {"done", "error", "cancelled"}:
break
time.sleep(0.05)
assert job["status"] == "done"
assert job["worker"]["mode"] == "process"
assert int(job["worker"]["pid"]) > 0
assert job["result"]["schema"] == "onec_adapter_methods.v1"
assert job["result"]["count"] == len(adapter_server.METHODS)
finally:
with adapter_server.ADAPTER_JOB_LOCK:
adapter_server.ADAPTER_JOBS.pop(job_id, None)
adapter_server.adapter_save_jobs_to_store()
def test_adapter_job_process_timeout_terminates_child(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(tmp_path / "adapter-state.sqlite"))
job_id = "isolated-timeout"
now = adapter_server.adapter_now()
adapter_server.ADAPTER_JOBS[job_id] = {
"schema": "onec_adapter_job.v1",
"status": "running",
"job_id": job_id,
"method": "help.methods",
"created_at": now,
"started_at": now,
"updated_at": now,
}
try:
adapter_server.adapter_run_job_in_process(job_id, "help.methods", {}, 0.001)
job = adapter_server.ADAPTER_JOBS[job_id]
assert job["status"] == "error"
assert job["error"] == "job_timeout"
assert "terminated" in job["diagnostics"]["message"]
finally:
adapter_server.ADAPTER_JOBS.pop(job_id, None)
def test_attributes_cache_requires_explicit_use_cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "cache.sqlite"))
config = {"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}
object_guid = "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
cached_payload = {
"schema": "onec_metadata_object_attributes.v1",
"status": "ok",
"base_id": "upo_test",
"query": {"kind": "Document", "name": "АвансовыйОтчет", "only": "attributes"},
"object": {"guid": object_guid, "kind": "Document", "name": "АвансовыйОтчет"},
"dimensions": [],
"resources": [],
"attributes": [{"name": "СтарыйКеш"}],
"tabular_sections": [],
"counts": {"attributes": 1},
}
adapter_server.metadata_guid_index_upsert(
config,
{
"guid": object_guid,
"guid_role": adapter_server.metadata_attributes_cache_role("attributes"),
"kind": "Document",
"kind_ru": "Документ",
"name": "АвансовыйОтчет",
"payload": cached_payload,
},
)
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (config, None))
monkeypatch.setattr(
adapter_server,
"get_object",
lambda *args, **kwargs: {
"status": "ok",
"object": {"guid": object_guid, "kind": "Document", "kind_ru": "Документ", "name": "АвансовыйОтчет"},
"semantic": {"sections": []},
},
)
monkeypatch.setattr(adapter_server, "resolve_type_guids", lambda *args, **kwargs: {})
monkeypatch.setattr(adapter_server, "extension_map_by_guid", lambda base_id: {})
cached_result = adapter_server.metadata_object_attributes(
{"base_id": "upo_test", "kind": "Document", "name": "АвансовыйОтчет", "only": "attributes", "use_cache": True}
)
live_result = adapter_server.metadata_object_attributes(
{"base_id": "upo_test", "kind": "Document", "name": "АвансовыйОтчет", "only": "attributes"}
)
assert cached_result["attributes"] == [{"name": "СтарыйКеш"}]
assert cached_result["cache"]["status"] == "hit"
assert live_result["status"] == "ok"
assert live_result["attributes"] == []
assert live_result["cache"]["status"] == "stored"
def test_payload_lossless_round_trip_envelopes() -> None:
payload = '{"Имя","Синоним ""с кавычками""",{1,2,3}}'.encode("utf-8-sig")
for compression in ("none", "raw_deflate", "zlib", "gzip"):
stored = compress_payload(payload, compression)
decoded = decode_payload_lossless(stored)
encoded = encode_payload_lossless(decoded)
restored, restored_compression = try_decompress(encoded)
assert restored_compression == compression
assert restored == payload
def test_noop_policy_can_keep_original_bytes() -> None:
payload = '{"Имя","Синоним"}'.encode("utf-8-sig")
stored = compress_payload(payload, "raw_deflate")
decoded = decode_payload_lossless(stored)
assert stored == stored
assert encode_payload_lossless(decoded) != b""
def test_brace_tree_canonical_serialize_and_encode() -> None:
payload = '{"Имя","Синоним ""с кавычками""",{1,2,3}}'.encode("utf-8-sig")
stored = compress_payload(payload, "raw_deflate")
decoded = decode_payload_lossless(stored)
tree = parse_brace_text(decoded["text"] or "")
assert serialize_brace_tree(tree) == '{"Имя","Синоним ""с кавычками""",{1,2,3}}'
encoded = encode_brace_tree(tree, decoded)
restored, compression = try_decompress(encoded)
assert compression == "raw_deflate"
assert restored.decode("utf-8-sig") == '{"Имя","Синоним ""с кавычками""",{1,2,3}}'
def test_set_tree_path_preserves_existing_scalar_kind() -> None:
tree = parse_brace_text('{"Имя",{1,"Старый заголовок"}}')
changed = set_tree_path(tree, "1.1", "Новый заголовок")
changed = set_tree_path(changed, "1.0", 2)
assert get_tree_path(changed, "1.1")["type"] == "string"
assert get_tree_path(changed, "1.0")["type"] == "atom"
assert serialize_brace_tree(changed) == '{"Имя",{2,"Новый заголовок"}}'
def test_patch_brace_text_path_preserves_surrounding_format() -> None:
text = '\ufeff {"Имя", {1, "Старый ""заголовок""", 3}}\n'
patched, info = patch_brace_text_path(text, "1.1", 'Новый "заголовок"')
assert patched == '\ufeff {"Имя", {1, "Новый ""заголовок""", 3}}\n'
assert info["old"] == 'Старый "заголовок"'
assert info["new"] == 'Новый "заголовок"'
assert info["old_node_type"] == "string"
assert info["new_node_type"] == "string"
def test_patch_brace_text_path_preserves_atom_kind() -> None:
patched, info = patch_brace_text_path('{"Имя",{1,2,3}}', "1.1", 42)
assert patched == '{"Имя",{1,42,3}}'
assert info["old"] == "2"
assert info["new"] == "42"
assert info["old_node_type"] == "atom"
def test_append_brace_text_child_preserves_surrounding_text() -> None:
patched, info = append_brace_text_child('{"A",{0,2,{"Y",1},{"Z",2}},"B"}', "1", parse_brace_text('{"X",3}'))
assert patched == '{"A",{0,3,{"Y",1},{"Z",2},{"X",3}},"B"}'
assert info["parent_path"] == "1"
assert info["inserted_index"] == 4
assert info["count_updated"] is True
assert info["count_before"] == 2
assert info["count_after"] == 3
def test_append_brace_text_child_keeps_non_declared_container_countless() -> None:
patched, info = append_brace_text_child('{"A",{1,4},"B"}', "1", parse_brace_text('{"X",3}'))
assert patched == '{"A",{1,4,{"X",3}},"B"}'
assert info["count_updated"] is False
def test_decode_config_semantic_from_upo_sample() -> None:
sample = ROOT / "reports" / "1c-sql" / "upo" / "Config-samples" / "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
decoded = decode_payload_lossless(sample.read_bytes())
tree = parse_brace_text(decoded["text"] or "")
identity = find_identity(tree)
semantic = decode_config_semantic(tree, kind="Document")
assert identity is not None
assert identity.guid == "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
assert semantic["identity"]["name"] == "АвансовыйОтчет"
assert {section["category"] for section in semantic["sections"]} == {"TabularSection", "Attribute"}
assert semantic["counts"]["generic_record_containers"] > 0
tabular_section = next(section for section in semantic["sections"] if section["category"] == "TabularSection")
assert any(record.get("columns") for record in tabular_section["records"])
first_column = next(column for record in tabular_section["records"] for column in record.get("columns", []) if column.get("type"))
assert first_column["type"]["kind"] in {"string", "number", "boolean", "date", "reference"}
attribute_section = next(section for section in semantic["sections"] if section["category"] == "Attribute")
first_typed_attribute = next(record for record in attribute_section["records"] if record.get("type"))
assert first_typed_attribute["type"]["kind"] in {"string", "number", "boolean", "date", "reference"}
assert attribute_section["records"][0]["identity"]["guid"] == "977014d5-3fac-4bdf-9d20-81b437053042"
assert attribute_section["records"][0]["likely_name"] == "Автор"
def test_decode_config_semantic_can_skip_generic_tree_scan() -> None:
sample = ROOT / "reports" / "1c-sql" / "upo" / "Config-samples" / "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
decoded = decode_payload_lossless(sample.read_bytes())
tree = parse_brace_text(decoded["text"] or "")
semantic = decode_config_semantic(tree, kind="Document", include_generic=False)
assert semantic["sections"]
assert semantic["generic_sections"] == []
assert semantic["generic_record_containers"] == []
assert semantic["counts"]["generic_record_containers"] == 0
def test_decode_config_semantic_can_limit_section_categories() -> None:
sample = ROOT / "reports" / "1c-sql" / "upo" / "Config-samples" / "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
decoded = decode_payload_lossless(sample.read_bytes())
tree = parse_brace_text(decoded["text"] or "")
semantic = decode_config_semantic(tree, kind="Document", include_generic=False, categories={"Attribute"})
assert {section["category"] for section in semantic["sections"]} == {"Attribute"}
assert [rule["category"] for rule in semantic["section_rules"]] == ["Attribute"]
def test_decode_config_semantic_lightweight_keeps_public_fields() -> None:
sample = ROOT / "reports" / "1c-sql" / "upo" / "Config-samples" / "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
decoded = decode_payload_lossless(sample.read_bytes())
tree = parse_brace_text(decoded["text"] or "")
semantic = decode_config_semantic(
tree,
kind="Document",
include_generic=False,
categories={"Attribute"},
lightweight=True,
)
attribute_section = semantic["sections"][0]
first_attribute = attribute_section["records"][0]
assert first_attribute["identity"]["name"] == "Автор"
assert first_attribute["type"]
assert "strings_sample" not in first_attribute
assert "guids_sample" not in first_attribute
def test_public_storage_trace_scrubber_hides_sql_coordinates() -> None:
payload = {
"status": "source_missing",
"source": {"kind": "live_sql", "database": "upo_test", "table": "Config", "file_name": "abc"},
"object": {"name": "Документ", "storage_routes": [{"file_name": "abc"}]},
"modules": [
{
"name": "Config:abc#stream:0",
"module_id": "Config:abc#stream:0",
"source": {"kind": "live_sql", "database": "upo_test"},
"payload": {"payload_bytes": 10, "sha1": "1"},
}
],
}
public = strip_storage_traces(payload)
text = str(public)
assert public["source"] == {"kind": "live_metadata"}
assert "Документ" in text
for marker in ("live_sql", "database", "file_name", "module_id", "payload_bytes", "sha1", "storage_routes"):
assert marker not in text
def test_public_module_and_form_rows_hide_internal_handles() -> None:
module = public_module_row(
{
"module_id": "Config:abc#stream:0",
"source": {"kind": "live_sql", "database": "upo_test"},
"file_name": "abc",
"stream_index": 0,
"payload_role": "bsl_module_payload",
"sha1": "1",
"text_preview": "Процедура Команда()\nКонецПроцедуры",
},
ordinal=2,
)
form = public_form_row(
{
"name": "ФормаДокумента",
"source": {"kind": "live_sql", "database": "upo_test"},
"parts": [{"part_id": "abc.0", "source": {"kind": "live_sql"}, "suffix": ".0", "sha1": "1", "role": "form_payload"}],
}
)
assert module["name"] == "Модуль объекта 2"
assert "module_id" not in module
assert "source" not in module
assert "file_name" not in module
assert form["parts"] == [{"role": "form_payload"}]
def test_classify_stream_payload_with_bsl_marker() -> None:
module_text = "Процедура Команда1(Команда)\r\nКонецПроцедуры\r\n".encode("utf-8-sig")
header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
stored = compress_payload(b"prefix" + header + module_text, "raw_deflate")
classified = classify_payload(stored)
assert classified["role"] == "bsl_module_payload"
assert classified["counts"]["stream_blocks"] == 1
assert classified["stream_blocks"][0]["has_bsl_marker"] is True
def test_replace_stream_block_updates_header_and_text() -> None:
module_text_raw = "Процедура Команда1(Команда)\r\nКонецПроцедуры\r\n"
module_text = module_text_raw.encode("utf-8-sig")
header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
payload = b"prefix" + header + module_text + b"tail"
expected_text_sha1 = adapter_server.code_text_sha1(module_text_raw)
changed_payload, edit = replace_stream_block(
payload,
0,
replace={"old": "Команда1", "new": "Команда2"},
expected_contains="Процедура Команда1",
expected_text_sha1=expected_text_sha1,
)
stored = compress_payload(changed_payload, "raw_deflate")
classified = classify_payload(stored, include_text=True)
assert edit["old_sha1"] != edit["new_sha1"]
assert edit["old_text_sha1"] == expected_text_sha1
assert edit["new_text_sha1"] == adapter_server.code_text_sha1(classified["stream_blocks"][0]["text"])
assert classified["role"] == "bsl_module_payload"
assert "Процедура Команда2" in classified["stream_blocks"][0]["text"]
with pytest.raises(ValueError, match="expected_text_sha1"):
replace_stream_block(
payload,
0,
text=module_text_raw,
expected_text_sha1="0" * 40,
)
def test_replace_stream_block_routine_edit() -> None:
module_text = (
"Процедура Команда1(Команда)\r\n"
"\tСообщить(\"old\");\r\n"
"КонецПроцедуры\r\n"
).encode("utf-8-sig")
header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
payload = b"prefix" + header + module_text
new_routine = "Процедура Команда1(Команда)\r\n\tСообщить(\"new\");\r\nКонецПроцедуры\r\n"
changed_payload, edit = replace_stream_block(payload, 0, routine={"operation": "replace", "name": "Команда1", "text": new_routine})
stored = compress_payload(changed_payload, "raw_deflate")
classified = classify_payload(stored, include_text=True)
text = classified["stream_blocks"][0]["text"]
assert edit["routine"]["status"] == "replaced"
assert 'Сообщить("new")' in text
assert 'Сообщить("old")' not in text
assert validate_bsl_text(text)["status"] == "ok"
def test_replace_stream_block_routine_preconditions() -> None:
module_text_raw = (
"Процедура Команда1(Команда)\r\n"
"\tСообщить(\"old\");\r\n"
"КонецПроцедуры\r\n"
)
module_text = module_text_raw.encode("utf-8-sig")
header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
payload = b"prefix" + header + module_text
new_routine = "Процедура Команда1(Команда)\r\n\tСообщить(\"new\");\r\nКонецПроцедуры\r\n"
changed_payload, edit = replace_stream_block(
payload,
0,
routine={
"operation": "replace",
"name": "Команда1",
"text": new_routine,
"expected_old_contains": 'Сообщить("old")',
"expected_old_sha1": text_sha1(module_text_raw.rstrip("\r\n")),
},
)
classified = classify_payload(compress_payload(changed_payload, "raw_deflate"), include_text=True)
assert edit["routine"]["old_sha1"] == text_sha1(module_text_raw.rstrip("\r\n"))
assert 'Сообщить("new")' in classified["stream_blocks"][0]["text"]
with pytest.raises(ValueError, match="expected_old_contains"):
replace_stream_block(
payload,
0,
routine={
"operation": "replace",
"name": "Команда1",
"text": new_routine,
"expected_old_contains": "missing old marker",
},
)
with pytest.raises(ValueError, match="expected_old_sha1"):
replace_stream_block(
payload,
0,
routine={
"operation": "replace",
"name": "Команда1",
"text": new_routine,
"expected_old_sha1": "0" * 40,
},
)
def test_validate_bsl_text_routine_balance() -> None:
ok = validate_bsl_text("Процедура Команда1()\nКонецПроцедуры\n")
async_ok = validate_bsl_text("Асинх Процедура Команда1()\nКонецПроцедуры\n")
broken = validate_bsl_text("Процедура Команда1()\n")
assert ok["status"] == "ok"
assert async_ok["status"] == "ok"
assert async_ok["routines_sample"][0]["name"] == "Команда1"
assert broken["status"] == "error"
assert broken["issues"][0]["code"] == "routine_balance"
def test_routine_blocks_ignore_comment_mentions() -> None:
text = (
"// Процедура отмены / проведения у подчиненных счет-фактур.\n"
"Процедура ОбработкаПодчиненныхСчетФактур()\n"
"\tСообщить(\"ok\");\n"
"КонецПроцедуры\n"
"\n"
"// Процедура формирует счет-фактуры для покупателей.\n"
"Процедура СформироватьСчетаФактуры()\n"
"КонецПроцедуры\n"
)
routines = routine_blocks(text)
assert [routine["name"] for routine in routines] == [
"ОбработкаПодчиненныхСчетФактур",
"СформироватьСчетаФактуры",
]
def test_module_text_response_reports_routine_match_mode() -> None:
text = "Процедура ОбработкаПодчиненныхСчетФактур()\nКонецПроцедуры\n"
exact = module_text_response(text, {"routine_name": "ОбработкаПодчиненныхСчетФактур", "max_chars": 100})
case_insensitive = module_text_response(text, {"routine_name": "обработкаподчиненныхсчетфактур", "max_chars": 100})
assert exact["selection"]["match_by"] == "routine_exact"
assert case_insensitive["selection"]["match_by"] == "routine_case_insensitive"
def test_modules_read_falls_back_to_single_bsl_stream_in_saved_command_payload(monkeypatch: pytest.MonkeyPatch) -> None:
class FakeStreamClassifier:
pass
bsl_text = "&НаКлиенте\nПроцедура ОбработкаКоманды()\nКонецПроцедуры\n"
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(adapter_server, "metadata_form_owner_cache_lookup", lambda *args, **kwargs: None)
monkeypatch.setattr(adapter_server, "cached_module_owner_payload", lambda *args, **kwargs: None)
monkeypatch.setattr(adapter_server, "extension_module_owner_payload", lambda *args, **kwargs: None)
monkeypatch.setattr(adapter_server, "saved_state_public_module_context", lambda **kwargs: None)
monkeypatch.setattr(adapter_server, "saved_state_module_owner_identity", lambda **kwargs: None)
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (b"compressed", {"database": "upo_test"}, None))
monkeypatch.setattr(adapter_server, "payload_text_from_bytes", lambda data: {"status": "undecodable", "text": ""})
monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
FakeStreamClassifier.classify_payload = staticmethod(
lambda data, include_text=True: {
"role": "bsl_module_payload",
"stream_blocks": [
{"text": "descriptor", "has_bsl_marker": False},
{"text": bsl_text, "has_bsl_marker": True},
],
}
)
result = adapter_server.read_module(
{
"base_id": "upo_test",
"kind": "CalculationRegister",
"module_ref": "ConfigCASSave:extension__command.2",
}
)
assert result["status"] == "ok"
assert result["text"] == bsl_text
assert result["module"]["kind"] == "command_module"
def test_modules_read_returns_owner_from_object_selector(monkeypatch: pytest.MonkeyPatch) -> None:
module_ref = "ConfigCAS:module-file"
monkeypatch.setattr(
adapter_server,
"metadata_object_modules",
lambda payload: {
"status": "ok",
"object": {
"kind": "Document",
"name": "ЗаказПокупателя",
"synonym": "Заказ покупателя",
"guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
},
"modules": [{"module_id": module_ref}],
},
)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (b"payload", {"database": "upo_test"}, None),
)
monkeypatch.setattr(
adapter_server,
"payload_text_from_bytes",
lambda data: {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры\n"},
)
result = adapter_server.read_module(
{
"base_id": "upo_test",
"kind": "Document",
"name": "ЗаказПокупателя",
"module_ordinal": 1,
"summary": True,
}
)
assert result["status"] == "ok"
assert result["owner"]["status"] == "resolved"
assert result["owner"]["kind"] == "Document"
assert result["owner"]["name"] == "ЗаказПокупателя"
assert result["module"]["module_ordinal"] == 1
def test_modules_read_direct_call_accepts_public_ref_selector(monkeypatch: pytest.MonkeyPatch) -> None:
module_ref = "ConfigCAS:module-file"
seen_payloads: list[dict[str, Any]] = []
def fake_metadata_object_modules(payload: dict[str, Any]) -> dict[str, Any]:
seen_payloads.append(payload)
return {
"status": "ok",
"object": {
"kind": "DataProcessor",
"name": "ТестовыйОбъект",
"synonym": "Тестовый объект",
"guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
},
"modules": [{"module_id": module_ref}],
}
monkeypatch.setattr(adapter_server, "metadata_object_modules", fake_metadata_object_modules)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (b"payload", {"database": "upo_test"}, None),
)
monkeypatch.setattr(
adapter_server,
"payload_text_from_bytes",
lambda data: {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры\n"},
)
result = adapter_server.read_module(
{
"base_id": "upo_test",
"ref": "Обработка.ТестовыйОбъект",
"module_ordinal": 1,
"summary": True,
}
)
assert result["status"] == "ok"
assert seen_payloads[0]["kind"] == "DataProcessor"
assert seen_payloads[0]["name"] == "ТестовыйОбъект"
assert result["owner"]["kind"] == "DataProcessor"
assert result["owner"]["name"] == "ТестовыйОбъект"
def test_modules_read_returns_owner_from_module_owner_cache(monkeypatch: pytest.MonkeyPatch) -> None:
module_ref = "ConfigCAS:extension-module#stream:0"
monkeypatch.setattr(
adapter_server,
"sql_config_for_base",
lambda base_id: ({"server": "sql-host", "database": "upo_test", "user": "sa", "password": "secret"}, None),
)
monkeypatch.setattr(
adapter_server,
"metadata_module_owner_cache_lookup",
lambda config, ref: {
"owner": {
"kind": "Catalog",
"name": "фс_НоменклатураРасширение",
"synonym": "Номенклатура расширение",
"guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
},
"module_payload": {"module_name": "Модуль объекта", "module_ordinal": 1, "stream_index": 0},
}
if ref == module_ref
else None,
)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (b"payload", {"database": "upo_test"}, None),
)
class FakeStreamClassifier:
pass
monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
FakeStreamClassifier.classify_payload = staticmethod(
lambda data, include_text=True: {
"role": "configcas",
"stream_blocks": [{"text": "Процедура Команда()\nКонецПроцедуры\n"}],
}
)
result = adapter_server.read_module({"base_id": "upo_test", "module_ref": module_ref, "summary": True})
assert result["status"] == "ok"
assert result["owner"]["source"] == "metadata.module_owner_cache"
assert result["owner"]["name"] == "фс_НоменклатураРасширение"
assert result["module"]["name"] == "Модуль объекта"
assert result["module"]["module_ordinal"] == 1
assert result["module"]["stream_index"] == 0
def test_modules_search_form_payload_returns_container_bsl_offset(monkeypatch: pytest.MonkeyPatch) -> None:
form_file = "form-payload-file"
container_prefix = "{4,{0},\""
form_text = (
container_prefix
+ "\r\n&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\n\t// body\r\nКонецПроцедуры\r\n"
+ "\"}"
)
expected_offset = form_text.index("&НаКлиенте")
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {"status": "ok", "files": [{"FileName": form_file}], "counts": {"files": 1}},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({form_file: b"payload"}, {"database": "upo_test"}, None),
)
monkeypatch.setattr(
adapter_server,
"payload_text_from_bytes",
lambda data: {"status": "ok", "text": form_text},
)
result = adapter_server.search_modules(
{
"base_id": "upo_test",
"query": "ЗаменаДомена",
"scope": "configcas",
"scan_limit": 10,
"include_storage": False,
"state": "active",
}
)
assert result["status"] == "ok"
match = result["matches"][0]
assert match["read_selector"]["module_ref"] == f"ConfigCAS:{form_file}"
assert match["read_selector"]["bsl_offset"] == expected_offset
assert match["extraction"]["container_offset"] == expected_offset
def test_modules_read_form_module_ref_uses_container_bsl_offset(monkeypatch: pytest.MonkeyPatch) -> None:
form_file = "form-payload-file"
form_text = (
"{4,{0},\""
"\r\n&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\n\t// body\r\nКонецПроцедуры\r\n"
"\"}"
)
bsl_offset = form_text.index("&НаКлиенте")
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (b"payload", {"database": "upo_test"}, None),
)
monkeypatch.setattr(
adapter_server,
"payload_text_from_bytes",
lambda data: {"status": "ok", "text": form_text},
)
result = adapter_server.read_module(
{
"base_id": "upo_test",
"module_ref": f"ConfigCAS:{form_file}#form_module",
"bsl_offset": bsl_offset,
"routine_name": "ЗаменаДомена",
"include_line_numbers": True,
}
)
assert result["status"] == "ok"
assert result["selection"]["routine_name"] == "ЗаменаДомена"
assert result["text"].startswith("Процедура ЗаменаДомена(Команда)")
assert "// body" in result["text"]
assert result["extraction"]["bsl_offset"] == bsl_offset
def test_modules_read_skips_extension_owner_scan_for_saved_state_stream_ref(monkeypatch: pytest.MonkeyPatch) -> None:
module_ref = "ConfigCASSave:object__module.0#stream:4"
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(
adapter_server,
"extension_definition_guid_sources",
lambda *args, **kwargs: pytest.fail("saved-state module refs must not scan extension definitions"),
)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (b"payload", {"database": "upo_test"}, None),
)
class FakeStreamClassifier:
pass
streams = [{"text": ""} for _ in range(5)]
streams[4] = {"text": "Процедура Команда()\nКонецПроцедуры\n"}
monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
FakeStreamClassifier.classify_payload = staticmethod(
lambda data, include_text=True: {
"role": "bsl_module_payload",
"stream_blocks": streams,
}
)
result = adapter_server.read_module({"base_id": "upo_test", "module_ref": module_ref, "summary": True})
assert result["status"] == "ok"
assert result["owner"]["source"] == "direct_saved_state_module_ref"
assert result["origin"]["source"] == "saved_state"
assert result["module"]["stream_index"] == 4
def test_modules_read_resolves_extension_owner_for_configcas_stream_ref(monkeypatch: pytest.MonkeyPatch) -> None:
module_ref = "ConfigCAS:12d98fe77f46c4310958c30f7fe87ef8abaf3d32#stream:4"
owner_guid = "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
extension_guid = "11111111-2222-3333-4444-555555555555"
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: (None, None))
monkeypatch.setattr(
adapter_server,
"extension_definition_guid_sources",
lambda base_id, *, timeout_seconds=60: (
{
owner_guid: [
{
"extension": {"guid": extension_guid, "name": "фс_ДоработкиОбщее", "active": True},
"storage_role": "Catalog",
"sql_number": 12,
}
]
},
None,
),
)
monkeypatch.setattr(
adapter_server,
"metadata_object_modules",
lambda payload: {
"status": "ok",
"object": {
"kind": "Catalog",
"name": "РасширенныйСправочник",
"synonym": "Расширенный справочник",
"guid": owner_guid,
},
"modules": [{"module_id": module_ref}],
},
)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (b"payload", {"database": "upo_test"}, None),
)
class FakeStreamClassifier:
pass
streams = [{"text": ""} for _ in range(5)]
streams[4] = {"text": "Процедура Команда()\nКонецПроцедуры\n"}
monkeypatch.setitem(sys.modules, "parser.cas_payload", FakeStreamClassifier())
FakeStreamClassifier.classify_payload = staticmethod(
lambda data, include_text=True: {
"role": "configcas",
"stream_blocks": streams,
}
)
result = adapter_server.read_module({"base_id": "upo_test", "module_ref": module_ref, "summary": True})
assert result["status"] == "ok"
assert result["owner"]["source"] == "extension_definition_guid_sources"
assert result["owner"]["name"] == "РасширенныйСправочник"
assert result["origin"]["source"] == "extension"
assert result["origin"]["extension"]["name"] == "фс_ДоработкиОбщее"
assert result["module"]["module_ordinal"] == 1
assert result["module"]["stream_index"] == 4
def test_mcp_modules_read_does_not_resolve_owner_itself(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_mcp,
"metadata_cache_lookup_identity",
lambda *args, **kwargs: pytest.fail("MCP must not resolve modules.read owner itself"),
)
result = adapter_mcp.enrich_modules_read_result(
{"base_id": "upo_test", "guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"},
{"status": "ok", "module": {"name": "Модуль объекта"}},
)
assert "owner" not in result
assert "owner_display" not in result
def test_mcp_onec_request_schema_stays_generic() -> None:
onec_request = next(tool for tool in adapter_mcp.TOOLS if tool["name"] == "onec_request")
schema = onec_request["inputSchema"]
assert schema["required"] == ["method"]
assert "oneOf" not in schema
assert schema["properties"]["method"]["type"] == "string"
assert schema["properties"]["payload"]["additionalProperties"] is True
def test_mcp_onec_request_examples_are_generic_and_selector_oriented() -> None:
onec_request = next(tool for tool in adapter_mcp.TOOLS if tool["name"] == "onec_request")
schema = onec_request["inputSchema"]
examples_json = json.dumps(schema.get("examples") or [], ensure_ascii=False)
for concrete_value in mcp_adapter_contract.FORBIDDEN_CONCRETE_SELECTOR_VALUES:
assert concrete_value not in examples_json
assert "" in examples_json
assert "" in examples_json
assert "." in examples_json
assert "extension.objects.find" in examples_json
assert "" in examples_json
assert "" in examples_json
assert "" in examples_json
assert "owner_scan_limit" in examples_json
assert '"source_state": "working"' in examples_json
assert "code.write" in examples_json
assert "metadata.write" in examples_json
assert "metadata.form.command_button.write" in examples_json
assert "metadata.form.command_button.verify" in examples_json
assert "ОбщаяФорма.." in examples_json
assert "handler_routine_text" in examples_json
assert "include_handler" in examples_json
assert "routine_text" in examples_json
assert "read_selector.method" in onec_request["description"]
assert "diagnostics.owner_resolution" in onec_request["description"]
assert "source_state=working" in onec_request["description"]
assert "prefer high-level metadata.write" in onec_request["description"]
assert "metadata.form.command_button.write" in onec_request["description"]
assert "metadata.form.command_button.verify" in schema["properties"]["method"]["description"]
assert "code.write" in onec_request["description"]
assert "ref, kind/name/guid, or object_type/object_name/object_guid" in onec_request["description"]
examples = {
example["method"]: example["payload"]
for example in schema.get("examples") or []
if isinstance(example, dict) and isinstance(example.get("payload"), dict)
}
prepare = examples["metadata.saved_state.prepare"]
assert prepare["layer"] == "extension_saved_state"
assert "ref" in prepare
assert not {"target_table", "table", "file_name"} & prepare.keys()
diff = examples["metadata.saved_state.diff"]
assert "ref" in diff
assert "module_ordinal" in diff
assert not {"target_table", "table", "file_name", "module_ref"} & diff.keys()
status = examples["metadata.saved_state.status"]
assert status["layer"] == "extension_saved_state"
assert not {"target_table", "table"} & status.keys()
changes = examples["metadata.saved_state.changes.list"]
assert changes["layer"] == "extension_saved_state"
payload_description = schema["properties"]["payload"]["description"]
assert "ref" in payload_description
assert "kind/name/guid" in payload_description
assert "object_type/object_name/object_guid" in payload_description
def test_mcp_source_state_maps_to_adapter_state_for_working_methods() -> None:
working = adapter_mcp.apply_freshness_request_policy(
{"base_id": "upo_test", "source_mode": "designer", "source_state": "working"},
"modules.search",
)
applied = adapter_mcp.apply_freshness_request_policy(
{"base_id": "upo_test", "source_state": "applied"},
"code.search",
)
both = adapter_mcp.apply_freshness_request_policy(
{"base_id": "upo_test", "source_state": "all"},
"extension.objects.find",
)
explicit = adapter_mcp.apply_freshness_request_policy(
{"base_id": "upo_test", "source_state": "all", "state": "save"},
"metadata.resolve_overrides",
)
assert working["state"] == "working"
assert applied["state"] == "active"
assert both["state"] == "both"
assert explicit["state"] == "save"
def test_mcp_code_write_uses_working_saved_state_without_agent_sql_gates() -> None:
transformed = adapter_mcp.apply_freshness_request_policy(
{
"base_id": "upo_test",
"object_type": "CommonForm",
"object_name": "t_Форма",
"routine_name": "ЗаменаДомена",
"routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры",
},
"code.write",
)
assert transformed["source_mode"] == "designer"
assert transformed["source_state"] == "working"
assert transformed["cache_policy"] == "none"
assert "allow_sql_saved_state_apply" not in transformed
assert "allow_saved_state_write" not in transformed
def test_mcp_blocks_metadata_write_bsl_and_suggests_code_write() -> None:
result = adapter_mcp.run_or_enqueue_adapter_method(
"metadata.write",
{
"base_id": "upo_test",
"target": {"kind": "module", "object_type": "CommonForm", "object_name": "t_Форма"},
"routine_name": "ЗаменаДомена",
"routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры",
"mode": "apply",
},
)
assert result["status"] == "blocked"
assert result["reason"] == "use_code_write_for_bsl"
suggested = result["diagnostics"]["suggested_request"]
assert suggested["method"] == "code.write"
assert suggested["payload"]["object_type"] == "CommonForm"
assert suggested["payload"]["object_name"] == "t_Форма"
assert suggested["payload"]["routine_name"] == "ЗаменаДомена"
assert suggested["payload"]["mode"] == "apply"
assert "allow_sql_saved_state_apply" not in suggested["payload"]
assert "allow_saved_state_write" not in suggested["payload"]
def test_mcp_source_does_not_advertise_extension_filter_for_metadata_objects_list() -> None:
source = Path(adapter_mcp.__file__).read_text(encoding="utf-8")
start = source.index('"method": {"const": "metadata.objects.list"}')
end = source.index('"method": {"const": "metadata.object.full"}', start)
block = source[start:end]
assert '"extension"' not in block
assert '"include_extensions"' not in block
def test_mcp_schema_template_selector_properties_stay_complete() -> None:
assert set(adapter_mcp.OBJECT_SELECTOR_SCHEMA_PROPERTIES) == set(adapter_server.OBJECT_SELECTOR_ARGUMENTS)
source = Path(adapter_mcp.__file__).read_text(encoding="utf-8")
assert '"required": ["base_id", "object_type", "object_name"]' not in source
def test_mcp_adapter_contract_script_checks_selector_guidance() -> None:
report = mcp_adapter_contract.check_contract()
assert report["passed"] is True
assert report["checks"]["generic_onec_request_schema"] is True
assert report["checks"]["adapter_help_selector_guidance"] is True
assert report["checks"]["adapter_selector_argument_type_validation"] is True
assert report["checks"]["adapter_selector_normalizer_type_validation"] is True
assert report["checks"]["adapter_selector_required_message"] is True
assert report["checks"]["adapter_selector_presence_aliases"] is True
assert report["checks"]["adapter_selector_capability_descriptor"] is True
assert report["checks"]["adapter_definition_read_selector_public_ref"] is True
assert report["checks"]["adapter_module_read_selector_public_ref"] is True
assert report["checks"]["agent_facing_object_methods_name_first"] is True
assert report["checks"]["mcp_selector_schema_template"] is True
assert report["checks"]["mcp_tool_selector_guidance"] is True
assert report["checks"]["selector_chain_working_state"] is True
assert report["checks"]["no_concrete_selector_values_in_production"] is True
assert report["production_selector_files"] > 0
assert not report["issues"]
def test_mcp_adapter_contract_scans_production_selector_files() -> None:
scanned = {path.relative_to(ROOT).as_posix() for path in mcp_adapter_contract.iter_production_selector_contract_files()}
assert "plugins/1c/connector/adapter_1c_server.py" in scanned
assert "plugins/1c/mcp/adapter_1c_mcp.py" in scanned
assert "docs/1c-adapter-api-contract.md" in scanned
assert not any("__pycache__" in path for path in scanned)
def test_mcp_adapter_contract_maps_selector_schema_issues_to_check() -> None:
checks = mcp_adapter_contract.contract_checks(
[{"code": "mcp_schema_requires_object_type_name_selector"}],
onec_request_present=True,
)
assert checks["mcp_selector_schema_template"] is False
def test_mcp_adapter_contract_selector_issue_codes_flip_a_check() -> None:
issue_codes = [
"mcp_tool_selector_guidance_missing",
"mcp_examples_public_ref_placeholder_missing",
"adapter_help_selector_guidance_missing",
"adapter_selector_argument_type_not_validated",
"adapter_selector_normalizer_type_not_validated",
"adapter_selector_required_message_incomplete",
"adapter_selector_message_constant_incomplete",
"adapter_selector_presence_alias_mismatch",
"adapter_selector_capability_descriptor_mismatch",
"adapter_help_selector_capabilities_missing",
"adapter_definition_read_selector_public_ref_missing",
"adapter_module_read_selector_public_ref_missing",
"agent_facing_object_method_not_name_first",
"adapter_contract_version_missing",
"mcp_contract_version_mismatch",
"adapter_help_contract_version_missing",
"mcp_selector_schema_properties_mismatch",
"mcp_schema_requires_object_type_name_selector",
"selector_chain_working_state_missing",
"production_concrete_selector_value_present",
]
for code in issue_codes:
checks = mcp_adapter_contract.contract_checks([{"code": code}], onec_request_present=True)
assert any(value is False for value in checks.values()), code
def test_mcp_selector_chain_smoke_uses_generic_placeholders() -> None:
report = mcp_selector_chain_smoke.build_report()
assert report["passed"] is True
assert report["schema"] == "onec_mcp_selector_chain_smoke.v1"
assert report["counts"]["chains"] >= 3
assert report["counts"]["steps"] >= 6
examples_json = json.dumps(report["chains"], ensure_ascii=False)
assert "" in examples_json
assert "." in examples_json
assert "" in examples_json
code_search_steps = [
step
for chain in report["chains"]
for step in chain.get("steps") or []
if isinstance(step, dict) and step.get("method") == "code.search"
]
assert code_search_steps
assert all((step.get("payload") or {}).get("source_state") == "working" for step in code_search_steps)
for concrete_value in mcp_selector_chain_smoke.FORBIDDEN_CONCRETE_SELECTOR_VALUES:
assert concrete_value not in examples_json
def test_mcp_selector_chain_selects_name_first_form_write_plan_candidate() -> None:
candidate = mcp_selector_chain_smoke.first_public_form_write_plan_candidate(
{
"forms": [
{
"selector": {
"extension": "test2",
"ref": "Catalog.test2",
"form": "t_Форма",
},
"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 candidate == {
"target": {
"kind": "form",
"extension": "test2",
"ref": "Catalog.test2",
"form": "t_Форма",
"command": "ЗаменаДомена",
},
"edit": {
"property": "Заголовок",
"value": "Замена домена [NAME-FIRST PLAN]",
},
}
assert not any(key in candidate["target"] for key in ("table", "file_name", "guid", "form_guid"))
def test_mcp_selector_chain_form_composition_strict_check() -> None:
missing = mcp_selector_chain_smoke.live_form_write_plan_composition_required_issue(
{
"form_write_plan_composition": {
"attempted": True,
"status": "skipped_no_public_form_target",
"composed": False,
"name_first": False,
}
}
)
ready = mcp_selector_chain_smoke.live_form_write_plan_composition_required_issue(
{
"form_write_plan_composition": {
"attempted": True,
"status": "planned",
"composed": True,
"name_first": True,
}
}
)
assert missing["code"] == "live_form_write_plan_composition_required"
assert ready is None
def test_mcp_selector_chain_smoke_rejects_concrete_selector_values() -> None:
issues = mcp_selector_chain_smoke.validate_chain(
{
"name": "bad_chain",
"steps": [
{
"tool": "onec_request",
"method": "metadata.definition.find",
"payload": {"base_id": "", "query": "ПечатьЭтикеток"},
}
],
}
)
assert any(issue["code"] == "chain_contains_concrete_selector_value" for issue in issues)
def test_mcp_selector_chain_smoke_rejects_working_analysis_without_source_state() -> None:
issues = mcp_selector_chain_smoke.validate_chain(
{
"name": "stale_code_search",
"steps": [
{
"tool": "onec_request",
"method": "code.search",
"payload": {"base_id": "base_for_test", "query": "Процедура"},
}
],
}
)
assert any(issue["code"] == "chain_step_working_state_missing" for issue in issues)
def selector_chain_help_methods_response() -> dict[str, Any]:
methods = []
for method in mcp_selector_chain_smoke.LIVE_HELP_SELECTOR_METHODS:
capabilities = {
"accepts_ref": True,
"accepts_object_aliases": True,
"requires_query": method in {"metadata.definition.find", "code.search"},
}
if method == "metadata.definition.find":
capabilities["allows_global_areas"] = True
if method in {"modules.read", "code.read"}:
capabilities["accepts_module_ref"] = True
methods.append({"name": method, "selector_capabilities": capabilities})
return {"status": "ok", "contract_version": mcp_selector_chain_smoke.EXPECTED_CONTRACT_VERSION, "methods": methods}
def test_mcp_selector_chain_live_report_uses_discovered_ref(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, dict[str, Any]]] = []
def fake_rpc_call(adapter_url: str, method: str, payload: dict[str, Any], *, timeout: float, transport: str = "rest", session_id: str | None = None) -> dict[str, Any]:
calls.append((method, payload))
if method == "help.methods":
return selector_chain_help_methods_response()
if method == "metadata.objects.list":
if payload.get("kind") == "DataProcessor":
return {
"status": "ok",
"objects": [
{
"kind": "DataProcessor",
"name": "DiscoveredObject",
"guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
}
],
}
return {"status": "ok", "objects": []}
if method == "metadata.object.modules":
return {"status": "ok", "modules": [{"module_ordinal": 1, "name": "ObjectModule"}]}
if method == "metadata.definition.find":
assert payload["query"] == "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"
return {
"status": "ok",
"related_selectors": {
"modules": {
"base_id": payload["base_id"],
"method": "metadata.object.modules",
"kind": "DataProcessor",
"name": "DiscoveredObject",
"ref": "DataProcessor.DiscoveredObject",
}
},
}
if method == "modules.read":
return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
if method == "code.search":
return {
"status": "ok",
"items": [
{
"read_selector": {
"method": "code.read",
"base_id": payload["base_id"],
"ref": payload["ref"],
"module_ordinal": payload["module_ordinal"],
}
}
],
}
if method == "code.read":
return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
return {"status": "error", "error": method}
monkeypatch.setattr(mcp_selector_chain_smoke, "rpc_call", fake_rpc_call)
report = mcp_selector_chain_smoke.build_live_report(
"http://adapter.example",
"base_for_test",
timeout=1,
kinds=("DataProcessor",),
)
assert report["passed"] is True
assert report["object"]["ref"] == "DataProcessor.DiscoveredObject"
assert any(method == "modules.read" and payload["ref"] == "DataProcessor.DiscoveredObject" for method, payload in calls)
assert any(method == "code.search" and payload["query"] == "Команда" and payload["state"] == "working" for method, payload in calls)
assert any(method == "code.read" and payload["ref"] == "DataProcessor.DiscoveredObject" for method, payload in calls)
def test_mcp_selector_chain_live_report_uses_canonical_definition_object(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_rpc_call(adapter_url: str, method: str, payload: dict[str, Any], *, timeout: float, transport: str = "rest", session_id: str | None = None) -> dict[str, Any]:
if method == "help.methods":
return selector_chain_help_methods_response()
if method == "metadata.objects.list":
return {
"status": "ok",
"objects": [
{
"kind": "Document",
"name": "RawDisplayName",
"guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
}
],
}
if method == "metadata.object.modules":
return {"status": "ok", "modules": [{"module_ordinal": 1}]}
if method == "metadata.object.get":
return {
"status": "ok",
"object": {
"kind": "Document",
"name": "CanonicalName",
"guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
},
}
if method == "metadata.definition.find":
return {
"status": "ok",
"object": {
"kind": "Document",
"name": "CanonicalName",
"guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
},
"related_selectors": {
"modules": {
"base_id": payload["base_id"],
"method": "metadata.object.modules",
"kind": "Document",
"name": "CanonicalName",
"guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
"ref": "Document.CanonicalName",
}
},
}
if method == "modules.read":
return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
if method == "code.search":
return {"status": "ok", "items": [{"read_selector": {"method": "code.read", "base_id": payload["base_id"], "ref": payload["ref"]}}]}
if method == "code.read":
return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
return {"status": "error", "error": method}
monkeypatch.setattr(mcp_selector_chain_smoke, "rpc_call", fake_rpc_call)
report = mcp_selector_chain_smoke.build_live_report("http://adapter.example", "base_for_test", timeout=1, kinds=("Document",))
assert report["passed"] is True
assert report["object"]["name"] == "CanonicalName"
assert report["object"]["ref"] == "Document.CanonicalName"
def test_mcp_selector_chain_live_report_accepts_explicit_object_selector(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, dict[str, Any]]] = []
selector_guid = "00000000-0000-4000-8000-000000000001"
def fake_rpc_call(adapter_url: str, method: str, payload: dict[str, Any], *, timeout: float, transport: str = "rest", session_id: str | None = None) -> dict[str, Any]:
calls.append((method, payload))
if method == "help.methods":
return selector_chain_help_methods_response()
if method == "metadata.object.modules":
assert payload["guid"] == selector_guid
return {"status": "ok", "modules": [{"module_ordinal": 1}], "object": {"kind": "Document", "name": "SelectedObject", "guid": payload["guid"]}}
if method == "metadata.object.get":
return {"status": "ok", "object": {"kind": "Document", "name": "SelectedObject", "guid": payload["guid"]}}
if method == "metadata.definition.find":
return {
"status": "ok",
"object": {"kind": "Document", "name": "SelectedObject", "guid": payload["guid"]},
"related_selectors": {
"modules": {
"base_id": payload["base_id"],
"method": "metadata.object.modules",
"kind": "Document",
"name": "SelectedObject",
"guid": payload["guid"],
"ref": "Document.SelectedObject",
}
},
}
if method == "modules.read":
return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
if method == "code.search":
return {"status": "ok", "items": [{"read_selector": {"method": "code.read", "base_id": payload["base_id"], "ref": payload["ref"]}}]}
if method == "code.read":
return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
return {"status": "error", "error": method}
monkeypatch.setattr(mcp_selector_chain_smoke, "rpc_call", fake_rpc_call)
report = mcp_selector_chain_smoke.build_live_report(
"http://adapter.example",
"base_for_test",
timeout=1,
kinds=("Catalog",),
object_selector={"kind": "Document", "guid": selector_guid},
)
assert report["passed"] is True
assert report["object"]["ref"] == "Document.SelectedObject"
assert not any(method == "metadata.objects.list" for method, _ in calls)
def test_mcp_selector_chain_live_report_requires_related_ref(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_rpc_call(adapter_url: str, method: str, payload: dict[str, Any], *, timeout: float, transport: str = "rest", session_id: str | None = None) -> dict[str, Any]:
if method == "help.methods":
return selector_chain_help_methods_response()
if method == "metadata.objects.list":
return {"status": "ok", "objects": [{"kind": "DataProcessor", "name": "DiscoveredObject"}]}
if method == "metadata.object.modules":
return {"status": "ok", "modules": [{"module_ordinal": 1}]}
if method == "metadata.definition.find":
assert payload["query"] == "DataProcessor.DiscoveredObject"
return {
"status": "ok",
"related_selectors": {
"modules": {
"base_id": payload["base_id"],
"method": "metadata.object.modules",
"kind": "DataProcessor",
"name": "DiscoveredObject",
}
},
}
if method == "modules.read":
return {"status": "ok", "text": ""}
return {"status": "error"}
monkeypatch.setattr(mcp_selector_chain_smoke, "rpc_call", fake_rpc_call)
report = mcp_selector_chain_smoke.build_live_report("http://adapter.example", "base_for_test", timeout=1, kinds=("DataProcessor",))
assert report["passed"] is False
assert any(issue["code"] == "live_related_selector_ref_missing" for issue in report["issues"])
def test_mcp_selector_chain_mcp_transport_unwraps_tool_text(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
def fake_post_json(url: str, payload: dict[str, Any], *, timeout: float, headers: dict[str, str] | None = None) -> tuple[dict[str, str], dict[str, Any]]:
captured.update({"url": url, "payload": payload, "headers": headers})
return (
{},
{
"jsonrpc": "2.0",
"id": payload["id"],
"result": {
"content": [
{
"type": "text",
"text": json.dumps({"status": "ok", "objects": []}, ensure_ascii=False),
}
]
},
},
)
monkeypatch.setattr(mcp_selector_chain_smoke, "post_json", fake_post_json)
result = mcp_selector_chain_smoke.mcp_rpc_call(
"http://mcp.example",
"metadata.objects.list",
{"base_id": "base_for_test", "kind": "DataProcessor"},
timeout=1,
)
assert result == {"status": "ok", "objects": []}
assert captured["url"] == "http://mcp.example/mcp"
assert captured["payload"]["method"] == "tools/call"
assert captured["payload"]["params"]["name"] == "onec_request"
assert captured["payload"]["params"]["arguments"]["method"] == "metadata.objects.list"
assert captured["headers"]["Accept"] == "application/json, text/event-stream"
def test_mcp_selector_chain_rest_transport_uses_process_service_token(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
class FakeResponse:
headers: dict[str, str] = {}
def __enter__(self) -> "FakeResponse":
return self
def __exit__(self, *args: Any) -> None:
return None
def read(self) -> bytes:
return b'{"status":"ok"}'
def fake_urlopen(request: Any, *, timeout: float) -> FakeResponse:
captured["authorization"] = request.get_header("Authorization")
return FakeResponse()
monkeypatch.setenv("ONEC_ADAPTER_SERVICE_TOKEN", "transient-test-token")
monkeypatch.setattr(mcp_selector_chain_smoke, "urlopen", fake_urlopen)
result = mcp_selector_chain_smoke.rest_rpc_call(
"http://adapter.example",
"adapter.help.methods",
{},
timeout=1,
)
assert result == {"status": "ok"}
assert captured["authorization"] == "Bearer transient-test-token"
def test_related_form_sections_follow_live_object_descriptor_layouts() -> None:
assert adapter_server.RELATED_SECTION_RULES["Report"] == [
{"path": "3", "category": "Template"},
{"path": "5", "category": "Form"},
{"path": "7", "category": "Command"},
]
assert adapter_server.RELATED_SECTION_RULES["DataProcessor"] == [
{"path": "4", "category": "Template"},
{"path": "5", "category": "Command"},
{"path": "6", "category": "Form"},
]
assert adapter_server.RELATED_SECTION_RULES["Catalog"] == [
{"path": "3", "category": "Template"},
{"path": "4", "category": "Command"},
{"path": "7", "category": "Form"},
]
assert adapter_server.RELATED_SECTION_RULES["Enum"] == [
{"path": "3", "category": "Form"},
{"path": "4", "category": "Template"},
]
assert adapter_server.RELATED_SECTION_RULES["InformationRegister"] == [
{"path": "5", "category": "Form"},
{"path": "6", "category": "Template"},
{"path": "8", "category": "Command"},
]
assert adapter_server.RELATED_SECTION_RULES["AccumulationRegister"] == [
{"path": "4", "category": "Command"},
{"path": "8", "category": "Form"},
]
assert adapter_server.RELATED_SECTION_RULES["BusinessProcess"] == [{"path": "4", "category": "Form"}]
assert adapter_server.RELATED_SECTION_RULES["Task"] == [
{"path": "4", "category": "Form"},
{"path": "8", "category": "Command"},
]
assert adapter_server.RELATED_SECTION_RULES["ChartOfCharacteristicTypes"] == [
{"path": "4", "category": "Template"},
{"path": "7", "category": "Form"},
]
assert adapter_server.RELATED_SECTION_RULES["ChartOfAccounts"] == [{"path": "6", "category": "Form"}]
assert adapter_server.RELATED_SECTION_RULES["ChartOfCalculationTypes"] == [{"path": "7", "category": "Form"}]
assert adapter_server.RELATED_SECTION_RULES["DocumentJournal"] == [
{"path": "3", "category": "Template"},
{"path": "5", "category": "Command"},
{"path": "6", "category": "Form"},
]
assert adapter_server.RELATED_SECTION_RULES["ExchangePlan"] == [
{"path": "4", "category": "Template"},
{"path": "6", "category": "Form"},
{"path": "7", "category": "Command"},
]
assert adapter_server.RELATED_SECTION_RULES["SelectionCriterion"] == [{"path": "3", "category": "Form"}]
assert adapter_server.RELATED_SECTION_RULES["SettingsStorage"] == [{"path": "4", "category": "Form"}]
for kind in (
"Enum",
"InformationRegister",
"AccumulationRegister",
"BusinessProcess",
"Task",
"DocumentJournal",
"SettingsStorage",
):
assert "forms" in adapter_server.KIND_CAPABILITIES[kind]
def test_public_child_identity_falls_back_to_inline_record_identity() -> None:
result = adapter_server.public_child_identity(
{
"guid": "00000000-0000-0000-0000-000000000001",
"status": "source_missing",
"record_identity": {
"guid": "00000000-0000-0000-0000-000000000002",
"name": "КомандаОбъекта",
"synonyms": {"ru": "Команда объекта"},
},
}
)
assert result["guid"] == "00000000-0000-0000-0000-000000000002"
assert result["name"] == "КомандаОбъекта"
assert result["synonym"] == "Команда объекта"
def test_mcp_selector_chain_live_mcp_transport_initializes_session(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, str | None]] = []
monkeypatch.setattr(
mcp_selector_chain_smoke,
"mcp_initialize",
lambda mcp_url, *, timeout: (
"session-for-test",
{
"jsonrpc": "2.0",
"result": {"serverInfo": {"contract_version": mcp_selector_chain_smoke.EXPECTED_CONTRACT_VERSION}},
},
),
)
monkeypatch.setattr(
mcp_selector_chain_smoke,
"mcp_onec_request_schema_check",
lambda mcp_url, *, timeout, session_id: (
{
"status": "ok",
"contract_version": mcp_selector_chain_smoke.EXPECTED_CONTRACT_VERSION,
"tools": 5,
"onec_request": True,
"selector_guidance": True,
"placeholder_examples": True,
},
[],
),
)
def fake_rpc_call(endpoint_url: str, method: str, payload: dict[str, Any], *, timeout: float, transport: str = "rest", session_id: str | None = None) -> dict[str, Any]:
calls.append((method, session_id))
if method == "help.methods":
return selector_chain_help_methods_response()
if method == "metadata.objects.list":
return {"status": "ok", "objects": [{"kind": "DataProcessor", "name": "DiscoveredObject"}]}
if method == "metadata.object.modules":
return {"status": "ok", "modules": [{"module_ordinal": 1}]}
if method == "metadata.definition.find":
assert payload["query"] == "DataProcessor.DiscoveredObject"
return {
"status": "ok",
"related_selectors": {
"modules": {
"base_id": payload["base_id"],
"method": "metadata.object.modules",
"kind": "DataProcessor",
"name": "DiscoveredObject",
"ref": "DataProcessor.DiscoveredObject",
}
},
}
if method == "modules.read":
return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
if method == "code.search":
return {
"status": "ok",
"items": [
{
"read_selector": {
"method": "code.read",
"base_id": payload["base_id"],
"ref": payload["ref"],
"module_ordinal": payload["module_ordinal"],
}
}
],
}
if method == "code.read":
return {"status": "ok", "text": "Процедура Команда()\nКонецПроцедуры"}
return {"status": "error", "error": method}
monkeypatch.setattr(mcp_selector_chain_smoke, "rpc_call", fake_rpc_call)
report = mcp_selector_chain_smoke.build_live_report(
"http://mcp.example",
"base_for_test",
timeout=1,
kinds=("DataProcessor",),
transport="mcp",
)
assert report["passed"] is True
assert report["transport"] == "mcp"
assert report["steps"][0]["name"] == "mcp.initialize"
assert report["steps"][0]["status"] == "ok"
assert report["steps"][0]["session"] is True
assert report["steps"][0]["contract_version"] == mcp_selector_chain_smoke.EXPECTED_CONTRACT_VERSION
assert report["steps"][1]["name"] == "mcp.tools.list"
assert report["steps"][1]["selector_guidance"] is True
assert report["steps"][2]["name"] == "adapter.help.methods"
assert report["steps"][2]["selector_capabilities"] is True
assert calls
assert all(session_id == "session-for-test" for _, session_id in calls)
def test_mcp_selector_chain_live_report_rejects_stale_adapter_help(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_rpc_call(adapter_url: str, method: str, payload: dict[str, Any], *, timeout: float, transport: str = "rest", session_id: str | None = None) -> dict[str, Any]:
if method == "help.methods":
return {"status": "ok", "contract_version": mcp_selector_chain_smoke.EXPECTED_CONTRACT_VERSION, "methods": [{"name": "code.search"}]}
return {"status": "error", "error": method}
monkeypatch.setattr(mcp_selector_chain_smoke, "rpc_call", fake_rpc_call)
report = mcp_selector_chain_smoke.build_live_report(
"http://adapter.example",
"base_for_test",
timeout=1,
kinds=("DataProcessor",),
)
assert report["passed"] is False
assert report["steps"][0]["name"] == "adapter.help.methods"
assert report["steps"][0]["status"] == "error"
assert any(issue["code"] == "live_adapter_selector_capabilities_missing" for issue in report["issues"])
def test_mcp_selector_chain_live_mcp_transport_rejects_stale_schema(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
mcp_selector_chain_smoke,
"mcp_initialize",
lambda mcp_url, *, timeout: (
"session-for-test",
{
"jsonrpc": "2.0",
"result": {"serverInfo": {"contract_version": mcp_selector_chain_smoke.EXPECTED_CONTRACT_VERSION}},
},
),
)
monkeypatch.setattr(
mcp_selector_chain_smoke,
"mcp_onec_request_schema_check",
lambda mcp_url, *, timeout, session_id: (
{
"status": "ok",
"contract_version": mcp_selector_chain_smoke.EXPECTED_CONTRACT_VERSION,
"tools": 5,
"onec_request": True,
"selector_guidance": False,
"placeholder_examples": False,
},
[{"code": "live_mcp_selector_guidance_missing", "term": "ref"}],
),
)
report = mcp_selector_chain_smoke.build_live_report(
"http://mcp.example",
"base_for_test",
timeout=1,
kinds=("DataProcessor",),
transport="mcp",
)
assert report["passed"] is False
assert report["steps"][1]["name"] == "mcp.tools.list"
assert report["steps"][1]["status"] == "error"
assert any(issue["code"] == "live_mcp_selector_guidance_missing" for issue in report["issues"])
def test_adapter_selector_diagnostics_stay_generic() -> None:
source = Path(adapter_server.__file__).read_text(encoding="utf-8")
assert "Pass object_type/object_name or object_guid" not in source
assert "Pass an object selector: kind/name or guid" not in source
assert "guid or kind/name/ordinal" not in source
def test_object_selector_methods_reject_non_string_selector_arguments() -> None:
for method in adapter_server.OBJECT_SELECTOR_ALIAS_METHODS:
for argument in adapter_server.OBJECT_SELECTOR_ARGUMENTS:
result = adapter_server.validate_adapter_job_payload(method, {"base_id": "upo_test", argument: 123})
assert result is not None, (method, argument)
assert result["status"] == "invalid_argument", (method, argument)
assert result["argument"] == argument, (method, argument)
def test_direct_object_selector_validator_rejects_non_string_public_ref() -> None:
result = adapter_server.validate_metadata_object_forms_payload({"base_id": "upo_test", "ref": 123})
assert result is not None
assert result["status"] == "invalid_argument"
assert result["argument"] == "ref"
def test_direct_job_object_selector_validators_reject_non_string_kind() -> None:
full_result = adapter_server.validate_metadata_object_full_payload({"base_id": "upo_test", "kind": 123})
_, attributes_result = adapter_server.validate_metadata_object_attributes_payload({"base_id": "upo_test", "kind": 123})
assert full_result is not None
assert full_result["status"] == "invalid_argument"
assert full_result["argument"] == "kind"
assert attributes_result is not None
assert attributes_result["status"] == "invalid_argument"
assert attributes_result["argument"] == "kind"
def test_direct_object_scoped_runtime_methods_reject_non_string_kind() -> None:
cases = [
adapter_server.code_search({"base_id": "upo_test", "query": "Команда", "kind": 123}),
adapter_server.read_module({"base_id": "upo_test", "module_ordinal": 1, "kind": 123}),
adapter_server.metadata_object_forms({"base_id": "upo_test", "kind": 123}),
]
for result in cases:
assert result["status"] == "invalid_argument"
assert result["argument"] == "kind"
def test_has_object_selector_accepts_public_aliases_without_normalization() -> None:
assert adapter_server.has_object_selector({"ref": "Обработка.Имя"}) is True
assert adapter_server.has_object_selector({"object_name": "Имя"}) is True
assert adapter_server.has_object_selector({"object_guid": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"}) is True
assert adapter_server.has_object_selector({"object_type": "Обработка"}) is False
assert adapter_server.has_object_selector({"kind": "DataProcessor"}) is False
def test_object_selector_required_message_stays_complete() -> None:
for term in adapter_server.OBJECT_SELECTOR_GUIDANCE_TERMS:
assert term in adapter_server.OBJECT_SELECTOR_REQUIRED_MESSAGE
assert "ordinal" in adapter_server.OBJECT_SELECTOR_REQUIRED_MESSAGE
def test_object_selector_diagnostic_constants_stay_complete() -> None:
expected_terms = {
"OBJECT_SELECTOR_REQUIRED_MESSAGE_NO_ORDINAL": adapter_server.OBJECT_SELECTOR_GUIDANCE_TERMS,
"OBJECT_SELECTOR_GLOBAL_REQUIRED_MESSAGE": (*adapter_server.OBJECT_SELECTOR_GUIDANCE_TERMS, "areas metadata/extensions"),
"OBJECT_SELECTOR_OR_MODULE_REQUIRED_MESSAGE": (*adapter_server.OBJECT_SELECTOR_GUIDANCE_TERMS, "module_ref", "module_id"),
"MODULE_READ_SELECTOR_OR_MODULE_ID_MESSAGE": (*adapter_server.OBJECT_SELECTOR_GUIDANCE_TERMS, "ordinal", "module_id"),
}
for constant_name, terms in expected_terms.items():
message = getattr(adapter_server, constant_name)
for term in terms:
assert term in message, constant_name
def test_object_selector_capability_descriptor_drives_alias_methods() -> None:
assert set(adapter_server.OBJECT_SELECTOR_ALIAS_METHODS) == set(adapter_server.OBJECT_SELECTOR_METHOD_CAPABILITIES)
assert adapter_server.object_selector_capabilities("metadata.definition.find")["allows_global_areas"] is True
assert adapter_server.object_selector_capabilities("modules.read")["accepts_module_ref"] is True
assert adapter_server.object_selector_capabilities("code.read")["accepts_module_ref"] is True
assert adapter_server.object_selector_capabilities("metadata.object.full")["requires_object_selector"] is True
assert adapter_server.object_selector_capabilities("help.methods") is None
def test_adapter_help_exposes_selector_capabilities_for_object_methods() -> None:
result = adapter_server.call_method("help.methods", {})
methods = {row["name"]: row for row in result["methods"]}
for method in adapter_server.OBJECT_SELECTOR_ALIAS_METHODS:
capabilities = methods[method].get("selector_capabilities")
assert isinstance(capabilities, dict), method
assert capabilities["accepts_ref"] is True, method
assert capabilities["accepts_kind_name_guid"] is True, method
assert capabilities["accepts_object_aliases"] is True, method
def test_mcp_enriches_code_search_read_selector_display() -> None:
result = {
"status": "ok",
"items": [
{
"match": "Фрагмент",
"read_selector": {
"base_id": "upo_test",
"method": "code.read",
"ref": "DataProcessor.ТестоваяОбработка",
"kind": "DataProcessor",
"name": "ТестоваяОбработка",
"module_ref": "ConfigCAS:module-guid#stream:0",
},
}
],
"diagnostics": {
"owner_resolution": {
"strategy": "live_metadata_scan",
"owner_scan_limit_hit": False,
}
},
}
enriched = adapter_mcp.maybe_enrich_owner_fields("code.search", {"base_id": "upo_test"}, result)
item = enriched["items"][0]
assert item["read_selector"]["method"] == "code.read"
assert item["read_selector"]["ref"] == "DataProcessor.ТестоваяОбработка"
assert item["read_selector"]["name"] == "ТестоваяОбработка"
assert item["read_selector"]["module_ref"] == "ConfigCAS:module-guid#stream:0"
assert item["source_ref_display"] == {
"kind": "DataProcessor",
"name": "ТестоваяОбработка",
"guid": "ConfigCAS:module-guid#stream:0",
"method": "code.read",
}
assert enriched["diagnostics"]["owner_resolution"]["strategy"] == "live_metadata_scan"
def test_adapter_help_describes_public_read_selectors() -> None:
result = adapter_server.call_method("help.methods", {})
methods = {row["name"]: row for row in result["methods"]}
assert "read_selector.method=modules.read" in methods["modules.search"]["description"]
assert "diagnostics.owner_resolution" in methods["modules.search"]["description"]
assert "module_ref from modules.search read_selector" in methods["modules.read"]["description"]
assert "read_selector.method is code.read" in methods["code.search"]["description"]
assert "module_ref from code.search read_selector" in methods["code.read"]["description"]
assert "related_selectors" in methods["metadata.definition.find"]["description"]
assert "object_type/object_name/object_guid" in methods["metadata.definition.find"]["description"]
assert "object_type/object_name/object_guid" in methods["modules.search"]["description"]
assert "object_type/object_name/object_guid" in methods["modules.read"]["description"]
assert "object_type/object_name/object_guid" in methods["code.search"]["description"]
assert "object_type/object_name/object_guid" in methods["code.read"]["description"]
assert "public kind counts" in methods["metadata.adapter.audit"]["description"]
assert "code carrier matrix" in methods["metadata.adapter.audit"]["description"]
assert "saved-state layer" in methods["metadata.write.capabilities"]["description"]
def test_metadata_write_capabilities_reports_saved_state_rule() -> None:
result = adapter_server.call_method("metadata.write.capabilities", {"base_id": "upo_test"})
assert result["status"] == "ok"
assert result["default_write_layer"] == "save"
assert result["write_capabilities"]["bsl_modules"]["agent_method"] == "code.write"
assert result["write_capabilities"]["scheduled_jobs"]["status"] == "partial_saved_state"
assert result["write_capabilities"]["scheduled_jobs"]["agent_method"] == "metadata.write"
assert "web_http_services" in result["unsupported_summary"]
def test_adapter_help_describes_selector_aliases_for_all_object_methods() -> None:
result = adapter_server.call_method("help.methods", {})
methods = {row["name"]: row for row in result["methods"]}
for method in adapter_server.OBJECT_SELECTOR_ALIAS_METHODS:
assert "object_type/object_name/object_guid" in methods[method]["description"], method
def test_adapter_help_completes_partial_selector_guidance() -> None:
row = adapter_server.public_method_row(
{
"name": "code.search",
"transport": "POST /rpc",
"description": "Accepts object_type/object_name/object_guid.",
}
)
assert "ref" in row["description"]
assert "kind/name/guid" in row["description"]
assert "object_type/object_name/object_guid" in row["description"]
def test_mcp_unified_methods_do_not_shadow_adapter_methods() -> None:
adapter_methods = {str(row.get("name") or "") for row in adapter_server.METHODS if row.get("name")}
assert adapter_methods.isdisjoint(adapter_mcp.UNIFIED_METHODS)
def test_mcp_generic_request_passes_any_adapter_method(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, str, object | None]] = []
def fake_http_json(method: str, path: str, body: object | None = None, *, timeout: float | None = None) -> dict[str, object]:
calls.append((method, path, body))
return {"status": "ok", "method": "future.adapter.method"}
monkeypatch.setattr(adapter_mcp, "http_json", fake_http_json)
result = adapter_mcp.call_adapter_method("future.adapter.method", {"base_id": "upo_test", "x": 1})
assert result["status"] == "ok"
assert calls == [
(
"POST",
"/rpc",
{"method": "future.adapter.method", "payload": {"base_id": "upo_test", "x": 1}},
)
]
def test_mcp_access_role_users_tool_forwards_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, dict[str, object]]] = []
def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
calls.append((method, payload))
return {"status": "ok", "users": [{"name": "Ivan.PETROV"}]}
monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)
result = adapter_mcp.handle_tool_call("access_role_users", {"base_id": "upo_test", "role": "запись изменение номенклатура поставщиков", "limit": 50})
decoded = json.loads(result["content"][0]["text"])
assert decoded["status"] == "ok"
assert calls == [
(
"access.role.users",
{"base_id": "upo_test", "role": "запись изменение номенклатура поставщиков", "limit": 50},
)
]
def test_mcp_access_object_subjects_tool_forwards_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, dict[str, object]]] = []
def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
calls.append((method, payload))
return {"status": "ok", "users": [{"user": {"name": "Ivan.PETROV"}}]}
monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)
result = adapter_mcp.handle_tool_call(
"access_object_subjects",
{"base_id": "upo_test", "ref": "РегистрСведений.УОП_АктуальныеСпецификации", "action": "write", "include_access_key_scope": True},
)
decoded = json.loads(result["content"][0]["text"])
assert decoded["status"] == "ok"
assert calls == [
(
"access.object.subjects",
{"base_id": "upo_test", "ref": "РегистрСведений.УОП_АктуальныеСпецификации", "action": "write", "include_access_key_scope": True},
)
]
def test_mcp_access_object_roles_tool_forwards_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, dict[str, object]]] = []
def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
calls.append((method, payload))
return {"status": "ok", "roles": [{"name": "Базовые права БСП"}]}
monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)
result = adapter_mcp.handle_tool_call(
"access_object_roles",
{
"base_id": "upo_test",
"ref": "РегистрСведений.УОП_АктуальныеСпецификации",
"action": "write",
"max_effective_permissions_per_user": 1000,
},
)
decoded = json.loads(result["content"][0]["text"])
assert decoded["status"] == "ok"
assert calls == [
(
"access.object.roles",
{
"base_id": "upo_test",
"ref": "РегистрСведений.УОП_АктуальныеСпецификации",
"action": "write",
"max_effective_permissions_per_user": 1000,
},
)
]
def test_mcp_access_key_tools_forward_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, dict[str, object]]] = []
def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
calls.append((method, payload))
return {"status": "ok", "counts": {"rows": 1}}
monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)
keys_result = adapter_mcp.handle_tool_call(
"access_keys_query",
{"base_id": "upo_test", "kind": "group", "group": "Администраторы", "limit": 5},
)
object_keys_result = adapter_mcp.handle_tool_call(
"access_object_keys_resolve",
{"base_id": "upo_test", "object": "Catalog.Номенклатура", "limit": 5},
)
assert json.loads(keys_result["content"][0]["text"])["status"] == "ok"
assert json.loads(object_keys_result["content"][0]["text"])["status"] == "ok"
assert calls == [
("access.keys.query", {"base_id": "upo_test", "kind": "group", "group": "Администраторы", "limit": 5}),
("access.object_keys.resolve", {"base_id": "upo_test", "object": "Catalog.Номенклатура", "limit": 5}),
]
def test_mcp_access_users_search_tool_forwards_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, dict[str, object]]] = []
def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
calls.append((method, payload))
return {"status": "ok", "users": [{"name": "Lyubov.IVANOVA"}]}
monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)
result = adapter_mcp.handle_tool_call("access_users_search", {"base_id": "upo_test", "query": "Lyubov", "limit": 5})
decoded = json.loads(result["content"][0]["text"])
assert decoded["status"] == "ok"
assert calls == [("access.users.search", {"base_id": "upo_test", "query": "Lyubov", "limit": 5})]
def test_mcp_infobase_user_tools_forward_to_default_user_methods(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, dict[str, object]]] = []
def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
calls.append((method, payload))
return {"status": "ok"}
monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)
search = adapter_mcp.handle_tool_call("infobase_users_search", {"base_id": "upo_test", "query": "Аналитик"})
get_one = adapter_mcp.handle_tool_call("infobase_user_get", {"base_id": "upo_test", "user": "Аналитик"})
capabilities = adapter_mcp.handle_tool_call("infobase_user_password_capabilities", {"base_id": "upo_test"})
password_status = adapter_mcp.handle_tool_call("infobase_user_password_status", {"base_id": "upo_test", "user": "Аналитик"})
password_set = adapter_mcp.handle_tool_call(
"infobase_user_password_set",
{
"base_id": "upo_test",
"user": "Аналитик",
"confirm_user_id": "a74272fec8cf262a405a568d7871fb07",
"new_password": "test-password",
"allow_password_change": True,
},
)
password_clear = adapter_mcp.handle_tool_call(
"infobase_user_password_clear",
{
"base_id": "upo_test",
"user": "Аналитик",
"confirm_user_id": "a74272fec8cf262a405a568d7871fb07",
"allow_password_clear": True,
},
)
assert json.loads(search["content"][0]["text"])["status"] == "ok"
assert json.loads(get_one["content"][0]["text"])["status"] == "ok"
assert json.loads(capabilities["content"][0]["text"])["status"] == "ok"
assert json.loads(password_status["content"][0]["text"])["status"] == "ok"
assert json.loads(password_set["content"][0]["text"])["status"] == "ok"
assert json.loads(password_clear["content"][0]["text"])["status"] == "ok"
assert calls == [
("infobase.users.search", {"base_id": "upo_test", "query": "Аналитик"}),
("infobase.user.get", {"base_id": "upo_test", "user": "Аналитик"}),
("infobase.user.password.capabilities", {"base_id": "upo_test"}),
("infobase.user.password.status", {"base_id": "upo_test", "user": "Аналитик"}),
(
"infobase.user.password.set",
{
"base_id": "upo_test",
"user": "Аналитик",
"confirm_user_id": "a74272fec8cf262a405a568d7871fb07",
"new_password": "test-password",
"allow_password_change": True,
},
),
(
"infobase.user.password.clear",
{
"base_id": "upo_test",
"user": "Аналитик",
"confirm_user_id": "a74272fec8cf262a405a568d7871fb07",
"allow_password_clear": True,
},
),
]
def test_mcp_access_rls_discover_tool_forwards_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, dict[str, object]]] = []
def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
calls.append((method, payload))
return {"status": "ok", "candidates": [{"name": "КлючиДоступаКОбъектам"}]}
monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)
result = adapter_mcp.handle_tool_call("access_rls_discover", {"base_id": "upo_test", "terms": ["Доступ"], "limit": 5})
decoded = json.loads(result["content"][0]["text"])
assert decoded["status"] == "ok"
assert calls == [
(
"access.rls.discover",
{"base_id": "upo_test", "terms": ["Доступ"], "limit": 5},
)
]
def test_mcp_access_role_audit_export_tool_forwards_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, dict[str, object]]] = []
def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
calls.append((method, payload))
return {"status": "ok", "rows": [{"user_name": "Ivan.PETROV"}]}
monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)
result = adapter_mcp.handle_tool_call("access_role_audit_export", {"base_id": "upo_test", "role": "запись изменение номенклатура поставщиков", "format": "csv"})
decoded = json.loads(result["content"][0]["text"])
assert decoded["status"] == "ok"
assert calls == [
(
"access.role.audit_export",
{"base_id": "upo_test", "role": "запись изменение номенклатура поставщиков", "format": "csv"},
)
]
def test_mcp_access_role_audit_analyze_tool_forwards_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, dict[str, object]]] = []
def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
calls.append((method, payload))
return {"status": "ok", "risk_level": "low", "findings": []}
monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)
result = adapter_mcp.handle_tool_call("access_role_audit_analyze", {"base_id": "upo_test", "role": "Администратор", "user_threshold": 10})
decoded = json.loads(result["content"][0]["text"])
assert decoded["status"] == "ok"
assert calls == [
(
"access.role.audit_analyze",
{"base_id": "upo_test", "role": "Администратор", "user_threshold": 10},
)
]
def test_mcp_access_role_audit_compare_latest_uses_local_reports(tmp_path: Path) -> None:
folder = tmp_path / "upo_test"
folder.mkdir()
old_export = folder / "old.json"
new_export = folder / "new.json"
old_export.write_text(json.dumps({"rows": [{"user_id": "u1", "user_name": "Ivan.PETROV", "access_path": "A"}]}, ensure_ascii=False), encoding="utf-8")
new_export.write_text(
json.dumps(
{
"rows": [
{"user_id": "u1", "user_name": "Ivan.PETROV", "access_path": "A"},
{"user_id": "u2", "user_name": "Olga.IVANOVA", "access_path": "B"},
]
},
ensure_ascii=False,
),
encoding="utf-8",
)
for name, generated_at, export_path in [
("old.summary.json", "2026-07-01T10:00:00+00:00", old_export),
("new.summary.json", "2026-07-02T10:00:00+00:00", new_export),
]:
(folder / name).write_text(
json.dumps(
{
"schema": "onec_access_role_audit_artifact.v1",
"generated_at": generated_at,
"query": {"role": "Роль A"},
"artifacts": {"json": str(export_path)},
},
ensure_ascii=False,
),
encoding="utf-8",
)
result = adapter_mcp.handle_tool_call(
"access_role_audit_compare_latest",
{"base_id": "upo_test", "role": "Роль A", "report_root": str(tmp_path), "write_artifacts": False},
)
decoded = json.loads(result["content"][0]["text"])
assert decoded["status"] == "ok"
assert decoded["counts"]["added_users"] == 1
assert decoded["added_users"][0]["user_name"] == "Olga.IVANOVA"
assert decoded["sources"]["old"].endswith("old.summary.json")
assert decoded["sources"]["new"].endswith("new.summary.json")
def test_mcp_access_tools_require_base_id() -> None:
result = adapter_mcp.handle_tool_call("access_role_users", {"role": "Администратор"})
decoded = json.loads(result["content"][0]["text"])
assert decoded["schema"] == "adapter_1c_mcp_policy.v1"
assert decoded["status"] == "blocked"
assert decoded["reason"] == "base_id_required"
assert adapter_mcp.method_requires_base_id("access.role.users") is True
def test_export_1c_access_role_audit_writes_artifacts(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, dict[str, Any]]] = []
def fake_rpc(base_url: str, method: str, payload: dict[str, Any], *, timeout: int) -> dict[str, Any]:
calls.append((method, payload))
if method == "access.role.audit_export":
return {
"status": "ok",
"csv": "base_id,user_name\nupo_test,Ivan.PETROV\n",
"counts": {"rows": 1, "users": 1},
"rows": [{"user_name": "Ivan.PETROV", "group_name": "Закупщик", "profile_name": "УПО. Закупки", "matched_role_name": "Роль", "access_path": "Роль -> УПО. Закупки -> Закупщик -> Ivan.PETROV"}],
}
if method == "access.role.audit_analyze":
return {
"status": "ok",
"risk_level": "low",
"findings": [],
}
raise AssertionError(method)
monkeypatch.setattr(access_role_audit_export_script, "rpc", fake_rpc)
summary = access_role_audit_export_script.export_role_audit(
adapter_url="http://adapter.example",
base_id="upo_test",
role="запись изменение номенклатура поставщиков",
report_root=tmp_path,
timeout=120,
user_threshold=30,
include_analysis=True,
limit=20000,
include_html=True,
)
artifacts = summary["artifacts"]
assert summary["status"] == "ok"
assert summary["risk_level"] == "low"
assert Path(artifacts["csv"]).read_text(encoding="utf-8-sig").startswith("base_id,user_name")
assert json.loads(Path(artifacts["json"]).read_text(encoding="utf-8"))["counts"]["rows"] == 1
assert json.loads(Path(artifacts["analysis"]).read_text(encoding="utf-8"))["risk_level"] == "low"
assert json.loads(Path(artifacts["summary"]).read_text(encoding="utf-8"))["schema"] == "onec_access_role_audit_artifact.v1"
html_text = Path(artifacts["html"]).read_text(encoding="utf-8")
assert "1C Access Role Audit" in html_text
assert "Ivan.PETROV" in html_text
index_text = Path(artifacts["index"]).read_text(encoding="utf-8")
assert "1C Access Audit Index" in index_text
assert "запись изменение номенклатура поставщиков" in index_text
assert "low" in index_text
assert Path(artifacts["csv"]).name in index_text
assert calls[0][0] == "access.role.audit_export"
assert calls[1][0] == "access.role.audit_analyze"
def test_compare_1c_access_role_audit_detects_user_and_path_changes(tmp_path: Path) -> None:
old_path = tmp_path / "old.json"
new_path = tmp_path / "new.json"
output_path = tmp_path / "compare.json"
html_path = tmp_path / "compare.html"
old_path.write_text(
json.dumps(
{
"schema": "onec_access_role_audit_export.v1",
"rows": [
{"user_id": "u1", "user_name": "Ivan.PETROV", "access_path": "Роль -> Профиль -> Группа A -> Ivan.PETROV"},
{"user_id": "u2", "user_name": "Anna.SIDOROVA", "access_path": "Роль -> Профиль -> Группа A -> Anna.SIDOROVA"},
],
},
ensure_ascii=False,
),
encoding="utf-8",
)
new_path.write_text(
json.dumps(
{
"schema": "onec_access_role_audit_export.v1",
"rows": [
{"user_id": "u1", "user_name": "Ivan.PETROV", "access_path": "Роль -> Профиль -> Группа B -> Ivan.PETROV"},
{"user_id": "u3", "user_name": "Olga.IVANOVA", "access_path": "Роль -> Профиль -> Группа B -> Olga.IVANOVA"},
],
},
ensure_ascii=False,
),
encoding="utf-8",
)
result = access_role_audit_compare_script.compare_files(old_path, new_path, output=output_path, html_output=html_path)
assert result["status"] == "ok"
assert result["counts"] == {
"old_users": 2,
"new_users": 2,
"added_users": 1,
"removed_users": 1,
"unchanged_users": 1,
"changed_access_paths": 1,
}
assert result["added_users"][0]["user_name"] == "Olga.IVANOVA"
assert result["removed_users"][0]["user_name"] == "Anna.SIDOROVA"
assert result["changed_access_paths"][0]["user"]["user_name"] == "Ivan.PETROV"
assert json.loads(output_path.read_text(encoding="utf-8"))["schema"] == "onec_access_role_audit_compare.v1"
assert "1C Access Audit Compare" in html_path.read_text(encoding="utf-8")
def test_compare_1c_access_role_audit_finds_latest_summaries_by_role(tmp_path: Path) -> None:
folder = tmp_path / "upo_test"
folder.mkdir()
for name, generated_at, role in [
("old.summary.json", "2026-07-01T10:00:00+00:00", "Роль A"),
("new.summary.json", "2026-07-02T10:00:00+00:00", "Роль A"),
("other.summary.json", "2026-07-03T10:00:00+00:00", "Роль B"),
]:
(folder / name).write_text(
json.dumps({"schema": "onec_access_role_audit_artifact.v1", "generated_at": generated_at, "query": {"role": role}}, ensure_ascii=False),
encoding="utf-8",
)
result = access_role_audit_compare_script.find_latest_summaries(tmp_path, "upo_test", role="Роль A", count=2)
assert [path.name for path in result] == ["new.summary.json", "old.summary.json"]
def test_export_1c_access_role_audit_index_links_existing_compare(tmp_path: Path) -> None:
folder = tmp_path / "upo_test"
folder.mkdir()
old_summary = folder / "old.summary.json"
new_summary = folder / "new.summary.json"
old_summary.write_text(
json.dumps({"generated_at": "2026-07-01T10:00:00+00:00", "query": {"role": "Роль A"}, "artifacts": {"summary": str(old_summary)}}, ensure_ascii=False),
encoding="utf-8",
)
new_summary.write_text(
json.dumps({"generated_at": "2026-07-02T10:00:00+00:00", "query": {"role": "Роль A"}, "artifacts": {"summary": str(new_summary)}}, ensure_ascii=False),
encoding="utf-8",
)
(folder / "compare-old-new.html").write_text("", encoding="utf-8")
index_path = access_role_audit_export_script.update_index(tmp_path, "upo_test")
index_text = index_path.read_text(encoding="utf-8")
assert "compare_previous" in index_text
assert "compare-old-new.html" in index_text
def test_audit_1c_access_role_exports_and_compares_previous(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
folder = tmp_path / "upo_test"
folder.mkdir()
old_export = folder / "old.json"
old_summary = folder / "old.summary.json"
old_export.write_text(json.dumps({"rows": [{"user_id": "u1", "user_name": "Ivan.PETROV", "access_path": "A"}]}, ensure_ascii=False), encoding="utf-8")
old_summary.write_text(
json.dumps(
{
"schema": "onec_access_role_audit_artifact.v1",
"generated_at": "2026-07-01T10:00:00+00:00",
"query": {"role": "Роль A"},
"artifacts": {"json": str(old_export)},
},
ensure_ascii=False,
),
encoding="utf-8",
)
def fake_export_role_audit(**kwargs: Any) -> dict[str, Any]:
new_export = folder / "new.json"
new_summary = folder / "new.summary.json"
new_export.write_text(
json.dumps(
{
"rows": [
{"user_id": "u1", "user_name": "Ivan.PETROV", "access_path": "A"},
{"user_id": "u2", "user_name": "Olga.IVANOVA", "access_path": "B"},
]
},
ensure_ascii=False,
),
encoding="utf-8",
)
new_summary.write_text(
json.dumps(
{
"schema": "onec_access_role_audit_artifact.v1",
"status": "ok",
"generated_at": "2026-07-02T10:00:00+00:00",
"query": {"role": kwargs["role"]},
"risk_level": "low",
"artifacts": {"json": str(new_export), "summary": str(new_summary)},
},
ensure_ascii=False,
),
encoding="utf-8",
)
return {"status": "ok", "risk_level": "low", "artifacts": {"summary": str(new_summary)}}
monkeypatch.setattr(access_role_audit_script.export_script, "export_role_audit", fake_export_role_audit)
result = access_role_audit_script.audit_role(
adapter_url="http://adapter.example",
base_id="upo_test",
role="Роль A",
report_root=tmp_path,
timeout=120,
user_threshold=30,
limit=20000,
)
assert result["status"] == "ok"
assert result["verdict"] == "changed"
assert result["compare"]["counts"]["added_users"] == 1
def test_mcp_bulk_execute_routes_subrequests_to_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, dict[str, object]]] = []
monkeypatch.setattr(adapter_mcp, "read_cached_response", lambda cache_key: None)
monkeypatch.setattr(adapter_mcp, "write_cache_response", lambda *args, **kwargs: None)
def fake_call_adapter_method(method: str, payload: dict[str, object], *, timeout: float | None = None) -> dict[str, object]:
calls.append((method, payload))
return {"status": "ok", "items": [{"method": method}]}
monkeypatch.setattr(adapter_mcp, "call_adapter_method", fake_call_adapter_method)
result = adapter_mcp._run_bulk_execute(
{
"base_id": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec",
"requests": [{"method": "code.read", "payload": {"module_ref": "ConfigCAS:module#stream:1"}}],
},
0.0,
"test-request",
)
assert result["status"] == "ok"
assert calls == [
(
"code.read",
{"module_ref": "ConfigCAS:module#stream:1", "base_id": "84e4c0c3-2a21-4aba-a7b0-f92b3f2878ec"},
)
]
def test_modules_search_validates_query_and_include_storage_types() -> None:
bad_query = search_modules({"base_id": "upo_test", "query": ["Запрос"]})
empty_query = search_modules({"base_id": "upo_test", "query": ""})
bad_include_storage = search_modules({"base_id": "upo_test", "query": "Запрос", "include_storage": "false"})
bad_limit = search_modules({"base_id": "upo_test", "query": "Запрос", "limit": "1"})
bad_max_matches = search_modules({"base_id": "upo_test", "query": "Запрос", "max_matches": "3"})
conflicting_limit = search_modules({"base_id": "upo_test", "query": "Запрос", "limit": 5, "max_matches": 3})
bad_scan_limit = search_modules({"base_id": "upo_test", "query": "Запрос", "scan_limit": "10"})
assert bad_query["status"] == "invalid_argument"
assert bad_query["argument"] == "query"
assert empty_query["status"] == "invalid_argument"
assert empty_query["error"] == "invalid_argument"
assert empty_query["argument"] == "query"
assert bad_include_storage["status"] == "invalid_argument"
assert bad_include_storage["argument"] == "include_storage"
assert bad_limit["status"] == "invalid_argument"
assert bad_limit["argument"] == "limit"
assert bad_max_matches["status"] == "invalid_argument"
assert bad_max_matches["argument"] == "max_matches"
assert conflicting_limit["status"] == "invalid_argument"
assert conflicting_limit["argument"] == "max_matches"
assert bad_scan_limit["status"] == "invalid_argument"
assert bad_scan_limit["argument"] == "scan_limit"
def test_metadata_objects_list_validates_boolean_flags() -> None:
for argument in ["include_storage", "include_missing", "only_missing", "exact_counts", "refresh_cache"]:
result = call_method("metadata.objects.list", {"base_id": "upo_test", "kind": "Document", argument: "true"})
assert result["status"] == "invalid_argument"
assert result["argument"] == argument
def test_metadata_objects_list_rejects_extension_filter() -> None:
result = call_method(
"metadata.objects.list",
{"base_id": "upo_test", "kind": "CommonForm", "extension": "test2", "name_filter": "t_Форма"},
)
assert result["status"] == "invalid_argument"
assert result["argument"] == "extension"
assert result["diagnostics"]["next_method"] == "extension.objects.find"
assert result["diagnostics"]["next_payload"]["extension"] == "test2"
assert result["diagnostics"]["next_payload"]["kind"] == "CommonForm"
assert result["diagnostics"]["next_payload"]["query"] == "t_Форма"
def test_public_metadata_methods_validate_common_argument_types() -> None:
cases = [
("modules.read", {"base_id": "upo_test", "kind": "Document", "name": "X", "module_ordinal": 1, "include_storage": "false"}, "include_storage"),
("modules.read", {"base_id": "upo_test", "kind": "Document", "name": "X", "module_ordinal": 1, "include_text": "false"}, "include_text"),
("modules.read", {"base_id": "upo_test", "kind": "Document", "name": "X", "module_ordinal": 1, "summary": "true"}, "summary"),
("modules.read", {"base_id": "upo_test", "kind": "Document", "name": "X", "module_ordinal": 1, "routines_only": "true"}, "routines_only"),
("modules.read", {"base_id": "upo_test", "kind": "Document", "name": "X", "module_ordinal": 1, "include_container_preview": "true"}, "include_container_preview"),
("modules.read", {"base_id": "upo_test", "kind": "Document", "name": "X", "module_ordinal": 1, "include_container_preview": True, "container_preview_chars": "10"}, "container_preview_chars"),
("metadata.object.form.details", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_storage": "false"}, "include_storage"),
("metadata.object.form.details", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_module_text": "true"}, "include_module_text"),
("metadata.object.form.details", {"base_id": "upo_test", "kind": "Document", "name": "X", "max_forms": "1"}, "max_forms"),
("metadata.object.form.details", {"base_id": "upo_test", "kind": "Document", "name": "X", "max_items": "10"}, "max_items"),
("metadata.form.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "form": "Y", "include_storage": "false"}, "include_storage"),
("metadata.form.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "form": "Y", "include_module_text": "true"}, "include_module_text"),
("metadata.form.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "form": "Y", "max_items": "10"}, "max_items"),
("metadata.form.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "form": "Y", "evidence_mode": "wide"}, "evidence_mode"),
("metadata.object.template.details", {"base_id": "upo_test", "kind": "Document", "name": "X", "template": "Y", "include_storage": "false"}, "include_storage"),
("metadata.object.template.details", {"base_id": "upo_test", "kind": "Document", "name": "X", "template": "Y", "include_preview": "true"}, "include_preview"),
("metadata.object.template.details", {"base_id": "upo_test", "kind": "Document", "name": "X", "template": "Y", "evidence_mode": "wide"}, "evidence_mode"),
("metadata.object.commands", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_storage": "false"}, "include_storage"),
("metadata.object.commands", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_form_commands": "true"}, "include_form_commands"),
("metadata.object.commands", {"base_id": "upo_test", "kind": "Document", "name": "X", "command": ["Записать"]}, "command"),
("metadata.object.related", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_storage": "false"}, "include_storage"),
("metadata.object.related", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_text": "false"}, "include_text"),
("metadata.object.related", {"base_id": "upo_test", "kind": "Document", "name": "X", "guids_per_record": "5"}, "guids_per_record"),
("metadata.object.special.details", {"base_id": "upo_test", "kind": "DocumentJournal", "name": "X", "include_column_types": "true"}, "include_column_types"),
("metadata.object.properties", {"base_id": "upo_test", "kind": "DocumentJournal", "name": "X", "include_column_types": "true"}, "include_column_types"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_storage": "false"}, "include_storage"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_module_text": "true"}, "include_module_text"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_form_details": "true"}, "include_form_details"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_form_module_text": "true"}, "include_form_module_text"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_template_details": "true"}, "include_template_details"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_template_preview": "true"}, "include_template_preview"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_parts_summary": "true"}, "include_parts_summary"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "sections": "forms"}, "sections"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "sections": {"include": "forms"}}, "sections"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "sections": []}, "sections"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "sections": ["unknown"]}, "sections"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "sections": [1]}, "sections"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "max_forms": "1"}, "max_forms"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "max_form_items": "10"}, "max_form_items"),
("metadata.object.full", {"base_id": "upo_test", "kind": "Document", "name": "X", "evidence_mode": "wide"}, "evidence_mode"),
("metadata.object.attributes", {"base_id": "upo_test", "kind": "Document", "name": "X", "refresh_cache": "true"}, "refresh_cache"),
("metadata.object.attributes", {"base_id": "upo_test", "kind": "Document", "name": "X", "use_cache": "true"}, "use_cache"),
("metadata.object.attributes", {"base_id": "upo_test", "kind": "Document", "name": "X", "limit": "20"}, "limit"),
("adapter.job.get", {"job_id": "not_real", "consume": "true"}, "consume"),
("adapter.job.get", {"job_id": "not_real", "include_partial_result": "true"}, "include_partial_result"),
("adapter.job.get", {"job_id": ["x"]}, "job_id"),
("adapter.job.cancel", {"job_id": ["x"]}, "job_id"),
("adapter.job.start", {"method": "metadata.object.get", "payload": {"base_id": "upo_test"}, "timeout_seconds": "10"}, "timeout_seconds"),
("adapter.job.start", {"method": ["metadata.object.get"], "payload": {"base_id": "upo_test"}}, "method"),
("adapter.job.start", {"method": "metadata.object.get", "payload": "bad"}, "payload"),
("metadata.snapshot", {"base_id": "upo_test", "include_modules": "true"}, "include_modules"),
("metadata.object.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_storage": "false"}, "include_storage"),
("metadata.object.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_text": "true"}, "include_text"),
("metadata.object.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_tree": "true"}, "include_tree"),
("metadata.object.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "max_depth": "3"}, "max_depth"),
("metadata.object.decode", {"base_id": "upo_test", "kind": "Document", "name": "X", "evidence_mode": "wide"}, "evidence_mode"),
("metadata.object.parts", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_storage": "false"}, "include_storage"),
("metadata.object.parts", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_text": "true"}, "include_text"),
("metadata.object.parts", {"base_id": "upo_test", "kind": "Document", "name": "X", "include_tree": "true"}, "include_tree"),
("metadata.object.parts", {"base_id": "upo_test", "kind": "Document", "name": "X", "part_limit": "20"}, "part_limit"),
("metadata.object.parts", {"base_id": "upo_test", "kind": "Document", "name": "X", "evidence_mode": "wide"}, "evidence_mode"),
("schema.tables.list", {"base_id": "upo_test", "limit": "5"}, "limit"),
("storage.files.list", {"base_id": "upo_test", "table": "Config", "limit": "5"}, "limit"),
("storage.file.get", {"base_id": "upo_test", "table": "Config", "file_name": "x", "include_payload": "true"}, "include_payload"),
("codec.decode", {"base_id": "upo_test", "table": "Config", "file_name": "x", "include_text": "true"}, "include_text"),
("codec.decode", {"base_id": "upo_test", "table": "Config", "file_name": "x", "include_tree": "true"}, "include_tree"),
("payload.diff", {"diagnostic": True, "before": [], "after": {"text": "{1}"}}, "before"),
("payload.diff", {"diagnostic": True, "before": {"text": "{1}"}, "after": {"text": "{2}"}, "include_tree_diff": "true"}, "include_tree_diff"),
("payload.diff", {"diagnostic": True, "before": {"text": "{1}"}, "after": {"text": "{2}"}, "max_changes": "10"}, "max_changes"),
("codec.encode", {"base_id": "upo_test", "table": "Config", "file_name": "x", "text": "", "include_payload": "true"}, "include_payload"),
("query.validate", {"base_id": "upo_test", "query": ["select 1"]}, "query"),
("query.run", {"base_id": "upo_test", "query": ["select 1"]}, "query"),
("query.run", {"base_id": "upo_test", "query": "select 1", "limit": "1"}, "limit"),
("metadata.cache.lookup", {"base_id": "upo_test", "kind": ["Document"], "name": "X"}, "kind"),
("metadata.cache.lookup", {"base_id": "upo_test", "kind": "Document", "name": ["X"]}, "name"),
("metadata.cache.rebuild", {"base_id": "upo_test", "batch_size": "10"}, "batch_size"),
("metadata.cache.rebuild", {"base_id": "upo_test", "timeout_seconds": "10"}, "timeout_seconds"),
]
for method, payload, argument in cases:
result = call_method(method, payload)
assert result["status"] == "invalid_argument"
assert result["argument"] == argument
def test_replace_routine_text_append_and_replace() -> None:
text = "Процедура Один()\nКонецПроцедуры\n"
appended, append_edit = replace_routine_text(text, "Функция Два()\n\tВозврат 2;\nКонецФункции\n", operation="append")
replaced, replace_edit = replace_routine_text(appended, "Процедура Один()\n\tСообщить(1);\nКонецПроцедуры\n", operation="replace")
assert append_edit["status"] == "appended"
assert replace_edit["status"] == "replaced"
assert "Функция Два" in replaced
assert "Сообщить(1)" in replaced
def test_replace_routine_text_preserves_module_eol_style() -> None:
text = "#Область ПрограммныйИнтерфейс \r\n\r\nПроцедура Один()\r\n\tСообщить(0);\r\nКонецПроцедуры\r\n\r\nФункция Два()\r\n\tВозврат 2;\r\nКонецФункции\r\n"
replacement = "Процедура Один()\n\tСообщить(1);\nКонецПроцедуры\n"
replaced, edit = replace_routine_text(text, replacement, operation="replace")
assert edit["status"] == "replaced"
assert "#Область ПрограммныйИнтерфейс \r\n" in replaced
assert "Сообщить(1);\r\nКонецПроцедуры\r\n" in replaced
assert "Сообщить(1);\nКонецПроцедуры" not in replaced.replace("\r\n", "")
def test_replace_routine_text_preconditions_and_old_sha1() -> None:
text = "Процедура Один()\n\tСообщить(0);\nКонецПроцедуры\n"
replacement = "Процедура Один()\n\tСообщить(1);\nКонецПроцедуры\n"
replaced, edit = replace_routine_text(
text,
replacement,
operation="replace",
expected_old_contains="Сообщить(0)",
expected_old_sha1=text_sha1(text.rstrip("\n")),
)
assert edit["old_sha1"] == text_sha1(text.rstrip("\n"))
assert "Сообщить(1)" in replaced
with pytest.raises(ValueError, match="expected_old_contains"):
replace_routine_text(text, replacement, operation="replace", expected_old_contains="НетТакогоТекста")
with pytest.raises(ValueError, match="expected_old_sha1"):
replace_routine_text(text, replacement, operation="replace", expected_old_sha1="0" * 40)
def test_classify_root5_base64_html_payload() -> None:
html = b"help"
encoded = base64.b64encode(html).decode("ascii")
stored = compress_payload(f'{{5,1,"ru",{{#base64:{encoded}}}}}'.encode("utf-8-sig"), "raw_deflate")
classified = classify_payload(stored)
assert classified["role"] == "help_or_html_payload"
assert classified["root"]["root_marker"] == "5"
assert classified["base64_blocks"][0]["has_html_marker"] is True
def test_decode_form_payload_profile() -> None:
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{10,11111111-1111-1111-1111-111111111111,"ПриОткрытии"},0,0,'
'{22,{},0,0,0,9,"ФормаКоманднаяПанель",{},{}},0},'
'"#Область X\nПроцедура ПриОткрытии()\nКонецПроцедуры\nПроцедура Команда1(Команда)\nКонецПроцедуры",'
'{4,1,{22,{},0,0,0,9,"Объект",{},{} }},'
'{0,0},'
'{0,1,{22,{},0,0,0,9,"Команда1",{},{} }},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text))
assert profile["status"] == "ok"
assert profile["events"][0]["handler"] == "ПриОткрытии"
assert profile["items"][0]["name"] == "ФормаКоманднаяПанель"
assert profile["items"][0]["semantic"]["groups"]["Основные"][0]["name"] == "Идентификатор"
assert any(item["name"] == "Вид" and item["value"] == "Командная панель" for item in profile["items"][0]["semantic"]["groups"]["Основные"])
assert profile["items"][0]["semantic"]["coverage"]["status"] in {"ok", "partial"}
assert "unmapped_parameters" in profile["items"][0]["semantic"]
assert profile["attributes"][0]["name"] == "Объект"
assert profile["attributes"][0]["semantic"]["groups"]["Основные"][0]["name"] == "Идентификатор"
assert profile["commands"][0]["name"] == "Команда1"
assert profile["module"]["routine_count"] == 2
assert profile["handler_links"][0]["handler"] == "ПриОткрытии"
assert profile["handler_links"][0]["status"] == "resolved"
assert profile["counts"]["resolved_handlers"] == 1
assert profile["counts"]["missing_handlers"] == 0
assert profile["command_links"][0]["command"] == "Команда1"
assert profile["command_links"][0]["status"] == "resolved"
assert profile["counts"]["resolved_commands"] == 1
def test_decode_form_payload_exposes_input_fields_and_dynamic_list_columns() -> None:
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
'{37,{40,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,2,"ОформленоОрганизации",0,0,{1,1,{"ru","Организации"}},{1,0},{1,{13}}},0},'
'"Процедура ОформленоОрганизацииНачалоВыбора()\nКонецПроцедуры",'
'{4,2,'
'{9,{1},0,"Список",{1,1,{"ru","Список"}},{"Pattern"},0,0,0,0,{0,6,"FieldsMapItemId0",{"N",16},"FieldsMapItemName0",{"S","Number"},"FieldsMapItemSecondaryName0",{"S","Номер"}}},'
'{9,{13},0,"ОрганизацииСАТУРНПредставление",{1,1,{"ru","Организации представление"}},{"Pattern",{"S"}},0,0,0,0,{0,0}}'
'},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
by_name = {item["name"]: item for item in profile["items"]}
field_semantic = by_name["ОформленоОрганизации"]["semantic"]["groups"]["Основные"]
assert by_name["ОформленоОрганизации"]["type_name"] == "Поле ввода"
assert by_name["ОформленоОрганизации"]["path_to_data"] == "ОрганизацииСАТУРНПредставление"
assert any(item["name"] == "Вид" and item["value"] == "Поле ввода" for item in field_semantic)
assert any(item["name"] == "ПутьКДанным" and item["value"] == "ОрганизацииСАТУРНПредставление" for item in field_semantic)
column = by_name["Номер"]
assert column["type_name"] == "Колонка динамического списка"
column_semantic = column["semantic"]["groups"]["Основные"]
assert any(item["name"] == "ПутьКДанным" and item["value"] == "Список.Number" for item in column_semantic)
summary_profile = decode_form_payload(parse_brace_text(text), max_items=20, include_parameters=False)
summary_field = {item["name"]: item for item in summary_profile["items"]}["ОформленоОрганизации"]
assert "parameters" not in summary_field
assert summary_field["semantic"]["groups"]["Основные"]
assert "unmapped_parameters" not in summary_field["semantic"]
assert "coverage" not in summary_field["semantic"]
assert all("parameter_index" not in prop for props in summary_field["semantic"]["groups"].values() for prop in props)
assert all("source" not in prop for props in summary_field["semantic"]["groups"].values() for prop in props)
def test_decode_form_payload_exposes_marker_48_field_data_path() -> None:
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
'{48,{40,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,1,{0,{}},2,"ПолеА",1,0,{1,0},{1,0},{1,{13}}},0},'
'"",'
'{4,1,{9,{13},0,"А",{1,1,{"ru","А"}},{"Pattern",{"S"}},0,0,0,0,{0,0}}},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
field = {item["name"]: item for item in profile["items"]}["ПолеА"]
assert field["marker"] == "48"
assert field["type_name"] == "Поле ввода"
assert field["path_to_data"] == "А"
assert field["data_path_attribute_id"] == "13"
assert any(item["name"] == "ПутьКДанным" and item["value"] == "А" for item in field["semantic"]["groups"]["Основные"])
def test_form_payload_resolves_composite_tabular_field_data_path() -> None:
reference = data_path_reference_from_node(parse_brace_text("{2,{7},{1}}"))
assert reference == {"attribute_id": "7", "field_id": "1"}
table_attribute = parse_brace_text(
'{9,{7},0,"ТЗ",{1,1,{"ru","ТЗ"}},{"Pattern",{"#","acf6192e-81ca-46ef-93a6-5a6968b78663"}},'
'{0,{0,{"B",1},0}},{0,{0,{"B",1},0}},{0,0},3,'
'{5,1,0,"К1",{1,1,{"ru","К1"}},{"Pattern",{"N",10,0,0}},{0,{0,{"B",1},0}},{0,{0,{"B",1},0}},{0,0},0},'
'{5,2,0,"К2",{1,1,{"ru","К2"}},{"Pattern",{"N",10,0,0}},{0,{0,{"B",1},0}},{0,{0,{"B",1},0}},{0,0},0}}'
)
fields = dynamic_list_fields(table_attribute, owner_name="ТЗ")
assert [field["path_to_data"] for field in fields] == ["ТЗ.К1", "ТЗ.К2"]
items = [
{"marker": "48", "name": "ТЗК1", "data_path_attribute_id": "7", "data_path_field_id": "1", "semantic": {"groups": {}}},
{"marker": "48", "name": "А", "data_path_attribute_id": "2", "semantic": {"groups": {}}},
]
attributes = [
{"id": "7", "name": "ТЗ", "dynamic_list_fields": fields},
{"id": "2", "name": "А", "dynamic_list_fields": [{"id": "2", "path_to_data": "ТЗ.К2"}]},
]
enrich_item_data_paths(items, attributes)
assert items[0]["path_to_data"] == "ТЗ.К1"
assert items[1]["path_to_data"] == "А"
def test_tabular_attribute_field_keeps_internal_name_separate_from_title() -> None:
table_attribute = parse_brace_text(
'{9,{7},0,"ТЗ",{1,1,{"ru","ТЗ"}},{"Pattern"},0,0,0,0,0,0,0,1,'
'{5,967,0,"НомерВерсии",{1,1,{"ru","Номер версии"}},{"Pattern"},0,0,0,0}}'
)
fields = dynamic_list_fields(table_attribute, owner_name="ТЗ")
assert fields[0]["name"] == "НомерВерсии"
assert fields[0]["title"] == "Номер версии"
assert fields[0]["path_to_data"] == "ТЗ.НомерВерсии"
def test_decode_form_payload_exposes_input_field_usage_and_layout_properties() -> None:
specific_values = ["0"] * 66
specific_values[0] = "36"
specific_values[47] = "1"
specific_values[12] = "1"
specific_values[49] = "0"
specific_values[50] = "28"
specific_values[13] = "1"
specific_values[53] = "0"
specific_values[41] = "0"
specific_block = "{" + ",".join(specific_values) + "}"
direct_values = ["0"] * 27
direct_values[3] = "2"
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
'{37,{40,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,2,"ОформленоОрганизации",0,0,{1,1,{"ru","Организации"}},{1,0},{1,{13}},'
+ ",".join(direct_values)
+ ","
+ specific_block
+ '},0},'
'"",'
'{4,1,{9,{13},0,"ОрганизацииСАТУРНПредставление",{1,1,{"ru","Организации представление"}},{"Pattern",{"S"}},0,0,0,0,{0,0}}},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
field = {item["name"]: item for item in profile["items"]}["ОформленоОрганизации"]
assert field["type_name"] == "Поле ввода"
basic = field["semantic"]["groups"]["Основные"]
usage = field["semantic"]["groups"]["Использование"]
layout = field["semantic"]["groups"]["Расположение"]
assert any(item["name"] == "ПоложениеЗаголовка" and item["value"] == "None" for item in basic)
assert any(item["name"] == "КнопкаВыпадающегоСписка" and item["value"] is True for item in usage)
assert any(item["name"] == "КнопкаВыбора" and item["value"] is True for item in usage)
assert any(item["name"] == "КнопкаОчистки" and item["value"] is True for item in usage)
assert any(item["name"] == "МаксимальнаяШирина" and item["value"] == "28" for item in layout)
unmapped_indexes = {item["index"] for item in field["semantic"]["unmapped_parameters"]}
assert not ({7, 12, 13, 47, 50} & unmapped_indexes)
assert field["semantic"]["coverage"]["mapped"] >= 9
def test_decode_form_payload_distinguishes_label_field_subtype() -> None:
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
'{37,{40,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,1,"Номер",1,0,{1,1,{"ru","Номер"}},{1,0},{1,{3}}},0},'
'"",'
'{4,1,{9,{3},0,"Number",{1,1,{"ru","Номер"}},{"Pattern",{"S"}},0,0,0,0,{0,0}}},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20, include_parameters=False)
field = {item["name"]: item for item in profile["items"]}["Номер"]
assert field["type_name"] == "Поле надписи"
assert any(item["name"] == "Вид" and item["value"] == "Поле надписи" for item in field["semantic"]["groups"]["Основные"])
assert field["path_to_data"] == "Number"
assert any(item["name"] == "ПутьКДанным" and item["value"] == "Number" for item in field["semantic"]["groups"]["Основные"])
def test_decode_form_payload_exposes_command_button_semantic_properties() -> None:
values = ["0"] * 52
values[0] = "31"
values[5] = '"СписокАрхивироватьДокументы"'
values[7] = "1"
values[10] = "3"
values[11] = "1"
values[14] = "2"
values[26] = "1"
values[31] = "1"
values[41] = "3"
values[42] = "3"
values[43] = "3"
values[49] = "1"
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20, include_parameters=False)
button = {item["name"]: item for item in profile["items"]}["СписокАрхивироватьДокументы"]
groups = button["semantic"]["groups"]
assert any(item["name"] == "Вид" and item["value"] == "Кнопка командной панели" for item in groups["Основные"])
assert any(item["name"] == "Видимость" and item["value"] is True for item in groups["Основные"])
assert any(item["name"] == "Доступность" and item["value"] is True for item in groups["Основные"])
assert any(item["name"] == "Отображение" and item["value"] == "Авто" for item in groups["Основные"])
assert any(item["name"] == "ButtonImportance" and item["value"] == "Main" for item in groups["Основные"])
assert any(item["name"] == "ПоложениеВКоманднойПанели" and item["value"] == "В дополнительном подменю" for item in groups["Расположение"])
assert any(item["name"] == "УникальностьКоманды" and item["value"] is True for item in groups["Расположение"])
assert any(item["name"] == "GroupHorizontalAlign" and item["value"] == "Auto" for item in groups["Расположение"])
assert any(item["name"] == "GroupVerticalAlign" and item["value"] == "Auto" for item in groups["Расположение"])
assert all("parameter_index" not in prop for props in groups.values() for prop in props)
assert all("source" not in prop for props in groups.values() for prop in props)
def test_enrich_command_button_decodes_importance_and_group_alignment() -> None:
from parser.form_payload import enrich_command_button_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(52)]
values[0] = atom(31)
values[11] = atom(0)
values[41] = atom(2)
values[42] = atom(2)
row = {"marker": "31", "semantic": {"groups": {}}}
enrich_command_button_specific_semantics(row, seq(*values))
basic = {item["name"]: item["value"] for item in row["semantic"]["groups"]["Основные"]}
layout = {item["name"]: item["value"] for item in row["semantic"]["groups"]["Расположение"]}
assert basic["ButtonImportance"] == "Supplementary"
assert layout["GroupHorizontalAlign"] == "Right"
assert layout["GroupVerticalAlign"] == "Bottom"
@pytest.mark.parametrize(("secondary_slot", "expected"), [("0", "В дополнительном подменю"), ("1", "InCommandBar")])
def test_enrich_command_button_uses_secondary_location_slot_for_compact_value(
secondary_slot: str,
expected: str,
) -> None:
from parser.form_payload import enrich_command_button_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(52)]
values[0] = atom(31)
values[14] = atom(2)
values[15] = atom(secondary_slot)
row = {"marker": "31", "semantic": {"groups": {}}}
enrich_command_button_specific_semantics(row, seq(*values))
layout = {item["name"]: item["value"] for item in row["semantic"]["groups"]["Расположение"]}
assert layout["ПоложениеВКоманднойПанели"] == expected
def test_enrich_decoration_decodes_group_alignment() -> None:
from parser.form_payload import enrich_decoration_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(36)]
values[0] = atom(12)
values[32] = atom(2)
values[33] = atom(1)
row = {"marker": "12", "semantic": {"groups": {}}}
enrich_decoration_specific_semantics(row, seq(*values))
layout = {item["name"]: item["value"] for item in row["semantic"]["groups"]["Расположение"]}
assert layout["GroupHorizontalAlign"] == "Right"
assert layout["GroupVerticalAlign"] == "Center"
def test_enrich_decoration_decodes_size_stretch_and_colors() -> None:
from parser.form_payload import enrich_decoration_specific_semantics
node = parse_brace_text(
'{12,{1},0,0,0,0,"Надпись",{1,0},{1,0},1,15,0,1,2,{3,3,{-21}},{7,3,0,1,100},'
'{0,0,0},1,{5,0,0,3,0,{0,1,0},{3,3,{-35}},{3,4,{0}},{3,0,{0},0,1,0,00000000-0000-0000-0000-000000000000}}}'
)
row = {"marker": "12", "semantic": {"groups": {}}}
enrich_decoration_specific_semantics(row, node)
props = {item["name"]: item["value"] for values in row["semantic"]["groups"].values() for item in values}
assert props["Ширина"] == "15"
assert props["РастягиватьПоГоризонтали"] is True
assert props["ЦветТекста"] == "style:ButtonTextColor"
assert props["ЦветФона"] == "style:TableHeaderBackColor"
def test_enrich_decoration_decodes_extended_layout_slots() -> None:
from parser.form_payload import enrich_decoration_specific_semantics
values = ["0"] * 36
values[0] = "12"
values[6] = '"Шаг"'
values[11] = "2"
values[18] = "{5,0,1,3,0,{0},{3,4,{0}},{3,4,{0}},{0}}"
values[21] = "0"
values[27] = "0"
values[28] = "240"
values[29] = "0"
values[31] = "3"
row = {"marker": "12", "semantic": {"groups": {}}}
enrich_decoration_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {item["name"]: item["value"] for items in row["semantic"]["groups"].values() for item in items}
assert props["Высота"] == "2"
assert props["Видимость"] is False
assert props["АвтоМаксимальнаяШирина"] is False
assert props["МаксимальнаяШирина"] == "240"
assert props["АвтоМаксимальнаяВысота"] is False
assert props["МаксимальнаяВысота"] == "3"
assert props["ГоризонтальноеПоложениеВГруппе"] == "Center"
def test_enrich_radio_button_decodes_tumbler_type() -> None:
from parser.form_payload import enrich_radio_button_specific_semantics
values = ["0"] * 40
values[0] = "37"
values[5] = "5"
values[6] = '"Вариант"'
values[39] = "{8,{0},0,{3,4,{0}},{7,3,0,1,100},{3,4,{0}},0,2,0,0,0,2}"
row = {"marker": "37", "type_code": "5", "semantic": {"groups": {}}}
enrich_radio_button_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
assert row["semantic"]["groups"]["Прочее"][0]["value"] == "Tumbler"
def test_enrich_input_field_decodes_vertical_group_alignment() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
values = [atom(0) for _ in range(59)]
values[0] = atom(37)
values[53] = atom(3)
values[54] = atom(0)
row = {"marker": "37", "semantic": {"groups": {}}}
enrich_input_field_specific_semantics(row, seq(*values))
layout = {item["name"]: item["value"] for item in row["semantic"]["groups"]["Расположение"]}
assert layout["GroupHorizontalAlign"] == "Auto"
assert layout["GroupVerticalAlign"] == "Top"
def test_enrich_container_decodes_variable_tail_alignment_positions() -> None:
from parser.form_payload import enrich_container_specific_semantics
atom = lambda value: {"type": "atom", "value": str(value)}
seq = lambda *items: {"type": "list", "items": list(items)}
cases = (
(32, "Группа", 2, "Bottom"),
(32, "Командная панель", 1, "Center"),
(34, "Группа колонок", 1, "Center"),
(35, "Страницы", 1, "Center"),
(38, "Группа", 0, "Top"),
)
for length, type_name, vertical_code, expected in cases:
values = [atom(0) for _ in range(length)]
values[0] = atom(22)
values[length - 3] = atom(3)
values[length - 2] = atom(vertical_code)
row = {"marker": "22", "type_name": type_name, "semantic": {"groups": {}}}
enrich_container_specific_semantics(row, seq(*values))
layout = {item["name"]: item["value"] for item in row["semantic"]["groups"]["Расположение"]}
assert layout["GroupHorizontalAlign"] == "Auto"
assert layout["GroupVerticalAlign"] == expected
def test_decode_form_payload_exposes_page_group_and_dynamic_list_semantics() -> None:
page_values = ["0"] * 22
page_values[0] = "22"
page_values[5] = "4"
page_values[6] = '"СтраницаОформлено"'
page_values[10] = "1"
group_values = ["0"] * 22
group_values[0] = "22"
group_values[5] = "5"
group_values[6] = '"СтраницаОформленоОтбор"'
group_values[10] = "1"
list_values = ["0"] * 22
list_values[0] = "55"
list_values[5] = '"Список"'
list_values[13] = "1"
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(page_values)
+ "},{"
+ ",".join(group_values)
+ "},{"
+ ",".join(list_values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20, include_parameters=False)
by_name = {item["name"]: item for item in profile["items"]}
page = by_name["СтраницаОформлено"]
page_basic = page["semantic"]["groups"]["Основные"]
assert page["type_name"] == "Страница"
assert any(item["name"] == "Вид" and item["value"] == "Страница" for item in page_basic)
assert any(item["name"] == "Видимость" and item["value"] is True for item in page_basic)
group = by_name["СтраницаОформленоОтбор"]
group_basic = group["semantic"]["groups"]["Основные"]
assert group["type_name"] == "Группа"
assert any(item["name"] == "Вид" and item["value"] == "Группа" for item in group_basic)
assert any(item["name"] == "Видимость" and item["value"] is True for item in group_basic)
dynamic_list = by_name["Список"]
list_basic = dynamic_list["semantic"]["groups"]["Основные"]
assert dynamic_list["type_name"] == "Динамический список"
assert any(item["name"] == "Вид" and item["value"] == "Динамический список" for item in list_basic)
assert any(item["name"] == "Видимость" and item["value"] is True for item in list_basic)
assert all("parameter_index" not in prop for row in by_name.values() for props in row["semantic"]["groups"].values() for prop in props)
assert all("source" not in prop for row in by_name.values() for props in row["semantic"]["groups"].values() for prop in props)
def test_decode_form_payload_handles_shifted_container_type_and_name() -> None:
group_values = ["0"] * 22
group_values[0] = "22"
group_values[5] = '""'
group_values[6] = "5"
group_values[7] = '"ГруппаГоризонтальная"'
group_values[10] = "1"
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(group_values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
group = profile["items"][0]
assert group["name"] == "ГруппаГоризонтальная"
assert group["name_path"] == "1.30.7"
assert group["type_code"] == "5"
assert group["type_name"] == "Группа"
def test_decode_form_payload_exposes_extended_tooltip_items() -> None:
tooltip_values = ["0"] * 20
tooltip_values[0] = "12"
tooltip_values[1] = "{40,00000000-0000-0000-0000-000000000068}"
tooltip_values[2] = "0"
tooltip_values[3] = "0"
tooltip_values[4] = "0"
tooltip_values[5] = "0"
tooltip_values[6] = '"ПолеРасширеннаяПодсказка"'
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(tooltip_values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
tooltip = profile["items"][0]
assert tooltip["name"] == "ПолеРасширеннаяПодсказка"
assert tooltip["name_path"] == "1.30.6"
assert tooltip["marker"] == "12"
assert tooltip["type_code"] == "0"
assert tooltip["type_name"] == "Расширенная подсказка"
assert any(item["name"] == "Имя" and item["value"] == "ПолеРасширеннаяПодсказка" for item in tooltip["semantic"]["groups"]["Основные"])
def test_decode_form_payload_reads_form_events_from_common_section() -> None:
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,'
'{3,3ccc650e-f631-4cae-8e33-3eaac610b5f9,"ПриОткрытии",'
'9f2e5ddb-3492-4f5d-8f0d-416b8d1d5c5b,"",'
'e773807c-0c0c-4689-a093-231ddcd6409f,"ПередЗагрузкойДанныхИзНастроекНаСервере",'
'1,0,3ccc650e-f631-4cae-8e33-3eaac610b5f9,0,1,9f2e5ddb-3492-4f5d-8f0d-416b8d1d5c5b,0,1,e773807c-0c0c-4689-a093-231ddcd6409f,0,1},'
'0,0,0,0,0,{},0,{},'
'{0},0,0,0,0},'
'"Процедура ПриОткрытии(Отказ)\nКонецПроцедуры\nПроцедура ПередЗагрузкойДанныхИзНастроекНаСервере(Настройки)\nКонецПроцедуры",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
events = {event["name"]: event for event in profile["events"]}
assert events["OnOpen"]["handler"] == "ПриОткрытии"
assert events["OnOpen"]["path"] == "1.18.2"
assert events["BeforeLoadDataFromSettingsAtServer"]["handler"] == "ПередЗагрузкойДанныхИзНастроекНаСервере"
assert "OnCreateAtServer" not in events
def test_decode_form_payload_exposes_table_additions_and_buttons() -> None:
search_values = ["0"] * 25
search_values[0] = "6"
search_values[1] = "{40,00000000-0000-0000-0000-000000000024}"
search_values[5] = "0"
search_values[6] = '"ТЗСтрокаПоиска"'
button_values = ["0"] * 60
button_values[0] = "34"
button_values[1] = "{40,00000000-0000-0000-0000-000000000057}"
button_values[5] = "0"
button_values[6] = '"ТЗИзменитьФорму"'
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(search_values)
+ "},{"
+ ",".join(button_values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
by_name = {item["name"]: item for item in profile["items"]}
assert by_name["ТЗСтрокаПоиска"]["marker"] == "6"
assert by_name["ТЗСтрокаПоиска"]["type_code"] == "0"
assert by_name["ТЗСтрокаПоиска"]["type_name"] == "SearchStringAddition"
assert by_name["ТЗИзменитьФорму"]["marker"] == "34"
assert by_name["ТЗИзменитьФорму"]["type_name"] == "Кнопка"
def test_decode_form_payload_derives_child_item_references() -> None:
table_values = ["0"] * 24
table_values[0] = "73"
table_values[1] = "{40,00000000-0000-0000-0000-000000000020}"
table_values[6] = '"ТЗ"'
command_bar_values = ["0"] * 24
command_bar_values[0] = "22"
command_bar_values[1] = "{40,00000000-0000-0000-0000-000000000022}"
command_bar_values[5] = "9"
command_bar_values[6] = '"ТЗКоманднаяПанель"'
menu_values = ["0"] * 24
menu_values[0] = "22"
menu_values[1] = "{40,00000000-0000-0000-0000-000000000021}"
menu_values[5] = "8"
menu_values[6] = '"ТЗКонтекстноеМеню"'
tooltip_values = ["0"] * 20
tooltip_values[0] = "12"
tooltip_values[1] = "{40,00000000-0000-0000-0000-000000000023}"
tooltip_values[6] = '"ТЗРасширеннаяПодсказка"'
search_values = ["0"] * 25
search_values[0] = "6"
search_values[1] = "{40,00000000-0000-0000-0000-000000000024}"
search_values[5] = "0"
search_values[6] = '"ТЗСтрокаПоиска"'
table_values[13] = "{" + ",".join(command_bar_values) + "}"
table_values[14] = "{" + ",".join(menu_values) + "}"
table_values[15] = "{" + ",".join(tooltip_values) + "}"
table_values[16] = "{" + ",".join(search_values) + "}"
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(table_values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
table = {item["name"]: item for item in profile["items"]}["ТЗ"]
props = {prop["name"]: prop["value"] for props in table["semantic"]["groups"].values() for prop in props}
assert props["AutoCommandBar"] == "ТЗКоманднаяПанель"
assert props["КонтекстноеМеню"] == "ТЗКонтекстноеМеню"
assert props["РасширеннаяПодсказка"] == "ТЗРасширеннаяПодсказка"
assert props["SearchStringAddition"] == "ТЗСтрокаПоиска"
def test_decode_form_payload_exposes_section_kind_and_command_action() -> None:
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,0,0},'
'"",'
'{2,1,{9,{13},0,"Объект",{1,1,{"ru","Объект"}}}},'
'{0,0},'
'{2,1,{11,{13},0,"КомандаПрименить",{1,1,{"ru","Применить"}}}},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
attribute_props = {prop["name"]: prop["value"] for props in profile["attributes"][0]["semantic"]["groups"].values() for prop in props}
command_props = {prop["name"]: prop["value"] for props in profile["commands"][0]["semantic"]["groups"].values() for prop in props}
assert attribute_props["Вид"] == "Attribute"
assert attribute_props["MainAttribute"] is True
assert command_props["Вид"] == "Command"
assert command_props["Action"] == "КомандаПрименить"
def test_form_command_uses_explicit_action_parameter_for_handler_binding() -> None:
row = {"category": "Command", "name": "КомандаФормы"}
parameters = [
{"index": 2, "value": "КомандаФормы"},
{"index": 8, "value": "ВыполнитьКоманду"},
]
semantic = section_record_semantic_properties(row, parameters)
links = command_handler_links(
[row],
{"routines_sample": [{"name": "ВыполнитьКоманду"}]},
)
action = next(
prop["value"]
for prop in semantic["groups"]["Основные"]
if prop["name"] == "Action"
)
assert action == "ВыполнитьКоманду"
assert row["action"] == "ВыполнитьКоманду"
assert row["action_source"] == "form_payload_parameter_8"
assert semantic["coverage"]["mapped"] == 2
assert links[0]["handler"] == "ВыполнитьКоманду"
assert links[0]["status"] == "resolved"
assert links[0]["match_by"] == "action"
def test_form_section_records_decode_saved_data_and_current_row_properties() -> None:
command = section_record_semantic_properties(
{"category": "Command", "name": "ЗаписатьИЗакрыть"},
[
{"index": 8, "value": "ЗаписатьИЗакрытьВыполнить"},
{"index": 9, "value": "3"},
{"index": 10, "value": "1"},
],
)
attribute = section_record_semantic_properties(
{"category": "Attribute", "index": 1, "name": "ПредставлениеФайлаОтвета"},
[
{"index": 10, "value": "0"},
{"index": 11, "value": "1"},
],
)
command_props = {prop["name"]: prop for props in command["groups"].values() for prop in props}
attribute_props = {prop["name"]: prop for props in attribute["groups"].values() for prop in props}
assert command_props["CurrentRowUse"]["value"] == "DontUse"
assert command_props["CurrentRowUse"]["parameter_index"] == 9
assert command_props["ModifiesSavedData"]["value"] is True
assert command_props["ModifiesSavedData"]["parameter_index"] == 10
assert attribute_props["MainAttribute"]["value"] is False
assert attribute_props["SavedData"]["value"] is True
assert attribute_props["SavedData"]["parameter_index"] == 11
def test_form_table_additions_expose_structured_addition_source() -> None:
records = [
{"name": "ТЗ", "marker": "73", "type_name": "Таблица формы", "path": "1.25.10", "semantic": {"groups": {}}},
{"name": "ТЗСтрокаПоиска", "marker": "6", "type_name": "SearchStringAddition", "path": "1.25.10.20", "semantic": {"groups": {}}},
]
enrich_item_reference_semantics(records)
source = records[1]["addition_source"]
props = {prop["name"]: prop["value"] for props in records[1]["semantic"]["groups"].values() for prop in props}
assert source == {"owner_element": "ТЗ", "representation": "SearchStringRepresentation"}
assert props["AdditionSource"] == source
def test_embedded_dynamic_list_tail_addition_is_normalized_as_public_item() -> None:
node = parse_brace_text(
'{5,{7,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,0,"СписокСтрокаПоиска",'
'{1,0},{1,0},0,0,0,1,{},1,{},1,{},2,{3,0},0,3,3,0}'
)
row = embedded_table_addition_row(
node,
"1.27.116",
2,
include_parameters=True,
max_parameters=100,
)
assert row is not None
assert row["marker"] == "6"
assert row["type_code"] == "0"
assert row["type_name"] == "SearchStringAddition"
assert row["name"] == "СписокСтрокаПоиска"
assert row["id"] == "7"
props = {prop["name"]: prop for props in row["semantic"]["groups"].values() for prop in props}
assert props["Видимость"]["value"] is False
assert props["Доступность"]["value"] is False
def test_form_command_reports_missing_explicit_action_handler() -> None:
row = {"category": "Command", "name": "КомандаФормы"}
section_record_semantic_properties(row, [{"index": 8, "value": "НеНайденныйОбработчик"}])
link = command_handler_links([row], {"routines_sample": []})[0]
assert link["status"] == "missing"
assert link["match_by"] == "action"
def test_form_command_handler_resolution_uses_all_module_routine_names() -> None:
row = {"category": "Command", "name": "КомандаФормы"}
section_record_semantic_properties(row, [{"index": 8, "value": "ПозднийОбработчик"}])
module = {
"routine_count": 81,
"routine_names": [*[f"Процедура{index}" for index in range(80)], "ПозднийОбработчик"],
"routines_sample": [{"name": f"Процедура{index}"} for index in range(80)],
}
link = command_handler_links([row], module)[0]
assert link["status"] == "resolved"
assert link["handler"] == "ПозднийОбработчик"
def test_decode_form_payload_exposes_marker_48_field_details() -> None:
details_values = ["0"] * 71
details_values[0] = "38"
details_values[2] = "5"
details_values[3] = "8"
details_values[8] = "1"
details_values[49] = "0"
field_values = ["0"] * 75
field_values[0] = "48"
field_values[1] = "{40,00000000-0000-0000-0000-000000000012}"
field_values[6] = "2"
field_values[7] = '"КодПрограммы"'
field_values[40] = "{" + ",".join(details_values) + "}"
field_values[62] = "3"
field_values[63] = "1"
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(field_values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
props = {prop["name"]: prop["value"] for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}
assert props["Ширина"] == "5"
assert props["Высота"] == "8"
assert props["АвтоМаксимальнаяШирина"] is False
assert props["MultiLine"] is True
assert props["РежимРедактирования"] == "EnterOnInput"
assert props["AutoEditMode"] is True
def test_decode_form_payload_exposes_group_layout_semantics() -> None:
group_values = ["0"] * 35
group_values[0] = "22"
group_values[1] = "{40,00000000-0000-0000-0000-000000000067}"
group_values[5] = '""'
group_values[6] = "5"
group_values[7] = '"ГруппаГоризонтальная"'
group_values[10] = "0"
group_values[22] = "2"
group_values[23] = "cd5394d0-7dda-4b56-8927-93ccbe967a01"
group_values[25] = "cd5394d0-7dda-4b56-8927-93ccbe967a01"
group_values[27] = "1"
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(group_values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
props = {prop["name"]: prop["value"] for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}
assert props["Группировка"] == "AlwaysHorizontal"
assert props["Поведение"] == "Usual"
assert props["Отображение"] == "None"
assert props["ПоказыватьЗаголовок"] is False
def test_decode_form_payload_reads_live_group_layout_slots() -> None:
def group_props(values: list[str]) -> dict[str, object]:
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
return {prop["name"]: prop for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}
vertical_values = ["0"] * 35
vertical_values[0] = "22"
vertical_values[1] = "{40,00000000-0000-0000-0000-000000000065}"
vertical_values[5] = "5"
vertical_values[6] = '"ГруппаФорма"'
vertical_values[10] = "1"
vertical_values[21] = "2"
vertical_values[22] = "cd5394d0-7dda-4b56-8927-93ccbe967a01"
vertical_values[24] = "143c00f7-a42d-4cd7-9189-88e4467dc768"
vertical_values[26] = "1"
vertical_props = group_props(vertical_values)
horizontal_values = ["0"] * 35
horizontal_values[0] = "22"
horizontal_values[1] = "{40,00000000-0000-0000-0000-000000000001}"
horizontal_values[5] = "5"
horizontal_values[6] = '"Группа1"'
horizontal_values[10] = "1"
horizontal_values[21] = "3"
horizontal_values[22] = "77ffcc29-7f2d-4223-b22f-19666e7250ba"
horizontal_values[24] = "77ffcc29-7f2d-4223-b22f-19666e7250ba"
horizontal_values[26] = "77ffcc29-7f2d-4223-b22f-19666e7250ba"
horizontal_props = group_props(horizontal_values)
assert vertical_props["Группировка"]["value"] == "Vertical"
assert vertical_props["Группировка"]["parameter_index"] == 21
assert vertical_props["Отображение"]["value"] == "WeakSeparation"
assert vertical_props["Отображение"]["parameter_index"] == 24
assert horizontal_props["Группировка"]["value"] == "HorizontalIfPossible"
assert horizontal_props["Отображение"]["value"] == "None"
assert horizontal_props["ПоказыватьЗаголовок"]["value"] is False
def test_decode_form_payload_exposes_table_layout_semantics() -> None:
table_values = ["0"] * 60
table_values[0] = "73"
table_values[1] = "{40,00000000-0000-0000-0000-000000000020}"
table_values[6] = '"ТЗ"'
table_values[31] = "2"
table_values[33] = "0"
table_values[34] = "0"
table_values[35] = "0"
table_values[39] = "2"
table_values[42] = "1"
table_values[43] = "1"
table_values[53] = "1"
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(table_values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
props = {prop["name"]: prop["value"] for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}
assert props["Отображение"] == "List"
assert props["HeightInTableRows"] == "2"
assert props["RowSelectionMode"] == "Cell"
assert props["HorizontalLinesBWA"] is False
assert props["VerticalLinesBWA"] is False
assert props["UseAlternationRowColorBWA"] is False
assert props["AutoInsertNewRow"] is True
assert props["EnableStartDrag"] is True
assert props["EnableDrag"] is True
def test_decode_form_payload_exposes_dynamic_table_confirmed_semantics() -> None:
table_values = ["0"] * 60
table_values[0] = "55"
table_values[1] = "{40,00000000-0000-0000-0000-000000000020}"
table_values[5] = '"РаботыИУслуги"'
table_values[13] = "1"
table_values[14] = "1"
table_values[15] = "0"
table_values[17] = "0"
table_values[21] = "5"
table_values[28] = "1"
table_values[31] = "2"
table_values[33] = "1"
table_values[34] = "0"
table_values[35] = "0"
table_values[41] = "1"
table_values[42] = "1"
table_values[53] = "1"
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(table_values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
props = {prop["name"]: prop["value"] for group in profile["items"][0]["semantic"]["groups"].values() for prop in group}
assert props["Вид"] == "Динамический список"
assert props["ТолькоПросмотр"] is True
assert props["ПропускатьПриВводе"] is False
assert props["ChangeRowSet"] is False
assert props["Отображение"] == "List"
assert props["HeightInTableRows"] == "5"
assert props["Footer"] is True
assert props["RowSelectionMode"] == "Cell"
assert props["HorizontalLinesBWA"] is True
assert props["VerticalLinesBWA"] is False
assert props["UseAlternationRowColorBWA"] is False
assert props["AutoInsertNewRow"] is True
assert props["EnableStartDrag"] is True
assert props["EnableDrag"] is True
def test_decode_form_payload_exposes_dynamic_table_relative_tail_locations() -> None:
table_values = ["0"] * 90
table_values[0] = "55"
table_values[1] = "{40,00000000-0000-0000-0000-000000000020}"
table_values[5] = '"Список"'
table_values[64] = "{12,0}"
table_values[65] = "6"
table_values[66] = "0"
table_values[67] = "2"
table_values[69] = "{5,0}"
table_values[71] = "{5,0}"
table_values[73] = "{5,0}"
table_values[88] = "0"
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(table_values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
props = {prop["name"]: prop for group in profile["items"][0]["semantic"]["groups"].values() for prop in group}
assert props["SearchStringLocation"]["value"] == "PullFromTop"
assert props["SearchStringLocation"]["parameter_index"] == 65
assert props["ViewStatusLocation"]["value"] == "Default"
assert props["SearchControlLocation"]["value"] == "CommandBar"
assert props["FileDragMode"]["value"] == "AsFile"
assert props["FileDragMode"]["parameter_index"] == 88
def test_dynamic_list_decodes_live_property_bag_and_references() -> None:
from parser.form_payload import enrich_dynamic_list_specific_semantics, enrich_item_reference_semantics
values = ["0"] * 78
values[0] = "55"
values[5] = '"Список"'
values[6] = "0"
values[13] = "1"
values[16] = "1"
values[54] = "10"
pairs = {
55: ("5", '{"B",0}'),
57: ("6", '{"N",60}'),
59: ("7", '{"#",2fdc88ec-7c9b-43cd-8ba5-873f043bdd88,{0,00010101000000,00010101000000}}'),
61: ("8", '{"#",59ef2b80-c86b-11d5-a3c1-0050bae0a776,0}'),
63: ("9", '{"B",0}'),
65: ("10", '{"U"}'),
67: ("11", '{"B",1}'),
69: ("12", '{"B",0}'),
71: ("14", '{"#",eac7bfa0-10b4-4369-996c-d258871ad519,0}'),
73: ("16", '{"U"}'),
}
for index, (key, value) in pairs.items():
values[index] = key
values[index + 1] = value
values[75] = "{0,1,0}"
values[76] = "{0}"
values[77] = "1"
node = parse_brace_text("{" + ",".join(values) + "}")
row = {
"marker": "55",
"name": "Список",
"path": "1.27",
"semantic": {"groups": {}, "unmapped_parameters": [], "coverage": {}},
}
enrich_dynamic_list_specific_semantics(row, node)
enrich_item_reference_semantics([
{"id": "1", "name": "НастройкиПользователя", "path": "1.25", "semantic": {"groups": {}}},
row,
])
props = {prop["name"]: prop for group in row["semantic"]["groups"].values() for prop in group}
assert props["ПоложениеКоманднойПанели"]["value"] == "None"
assert props["АктивизироватьПоУмолчанию"]["value"] is True
assert props["AutoRefresh"]["value"] is False
assert props["AutoRefreshPeriod"]["value"] == "60"
assert props["ChoiceFoldersAndItems"]["value"] == "Items"
assert props["RestoreCurrentRow"]["value"] is False
assert props["ShowRoot"]["value"] is True
assert props["AllowRootChoice"]["value"] is False
assert props["UpdateOnDataChange"]["value"] == "Auto"
assert props["AllowGettingCurrentRowURL"]["value"] is True
assert props["InitialTreeView"]["value"] == "ExpandTopLevel"
assert props["RowPictureDataPath"]["value"] == "Список.DefaultPicture"
assert props["UserSettingsGroup"]["value"] == "НастройкиПользователя"
def test_compact_command_bar_and_label_field_tail_properties() -> None:
from parser.form_payload import enrich_container_specific_semantics, enrich_input_field_specific_semantics
command_values = ["0"] * 29
command_values[0] = "22"
command_values[5] = "9"
command_values[6] = '"КоманднаяПанель"'
command_values[10] = "1"
command_values[28] = "0"
command_row = {"marker": "22", "type_name": "Командная панель", "semantic": {"groups": {}}}
enrich_container_specific_semantics(command_row, parse_brace_text("{" + ",".join(command_values) + "}"))
field_values = ["0"] * 57
field_values[0] = "35"
field_values[5] = "1"
field_values[6] = '"ДатаСоздания"'
field_values[39] = "{11,0,0,2,2,2,{1,0},0,{0},{0},{0},2,{0,1,0},{0},{0},0,8,0,1,0}"
field_row = {"marker": "35", "type_code": "1", "semantic": {"groups": {}}}
enrich_input_field_specific_semantics(field_row, parse_brace_text("{" + ",".join(field_values) + "}"))
command_props = {prop["name"]: prop for group in command_row["semantic"]["groups"].values() for prop in group}
field_props = {prop["name"]: prop for group in field_row["semantic"]["groups"].values() for prop in group}
assert command_props["Автозаполнение"]["value"] is False
assert command_props["Автозаполнение"]["parameter_index"] == 28
assert field_props["АвтоМаксимальнаяШирина"]["value"] is False
assert field_props["МаксимальнаяШирина"]["value"] == "8"
assert field_props["МаксимальнаяШирина"]["parameter_index"] == 39
def test_compact_base_group_decodes_vertical_weak_separation_without_title() -> None:
from parser.form_payload import enrich_container_specific_semantics
values = ["0"] * 30
values[0] = "22"
values[5] = "5"
values[6] = '"НастройкиПользователя"'
values[10] = "1"
values[21] = "0"
values[22] = "1"
values[24] = "1"
row = {"marker": "22", "type_name": "Группа", "semantic": {"groups": {}}}
enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
assert props["Группировка"] == "Vertical"
assert props["Отображение"] == "WeakSeparation"
assert props["ПоказыватьЗаголовок"] is False
@pytest.mark.parametrize(
("options", "expected_group", "expected_representation"),
[
("{29,0,0,0,0}", "Vertical", "None"),
("{29,1,0,0,0}", "AlwaysHorizontal", "None"),
("{29,1,0,2,1}", "HorizontalIfPossible", "WeakSeparation"),
("{29,1,0,2,0,{0},{1,0},{0},0,{0},0,0,0,1,{1,0},0,0,3,3,2,0,0,2}", "HorizontalIfPossible", "WeakSeparation"),
("{29,0,0,3,0}", "Vertical", "NormalSeparation"),
],
)
def test_managed_group_options_decode_grouping_and_representation(
options: str,
expected_group: str,
expected_representation: str,
) -> None:
from parser.form_payload import enrich_container_specific_semantics
values = ["0"] * 30
values[0] = "22"
values[5] = "5"
values[6] = '"Группа"'
values[10] = "1"
values[20] = options
row = {"marker": "22", "type_name": "Группа", "semantic": {"groups": {}}}
enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
assert props["Группировка"] == expected_group
assert props["Отображение"] == expected_representation
assert props["ПоказыватьЗаголовок"] is False
def test_report_page_and_managed_group_decode_horizontal_layout() -> None:
from parser.form_payload import enrich_container_specific_semantics
page_values = ["0"] * 30
page_values[0] = "22"
page_values[5] = "6"
page_values[10] = "1"
page_values[20] = '{18,{3,4,{0}},1,0,{0},{1,0},1,{"Pattern"},"",{3,4,{0}},0,0,3,3,0,0,1}'
page_row = {"marker": "22", "type_name": "Страница", "semantic": {"groups": {}}}
enrich_container_specific_semantics(page_row, parse_brace_text("{" + ",".join(page_values) + "}"))
options = ["0"] * 28
options[0] = "29"
options[1] = "1"
options[2] = "5"
options[27] = "1"
group_values = ["0"] * 30
group_values[0] = "22"
group_values[5] = "5"
group_values[10] = "1"
group_values[20] = "{" + ",".join(options) + "}"
group_row = {"marker": "22", "type_name": "Группа", "semantic": {"groups": {}}}
enrich_container_specific_semantics(group_row, parse_brace_text("{" + ",".join(group_values) + "}"))
page_props = {prop["name"]: prop["value"] for group in page_row["semantic"]["groups"].values() for prop in group}
group_props = {prop["name"]: prop["value"] for group in group_row["semantic"]["groups"].values() for prop in group}
assert page_props["Группировка"] == "Horizontal"
assert group_props["Группировка"] == "Horizontal"
assert group_props["ChildItemsWidth"] == "LeftNarrowest"
def test_command_bar_decodes_form_command_source_guid() -> None:
from parser.form_payload import enrich_container_specific_semantics
values = ["0"] * 32
values[0] = "22"
values[5] = "9"
values[10] = "1"
values[22] = "a9f3b1ac-f51b-431e-b102-55a69acdecad"
row = {"marker": "22", "type_name": "Командная панель", "semantic": {"groups": {}}}
enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
assert props["CommandSource"] == "Form"
def test_button_group_decodes_form_command_source() -> None:
from parser.form_payload import enrich_container_specific_semantics
values = ["0"] * 30
values[0] = "22"
values[5] = "6"
values[10] = "1"
values[20] = "{2,{0,02023637-7868-4a5f-8576-835a76e0c9ba},2,0}"
row = {"marker": "22", "type_name": "Группа кнопок", "semantic": {"groups": {}}}
enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
assert props["CommandSource"] == "Form"
@pytest.mark.parametrize(("mode", "expected"), [("0", "Horizontal"), ("2", "InCell")])
def test_column_group_decodes_layout(mode: str, expected: str) -> None:
from parser.form_payload import enrich_container_specific_semantics
values = ["0"] * 38
values[0] = "22"
values[5] = "2"
values[10] = "1"
values[14] = "1"
values[20] = "{2," + mode + ",1,0,3}"
row = {"marker": "22", "type_name": "Группа колонок", "semantic": {"groups": {}}}
enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
assert props["Группировка"] == expected
assert props["РастягиватьПоГоризонтали"] is True
def test_managed_group_decodes_width_united_and_background() -> None:
from parser.form_payload import enrich_container_specific_semantics
values = ["0"] * 34
values[0] = "22"
values[5] = "5"
values[6] = '"Группа"'
values[10] = "1"
values[12] = "24"
values[20] = "{29,1,0,0,0,{0},{1,0},{0},0,{3,3,{-35}},0,0,0,1,{1,0},0,0,3,3,2,0,0,1}"
row = {"marker": "22", "type_name": "Группа", "semantic": {"groups": {}}}
enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {item["name"]: item["value"] for items in row["semantic"]["groups"].values() for item in items}
assert props["Ширина"] == "24"
assert props["United"] is False
assert props["ЦветФона"] == "style:TableHeaderBackColor"
def test_compact_typed_group_decodes_horizontal_if_possible_from_secondary_slot() -> None:
from parser.form_payload import enrich_container_specific_semantics
values = ["0"] * 34
values[0] = "22"
values[5] = "5"
values[6] = '"ГруппаВариант"'
values[10] = "1"
values[12] = "0"
values[21] = "2"
values[22] = "3d3cb80c-508b-41fa-8a18-680cdf5f1712"
values[24] = "77ffcc29-7f2d-4223-b22f-19666e7250ba"
row = {"marker": "22", "type_name": "Группа", "semantic": {"groups": {}}}
enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
assert props["Группировка"] == "HorizontalIfPossible"
assert props["Отображение"] == "WeakSeparation"
def test_decode_form_payload_reads_table_height_from_live_marker_73_slot() -> None:
table_values = ["0"] * 60
table_values[0] = "73"
table_values[1] = "{40,00000000-0000-0000-0000-000000000020}"
table_values[5] = '"ТЗ"'
table_values[38] = "2"
table_values[39] = "0"
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(table_values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
props = {prop["name"]: prop for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}
assert props["HeightInTableRows"]["value"] == "2"
assert props["HeightInTableRows"]["parameter_index"] == 38
def test_decode_form_payload_reads_live_marker_48_behavior_slots() -> None:
field_values = ["0"] * 74
field_values[0] = "48"
field_values[1] = "{3,00000000-0000-0000-0000-000000000003}"
field_values[5] = "2"
field_values[6] = '"КодПрограммы"'
field_values[7] = "0"
field_values[13] = "0"
field_values[14] = "1"
field_values[15] = "1"
field_values[43] = "0"
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(field_values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
props = {prop["name"]: prop for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}
assert props["ПоложениеЗаголовка"]["value"] == "None"
assert props["ПоложениеЗаголовка"]["parameter_index"] == 7
assert props["Видимость"]["value"] is False
assert props["Видимость"]["parameter_index"] == 43
assert props["Доступность"]["value"] is False
assert props["Доступность"]["parameter_index"] == 13
assert props["ТолькоПросмотр"]["value"] is True
assert props["ТолькоПросмотр"]["parameter_index"] == 14
assert props["ПропускатьПриВводе"]["value"] is True
assert props["ПропускатьПриВводе"]["parameter_index"] == 15
def test_decode_form_payload_reads_marker_34_button_visibility_and_availability() -> None:
button_values = ["0"] * 56
button_values[0] = "34"
button_values[1] = "{15,00000000-0000-0000-0000-000000000015}"
button_values[5] = '"ФормаКомандаОбновить"'
button_values[7] = "0"
button_values[8] = "{1,409b9a53-7f7e-4178-86c1-33176c7c7a7a}"
button_values[26] = "0"
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(button_values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
props = {prop["name"]: prop for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}
assert props["Видимость"]["value"] is False
assert props["Видимость"]["parameter_index"] == 26
assert props["Доступность"]["value"] is False
assert props["Доступность"]["parameter_index"] == 7
def test_decode_form_payload_reads_live_container_and_table_behavior_slots() -> None:
def profile_props(values: list[str]) -> dict[str, dict[str, object]]:
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
return {prop["name"]: prop for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}
vertical_group = ["0"] * 36
vertical_group[0] = "22"
vertical_group[1] = "{65,00000000-0000-0000-0000-000000000065}"
vertical_group[5] = "5"
vertical_group[6] = '"ГруппаФорма"'
vertical_group[10] = "1"
vertical_group[21] = "2"
vertical_group[22] = "cd5394d0-7dda-4b56-8927-93ccbe967a01"
vertical_group[24] = "143c00f7-a42d-4cd7-9189-88e4467dc768"
vertical_group[26] = "0"
horizontal_group = ["0"] * 36
horizontal_group[0] = "22"
horizontal_group[1] = "{1,00000000-0000-0000-0000-000000000001}"
horizontal_group[5] = "5"
horizontal_group[6] = '"Группа1"'
horizontal_group[10] = "1"
horizontal_group[21] = "3"
horizontal_group[22] = "77ffcc29-7f2d-4223-b22f-19666e7250ba"
horizontal_group[24] = "77ffcc29-7f2d-4223-b22f-19666e7250ba"
horizontal_group[26] = "77ffcc29-7f2d-4223-b22f-19666e7250ba"
horizontal_group[28] = "0"
table_values = ["0"] * 74
table_values[0] = "73"
table_values[1] = "{20,00000000-0000-0000-0000-000000000020}"
table_values[5] = '"ТЗ"'
table_values[13] = "0"
table_values[38] = "2"
table_values[72] = "0"
vertical_props = profile_props(vertical_group)
horizontal_props = profile_props(horizontal_group)
table_props = profile_props(table_values)
assert vertical_props["Видимость"]["value"] is False
assert vertical_props["Видимость"]["parameter_index"] == 26
assert horizontal_props["Видимость"]["value"] is False
assert horizontal_props["Видимость"]["parameter_index"] == 28
assert table_props["Видимость"]["value"] is False
assert table_props["Видимость"]["parameter_index"] == 72
assert table_props["Доступность"]["value"] is False
assert table_props["Доступность"]["parameter_index"] == 13
def test_decode_form_payload_reads_marker_6_table_addition_behavior_slots() -> None:
addition_values = ["0"] * 24
addition_values[0] = "6"
addition_values[1] = "{24,00000000-0000-0000-0000-000000000024}"
addition_values[5] = "2"
addition_values[6] = '"ТЗУправлениеПоиском"'
addition_values[9] = "0"
addition_values[10] = "0"
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
"{"
+ ",".join(addition_values)
+ '},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
props = {prop["name"]: prop for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}
assert props["Видимость"]["value"] is False
assert props["Видимость"]["parameter_index"] == 9
assert props["Доступность"]["value"] is False
assert props["Доступность"]["parameter_index"] == 10
def test_form_sql_xml_compare_matches_columns_by_name_before_id() -> None:
sql_form = {
"profile": {
"items": [
{"name": "ДругаяГруппа", "id": "1", "type_name": "Группа"},
{"name": "К1", "id": "1", "type_name": "Колонка динамического списка"},
]
}
}
xml_profile = {"items": [{"kind": "Column", "name": "К1", "id": "1", "type_name": "Column"}]}
result = form_sql_xml_compare_script.compare_form(sql_form, xml_profile, sample_limit=10)
assert result["counts"]["matched_items"] == 1
assert result["counts"]["matched_properties"] >= 3
assert result["counts"].get("mismatches", 0) == 0
assert result["samples"]["matched"][0]["sql_type"] == "Колонка динамического списка"
def test_form_sql_xml_compare_reports_root_form_semantics() -> None:
sql_form = {
"profile": {
"form_parameters": [
{"index": 17, "value": "0", "value_kind": "boolean_or_number"},
{"index": 56, "value": "0", "value_kind": "boolean_or_number"},
]
}
}
xml_profile = decode_form_xml(
""
)
result = form_sql_xml_compare_script.compare_form(sql_form, xml_profile, sample_limit=10)
assert result["counts"]["matched_form_properties"] == 2
assert result["counts"]["xml_only_form_properties"] == 1
assert result["samples"]["xml_only_form_property"] == [
{"property": "РежимОткрытияОкна", "xml_name": "WindowOpeningMode", "xml_value": "DontBlock"}
]
assert result["form_property_routes"] == [
{
"xml_name": "ShowCommandBar",
"property": "ОтображатьКоманднуюПанель",
"parameter_indices": [17, 56],
"sql_source": "controlled_designer_form_show_command_bar",
"write_shape": "paired_scalar",
}
]
def test_form_sql_xml_compare_matches_xml_button_to_command_by_name() -> None:
sql_form = {"profile": {"commands": [{"name": "КомандаПример1", "id": "2", "title": "Пример1"}]}}
xml_profile = {"items": [{"kind": "Button", "name": "КомандаПример1", "id": "61", "title": "Пример1"}]}
result = form_sql_xml_compare_script.compare_form(sql_form, xml_profile, sample_limit=10)
assert result["counts"]["matched_items"] == 1
assert result["counts"].get("mismatches", 0) == 0
assert result["samples"]["matched"][0]["name"] == "КомандаПример1"
def test_form_sql_xml_compare_reports_command_name_matches() -> None:
sql_form = {
"profile": {
"items": [
{
"name": "Кнопка",
"id": "105",
"path": "1.22.23",
"semantic": {
"groups": {
"Команда": [
{
"name": "ИмяКоманды",
"value": "Form.StandardCommand.CustomizeForm",
"source": "button_command_link:standard_command_guid",
"parameter_index": 8,
}
]
}
},
}
]
}
}
xml_profile = {
"items": [
{
"kind": "Button",
"name": "Кнопка",
"id": "105",
"semantic": {
"groups": {
"Основные": [
{
"name": "ИмяКоманды",
"xml_name": "CommandName",
"value": "Form.StandardCommand.CustomizeForm",
}
]
}
},
}
]
}
result = form_sql_xml_compare_script.compare_form(sql_form, xml_profile, sample_limit=10)
assert result["counts"]["command_name_matches"] == 1
assert result["counts"].get("command_name_mismatches", 0) == 0
assert result["samples"]["command_name_match"][0]["item"] == "Кнопка"
assert result["property_routes"] == [
{
"xml_kind": "Button",
"xml_name": "CommandName",
"property": "ИмяКоманды",
"sql_marker": None,
"parameter_index": 8,
"sql_source": "button_command_link:standard_command_guid",
"matches": 1,
}
]
def test_form_sql_xml_compare_matches_nested_element_events() -> None:
sql_form = {
"profile": {
"items": [
{
"name": "КодПрограммы",
"id": "12",
"events": [
{
"event_name": "EditTextChange",
"handler": "КодПрограммыИзменениеТекстаРедактирования",
"guid": "14256303-d2b7-4a58-bfab-e77493d10a59",
}
],
}
]
}
}
xml_profile = {
"items": [
{
"kind": "Event",
"name": "EditTextChange",
"type_name": "Событие",
"semantic": {
"groups": {
"Основные": [
{"name": "Обработчик", "xml_name": "Handler", "value": "КодПрограммыИзменениеТекстаРедактирования"}
]
}
},
}
]
}
result = form_sql_xml_compare_script.compare_form(sql_form, xml_profile, sample_limit=10)
assert result["counts"]["matched_items"] == 1
assert result["counts"].get("missing_sql_item", 0) == 0
assert result["counts"].get("mismatches", 0) == 0
assert result["counts"]["matched_properties"] >= 3
def test_form_sql_xml_compare_links_additional_column_to_sql_field_data_path() -> None:
sql_form = {
"profile": {
"items": [
{
"name": "СтрокиЗначение",
"id": "108",
"type_name": "Поле ввода",
"path": "1.20.30",
"path_to_data": "Объект.Строки.Значение",
}
]
}
}
xml_profile = {
"items": [
{
"kind": "Column",
"kind_ru": "Колонка реквизита",
"name": "Значение",
"id": "1",
"additional_columns_table": "Объект.Строки",
"semantic": {"groups": {}},
}
]
}
result = form_sql_xml_compare_script.compare_form(sql_form, xml_profile, sample_limit=10)
assert result["counts"]["matched_items"] == 1
assert result["counts"]["logical_column_sql_match"] == 1
assert result["counts"].get("missing_sql_item", 0) == 0
def test_decode_form_payload_links_marker_34_buttons_to_commands_by_name() -> None:
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
'{34,{40,00000000-0000-0000-0000-000000000061},0,1,{},1,"КомандаПример1",{},1,{}, {},3,0,0,0,2,2,0,0,0,{},{},{},{},{},0,{},1,{}, "",2,0,1,{}, {},1,0,0,1,0,0,0,3,3,3,0,0,1,0,0,0,1,0,0,"",0,0,0,1,{}},0},'
'"Процедура КомандаПример1(Команда)\nКонецПроцедуры",'
'{0,0},'
'{0,0},'
'{2,1,{11,{13},0,"КомандаПример1",{1,1,{"ru","Пример1"}}}},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
props = {prop["name"]: prop["value"] for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}
assert props["ИмяКоманды"] == "Form.Command.КомандаПример1"
def test_decode_form_payload_links_marker_34_button_to_command_by_binding_id() -> None:
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
'{34,{15,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,1,"ФормаКомандаОбновить",{1,0},1,{2,409b9a53-7f7e-4178-86c1-33176c7c7a7a},{0},3,0,0,0,2,2,0,0,0,{},0},0},'
'"Процедура КомандаПример1(Команда)\nКонецПроцедуры",'
'{0,0},'
'{0,0},'
'{4,3,{11,{1},0,"КомандаПрименить"},{11,{2},0,"КомандаПример1"},{11,{3},0,"КомандаПример2"}},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
props = {prop["name"]: prop["value"] for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}
assert profile["items"][0]["command_binding"]["command_id"] == "2"
assert props["ИмяКоманды"] == "Form.Command.КомандаПример1"
assert profile["button_command_links"][0]["match_by"] == "form_command_id"
def test_decode_form_payload_links_marker_34_button_to_standard_command_by_binding_guid() -> None:
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
'{34,{57,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,"ТЗИзменитьФорму",{1,0},1,{0,198ea630-fda2-4cda-8a23-f999f4c67ee6},{0},3,0,0,0,2,2,0,0,0,{},0},0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
props = {prop["name"]: prop["value"] for props in profile["items"][0]["semantic"]["groups"].values() for prop in props}
assert profile["items"][0]["command_binding"]["scope"] == "standard"
assert props["ИмяКоманды"] == "Form.StandardCommand.CustomizeForm"
assert profile["button_command_links"][0]["match_by"] == "standard_command_guid"
def test_decode_form_payload_reads_marker_48_and_73_names_from_xml_verified_slots() -> None:
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
'{48,{3,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,2,"А",1,0,"","","","",1,0},'
'{73,{20,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,"ТЗ",0,0,1,"","",{},0,1,0},'
'0},'
'"",'
'{0,0},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20, include_parameters=True)
by_id = {item["id"]: item for item in profile["items"]}
assert by_id["3"]["name"] == "А"
assert by_id["3"]["type_code"] == "2"
assert by_id["3"]["type_name"] == "Поле ввода"
assert by_id["20"]["name"] == "ТЗ"
def test_decode_form_payload_links_element_events_to_module_routines() -> None:
text = (
'{4,'
'{50,0,0,0,0,1,0,0,00000000-0000-0000-0000-000000000000,1,{},0,0,1,0,0,0,0,0,0,0,0,0,0,{},0,{},'
'{0},0,0,'
'{37,{40,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,2,"ОформленоОрганизации",0,0,{1,1,{"ru","Организации"}},{1,0},{1,{13}},'
'{2,1960479b-4d89-4eba-8b39-0aa802020558,"ОформленоОрганизацииНачалоВыбора",b50dc41b-c15a-4ebe-a17f-d01e51c47de6,"ОформленоОрганизацииОчистка"}},0},'
'"Процедура ОформленоОрганизацииНачалоВыбора()\nКонецПроцедуры",'
'{4,1,{9,{13},0,"ОрганизацииСАТУРНПредставление",{1,1,{"ru","Организации представление"}},{"Pattern",{"S"}},0,0,0,0,{0,0}}},'
'{0,0},'
'{0,0},'
'{0,0},{0,0},0,0}'
)
profile = decode_form_payload(parse_brace_text(text), max_items=20)
field = {item["name"]: item for item in profile["items"]}["ОформленоОрганизации"]
assert [event["handler"] for event in field["events"]] == ["ОформленоОрганизацииНачалоВыбора", "ОформленоОрганизацииОчистка"]
assert [event["name"] for event in field["semantic"]["groups"]["События"]] == ["StartChoice", "Clearing"]
assert field["handler_links"][0]["status"] == "resolved"
assert field["handler_links"][1]["status"] == "missing"
def test_event_handlers_from_node_does_not_assign_child_events_to_parent_container() -> None:
page = parse_brace_text('{22,{},0,0,0,4,"Page1",{},'
'{37,{40,02023637-7868-4a5f-8576-835a76e0c9ba},0,0,0,2,"List1",0,0,{1,1,{"ru","List1"}},{1,0},{1,{13}},'
'{1,60edb81d-887b-478e-94ee-7fef2b13393d,"List1OnActivate"}},0}'
'}'
)["items"][0]
child = page["items"][8]
page_events = event_handlers_from_node(page, "1.27.31", owner="Page1")
child_events = event_handlers_from_node(child, "1.27.31.8", owner="List1")
assert page_events == []
assert len(child_events) == 1
assert child_events[0]["owner"] == "List1"
assert child_events[0]["guid"] == "60edb81d-887b-478e-94ee-7fef2b13393d"
assert child_events[0]["handler"] == "List1OnActivate"
def test_event_handlers_ignores_numeric_non_bsl_handlers(monkeypatch: pytest.MonkeyPatch) -> None:
node = parse_brace_text(
'{3,60edb81d-887b-478e-94ee-7fef2b13393d,"HandlerOpen",12345678-1234-1234-1234-1234567890ab,2,87654321-4321-4321-4321-ba0987654321,3}'
)
monkeypatch.setattr("parser.form_payload.get_by_path", lambda tree, path: node)
events = event_handlers({})
assert len(events) == 1
assert events[0]["handler"] == "HandlerOpen"
def test_event_handlers_discovers_compact_base_form_event_block() -> None:
form_values = ["0"] * 24
form_values[23] = (
'{2,390d5e4b-e732-4c88-8748-9e211a416984,"ПриЧтенииНаСервере",'
'bf0ac0e1-bcbb-4dfe-8fc4-0b1923b461a6,"ПередЗаписьюНаСервере",1,0}'
)
tree = parse_brace_text("{4,{" + ",".join(form_values) + "}}")
events = event_handlers(tree)
assert [(event["name"], event["handler"]) for event in events] == [
("OnReadAtServer", "ПриЧтенииНаСервере"),
("BeforeWriteAtServer", "ПередЗаписьюНаСервере"),
]
def test_public_metadata_item_hides_reference_type_guid() -> None:
record = {
"likely_name": "Ответственный",
"type": {
"code": "#",
"kind": "reference",
"presentation": "Ссылка",
"type_guid": "a0c73d8d-dbc6-4ea9-ba05-d0ea3681cda2",
},
"identity": {
"guid": "c3a0c22f-d25c-484e-bca4-1914f9e5a661",
"name": "Ответственный",
"synonyms": {"ru": "Ответственный"},
},
"path": "5.56",
"index": 54,
"guids_sample": ["a0c73d8d-dbc6-4ea9-ba05-d0ea3681cda2"],
}
resolved_types = {
"a0c73d8d-dbc6-4ea9-ba05-d0ea3681cda2": {
"status": "ok",
"guid_role": "generated_type",
"generated_category": "Ref",
"generated_name": "CatalogRef.Пользователи",
"kind": "Catalog",
"name": "Пользователи",
"presentation": "cfg:CatalogRef.Пользователи",
}
}
public = public_metadata_item(record, resolved_types)
assert public["type"]["presentation"] == "СправочникСсылка.Пользователи"
assert public["synonym"] == "Ответственный"
assert "type_guid" not in public["type"]
assert "resolved" not in public["type"]
assert "identity" not in public
assert "guids_sample" not in public
def test_public_semantic_profile_hides_reference_type_guid() -> None:
semantic = {
"sections": [
{
"category": "Attribute",
"storage_routes": [{"table": "Config"}],
"records": [
{
"likely_name": "Ответственный",
"type": {
"code": "#",
"kind": "reference",
"presentation": "Ссылка",
"type_guid": "a0c73d8d-dbc6-4ea9-ba05-d0ea3681cda2",
},
"path": "5.56",
"index": 54,
"strings_sample": ["Pattern"],
"guids_sample": ["a0c73d8d-dbc6-4ea9-ba05-d0ea3681cda2"],
}
],
}
],
"section_rules": [{"path": "5", "category": "Attribute"}],
"generic_sections": [{"path": "1", "strings_sample": ["ПлатежноеПоручение"]}],
}
resolved_types = {
"a0c73d8d-dbc6-4ea9-ba05-d0ea3681cda2": {
"status": "ok",
"generated_category": "Ref",
"kind": "Catalog",
"name": "Пользователи",
}
}
public = public_semantic_profile(semantic, resolved_types=resolved_types)
record = public["sections"][0]["records"][0]
assert record["type"]["presentation"] == "СправочникСсылка.Пользователи"
assert "type_guid" not in record["type"]
assert "section_rules" not in public
assert "generic_sections" not in public
assert "storage_routes" not in public["sections"][0]
assert "path" not in record
assert "guids_sample" not in record
def test_public_form_profile_hides_decoder_coordinates() -> None:
profile = {
"schema": "onec_form_payload_profile.v1",
"status": "ok",
"items": [
{
"name": "ФормаКоманднаяПанель",
"name_path": "1.34.6",
"id_path": "1.34.1.0",
"path": "1.34",
"marker": "22",
"type_code": "9",
"strings_sample": ["Pattern"],
"guids_sample": ["02023637-7868-4a5f-8576-835a76e0c9ba"],
}
],
"commands": [
{
"name": "Команда1",
"path": "5.1",
"id_path": "5.1.1",
"strings_sample": ["Команда1"],
}
],
}
public = public_form_profile(profile)
item = public["items"][0]
command = public["commands"][0]
assert item["name"] == "ФормаКоманднаяПанель"
assert "name_path" not in item
assert "id_path" not in item
assert "path" not in item
assert "strings_sample" not in item
assert "guids_sample" not in item
assert command["name"] == "Команда1"
assert "path" not in command
assert "strings_sample" not in command
def test_decode_form_xml_exposes_configurator_semantic_groups() -> None:
xml = """
"""
profile = decode_form_xml(xml)
assert profile["status"] == "ok"
assert profile["counts"]["items_total"] == 8
form_groups = profile["form"]["semantic"]["groups"]
assert {"name": "ПоложениеКоманднойПанели", "xml_name": "CommandBarLocation", "value": "None", "source": "form_xml", "status": "ok"} in form_groups["Основные"]
assert {"name": "ОтображатьКоманднуюПанель", "xml_name": "ShowCommandBar", "value": False, "source": "form_xml", "status": "ok"} in form_groups["Основные"]
by_name = {item["name"]: item for item in profile["items"]}
number = by_name["Номер"]["semantic"]["groups"]
assert {"name": "ПутьКДанным", "xml_name": "DataPath", "value": "Список.Number", "source": "form_xml", "status": "ok"} in number["Основные"]
assert {"name": "АктивизироватьПоУмолчанию", "xml_name": "DefaultItem", "value": True, "source": "form_xml", "status": "ok"} in number["Основные"]
button = by_name["СписокАрхивироватьДокументы"]["semantic"]["groups"]
assert {"name": "ИмяКоманды", "xml_name": "CommandName", "value": "Form.Command.АрхивироватьДокументы", "source": "form_xml", "status": "ok"} in button["Основные"]
assert {"name": "ПоложениеВКоманднойПанели", "xml_name": "LocationInCommandBar", "value": "InAdditionalSubmenu", "source": "form_xml", "status": "ok"} in button["Расположение"]
field = by_name["ОформленоОрганизации"]["semantic"]["groups"]
assert {"name": "КнопкаВыпадающегоСписка", "xml_name": "DropListButton", "value": True, "source": "form_xml", "status": "ok"} in field["Использование"]
assert {"name": "МаксимальнаяШирина", "xml_name": "MaxWidth", "value": "28", "source": "form_xml", "status": "ok"} in field["Расположение"]
assert by_name["Пометка"]["kind_ru"] == "Поле флажка"
assert by_name["СтрокаПоиска"]["kind_ru"] == "Дополнение строки поиска"
assert by_name["Выполнить"]["kind_ru"] == "Команда формы"
assert by_name["Значение"]["additional_columns_table"] == "Объект.Строки"
command = by_name["Выполнить"]["semantic"]["groups"]
assert {"name": "Действие", "xml_name": "Action", "value": "Выполнить", "source": "form_xml", "status": "ok"} in command["Использование"]
def test_payload_public_properties_detects_template_tabular_document() -> None:
public = payload_public_properties(
{
"role": "template_payload",
"markers": ["MOXCEL", "utf8_bom"],
"root": {"root_marker": "8"},
"counts": {"stream_blocks": 0, "base64_blocks": 0},
"stream_blocks": [],
"base64_blocks": [],
}
)
assert public["content_kind"] == "template"
assert public["features"]["tabular_document"] is True
assert public["root_marker"] == "8"
def test_compare_template_sql_xml_profiles_reports_coordinate_hint_progress() -> None:
from scripts.compare_1c_template_sql_xml_profiles import compare, render_markdown
payload = compare(
{
"base_id": "upo_test",
"items": [
{
"owner": {"kind": "Document", "name": "АвансовыйОтчет"},
"template": {"name": "ПФ_MXL_АвансовыйОтчет"},
"profile": {
"capacity_dimensions": {"rows": 75, "columns": 26},
"used_dimensions": {"rows": 74, "columns": 25},
"counts": {"cells": 0, "cell_coordinate_hints": 10, "cell_style_coordinate_hints": 10, "merge_record_block_candidates": 1},
},
}
],
},
{
"root": "xml",
"templates": [
{
"path": r"Z:\codex\1C\XML\UPO\Структура базы 1с\Конфигурация\Documents\АвансовыйОтчет\Templates\ПФ_MXL_АвансовыйОтчет\Ext\Template.xml",
"xml_kind": "tabular_document",
"capacity_dimensions": {"rows": 75, "columns": 26},
"used_dimensions": {"rows": 75, "columns": 26},
"counts": {"cells": 100, "parameters": 0, "merges": 12, "format_indexes": 0},
}
],
},
)
comparison = payload["comparisons"][0]
assert payload["counts"]["progress_counts"] == {"cell_coordinate_hints_available": 1, "merge_record_blocks_available": 1}
assert comparison["progress"][0]["sql"] == 10
assert comparison["progress"][0]["coverage_ratio"] == 0.1
assert comparison["progress"][1]["sql"] == 1
assert comparison["progress"][1]["xml_merges"] == 12
assert comparison["sql"]["used_delta"] == {"rows": -1, "columns": -1}
assert [gap["code"] for gap in comparison["gaps"]] == ["used_dimensions_mismatch", "cells_missing_or_limited", "merged_ranges_missing"]
markdown = render_markdown(payload)
assert "| SQL used | XML used | Used delta | SQL cells | Coord hints | Merge blocks |" in markdown
assert "`74x25` | `75x26` | `-1x-1` | 0 | 10 | 1" in markdown
def test_extract_moxel_public_structure_decodes_named_area_ranges() -> None:
payload = (
"MOXCEL\x00\x08\x00\x01\x00\x0c\x00\ufeff{8,1,12,\r\n"
'{"ru","ru",1,1,"ru","Русский","Русский",1},\r\n'
"{128,72},\r\n"
'0,0,2,0,\r\n'
'{16,4,{1,1,{"ru","[ДатаПлана] Заголовок"}},0},1,\r\n'
'{24,5,"Номенклатура",{1,1,{"","НоменклатураПредставление"}},0},2,\r\n'
'{35,"ОбластьБлюдо",\r\n'
"{1,\r\n"
"{3,0,6,1,6,00000000-0000-0000-0000-000000000000},0},"
'"ОбластьИтог",\r\n'
"{1,\r\n"
"{3,0,9,1,16,00000000-0000-0000-0000-000000000000},0}"
"}"
).encode("utf-8")
structure = adapter_server.extract_moxel_public_structure(payload)
assert structure["capabilities"]["decoded_text"] is True
assert structure["capabilities"]["named_area_coordinates"] is True
assert structure["dimensions"] == {"rows": 128, "columns": 72}
assert structure["capacity_dimensions"] == {"rows": 128, "columns": 72}
assert structure["used_dimensions"] == {
"rows": 2,
"columns": 17,
"evidence": ["cells", "cell_coordinate_hints", "named_areas"],
"bounded_by_capacity": {"rows": True, "columns": True},
}
assert structure["counts"]["cells"] == 2
assert structure["counts"]["cell_parameters"] == 2
assert structure["counts"]["named_areas"] == 2
assert structure["capabilities"]["cell_coordinates"] is True
assert structure["cells"][0]["one_based"] == {"row": 1, "column": 2}
assert structure["cells"][0]["text"] == "[ДатаПлана] Заголовок"
assert structure["cells"][1]["one_based"] == {"row": 1, "column": 3}
assert structure["cells"][1]["parameter"] == "Номенклатура"
assert structure["cells"][1]["text"] == "НоменклатураПредставление"
assert {"name": "ДатаПлана", "row": 1, "column": 2, "source": "placeholder", "cell_text": "[ДатаПлана] Заголовок", "one_based": {"row": 1, "column": 2}} in structure["cell_parameters"]
assert {"name": "Номенклатура", "row": 1, "column": 3, "source": "cell_parameter", "cell_text": "НоменклатураПредставление", "one_based": {"row": 1, "column": 3}} in structure["cell_parameters"]
assert {"name": "НоменклатураПредставление", "row": 1, "column": 3, "source": "cell_text_identifier", "parameter": "Номенклатура", "one_based": {"row": 1, "column": 3}} in structure["cell_text_identifiers"]
assert structure["named_areas"][0]["name"] == "ОбластьБлюдо"
assert structure["named_areas"][0]["range"]["zero_based"] == {
"top": 0,
"left": 6,
"bottom": 1,
"right": 6,
"row_start": 0,
"column_start": 6,
"row_end": 1,
"column_end": 6,
}
assert structure["named_areas"][1]["range"]["width"] == 8
assert structure["capabilities"]["merged_cell_candidates"] is True
assert structure["merged_range_candidates"][0]["source"] == "heuristic_named_area_range"
assert structure["merged_range_candidates"][0]["confidence"] == "low"
assert structure["moxel_record_diagnostics"]["schema"] == "moxel_record_diagnostics.v1"
assert structure["moxel_record_diagnostics"]["authoritative_merge_decoder"] is False
assert structure["moxel_record_diagnostics"]["head_counts"]
assert structure["moxel_record_diagnostics"]["head_samples"]
assert structure["moxel_record_diagnostics"]["head_samples"][0]["samples"]
assert "tree_position" in structure["moxel_record_diagnostics"]["head_samples"][0]["samples"][0]
assert structure["moxel_record_diagnostics"]["top_level_records"]
assert structure["moxel_record_diagnostics"]["top_level_records"][0]["tree_position"] == "$.4"
assert structure["moxel_record_diagnostics"]["top_level_shapes"]
assert {"head", "list_length", "numeric_count", "count", "positions", "numeric_prefixes"} <= set(structure["moxel_record_diagnostics"]["top_level_shapes"][0])
assert structure["moxel_record_diagnostics"]["top_level_shape_candidates"]
assert {"rank", "score", "reasons", "confidence", "source", "suggested_windows"} <= set(structure["moxel_record_diagnostics"]["top_level_shape_candidates"][0])
assert structure["moxel_record_diagnostics"]["top_level_shape_candidates"][0]["rank"] == 1
assert structure["moxel_record_diagnostics"]["top_level_candidate_summary"]["total"] >= 1
assert structure["moxel_record_diagnostics"]["top_level_candidate_summary"]["score_counts"]
assert structure["moxel_record_diagnostics"]["top_level_candidate_summary"]["reason_counts"]
assert structure["moxel_record_diagnostics"]["samples"]
def test_moxel_dimensions_split_used_from_format_extents() -> None:
capacity_dimensions = {"rows": 128, "columns": 72}
used_dimensions = adapter_server.infer_moxel_used_dimensions(
capacity_dimensions=capacity_dimensions,
cells=[{"row": 2, "column": 3, "text": "Итого"}],
named_areas=[],
named_range_candidates=[],
merged_ranges=[],
)
format_dimensions = adapter_server.infer_moxel_format_dimensions(
capacity_dimensions=capacity_dimensions,
column_widths=[{"column": 12, "width": 1000}],
row_heights=[],
)
assert used_dimensions == {
"rows": 2,
"columns": 3,
"evidence": ["cells"],
"bounded_by_capacity": {"rows": True, "columns": True},
}
assert format_dimensions == {
"rows": None,
"columns": 12,
"evidence": ["column_widths"],
"bounded_by_capacity": {"rows": True, "columns": True},
}
def test_moxel_used_dimensions_prefers_coordinate_hint_columns() -> None:
used_dimensions = adapter_server.infer_moxel_used_dimensions(
capacity_dimensions={"rows": 128, "columns": 72},
cells=[{"row": 74, "column": 72, "text": "bad decoded column"}],
cell_coordinate_hints=[
{
"text": "bad decoded column",
"one_based": {"row": 74, "column": 26},
"confidence": "high",
}
],
named_areas=[],
named_range_candidates=[],
merged_ranges=[],
)
assert used_dimensions == {
"rows": 74,
"columns": 26,
"evidence": ["cells", "cell_coordinate_hints"],
"bounded_by_capacity": {"rows": True, "columns": True},
}
def test_extract_moxel_merge_record_block_candidates_from_singleton_count() -> None:
diagnostics = {
"top_level_records": [
{"tree_position": "$.10", "numeric_items": [3]},
{"tree_position": "$.11", "numeric_items": [1065758, 0, 0, 0, 0, 64, 768, 3, 1]},
{"tree_position": "$.12", "numeric_items": [1049374, 1, 1, 1, 1, 32, 24, 1]},
{"tree_position": "$.13", "numeric_items": [1098526, 1, 1, 1, 1, 6, 24, 3, 1, 1]},
]
}
candidates = adapter_server.extract_moxel_merge_record_block_candidates(diagnostics)
assert len(candidates) == 1
candidate = candidates[0]
assert candidate["count"] == 3
assert candidate["tree_position"] == "$.10"
assert candidate["record_window"] == {"start": "$.11", "end": "$.13", "inspected": 3}
assert candidate["source"] == "moxel_top_level_count_before_coordinate_block"
assert candidate["confidence"] == "medium"
evidence = candidate["evidence"]
assert evidence["singleton_count_record"] == [3]
assert evidence["following_long_numeric_records"] == 3
assert evidence["following_coordinate_like_records"] == 3
assert evidence["shape_summary"] == [
{"shape": "1049374:8", "count": 1},
{"shape": "1065758:9", "count": 1},
{"shape": "1098526:10", "count": 1},
]
assert evidence["column_edge_hints"] == [
{"column_or_edge": 1, "source": "moxel_merge_block_scalar_div32", "confidence": "low"},
{"column_or_edge": 2, "source": "moxel_merge_block_scalar_div32", "confidence": "medium"},
{"column_or_edge": 24, "source": "moxel_merge_block_scalar_div32", "confidence": "medium"},
]
assert evidence["row_or_size_hints"][:2] == [
{"value": 3, "count": 2, "positions": ["$.11", "$.13"], "source": "moxel_merge_block_small_scalar", "confidence": "low"},
{"value": 24, "count": 2, "positions": ["$.12", "$.13"], "source": "moxel_merge_block_small_scalar", "confidence": "low"},
]
assert evidence["record_analysis"]["schema"] == "moxel_numeric_block_records.v1"
assert evidence["record_analysis"]["records_analyzed"] == 3
assert evidence["record_analysis"]["packed_div32_by_value"][:3] == [
{"value": 1, "count": 1, "record_indexes": [2], "runs": [{"start": 2, "end": 2, "length": 1}]},
{"value": 2, "count": 1, "record_indexes": [1], "runs": [{"start": 1, "end": 1, "length": 1}]},
{"value": 24, "count": 1, "record_indexes": [1], "runs": [{"start": 1, "end": 1, "length": 1}]},
]
assert evidence["record_analysis"]["records"][0] == {
"record_index": 1,
"tree_position": "$.11",
"shape": "1065758:9",
"head": 1065758,
"numeric_count": 9,
"packed_div32_values": [2, 24],
"small_scalars": [3, 64],
}
assert candidate["sample_records"][0] == {
"record_index": 1,
"tree_position": "$.11",
"numeric_items": [1065758, 0, 0, 0, 0, 64, 768, 3, 1],
"shape": "1065758:9",
"packed_div32_values": [2, 24],
"small_scalars": [64, 3],
}
def test_moxel_merge_count_hint_does_not_treat_named_area_block_as_merge_records() -> None:
diagnostics = {
"top_level_records": [
{
"tree_position": "$.346",
"numeric_items": [24, 0, 13, 0, 1, 1, 2, 2, 3, 3, 4, 4, 6, 5, 7, 6, 5, 7, 8, 8, 9, 9, 10, 15],
},
{"tree_position": "$.356", "numeric_items": [1]},
{"tree_position": "$.357", "numeric_items": [0]},
{"tree_position": "$.358", "numeric_items": [0]},
{"tree_position": "$.359", "numeric_items": [3]},
{"tree_position": "$.360", "numeric_items": [1065758, 0, 0, 0, 0, 64, 768, 3, 1], "strings": ["AllColumns"]},
{"tree_position": "$.361", "numeric_items": [1049374, 1, 1, 1, 1, 32, 24, 1], "strings": ["AllRows"]},
{"tree_position": "$.362", "numeric_items": [1098526, 1, 1, 1, 1, 6, 24, 3, 1, 1], "strings": ["Area_Merge_1"]},
]
}
assert adapter_server.extract_moxel_merge_record_block_candidates(diagnostics) == []
hints = adapter_server.extract_moxel_merge_count_hints(diagnostics)
assert len(hints) == 1
assert hints[0]["count"] == 1
assert hints[0]["tree_position"] == "$.356"
assert hints[0]["source"] == "moxel_top_level_merge_count_hint"
assert hints[0]["evidence"]["following_named_item_count"] == 3
assert [item["tree_position"] for item in hints[0]["evidence"]["zero_followers"]] == ["$.357", "$.358"]
def test_moxel_merge_record_block_candidates_are_filtered_by_merge_count_hints() -> None:
candidates = [{"count": 3, "tree_position": "$.359"}, {"count": 1, "tree_position": "$.400"}]
filtered = adapter_server.filter_moxel_merge_record_block_candidates_by_count_hints(
candidates,
[{"count": 1, "tree_position": "$.356"}],
)
assert filtered == [{"count": 1, "tree_position": "$.400"}]
assert adapter_server.filter_moxel_merge_record_block_candidates_by_count_hints(candidates, [{"count": 0}]) == []
def test_extract_moxel_merged_ranges_from_tree_decodes_exclusive_edges() -> None:
tree = {
"type": "list",
"items": [
{"type": "list", "items": [{"type": "atom", "value": "0"}]},
{
"type": "list",
"items": [
{"type": "atom", "value": "4"},
{"type": "list", "items": [{"type": "atom", "value": value} for value in ["1", "2", "4", "2", "0"]]},
{"type": "list", "items": [{"type": "atom", "value": value} for value in ["7", "3", "7", "6", "0"]]},
{"type": "list", "items": [{"type": "atom", "value": value} for value in ["2", "7", "6", "9", "0"]]},
{"type": "list", "items": [{"type": "atom", "value": value} for value in ["17", "10", "22", "10", "0"]]},
],
},
],
}
ranges = adapter_server.extract_moxel_merged_ranges_from_tree(tree, [{"count": 4, "tree_position": "$.1"}])
assert [item["range"]["zero_based"] for item in ranges] == [
{"top": 2, "left": 1, "bottom": 2, "right": 3, "row_start": 2, "column_start": 1, "row_end": 2, "column_end": 3},
{"top": 3, "left": 7, "bottom": 5, "right": 7, "row_start": 3, "column_start": 7, "row_end": 5, "column_end": 7},
{"top": 7, "left": 2, "bottom": 8, "right": 5, "row_start": 7, "column_start": 2, "row_end": 8, "column_end": 5},
{"top": 10, "left": 17, "bottom": 10, "right": 21, "row_start": 10, "column_start": 17, "row_end": 10, "column_end": 21},
]
assert ranges[0]["raw"] == {"left": 1, "top": 2, "right_exclusive": 4, "bottom_exclusive": 2, "flag": 0}
assert ranges[0]["source"] == "moxel_tree_merge_block"
def test_extract_moxel_format_table_from_tree_prefers_contiguous_format_run() -> None:
def record(values: list[str]) -> dict[str, Any]:
return {"type": "list", "items": [{"type": "atom", "value": value} for value in values]}
tree = {
"type": "list",
"items": [
record(["17281", "0", "80", "0", "8", "0"]),
{"type": "atom", "value": "separator"},
record(["17281", "0", "80", "0", "8", "0"]),
record(["17281", "1", "120", "6", "24", "2"]),
record(["129", "0", "240"]),
],
}
formats = adapter_server.extract_moxel_format_table_from_tree(tree)
assert [item["format_index"] for item in formats] == [1, 2, 3]
assert formats[0]["font_index"] == 0
assert formats[0]["width"] == 80
assert formats[0]["horizontal_alignment"] == {"code": 0, "value": "Left"}
assert formats[0]["vertical_alignment"] == {"code": 8, "value": "Bottom"}
assert formats[0]["text_placement"] == {"code": 0, "value": "Auto"}
assert formats[1]["horizontal_alignment"] == {"code": 6, "value": "Center"}
assert formats[1]["vertical_alignment"] == {"code": 24, "value": "Center"}
assert formats[1]["text_placement"] == {"code": 2, "value": "Block"}
assert formats[2]["record_type"] == 129
assert formats[2]["width"] == 240
def test_extract_moxel_format_table_from_diagnostics_prefers_contiguous_format_run() -> None:
diagnostics = {
"top_level_records": [
{"tree_position": "$.4", "numeric_items": [17281, 0, 80, 0, 8, 0]},
{"tree_position": "$.274", "numeric_items": [17281, 0, 80, 0, 8, 0]},
{"tree_position": "$.275", "numeric_items": [17281, 1, 120, 6, 24, 2]},
{"tree_position": "$.276", "numeric_items": [129, 0, 240]},
]
}
formats = adapter_server.extract_moxel_format_table_from_diagnostics(diagnostics)
assert [item["format_index"] for item in formats] == [1, 2, 3]
assert formats[0]["tree_position"] == "$.274"
assert formats[1]["horizontal_alignment"] == {"code": 6, "value": "Center"}
assert formats[1]["text_placement"] == {"code": 2, "value": "Block"}
assert formats[2]["record_type"] == 129
def test_extract_moxel_format_table_decodes_extended_color_border_flags() -> None:
diagnostics = {
"top_level_records": [
{"tree_position": "$.66", "numeric_items": [1153, 0, 100, 0]},
{"tree_position": "$.67", "numeric_items": [34945, 0, 120, 3, 2]},
{"tree_position": "$.68", "numeric_items": [191, 0, 0, 0, 0, 0, 4, 140]},
{"tree_position": "$.69", "numeric_items": [36031, 0, 1, 0, 2, 0, 4, 160, 5, 6, 1]},
]
}
formats = adapter_server.extract_moxel_format_table_from_diagnostics(diagnostics)
assert [item["width"] for item in formats] == [100, 120, 140, 160]
assert formats[0]["record_type_hex"] == "0x481"
assert formats[0]["text_color"] == {"style_index": 0, "source": "moxel_format_record_flag_0x0400"}
assert formats[1]["back_color"] == {"style_index": 3, "source": "moxel_format_record_flag_0x0800"}
assert formats[1]["fill_type"] == {"code": 2, "value": "Template", "source": "moxel_format_record_flag_0x8000"}
assert formats[2]["borders"] == {
"left": 0,
"top": 0,
"right": 0,
"bottom": 0,
"color_style_index": 4,
"flags": ["0x0002", "0x0004", "0x0008", "0x0010", "0x0020"],
"source": "moxel_format_record_border_flags",
}
assert formats[3]["borders"]["left"] == 1
assert formats[3]["borders"]["right"] == 2
assert formats[3]["text_color"]["style_index"] == 5
assert formats[3]["back_color"]["style_index"] == 6
assert formats[3]["fill_type"]["value"] == "Parameter"
def test_extract_moxel_format_table_decodes_partial_border_flags() -> None:
diagnostics = {
"top_level_records": [
{"tree_position": "$.55", "numeric_items": [163, 0, 1, 0, 141]},
{"tree_position": "$.56", "numeric_items": [165, 0, 1, 0, 142]},
{"tree_position": "$.57", "numeric_items": [169, 0, 1, 0, 143]},
{"tree_position": "$.58", "numeric_items": [177, 0, 1, 0, 144]},
]
}
formats = adapter_server.extract_moxel_format_table_from_diagnostics(diagnostics)
assert [item["width"] for item in formats] == [141, 142, 143, 144]
assert formats[0]["borders"] == {
"left": 1,
"color_style_index": 0,
"flags": ["0x0002", "0x0020"],
"source": "moxel_format_record_border_flags",
}
assert formats[1]["borders"]["top"] == 1
assert formats[2]["borders"]["right"] == 1
assert formats[3]["borders"]["bottom"] == 1
def test_extract_moxel_format_style_index_table_reports_references_and_candidates() -> None:
diagnostics = {
"top_level_records": [
{"tree_position": "$.55", "numeric_items": [129, 0, 111]},
{"tree_position": "$.56", "numeric_items": [2177, 0, 112, 2]},
{"tree_position": "$.57", "numeric_items": [2177, 0, 113, 3]},
{
"tree_position": "$.75",
"numeric_items": [2, 2, 3],
"child_records": [
{
"tree_position": "$.75.2",
"numeric_items": [4, 3, 3],
"child_records": [{"tree_position": "$.75.2.2", "numeric_items": [-25]}],
},
{
"tree_position": "$.75.4",
"numeric_items": [4, 3, 3],
"child_records": [{"tree_position": "$.75.4.2", "numeric_items": [-26]}],
},
],
},
{
"tree_position": "$.74",
"numeric_items": [4, 3, 3],
"child_records": [{"tree_position": "$.74.2", "numeric_items": [-1]}],
},
{
"tree_position": "$.76",
"numeric_items": [4, 0, 0],
"child_records": [{"tree_position": "$.76.2", "numeric_items": [12971252]}],
},
]
}
formats = adapter_server.extract_moxel_format_table_from_diagnostics(diagnostics)
table = adapter_server.extract_moxel_format_style_index_table(formats, diagnostics)
assert table["counts"] == {"style_references": 2, "candidate_records": 1, "style_object_candidates": 4}
assert [(item["style_index"], item["roles"], item["format_indexes"]) for item in table["style_references"]] == [
(2, ["back_color"], [2]),
(3, ["back_color"], [3]),
]
assert table["style_references"][0]["style_code_candidates"][0]["style_code"] == -25
assert table["style_references"][0]["style_code"] == -25
assert table["style_references"][1]["style_code_candidates"][0]["style_code"] == -26
assert table["style_references"][1]["style_code"] == -26
assert table["candidate_records"][0]["tree_position"] == "$.75"
assert table["candidate_records"][0]["referenced_style_indexes"] == [2, 3]
assert table["style_object_candidates"][0]["style_code"] == -25
assert table["style_references"][0]["style_object"]["style_code"] == -1
assert table["style_references"][1]["style_object"]["color"]["hex"] == "0xC5ECF4"
assert table["style_object_candidates"][3]["color"]["rgb_little_endian"]["hex"] == "#F4ECC5"
def test_enrich_moxel_format_table_with_style_references_copies_to_cell_links() -> None:
formats = [
{
"format_index": 2,
"font_index": 0,
"width": 112,
"back_color": {"style_index": 2, "source": "moxel_format_record_flag_0x0800"},
"borders": {"left": 1, "color_style_index": 3},
}
]
style_table = {
"style_references": [
{
"style_index": 2,
"roles": ["back_color"],
"style_object": {"kind": "packed_color", "color": {"hex": "0xC5ECF4"}},
"source": "moxel_format_record_style_index",
"confidence": "medium",
},
{
"style_index": 3,
"roles": ["border_color"],
"style_code": -28,
"source": "moxel_format_record_style_index",
"confidence": "medium",
},
]
}
enriched = adapter_server.enrich_moxel_format_table_with_style_references(formats, style_table)
links = adapter_server.extract_moxel_cell_format_links(
[{"row": 1, "column": 1, "cell_id": 2, "text": "A"}],
enriched,
)
assert enriched[0]["back_color"]["style_reference"]["style_object"]["color"]["hex"] == "0xC5ECF4"
assert enriched[0]["back_color"]["resolved"]["color"]["hex"] == "0xC5ECF4"
assert enriched[0]["back_color"]["resolved_style"]["color"]["hex"] == "0xC5ECF4"
assert enriched[0]["borders"]["color_style_reference"]["style_code"] == -28
assert enriched[0]["borders"]["color_resolved"]["style_code"] == -28
assert links[0]["format"]["back_color"]["style_reference"]["style_object"]["kind"] == "packed_color"
assert links[0]["format"]["back_color"]["resolved"]["kind"] == "packed_color"
assert links[0]["format"]["borders"]["color_style_reference"]["style_code"] == -28
assert links[0]["format"]["borders"]["color_resolved"]["kind"] == "style_code"
fallback = adapter_server.enrich_moxel_format_table_with_style_references(
[
{
"format_index": 2,
"back_color": {
"style_index": 2,
"style_reference": style_table["style_references"][0],
},
}
],
{},
)
assert fallback[0]["back_color"]["resolved"]["color"]["hex"] == "0xC5ECF4"
def test_extract_moxel_record_diagnostics_includes_top_level_child_records() -> None:
tree = {
"type": "list",
"items": [
{
"type": "list",
"items": [
{"type": "atom", "value": "2"},
{
"type": "list",
"items": [
{"type": "atom", "value": "2"},
{"type": "string", "value": "A"},
{
"type": "list",
"items": [{"type": "atom", "value": "9"}, {"type": "string", "value": "AA"}],
},
],
},
{
"type": "list",
"items": [{"type": "atom", "value": "3"}, {"type": "string", "value": "B"}],
},
],
}
],
}
diagnostics = adapter_server.extract_moxel_record_diagnostics(tree)
record = diagnostics["top_level_records"][0]
assert record["numeric_items"] == [2]
assert record["child_records"] == [
{
"tree_position": "$.0.1",
"head": 2,
"list_length": 3,
"numeric_items": [2],
"numeric_items_truncated": False,
"strings": ["A"],
"strings_truncated": False,
"child_records": [
{
"tree_position": "$.0.1.2",
"head": 9,
"list_length": 2,
"numeric_items": [9],
"numeric_items_truncated": False,
"strings": ["AA"],
"strings_truncated": False,
}
],
},
{
"tree_position": "$.0.2",
"head": 3,
"list_length": 2,
"numeric_items": [3],
"numeric_items_truncated": False,
"strings": ["B"],
"strings_truncated": False,
},
]
def test_extract_moxel_font_table_from_diagnostics_decodes_face_height_and_weight() -> None:
diagnostics = {
"top_level_records": [
{
"tree_position": "$.285",
"numeric_items": [8, 0, 575, 80, 0, 0, 0, 400, 0, 0, 0, 0, 0, 0, 0, 0, 1, 100, 0],
"strings": ["Arial"],
},
{
"tree_position": "$.286",
"numeric_items": [8, 0, 575, 100, 0, 0, 0, 700, 0, 0, 0, 0, 0, 0, 0, 0, 1, 100, 0],
"strings": ["Courier New"],
},
]
}
fonts = adapter_server.extract_moxel_font_table_from_diagnostics(diagnostics)
assert fonts[0]["font_index"] == 0
assert fonts[0]["face_name"] == "Arial"
assert fonts[0]["height"] == 8.0
assert fonts[0]["bold"] is False
assert fonts[1]["font_index"] == 1
assert fonts[1]["face_name"] == "Courier New"
assert fonts[1]["height"] == 10.0
assert fonts[1]["bold"] is True
def test_extract_moxel_font_table_decodes_style_bits() -> None:
diagnostics = {
"top_level_records": [
{
"tree_position": "$.62",
"numeric_items": [8, 0, 575, 90, 0, 0, 0, 400, 1, 0, 0, 0, 0, 0, 0, 0, 1, 100, 0],
"strings": ["Courier New"],
},
{
"tree_position": "$.63",
"numeric_items": [8, 0, 575, 80, 0, 0, 0, 400, 0, 1, 1, 0, 0, 0, 0, 0, 1, 100, 0],
"strings": ["Arial"],
},
]
}
fonts = adapter_server.extract_moxel_font_table_from_diagnostics(diagnostics)
assert fonts[0]["italic"] is True
assert fonts[0]["underline"] is False
assert fonts[1]["underline"] is True
assert fonts[1]["strikeout"] is True
def test_enrich_moxel_format_table_with_fonts_keeps_font_index_zero() -> None:
enriched = adapter_server.enrich_moxel_format_table_with_fonts(
[{"format_index": 1, "font_index": 0, "width": 80}, {"format_index": 2, "font_index": 1, "width": 120}],
[
{"font_index": 0, "face_name": "Arial", "height": 8.0, "weight": 400, "bold": False},
{"font_index": 1, "face_name": "Courier New", "height": 10.0, "weight": 700, "bold": True},
],
)
assert enriched[0]["font"]["face_name"] == "Arial"
assert enriched[1]["font"]["bold"] is True
def test_extract_moxel_cell_format_links_maps_cell_id_to_format_index() -> None:
links = adapter_server.extract_moxel_cell_format_links(
[
{"row": 1, "column": 2, "cell_id": 3, "text": "FMT_R1C2"},
{"row": 1, "column": 3, "cell_id": 99, "text": "no format"},
],
[
{
"format_index": 3,
"font_index": 0,
"width": 160,
"font": {"font_index": 0, "face_name": "Arial", "height": 8.0, "bold": False},
"horizontal_alignment": {"code": 2, "value": "Right"},
"text_placement": {"code": 1, "value": "Cut"},
},
],
)
assert links == [
{
"row": 1,
"column": 2,
"one_based": {"row": 1, "column": 2},
"zero_based": {"row": 0, "column": 1},
"format_index": 3,
"format": {
"format_index": 3,
"font_index": 0,
"width": 160,
"font": {"font_index": 0, "face_name": "Arial", "height": 8.0, "bold": False},
"horizontal_alignment": {"code": 2, "value": "Right"},
"text_placement": {"code": 1, "value": "Cut"},
},
"text": "FMT_R1C2",
"source": "moxel_cell_id_as_format_index",
"confidence": "medium",
"diagnostics": {
"message": "In controlled MOXCEL fixtures this cell scalar matches XML /formatIndex. Validate on more one-property probes before treating it as an authoritative style binding."
},
}
]
def test_summarize_moxel_cell_format_links_reports_coverage_confidence() -> None:
high = adapter_server.summarize_moxel_cell_format_links(
[{"row": row, "column": column} for row in range(1, 3) for column in range(1, 4)],
[{"format_index": 1}, {"format_index": 2}, {"format_index": 3}],
[
{"row": row, "column": column, "format_index": column}
for row in range(1, 3)
for column in range(1, 4)
],
)
low = adapter_server.summarize_moxel_cell_format_links(
[{"row": 1, "column": column} for column in range(1, 101)],
[{"format_index": 1}],
[{"row": 1, "column": 1, "format_index": 1}],
)
assert high["linked_cells_ratio_percent"] == 100.0
assert high["distinct_format_indexes"] == 3
assert high["confidence"] == "high"
assert low["linked_cells_ratio_percent"] == 1.0
assert low["confidence"] == "low"
def test_filter_moxel_merge_count_hints_by_tree_removes_positive_slots_without_records() -> None:
tree = {
"type": "list",
"items": [
{"type": "list", "items": [{"type": "atom", "value": "8"}]},
{"type": "list", "items": [{"type": "atom", "value": "0"}]},
{
"type": "list",
"items": [
{"type": "atom", "value": "1"},
{"type": "list", "items": [{"type": "atom", "value": value} for value in ["1", "2", "4", "2", "0"]]},
],
},
],
}
filtered = adapter_server.filter_moxel_merge_count_hints_by_tree(
tree,
[
{"count": 8, "tree_position": "$.0"},
{"count": 0, "tree_position": "$.1"},
{"count": 1, "tree_position": "$.2", "evidence": {"singleton_count_record": [1]}},
],
)
assert [item["count"] for item in filtered] == [0, 1]
assert filtered[1]["evidence"]["merge_record_children"] == 1
def test_filter_moxel_merge_count_hints_by_ranges_removes_positive_slots_without_ranges() -> None:
hints = [
{"count": 8, "tree_position": "$.0"},
{"count": 0, "tree_position": "$.1"},
{"count": 1, "tree_position": "$.2"},
]
assert adapter_server.filter_moxel_merge_count_hints_by_ranges(hints, []) == [{"count": 0, "tree_position": "$.1"}]
assert adapter_server.filter_moxel_merge_count_hints_by_ranges(hints, [{"range": {"zero_based": {}}}]) == hints
def test_compact_template_structure_formats_section_includes_widths_heights_and_styles() -> None:
compact = adapter_server.compact_template_structure(
{
"format": "MOXCEL",
"cell_style_candidates": [{"text": "A1"}],
"cells": [{"row": 1, "column": 1, "cell_id": 1, "text": "A1"}],
"column_widths": [{"column": 1, "width": 120}],
"format_table": [{"format_index": 1, "width": 120, "font_index": 0}],
"font_table": [{"font_index": 0, "face_name": "Arial", "height": 8.0, "bold": False}],
"row_heights": [{"row": 1, "height": 18}],
},
{},
view="structure",
sections={"formats"},
)
assert compact["cell_style_candidates"] == [{"text": "A1"}]
assert compact["column_widths"] == [{"column": 1, "width": 120}]
assert compact["font_table"] == [{"font_index": 0, "face_name": "Arial", "height": 8.0, "bold": False}]
assert compact["format_table"][0]["font"]["face_name"] == "Arial"
assert compact["cell_format_links"][0]["format_index"] == 1
assert compact["cell_format_links"][0]["format"]["width"] == 120
assert compact["cell_format_links"][0]["format"]["font"]["face_name"] == "Arial"
assert compact["cell_format_link_stats"]["cell_format_links"] == 1
assert compact["row_heights"] == [{"row": 1, "height": 18}]
def test_compact_template_structure_recovers_format_table_from_diagnostics() -> None:
compact = adapter_server.compact_template_structure(
{
"format": "MOXCEL",
"moxel_record_diagnostics": [
{
"top_level_records": [
{"tree_position": "$.274", "numeric_items": [17281, 0, 80, 0, 8, 0]},
{"tree_position": "$.275", "numeric_items": [129, 1, 120]},
]
}
],
},
{},
view="structure",
sections={"formats"},
)
assert compact["counts"]["format_table"] == 2
assert [item["width"] for item in compact["format_table"]] == [80, 120]
def test_compact_template_structure_recovers_style_index_table_from_diagnostics() -> None:
compact = adapter_server.compact_template_structure(
{
"format": "MOXCEL",
"moxel_record_diagnostics": [
{
"top_level_records": [
{"tree_position": "$.55", "numeric_items": [129, 0, 111]},
{"tree_position": "$.56", "numeric_items": [2177, 0, 112, 2]},
{"tree_position": "$.57", "numeric_items": [2177, 0, 113, 3]},
{"tree_position": "$.75", "numeric_items": [2, 2, 3]},
]
}
],
},
{},
view="structure",
sections={"formats"},
)
assert compact["counts"]["format_style_index_table"] == 2
assert [item["style_index"] for item in compact["format_style_index_table"]["style_references"]] == [2, 3]
assert compact["format_style_index_table"]["candidate_records"][0]["tree_position"] == "$.75"
def test_parse_template_sections_accepts_formats_aliases() -> None:
assert adapter_server.parse_template_sections({"sections": "summary,formats,format_table,font_table,fonts,format_style_index_table,style_index_table,style_references,cell_format_links,cell_format_link_stats,format_links,row_heights,heights"}) == {
"formats",
"format_table",
"font_table",
"fonts",
"format_style_index_table",
"style_index_table",
"style_references",
"cell_format_links",
"cell_format_link_stats",
"format_links",
"row_heights",
"heights",
}
def test_merge_template_structures_recomputes_public_dimensions() -> None:
merged = adapter_server.merge_template_structures(
[
{
"features": {"tabular_document": True},
"structure": {
"format": "MOXCEL",
"capabilities": {"cell_coordinates": True, "column_widths": True},
"dimensions": {"rows": 128, "columns": 72},
"capacity_dimensions": {"rows": 128, "columns": 72},
"cells": [{"row": 2, "column": 3, "text": "A"}],
"column_widths": [{"column": 12, "width": 1000}],
},
},
{
"features": {"tabular_document": True},
"structure": {
"format": "MOXCEL",
"capabilities": {"cell_coordinates": True},
"dimensions": {"rows": 128, "columns": 72},
"capacity_dimensions": {"rows": 128, "columns": 72},
"cells": [{"row": 5, "column": 4, "text": "B"}],
},
},
]
)
assert merged["capacity_dimensions"] == {"rows": 128, "columns": 72}
assert merged["used_dimensions"] == {
"rows": 5,
"columns": 4,
"evidence": ["cells"],
"bounded_by_capacity": {"rows": True, "columns": True},
}
assert merged["format_dimensions"] == {
"rows": None,
"columns": 12,
"evidence": ["column_widths"],
"bounded_by_capacity": {"rows": True, "columns": True},
}
def test_extract_moxel_cells_from_tree_finds_nested_row_runs() -> None:
tree = parse_brace_text(
'{8,1,12,{128,72},{99,{4,0,1,2,{16,2,{1,1,{"ru","nested text"}},0}}}}'
)
cells = adapter_server.extract_moxel_cells_from_tree(tree)
assert len(cells) == 1
assert cells[0]["text"] == "nested text"
assert cells[0]["one_based"] == {"row": 5, "column": 3}
def test_extract_moxel_named_area_candidates_from_tree_without_coordinates() -> None:
tree = parse_brace_text(
'{8,1,12,{1,"ПримерГоризонтальнойОбласти",{1,{1,-1,3,-1,4,00000000-0000-0000-0000-000000000000},0}}}'
)
areas = adapter_server.extract_moxel_named_area_candidates_from_tree(tree)
assert areas[0]["name"] == "ПримерГоризонтальнойОбласти"
assert areas[0]["source"] == "moxel_tree_named_area_candidate"
assert areas[0]["range"] is None
assert areas[0]["range_candidate"]["raw_scalars"] == [
"1",
"1",
"-1",
"3",
"-1",
"4",
"00000000-0000-0000-0000-000000000000",
"0",
]
def test_extract_moxel_named_range_candidates_from_tree_includes_cell_name() -> None:
tree = parse_brace_text(
'{8,1,12,{2,"R7C2_TEST",{1,{3,1,6,1,6,00000000-0000-0000-0000-000000000000},0},'
'"ПримерГоризонтальнойОбласти",{1,{1,-1,3,-1,4,00000000-0000-0000-0000-000000000000},0}}}'
)
ranges = adapter_server.extract_moxel_named_range_candidates_from_tree(tree)
assert ranges[0]["name"] == "R7C2_TEST"
assert ranges[0]["kind"] == "named_cell_or_range"
assert ranges[0]["range"]["one_based"] == {
"top": 7,
"left": 2,
"bottom": 7,
"right": 2,
"row_start": 7,
"column_start": 2,
"row_end": 7,
"column_end": 2,
}
assert ranges[0]["range_candidate"]["coordinate_order"] == "left,top,right,bottom"
assert ranges[0]["range_candidate"]["raw_scalars"] == [
"1",
"3",
"1",
"6",
"1",
"6",
"00000000-0000-0000-0000-000000000000",
"0",
]
assert ranges[1]["name"] == "ПримерГоризонтальнойОбласти"
assert ranges[1]["kind"] == "named_area"
def test_analyze_template_structure_reports_area_widths_and_intersections() -> None:
structure = {
"capabilities": {"named_area_coordinates": True, "cell_coordinates": False},
"named_areas": [
{"name": "ОбластьА", "occurrence": 1, "range": adapter_server.moxel_range(0, 0, 0, 2)},
{"name": "ОбластьБ", "occurrence": 1, "range": adapter_server.moxel_range(0, 1, 0, 3)},
],
"merged_range_candidates": [
{"name": "ОбластьА", "range": adapter_server.moxel_range(0, 0, 0, 2), "confidence": "low"}
],
"cell_parameters": [{"name": "ДатаПлана", "row": 1, "column": 1, "source": "placeholder"}],
"cell_text_identifiers": [{"name": "НоменклатураПредставление", "row": 1, "column": 2, "source": "cell_text_identifier"}],
"cell_style_candidates": [
{
"text": "Ячейка 7 - 2",
"tree_position": "$.30",
"coordinate_hints": {
"one_based": {"column": 2},
"zero_based": {"column": 1},
"source": "moxel_schema_rule:inline_text_column_from_last_preceding_scalar_plus_one",
},
}
],
"cell_coordinate_hints": [
{
"text": "Ячейка 7 - 2",
"one_based": {"row": 7, "column": 2},
"confidence": "high",
"source": "moxel_inline_text_coordinate_hint",
}
],
"area_cell_coverage": [{"name": "ОбластьА", "cell_count": 1, "parameter_count": 1}],
"parameters": [{"name": "ДатаПлана"}, {"name": "НоменклатураПредставление"}],
}
analysis = adapter_server.analyze_template_structure(structure)
assert analysis["checks"]["area_widths"] == "ok"
assert analysis["counts"]["area_widths"] == 2
assert analysis["counts"]["intersections_returned"] == 1
assert analysis["counts"]["merged_range_candidates"] == 1
assert analysis["counts"]["cell_parameters"] == 1
assert analysis["counts"]["cell_text_identifiers"] == 1
assert analysis["counts"]["cell_coordinate_hints"] == 1
assert analysis["counts"]["cell_style_coordinate_hints"] == 1
assert analysis["checks"]["parameters_without_cells"] == "ok"
assert analysis["checks"]["cell_coordinate_hints"] == "ok"
assert analysis["checks"]["cell_style_coordinate_hints"] == "ok"
assert analysis["checks"]["merged_cell_candidates"] == "ok"
assert analysis["cell_coordinate_hints"][0]["one_based"] == {"row": 7, "column": 2}
assert analysis["cell_style_coordinate_hints"][0]["coordinate_hints"]["one_based"] == {"column": 2}
assert analysis["intersections"][0]["range"]["one_based"]["left"] == 2
assert analysis["issues"][0]["code"] == "cell_coordinates_not_decoded"
def test_template_response_summary_keeps_counts_and_limits_lists() -> None:
structure = {
"format": "MOXCEL",
"capabilities": {"cell_coordinates": True, "named_area_coordinates": True},
"dimensions": {"rows": 10, "columns": 5},
"capacity_dimensions": {"rows": 10, "columns": 5},
"used_dimensions": {"rows": 3, "columns": 1, "evidence": ["cells"], "bounded_by_capacity": {"rows": True, "columns": True}},
"format_dimensions": {"rows": None, "columns": 1, "evidence": ["column_widths"], "bounded_by_capacity": {"rows": True, "columns": True}},
"named_areas": [
{"name": "Область1", "range": adapter_server.moxel_range(0, 0, 0, 0)},
{"name": "Область2", "range": adapter_server.moxel_range(1, 0, 1, 0)},
],
"cells": [
{"row": 1, "column": 1, "text": "A"},
{"row": 2, "column": 1, "text": "B"},
{"row": 3, "column": 1, "text": "C"},
],
"cell_parameters": [{"name": "ДатаПлана", "row": 1, "column": 1}],
"cell_coordinate_hints": [
{"text": "A", "one_based": {"row": 1, "column": 1}, "confidence": "high"},
{"text": "B", "one_based": {"row": 2, "column": 1}, "confidence": "high"},
],
"cell_style_candidates": [
{
"text": "A",
"tree_position": "$.1",
"coordinate_hints": {"one_based": {"column": 1}, "source": "rule"},
}
],
"column_widths": [{"column": 1, "width": 1000}],
"merged_range_candidates": [{"name": "Область1", "range": adapter_server.moxel_range(0, 0, 1, 0)}],
"moxel_record_diagnostics": [
{
"schema": "moxel_record_diagnostics.v1",
"head_counts": [{"head": 8, "count": 1}, {"head": 16, "count": 2}],
"head_samples": [
{"head": 8, "count": 1, "samples": [{"tree_position": "$", "head": 8}]},
{"head": 16, "count": 2, "samples": [{"tree_position": "$.1", "head": 16}, {"tree_position": "$.2", "head": 16}]},
],
"top_level_records": [
{"tree_position": "$.1", "head": 8, "numeric_items": [8, 1]},
{"tree_position": "$.2", "head": 16, "numeric_items": [16, 2, 10]},
{"tree_position": "$.3", "head": 24, "numeric_items": [24, 3]},
],
"top_level_shapes": [
{"head": 16, "list_length": 4, "numeric_count": 3, "string_count": 0, "count": 2, "positions": ["$.2"], "numeric_prefixes": [[16, 1, 2]]},
{"head": 8, "list_length": 2, "numeric_count": 1, "string_count": 0, "count": 1, "positions": ["$.1"], "numeric_prefixes": [[8]]},
],
"top_level_shape_candidates": [
{
"rank": 1,
"head": 16,
"score": 5,
"reasons": ["rare_shape"],
"positions": ["$.2"],
"confidence": "low",
"source": "heuristic_top_level_shape",
"suggested_windows": [
{
"center": 2,
"start": 0,
"end": 4,
"request_hint": {
"moxel_record_start": 0,
"moxel_record_end": 4,
"moxel_record_heads": "16",
"moxel_record_context": 1,
},
},
{
"center": 2,
"start": 2,
"end": 2,
"request_hint": {
"moxel_record_start": 2,
"moxel_record_end": 2,
"moxel_record_heads": "16",
"moxel_record_context": 0,
},
},
],
},
{
"rank": 2,
"head": 8,
"score": 5,
"reasons": ["rare_shape"],
"positions": ["$.1"],
"confidence": "low",
"source": "heuristic_top_level_shape",
"suggested_windows": [{"center": 1, "start": 0, "end": 3, "request_hint": {"moxel_record_heads": "8"}}],
},
],
"top_level_candidate_summary": {
"total": 2,
"score_min": 5,
"score_max": 5,
"score_counts": [{"score": 5, "count": 2}],
"reason_counts": [{"reason": "rare_shape", "count": 2}],
},
"samples": [{"head": 8}, {"head": 16}],
"coordinate_like_samples": [{"head": 16}],
}
],
"strings_sample": ["Строка", "ЕщеСтрока"],
"payload": {"compression": "raw_deflate", "encoding": "cp1251"},
"tree_root_summary": {"type": "list", "items_count": 3, "head": "8"},
"moxel_text_excerpt": "0123456789ABCDEFGHIJ",
}
result = {
"schema": "onec_templates_analyze.v1",
"status": "ok",
"templates": [{"name": "Макет", "parts": [{"part_id": "x"}], "structure": structure, "analysis": adapter_server.analyze_template_structure(structure)}],
}
compact = adapter_server.apply_template_response_view(
result,
{"view": "summary", "max_cells": 1, "max_areas": 1},
default_view="summary",
map_mode=True,
)
assert compact["schema"] == "onec_templates_map.v1"
assert compact["view"] == "summary"
template = compact["templates"][0]
assert "parts" not in template
assert template["structure"]["capacity_dimensions"] == {"rows": 10, "columns": 5}
assert template["structure"]["used_dimensions"]["columns"] == 1
assert template["structure"]["format_dimensions"]["evidence"] == ["column_widths"]
assert template["structure"]["counts"]["cells"] == 3
assert template["structure"]["counts"]["cell_coordinate_hints"] == 2
assert template["structure"]["counts"]["cell_style_coordinate_hints"] == 1
assert len(template["structure"]["samples"]["cells"]) == 1
assert len(template["structure"]["samples"]["cell_coordinate_hints"]) == 1
assert template["analysis"]["counts"]["cells"] == 3
assert template["analysis"]["counts"]["cell_coordinate_hints"] == 2
assert "cells_sample" not in template["analysis"]
hints_compact = adapter_server.apply_template_response_view(
result,
{"view": "summary", "sections": "coordinate_hints", "max_cells": 1},
default_view="summary",
map_mode=True,
)
hints_template = hints_compact["templates"][0]
assert len(hints_template["structure"]["cell_coordinate_hints"]) == 1
assert len(hints_template["analysis"]["cell_coordinate_hints"]) == 1
assert len(hints_template["analysis"]["cell_style_coordinate_hints"]) == 1
diagnostics_compact = adapter_server.apply_template_response_view(
result,
{"view": "summary", "sections": "moxel_records", "max_moxel_records": 1},
default_view="summary",
map_mode=True,
)
diagnostics = diagnostics_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
assert len(diagnostics["head_counts"]) == 1
assert len(diagnostics["head_samples"]) == 1
assert diagnostics["head_samples"][0]["samples"][0]["tree_position"] == "$"
assert len(diagnostics["top_level_records"]) == 1
assert diagnostics["top_level_record_summary"]["total"] == 1
assert diagnostics["top_level_record_summary"]["head_counts"] == [{"head": 8, "count": 1}]
assert len(diagnostics["top_level_shapes"]) == 1
assert len(diagnostics["top_level_shape_candidates"]) == 1
assert diagnostics["top_level_candidate_summary"]["returned_count"] == 1
assert len(diagnostics["samples"]) == 1
assert len(diagnostics["coordinate_like_samples"]) == 1
window_compact = adapter_server.apply_template_response_view(
result,
{
"view": "summary",
"sections": "moxel_records",
"max_moxel_records": 10,
"moxel_record_start": 2,
"moxel_record_end": 2,
},
default_view="summary",
map_mode=True,
)
window_diagnostics = window_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
assert window_diagnostics["top_level_window"] == {"start": 2, "end": 2, "source": "moxel_record_start/moxel_record_end"}
assert window_diagnostics["top_level_records"] == [{"tree_position": "$.2", "head": 16, "numeric_items": [16, 2, 10]}]
assert window_diagnostics["top_level_record_summary"]["position_range"] == {"start": 2, "end": 2}
head_compact = adapter_server.apply_template_response_view(
result,
{
"view": "summary",
"sections": "moxel_records",
"max_moxel_records": 10,
"moxel_record_heads": "16",
},
default_view="summary",
map_mode=True,
)
head_diagnostics = head_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
assert head_diagnostics["top_level_head_filter"] == [16]
assert head_diagnostics["top_level_records"] == [{"tree_position": "$.2", "head": 16, "numeric_items": [16, 2, 10]}]
context_compact = adapter_server.apply_template_response_view(
result,
{
"view": "summary",
"sections": "moxel_records",
"max_moxel_records": 10,
"moxel_record_heads": [16],
"moxel_record_context": 1,
},
default_view="summary",
map_mode=True,
)
context_diagnostics = context_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
assert context_diagnostics["top_level_context"] == {"radius": 1, "source": "moxel_record_context", "match_field": "match"}
assert context_diagnostics["top_level_records"] == [
{"tree_position": "$.1", "head": 8, "numeric_items": [8, 1], "match": False},
{"tree_position": "$.2", "head": 16, "numeric_items": [16, 2, 10], "match": True},
{"tree_position": "$.3", "head": 24, "numeric_items": [24, 3], "match": False},
]
assert context_diagnostics["top_level_record_summary"]["matched_count"] == 1
undecoded_compact = adapter_server.apply_template_response_view(
result,
{
"view": "summary",
"sections": "undecoded_evidence,payload,tree_root",
"max_strings": 1,
"max_moxel_records": 1,
"max_excerpt_chars": 10,
},
default_view="summary",
map_mode=True,
)
undecoded_structure = undecoded_compact["templates"][0]["structure"]
assert undecoded_structure["payload"] == {"compression": "raw_deflate", "encoding": "cp1251"}
assert undecoded_structure["tree_root_summary"] == {"type": "list", "items_count": 3, "head": "8"}
assert undecoded_structure["undecoded_evidence"]["payload"] == {"compression": "raw_deflate", "encoding": "cp1251"}
assert undecoded_structure["undecoded_evidence"]["tree_root_summary"] == {"type": "list", "items_count": 3, "head": "8"}
assert undecoded_structure["undecoded_evidence"]["strings_sample"] == ["Строка"]
assert undecoded_structure["undecoded_evidence"]["moxel_text_excerpt"] == "0123456789"
assert len(undecoded_structure["undecoded_evidence"]["coordinate_like_samples"]) == 1
candidate_focus_compact = adapter_server.apply_template_response_view(
result,
{
"view": "summary",
"sections": "moxel_records",
"max_moxel_records": 10,
"moxel_candidate_rank": 1,
},
default_view="summary",
map_mode=True,
)
candidate_focus = candidate_focus_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
assert candidate_focus["top_level_candidate_focus"]["rank"] == 1
assert candidate_focus["top_level_candidate_focus"]["window_index"] == 1
assert candidate_focus["top_level_candidate_focus"]["status"] == "ok"
assert candidate_focus["top_level_head_filter"] == [16]
assert candidate_focus["top_level_records"] == [
{"tree_position": "$.1", "head": 8, "numeric_items": [8, 1], "match": False},
{"tree_position": "$.2", "head": 16, "numeric_items": [16, 2, 10], "match": True},
{"tree_position": "$.3", "head": 24, "numeric_items": [24, 3], "match": False},
]
assert candidate_focus["top_level_record_summary"]["numeric_field_summary"][0]["head"] == 8
candidate_second_window_compact = adapter_server.apply_template_response_view(
result,
{
"view": "summary",
"sections": "moxel_records",
"max_moxel_records": 10,
"moxel_candidate_rank": 1,
"moxel_candidate_window_index": 2,
},
default_view="summary",
map_mode=True,
)
candidate_second_window = candidate_second_window_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
assert candidate_second_window["top_level_candidate_focus"]["rank"] == 1
assert candidate_second_window["top_level_candidate_focus"]["window_index"] == 2
assert candidate_second_window["top_level_candidate_focus"]["status"] == "ok"
assert candidate_second_window["top_level_window"] == {"start": 2, "end": 2, "source": "moxel_record_start/moxel_record_end"}
assert candidate_second_window["top_level_records"] == [{"tree_position": "$.2", "head": 16, "numeric_items": [16, 2, 10]}]
candidate_missing_window_compact = adapter_server.apply_template_response_view(
result,
{
"view": "summary",
"sections": "moxel_records",
"max_moxel_records": 10,
"moxel_candidate_rank": 1,
"moxel_candidate_window_index": 3,
},
default_view="summary",
map_mode=True,
)
candidate_missing_window = candidate_missing_window_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
assert candidate_missing_window["top_level_candidate_focus"]["rank"] == 1
assert candidate_missing_window["top_level_candidate_focus"]["window_index"] == 3
assert candidate_missing_window["top_level_candidate_focus"]["status"] == "window_not_found"
assert candidate_missing_window["top_level_candidate_focus"]["available_windows"] == 2
reason_filter_compact = adapter_server.apply_template_response_view(
result,
{
"view": "summary",
"sections": "moxel_records",
"max_moxel_records": 10,
"moxel_candidate_reasons": "rare_shape",
},
default_view="summary",
map_mode=True,
)
reason_filter = reason_filter_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
assert reason_filter["top_level_candidate_reason_filter"] == ["rare_shape"]
assert [candidate["rank"] for candidate in reason_filter["top_level_shape_candidates"]] == [1, 2]
min_score_compact = adapter_server.apply_template_response_view(
result,
{
"view": "summary",
"sections": "moxel_records",
"max_moxel_records": 10,
"moxel_candidate_min_score": 6,
},
default_view="summary",
map_mode=True,
)
min_score = min_score_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
assert min_score["top_level_candidate_min_score"] == 6
assert min_score["top_level_candidate_summary"]["total"] == 2
assert min_score["top_level_candidate_summary"]["returned_count"] == 0
assert min_score["top_level_shape_candidates"] == []
candidate_head_compact = adapter_server.apply_template_response_view(
result,
{
"view": "summary",
"sections": "moxel_records",
"max_moxel_records": 10,
"moxel_candidate_heads": "8",
},
default_view="summary",
map_mode=True,
)
candidate_head = candidate_head_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
assert candidate_head["top_level_candidate_head_filter"] == [8]
assert [candidate["rank"] for candidate in candidate_head["top_level_shape_candidates"]] == [2]
candidate_window_compact = adapter_server.apply_template_response_view(
result,
{
"view": "summary",
"sections": "moxel_records",
"max_moxel_records": 10,
"moxel_candidate_start": 2,
"moxel_candidate_end": 2,
},
default_view="summary",
map_mode=True,
)
candidate_window = candidate_window_compact["templates"][0]["structure"]["moxel_record_diagnostics"][0]
assert candidate_window["top_level_candidate_window_filter"] == {
"start": 2,
"end": 2,
"source": "moxel_candidate_start/moxel_candidate_end",
}
assert [candidate["rank"] for candidate in candidate_window["top_level_shape_candidates"]] == [1]
def test_templates_read_validates_moxel_candidate_window_index() -> None:
payload = {
"base_id": "upo_test",
"table": "ConfigCAS",
"file_name": "abc",
"moxel_candidate_rank": 1,
"moxel_candidate_window_index": 0,
}
result = adapter_server.validate_templates_read_payload(payload, "templates.map")
assert result["status"] == "invalid_argument"
assert result["argument"] == "moxel_candidate_window_index"
def test_moxel_top_level_record_summary_includes_numeric_field_hints() -> None:
records = [
{"tree_position": "$.10", "head": 100, "numeric_items": [100, 2, 84, 0], "match": False},
{"tree_position": "$.11", "head": 100, "numeric_items": [100, 2, 90, 1], "match": True},
{"tree_position": "$.12", "head": 100, "numeric_items": [100, 2, 120, 1], "match": True},
]
summary = adapter_server.summarize_moxel_top_level_records(records)
numeric_summary = summary["numeric_field_summary"][0]
assert numeric_summary["head"] == 100
assert numeric_summary["varying_fields"][0]["index"] == 2
assert numeric_summary["field_hints"] == [
{
"index": 2,
"kind": "coordinate_or_offset_like",
"confidence": "low",
"reason": "field is non-negative, varies across records, and has a wider numeric span",
"min": 84,
"max": 120,
"values": [84, 90, 120],
},
{
"index": 3,
"kind": "flag_like",
"confidence": "low",
"reason": "field varies only between 0 and 1 in the returned records",
"values": [0, 1],
},
]
assert numeric_summary["numeric_field_matrix"] == {
"fields": [2, 3],
"rows": [
{"tree_position": "$.10", "values": {"2": 84, "3": 0}, "match": False},
{"tree_position": "$.11", "values": {"2": 90, "3": 1}, "match": True},
{"tree_position": "$.12", "values": {"2": 120, "3": 1}, "match": True},
],
"field_runs": [
{
"field": 2,
"runs": [
{"value": 84, "start": "$.10", "end": "$.10", "rows": 1, "matched_count": 0},
{"value": 90, "start": "$.11", "end": "$.11", "rows": 1, "matched_count": 1},
{"value": 120, "start": "$.12", "end": "$.12", "rows": 1, "matched_count": 1},
],
},
{
"field": 3,
"runs": [
{"value": 0, "start": "$.10", "end": "$.10", "rows": 1, "matched_count": 0},
{"value": 1, "start": "$.11", "end": "$.12", "rows": 2, "matched_count": 2},
],
},
],
"field_transitions": [
{
"field": 2,
"transitions": [
{
"from": 84,
"to": 90,
"before": "$.10",
"after": "$.11",
"before_rows": 1,
"after_rows": 1,
"before_matched_count": 0,
"after_matched_count": 1,
},
{
"from": 90,
"to": 120,
"before": "$.11",
"after": "$.12",
"before_rows": 1,
"after_rows": 1,
"before_matched_count": 1,
"after_matched_count": 1,
},
],
},
{
"field": 3,
"transitions": [
{
"from": 0,
"to": 1,
"before": "$.10",
"after": "$.11",
"before_rows": 1,
"after_rows": 2,
"before_matched_count": 0,
"after_matched_count": 2,
}
],
},
],
}
def test_extract_moxel_cell_style_candidates_from_inline_text_cell() -> None:
tree = parse_brace_text(
'{8,1,12,{"ru","ru",1,1,"ru","Русский","Русский",1},{128,72},'
'{0,0},{0,0},1,2,1,3,0,1,1,'
'{16,1,{1,1,{"ru","пример ячейка 4 2"}},0},'
'{2,0,00000000-0000-0000-0000-000000000000,1,1,2}}'
)
candidates = adapter_server.extract_moxel_cell_style_candidates_from_tree(tree)
assert len(candidates) == 1
candidate = candidates[0]
assert candidate["type_code"] == 16
assert candidate["cell_id"] == 1
assert candidate["text"] == "пример ячейка 4 2"
assert candidate["texts"] == ["пример ячейка 4 2"]
assert candidate["source"] == "moxel_inline_text_cell"
assert candidate["confidence"] == "low"
assert candidate["coordinate_hints"] == {
"one_based": {"column": 2},
"zero_based": {"column": 1},
"confidence": "high",
"source": "moxel_schema_rule:inline_text_column_from_last_preceding_scalar_plus_one",
}
assert candidate["style_evidence"]["last_7_preceding_values"] == ["1", "2", "1", "3", "0", "1", "1"]
assert candidate["style_evidence"]["immediate_preceding_values"] == ["1", "2", "1", "3", "0", "1", "1"]
assert candidate["next_moxel_record"]["head"] == "2"
assert candidate["next_moxel_record"]["scalar_prefix"] == ["2", "0", "00000000-0000-0000-0000-000000000000", "1", "1", "2"]
assert adapter_server.moxel_structure_counts({"cell_style_candidates": candidates})["cell_style_coordinate_hints"] == 1
def test_extract_moxel_cell_coordinate_hints_links_style_column_to_decoded_row() -> None:
hints = adapter_server.extract_moxel_cell_coordinate_hints(
[{"row": 4, "column": 9, "cell_id": 1, "text": "пример ячейка 4 2", "one_based": {"row": 4, "column": 9}, "zero_based": {"row": 3, "column": 8}}],
[
{
"cell_id": 1,
"text": "пример ячейка 4 2",
"tree_position": "$.20",
"coordinate_hints": {
"one_based": {"column": 2},
"zero_based": {"column": 1},
"confidence": "high",
"source": "moxel_schema_rule:inline_text_column_from_last_preceding_scalar_plus_one",
},
}
],
)
assert hints == [
{
"text": "пример ячейка 4 2",
"cell_id": 1,
"tree_position": "$.20",
"one_based": {"row": 4, "column": 2},
"zero_based": {"row": 3, "column": 1},
"confidence": "high",
"source": "moxel_inline_text_coordinate_hint",
"evidence": {
"column": "moxel_schema_rule:inline_text_column_from_last_preceding_scalar_plus_one",
"row": "matched_decoded_cell_row",
"matched_cell": {"row": 4, "column": 9, "cell_id": 1, "source": None},
},
}
]
def test_extract_moxel_cell_style_candidates_from_nested_inline_text_cell() -> None:
tree = parse_brace_text(
'{8,1,12,{99,{4,0,1,4,{16,7,{1,1,{"ru","nested style text"}},0}}}}'
)
candidates = adapter_server.extract_moxel_cell_style_candidates_from_tree(tree)
assert len(candidates) == 1
assert candidates[0]["tree_position"] == "$.3.1.4"
assert candidates[0]["text"] == "nested style text"
assert candidates[0]["coordinate_hints"]["one_based"] == {"column": 5}
def test_extract_moxel_cells_from_tree_uses_row_run_columns() -> None:
tree = parse_brace_text(
'{9,0,3,1,'
'{16,0,{1,1,{"ru","10-2"}},0},2,'
'{16,0,{1,1,{"ru","10-3"}},0},3,'
'{16,0,{1,1,{"ru","10-4"}},0}}'
)
cells = adapter_server.extract_moxel_cells_from_tree(tree)
assert [cell["text"] for cell in cells] == ["10-2", "10-3", "10-4"]
assert [(cell["one_based"]["row"], cell["one_based"]["column"]) for cell in cells] == [
(10, 2),
(10, 3),
(10, 4),
]
def test_extract_moxel_cells_from_tree_uses_flag_as_first_column_for_single_cell_row() -> None:
tree = parse_brace_text('{4,0,1,2,{16,2,{1,1,{"ru","Ячейка 5 - 3"}},0}}')
cells = adapter_server.extract_moxel_cells_from_tree(tree)
assert len(cells) == 1
assert cells[0]["text"] == "Ячейка 5 - 3"
assert cells[0]["one_based"] == {"row": 5, "column": 3}
def test_extract_moxel_cells_from_tree_accepts_next_row_header_with_zero_flag() -> None:
tree = parse_brace_text(
'{10,0,3,1,'
'{16,0,{1,1,{"ru","11-2"}},0},2,'
'{16,0,{1,1,{"ru","11-3"}},0},3,'
'{16,0,{1,1,{"ru","11-4"}},0},'
'11,0,4,0,'
'{16,0,{1,1,{"ru","12-1"}},0},1,'
'{16,0,{1,1,{"ru","12-2"}},0},2,'
'{16,0,{1,1,{"ru","12-3"}},0},3,'
'{16,0,{1,1,{"ru","12-4"}},0}}'
)
cells = adapter_server.extract_moxel_cells_from_tree(tree)
assert [(cell["text"], cell["one_based"]["row"], cell["one_based"]["column"]) for cell in cells] == [
("11-2", 11, 2),
("11-3", 11, 3),
("11-4", 11, 4),
("12-1", 12, 1),
("12-2", 12, 2),
("12-3", 12, 3),
("12-4", 12, 4),
]
def test_extract_moxel_cells_from_tree_falls_back_to_legacy_cell_column_pairs() -> None:
tree = parse_brace_text(
'{9,0,2,0,'
'{16,0,{1,1,{"ru","legacy-1"}},0},1,'
'{16,0,{1,1,{"ru","legacy-2"}},0},4}'
)
cells = adapter_server.extract_moxel_cells_from_tree(tree)
assert [cell["text"] for cell in cells] == ["legacy-1", "legacy-2"]
assert [(cell["one_based"]["row"], cell["one_based"]["column"]) for cell in cells] == [
(10, 2),
(10, 5),
]
def test_payload_public_preview_hides_stream_coordinates() -> None:
preview = payload_public_preview(
{
"stream_blocks": [
{
"header_offset": 12,
"data_offset": 30,
"sha1": "abc",
"encoding": "utf-8-sig",
"text_preview": "Процедура Команда()",
"has_bsl_marker": True,
}
],
"base64_blocks": [
{
"block_length": 100,
"sha1": "def",
"encoding": "utf-8",
"text_preview": "",
"has_html_marker": True,
}
],
}
)
assert preview["streams"][0]["text_preview"] == "Процедура Команда()"
assert preview["streams"][0]["has_bsl_marker"] is True
assert "header_offset" not in preview["streams"][0]
assert "sha1" not in preview["streams"][0]
assert preview["base64"][0]["has_html_marker"] is True
assert "block_length" not in preview["base64"][0]
def test_payload_diff_compares_inline_text_tree_and_strings() -> None:
result = adapter_server.payload_diff(
{
"diagnostic": True,
"before": {"text": '{1,"Пример"}'},
"after": {"text": '{1,"Проверка"}'},
"max_changes": 20,
"max_text_diff_lines": 20,
}
)
assert result["status"] == "changed"
assert result["bytes"]["same"] is False
assert result["text"]["same"] is False
assert any("Пример" in line for line in result["text"]["diff_lines"])
assert any("Проверка" in line for line in result["text"]["diff_lines"])
assert result["tree"]["changes"] == [{"path": "$.1", "old": "Пример", "new": "Проверка"}]
assert result["strings"]["changes"] == [{"index": 0, "old": "Пример", "new": "Проверка"}]
def test_payload_diff_reads_live_sources(monkeypatch: pytest.MonkeyPatch) -> None:
payloads = {
("ConfigSave", "before"): '{1,"До"}'.encode("utf-8-sig"),
("ConfigSave", "after"): '{1,"После"}'.encode("utf-8-sig"),
}
def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30):
return payloads[(table, file_name)], {"database": base_id}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.payload_diff(
{
"diagnostic": True,
"base_id": "upo_test",
"before": {"table": "ConfigSave", "file_name": "before"},
"after": {"table": "ConfigSave", "file_name": "after"},
"include_evidence": False,
}
)
assert result["status"] == "changed"
assert result["source"]["before"]["file_name"] == "before"
assert result["source"]["after"]["file_name"] == "after"
assert result["tree"]["changes"][0] == {"path": "$.1", "old": "До", "new": "После"}
assert "evidence" not in result
def test_payload_diff_requires_diagnostic() -> None:
result = adapter_server.payload_diff({"before": {"text": "{1}"}, "after": {"text": "{2}"}})
assert result["status"] == "invalid_argument"
assert result["argument"] == "diagnostic"
def test_payload_public_undecoded_evidence_hides_block_coordinates() -> None:
evidence = payload_public_undecoded_evidence(
{
"status": "ok",
"role": "bsl_module_payload",
"compression": "raw_deflate",
"encoding": "cp1251",
"raw_bytes": 10,
"payload_bytes": 20,
"sha1": "abc",
"payload_sha1": "def",
"markers": ["marker1", "marker2"],
"root": {"root_marker": "1"},
"strings_sample": ["Строка"],
"counts": {"stream_blocks": 1, "base64_blocks": 1},
"text": "0123456789ABCDE",
"stream_blocks": [
{
"header_offset": 12,
"data_offset": 30,
"sha1": "hidden",
"encoding": "utf-8-sig",
"text_preview": "Процедура Команда()",
"has_bsl_marker": True,
}
],
"base64_blocks": [
{
"block_length": 100,
"sha1": "hidden2",
"encoding": "utf-8",
"text_preview": "",
"has_html_marker": True,
}
],
},
max_excerpt_chars=10,
)
assert evidence["payload"]["encoding"] == "cp1251"
assert evidence["text_excerpt"] == "0123456789"
assert evidence["stream_blocks_sample"][0]["has_bsl_marker"] is True
assert "header_offset" not in evidence["stream_blocks_sample"][0]
assert "sha1" not in evidence["stream_blocks_sample"][0]
assert evidence["base64_blocks_sample"][0]["has_html_marker"] is True
assert "block_length" not in evidence["base64_blocks_sample"][0]
def test_payload_public_undecoded_evidence_raw_requires_storage_details() -> None:
classification = {
"status": "ok",
"role": "bsl_module_payload",
"compression": "none",
"encoding": "utf-8",
"raw_bytes": 10,
"payload_bytes": 10,
"sha1": "payload-sha1",
"payload_sha1": "payload-sha1",
"stream_blocks": [
{
"header_offset": 12,
"data_offset": 30,
"declared_1": 8,
"declared_2": 8,
"bytes": 8,
"sha1": "block-sha1",
"encoding": "utf-8",
"text_preview": "Процедура",
"text": "Процедура Команда()\nКонецПроцедуры",
"has_bsl_marker": True,
}
],
"base64_blocks": [],
}
public_raw = payload_public_undecoded_evidence(classification, mode="raw", allow_storage_details=False)
storage_raw = payload_public_undecoded_evidence(classification, mode="raw", allow_storage_details=True)
assert "header_offset" not in public_raw["stream_blocks_sample"][0]
assert public_raw["stream_blocks_sample"][0]["text_excerpt"].startswith("Процедура")
assert storage_raw["stream_blocks_sample"][0]["header_offset"] == 12
assert storage_raw["stream_blocks_sample"][0]["sha1"] == "block-sha1"
def test_metadata_object_parts_exposes_undecoded_evidence_without_storage(monkeypatch: pytest.MonkeyPatch) -> None:
payload_text = '{1,"Тест"}'
payload_bytes = compress_payload(payload_text.encode("cp1251"), "raw_deflate")
monkeypatch.setattr(
adapter_server,
"resolve_object_guid",
lambda *args, **kwargs: ("guid-1", "Document", {"guid": "guid-1", "kind": "Document"}, None),
)
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda *args, **kwargs: {"status": "ok", "files": [{"FileName": "guid-1"}]},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({"guid-1": payload_bytes}, {"database": "db"}, None),
)
result = adapter_server.metadata_object_parts({"base_id": "upo_test", "guid": "guid-1", "include_text": True})
assert result["status"] == "ok"
part = result["parts"][0]
assert part["content_kind"] == "metadata"
assert part["undecoded_evidence"]["payload"]["compression"] == "raw_deflate"
assert part["undecoded_evidence"]["strings_sample"] == ["Тест"]
def test_metadata_object_parts_raw_evidence_exposes_storage_when_requested(monkeypatch: pytest.MonkeyPatch) -> None:
payload_bytes = b"\r\n00000004 00000004 7fffffff \r\ntext"
monkeypatch.setattr(
adapter_server,
"resolve_object_guid",
lambda *args, **kwargs: ("guid-1", "Document", {"guid": "guid-1", "kind": "Document"}, None),
)
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda *args, **kwargs: {"status": "ok", "files": [{"FileName": "guid-1"}]},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda *args, **kwargs: ({"guid-1": payload_bytes}, {"database": "db"}, None),
)
result = adapter_server.metadata_object_parts(
{
"base_id": "upo_test",
"guid": "guid-1",
"include_storage": True,
"evidence_mode": "raw",
}
)
assert result["status"] == "ok"
assert result["parts"][0]["undecoded_evidence"]["stream_blocks_sample"][0]["header_offset"] == 0
assert result["parts"][0]["undecoded_evidence"]["stream_blocks_sample"][0]["bytes"] == 4
def test_metadata_form_decode_returns_undecoded_evidence_for_raw_binary(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (b"\x00\x01\x02raw-binary", {"database": "db"}, None),
)
result = adapter_server.metadata_form_decode({"base_id": "upo_test", "file_name": "form-guid"})
assert result["status"] == "undecodable"
assert result["undecoded_evidence"]["payload"]["raw_bytes"] == len(b"\x00\x01\x02raw-binary")
assert result["undecoded_evidence"]["role"] in {"binary_or_unknown_payload", "stream_container", "brace_payload", "form_payload", "metadata_payload", "template_payload", "help_or_html_payload", "bsl_module_payload"}
def test_mcp_full_sections_validation() -> None:
payload: dict[str, object] = {"base_id": "upo_test", "sections": "forms"}
error = validate_metadata_object_full_sections(payload)
assert error is not None
assert error["status"] == "invalid_argument"
assert error["argument"] == "sections"
payload = {"sections": ["forms", "card", "forms"]}
error = validate_metadata_object_full_sections(payload)
assert error is None
assert payload["sections"] == ["forms", "card"]
payload = {"sections": []}
error = validate_metadata_object_full_sections(payload)
assert error is not None
assert error["status"] == "invalid_argument"
payload = {"sections": ["unknown"]}
error = validate_metadata_object_full_sections(payload)
assert error is not None
assert error["status"] == "invalid_argument"
payload = {"sections": [1]}
error = validate_metadata_object_full_sections(payload)
assert error is not None
assert error["status"] == "invalid_argument"
payload = {"sections": ["all"]}
error = validate_metadata_object_full_sections(payload)
assert error is None
assert payload["sections"] == ["card", "semantic", "modules", "templates", "forms", "commands"]
def test_adapter_full_sections_validation() -> None:
payload: dict[str, object] = {"base_id": "upo_test", "sections": "forms"}
error = validate_metadata_object_full_payload(payload)
assert error is not None
assert error["status"] == "invalid_argument"
assert error["argument"] == "sections"
payload = {"base_id": "upo_test", "sections": ["forms", "card", "forms"]}
error = validate_metadata_object_full_payload(payload)
assert error is None
assert payload["_sections"] == ["forms", "card"]
payload = {"base_id": "upo_test", "sections": []}
error = validate_metadata_object_full_payload(payload)
assert error is not None
assert error["status"] == "invalid_argument"
assert error["argument"] == "sections"
payload = {"base_id": "upo_test", "sections": ["unknown"]}
error = validate_metadata_object_full_payload(payload)
assert error is not None
assert error["status"] == "invalid_argument"
assert error["argument"] == "sections"
payload = {"base_id": "upo_test", "sections": [1]}
error = validate_metadata_object_full_payload(payload)
assert error is not None
assert error["status"] == "invalid_argument"
assert error["argument"] == "sections"
payload = {"base_id": "upo_test", "sections": ["all"]}
error = validate_metadata_object_full_payload(payload)
assert error is None
assert payload["_sections"] == ["card", "semantic", "modules", "templates", "forms", "commands"]
def test_form_element_write_requires_saved_state_opt_in() -> None:
result = adapter_server.metadata_form_element_write(
{"base_id": "upo_test", "table": "ConfigSave", "form_guid": "form-guid", "element": "Кнопка", "property": "title", "value": "Новый"}
)
assert result["status"] == "invalid_argument"
assert result["argument"] == "allow_saved_state_write"
def test_form_element_write_rejects_active_config_table() -> None:
result = adapter_server.metadata_form_element_write(
{
"base_id": "upo_test",
"table": "Config",
"form_guid": "form-guid",
"element": "Кнопка",
"allow_saved_state_write": True,
"property": "title",
"value": "Новый",
}
)
assert result["status"] == "invalid_argument"
assert result["argument"] == "table"
def test_form_element_write_keeps_owner_guid_out_of_form_guid_resolution(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
owner_guid = "22222222-2222-2222-2222-222222222222"
def fake_resolve(payload: dict[str, Any]) -> dict[str, Any]:
seen["resolve_payload"] = payload
return {
"schema": "onec_form_write_target_resolution.v1",
"status": "not_found",
"base_id": payload["base_id"],
"diagnostics": {"message": "test stop after selector normalization"},
}
monkeypatch.setattr(adapter_server, "metadata_form_write_target_resolve", fake_resolve)
result = adapter_server.call_method(
"metadata.form.element.write",
{
"base_id": "upo_test",
"ref": "Обработки.ВторойОбмен",
"object_guid": owner_guid,
"form_name": "ФормаОбмена",
"element_name": "КнопкаВыполнить",
"property": "title",
"value": "Выполнить",
"allow_saved_state_write": True,
},
)
assert result["status"] == "not_found"
assert seen["resolve_payload"]["kind"] == "DataProcessor"
assert seen["resolve_payload"]["name"] == "ВторойОбмен"
assert seen["resolve_payload"]["guid"] == owner_guid
assert seen["resolve_payload"]["form_name"] == "ФормаОбмена"
assert seen["resolve_payload"].get("form_guid") is None
def test_form_element_write_help_exposes_owner_form_and_write_guards() -> None:
methods = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}
planner = methods["metadata.form.element.write"]
apply = methods["metadata.form.element.write_apply"]
for method in (planner, apply):
assert method["selector_capabilities"]["accepts_ordinal"] is False
assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name", "form_guid"]
assert method["selector_capabilities"]["child_selector_arguments"] == [
"element",
"element_name",
"command",
"attribute",
]
assert method["input_schema"]["properties"]["object_guid"]["type"] == "string"
assert method["input_schema"]["properties"]["form_guid"]["type"] == "string"
assert planner["selector_capabilities"]["write_guards"] == ["allow_saved_state_write", "proposal_only"]
assert "allow_sql_saved_state_apply" in apply["selector_capabilities"]["write_guards"]
def test_form_target_move_resolves_nested_form_by_owner_before_decode(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
owner_guid = "22222222-2222-2222-2222-222222222222"
def fake_resolve(payload: dict[str, Any]) -> dict[str, Any]:
seen["resolve_payload"] = payload
return {
"schema": "onec_form_write_target_resolution.v1",
"status": "not_found",
"base_id": payload["base_id"],
"diagnostics": {"message": "test stop after owner/form resolution"},
}
monkeypatch.setattr(adapter_server, "metadata_form_write_target_resolve", fake_resolve)
result = adapter_server.call_method(
"metadata.form.target.move",
{
"base_id": "upo_test",
"ref": "Обработки.ВторойОбмен",
"object_guid": owner_guid,
"form_name": "ФормаОбмена",
"from_element": "КнопкаСтарая",
"to_element": "КнопкаНовая",
"allow_saved_state_write": True,
},
)
assert result["status"] == "not_found"
assert seen["resolve_payload"]["kind"] == "DataProcessor"
assert seen["resolve_payload"]["name"] == "ВторойОбмен"
assert seen["resolve_payload"]["guid"] == owner_guid
assert seen["resolve_payload"]["form_name"] == "ФормаОбмена"
assert seen["resolve_payload"]["element"] == "КнопкаСтарая"
assert seen["resolve_payload"].get("form_guid") is None
def test_form_write_target_resolve_preserves_legacy_guid_only_form_selector(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
seen["decode_payload"] = payload
return {"schema": "onec_form_decode.v1", "status": "not_found"}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
result = adapter_server.metadata_form_write_target_resolve(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"guid": "legacy-form-guid",
"element": "Кнопка",
}
)
assert result["status"] == "not_found"
assert seen["decode_payload"]["form_guid"] == "legacy-form-guid"
assert "guid" not in seen["decode_payload"]
def test_form_element_write_builds_saved_state_change_proposal(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
seen["decode"] = payload
return {
"status": "ok",
"source": {"kind": "live_sql", "table": "ConfigSave", "file_name": "form-guid.0"},
"form": {"guid": "form-guid", "file_name": "form-guid.0"},
"profile": {
"items": [
{
"name": "СписокАрхивироватьДокументы",
"name_path": "1.27.5",
"id": "172",
"id_path": "1.27.1.0",
"title": "Архивировать",
"title_path": "1.27.6.2.1",
"path": "1.27",
"parameters": [
{"index": 7, "presentation": "Видимость", "value": "1"},
],
}
]
},
}
def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
seen["changes"] = payload
return {
"schema": "onec_change_proposal.v1",
"status": "accepted_for_review",
"applied": False,
"edits": payload["edits"],
"source": payload["source"],
}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)
result = adapter_server.metadata_form_element_write(
{
"base_id": "upo_test",
"table": "ConfigSave",
"form_guid": "form-guid",
"element": "СписокАрхивироватьДокументы",
"allow_saved_state_write": True,
"edits": [
{"property": "title", "value": "В архив"},
{"property": "Видимость", "value": False},
],
}
)
assert seen["decode"]["include_storage"] is True
assert seen["decode"]["include_parameters"] is True
assert seen["changes"]["source"] == {"base_id": "upo_test", "table": "ConfigSave", "file_name": "form-guid.0"}
assert seen["changes"]["edits"] == [
{"path": "1.27.6.2.1", "value": "В архив", "node_type": "auto", "property": "title", "old": "Архивировать"},
{"path": "1.27.7", "value": "0", "node_type": "auto", "property": "Видимость", "old": "1"},
]
assert result["status"] == "accepted_for_review"
assert result["write_mode"]["sql_write_performed"] is False
assert result["element"]["name"] == "СписокАрхивироватьДокументы"
def test_form_element_write_builds_saved_state_command_proposal(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
"form": {"guid": "form-guid", "file_name": "form-guid.0"},
"profile": {
"items": [],
"commands": [
{
"name": "КомандаПример1",
"id": "2",
"title": "Пример1",
"path": "5.3",
"id_path": "5.3.1.0",
"title_path": "5.3.3.2.1",
"parameters": [{"index": 3, "presentation": "Заголовок", "value": ""}],
}
],
},
}
def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
seen["changes"] = payload
return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)
result = adapter_server.metadata_form_element_write(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"element": "КомандаПример1",
"allow_saved_state_write": True,
"edits": [{"property": "title", "value": "ПРОВЕРКА"}],
}
)
assert seen["changes"]["edits"] == [
{"path": "5.3.3.2.1", "value": "ПРОВЕРКА", "node_type": "auto", "property": "title", "old": "Пример1"}
]
assert result["status"] == "accepted_for_review"
assert result["element"]["section"] == "commands"
assert result["element"]["name"] == "КомандаПример1"
def test_form_element_write_routes_empty_element_title_to_linked_command(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
"form": {"guid": "form-guid", "file_name": "form-guid.0"},
"profile": {
"items": [
{
"name": "КомандаПример1",
"title": None,
"path": "1.10",
"title_path": "1.10.6",
"marker": "31",
}
],
"commands": [
{
"name": "КомандаПример1",
"id": "2",
"title": "Пример1",
"path": "5.3",
"title_path": "5.3.3.2.1",
}
],
},
}
def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
seen["changes"] = payload
return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)
result = adapter_server.metadata_form_element_write(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"element_path": "1.10",
"allow_saved_state_write": True,
"edits": [{"property": "Заголовок", "value": "ПРОВЕРКА"}],
}
)
assert seen["changes"]["preserve_format"] is True
assert seen["changes"]["edits"] == [
{"path": "5.3.3.2.1", "value": "ПРОВЕРКА", "node_type": "auto", "property": "Заголовок", "old": "Пример1"}
]
assert result["requested_element"]["section"] == "items"
assert result["element"]["section"] == "commands"
assert result["effective_sources"][0]["kind"] == "linked_command_title"
def test_group_visibility_write_uses_controlled_designer_variant_parameter() -> None:
common = {
"marker": "22",
"type_name": "Группа",
"parameters": [
{"index": 10, "presentation": "Параметр 10", "value": "1"},
],
"semantic": {
"groups": {
"Основные": [
{"name": "Видимость", "value": True, "parameter_index": 10, "source": "form_payload_container"}
]
}
},
}
group_form = {
**common,
"name": "ГруппаФорма",
"path": "1.25.23",
"parameters": [
*common["parameters"],
{"index": 26, "presentation": "Параметр 26", "value": "1"},
{"index": 28, "presentation": "Параметр 28", "value": "1"},
],
}
nested_group = {
**common,
"name": "Группа1",
"path": "1.25.23.23",
"parameters": [
*common["parameters"],
{"index": 26, "presentation": "Параметр 26", "value": "77ffcc29-7f2d-4223-b22f-19666e7250ba"},
{"index": 28, "presentation": "Параметр 28", "value": "1"},
],
}
group_edit, group_error = adapter_server.form_element_write_edit(group_form, {"property": "Видимость", "value": False}, 0)
nested_edit, nested_error = adapter_server.form_element_write_edit(nested_group, {"property": "visible", "value": False}, 0)
assert group_error is None
assert nested_error is None
assert group_edit["path"] == "1.25.23.26"
assert group_edit["old"] == "1"
assert group_edit["value"] == "0"
assert nested_edit["path"] == "1.25.23.23.28"
assert nested_edit["old"] == "1"
assert nested_edit["value"] == "0"
def test_form_element_write_uses_property_registry_alias(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
"form": {"guid": "form-guid", "file_name": "form-guid.0"},
"profile": {
"items": [
{
"name": "ПолеТест",
"title": "Поле",
"path": "1.10",
"parameters": [{"index": 7, "presentation": "Видимость", "value": "1"}],
}
],
},
}
def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
seen["changes"] = payload
return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)
result = adapter_server.metadata_form_element_write(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"element": "ПолеТест",
"allow_saved_state_write": True,
"edits": [{"property": "visible", "value": False}],
}
)
assert result["status"] == "accepted_for_review"
assert seen["changes"]["edits"] == [
{"path": "1.10.7", "value": "0", "node_type": "auto", "property": "visible", "old": "1"}
]
assert result["form_element_edits"][0]["property"] == "visible"
def test_form_element_write_uses_command_button_location_alias(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
"form": {"guid": "form-guid", "file_name": "form-guid.0"},
"profile": {
"items": [
{
"name": "КнопкаПровести",
"title": "Провести",
"path": "1.20",
"marker": "31",
"parameters": [
{"index": 14, "presentation": "ПоложениеВКоманднойПанели", "value": "0"},
{"index": 31, "presentation": "Доступность", "value": "1"},
],
}
],
},
}
def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
seen["changes"] = payload
return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)
result = adapter_server.metadata_form_element_write(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"element": "КнопкаПровести",
"allow_saved_state_write": True,
"edits": [{"property": "command_bar_location", "value": "2"}],
}
)
assert result["status"] == "accepted_for_review"
assert seen["changes"]["edits"] == [
{"path": "1.20.14", "value": "2", "node_type": "auto", "property": "command_bar_location", "old": "0"}
]
writable = result["form_element_edits"][0]
assert writable["property"] == "command_bar_location"
def test_form_element_write_changes_button_to_local_form_command(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
"form": {"guid": "form-guid", "file_name": "form-guid.0"},
"profile": {
"items": [
{
"name": "ФормаКомандаОбновить",
"title": "",
"path": "1.25.25.27",
"marker": "34",
"command_binding": {
"command_id": "1",
"group_guid": "409b9a53-7f7e-4178-86c1-33176c7c7a7a",
"command_id_path": "1.25.25.27.8.0",
"group_guid_path": "1.25.25.27.8.1",
"scope": "form",
},
}
],
"commands": [
{"name": "КомандаПрименить", "id": "1", "path": "5.1"},
{"name": "КомандаПример1", "id": "2", "path": "5.2"},
],
},
}
def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
seen["changes"] = payload
return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)
result = adapter_server.metadata_form_element_write(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"element": "ФормаКомандаОбновить",
"allow_saved_state_write": True,
"edits": [{"property": "ИмяКоманды", "value": "Form.Command.КомандаПример1"}],
}
)
assert result["status"] == "accepted_for_review"
assert seen["changes"]["edits"] == [
{"path": "1.25.25.27.8.0", "value": "2", "node_type": "auto", "property": "ИмяКоманды", "old": "1"},
{
"path": "1.25.25.27.8.1",
"value": "409b9a53-7f7e-4178-86c1-33176c7c7a7a",
"node_type": "auto",
"property": "ИмяКоманды",
"old": "409b9a53-7f7e-4178-86c1-33176c7c7a7a",
},
]
assert result["semantic_diff"][0]["property"] == "ИмяКоманды"
def test_form_element_write_changes_button_to_standard_command(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
"form": {"guid": "form-guid", "file_name": "form-guid.0"},
"profile": {
"items": [
{
"name": "ТЗИзменитьФорму",
"title": "",
"path": "1.25.23.25.62.23",
"marker": "34",
"command_binding": {
"command_id": "2",
"group_guid": "409b9a53-7f7e-4178-86c1-33176c7c7a7a",
"command_id_path": "1.25.23.25.62.23.8.0",
"group_guid_path": "1.25.23.25.62.23.8.1",
"scope": "form",
},
}
],
"commands": [{"name": "КомандаПример1", "id": "2", "path": "5.2"}],
},
}
def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
seen["changes"] = payload
return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)
result = adapter_server.metadata_form_element_write(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"element": "ТЗИзменитьФорму",
"allow_saved_state_write": True,
"edits": [{"property": "command_name", "value": "Form.StandardCommand.CustomizeForm"}],
}
)
assert result["status"] == "accepted_for_review"
assert seen["changes"]["edits"] == [
{"path": "1.25.23.25.62.23.8.0", "value": "0", "node_type": "auto", "property": "command_name", "old": "2"},
{
"path": "1.25.23.25.62.23.8.1",
"value": "198ea630-fda2-4cda-8a23-f999f4c67ee6",
"node_type": "auto",
"property": "command_name",
"old": "409b9a53-7f7e-4178-86c1-33176c7c7a7a",
},
]
assert result["semantic_diff"][0]["property"] == "command_name"
def test_form_write_target_resolve_reports_registry_metadata_for_parameters(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
"form": {"file_name": payload["file_name"]},
"profile": {
"items": [
{
"name": "КнопкаПровести",
"title": "Провести",
"path": "1.20",
"marker": "31",
"parameters": [{"index": 14, "presentation": "ПоложениеВКоманднойПанели", "value": "0"}],
}
],
},
}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
result = adapter_server.metadata_form_write_target_resolve(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"element": "КнопкаПровести",
"property": "command_bar_location",
"value": "2",
}
)
assert result["status"] == "ok"
assert result["display"]["actual"] == "0"
assert result["write_target"]["path"] == "1.20.14"
assert result["property"]["old"] == "0"
parameter_rows = [row for row in result["writable_properties"] if row.get("path") == "1.20.14"]
assert parameter_rows[0]["canonical_property"] == "command_bar_location"
assert parameter_rows[0]["value_type"] == "enum_atom"
def test_form_write_target_resolve_prefers_element_selector_over_same_named_attribute(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
"form": {"file_name": payload["file_name"]},
"profile": {
"items": [
{
"name": "А",
"title": "",
"path": "1.25.24.24.24",
"marker": "48",
"path_to_data": "А",
}
],
"attributes": [
{
"name": "А",
"title": "А",
"path": "3.3",
"marker": "9",
"title_path": "3.3.4.2.1",
}
],
},
}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
result = adapter_server.metadata_form_write_target_resolve(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"element": "А",
"property": "Заголовок",
"value": "Новое А",
}
)
assert result["status"] == "ok"
assert result["target"]["section"] == "items"
assert result["effective_target"]["section"] == "attributes"
assert result["write_target"]["path"] == "3.3.4.2.1"
assert result["effective_source"]["kind"] == "data_path_form_attribute_title"
def test_form_write_target_resolve_not_found_omits_internal_selector_fields(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
"form": {"file_name": payload["file_name"]},
"profile": {"items": [{"name": "ДругоеПоле", "path": "1.10", "marker": "48"}]},
}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
result = adapter_server.metadata_form_write_target_resolve(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"element": "Список",
"property": "Заголовок",
"value": "Новое имя",
}
)
assert result["status"] == "not_found"
assert result["query"] == {"element": "Список"}
def test_saved_state_semantic_verify_uses_property_registry_alias(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
"form": {"file_name": payload["file_name"]},
"profile": {
"items": [
{
"name": "КнопкаПровести",
"title": "Провести",
"path": "1.20",
"marker": "31",
"parameters": [{"index": 14, "presentation": "ПоложениеВКоманднойПанели", "value": "2"}],
}
],
},
}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
result = adapter_server.semantic_verify_saved_state_apply(
base_id="upo_test",
table="ConfigCASSave",
file_name="form-guid.0",
proposal={
"method": "metadata.form.element.write",
"element": {"section": "items", "name": "КнопкаПровести", "path": "1.20"},
"form_element_edits": [{"property": "command_bar_location", "value": "2"}],
},
timeout_seconds=30,
)
assert result is not None
assert result["status"] == "ok"
assert result["checks"] == [{"property": "command_bar_location", "expected": "2", "actual": "2", "ok": True}]
def test_form_write_target_resolve_explains_linked_command_source(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
"form": {"file_name": payload["file_name"]},
"profile": {
"items": [{"name": "КомандаПример1", "title": "", "path": "1.10", "title_path": "1.10.6"}],
"commands": [{"name": "КомандаПример1", "title": "Пример1", "path": "5.3", "title_path": "5.3.3.2.1"}],
},
}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
result = adapter_server.metadata_form_write_target_resolve(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"element_path": "1.10",
"property": "Заголовок",
"value": "ПРОВЕРКА",
}
)
assert result["status"] == "ok"
assert result["display"]["source_kind"] == "linked_command_title"
assert result["display"]["actual"] == "Пример1"
assert result["write_target"]["section"] == "commands"
assert result["write_target"]["path"] == "5.3.3.2.1"
assert result["alternatives"][0]["kind"] == "local_override_title"
def test_form_element_write_routes_data_path_title_to_form_attribute(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
"form": {"guid": "form-guid", "file_name": "form-guid.0"},
"profile": {
"items": [
{
"name": "ПолеКонтрагент",
"title": "",
"path": "1.10",
"title_path": "1.10.6",
"path_to_data": "Контрагент",
}
],
"attributes": [
{
"name": "Контрагент",
"title": "Контрагент",
"path": "3.2",
"title_path": "3.2.3.2.1",
}
],
},
}
def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
seen["changes"] = payload
return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)
result = adapter_server.metadata_form_element_write(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"element": "ПолеКонтрагент",
"allow_saved_state_write": True,
"edits": [{"property": "Заголовок", "value": "Клиент"}],
}
)
assert result["status"] == "accepted_for_review"
assert seen["changes"]["edits"] == [
{"path": "3.2.3.2.1", "value": "Клиент", "node_type": "auto", "property": "Заголовок", "old": "Контрагент"}
]
assert result["requested_element"]["section"] == "items"
assert result["element"]["section"] == "attributes"
assert result["effective_sources"][0]["kind"] == "data_path_form_attribute_title"
def test_form_element_write_routes_tabular_data_path_title_to_form_attribute_field(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
"form": {"guid": "form-guid", "file_name": "form-guid.0"},
"profile": {
"items": [
{
"name": "ТЗК1",
"title": "",
"path": "1.10",
"title_path": None,
"path_to_data": "ТЗ.К1",
}
],
"attributes": [
{
"name": "ТЗ",
"title": "ТЗ",
"path": "3.6",
"title_path": "3.6.4.2.1",
"dynamic_list_fields": [
{
"name": "К1",
"data_name": "К1",
"id": "1",
"title": "К1",
"path": "3.6.14",
"title_path": "3.6.14.4.2.1",
"path_to_data": "ТЗ.К1",
}
],
}
],
},
}
def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
seen["changes"] = payload
return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)
result = adapter_server.metadata_form_element_write(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"element": "ТЗК1",
"allow_saved_state_write": True,
"edits": [{"property": "Заголовок", "value": "Колонка 1"}],
}
)
assert result["status"] == "accepted_for_review"
assert seen["changes"]["edits"] == [
{"path": "3.6.14.4.2.1", "value": "Колонка 1", "node_type": "auto", "property": "Заголовок", "old": "К1"}
]
assert result["element"]["section"] == "attribute_fields"
assert result["effective_sources"][0]["kind"] == "data_path_form_attribute_field_title"
def test_form_element_write_routes_object_data_path_title_to_local_element(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
"form": {"guid": "form-guid", "file_name": "form-guid.0"},
"profile": {
"items": [
{
"name": "ПолеКонтрагент",
"title": "",
"path": "1.10",
"title_path": "1.10.6",
"path_to_data": "Объект.Контрагент",
}
],
"attributes": [{"name": "Объект", "title": "Объект", "path": "3.1", "title_path": "3.1.3.2.1"}],
},
}
def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
seen["changes"] = payload
return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)
result = adapter_server.metadata_form_element_write(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"element": "ПолеКонтрагент",
"allow_saved_state_write": True,
"edits": [{"property": "Заголовок", "value": "Клиент"}],
}
)
assert result["status"] == "accepted_for_review"
assert seen["changes"]["edits"] == [
{"path": "1.10.6", "value": "Клиент", "node_type": "auto", "property": "Заголовок", "old": ""}
]
assert result["element"]["section"] == "items"
assert result["effective_sources"][0]["kind"] == "data_path_object_attribute_local_title"
def test_form_element_write_allows_explicit_local_title_override(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
"form": {"guid": "form-guid", "file_name": "form-guid.0"},
"profile": {
"items": [
{
"name": "ПолеКонтрагент",
"title": "",
"path": "1.10",
"title_path": "1.10.6",
"path_to_data": "Объект.Контрагент",
}
],
},
}
def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
seen["changes"] = payload
return {"schema": "onec_change_proposal.v1", "status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)
result = adapter_server.metadata_form_element_write(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"element": "ПолеКонтрагент",
"allow_saved_state_write": True,
"edits": [{"property": "Заголовок", "value": "Клиент", "source": "local_override"}],
}
)
assert result["status"] == "accepted_for_review"
assert seen["changes"]["edits"] == [
{"path": "1.10.6", "value": "Клиент", "node_type": "auto", "property": "Заголовок", "old": ""}
]
assert result["effective_sources"][0]["kind"] == "local_override_title"
def test_metadata_write_routes_form_plan(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {"schema": "onec_form_element_write_apply.v1", "status": "planned", "proposal": {"status": "accepted_for_review"}}
monkeypatch.setattr(adapter_server, "metadata_form_element_write_apply", fake_apply)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"kind": "form", "table": "ConfigCASSave", "file_name": "form-guid.0", "element": "КомандаПример1"},
"mode": "plan",
"edits": [{"property": "Заголовок", "value": "ПРОВЕРКА"}],
}
)
assert seen["payload"]["table"] == "ConfigCASSave"
assert seen["payload"]["file_name"] == "form-guid.0"
assert seen["payload"]["element"] == "КомандаПример1"
assert seen["payload"]["allow_saved_state_write"] is True
assert "target" not in seen["payload"]
assert "kind" not in seen["payload"]
assert result["schema"] == "onec_metadata_write.v1"
assert result["routed_method"] == "metadata.form.element.write_apply"
def test_metadata_write_routes_common_form_command_path_to_command_button_writer(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_command_button_write(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {
"schema": "onec_form_command_button_write.v1",
"status": "planned",
"idempotency": {"status": "new"},
"semantic_verify": {"verified": True},
}
monkeypatch.setattr(adapter_server, "metadata_form_command_button_write", fake_command_button_write)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"extension": "test2",
"target": {"canonical_path": "ОбщаяФорма.t_Форма.Команда.РасчетС"},
"mode": "plan",
"title": "РасчетС",
}
)
assert result["status"] == "planned"
assert result["routed_method"] == "metadata.form.command_button.write"
assert seen["payload"]["form"] == "t_Форма"
assert seen["payload"]["command_name"] == "РасчетС"
assert seen["payload"]["button_name"] == "РасчетС"
assert seen["payload"]["allow_saved_state_write"] is True
assert result["path_resolution"]["path_kind"] == "form_command"
def test_metadata_write_routes_common_form_button_path_to_command_button_writer(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_command_button_write(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {"schema": "onec_form_command_button_write.v1", "status": "planned"}
monkeypatch.setattr(adapter_server, "metadata_form_command_button_write", fake_command_button_write)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"extension": "test2",
"target": {"canonical_path": "ОбщаяФорма.t_Форма.Кнопка.РасчетС"},
"mode": "plan",
}
)
assert result["status"] == "planned"
assert result["routed_method"] == "metadata.form.command_button.write"
assert seen["payload"]["command_name"] == "РасчетС"
assert result["path_resolution"]["form_member_kind"] == "button"
def test_metadata_write_history_records_call_method_write(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "sql.example", "database": base_id}, None))
def fake_command_button_write(payload: dict[str, Any]) -> dict[str, Any]:
return {
"schema": "onec_form_command_button_write.v1",
"status": "verified",
"base_id": payload["base_id"],
"backup": {"backup_id": "0123456789abcdef0123456789abcdef"},
"target": {"kind": "CommonForm", "form": "t_Форма", "extension": "test2", "table": "ConfigCASSave"},
}
monkeypatch.setattr(adapter_server, "metadata_form_command_button_write", fake_command_button_write)
write_result = adapter_server.call_method(
"metadata.form.command_button.write",
{
"base_id": "upo_test",
"extension": "test2",
"form": "t_Форма",
"command_name": "РасчетС",
"allow_saved_state_write": True,
},
)
assert write_result["operation_id"]
history = adapter_server.call_method("metadata.write.history", {"base_id": "upo_test", "operation_id": write_result["operation_id"]})
assert history["status"] == "ok"
assert history["counts"]["operations"] == 1
operation = history["operations"][0]
assert operation["operation_id"] == write_result["operation_id"]
assert operation["method"] == "metadata.form.command_button.write"
assert operation["status"] == "verified"
assert operation["backup_ids"] == ["0123456789abcdef0123456789abcdef"]
assert operation["result"]["status"] == "verified"
by_method = adapter_server.call_method("metadata.write.history", {"base_id": "upo_test", "operation_method": "metadata.form.command_button.write"})
assert by_method["counts"]["operations"] == 1
assert by_method["operations"][0]["operation_id"] == write_result["operation_id"]
by_backup = adapter_server.call_method("metadata.write.history", {"base_id": "upo_test", "backup_id": "0123456789abcdef0123456789abcdef"})
assert by_backup["counts"]["operations"] == 1
assert by_backup["operations"][0]["operation_id"] == write_result["operation_id"]
summary = adapter_server.call_method("metadata.write.history", {"base_id": "upo_test", "include_summary": True})
assert summary["summary"]["by_method"]["metadata.form.command_button.write"] == 1
assert summary["summary"]["by_status"]["verified"] == 1
assert summary["summary"]["with_backups"] == 1
def test_metadata_write_rollback_uses_operation_backup(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "sql.example", "database": base_id}, None))
seen: dict[str, Any] = {}
monkeypatch.setattr(
adapter_server,
"metadata_form_command_button_write",
lambda payload: {
"schema": "onec_form_command_button_write.v1",
"status": "verified",
"base_id": payload["base_id"],
"backup": {"backup_id": "0123456789abcdef0123456789abcdef"},
},
)
def fake_storage_saved_state_rollback(payload: dict[str, Any]) -> dict[str, Any]:
seen["rollback_payload"] = payload
return {
"schema": "onec_storage_saved_state_rollback.v1",
"status": "applied",
"applied": True,
"base_id": payload["base_id"],
"backup": {"backup_id": payload["backup_id"]},
}
monkeypatch.setattr(adapter_server, "storage_saved_state_rollback", fake_storage_saved_state_rollback)
write_result = adapter_server.call_method(
"metadata.form.command_button.write",
{
"base_id": "upo_test",
"form": "t_Форма",
"command_name": "РасчетС",
"allow_saved_state_write": True,
},
)
rollback = adapter_server.call_method(
"metadata.write.rollback",
{
"base_id": "upo_test",
"operation_id": write_result["operation_id"],
"allow_sql_saved_state_rollback": True,
},
)
assert rollback["status"] == "applied"
assert rollback["applied"] is True
assert rollback["backup_id"] == "0123456789abcdef0123456789abcdef"
assert seen["rollback_payload"]["backup_id"] == "0123456789abcdef0123456789abcdef"
assert seen["rollback_payload"]["allow_sql_saved_state_rollback"] is True
def test_metadata_write_rollback_checks_explicit_gate_before_repository(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"repository_apply_gate",
lambda *_args, **_kwargs: pytest.fail("repository gate must not run before rollback opt-in"),
)
result = adapter_server.metadata_write_rollback(
{
"base_id": "upo_test",
"operation_id": "operation-id",
}
)
assert result["status"] == "invalid_argument"
assert result["argument"] == "allow_sql_saved_state_rollback"
def test_metadata_write_active_form_target_requires_saved_state_prepare(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_prepare(payload: dict[str, Any]) -> dict[str, Any]:
seen["prepare"] = payload
return {"schema": "onec_saved_state_prepare.v1", "status": "plan_ready", "ready_to_copy": True}
monkeypatch.setattr(adapter_server, "metadata_saved_state_prepare", fake_prepare)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"kind": "form", "table": "ConfigCAS", "file_name": "form-guid.0", "element": "КомандаПример1"},
"mode": "plan",
"edits": [{"property": "Заголовок", "value": "ПРОВЕРКА"}],
}
)
assert result["status"] == "blocked"
assert result["error"] == "saved_state_prepare_required"
assert result["prepared_target"] == {"layer": "extension_saved_state"}
assert result["next_resolution"]["method"] == "metadata.saved_state.prepare"
assert result["next_resolution"]["params"] == {
"base_id": "upo_test",
"layer": "extension_saved_state",
"mode": "plan",
"selector_required": "Pass a public 1C ref or kind/name selector.",
}
assert seen["prepare"]["mode"] == "plan"
assert seen["prepare"]["source_table"] == "ConfigCAS"
assert seen["prepare"]["target_table"] == "ConfigCASSave"
def test_metadata_write_active_form_target_auto_prepares_when_apply_allowed(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_prepare(payload: dict[str, Any]) -> dict[str, Any]:
seen["prepare"] = payload
return {"schema": "onec_saved_state_prepare.v1", "status": "verified", "applied": True}
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["apply"] = payload
return {"schema": "onec_form_element_write_apply.v1", "status": "verified", "applied": True}
monkeypatch.setattr(adapter_server, "metadata_saved_state_prepare", fake_prepare)
monkeypatch.setattr(adapter_server, "metadata_form_element_write_apply", fake_apply)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"kind": "form", "table": "ConfigCAS", "file_name": "form-guid.0", "element": "КомандаПример1"},
"mode": "apply_and_verify",
"allow_sql_saved_state_apply": True,
"edits": [{"property": "Заголовок", "value": "ПРОВЕРКА"}],
}
)
assert seen["prepare"]["mode"] == "apply_and_verify"
assert seen["prepare"]["allow_sql_saved_state_prepare"] is True
assert seen["apply"]["table"] == "ConfigCASSave"
assert seen["apply"]["file_name"] == "form-guid.0"
assert result["status"] == "verified"
assert result["resolution"]["method"] == "metadata.saved_state.prepare"
def test_metadata_write_missing_form_target_auto_prepares_and_retries(monkeypatch: pytest.MonkeyPatch) -> None:
calls: dict[str, int] = {"apply": 0}
seen: dict[str, Any] = {}
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
calls["apply"] += 1
seen["apply"] = payload
if calls["apply"] == 1:
return {"schema": "onec_form_element_write_apply.v1", "status": "not_found", "proposal": {"status": "not_found"}}
return {"schema": "onec_form_element_write_apply.v1", "status": "verified", "applied": True}
def fake_prepare(payload: dict[str, Any]) -> dict[str, Any]:
seen["prepare"] = payload
return {"schema": "onec_saved_state_prepare.v1", "status": "verified", "applied": True}
monkeypatch.setattr(adapter_server, "metadata_form_element_write_apply", fake_apply)
monkeypatch.setattr(adapter_server, "metadata_saved_state_prepare", fake_prepare)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"kind": "form", "extension": "test2", "object_type": "CommonForm", "object_name": "t_Форма", "element": "КомандаПример1"},
"mode": "apply_and_verify",
"allow_sql_saved_state_apply": True,
"edits": [{"property": "Заголовок", "value": "ПРОВЕРКА"}],
}
)
assert calls["apply"] == 2
assert seen["apply"]["table"] == "ConfigCASSave"
assert seen["prepare"]["target_table"] == "ConfigCASSave"
assert seen["prepare"]["extension"] == "test2"
assert seen["prepare"]["object_type"] == "CommonForm"
assert seen["prepare"]["object_name"] == "t_Форма"
assert result["status"] == "verified"
def test_metadata_write_plan_requires_full_path() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {"canonical_path": "Наименование"},
"intent": {"operation": "property_change", "property": "Синоним", "value": "Имя"},
}
)
assert result["schema"] == "onec_metadata_write_plan.v1"
assert result["allowed"] is False
assert result["status"] == "needs_origin"
assert {problem["code"] for problem in result["problems"]} >= {"local_or_short_name"}
def test_metadata_write_plan_blocks_effective_path_without_origin() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"resolve_origin": False,
"target": {"canonical_path": "Справочник.Контрагенты.Наименование"},
"intent": {"operation": "property_change", "property": "Синоним", "value": "Контрагент"},
}
)
assert result["allowed"] is False
assert result["status"] == "needs_origin"
assert result["target"]["canonical_path"] == "Справочник.Контрагенты.Наименование"
assert result["path_resolution"]["kind"] == "Catalog"
assert "layer_provenance" in result["required_guards"]
assert any(problem["code"] == "origin_lookup_required" for problem in result["problems"])
def test_metadata_write_plan_includes_origin_lookup(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_definition_find(payload: dict[str, Any]) -> dict[str, Any]:
seen.update(payload)
return {
"schema": "onec_metadata_definition_find.v1",
"status": "ok",
"object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
"matches": [
{
"area": "object",
"kind": "Реквизит",
"name": "Наименование",
"match_by": "name_exact",
"location": {"presentation": "Справочник.Контрагенты.Наименование"},
"origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
"read_selector": {"method": "metadata.object.attributes", "base_id": "upo_test", "kind": "Catalog", "name": "Контрагенты"},
}
],
"related_selectors": {"full": {"method": "metadata.object.full", "base_id": "upo_test", "kind": "Catalog", "name": "Контрагенты"}},
"counts": {"matches": 1, "origin_extension": 0, "origin_unresolved": 0},
}
monkeypatch.setattr(adapter_server, "metadata_definition_find", fake_definition_find)
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {"canonical_path": "Справочник.Контрагенты.Наименование"},
"intent": {"operation": "property_change", "property": "Синоним", "value": "Контрагент"},
}
)
assert seen["query"] == "Наименование"
assert seen["kind"] == "Catalog"
assert seen["name"] == "Контрагенты"
assert seen["areas"] == ["object", "extensions"]
assert result["allowed"] is False
assert result["status"] == "needs_route"
assert result["origin_lookup"]["status"] == "ok"
assert result["origin_lookup"]["matches"][0]["origin"]["source"] == "configuration"
assert result["origin_lookup"]["matches"][0]["read_selector"]["method"] == "metadata.object.attributes"
assert result["route"]["recommended_write"]["write_surface"] == "base_saved_state"
assert result["route"]["recommended_write"]["table"] == "ConfigSave"
assert any(problem["code"] == "write_route_required" for problem in result["problems"])
def test_metadata_write_plan_recommends_extension_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_definition_find(payload: dict[str, Any]) -> dict[str, Any]:
return {
"schema": "onec_metadata_definition_find.v1",
"status": "ok",
"object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
"matches": [
{
"area": "object",
"kind": "Реквизит",
"name": "ВнешнийКод",
"match_by": "name_exact",
"origin": {
"source": "extension",
"presentation": "Расширение",
"status": "ok",
"extension": {"name": "CRM"},
},
"read_selector": {"method": "metadata.object.attributes", "base_id": "upo_test", "kind": "Catalog", "name": "Контрагенты"},
}
],
"counts": {"matches": 1, "origin_extension": 1, "origin_unresolved": 0},
}
monkeypatch.setattr(adapter_server, "metadata_definition_find", fake_definition_find)
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {"canonical_path": "Справочник.Контрагенты.ВнешнийКод"},
"intent": {"operation": "property_change", "property": "Синоним", "value": "Внешний код"},
}
)
recommended = result["route"]["recommended_write"]
assert recommended["write_surface"] == "extension_saved_state"
assert recommended["table"] == "ConfigCASSave"
assert recommended["extension"]["name_or_guid"] == "CRM"
assert result["status"] == "needs_route"
def test_metadata_write_plan_blocks_base_preference_for_extension_origin(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_definition_find",
lambda payload: {
"schema": "onec_metadata_definition_find.v1",
"status": "ok",
"object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
"matches": [
{
"area": "object",
"kind": "Реквизит",
"name": "ВнешнийКод",
"match_by": "name_exact",
"origin": {
"source": "extension",
"presentation": "Расширение",
"status": "ok",
"extension": {"name": "CRM"},
},
}
],
"counts": {"matches": 1, "origin_extension": 1, "origin_unresolved": 0},
},
)
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"preferred_layer": "base",
"target": {"canonical_path": "Справочник.Контрагенты.ВнешнийКод"},
"intent": {"operation": "property_change", "property": "Синоним", "value": "Внешний код"},
}
)
assert result["route"]["preferred_layer"] == "base"
assert result["route"]["recommended_write"]["write_surface"] == "extension_saved_state"
conflict = [problem for problem in result["problems"] if problem["code"] == "preferred_layer_conflict"][0]
assert conflict["preferred_layer"] == "base"
assert conflict["recommended_layer"] == "extension"
def test_metadata_write_plan_accepts_matching_preferred_extension(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_definition_find",
lambda payload: {
"schema": "onec_metadata_definition_find.v1",
"status": "ok",
"object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
"matches": [
{
"area": "object",
"kind": "Реквизит",
"name": "ВнешнийКод",
"match_by": "name_exact",
"origin": {
"source": "extension",
"presentation": "Расширение",
"status": "ok",
"extension": {"name": "CRM"},
},
}
],
"counts": {"matches": 1, "origin_extension": 1, "origin_unresolved": 0},
},
)
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"preferred_layer": "extension",
"preferred_extension": "crm",
"target": {"canonical_path": "Справочник.Контрагенты.ВнешнийКод"},
"intent": {"operation": "property_change", "property": "Синоним", "value": "Внешний код"},
}
)
assert result["route"]["preferred_extension"] == "crm"
assert result["route"]["recommended_write"]["extension"]["name_or_guid"] == "CRM"
assert not any(problem["code"] == "preferred_extension_conflict" for problem in result["problems"])
def test_metadata_write_plan_blocks_wrong_preferred_extension(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_definition_find",
lambda payload: {
"schema": "onec_metadata_definition_find.v1",
"status": "ok",
"object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
"matches": [
{
"area": "object",
"kind": "Реквизит",
"name": "ВнешнийКод",
"match_by": "name_exact",
"origin": {
"source": "extension",
"presentation": "Расширение",
"status": "ok",
"extension": {"name": "CRM"},
},
}
],
"counts": {"matches": 1, "origin_extension": 1, "origin_unresolved": 0},
},
)
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"preferred_layer": "extension",
"preferred_extension": "Обмен",
"target": {"canonical_path": "Справочник.Контрагенты.ВнешнийКод"},
"intent": {"operation": "property_change", "property": "Синоним", "value": "Внешний код"},
}
)
conflict = [problem for problem in result["problems"] if problem["code"] == "preferred_extension_conflict"][0]
assert conflict["preferred_extension"] == "Обмен"
assert conflict["recommended_extension"] == "CRM"
def test_metadata_write_plan_blocks_extension_preference_for_base_origin(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_definition_find",
lambda payload: {
"schema": "onec_metadata_definition_find.v1",
"status": "ok",
"object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
"matches": [
{
"area": "object",
"kind": "Реквизит",
"name": "Наименование",
"match_by": "name_exact",
"origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
}
],
"counts": {"matches": 1, "origin_extension": 0, "origin_unresolved": 0},
},
)
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"preferred_layer": "расширение",
"target": {"canonical_path": "Справочник.Контрагенты.Наименование"},
"intent": {"operation": "property_change", "property": "Синоним", "value": "Контрагент"},
}
)
assert result["route"]["preferred_layer"] == "extension"
assert result["route"]["recommended_write"]["write_surface"] == "base_saved_state"
conflict = [problem for problem in result["problems"] if problem["code"] == "preferred_layer_conflict"][0]
assert conflict["preferred_layer"] == "extension"
assert conflict["recommended_layer"] == "base"
def test_metadata_write_plan_blocks_conflicting_origin_layers(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_definition_find(payload: dict[str, Any]) -> dict[str, Any]:
return {
"schema": "onec_metadata_definition_find.v1",
"status": "ok",
"object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
"matches": [
{
"area": "object",
"kind": "Реквизит",
"name": "Наименование",
"match_by": "name_exact",
"origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
},
{
"area": "extensions",
"kind": "Реквизит",
"name": "Наименование",
"match_by": "name_exact",
"origin": {
"source": "extension",
"presentation": "Расширение",
"status": "ok",
"extension": {"name": "CRM"},
},
},
],
"counts": {"matches": 2, "origin_extension": 1, "origin_unresolved": 0},
}
monkeypatch.setattr(adapter_server, "metadata_definition_find", fake_definition_find)
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {"canonical_path": "Справочник.Контрагенты.Наименование"},
"intent": {"operation": "property_change", "property": "Синоним", "value": "Контрагент"},
}
)
recommended = result["route"]["recommended_write"]
assert recommended["write_surface"] == "blocked_conflict"
assert recommended["status"] == "blocked"
assert "configuration" in recommended["layers"]
assert "extension:CRM" in recommended["layers"]
assert any(problem["code"] == "blocked_conflict" for problem in result["problems"])
def test_metadata_write_plan_uses_provided_configuration_origin() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"canonical_path": "ОбщийМодуль.Интеграция.Отправить",
"origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
},
"intent": {"operation": "replace", "old": "a", "new": "b"},
}
)
recommended = result["route"]["recommended_write"]
assert result["allowed"] is False
assert result["origin_lookup"]["method"] == "provided_origin_evidence"
assert recommended["write_surface"] == "base_saved_state"
assert any(problem["code"] == "write_route_required" for problem in result["problems"])
def test_metadata_write_plan_uses_provided_extension_origin() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"canonical_path": "ОбщийМодуль.Интеграция.Отправить",
"origin": {
"source": "extension",
"presentation": "Расширение",
"status": "ok",
"extension": {"name": "РасширениеCRM"},
},
},
"intent": {"operation": "replace", "old": "a", "new": "b"},
}
)
recommended = result["route"]["recommended_write"]
assert result["origin_lookup"]["method"] == "provided_origin_evidence"
assert recommended["write_surface"] == "extension_saved_state"
assert recommended["extension"]["name_or_guid"] == "РасширениеCRM"
def test_metadata_write_plan_blocks_provided_unresolved_cas_origin() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"canonical_path": "ОбщийМодуль.Интеграция.Отправить",
"origin": {
"source": "cas_reference",
"presentation": "CAS module reference",
"status": "owner_unresolved",
"write_surface": "requires_owner_resolution",
},
},
"intent": {"operation": "replace", "old": "a", "new": "b"},
}
)
recommended = result["route"]["recommended_write"]
assert result["allowed"] is False
assert recommended["write_surface"] == "blocked_unknown"
assert recommended["reason"] == "origin_layer_not_resolved"
assert any(problem["code"] == "blocked_unknown" for problem in result["problems"])
def test_metadata_write_plan_reports_ambiguous_same_layer_origin(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_definition_find(payload: dict[str, Any]) -> dict[str, Any]:
return {
"schema": "onec_metadata_definition_find.v1",
"status": "ok",
"object": {"kind": "Catalog", "kind_ru": "Справочник", "name": "Контрагенты", "guid": "catalog-guid"},
"matches": [
{
"area": "object",
"kind": "Реквизит",
"name": "Код",
"match_by": "name_exact",
"location": {"presentation": "Справочник.Контрагенты.Код"},
"origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
},
{
"area": "form",
"kind": "РеквизитФормы",
"name": "Код",
"match_by": "name_exact",
"location": {"presentation": "Справочник.Контрагенты.Форма.ФормаЭлемента.Код"},
"origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
},
],
"counts": {"matches": 2, "origin_extension": 0, "origin_unresolved": 0},
}
monkeypatch.setattr(adapter_server, "metadata_definition_find", fake_definition_find)
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {"canonical_path": "Справочник.Контрагенты.Код"},
"intent": {"operation": "property_change", "property": "Синоним", "value": "Код"},
}
)
assert result["route"]["recommended_write"]["write_surface"] == "base_saved_state"
ambiguity = [problem for problem in result["problems"] if problem["code"] == "ambiguous_origin_matches"][0]
assert ambiguity["match_count"] == 2
assert ambiguity["candidates"][0]["presentation"] == "Справочник.Контрагенты.Код"
assert ambiguity["candidates"][1]["area"] == "form"
def test_metadata_write_plan_infers_form_target_from_canonical_path(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_definition_find(payload: dict[str, Any]) -> dict[str, Any]:
seen.update(payload)
return {
"schema": "onec_metadata_definition_find.v1",
"status": "ok",
"object": {"kind": "Document", "kind_ru": "Документ", "name": "Заказ", "guid": "doc-guid"},
"matches": [
{
"area": "form",
"kind": "ЭлементФормы",
"name": "КнопкаЗаписать",
"match_by": "name_exact",
"origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
}
],
"counts": {"matches": 1, "origin_extension": 0, "origin_unresolved": 0},
}
monkeypatch.setattr(adapter_server, "metadata_definition_find", fake_definition_find)
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {"canonical_path": "Документ.Заказ.Форма.ФормаДокумента.КнопкаЗаписать"},
"intent": {"operation": "property_change", "property": "Заголовок", "value": "Записать"},
}
)
assert result["target"]["target_kind"] == "form"
assert result["path_resolution"]["path_kind"] == "form_member"
assert result["path_resolution"]["form_name"] == "ФормаДокумента"
assert seen["query"] == "КнопкаЗаписать"
assert seen["areas"] == ["form", "extensions"]
assert result["route"]["apply_method"] == "metadata.form.element.write_apply"
hint_payload = result["route"]["apply_payload_hint"]["payload"]
assert hint_payload["kind"] == "Document"
assert hint_payload["name"] == "Заказ"
assert hint_payload["form"] == "ФормаДокумента"
assert hint_payload["element"] == "КнопкаЗаписать"
assert result["route"]["apply_payload_hint"]["ready_for_apply_method"] is False
next_resolution = result["route"]["apply_payload_hint"]["next_resolution"]
assert next_resolution["method"] == "metadata.form.write_target.resolve"
assert next_resolution["params"] == {
"base_id": "upo_test",
"ref": "Document.Заказ",
"kind": "Document",
"name": "Заказ",
"form": "ФормаДокумента",
"element": "КнопкаЗаписать",
}
assert "payload" not in next_resolution
def test_metadata_write_plan_infers_module_target_from_common_module_path(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_definition_find(payload: dict[str, Any]) -> dict[str, Any]:
seen.update(payload)
return {
"schema": "onec_metadata_definition_find.v1",
"status": "ok",
"object": {"kind": "CommonModule", "kind_ru": "ОбщийМодуль", "name": "Интеграция", "guid": "module-guid"},
"matches": [
{
"area": "modules",
"kind": "Процедура",
"name": "Отправить",
"match_by": "name_exact",
"origin": {"source": "configuration", "presentation": "Конфигурация", "status": "ok"},
}
],
"counts": {"matches": 1, "origin_extension": 0, "origin_unresolved": 0},
}
monkeypatch.setattr(adapter_server, "metadata_definition_find", fake_definition_find)
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {"canonical_path": "ОбщийМодуль.Интеграция.Отправить"},
"intent": {"operation": "replace", "old": "Сообщить(\"old\");", "new": "Сообщить(\"new\");"},
}
)
assert result["target"]["target_kind"] == "module"
assert result["path_resolution"]["path_kind"] == "module_routine"
assert result["path_resolution"]["routine_name"] == "Отправить"
assert seen["query"] == "Отправить"
assert seen["areas"] == ["modules", "extensions"]
assert result["route"]["apply_method"] == "metadata.module.write_apply"
hint_payload = result["route"]["apply_payload_hint"]["payload"]
assert hint_payload["kind"] == "CommonModule"
assert hint_payload["name"] == "Интеграция"
assert hint_payload["routine_name"] == "Отправить"
assert result["route"]["apply_payload_hint"]["ready_for_apply_method"] is False
next_resolution = result["route"]["apply_payload_hint"]["next_resolution"]
assert next_resolution["method"] == "metadata.saved_state.modules.search"
assert next_resolution["params"] == {
"base_id": "upo_test",
"ref": "CommonModule.Интеграция",
"kind": "CommonModule",
"name": "Интеграция",
"query": "Отправить",
}
assert "payload" not in next_resolution
def test_metadata_write_plan_infers_module_target_from_common_form_routine_path(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_definition_find(payload: dict[str, Any]) -> dict[str, Any]:
raise AssertionError("resolve_origin=false must not call metadata.definition.find")
monkeypatch.setattr(adapter_server, "metadata_definition_find", fake_definition_find)
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"resolve_origin": False,
"target": {"canonical_path": "ОбщаяФорма.t_Форма.ЗаменаДомена"},
"intent": {"operation": "replace", "routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры\n"},
}
)
assert result["target"]["target_kind"] == "module"
assert result["path_resolution"]["path_kind"] == "module_routine"
assert result["path_resolution"]["section"] == "form_module"
assert result["path_resolution"]["kind"] == "CommonForm"
assert result["path_resolution"]["name"] == "t_Форма"
assert result["path_resolution"]["routine_name"] == "ЗаменаДомена"
assert result["route"]["apply_method"] == "metadata.module.write_apply"
hint_payload = result["route"]["apply_payload_hint"]["payload"]
assert hint_payload["kind"] == "CommonForm"
assert hint_payload["name"] == "t_Форма"
assert hint_payload["routine_name"] == "ЗаменаДомена"
assert result["route"]["apply_payload_hint"]["ready_for_apply_method"] is False
next_resolution = result["route"]["apply_payload_hint"]["next_resolution"]
assert next_resolution["method"] == "metadata.saved_state.modules.search"
assert next_resolution["params"] == {
"base_id": "upo_test",
"ref": "CommonForm.t_Форма",
"kind": "CommonForm",
"name": "t_Форма",
"query": "ЗаменаДомена",
}
def test_metadata_write_plan_allows_concrete_module_saved_state_reference() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
},
"intent": {"operation": "replace", "old": "Сообщить(\"old\");", "new": "Сообщить(\"new\");"},
}
)
assert result["allowed"] is True
assert result["status"] == "planned"
assert result["route"]["write_surface"] == "saved_state"
assert result["route"]["apply_method"] == "metadata.module.write_apply"
assert "expected_sha1" in result["required_guards"]
assert result["target"]["concrete_reference_field"] == "module_ref"
assert result["route"]["apply_payload_hint"]["ready_for_apply_method"] is True
def test_metadata_write_plan_resolves_public_module_selector_internally(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
seen.update(payload)
return {
"schema": "onec_saved_state_module_search.v1",
"status": "ok",
"modules": [
{
"owner": {"kind": "Catalog", "name": "Контрагенты"},
"module": {"kind": "manager_module", "name": "Модуль менеджера"},
"qualified_name": "Контрагенты.Модуль менеджера",
"payload": {"sha1": "saved-payload-sha1"},
"streams": [
{
"module_ref": "ConfigSave:object-guid.3#stream:0",
"module": {"kind": "manager_module", "name": "Модуль менеджера"},
"qualified_name": "Контрагенты.Модуль менеджера",
}
],
}
],
}
monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"ref": "Catalog.Контрагенты",
"module": "Модуль менеджера",
"qualified_name": "Контрагенты.Модуль менеджера",
"stream_ordinal": 1,
},
"intent": {
"operation": "replace_with_control",
"control_fragment": "Процедура Тест()",
"new": "Процедура Тест()\nКонецПроцедуры",
},
"resolve_origin": False,
}
)
assert seen["ref"] == "Catalog.Контрагенты"
assert seen["include_storage"] is True
assert result["allowed"] is True
assert result["route"]["name_resolution"]["status"] == "resolved"
assert result["route"]["name_resolution"]["selector"]["ref"] == "Catalog.Контрагенты"
hint = result["route"]["apply_payload_hint"]
assert hint["ready_for_apply_method"] is True
assert hint["payload"]["module_ref"] == "ConfigSave:object-guid.3#stream:0"
assert hint["payload"]["expected_sha1"] == "saved-payload-sha1"
def test_metadata_write_plan_resolves_public_form_selector_internally(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_resolve(payload: dict[str, Any]) -> dict[str, Any]:
seen.update(payload)
return {
"schema": "onec_form_write_target_resolution.v1",
"status": "ok",
"source": {
"kind": "live_sql",
"table": "ConfigCASSave",
"file_name": "extension-guid__form-guid.0",
},
"counts": {"matches": 1},
}
monkeypatch.setattr(adapter_server, "metadata_form_write_target_resolve", fake_resolve)
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "form",
"extension": "test2",
"ref": "Catalog.test2",
"form": "t_Форма",
"command": "ЗаменаДомена",
},
"edits": [{"property": "Заголовок", "value": "Новое имя"}],
"resolve_origin": False,
}
)
assert seen["table"] == "ConfigCASSave"
assert seen["ref"] == "Catalog.test2"
assert seen["form"] == "t_Форма"
assert seen["command"] == "ЗаменаДомена"
assert seen["property"] == "Заголовок"
assert seen["value"] == "Новое имя"
assert seen["include_storage"] is True
assert result["allowed"] is True
assert result["status"] == "planned"
assert result["route"]["name_resolution"] == {
"status": "resolved",
"method": "metadata.form.write_target.resolve",
"selector": {
"extension": "test2",
"ref": "Catalog.test2",
"form": "t_Форма",
"command": "ЗаменаДомена",
},
"counts": {"matches": 1},
}
hint = result["route"]["apply_payload_hint"]
assert hint["ready_for_apply_method"] is True
assert hint["payload"]["table"] == "ConfigCASSave"
assert hint["payload"]["file_name"] == "extension-guid__form-guid.0"
assert hint["payload"]["ref"] == "Catalog.test2"
assert hint["payload"]["form"] == "t_Форма"
assert hint["payload"]["command"] == "ЗаменаДомена"
assert hint["payload"]["edits"] == [{"property": "Заголовок", "value": "Новое имя"}]
def test_metadata_write_plan_reports_unresolved_public_form_selector(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_form_write_target_resolve",
lambda payload: {
"schema": "onec_form_write_target_resolution.v1",
"status": "not_found",
"counts": {"matches": 0},
},
)
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "form",
"ref": "Catalog.Контрагенты",
"form": "ФормаЭлемента",
"element": "КнопкаЗаписать",
},
"edits": [{"property": "Заголовок", "value": "Записать"}],
"resolve_origin": False,
}
)
assert result["allowed"] is False
assert result["status"] == "needs_origin"
assert result["route"]["name_resolution"]["status"] == "not_found"
assert any(problem["code"] == "public_form_target_not_resolved" for problem in result["problems"])
def test_write_gate_context_resolves_public_extension_name_to_guid(monkeypatch: pytest.MonkeyPatch) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
monkeypatch.setattr(
adapter_server,
"extension_filter_to_guid",
lambda base_id, extension, method: (
extension_guid,
None,
)
if (base_id, extension, method) == ("upo_test", "test2", "write.gate_context")
else (None, {"status": "not_found"}),
)
result = adapter_server.resolve_write_gate_context(
{
"base_id": "upo_test",
"target": {
"kind": "form",
"extension": "test2",
"ref": "Catalog.test2",
"form": "t_Форма",
"command": "ЗаменаДомена",
},
}
)
assert result["extension_guid"] == extension_guid
assert result["target"]["extension"] == "test2"
assert result["target"]["extension_guid"] == extension_guid
assert result["target"]["origin"]["extension"] == {
"guid": extension_guid,
"name": "test2",
}
assert result["target"]["origin"]["source"] == "extension"
assert result["target"]["origin"]["status"] == "ok"
assert result["development_layer_resolution"]["source"] == "extension_name"
assert adapter_server.repository_control.development_layer_id(result) == f"extension:{extension_guid}"
def test_write_gate_context_does_not_fall_back_to_base_for_unknown_extension(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
adapter_server,
"extension_filter_to_guid",
lambda *_args, **_kwargs: (
None,
{"status": "not_found", "error": "extension_not_found"},
),
)
result = adapter_server.resolve_write_gate_context(
{
"base_id": "upo_test",
"target": {
"kind": "form",
"extension": "missing-extension",
"ref": "Catalog.test2",
"form": "t_Форма",
},
}
)
assert "extension_guid" not in result
assert result["development_layer_resolution"]["status"] == "not_resolved"
assert result["development_layer_resolution"]["diagnostics"]["error"] == "extension_not_found"
assert adapter_server.repository_control.development_layer_id(result) == "extension:unresolved"
def test_metadata_write_preflight_routes_public_extension_form_gates_to_extension_layer(
monkeypatch: pytest.MonkeyPatch,
) -> None:
extension_guid = "fb26cf42-7609-11f1-828f-005056b0d483"
captured_layers: list[str] = []
monkeypatch.setattr(
adapter_server,
"extension_filter_to_guid",
lambda base_id, extension, method: (extension_guid, None),
)
monkeypatch.setattr(
adapter_server,
"metadata_form_write_target_resolve",
lambda payload: {
"schema": "onec_form_write_target_resolution.v1",
"status": "ok",
"source": {
"kind": "live_sql",
"table": "ConfigCASSave",
"file_name": f"{extension_guid}__form-guid.0",
},
"counts": {"matches": 1},
},
)
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_diff",
lambda payload: {
"status": "changed",
"target": {
"table": payload["table"],
"file_name": payload["file_name"],
},
"freshness": {
"source": "live_sql",
"status": "live_sql_verified",
"verified_against_sql": True,
},
},
)
def fake_gate(payload: dict[str, Any]) -> dict[str, Any]:
layer_id = adapter_server.repository_control.development_layer_id(payload)
captured_layers.append(layer_id)
return {"status": "ok", "allowed": True, "layer_id": layer_id}
monkeypatch.setattr(adapter_server.repository_control, "write_gate", fake_gate)
monkeypatch.setattr(adapter_server, "live_sql_support_gate", fake_gate)
result = adapter_server.metadata_write_preflight(
{
"base_id": "upo_test",
"target": {
"kind": "form",
"extension": "test2",
"ref": "Catalog.test2",
"form": "t_Форма",
"command": "ЗаменаДомена",
},
"edits": [{"property": "Заголовок", "value": "Новое имя"}],
"resolve_origin": False,
}
)
expected_layer = f"extension:{extension_guid}"
assert result["status"] == "ready"
assert result["allowed"] is True
assert result["repository"]["layer_id"] == expected_layer
assert result["support"]["layer_id"] == expected_layer
assert captured_layers == [expected_layer, expected_layer]
def test_metadata_write_plan_help_describes_name_first_form_and_module_targets() -> None:
methods = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}
for method_name in ("metadata.write.plan", "metadata.write.preflight"):
method = methods[method_name]
target_properties = method["input_schema"]["properties"]["target"]["properties"]
assert method["input_schema"]["required"] == ["base_id", "target"]
assert {"ref", "extension", "form", "element", "command", "attribute", "module", "qualified_name", "stream_ordinal"} <= set(target_properties)
assert method["input_schema"]["properties"]["edits"]["items"]["required"] == ["property", "value"]
examples = method["input_schema"]["examples"]
assert any((example.get("target") or {}).get("kind") == "form" and (example.get("target") or {}).get("ref") for example in examples)
assert any((example.get("target") or {}).get("kind") == "module" and (example.get("target") or {}).get("module") for example in examples)
description = methods["metadata.write.plan"]["description"]
assert "name-first" in description
assert "metadata.saved_state.modules.search" in description
def test_metadata_write_preflight_verifies_concrete_saved_state(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_diff(payload: dict[str, Any]) -> dict[str, Any]:
assert payload["module_ref"] == "ConfigCASSave:object-guid__module-guid.0#stream:4"
return {
"schema": "onec_saved_state_diff.v1",
"method": "metadata.saved_state.diff",
"status": "changed",
"target": {
"table": "ConfigCASSave",
"file_name": "object-guid__module-guid.0",
"module_ref": payload["module_ref"],
},
"source": {"kind": "live_sql"},
"current_state": {"source": "saved_state", "activation_state": "not_activated"},
"needs_prepare": False,
"comparison": {"differs": True},
"freshness": {
"source": "live_sql",
"status": "live_sql_verified",
"verified_against_sql": True,
"active_payload_sha1": "active",
"saved_payload_sha1": "saved",
},
}
monkeypatch.setattr(adapter_server, "metadata_saved_state_diff", fake_diff)
result = adapter_server.metadata_write_preflight(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
},
"intent": {"operation": "replace", "old": "a", "new": "b"},
}
)
assert result["schema"] == "onec_metadata_write_preflight.v1"
assert result["status"] == "ready"
assert result["allowed"] is True
assert result["route"]["writer"] == "metadata.module.write_apply"
assert result["saved_state"]["status"] == "changed"
assert result["saved_state"]["freshness"]["status"] == "live_sql_verified"
assert result["guards"]["rollback_available"] is True
def test_metadata_write_preflight_reports_prepare_needed(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_diff",
lambda payload: {
"schema": "onec_saved_state_diff.v1",
"method": "metadata.saved_state.diff",
"status": "not_found",
"error": "saved_state_not_found",
"target": {"table": "ConfigCASSave", "file_name": "object-guid__module-guid.0"},
"needs_prepare": True,
"prepare_payload": {
"method": "metadata.saved_state.prepare",
"base_id": payload["base_id"],
"target_table": payload["table"],
"file_name": payload["file_name"],
"mode": "plan",
},
},
)
result = adapter_server.metadata_write_preflight(
{
"base_id": "upo_test",
"target": {"kind": "module", "file_name": "object-guid__module-guid.0", "stream_index": 0, "table": "ConfigCASSave"},
"intent": {"operation": "replace", "old": "a", "new": "b"},
}
)
assert result["status"] == "needs_prepare"
assert result["allowed"] is False
assert result["saved_state"]["needs_prepare"] is True
assert result["saved_state"]["prepare_payload"]["method"] == "metadata.saved_state.prepare"
def test_metadata_write_preflight_blocks_effective_path_without_route() -> None:
result = adapter_server.metadata_write_preflight(
{
"base_id": "upo_test",
"target": {"kind": "form", "canonical_path": "Справочник.Контрагенты.Наименование"},
"intent": {"operation": "property_change", "property": "Заголовок", "value": "Контрагент"},
}
)
assert result["status"] in {"blocked", "needs_resolution"}
assert result["allowed"] is False
assert result["plan"]["allowed"] is False
assert result["diagnostics"]["read_only"] is True
def test_metadata_write_plan_blocks_container_module_ref_without_stream() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"module_ref": "ConfigCASSave:extension-guid__form-guid.0",
"routine_name": "ЗаменаДомена",
},
"intent": {"operation": "replace", "old": "a", "new": "b"},
}
)
hint = result["route"]["apply_payload_hint"]
assert result["allowed"] is True
assert hint["ready_for_apply_method"] is False
assert hint["next_resolution"]["method"] == "metadata.saved_state.modules.search"
assert hint["next_resolution"]["params"]["base_id"] == "upo_test"
assert hint["next_resolution"]["params"]["query"] == "ЗаменаДомена"
assert "module_ref" not in hint["next_resolution"]["params"]
assert "file_name" not in hint["next_resolution"]["params"]
assert "payload" not in hint["next_resolution"]
assert "#stream:" not in hint["payload"]["module_ref"]
def test_metadata_write_plan_preserves_module_file_name_reference() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"file_name": "object-guid__module-guid.0",
"stream_index": 4,
},
"intent": {"operation": "replace", "old": "a", "new": "b"},
}
)
hint_payload = result["route"]["apply_payload_hint"]["payload"]
assert result["allowed"] is True
assert result["target"]["concrete_reference_field"] == "file_name"
assert hint_payload["file_name"] == "object-guid__module-guid.0"
assert hint_payload["stream_index"] == 4
assert "module_ref" not in hint_payload
def test_metadata_write_plan_preserves_form_guid_reference() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "form",
"form_guid": "form-guid",
},
"intent": {"operation": "property_change", "property": "Заголовок", "value": "Новый"},
}
)
hint_payload = result["route"]["apply_payload_hint"]["payload"]
assert result["allowed"] is True
assert result["target"]["concrete_reference_field"] == "form_guid"
assert hint_payload["form_guid"] == "form-guid"
assert "file_name" not in hint_payload
def test_metadata_write_plan_blocks_form_guid_for_module_target() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"form_guid": "form-guid",
},
"intent": {"operation": "replace", "old": "a", "new": "b"},
}
)
assert result["allowed"] is False
assert result["status"] == "blocked"
mismatch = [problem for problem in result["problems"] if problem["code"] == "concrete_reference_kind_mismatch"][0]
assert mismatch["concrete_reference_field"] == "form_guid"
assert mismatch["target_kind"] == "module"
def test_metadata_write_plan_blocks_module_ref_for_form_target() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "form",
"module_ref": "ConfigCASSave:object__module.0#stream:4",
},
"intent": {"operation": "property_change", "property": "Заголовок", "value": "Новый"},
}
)
assert result["allowed"] is False
assert result["status"] == "blocked"
mismatch = [problem for problem in result["problems"] if problem["code"] == "concrete_reference_kind_mismatch"][0]
assert mismatch["concrete_reference_field"] == "module_ref"
assert mismatch["target_kind"] == "form"
def test_metadata_write_plan_blocks_replace_with_control_without_control_fragment() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
},
"intent": {"operation": "replace_with_control", "new": "Сообщить(\"new\");"},
}
)
assert result["allowed"] is False
assert result["status"] == "blocked"
assert "controlled_fragment_matches_current_source" in result["required_guards"]
assert any(problem["code"] == "missing_control_fragment" for problem in result["problems"])
def test_metadata_write_plan_normalizes_russian_replace_with_control() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
},
"intent": {"operation": "вместо с контролем", "new": "Сообщить(\"new\");"},
}
)
assert result["route"]["operation"] == "вместо с контролем"
assert result["route"]["operation_class"] == "replace_with_control"
assert "controlled_fragment_matches_current_source" in result["required_guards"]
assert any(problem["code"] == "missing_control_fragment" for problem in result["problems"])
def test_metadata_write_plan_allows_replace_with_control_with_control_fragment() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
},
"intent": {
"operation": "replace_with_control",
"control_fragment": "Сообщить(\"old\");",
"new": "Сообщить(\"new\");",
},
}
)
assert result["allowed"] is True
assert result["status"] == "planned"
assert result["problems"] == []
hint_payload = result["route"]["apply_payload_hint"]["payload"]
assert hint_payload["module_ref"] == "ConfigCASSave:object-guid__module-guid.0#stream:4"
assert hint_payload["expected_old_contains"] == "Сообщить(\"old\");"
assert hint_payload["new"] == "Сообщить(\"new\");"
def test_metadata_write_plan_normalizes_russian_insert_after_with_anchor() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
},
"intent": {
"operation": "вставить после",
"anchor": "Сообщить(\"anchor\");",
"new": "Сообщить(\"after\");",
},
}
)
assert result["allowed"] is True
assert result["status"] == "planned"
assert result["route"]["operation_class"] == "insert_after"
assert result["problems"] == []
hint_payload = result["route"]["apply_payload_hint"]["payload"]
assert hint_payload["operation"] == "insert_after"
assert hint_payload["expected_contains"] == "Сообщить(\"anchor\");"
def test_metadata_write_plan_blocks_insert_without_anchor() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
},
"intent": {"operation": "insert_after", "new": "Сообщить(\"after\");"},
}
)
assert result["allowed"] is False
assert result["status"] == "blocked"
assert any(problem["code"] == "missing_insert_anchor" for problem in result["problems"])
def test_metadata_write_plan_blocks_replace_without_new_code() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
},
"intent": {"operation": "replace", "old": "Сообщить(\"old\");"},
}
)
assert result["allowed"] is False
assert result["status"] == "blocked"
assert any(problem["code"] == "missing_new_code" for problem in result["problems"])
def test_metadata_write_plan_infers_operation_from_extension_action() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
"extension_action": {"status": "ok", "operation_class": "replace_with_control"},
},
"intent": {
"control_fragment": "Сообщить(\"old\");",
"new": "Сообщить(\"new\");",
},
}
)
assert result["allowed"] is True
assert result["route"]["operation_class"] == "replace_with_control"
assert result["route"]["operation_inferred_from"] == "extension_action"
assert result["route"]["apply_payload_hint"]["payload"]["expected_old_contains"] == "Сообщить(\"old\");"
def test_metadata_write_plan_blocks_unknown_extension_action() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
"extension_action": {"status": "unknown", "operation_class": "unknown_extension_action"},
},
"intent": {"new": "Сообщить(\"new\");"},
}
)
assert result["allowed"] is False
assert result["status"] == "blocked"
assert any(problem["code"] == "extension_action_unknown" for problem in result["problems"])
def test_metadata_write_plan_blocks_extension_action_operation_mismatch() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
"extension_action": {"status": "ok", "operation_class": "insert_before"},
},
"intent": {"operation": "replace", "old": "a", "new": "b"},
}
)
assert result["allowed"] is False
assert result["status"] == "blocked"
mismatch = [problem for problem in result["problems"] if problem["code"] == "extension_action_operation_mismatch"][0]
assert mismatch["requested_operation"] == "replace"
assert mismatch["extension_operation"] == "insert_before"
def test_metadata_write_plan_blocks_ambiguous_extension_actions() -> None:
result = adapter_server.metadata_write_plan(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"module_ref": "ConfigCASSave:object-guid__module-guid.0#stream:4",
},
"extension_actions": [
{"status": "ok", "operation_class": "insert_before"},
{"status": "ok", "operation_class": "replace"},
],
"intent": {"operation": "replace", "old": "a", "new": "b"},
}
)
assert result["allowed"] is False
assert result["status"] == "blocked"
ambiguous = [problem for problem in result["problems"] if problem["code"] == "extension_action_ambiguous"][0]
assert len(ambiguous["extension_actions"]) == 2
assert len(result["route"]["extension_actions"]) == 2
def test_metadata_write_blocks_canonical_path_without_write_plan_route() -> None:
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"kind": "form", "canonical_path": "Справочник.Контрагенты.Наименование"},
"mode": "plan",
"edits": [{"property": "Заголовок", "value": "Контрагент"}],
}
)
assert result["schema"] == "onec_metadata_write.v1"
assert result["status"] == "blocked"
assert result["error"] == "write_plan_required"
assert result["routed_method"] == "metadata.write.plan"
assert result["plan"]["allowed"] is False
assert result["plan"]["target"]["canonical_path"] == "Справочник.Контрагенты.Наименование"
def test_metadata_write_blocked_form_path_exposes_next_resolution() -> None:
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"canonical_path": "Документ.Заказ.Форма.ФормаДокумента.КнопкаЗаписать"},
"mode": "plan",
"edits": [{"property": "Заголовок", "value": "Записать"}],
}
)
assert result["status"] == "blocked"
assert result["target_kind"] == "form"
assert result["next_resolution"]["method"] == adapter_server.FORM_WRITE_TARGET_RESOLVE_METHOD
assert result["next_resolution"]["params"]["ref"] == "Document.Заказ"
assert result["next_resolution"]["params"]["form"] == "ФормаДокумента"
assert result["next_resolution"]["params"]["element"] == "КнопкаЗаписать"
assert "payload" not in result["next_resolution"]
assert result["apply_payload_hint"]["ready_for_apply_method"] is False
assert result["apply_payload_hint"]["payload"]["form"] == "ФормаДокумента"
assert result["apply_payload_hint"]["payload"]["element"] == "КнопкаЗаписать"
def test_metadata_write_blocked_module_path_exposes_next_resolution() -> None:
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"canonical_path": "ОбщийМодуль.Интеграция.Отправить"},
"intent": {"operation": "replace", "old": "Сообщить(\"old\");", "new": "Сообщить(\"new\");"},
}
)
assert result["status"] == "blocked"
assert result["target_kind"] == "module"
assert result["next_resolution"]["method"] == adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD
assert result["next_resolution"]["params"]["ref"] == "CommonModule.Интеграция"
assert result["next_resolution"]["params"]["query"] == "Отправить"
assert "payload" not in result["next_resolution"]
assert result["apply_payload_hint"]["ready_for_apply_method"] is False
assert result["apply_payload_hint"]["payload"]["kind"] == "CommonModule"
assert result["apply_payload_hint"]["payload"]["name"] == "Интеграция"
assert result["apply_payload_hint"]["payload"]["routine_name"] == "Отправить"
def test_metadata_write_uses_form_path_hint_with_concrete_file(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {"schema": "onec_form_element_write_apply.v1", "status": "planned"}
monkeypatch.setattr(adapter_server, "metadata_form_element_write_apply", fake_apply)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {
"canonical_path": "Документ.Заказ.Форма.ФормаДокумента.КнопкаЗаписать",
"file_name": "form-guid.0",
},
"mode": "plan",
"edits": [{"property": "Заголовок", "value": "Записать"}],
}
)
assert result["status"] == "planned"
assert result["target_kind"] == "form"
assert seen["payload"]["file_name"] == "form-guid.0"
assert seen["payload"]["form"] == "ФормаДокумента"
assert seen["payload"]["element"] == "КнопкаЗаписать"
def test_metadata_write_uses_form_path_hint_with_form_guid(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {"schema": "onec_form_element_write_apply.v1", "status": "planned"}
monkeypatch.setattr(adapter_server, "metadata_form_element_write_apply", fake_apply)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {
"canonical_path": "Документ.Заказ.Форма.ФормаДокумента.КнопкаЗаписать",
"form_guid": "form-guid",
},
"mode": "plan",
"edits": [{"property": "Заголовок", "value": "Записать"}],
}
)
assert result["status"] == "planned"
assert seen["payload"]["form_guid"] == "form-guid"
assert "file_name" not in seen["payload"]
assert seen["payload"]["form"] == "ФормаДокумента"
assert seen["payload"]["element"] == "КнопкаЗаписать"
def test_metadata_write_uses_module_path_hint_with_concrete_ref(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {"schema": "onec_module_write_apply.v1", "status": "planned"}
monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_apply)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {
"canonical_path": "ОбщийМодуль.Интеграция.Отправить",
"module_ref": "ConfigCASSave:common_module.0#stream:0",
},
"old": "Сообщить(\"old\");",
"new": "Сообщить(\"new\");",
}
)
assert result["status"] == "planned"
assert result["target_kind"] == "module"
assert seen["payload"]["module_ref"] == "ConfigCASSave:common_module.0#stream:0"
assert seen["payload"]["routine_name"] == "Отправить"
assert seen["payload"]["old"] == "Сообщить(\"old\");"
assert seen["payload"]["new"] == "Сообщить(\"new\");"
def test_metadata_write_routes_container_module_ref_to_embedded_writer(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
raise AssertionError("metadata.write must not call stream writer for container module refs")
def fake_embedded(payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
seen["payload"] = payload
seen["kwargs"] = kwargs
return {"schema": "onec_form_embedded_module_write.v1", "status": "planned"}
monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_apply)
monkeypatch.setattr(adapter_server, "form_embedded_module_handler_write_apply", fake_embedded)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"module_ref": "ConfigCASSave:extension-guid__form-guid.0",
"routine_name": "ЗаменаДомена",
},
"old": "a",
"new": "b",
}
)
assert result["status"] == "planned"
assert result["routed_method"] == "form_embedded_module_handler_write_apply"
assert seen["kwargs"]["table"] == "ConfigCASSave"
assert seen["kwargs"]["file_name"] == "extension-guid__form-guid.0"
assert seen["kwargs"]["handler_name"] == "ЗаменаДомена"
assert seen["payload"]["routine_text"] == "b"
def test_metadata_write_routes_resolved_form_embedded_module_to_embedded_writer(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
seen["search_payload"] = payload
return {
"status": "ok",
"modules": [
{
"payload": {"sha1": "a" * 40, "role": "form_embedded_module_payload"},
"streams": [
{
"module_ref": "ConfigCASSave:extension-guid__form-guid.0",
"module_path": "2",
}
],
}
],
"counts": {"modules": 1, "scanned": 1},
}
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
raise AssertionError("metadata.write must not call stream writer for resolved form container refs")
def fake_embedded(payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
seen["payload"] = payload
seen["kwargs"] = kwargs
return {"schema": "onec_form_embedded_module_write.v1", "status": "planned"}
monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)
monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_apply)
monkeypatch.setattr(adapter_server, "form_embedded_module_handler_write_apply", fake_embedded)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"object_type": "CommonForm",
"object_name": "t_Форма",
},
"routine_name": "ЗаменаДомена",
"routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры\n",
"routine_operation": "replace",
}
)
assert result["status"] == "planned"
assert result["routed_method"] == "form_embedded_module_handler_write_apply"
assert seen["search_payload"]["object_type"] == "CommonForm"
assert seen["search_payload"]["object_name"] == "t_Форма"
assert seen["kwargs"]["table"] == "ConfigCASSave"
assert seen["kwargs"]["file_name"] == "extension-guid__form-guid.0"
assert seen["kwargs"]["handler_name"] == "ЗаменаДомена"
assert seen["payload"]["module_path"] == "2"
assert seen["payload"]["expected_sha1"] == "a" * 40
def test_metadata_write_routes_common_form_routine_path_to_embedded_writer(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
seen["search_payload"] = payload
return {
"status": "ok",
"modules": [
{
"payload": {"sha1": "b" * 40, "role": "form_embedded_module_payload"},
"streams": [
{
"module_ref": "ConfigCASSave:extension-guid__form-guid.0",
"module_path": "2",
}
],
}
],
"counts": {"modules": 1, "scanned": 1},
}
def fake_embedded(payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
seen["payload"] = payload
seen["kwargs"] = kwargs
return {"schema": "onec_form_embedded_module_write.v1", "status": "planned"}
monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)
monkeypatch.setattr(adapter_server, "form_embedded_module_handler_write_apply", fake_embedded)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"canonical_path": "ОбщаяФорма.t_Форма.ЗаменаДомена"},
"routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры\n",
"routine_operation": "replace",
}
)
assert result["status"] == "planned"
assert result["target_kind"] == "module"
assert result["routed_method"] == "form_embedded_module_handler_write_apply"
assert seen["search_payload"]["object_type"] == "CommonForm"
assert seen["search_payload"]["object_name"] == "t_Форма"
assert seen["search_payload"]["query"] == "ЗаменаДомена"
assert seen["kwargs"]["handler_name"] == "ЗаменаДомена"
assert seen["payload"]["module_path"] == "2"
assert seen["payload"]["expected_sha1"] == "b" * 40
def test_metadata_write_common_form_upsert_searches_owner_without_new_routine_query(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
seen["search_payload"] = payload
return {
"status": "ok",
"modules": [
{
"payload": {"sha1": "d" * 40, "role": "form_embedded_module_payload"},
"streams": [{"module_ref": "ConfigCASSave:extension-guid__form-guid.0", "module_path": "2"}],
}
],
"counts": {"modules": 1, "scanned": 1},
}
def fake_embedded(payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
seen["payload"] = payload
seen["kwargs"] = kwargs
return {"schema": "onec_form_embedded_module_write.v1", "status": "planned"}
monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)
monkeypatch.setattr(adapter_server, "form_embedded_module_handler_write_apply", fake_embedded)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"canonical_path": "ОбщаяФорма.t_Форма.РасчетС"},
"routine_text": "Процедура РасчетС(Команда)\nКонецПроцедуры\n",
"routine_operation": "upsert",
}
)
assert result["status"] == "planned"
assert result["routed_method"] == "form_embedded_module_handler_write_apply"
assert seen["search_payload"]["object_type"] == "CommonForm"
assert seen["search_payload"]["object_name"] == "t_Форма"
assert "query" not in seen["search_payload"]
assert seen["kwargs"]["handler_name"] == "РасчетС"
def test_metadata_write_apply_defaults_to_save_first_flags(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"modules": [
{
"payload": {"sha1": "c" * 40, "role": "form_embedded_module_payload"},
"streams": [{"module_ref": "ConfigCASSave:extension-guid__form-guid.0", "module_path": "2"}],
}
],
"counts": {"modules": 1, "scanned": 1},
}
def fake_embedded(payload: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
seen["payload"] = payload
seen["kwargs"] = kwargs
return {"schema": "onec_form_embedded_module_write.v1", "status": "applied", "applied": True}
monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)
monkeypatch.setattr(adapter_server, "form_embedded_module_handler_write_apply", fake_embedded)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"mode": "apply",
"target": {"canonical_path": "ОбщаяФорма.t_Форма.ЗаменаДомена"},
"routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры\n",
"routine_operation": "replace",
}
)
assert result["status"] == "applied"
assert result["routed_method"] == "form_embedded_module_handler_write_apply"
assert seen["kwargs"]["mode"] == "apply"
assert seen["payload"]["allow_sql_saved_state_apply"] is True
assert seen["payload"]["allow_sql_saved_state_prepare"] is True
assert seen["payload"]["auto_prepare_saved_state"] is True
def test_preserve_bsl_routine_directives_keeps_existing_form_directive() -> None:
current = "\r\n&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\n\t// comment\r\nКонецПроцедуры\r\n"
replacement = "Процедура ЗаменаДомена(Команда)\r\nКонецПроцедуры\r\n"
result = adapter_server.preserve_bsl_routine_directives(current, replacement, "ЗаменаДомена")
assert result == "&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\nКонецПроцедуры\r\n"
def test_preserve_bsl_routine_directives_keeps_explicit_replacement_directive() -> None:
current = "\r\n&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\nКонецПроцедуры\r\n"
replacement = "&НаСервере\r\nПроцедура ЗаменаДомена(Команда)\r\nКонецПроцедуры\r\n"
result = adapter_server.preserve_bsl_routine_directives(current, replacement, "ЗаменаДомена")
assert result == replacement
def test_form_embedded_module_apply_blocks_canonicalized_payload(monkeypatch: pytest.MonkeyPatch) -> None:
original_text = '{"meta","data","\r\n&НаКлиенте\r\nПроцедура Cmd(Команда)\r\n\t// old\r\nКонецПроцедуры\r\n"}'
original = compress_payload(original_text.encode("utf-8-sig"), "raw_deflate")
canonicalized_text = '{"meta","data","\r\n&НаКлиенте\r\nПроцедура Cmd(Команда)\r\nКонецПроцедуры\r\n","extra"}'
canonicalized = compress_payload(canonicalized_text.encode("utf-8-sig"), "raw_deflate")
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (original, {"database": "upo_test"}, None),
)
monkeypatch.setattr(
adapter_server,
"changes_propose",
lambda payload: {
"schema": "onec_change_proposal.v1",
"status": "accepted_for_review",
"source": {"table": "ConfigCASSave", "file_name": "form.0"},
"original": {"sha1": hashlib.sha1(original).hexdigest(), "bytes": len(original)},
"encoded": {"sha1": hashlib.sha1(canonicalized).hexdigest(), "bytes": len(canonicalized), "payload_hex": canonicalized.hex()},
"edits": [{"mode": "path_preserve_format", "path": "2"}],
},
)
monkeypatch.setattr(
adapter_server,
"storage_saved_state_apply_proposal",
lambda payload: (_ for _ in ()).throw(AssertionError("unsafe payload must not be applied")),
)
result = adapter_server.form_embedded_module_handler_write_apply(
{
"routine_text": "Процедура Cmd(Команда)\nКонецПроцедуры\n",
"allow_sql_saved_state_apply": True,
},
base_id="upo_test",
table="ConfigCASSave",
file_name="form.0",
handler_name="Cmd",
mode="apply",
timeout_seconds=30,
method_name="metadata.write",
)
assert result["status"] == "blocked"
assert result["error"] == "unsafe_form_module_payload_write"
def test_storage_saved_state_apply_blocks_canonical_form_module_edit() -> None:
proposal = {
"schema": "onec_change_proposal.v1",
"status": "accepted_for_review",
"source": {"table": "ConfigCASSave", "file_name": "form.0"},
"original": {"sha1": "a" * 40, "bytes": 1},
"encoded": {"sha1": "b" * 40, "bytes": 1, "payload_hex": "00"},
"edits": [{"mode": "path", "path": "2"}],
}
result = adapter_server.storage_saved_state_apply_proposal(
{"base_id": "upo_test", "allow_sql_saved_state_apply": True, "proposal": proposal}
)
assert result["status"] == "blocked"
assert result["error"] == "unsafe_form_module_payload_write"
assert result["applied"] is False
def test_form_embedded_module_apply_allows_single_path_preserve_patch(monkeypatch: pytest.MonkeyPatch) -> None:
original_text = '{"meta","data","\r\n&НаКлиенте\r\nПроцедура Cmd(Команда)\r\n\t// old\r\nКонецПроцедуры\r\n"}'
original = compress_payload(original_text.encode("utf-8-sig"), "raw_deflate")
seen: dict[str, Any] = {}
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (original, {"database": "upo_test"}, None),
)
def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
expected_text, _info = patch_brace_text_path(
decode_payload_lossless(original)["text"],
"2",
payload["edits"][0]["value"],
)
encoded = encode_payload_lossless(decode_payload_lossless(original), text=expected_text)
return {
"schema": "onec_change_proposal.v1",
"status": "accepted_for_review",
"source": {"table": "ConfigCASSave", "file_name": "form.0"},
"original": {"sha1": hashlib.sha1(original).hexdigest(), "bytes": len(original)},
"encoded": {"sha1": hashlib.sha1(encoded).hexdigest(), "bytes": len(encoded), "payload_hex": encoded.hex()},
"edits": [{"mode": "path_preserve_format", "path": "2"}],
}
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["apply"] = payload
return {"schema": "onec_storage_saved_state_apply.v1", "status": "applied", "applied": True, "readback": {"verified": True}}
monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", fake_apply)
result = adapter_server.form_embedded_module_handler_write_apply(
{
"routine_text": "Процедура Cmd(Команда)\nКонецПроцедуры\n",
"allow_sql_saved_state_apply": True,
},
base_id="upo_test",
table="ConfigCASSave",
file_name="form.0",
handler_name="Cmd",
mode="apply",
timeout_seconds=30,
method_name="metadata.write",
)
assert result["status"] == "applied"
assert seen["apply"]["allow_sql_saved_state_apply"] is True
def test_form_embedded_module_full_text_preserves_trailing_marker() -> None:
current = "&НаКлиенте\nПроцедура Старая()\nКонецПроцедуры\n\n///----"
new = "&НаКлиенте\nПроцедура Новая()\nКонецПроцедуры\n"
result = adapter_server.preserve_form_embedded_module_suffix(current, new)
assert result == "&НаКлиенте\nПроцедура Новая()\nКонецПроцедуры\n\n///----"
def test_form_embedded_module_apply_replaces_unique_fragment(monkeypatch: pytest.MonkeyPatch) -> None:
original_text = '{"meta","data","\r\n&AtClient\r\nProcedure Cmd(Command)\r\n\tLine_old;\r\nEndProcedure\r\n"}'
original = compress_payload(original_text.encode("utf-8-sig"), "raw_deflate")
seen: dict[str, Any] = {}
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (original, {"database": "upo_test"}, None),
)
def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
seen["new_text"] = payload["edits"][0]["value"]
expected_text, _info = patch_brace_text_path(
decode_payload_lossless(original)["text"],
"2",
payload["edits"][0]["value"],
)
encoded = encode_payload_lossless(decode_payload_lossless(original), text=expected_text)
return {
"schema": "onec_change_proposal.v1",
"status": "accepted_for_review",
"source": {"table": "ConfigCASSave", "file_name": "form.0"},
"original": {"sha1": hashlib.sha1(original).hexdigest(), "bytes": len(original)},
"encoded": {"sha1": hashlib.sha1(encoded).hexdigest(), "bytes": len(encoded), "payload_hex": encoded.hex()},
"edits": [{"mode": "path_preserve_format", "path": "2"}],
}
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)
monkeypatch.setattr(
adapter_server,
"storage_saved_state_apply_proposal",
lambda payload: {"schema": "onec_storage_saved_state_apply.v1", "status": "applied", "applied": True, "readback": {"verified": True}},
)
result = adapter_server.form_embedded_module_handler_write_apply(
{
"old": "Line_old;",
"new": "Line_new;",
"allow_sql_saved_state_apply": True,
},
base_id="upo_test",
table="ConfigCASSave",
file_name="form.0",
handler_name="",
mode="apply",
timeout_seconds=30,
method_name="metadata.write",
)
assert result["status"] == "applied"
assert "Line_new;" in seen["new_text"]
def test_form_embedded_module_apply_blocks_ambiguous_fragment(monkeypatch: pytest.MonkeyPatch) -> None:
original_text = '{"meta","data","\r\nProcedure Cmd(Command)\r\n\tLine_x;\r\n\tLine_x;\r\nEndProcedure\r\n"}'
original = compress_payload(original_text.encode("utf-8-sig"), "raw_deflate")
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (original, {"database": "upo_test"}, None),
)
monkeypatch.setattr(
adapter_server,
"changes_propose",
lambda payload: (_ for _ in ()).throw(AssertionError("ambiguous fragment must not be proposed")),
)
result = adapter_server.form_embedded_module_handler_write_apply(
{"old": "Line_x;", "new": "Line_y;"},
base_id="upo_test",
table="ConfigCASSave",
file_name="form.0",
handler_name="",
mode="plan",
timeout_seconds=30,
method_name="metadata.write",
)
assert result["status"] == "ambiguous"
assert result["error"] == "ambiguous_fragment"
assert result["counts"]["occurrences"] == 2
def test_form_embedded_module_apply_replaces_fragment_inside_routine_scope(monkeypatch: pytest.MonkeyPatch) -> None:
original_text = '{"meta","data","\r\n&НаКлиенте\r\nПроцедура Cmd(Команда)\r\n\t// same\r\nКонецПроцедуры\r\n\r\nПроцедура Other(Команда)\r\n\t// same\r\nКонецПроцедуры\r\n"}'
original = compress_payload(original_text.encode("utf-8-sig"), "raw_deflate")
seen: dict[str, Any] = {}
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (original, {"database": "upo_test"}, None),
)
def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
seen["new_text"] = payload["edits"][0]["value"]
expected_text, _info = patch_brace_text_path(
decode_payload_lossless(original)["text"],
"2",
payload["edits"][0]["value"],
)
encoded = encode_payload_lossless(decode_payload_lossless(original), text=expected_text)
return {
"schema": "onec_change_proposal.v1",
"status": "accepted_for_review",
"source": {"table": "ConfigCASSave", "file_name": "form.0"},
"original": {"sha1": hashlib.sha1(original).hexdigest(), "bytes": len(original)},
"encoded": {"sha1": hashlib.sha1(encoded).hexdigest(), "bytes": len(encoded), "payload_hex": encoded.hex()},
"edits": [{"mode": "path_preserve_format", "path": "2"}],
}
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)
monkeypatch.setattr(
adapter_server,
"storage_saved_state_apply_proposal",
lambda payload: {"schema": "onec_storage_saved_state_apply.v1", "status": "applied", "applied": True, "readback": {"verified": True}},
)
result = adapter_server.form_embedded_module_handler_write_apply(
{
"old": "// same",
"new": "// changed",
"allow_sql_saved_state_apply": True,
},
base_id="upo_test",
table="ConfigCASSave",
file_name="form.0",
handler_name="Cmd",
mode="apply",
timeout_seconds=30,
method_name="metadata.write",
)
assert result["status"] == "applied"
assert seen["new_text"].count("// changed") == 1
assert seen["new_text"].count("// same") == 1
assert "Процедура Other" in seen["new_text"]
def test_code_write_facade_defaults_to_save_first_and_hides_storage(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_metadata_write(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {
"schema": "onec_metadata_write.v1",
"method": "metadata.write",
"status": "applied",
"execution_mode": "apply",
"target_kind": "module",
"base_id": "upo_test",
"routed_method": "form_embedded_module_handler_write_apply",
"result": {
"status": "applied",
"applied": True,
"source": {"table": "ConfigCASSave", "file_name": "form.0", "module_path": "2"},
},
}
monkeypatch.setattr(adapter_server, "metadata_write", fake_metadata_write)
result = adapter_server.code_write(
{
"base_id": "upo_test",
"object_type": "CommonForm",
"object_name": "t_Форма",
"routine_name": "ЗаменаДомена",
"routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры\n",
}
)
assert result["schema"] == "onec_code_write.v1"
assert result["status"] == "applied"
assert result["operation"] == "routine_replace"
assert result["applied"] is True
assert result["write_mode"] == {
"target": "saved_state",
"activation_state": "not_activated",
"production_apply": False,
}
assert "metadata_write" not in result
assert seen["payload"]["mode"] == "apply"
assert seen["payload"]["allow_sql_saved_state_apply"] is True
assert seen["payload"]["allow_sql_saved_state_prepare"] is True
assert seen["payload"]["target"] == {
"kind": "module",
"object_type": "CommonForm",
"object_name": "t_Форма",
"routine_name": "ЗаменаДомена",
}
def test_code_write_normalizes_public_owner_ref_before_routing(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_metadata_write(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {
"schema": "onec_metadata_write.v1",
"method": "metadata.write",
"status": "planned",
"execution_mode": "plan",
"target_kind": "module",
"base_id": payload["base_id"],
"routed_method": "metadata.module.write_apply",
"result": {"status": "planned", "applied": False},
}
monkeypatch.setattr(adapter_server, "metadata_write", fake_metadata_write)
result = adapter_server.call_method(
"code.write",
{
"base_id": "upo_test",
"ref": "Обработки.ОбменДанными",
"routine_name": "ВыполнитьОбмен",
"routine_text": "Процедура ВыполнитьОбмен()\nКонецПроцедуры\n",
"execution_mode": "plan",
},
)
assert result["status"] == "planned"
assert seen["payload"]["target"] == {
"kind": "module",
"object_type": "DataProcessor",
"object_name": "ОбменДанными",
"routine_name": "ВыполнитьОбмен",
}
assert result["target"]["object_type"] == "DataProcessor"
assert result["target"]["object_name"] == "ОбменДанными"
assert result["target"]["routine_name"] == "ВыполнитьОбмен"
def test_code_write_help_exposes_name_first_owner_and_write_guards() -> None:
method = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}["code.write"]
assert method["selector_capabilities"]["accepts_ordinal"] is False
assert method["selector_capabilities"]["accepts_module_ref"] is True
assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name"]
assert method["selector_capabilities"]["child_selector_arguments"] == ["routine_name"]
assert method["selector_capabilities"]["write_guards"] == [
"save_first",
"allow_sql_saved_state_apply",
"repository_apply_gate",
"verified_saved_state_target",
]
assert method["input_schema"]["properties"]["ref"]["type"] == "string"
assert method["input_schema"]["properties"]["object_name"]["type"] == "string"
def test_code_write_records_write_history(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ONEC_ADAPTER_CACHE_DB", str(tmp_path / "adapter-cache.sqlite"))
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"server": "sql.example", "database": base_id}, None))
monkeypatch.setattr(
adapter_server,
"metadata_write",
lambda payload: {
"schema": "onec_metadata_write.v1",
"method": "metadata.write",
"status": "applied",
"execution_mode": "apply",
"target_kind": "module",
"base_id": payload["base_id"],
"routed_method": "metadata.module.write_apply",
"result": {"status": "applied", "applied": True, "backup": {"backup_id": "abcdefabcdefabcdefabcdefabcdefab"}},
},
)
write_result = adapter_server.call_method(
"code.write",
{
"base_id": "upo_test",
"object_type": "CommonForm",
"object_name": "t_Форма",
"routine_name": "ЗаменаДомена",
"routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры\n",
},
)
assert write_result["operation_id"]
history = adapter_server.call_method("metadata.write.history", {"base_id": "upo_test", "operation_id": write_result["operation_id"]})
assert history["counts"]["operations"] == 1
operation = history["operations"][0]
assert operation["method"] == "code.write"
assert operation["status"] == "applied"
assert operation["routed_method"] == "metadata.module.write_apply"
assert operation["backup_ids"] == ["abcdefabcdefabcdefabcdefabcdefab"]
def test_code_write_rewrites_extension_first_common_form_path(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_metadata_write(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {"status": "planned", "target": payload.get("target"), "result": {"applied": False}}
monkeypatch.setattr(adapter_server, "metadata_write", fake_metadata_write)
result = adapter_server.code_write(
{
"base_id": "upo_test",
"extension": "test2",
"path": "test2.t_Форма.ЗаменаДомена",
"routine_text": "Процедура ЗаменаДомена(Команда)\nКонецПроцедуры\n",
"mode": "plan",
}
)
assert result["status"] == "planned"
assert seen["payload"]["target"]["path"] == "CommonForm.t_Форма.ЗаменаДомена"
assert seen["payload"]["target"]["object_type"] == "CommonForm"
assert seen["payload"]["target"]["object_name"] == "t_Форма"
assert seen["payload"]["routine_name"] == "ЗаменаДомена"
def test_code_write_facade_exposes_ambiguous_fragment_reason(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_write",
lambda payload: {
"schema": "onec_metadata_write.v1",
"method": "metadata.write",
"status": "ambiguous",
"execution_mode": "plan",
"target_kind": "module",
"base_id": "upo_test",
"routed_method": "form_embedded_module_handler_write_apply",
"result": {
"status": "ambiguous",
"error": "ambiguous_fragment",
"counts": {"occurrences": 3},
"scope": {"kind": "routine", "routine_name": "ЗаменаДомена"},
"diagnostics": {"message": "Fragment replacement requires old to occur exactly once."},
},
},
)
result = adapter_server.code_write(
{
"base_id": "upo_test",
"target": {"canonical_path": "ОбщаяФорма.t_Форма.ЗаменаДомена"},
"old": "\n",
"new": "\n",
"mode": "plan",
}
)
assert result["status"] == "ambiguous"
assert result["error"] == "ambiguous_fragment"
assert result["counts"] == {"occurrences": 3}
assert result["scope"] == {"kind": "routine", "routine_name": "ЗаменаДомена"}
def test_code_write_facade_exposes_success_fragment_scope(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_metadata_write(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {
"schema": "onec_metadata_write.v1",
"method": "metadata.write",
"status": "planned",
"execution_mode": "plan",
"target_kind": "module",
"base_id": "upo_test",
"routed_method": "form_embedded_module_handler_write_apply",
"result": {
"status": "planned",
"routine": {"status": "fragment_replaced", "operation": "fragment_replace", "scope": "routine", "occurrences": 1},
},
}
monkeypatch.setattr(
adapter_server,
"metadata_write",
fake_metadata_write,
)
result = adapter_server.code_write(
{
"base_id": "upo_test",
"target": {"canonical_path": "ОбщаяФорма.t_Форма.ЗаменаДомена"},
"old": "// old",
"new": "// new",
"mode": "plan",
}
)
assert result["status"] == "planned"
assert result["scope"] == {"kind": "routine", "routine_name": "ЗаменаДомена"}
assert result["counts"] == {"occurrences": 1}
assert seen["payload"]["routine_name"] == "ЗаменаДомена"
def test_metadata_write_blocks_concrete_module_when_plan_has_problems(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
raise AssertionError("metadata.write must not call apply when write plan is blocked")
monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_apply)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {
"canonical_path": "ОбщийМодуль.Интеграция.Отправить",
"module_ref": "ConfigCASSave:common_module.0#stream:0",
},
"intent": {"operation": "replace_with_control", "new": "Сообщить(\"new\");"},
}
)
assert result["status"] == "blocked"
assert result["error"] == "write_plan_blocked"
assert result["plan"]["allowed"] is False
assert any(problem["code"] == "missing_control_fragment" for problem in result["problems"])
def test_metadata_write_blocks_mismatched_concrete_reference_before_apply(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
raise AssertionError("metadata.write must not call apply for mismatched concrete reference")
monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_apply)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {
"kind": "module",
"canonical_path": "ОбщийМодуль.Интеграция.Отправить",
"form_guid": "form-guid",
},
"old": "a",
"new": "b",
}
)
assert result["status"] == "blocked"
assert result["error"] == "write_plan_blocked"
assert any(problem["code"] == "concrete_reference_kind_mismatch" for problem in result["problems"])
def test_code_symbol_resolve_full_path_uses_metadata_lookup(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_definition_find",
lambda payload: {
"schema": "onec_definition_find.v1",
"status": "ok",
"matches": [
{
"canonical_path": "Справочник.Номенклатура.Артикул",
"kind": "Catalog",
"name": "Номенклатура",
}
],
},
)
result = call_method(
"code.symbol.resolve",
{
"base_id": "base",
"expression": "Справочник.Номенклатура.Артикул",
"module_ref": "ConfigSave:file:0",
},
)
assert result["status"] == "resolved"
assert result["resolution_kind"] == "metadata_path"
assert result["canonical_path"] == "Справочник.Номенклатура.Артикул"
assert result["safe_as_metadata_path"] is True
def test_code_symbol_resolve_parameter_is_not_metadata(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"read_module",
lambda payload: {
"schema": "onec_module_read.v1",
"status": "ok",
"text": "Процедура ПередЗаписью(Отказ) Экспорт\n Отказ = Истина;\nКонецПроцедуры",
"owner": {"kind": "Catalog", "name": "Номенклатура"},
"module": {"name": "Модуль объекта"},
},
)
result = call_method(
"code.symbol.resolve",
{
"base_id": "base",
"expression": "Отказ.Код",
"routine_name": "ПередЗаписью",
"module_ref": "ConfigSave:file:0",
},
)
assert result["status"] == "resolved"
assert result["resolution_kind"] == "parameter"
assert result["context_path"] == "Отказ.Код"
assert result["safe_as_metadata_path"] is False
def test_code_symbol_resolve_short_object_name_stays_unsafe(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"read_module",
lambda payload: {
"schema": "onec_module_read.v1",
"status": "ok",
"text": "Процедура ПередЗаписью(Отказ) Экспорт\nКонецПроцедуры",
"owner": {"kind": "Catalog", "name": "Номенклатура"},
"module": {"name": "Модуль объекта"},
},
)
monkeypatch.setattr(
adapter_server,
"metadata_object_attributes",
lambda payload: {"schema": "onec_metadata_object_attributes.v1", "status": "ok", "attributes": []},
)
monkeypatch.setattr(
adapter_server,
"metadata_definition_find",
lambda payload: {
"schema": "onec_definition_find.v1",
"status": "ok",
"matches": [{"canonical_path": "Справочник.Номенклатура", "kind": "Catalog", "name": "Номенклатура"}],
},
)
result = call_method(
"code.symbol.resolve",
{
"base_id": "base",
"expression": "Номенклатура.ЕдИзмерение.Код",
"routine_name": "ПередЗаписью",
"module_ref": "ConfigSave:file:0",
},
)
assert result["status"] == "unresolved"
assert result["safe_as_metadata_path"] is False
assert result["candidates"][0]["canonical_path"] == "Справочник.Номенклатура"
assert result["candidates"][0]["reason"] == "short_object_name_requires_kind"
def test_module_origin_from_storage_table_marks_base_configuration() -> None:
origin = adapter_server.module_origin_from_storage_table("Config")
assert origin["source"] == "configuration"
assert origin["storage_table"] == "Config"
assert origin["write_surface"] == "base_saved_state"
def test_module_origin_from_storage_table_marks_saved_state() -> None:
origin = adapter_server.module_origin_from_storage_table("ConfigSave")
assert origin["source"] == "saved_state"
assert origin["storage_table"] == "ConfigSave"
assert origin["write_surface"] == "base_saved_state"
def test_module_origin_from_storage_table_requires_owner_for_configcas() -> None:
origin = adapter_server.module_origin_from_storage_table("ConfigCAS")
assert origin["source"] == "cas_reference"
assert origin["status"] == "owner_unresolved"
assert origin["write_surface"] == "requires_owner_resolution"
def test_code_read_preserves_module_origin(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"read_module",
lambda payload: {
"schema": "onec_module_read.v1",
"status": "ok",
"text": "Процедура Проверка()\nКонецПроцедуры",
"origin": adapter_server.module_origin_from_storage_table("ConfigSave"),
},
)
result = adapter_server.code_read({"base_id": "upo_test", "module_ref": "ConfigSave:object-module"})
assert result["schema"] == "onec_code_read.v1"
assert result["source"]["kind"] == "code_read"
assert result["origin"]["source"] == "saved_state"
assert result["origin"]["write_surface"] == "base_saved_state"
def test_form_write_matrix_build_routes_readable_title_to_effective_source(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_form_decode",
lambda payload: {
"status": "ok",
"source": {"table": payload["table"], "file_name": payload["file_name"]},
"form": {"file_name": payload["file_name"]},
"profile": {
"items": [
{
"name": "А",
"id": "3",
"title": "",
"path": "1.25.24.24.24",
"title_path": "1.25.24.24.24.4.2.1",
"path_to_data": "А",
}
],
"attributes": [
{"name": "А", "id": "2", "title": "А", "path": "3.3", "title_path": "3.3.4.2.1"}
],
},
},
)
result = adapter_server.metadata_form_write_matrix_build(
{"base_id": "upo_test", "table": "ConfigCASSave", "file_name": "form-guid.0"}
)
assert result["status"] == "ok"
routed = [
entry
for entry in result["entries"]
if entry["requested_target"]["section"] == "items"
and entry["requested_target"]["name"] == "А"
and entry["property"]["canonical_property"] == "title"
][0]
assert routed["can_smoke"] is True
assert routed["effective_target"]["section"] == "attributes"
assert routed["effective_source"]["kind"] == "data_path_form_attribute_title"
assert routed["property"]["write_path"] == "3.3.4.2.1"
def test_form_write_matrix_build_passes_owner_scope_only_to_file_resolution(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
owner_guid = "22222222-2222-2222-2222-222222222222"
def fake_resolve_file(payload: dict[str, Any], **kwargs: Any) -> tuple[str, str]:
seen["resolve_payload"] = payload
return "ConfigCASSave", "resolved-form.0"
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
seen["decode_payload"] = payload
return {
"status": "ok",
"source": {"table": payload["table"], "file_name": payload["file_name"]},
"form": {"name": "ФормаОбмена", "file_name": payload["file_name"]},
"profile": {"items": [], "commands": [], "attributes": [], "tables": [], "command_bars": []},
}
monkeypatch.setattr(adapter_server, "write_learning_resolve_file", fake_resolve_file)
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (None, {}, {"status": "not_found"}),
)
result = adapter_server.call_method(
"metadata.form.write_matrix.build",
{
"base_id": "upo_test",
"ref": "Обработки.ВторойОбмен",
"object_guid": owner_guid,
"form_name": "ФормаОбмена",
},
)
assert result["status"] == "ok"
assert seen["resolve_payload"]["kind"] == "DataProcessor"
assert seen["resolve_payload"]["name"] == "ВторойОбмен"
assert seen["resolve_payload"]["guid"] == owner_guid
assert seen["resolve_payload"]["form_name"] == "ФормаОбмена"
assert seen["decode_payload"]["file_name"] == "resolved-form.0"
for key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid"):
assert key not in seen["decode_payload"]
def test_form_target_move_and_write_matrix_help_expose_name_first_hierarchy() -> None:
methods = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}
move = methods["metadata.form.target.move"]
assert move["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name", "form_guid"]
assert move["selector_capabilities"]["child_selector_arguments"] == [
"from_element",
"to_element",
"with_element",
"after_element",
]
assert "allow_sql_saved_state_apply" in move["selector_capabilities"]["write_guards"]
matrix_build = methods["metadata.form.write_matrix.build"]
matrix_smoke = methods["metadata.form.write_matrix.smoke"]
assert matrix_build["selector_capabilities"]["read_only"] is True
assert matrix_smoke["selector_capabilities"]["read_only"] is False
assert matrix_smoke["selector_capabilities"]["write_guards"] == [
"allow_sql_saved_state_apply",
"allow_sql_saved_state_rollback",
"apply_and_rollback_only",
]
for method in (matrix_build, matrix_smoke):
assert method["selector_capabilities"]["accepts_ordinal"] is False
assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name", "form_guid"]
assert method["input_schema"]["properties"]["object_guid"]["type"] == "string"
assert method["input_schema"]["properties"]["form_guid"]["type"] == "string"
def test_form_write_matrix_smoke_runs_safe_candidates_with_rollback(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_form_write_matrix_build",
lambda payload: {
"status": "ok",
"source": {"table": "ConfigCASSave", "file_name": "form-guid.0"},
"form": {"file_name": "form-guid.0"},
"entries": [
{
"can_smoke": True,
"selector": {"element_path": "5.3", "command": "КомандаПример1"},
"property": {"canonical_property": "title", "test_value": "SMOKE"},
},
{
"can_smoke": False,
"reason": "identity_or_binding_property",
"selector": {"element_path": "5.3"},
"property": {"canonical_property": "name"},
},
],
"counts": {"entries": 2},
},
)
seen: dict[str, Any] = {}
def fake_metadata_write(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {
"status": "verified_and_rolled_back",
"result": {
"rolled_back": True,
"apply_result": {"semantic_verification": {"status": "ok", "checks": [{"ok": True}]}},
},
}
monkeypatch.setattr(adapter_server, "metadata_write", fake_metadata_write)
result = adapter_server.metadata_form_write_matrix_smoke(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"allow_sql_saved_state_apply": True,
"allow_sql_saved_state_rollback": True,
}
)
assert result["status"] == "ok"
assert result["counts"]["smoked"] == 1
assert result["counts"]["verified"] == 1
assert seen["payload"]["mode"] == "apply_and_rollback"
assert seen["payload"]["target"]["element_path"] == "5.3"
assert seen["payload"]["edits"] == [{"property": "title", "value": "SMOKE"}]
def test_write_learning_capture_uses_owner_scope_only_for_form_resolution(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
seen: dict[str, Any] = {}
owner_guid = "22222222-2222-2222-2222-222222222222"
monkeypatch.setenv("ONEC_ADAPTER_WRITE_LEARNING_DIR", str(tmp_path))
def fake_resolve_file(payload: dict[str, Any], **kwargs: Any) -> tuple[str, str]:
seen["resolve_payload"] = payload
return "ConfigCASSave", "resolved-form.0"
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
seen["decode_payload"] = payload
return {
"status": "ok",
"source": {"table": payload["table"], "file_name": payload["file_name"]},
"form": {"name": "ФормаОбмена", "file_name": payload["file_name"]},
"profile": {"items": [], "commands": [], "attributes": [], "tables": [], "command_bars": []},
}
monkeypatch.setattr(adapter_server, "write_learning_resolve_file", fake_resolve_file)
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (b"saved-form", {"server": "test", "database": "upo_test"}, None),
)
result = adapter_server.call_method(
"metadata.write_learning.capture_before",
{
"base_id": "upo_test",
"learning_id": "owner-form-scope",
"ref": "Обработки.ВторойОбмен",
"object_guid": owner_guid,
"form_name": "ФормаОбмена",
"element_name": "КнопкаВыполнить",
},
)
assert result["status"] == "ok"
assert Path(result["path"]).is_file()
assert seen["resolve_payload"]["kind"] == "DataProcessor"
assert seen["resolve_payload"]["name"] == "ВторойОбмен"
assert seen["resolve_payload"]["guid"] == owner_guid
assert seen["resolve_payload"]["form_name"] == "ФормаОбмена"
for key in ("ref", "kind", "name", "guid", "object_type", "object_name", "object_guid"):
assert key not in seen["decode_payload"]
def test_write_learning_capture_help_exposes_owner_and_form_selectors() -> None:
methods = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}
for method_name in ("metadata.write_learning.capture_before", "metadata.write_learning.capture_after"):
method = methods[method_name]
assert method["selector_capabilities"]["accepts_ordinal"] is False
assert method["selector_capabilities"]["form_selector_arguments"] == ["form", "form_name", "form_guid"]
assert method["selector_capabilities"]["write_guards"] == ["local_artifact_only", "no_sql_write"]
assert method["input_schema"]["properties"]["object_guid"]["type"] == "string"
assert method["input_schema"]["properties"]["form_guid"]["type"] == "string"
def test_write_learning_diff_and_infer_rule(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ONEC_ADAPTER_WRITE_LEARNING_DIR", str(tmp_path))
learning_id = "case1"
root = tmp_path / learning_id
root.mkdir()
before_path = root / "before-b.json"
after_path = root / "after-a.json"
base_capture = {
"schema": "onec_write_learning_capture.v1",
"status": "ok",
"learning_id": learning_id,
"base_id": "upo_test",
"source": {"kind": "live_sql", "table": "ConfigCASSave", "file_name": "form-guid.0"},
"form": {"file_name": "form-guid.0"},
"storage": {"sha1": "old", "bytes": 10},
"targets": [
{
"section": "commands",
"name": "КомандаПример1",
"path": "5.3",
"writable_properties": [
{
"property": "Заголовок",
"canonical_property": "title",
"presentation": "Заголовок",
"path": "5.3.3.2.1",
"value": "Пример1",
}
],
}
],
}
before = {**base_capture, "snapshot_id": "b", "stage": "before"}
after = json.loads(json.dumps({**base_capture, "snapshot_id": "a", "stage": "after", "storage": {"sha1": "new", "bytes": 12}}, ensure_ascii=False))
after["targets"][0]["writable_properties"][0]["value"] = "ПРОВЕРКА"
before_path.write_text(json.dumps(before, ensure_ascii=False), encoding="utf-8")
after_path.write_text(json.dumps(after, ensure_ascii=False), encoding="utf-8")
(root / "latest-before.json").write_text(json.dumps({"snapshot_id": "b", "path": str(before_path)}, ensure_ascii=False), encoding="utf-8")
(root / "latest-after.json").write_text(json.dumps({"snapshot_id": "a", "path": str(after_path)}, ensure_ascii=False), encoding="utf-8")
diff = adapter_server.metadata_write_learning_diff({"learning_id": learning_id})
assert diff["status"] == "changed"
assert diff["counts"]["changes"] == 1
assert diff["changes"][0]["old"] == "Пример1"
assert diff["changes"][0]["new"] == "ПРОВЕРКА"
rule = adapter_server.metadata_write_learning_infer_rule({"learning_id": learning_id})
assert rule["status"] == "ok"
assert rule["metadata_write"] == {
"method": "metadata.write",
"payload": {
"base_id": "upo_test",
"target": {"kind": "form", "table": "ConfigCASSave", "file_name": "form-guid.0", "element_path": "5.3"},
"mode": "plan",
"edits": [{"property": "title", "value": "ПРОВЕРКА", "expected_old": "Пример1"}],
},
}
public_rule = adapter_server.call_method("metadata.write_learning.infer_rule", {"learning_id": learning_id})
assert public_rule["metadata_write"]["payload"]["target"]["file_name"] == "form-guid.0"
assert public_rule["metadata_write"]["payload"]["target"]["element_path"] == "5.3"
def test_write_learning_capture_stores_decoded_targets(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ONEC_ADAPTER_WRITE_LEARNING_DIR", str(tmp_path))
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
"form": {"file_name": payload["file_name"]},
"profile": {
"commands": [
{
"name": "КомандаПример1",
"title": "Пример1",
"path": "5.3",
"title_path": "5.3.3.2.1",
}
]
},
}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda base_id, table, file_name, timeout_seconds=30: (b"payload", {"server": "s", "database": base_id}, None),
)
result = adapter_server.metadata_write_learning_capture(
{
"base_id": "upo_test",
"learning_id": "case2",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
},
"before",
)
assert result["status"] == "ok"
path = Path(result["path"])
assert path.exists()
capture = json.loads(path.read_text(encoding="utf-8"))
assert capture["storage"]["sha1"] == adapter_server.hashlib.sha1(b"payload").hexdigest()
assert "payload_hex" not in json.dumps(capture, ensure_ascii=False)
assert capture["targets"][0]["writable_properties"][0]["path"] == "5.3.3.2.1"
def test_saved_state_backups_list_filters_without_payload_hex(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
backup_dir = tmp_path / "backups"
backup_dir.mkdir()
(backup_dir / "20260625T010101Z-a.json").write_text(
json.dumps(
{
"schema": "onec_storage_apply_backup.v1",
"backup_id": "a",
"created_at_utc": "2026-06-25T01:01:01Z",
"base_id": "upo_test",
"source": {"database": "upo_test", "table": "ConfigCASSave", "file_name": "form.0"},
"original": {"sha1": "old", "bytes": 10, "payload_hex": "ffff"},
"replacement": {"sha1": "new", "bytes": 11},
},
ensure_ascii=False,
),
encoding="utf-8",
)
monkeypatch.setenv("ONEC_ADAPTER_BACKUP_DIR", str(backup_dir))
result = adapter_server.storage_saved_state_backups_list({"base_id": "upo_test", "table": "ConfigCASSave", "file_name": "form.0"})
assert result["status"] == "ok"
assert result["counts"]["returned"] == 1
assert result["backups"][0]["backup_id"] == "a"
assert "payload_hex" not in result["backups"][0]["original"]
def test_form_write_target_resolve_finds_command_property(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
"form": {"file_name": payload["file_name"]},
"profile": {
"items": [],
"commands": [
{
"name": "КомандаПример1",
"id": "2",
"title": "Пример1",
"path": "5.3",
"title_path": "5.3.3.2.1",
"parameters": [{"index": 3, "presentation": "Заголовок", "value": "Пример1"}],
}
],
},
}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
result = adapter_server.metadata_form_write_target_resolve(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"file_name": "form-guid.0",
"command": "КомандаПример1",
"property": "Заголовок",
"value": "ПРОВЕРКА",
}
)
assert result["status"] == "ok"
assert result["target"]["section"] == "commands"
assert result["property"] == {"property": "Заголовок", "path": "5.3.3.2.1", "old": "Пример1", "status": "ok", "new": "ПРОВЕРКА"}
assert result["semantic_diff"]["old"] == "Пример1"
assert result["semantic_diff"]["new"] == "ПРОВЕРКА"
def test_saved_state_forms_search_indexes_form_commands(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {
"status": "ok",
"files": [{"FileName": "extension__form.0", "PartCount": 1, "Bytes": 123}],
"counts": {"files": 1},
},
)
monkeypatch.setattr(
adapter_server,
"metadata_form_decode",
lambda payload: {
"status": "ok",
"source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
"form": {"file_name": payload["file_name"]},
"counts": {"commands": 1},
"profile": {"items": [], "commands": [{"name": "КомандаПример1", "title": "Пример1", "path": "5.3", "title_path": "5.3.3.2.1"}]},
},
)
result = adapter_server.metadata_saved_state_forms_search(
{
"base_id": "upo_test",
"tables": ["ConfigCASSave"],
"element": "КомандаПример1",
"include_storage": True,
}
)
assert result["status"] == "ok"
assert result["counts"]["forms"] == 1
assert result["forms"][0]["file_name"] == "extension__form.0"
assert result["forms"][0]["source"]["file_name"] == "extension__form.0"
assert result["forms"][0]["form"]["file_name"] == "extension__form.0"
assert result["forms"][0]["matches"][0]["section"] == "commands"
assert result["forms"][0]["matches"][0]["name"] == "КомандаПример1"
def test_saved_state_forms_search_scans_base_form_rows_without_suffix(monkeypatch: pytest.MonkeyPatch) -> None:
seen: list[str] = []
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {
"status": "ok",
"files": [{"FileName": "base-form-guid", "PartCount": 1, "Bytes": 123}],
"counts": {"files": 1},
},
)
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
seen.append(payload["file_name"])
return {
"status": "ok",
"source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
"form": {"file_name": payload["file_name"]},
"profile": {"items": [{"name": "Наименование", "title": "Наименование", "path": "1"}], "commands": []},
}
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
result = adapter_server.metadata_saved_state_forms_search(
{"base_id": "upo_test", "tables": ["ConfigSave"], "include_storage": True}
)
assert seen == ["base-form-guid"]
assert result["counts"]["forms"] == 1
assert result["forms"][0]["file_name"] == "base-form-guid"
def test_form_element_write_auto_resolves_file_name(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_resolve(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"source": {"table": "ConfigCASSave", "file_name": "form-guid.0"},
"target": {"section": "commands", "name": "КомандаПример1", "path": "5.3"},
"property": {"status": "ok", "path": "5.3.3.2.1", "old": "Пример1", "new": "ПРОВЕРКА"},
}
def fake_decode(payload: dict[str, Any]) -> dict[str, Any]:
seen["decode"] = payload
return {
"status": "ok",
"source": {"kind": "live_sql", "table": payload["table"], "file_name": payload["file_name"]},
"form": {"file_name": payload["file_name"]},
"profile": {
"commands": [
{"name": "КомандаПример1", "title": "Пример1", "path": "5.3", "title_path": "5.3.3.2.1"}
]
},
}
def fake_changes(payload: dict[str, Any]) -> dict[str, Any]:
seen["changes"] = payload
return {"status": "accepted_for_review", "source": payload["source"], "edits": payload["edits"]}
monkeypatch.setattr(adapter_server, "metadata_form_write_target_resolve", fake_resolve)
monkeypatch.setattr(adapter_server, "metadata_form_decode", fake_decode)
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes)
result = adapter_server.metadata_form_element_write(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"element": "КомандаПример1",
"allow_saved_state_write": True,
"edits": [{"property": "Заголовок", "value": "ПРОВЕРКА"}],
}
)
assert seen["decode"]["file_name"] == "form-guid.0"
assert "element_path" not in seen["decode"]
assert seen["changes"]["source"]["file_name"] == "form-guid.0"
assert result["semantic_diff"][0]["presentation"] == "КомандаПример1.Заголовок: Пример1 -> ПРОВЕРКА"
def test_objects_list_rejects_ambiguous_query_argument() -> None:
result = adapter_server.validate_adapter_job_payload("metadata.objects.list", {"base_id": "upo_test", "query": "Документ"})
assert result is not None
assert result["status"] == "invalid_argument"
assert result["argument"] == "query"
def test_form_element_write_apply_plan_returns_proposal(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_plan(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {"status": "accepted_for_review", "source": {"table": "ConfigSave", "file_name": "form-guid.0"}}
monkeypatch.setattr(adapter_server, "metadata_form_element_write", fake_plan)
result = adapter_server.metadata_form_element_write_apply(
{
"base_id": "upo_test",
"execution_mode": "plan",
"table": "ConfigSave",
"file_name": "form-guid.0",
"element_id": "67",
"edits": [{"property": "title", "value": "Новый"}],
}
)
assert seen["payload"]["allow_saved_state_write"] is True
assert "include_payload" not in seen["payload"]
assert result["status"] == "planned"
assert result["proposal"]["status"] == "accepted_for_review"
def test_form_element_write_apply_smoke_applies_and_rolls_back(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
proposal = {
"status": "accepted_for_review",
"source": {"table": "ConfigSave", "file_name": "form-guid.0"},
"encoded": {"payload_hex": "00"},
}
def fake_plan(payload: dict[str, Any]) -> dict[str, Any]:
seen["plan"] = payload
return proposal
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["apply"] = payload
return {"status": "applied", "applied": True, "backup": {"backup_id": "b" * 32}, "semantic_verification": {"status": "ok"}}
def fake_rollback(payload: dict[str, Any]) -> dict[str, Any]:
seen["rollback"] = payload
return {"status": "applied", "applied": True}
monkeypatch.setattr(adapter_server, "metadata_form_element_write", fake_plan)
monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", fake_apply)
monkeypatch.setattr(adapter_server, "storage_saved_state_rollback", fake_rollback)
result = adapter_server.metadata_form_element_write_apply(
{
"base_id": "upo_test",
"execution_mode": "apply_and_rollback",
"allow_sql_saved_state_apply": True,
"allow_sql_saved_state_rollback": True,
"table": "ConfigSave",
"file_name": "form-guid.0",
"element_id": "67",
"edits": [{"property": "title", "value": "Новый"}],
"timeout_seconds": 60,
}
)
assert seen["plan"]["allow_saved_state_write"] is True
assert seen["plan"]["include_payload"] is True
assert seen["apply"]["proposal"] is proposal
assert seen["rollback"]["backup_id"] == "b" * 32
assert result["status"] == "verified_and_rolled_back"
assert result["applied"] is True
assert result["rolled_back"] is True
def test_form_element_write_apply_and_verify_does_not_rollback(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
proposal = {
"status": "accepted_for_review",
"source": {"table": "ConfigSave", "file_name": "form-guid.0"},
"encoded": {"payload_hex": "00"},
}
def fake_plan(payload: dict[str, Any]) -> dict[str, Any]:
seen["plan"] = payload
return proposal
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["apply"] = payload
return {
"status": "applied",
"applied": True,
"readback": {"verified": True},
"semantic_verification": {"status": "ok"},
}
def fake_rollback(payload: dict[str, Any]) -> dict[str, Any]:
seen["rollback"] = payload
return {"status": "applied", "applied": True}
monkeypatch.setattr(adapter_server, "metadata_form_element_write", fake_plan)
monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", fake_apply)
monkeypatch.setattr(adapter_server, "storage_saved_state_rollback", fake_rollback)
result = adapter_server.metadata_form_element_write_apply(
{
"base_id": "upo_test",
"execution_mode": "apply_and_verify",
"allow_sql_saved_state_apply": True,
"table": "ConfigSave",
"file_name": "form-guid.0",
"element_id": "67",
"edits": [{"property": "title", "value": "Новый"}],
}
)
assert seen["plan"]["include_payload"] is True
assert seen["apply"]["proposal"] is proposal
assert "rollback" not in seen
assert result["status"] == "verified"
assert result["applied"] is True
def test_form_element_write_apply_response_hides_payload_hex(monkeypatch: pytest.MonkeyPatch) -> None:
proposal = {
"status": "accepted_for_review",
"source": {"table": "ConfigSave", "file_name": "form-guid.0"},
"encoded": {"payload_hex": "00", "sha1": "new"},
}
seen: dict[str, Any] = {}
monkeypatch.setattr(adapter_server, "metadata_form_element_write", lambda payload: proposal)
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["proposal"] = payload["proposal"]
return {"status": "applied", "applied": True, "readback": {"verified": True}, "semantic_verification": {"status": "ok"}}
monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", fake_apply)
result = adapter_server.metadata_form_element_write_apply(
{
"base_id": "upo_test",
"execution_mode": "apply_and_verify",
"allow_sql_saved_state_apply": True,
"table": "ConfigSave",
"file_name": "form-guid.0",
"element_id": "67",
"edits": [{"property": "title", "value": "Новый"}],
}
)
assert seen["proposal"]["encoded"]["payload_hex"] == "00"
assert "payload_hex" not in result["proposal"]["encoded"]
def test_adapter_service_token_uses_constant_time_bearer_check(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ONEC_ADAPTER_SERVICE_TOKEN", "secret-token")
assert adapter_server.adapter_request_authorized("Bearer secret-token") is True
assert adapter_server.adapter_request_authorized("bearer secret-token") is True
assert adapter_server.adapter_request_authorized("Bearer wrong") is False
assert adapter_server.adapter_request_authorized(None) is False
def test_query_validation_blocks_unsafe_read_features_but_ignores_literals() -> None:
blocked = adapter_server.validate_query(
{"base_id": "upo_test", "query": "SELECT * FROM OPENROWSET(BULK 'x', SINGLE_BLOB) AS payload"}
)
literal = adapter_server.validate_query(
{"base_id": "upo_test", "query": "SELECT N'update and openrowset are plain text' AS message"}
)
assert blocked["valid"] is False
assert blocked["reason"] == "unsafe_read_feature"
assert literal["valid"] is True
def test_sensitive_query_result_fields_are_masked() -> None:
rows, fields = adapter_server.mask_sensitive_query_rows(
[{"email": "test@example.com", "ИНН": "123", "Наименование": "Тест", "phone": None}]
)
assert rows == [{"email": "***", "ИНН": "***", "Наименование": "Тест", "phone": None}]
assert fields == ["email", "phone", "ИНН"]
def test_data_schema_resolves_public_object_and_logical_fields(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"get_object",
lambda *args, **kwargs: {
"status": "ok",
"object": {
"kind": "Catalog",
"name": "Контрагенты",
"guid": "a" * 32,
"storage": {"dbnames": [{"sql_number": 42}]},
},
},
)
monkeypatch.setattr(
adapter_server,
"data_sql_rows",
lambda *args, **kwargs: (
[
{"name": "_IDRRef", "type_name": "binary", "max_length": 16, "precision": 0, "scale": 0, "is_nullable": False},
{"name": "_Fld100", "type_name": "nvarchar", "max_length": 100, "precision": 0, "scale": 0, "is_nullable": True},
],
None,
),
)
monkeypatch.setattr(
adapter_server,
"metadata_object_attributes",
lambda payload: {
"status": "ok",
"attributes": [
{
"name": "ИНН",
"type": {"kind": "string", "length": 12},
"storage_routes": [{"physical_name_candidate": "_Fld100"}],
}
],
},
)
result = adapter_server.data_object_schema({"base_id": "upo_test", "kind": "Catalog", "name": "Контрагенты"})
assert result["status"] == "ok"
assert result["table"]["name"] == "_Reference42"
assert [field["name"] for field in result["fields"]] == ["ref", "ИНН"]
def test_data_schema_resolves_platform_85_constant_table_and_value(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"get_object",
lambda *args, **kwargs: {
"status": "ok",
"object": {
"kind": "Constant",
"name": "ИспользоватьСотрудников",
"guid": "a" * 32,
"storage": {"dbnames": [{"storage_role": "Const", "sql_number": 56995}]},
},
},
)
monkeypatch.setattr(
adapter_server,
"data_sql_rows",
lambda *args, **kwargs: (
[
{"name": "_Fld56996", "type_name": "binary", "max_length": 1, "precision": 0, "scale": 0, "is_nullable": False},
{"name": "_RecordKey", "type_name": "binary", "max_length": 1, "precision": 0, "scale": 0, "is_nullable": False},
],
None,
),
)
monkeypatch.setattr(adapter_server, "metadata_object_attributes", lambda payload: {"status": "ok"})
result = adapter_server.data_object_schema(
{"base_id": "upo_test", "kind": "Constant", "name": "ИспользоватьСотрудников"}
)
assert result["status"] == "ok"
assert result["table"]["name"] == "_Const56995"
assert [field["name"] for field in result["fields"]] == ["value", "record_key"]
assert result["fields"][0]["section"] == "value"
def test_data_schema_has_physical_enum_table_route() -> None:
assert adapter_server.DATA_TABLE_PREFIXES["Enum"] == "_Enum"
def test_data_schema_has_physical_business_process_table_route() -> None:
assert adapter_server.DATA_TABLE_PREFIXES["BusinessProcess"] == "_BPr"
def test_data_schema_maps_business_process_and_task_system_columns() -> None:
assert adapter_server.DATA_SYSTEM_COLUMNS["_Completed"] == "completed"
assert adapter_server.DATA_SYSTEM_COLUMNS["_Started"] == "started"
assert adapter_server.DATA_SYSTEM_COLUMNS["_HeadTaskRRef"] == "head_task_ref"
assert adapter_server.DATA_SYSTEM_COLUMNS["_BusinessProcess_RRRef"] == "business_process"
assert adapter_server.DATA_SYSTEM_COLUMNS["_Point_RRRef"] == "route_point"
assert adapter_server.DATA_SYSTEM_COLUMNS["_Executed"] == "executed"
def test_data_schema_cache_key_unifies_equivalent_public_selectors() -> None:
by_ref = adapter_server.data_schema_cache_key(
{"base_id": "upo_test", "ref": "ChartOfCalculationTypes.Начисления"}
)
enriched = adapter_server.data_schema_cache_key(
{
"base_id": "UPO_TEST",
"ref": "ChartOfCalculationTypes.Начисления",
"kind": "ChartOfCalculationTypes",
"name": "Начисления",
"guid": "be08c831-b5a8-4e28-b13c-26875635a7f1",
}
)
by_kind_name = adapter_server.data_schema_cache_key(
{"base_id": "upo_test", "kind": "ChartOfCalculationTypes", "name": "Начисления"}
)
assert by_ref == enriched == by_kind_name
@pytest.mark.parametrize("logical_name", ["completed", "started", "executed"])
def test_data_decoder_treats_workflow_flags_as_booleans(logical_name: str) -> None:
assert adapter_server.onec_data_value(b"\x01", logical_name=logical_name) is True
assert adapter_server.onec_data_value(b"\x00", logical_name=logical_name) is False
def test_data_schema_selector_keeps_public_ref_with_explicit_record_ref() -> None:
payload = adapter_server.data_schema_selector_payload(
{"base_id": "upo_test", "ref": "Enum.Статусы", "record_ref": "01" * 16}
)
assert payload["ref"] == "Enum.Статусы"
assert "record_ref" not in payload
def test_decode_data_rows_uses_constant_boolean_type() -> None:
rows = adapter_server.decode_data_rows(
[{"value": b"\x01"}],
{"value": "value"},
{"value": {"kind": "boolean", "presentation": "Булево"}},
)
assert rows == [{"value": True}]
def test_enrich_enum_data_rows_uses_guid_variant() -> None:
guid = "a18b60a1-81f1-4852-a612-0510aa3159b1"
rows = adapter_server.enrich_enum_data_rows(
[{"ref": {"type": "reference", "guid_variants": ["a6120510-aa31-59b1-4852-81f1a18b60a1", guid]}}],
{guid: {"name": "АсинхронныйОбмен", "synonym": "Прямое соединение", "value_ref": "Enum.ПрограммыБанка.EnumValue.АсинхронныйОбмен"}},
)
assert rows[0]["name"] == "АсинхронныйОбмен"
assert rows[0]["ref"] == {"type": "reference", "guid_variants": ["a6120510-aa31-59b1-4852-81f1a18b60a1", guid]}
assert rows[0]["value_ref"] == "Enum.ПрограммыБанка.EnumValue.АсинхронныйОбмен"
def test_data_read_builds_parameterized_logical_query_and_decodes_values(monkeypatch: pytest.MonkeyPatch) -> None:
schema = {
"status": "ok",
"base_id": "upo_test",
"object": {"kind": "Catalog", "name": "Контрагенты"},
"table": {"name": "_Reference42"},
"fields": [
{"name": "ref", "physical_name": "_IDRRef"},
{"name": "description", "physical_name": "_Description"},
{"name": "marked_for_deletion", "physical_name": "_Marked"},
],
}
seen: dict[str, Any] = {}
monkeypatch.setattr(adapter_server, "data_object_schema", lambda payload: schema)
def fake_rows(base_id: str, query: str, params: Any = None, **kwargs: Any) -> tuple[list[dict[str, Any]], None]:
seen.update({"base_id": base_id, "query": query, "params": params})
return [{"ref": bytes.fromhex("01" * 16), "description": "Тест", "marked_for_deletion": b"\x00"}], None
monkeypatch.setattr(adapter_server, "data_sql_rows", fake_rows)
result = adapter_server.data_read(
{"base_id": "upo_test", "kind": "Catalog", "name": "Контрагенты", "filters": {"description": "Тест"}, "limit": 10}
)
assert result["status"] == "ok"
assert "[_Description]=%s" in seen["query"]
assert "[_Marked]=0x00" in seen["query"]
assert seen["params"][0] == "Тест"
assert result["rows"][0]["description"] == "Тест"
assert result["rows"][0]["marked_for_deletion"] is False
def test_data_list_keeps_public_object_ref_separate_from_record_ref(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
schema = {
"status": "ok",
"base_id": "upo_test",
"object": {"kind": "Catalog", "name": "Контрагенты"},
"table": {"name": "_Reference42"},
"fields": [{"name": "ref", "physical_name": "_IDRRef"}],
}
def fake_schema(payload: dict[str, Any]) -> dict[str, Any]:
seen["schema_payload"] = payload
return schema
def fake_rows(base_id: str, query: str, params: Any = None, **kwargs: Any) -> tuple[list[dict[str, Any]], None]:
seen.update({"query": query, "params": params})
return [], None
monkeypatch.setattr(adapter_server, "data_object_schema", fake_schema)
monkeypatch.setattr(adapter_server, "data_sql_rows", fake_rows)
result = adapter_server.data_read({"base_id": "upo_test", "ref": "Catalog.Контрагенты", "limit": 10}, method="data.list")
assert result["status"] == "ok"
assert seen["schema_payload"]["ref"] == "Catalog.Контрагенты"
assert "[_IDRRef]=%s" not in seen["query"]
def test_data_get_uses_record_ref_and_object_ref_independently(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
schema = {
"status": "ok",
"base_id": "upo_test",
"object": {"kind": "Catalog", "name": "Контрагенты"},
"table": {"name": "_Reference42"},
"fields": [{"name": "ref", "physical_name": "_IDRRef"}],
}
monkeypatch.setattr(adapter_server, "data_object_schema", lambda payload: seen.setdefault("schema_payload", payload) and schema)
def fake_rows(base_id: str, query: str, params: Any = None, **kwargs: Any) -> tuple[list[dict[str, Any]], None]:
seen.update({"query": query, "params": params})
return [], None
monkeypatch.setattr(adapter_server, "data_sql_rows", fake_rows)
record_ref = "01" * 16
result = adapter_server.data_read(
{"base_id": "upo_test", "object_ref": "Catalog.Контрагенты", "record_ref": record_ref, "limit": 1},
method="data.get",
)
assert result["status"] == "ok"
assert seen["schema_payload"]["ref"] == "Catalog.Контрагенты"
assert "[_IDRRef]=%s" in seen["query"]
assert seen["params"][0] == bytes.fromhex(record_ref)
def test_data_virtual_accumulation_turnovers_use_signed_resources(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
schema = {
"status": "ok",
"base_id": "upo_test",
"object": {"kind": "AccumulationRegister", "name": "ОстаткиТоваров"},
"table": {"name": "_AccumRg10"},
"fields": [
{"name": "period", "physical_name": "_Period", "section": "system"},
{"name": "active", "physical_name": "_Active", "section": "system"},
{"name": "_RecordKind", "physical_name": "_RecordKind", "section": "system"},
{"name": "Номенклатура", "physical_name": "_Fld11RRef", "section": "dimensions"},
{"name": "Количество", "physical_name": "_Fld12", "section": "resources"},
],
}
monkeypatch.setattr(adapter_server, "data_object_schema", lambda payload: schema)
def fake_rows(base_id: str, query: str, params: Any = None, **kwargs: Any) -> tuple[list[dict[str, Any]], None]:
seen.update({"query": query, "params": params})
return [{"Номенклатура": bytes.fromhex("01" * 16), "Количество": 3.5}], None
monkeypatch.setattr(adapter_server, "data_sql_rows", fake_rows)
result = adapter_server.data_virtual(
{
"base_id": "upo_test",
"kind": "AccumulationRegister",
"name": "ОстаткиТоваров",
"virtual_table": "turnovers",
"start": "2025-01-01",
"end": "2025-01-31",
"allow_full_scan": True,
}
)
assert result["status"] == "ok"
assert "CASE WHEN [_RecordKind]=0" in seen["query"]
assert "GROUP BY [_Fld11RRef]" in seen["query"]
assert seen["params"][0].year == 4025
assert seen["params"][1].year == 4025
assert result["rows"][0]["Количество"] == 3.5
def test_data_virtual_information_slice_uses_dimensions_and_period(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
schema = {
"status": "ok",
"base_id": "upo_test",
"object": {"kind": "InformationRegister", "name": "Цены"},
"table": {"name": "_InfoRg20"},
"fields": [
{"name": "period", "physical_name": "_Period", "section": "system"},
{"name": "Номенклатура", "physical_name": "_Fld21RRef", "section": "dimensions"},
{"name": "Цена", "physical_name": "_Fld22", "section": "resources"},
],
}
monkeypatch.setattr(adapter_server, "data_object_schema", lambda payload: schema)
def fake_rows(base_id: str, query: str, params: Any = None, **kwargs: Any) -> tuple[list[dict[str, Any]], None]:
seen.update({"query": query, "params": params})
return [], None
monkeypatch.setattr(adapter_server, "data_sql_rows", fake_rows)
result = adapter_server.data_virtual(
{"base_id": "upo_test", "kind": "InformationRegister", "name": "Цены", "virtual_table": "СрезПоследних", "period": "2025-02-01", "allow_full_scan": True}
)
assert result["status"] == "ok"
assert "ROW_NUMBER() OVER (PARTITION BY [_Fld21RRef] ORDER BY [_Period] DESC)" in seen["query"]
assert "[_Period]<=%s" in seen["query"]
assert seen["params"][0].year == 4025
def test_metadata_module_write_apply_plans_stream_edit(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {
"status": "accepted_for_review",
"source": {"table": "ConfigCASSave", "file_name": "object__module.0", "stream_index": 4},
"encoded": {"sha1": "new"},
}
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)
result = adapter_server.metadata_module_write_apply(
{
"base_id": "upo_test",
"module_ref": "ConfigCASSave:object__module.0#stream:4",
"allow_saved_state_write": True,
"old": "Перем Параметры; ",
"new": "Перем Параметры; ",
"expected_contains": "Перем Параметры;",
"expected_text_sha1": "a" * 40,
}
)
assert result["status"] == "planned"
assert result["module_ref"] == "ConfigCASSave:object__module.0#stream:4"
assert seen["payload"]["source"]["table"] == "ConfigCASSave"
assert seen["payload"]["source"]["file_name"] == "object__module.0"
assert seen["payload"]["source"]["module_id"] == "ConfigCASSave:object__module.0#stream:4"
assert seen["payload"]["edits"] == [
{
"stream_index": 4,
"expected_contains": "Перем Параметры;",
"expected_text_sha1": "a" * 40,
"replace": {"old": "Перем Параметры; ", "new": "Перем Параметры; "},
}
]
def test_metadata_module_write_apply_applies_and_rolls_back(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
proposal = {
"status": "accepted_for_review",
"source": {"table": "ConfigCASSave", "file_name": "object__module.0"},
"encoded": {"payload_hex": "00", "sha1": "new"},
}
def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
seen["proposal_payload"] = payload
return proposal
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["apply"] = payload
return {"status": "applied", "applied": True, "backup": {"backup_id": "c" * 32}, "readback": {"verified": True}}
def fake_rollback(payload: dict[str, Any]) -> dict[str, Any]:
seen["rollback"] = payload
return {"status": "applied", "applied": True}
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)
monkeypatch.setattr(adapter_server, "metadata_write_apply_plan_gate", lambda *args, **kwargs: ({"allowed": True}, None))
monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", fake_apply)
monkeypatch.setattr(adapter_server, "storage_saved_state_rollback", fake_rollback)
result = adapter_server.metadata_module_write_apply(
{
"base_id": "upo_test",
"module_ref": "ConfigCASSave:object__module.0#stream:0",
"allow_saved_state_write": True,
"mode": "apply_and_rollback",
"allow_sql_saved_state_apply": True,
"allow_sql_saved_state_rollback": True,
"lock_session_id": "rlock-test",
"repository_object": "Справочник.Тест",
"old": "a",
"new": "b",
}
)
assert seen["proposal_payload"]["include_payload"] is True
assert seen["apply"]["proposal"] is proposal
assert seen["apply"]["lock_session_id"] == "rlock-test"
assert seen["apply"]["repository_object"] == "Справочник.Тест"
assert seen["rollback"]["backup_id"] == "c" * 32
assert seen["rollback"]["lock_session_id"] == "rlock-test"
assert seen["rollback"]["repository_object"] == "Справочник.Тест"
assert result["proposal"]["encoded"] == {"sha1": "new"}
assert result["status"] == "verified_and_rolled_back"
assert result["applied"] is True
assert result["rolled_back"] is True
def test_metadata_module_write_apply_accepts_flat_routine_edit(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
routine_text = "Функция Настроить(УсловноеОформление) Экспорт\n\tВозврат Неопределено;\nКонецФункции\n"
def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {
"status": "accepted_for_review",
"source": {"table": "ConfigCASSave", "file_name": "object__module.0", "stream_index": 4},
"encoded": {"sha1": "new"},
}
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)
result = adapter_server.metadata_module_write_apply(
{
"base_id": "upo_test",
"module_ref": "ConfigCASSave:object__module.0#stream:4",
"allow_saved_state_write": True,
"routine_name": "Настроить",
"routine_text": routine_text,
"routine_operation": "replace",
"expected_old_contains": "ТипУсловноеОформление",
}
)
assert result["status"] == "planned"
assert seen["payload"]["edits"] == [
{
"stream_index": 4,
"routine": {
"text": routine_text,
"operation": "replace",
"name": "Настроить",
"expected_old_contains": "ТипУсловноеОформление",
},
}
]
def test_metadata_module_write_apply_scopes_fragment_to_routine(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
stream_text = (
"&\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435\r\n"
"\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 Cmd(\u041a\u043e\u043c\u0430\u043d\u0434\u0430)\r\n"
"\t// same\r\n"
"\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\r\n\r\n"
"\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 Other(\u041a\u043e\u043c\u0430\u043d\u0434\u0430)\r\n"
"\t// same\r\n"
"\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\r\n"
)
stream_bytes = stream_text.encode("utf-8-sig")
header = f"\r\n{len(stream_bytes):08x} {len(stream_bytes):08x} 7fffffff \r\n".encode("ascii")
stored = compress_payload(b"prefix" + header + stream_bytes, "raw_deflate")
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (stored, {"database": "upo_test"}, None),
)
def fake_changes_propose(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {
"status": "accepted_for_review",
"source": {"table": "ConfigCASSave", "file_name": "object__module.0", "stream_index": 0},
"encoded": {"sha1": "new"},
}
monkeypatch.setattr(adapter_server, "changes_propose", fake_changes_propose)
monkeypatch.setattr(adapter_server, "metadata_write_apply_plan_gate", lambda *args, **kwargs: ({"allowed": True}, None))
result = adapter_server.metadata_module_write_apply(
{
"base_id": "upo_test",
"module_ref": "ConfigCASSave:object__module.0#stream:0",
"allow_saved_state_write": True,
"_force_fragment_replace": True,
"routine_name": "Cmd",
"old": "// same",
"new": "// changed",
}
)
assert result["status"] == "planned"
assert result["scope"] == {"kind": "routine", "routine_name": "Cmd"}
assert result["counts"] == {"occurrences": 1}
routine_edit = seen["payload"]["edits"][0]["routine"]
assert routine_edit["name"] == "Cmd"
assert routine_edit["operation"] == "replace"
assert routine_edit["text"].count("// changed") == 1
assert "// same" not in routine_edit["text"]
def test_metadata_module_write_apply_reports_routine_fragment_ambiguity(monkeypatch: pytest.MonkeyPatch) -> None:
stream_text = (
"\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 Cmd(\u041a\u043e\u043c\u0430\u043d\u0434\u0430)\r\n"
"\t// same\r\n"
"\t// same\r\n"
"\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\r\n"
)
stream_bytes = stream_text.encode("utf-8-sig")
header = f"\r\n{len(stream_bytes):08x} {len(stream_bytes):08x} 7fffffff \r\n".encode("ascii")
stored = compress_payload(b"prefix" + header + stream_bytes, "raw_deflate")
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (stored, {"database": "upo_test"}, None),
)
monkeypatch.setattr(
adapter_server,
"changes_propose",
lambda payload: (_ for _ in ()).throw(AssertionError("ambiguous routine fragment must not be proposed")),
)
monkeypatch.setattr(adapter_server, "metadata_write_apply_plan_gate", lambda *args, **kwargs: ({"allowed": True}, None))
result = adapter_server.metadata_module_write_apply(
{
"base_id": "upo_test",
"module_ref": "ConfigCASSave:object__module.0#stream:0",
"allow_saved_state_write": True,
"_force_fragment_replace": True,
"routine_name": "Cmd",
"old": "// same",
"new": "// changed",
}
)
assert result["status"] == "ambiguous"
assert result["error"] == "ambiguous_fragment"
assert result["counts"] == {"occurrences": 2}
assert result["scope"] == {"kind": "routine", "routine_name": "Cmd"}
def test_saved_state_modules_search_returns_stream_refs(monkeypatch: pytest.MonkeyPatch) -> None:
module_text = "Процедура Команда1(Команда)\r\nКонецПроцедуры\r\n".encode("utf-8-sig")
header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
stored = compress_payload(b"prefix" + header + module_text, "raw_deflate")
file_name = "owner-guid__module-guid.0"
def fake_files_list(payload: dict[str, Any]) -> dict[str, Any]:
return {
"status": "ok",
"files": [{"FileName": file_name, "PartCount": 1, "Bytes": len(stored)}],
}
def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
assert base_id == "upo_test"
assert table == "ConfigCASSave"
assert requested_file_name == file_name
return stored, {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "storage_files_list", fake_files_list)
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.metadata_saved_state_modules_search(
{
"base_id": "upo_test",
"tables": ["ConfigCASSave"],
"query": "Команда1",
"limit": 10,
"include_storage": True,
}
)
assert result["status"] == "ok"
assert result["counts"]["modules"] == 1
module = result["modules"][0]
assert module["identity"] == {"owner_guid": "owner-guid", "module_guid": "module-guid"}
assert module["streams"][0]["module_ref"] == "ConfigCASSave:owner-guid__module-guid.0#stream:0"
assert module["streams"][0]["match"]["in_text"] is True
write_target = module["streams"][0]["write_plan_target"]
assert write_target["kind"] == "module"
assert write_target["module_ref"] == "ConfigCASSave:owner-guid__module-guid.0#stream:0"
assert write_target["file_name"] == file_name
assert write_target["stream_index"] == 0
assert write_target["object_guid"] == "owner-guid"
assert write_target["module_guid"] == "module-guid"
assert write_target["expected_sha1"] == module["payload"]["sha1"]
def test_saved_state_modules_search_returns_form_embedded_module_container(monkeypatch: pytest.MonkeyPatch) -> None:
form_text = '{"meta","data","&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\nКонецПроцедуры\r\n\r\n///----\\",\r\n{4,0,0,0,{#base64:AAAA}}"}'
stored = compress_payload(form_text.encode("utf-8-sig"), "raw_deflate")
descriptor = compress_payload('{"t_Форма"}'.encode("utf-8-sig"), "raw_deflate")
file_name = "extension-guid__form-guid.0"
descriptor_file_name = "extension-guid__form-guid"
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {
"status": "ok",
"files": [
{"FileName": descriptor_file_name, "PartCount": 1, "Bytes": len(descriptor)},
{"FileName": file_name, "PartCount": 1, "Bytes": len(stored)},
],
},
)
def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
assert base_id == "upo_test"
assert table == "ConfigCASSave"
if requested_file_name == descriptor_file_name:
return descriptor, {"database": "upo_test"}, None
assert requested_file_name == file_name
return stored, {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.metadata_saved_state_modules_search(
{
"base_id": "upo_test",
"tables": ["ConfigCASSave"],
"query": "ЗаменаДомена",
"limit": 10,
"include_storage": True,
}
)
assert result["status"] == "ok"
assert result["counts"]["modules"] == 1
module = result["modules"][0]
assert module["payload"]["role"] == "form_embedded_module_payload"
assert module["form"]["name"] == "t_Форма"
assert module["module"] == {"kind": "form_module", "name": "Модуль формы"}
stream = module["streams"][0]
assert stream["module_ref"] == "ConfigCASSave:extension-guid__form-guid.0"
assert stream["module_path"] == "2"
assert stream["form"]["name"] == "t_Форма"
assert stream["module"] == {"kind": "form_module", "name": "Модуль формы"}
assert "///----" not in stream["preview"]
assert stream["match"]["in_text"] is True
write_target = stream["write_plan_target"]
assert write_target["kind"] == "module"
assert write_target["module_ref"] == "ConfigCASSave:extension-guid__form-guid.0"
assert "#stream:" not in write_target["module_ref"]
assert write_target["file_name"] == file_name
assert write_target["module_path"] == "2"
assert write_target["object_guid"] == "extension-guid"
assert write_target["form_guid"] == "form-guid"
def test_public_saved_state_module_search_row_is_name_first() -> None:
public = adapter_server.public_saved_state_modules_search_row(
{
"table": "ConfigCASSave",
"file_name": "extension-guid__form-guid.0",
"identity": {"owner_guid": "extension-guid", "module_guid": "form-guid"},
"owner": {
"status": "resolved",
"kind": "Catalog",
"name": "test2",
"synonym": "Test2",
"guid": "owner-guid",
"source": "saved_state_descriptor",
},
"form": {"name": "t_Форма", "guid": "form-guid", "source": "saved_state_descriptor"},
"module": {"kind": "form_module", "name": "Модуль формы"},
"qualified_name": "test2.t_Форма.Модуль формы",
"payload": {"sha1": "payload-sha1"},
"streams": [
{
"module_ref": "ConfigCASSave:extension-guid__form-guid.0",
"module_path": "2",
"preview": "Процедура Команда()\nКонецПроцедуры",
"match": {"query": "Команда", "in_text": True, "in_file_name": False},
"write_plan_target": {
"module_ref": "ConfigCASSave:extension-guid__form-guid.0",
"file_name": "extension-guid__form-guid.0",
},
}
],
}
)
assert public is not None
assert public["qualified_name"] == "test2.t_Форма.Модуль формы"
assert public["selector"] == {
"ref": "Catalog.test2",
"form": "t_Форма",
"module": "Модуль формы",
"qualified_name": "test2.t_Форма.Модуль формы",
}
assert public["streams"][0]["match"] == {"query": "Команда", "in_text": True, "in_name": False}
assert public["streams"][0]["write_plan_target"] == {
"kind": "module",
"ref": "Catalog.test2",
"form": "t_Форма",
"module": "Модуль формы",
"qualified_name": "test2.t_Форма.Модуль формы",
"stream_ordinal": 1,
}
for storage_marker in (
"ConfigCASSave",
"extension-guid",
"form-guid",
"owner-guid",
"module_ref",
"file_name",
"sha1",
):
assert storage_marker not in str(public)
def test_modules_search_strips_form_embedded_module_tail(monkeypatch: pytest.MonkeyPatch) -> None:
form_text = '{"meta","data","&НаКлиенте\r\nПроцедура ЗаменаДомена(Команда)\r\n\t// Отредактировано адаптером\r\nКонецПроцедуры\r\n\r\n///----\\",\r\n{4,0,0,0,{#base64:AAAA}}"}'
stored = compress_payload(form_text.encode("utf-8-sig"), "raw_deflate")
file_name = "extension-guid__form-guid.0"
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {
"status": "ok",
"files": [{"FileName": file_name, "PartCount": 1, "Bytes": len(stored)}],
},
)
monkeypatch.setattr(
adapter_server,
"read_storage_files_bytes",
lambda base_id, table, file_names, *, timeout_seconds=60: ({file_name: stored}, {"database": "upo_test"}, None),
)
monkeypatch.setattr(adapter_server, "metadata_form_owner_cache_lookup", lambda *args, **kwargs: None)
monkeypatch.setattr(adapter_server, "saved_state_public_module_context", lambda **kwargs: {})
result = adapter_server.search_modules(
{
"base_id": "upo_test",
"table": "ConfigCASSave",
"query": "Отредактировано адаптером",
"limit": 5,
}
)
assert result["status"] == "ok"
assert result["counts"]["matches"] == 1
snippet = result["matches"][0]["snippet"]
assert "Отредактировано адаптером" in snippet["text"]
assert "///----" not in snippet["text"]
def test_saved_state_modules_search_scans_nonzero_base_module_parts(monkeypatch: pytest.MonkeyPatch) -> None:
module_text = "Процедура Команда3(Команда)\r\nКонецПроцедуры\r\n".encode("utf-8-sig")
header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
stored = compress_payload(b"prefix" + header + module_text, "raw_deflate")
file_name = "object-guid.3"
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {
"status": "ok",
"files": [{"FileName": file_name, "PartCount": 1, "Bytes": len(stored)}],
},
)
def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
assert base_id == "upo_test"
assert table == "ConfigSave"
assert requested_file_name == file_name
return stored, {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.metadata_saved_state_modules_search(
{
"base_id": "upo_test",
"tables": ["ConfigSave"],
"query": "Команда3",
"limit": 10,
"include_storage": True,
}
)
assert result["status"] == "ok"
assert result["counts"]["modules"] == 1
assert result["modules"][0]["file_name"] == file_name
assert result["modules"][0]["streams"][0]["module_ref"] == "ConfigSave:object-guid.3#stream:0"
def test_saved_state_catalog_modules_report_owner_roles_and_repair_mojibake(monkeypatch: pytest.MonkeyPatch) -> None:
good_text = "// Это модуль менеджера\r\n&НаКлиенте\r\nПроцедура ОбработкаКоманды(Команда)\r\nКонецПроцедуры\r\n"
mojibake_text = good_text.encode("utf-8").decode("cp1251")
module_text = mojibake_text.encode("utf-8-sig")
header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
stored = compress_payload(b"prefix" + header + module_text, "raw_deflate")
file_name = "extension-guid__catalog-guid.3"
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {
"status": "ok",
"files": [{"FileName": file_name, "PartCount": 1, "Bytes": len(stored)}],
},
)
def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
assert requested_file_name == file_name
return stored, {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
monkeypatch.setattr(
adapter_server,
"saved_state_descriptor_identity",
lambda **kwargs: {
"name": "tt_Справочник1",
"synonym": "Tt справочник1",
"guid": "catalog-guid",
"source": "saved_state_descriptor",
},
)
result = adapter_server.metadata_saved_state_modules_search(
{
"base_id": "upo_test",
"tables": ["ConfigCASSave"],
"object_type": "Catalog",
"object_name": "tt_Справочник1",
"limit": 10,
}
)
assert result["status"] == "ok"
module = result["modules"][0]
assert module["owner"]["kind"] == "Catalog"
assert module["owner"]["name"] == "tt_Справочник1"
assert module["module"]["kind"] == "manager_module"
stream = module["streams"][0]
assert stream["module"]["kind"] == "manager_module"
assert stream["owner"]["name"] == "tt_Справочник1"
assert stream["encoding_repaired"] is True
assert adapter_server.is_bsl_like_text(stream["preview"])
def test_saved_state_command_module_resolves_owner_from_related_descriptor(monkeypatch: pytest.MonkeyPatch) -> None:
module_text = "&НаКлиенте\r\nПроцедура ОбработкаКоманды(Команда)\r\nКонецПроцедуры\r\n".encode("utf-8-sig")
header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
stored_module = compress_payload(b"prefix" + header + module_text, "raw_deflate")
descriptor_text = '{"tt_Справочник1","command-guid"}'
stored_descriptor = compress_payload(descriptor_text.encode("utf-8-sig"), "raw_deflate")
module_file_name = "extension-guid__command-guid.2"
descriptor_file_name = "extension-guid__catalog-guid"
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {
"status": "ok",
"files": [
{"FileName": descriptor_file_name, "PartCount": 1, "Bytes": len(stored_descriptor)},
{"FileName": module_file_name, "PartCount": 1, "Bytes": len(stored_module)},
],
},
)
def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
assert base_id == "upo_test"
assert table == "ConfigCASSave"
if requested_file_name == module_file_name:
return stored_module, {"database": "upo_test"}, None
if requested_file_name == descriptor_file_name:
return stored_descriptor, {"database": "upo_test"}, None
return None, {"database": "upo_test"}, {"status": "not_found"}
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.metadata_saved_state_modules_search(
{
"base_id": "upo_test",
"tables": ["ConfigCASSave"],
"object_type": "Catalog",
"object_name": "tt_Справочник1",
"limit": 10,
}
)
assert result["status"] == "ok"
module = result["modules"][0]
assert module["owner"]["kind"] == "Catalog"
assert module["owner"]["name"] == "tt_Справочник1"
assert module["owner"]["source"] == "saved_state_descriptor"
assert module["module"]["kind"] == "command_module"
stream = module["streams"][0]
assert stream["owner"]["name"] == "tt_Справочник1"
assert stream["module"]["kind"] == "command_module"
def test_saved_state_modules_search_resolves_object_name_to_owner_guid(monkeypatch: pytest.MonkeyPatch) -> None:
module_text = "Процедура ПередЗаписью(Отказ)\r\nКонецПроцедуры\r\n".encode("utf-8-sig")
header = f"\r\n{len(module_text):08x} {len(module_text):08x} 7fffffff \r\n".encode("ascii")
stored = compress_payload(b"prefix" + header + module_text, "raw_deflate")
file_name = "owner-guid__module-guid.0"
seen: dict[str, Any] = {}
monkeypatch.setattr(
adapter_server,
"metadata_cache_lookup_row",
lambda base_id, kind, name: {
"guid": "owner-guid",
"kind": kind,
"kind_ru": "Справочник",
"public_kind": "catalog",
"name": name,
"source": "base",
},
)
def fake_files_list(payload: dict[str, Any]) -> dict[str, Any]:
seen.setdefault("files_payload", payload)
return {"status": "ok", "files": [{"FileName": file_name, "PartCount": 1, "Bytes": len(stored)}]}
def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
return stored, {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "storage_files_list", fake_files_list)
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.call_method(
adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD,
{
"base_id": "upo_test",
"tables": ["ConfigCASSave"],
"object_type": "Catalog",
"object_name": "Номенклатура",
"query": "ПередЗаписью",
"limit": 10,
"include_storage": True,
},
)
assert result["status"] == "ok"
assert seen["files_payload"]["prefix"] == "owner-guid"
assert result["owner_resolution"]["status"] == "resolved"
assert result["owner_resolution"]["owner_guid"] == "owner-guid"
assert result["query"]["object_name"] == "Номенклатура"
assert result["modules"][0]["streams"][0]["module_ref"] == "ConfigCASSave:owner-guid__module-guid.0#stream:0"
assert result["modules"][0]["streams"][0]["write_plan_target"]["module_ref"] == "ConfigCASSave:owner-guid__module-guid.0#stream:0"
def test_saved_state_modules_search_normalizes_public_owner_ref(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_cache_lookup(base_id: str, kind: str, name: str) -> dict[str, Any]:
seen["cache_lookup"] = {"base_id": base_id, "kind": kind, "name": name}
return {
"guid": "owner-guid",
"kind": kind,
"kind_ru": "Справочник",
"public_kind": "catalog",
"name": name,
"source": "base",
}
def fake_files(payload: dict[str, Any]) -> dict[str, Any]:
seen["files_payload"] = payload
return {"status": "ok", "files": []}
monkeypatch.setattr(adapter_server, "metadata_cache_lookup_row", fake_cache_lookup)
monkeypatch.setattr(adapter_server, "storage_files_list", fake_files)
result = adapter_server.call_method(
adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD,
{
"base_id": "upo_test",
"ref": "Справочники.Номенклатура",
"tables": ["ConfigCASSave"],
},
)
assert result["status"] == "ok"
assert seen["cache_lookup"] == {
"base_id": "upo_test",
"kind": "Catalog",
"name": "Номенклатура",
}
assert seen["files_payload"]["prefix"] == "owner-guid"
assert result["query"]["object_type"] == "Catalog"
assert result["query"]["object_name"] == "Номенклатура"
def test_saved_state_modules_search_help_exposes_name_first_owner_selector() -> None:
method = {
row["name"]: row
for row in adapter_server.call_method("help.methods", {})["methods"]
}[adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD]
assert method["selector_capabilities"]["accepts_ordinal"] is False
assert method["selector_capabilities"]["allows_global_search"] is True
assert method["selector_capabilities"]["read_only"] is True
assert method["selector_capabilities"]["module_selector_arguments"] == ["file_name", "stream_index"]
assert method["input_schema"]["properties"]["ref"]["type"] == "string"
assert method["input_schema"]["properties"]["object_name"]["type"] == "string"
def test_saved_state_modules_search_resolves_common_form_name_to_saved_file(monkeypatch: pytest.MonkeyPatch) -> None:
form_text = '{"meta","data","&НаКлиенте\r\nПроцедура Cmd(Команда)\r\nКонецПроцедуры\r\n"}'
stored = compress_payload(form_text.encode("utf-8-sig"), "raw_deflate")
selected_file = "extension-guid__form-guid.0"
other_file = "extension-guid__other-form-guid.0"
seen: dict[str, Any] = {"read_files": []}
monkeypatch.setattr(
adapter_server,
"metadata_cache_lookup_row",
lambda base_id, kind, name: None,
)
monkeypatch.setattr(
adapter_server,
"list_objects",
lambda *args, **kwargs: {"objects": []},
)
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_forms_search",
lambda payload: {
"status": "ok",
"forms": [
{"file_name": selected_file, "name": "TestForm"},
{"file_name": other_file, "name": "TestFormCopy"},
],
"counts": {"forms": 2},
},
)
monkeypatch.setattr(
adapter_server,
"storage_files_list",
lambda payload: {
"status": "ok",
"files": [
{"FileName": selected_file, "PartCount": 1, "Bytes": len(stored)},
{"FileName": other_file, "PartCount": 1, "Bytes": len(stored)},
],
},
)
def fake_read_storage_file_bytes(base_id: str, table: str, requested_file_name: str, *, timeout_seconds: int = 30):
seen["read_files"].append(requested_file_name)
assert requested_file_name == selected_file
return stored, {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.metadata_saved_state_modules_search(
{
"base_id": "upo_test",
"tables": ["ConfigCASSave"],
"object_type": "CommonForm",
"object_name": "TestForm",
"query": "Cmd",
"limit": 10,
"include_storage": True,
}
)
assert result["status"] == "ok"
assert result["owner_resolution"]["status"] == "resolved"
assert result["owner_resolution"]["method"] == "metadata.saved_state.forms.search"
assert result["owner_resolution"]["file_names"] == [selected_file]
assert seen["read_files"] == [selected_file]
assert result["counts"]["modules"] == 1
assert result["modules"][0]["file_name"] == selected_file
assert result["modules"][0]["payload"]["role"] == "form_embedded_module_payload"
def test_saved_state_prepare_plans_active_to_save_copy(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_file_names(payload: dict[str, Any], base_id: str, source_table: str, timeout_seconds: int):
seen["source_table"] = source_table
return ["owner__module.0"], {"guid": "owner", "kind": "CommonForm", "name": "t_Форма"}, None
def fake_row_details(base_id: str, table: str, file_names: list[str], *, timeout_seconds: int = 30):
if table == "ConfigCAS":
return [
{"FileName": "owner__module.0", "PartNo": 0, "DataSize": 10, "BinaryBytes": 10, "BinarySHA1": "A" * 40}
], {"database": "upo_test"}, None
if table == "ConfigCASSave":
return [], {"database": "upo_test"}, None
raise AssertionError(table)
monkeypatch.setattr(adapter_server, "saved_state_prepare_file_names", fake_file_names)
monkeypatch.setattr(adapter_server, "saved_state_copy_row_details", fake_row_details)
result = adapter_server.metadata_saved_state_prepare(
{
"base_id": "upo_test",
"target_table": "ConfigCASSave",
"object_type": "CommonForm",
"object_name": "t_Форма",
"include_storage": True,
}
)
assert seen["source_table"] == "ConfigCAS"
assert result["status"] == "plan_ready"
assert result["ready_to_copy"] is True
assert result["source"]["table"] == "ConfigCAS"
assert result["target"]["table"] == "ConfigCASSave"
assert result["counts"] == {"file_names": 1, "source_rows": 1, "target_rows": 0}
assert result["write_mode"]["sql_write_performed"] is False
def test_saved_state_prepare_default_is_name_first_and_hides_storage(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, str] = {}
def fake_file_names(payload: dict[str, Any], base_id: str, source_table: str, timeout_seconds: int):
seen["source_table"] = source_table
seen["kind"] = str(payload.get("kind") or "")
seen["name"] = str(payload.get("name") or "")
return ["owner__module.0"], {"guid": "owner", "kind": "CommonForm", "name": "t_Форма"}, None
def fake_row_details(base_id: str, table: str, file_names: list[str], *, timeout_seconds: int = 30):
seen.setdefault("target_table", table)
rows = (
[{"FileName": "owner__module.0", "PartNo": 0, "DataSize": 10, "BinaryBytes": 10, "BinarySHA1": "A" * 40}]
if table == "ConfigCAS"
else []
)
return rows, {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "saved_state_prepare_file_names", fake_file_names)
monkeypatch.setattr(adapter_server, "saved_state_copy_row_details", fake_row_details)
result = adapter_server.metadata_saved_state_prepare(
{
"base_id": "upo_test",
"layer": "extension_saved_state",
"ref": "CommonForm.t_Форма",
}
)
assert seen == {
"source_table": "ConfigCAS",
"kind": "CommonForm",
"name": "t_Форма",
"target_table": "ConfigCAS",
}
assert result["status"] == "plan_ready"
assert result["source"] == {
"kind": "active_configuration",
"layer": "extension_configuration",
}
assert result["target"] == {"layer": "extension_saved_state"}
assert result["object"] == {
"kind": "CommonForm",
"name": "t_Форма",
"ref": "CommonForm.t_Форма",
}
assert result["counts"] == {
"selected_objects": 1,
"source_records": 1,
"existing_saved_records": 0,
"inserted_records": 0,
}
serialized = json.dumps(result, ensure_ascii=False)
for storage_marker in ("ConfigCASSave", "ConfigCAS", "owner__module.0", "BinarySHA1", "database", "file_names", "source_rows"):
assert storage_marker not in serialized
def test_saved_state_prepare_rejects_conflicting_semantic_and_storage_layers() -> None:
result = adapter_server.metadata_saved_state_prepare(
{
"base_id": "upo_test",
"layer": "base_saved_state",
"target_table": "ConfigCASSave",
"ref": "Catalog.Номенклатура",
}
)
assert result["status"] == "invalid_argument"
assert result["argument"] == "layer"
def test_saved_state_prepare_resolves_extension_route_cache(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(adapter_server, "sql_config_for_base", lambda base_id: ({"database": base_id, "server": "s"}, None))
monkeypatch.setattr(
adapter_server,
"extension_route_cache_lookup",
lambda config, query, kind_filter, guid_filter, extension_guid, limit: [
{
"extension_name": "test2",
"extension_guid": "ext-guid",
"object_kind": "CommonForm",
"name": "t_Форма",
"guid": "form-guid",
"descriptor_cas_key": "descriptor",
"route_json": json.dumps({"table": "ConfigCAS", "file_name": "descriptor"}, ensure_ascii=False),
"manifest_entries_json": json.dumps(
[
{"suffix": "", "cas_key": "descriptor"},
{"suffix": ".0", "cas_key": "payload"},
],
ensure_ascii=False,
),
},
{
"extension_name": "test2",
"extension_guid": "ext-guid",
"object_kind": "CommonForm",
"name": "tt_Форма3",
"guid": "other-form-guid",
"descriptor_cas_key": "other-descriptor",
"route_json": json.dumps({"table": "ConfigCAS", "file_name": "other-descriptor"}, ensure_ascii=False),
"manifest_entries_json": json.dumps(
[{"suffix": "", "cas_key": "other-descriptor"}],
ensure_ascii=False,
),
},
],
)
monkeypatch.setattr(
adapter_server,
"saved_state_copy_row_details",
lambda base_id, table, file_names, timeout_seconds=30: (
[{"FileName": name, "PartNo": 0, "DataSize": 10, "BinaryBytes": 10, "BinarySHA1": "A" * 40} for name in file_names] if table == "ConfigCAS" else [],
{"database": "upo_test"},
None,
),
)
result = adapter_server.metadata_saved_state_prepare(
{
"base_id": "upo_test",
"target_table": "ConfigCASSave",
"extension": "test2",
"object_type": "CommonForm",
"object_name": "t_Форма",
"include_storage": True,
}
)
assert result["status"] == "plan_ready"
assert result["file_names"] == ["descriptor", "payload"]
assert result["object"]["mode"] == "extension_route_cache"
assert result["counts"]["source_rows"] == 2
def test_saved_state_diff_compares_active_and_saved_payload(monkeypatch: pytest.MonkeyPatch) -> None:
reads: list[tuple[str, str]] = []
def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30):
reads.append((table, file_name))
if table == "ConfigCAS":
return b'{1,"active"}', {"database": "upo_test"}, None
if table == "ConfigCASSave":
return b'{1,"saved"}', {"database": "upo_test"}, None
raise AssertionError(table)
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.metadata_saved_state_diff(
{
"base_id": "upo_test",
"module_ref": "ConfigCASSave:form-module.0#stream:0",
"max_text_diff_lines": 20,
}
)
assert reads == [("ConfigCASSave", "form-module.0"), ("ConfigCAS", "form-module.0")]
assert result["schema"] == "onec_saved_state_diff.v1"
assert result["status"] == "changed"
assert result["needs_prepare"] is False
assert result["source"]["active"] == {"table": "ConfigCAS", "file_name": "form-module.0"}
assert result["source"]["saved"] == {"table": "ConfigCASSave", "file_name": "form-module.0"}
assert result["current_state"] == {"source": "saved_state", "activation_state": "not_activated"}
assert result["comparison"]["differs"] is True
assert result["freshness"]["status"] == "live_sql_verified"
def test_saved_state_diff_reports_prepare_needed_when_save_missing(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30):
if table == "ConfigSave":
return None, {"database": "upo_test"}, {
"schema": "onec_adapter_source_missing.v1",
"method": "storage.file.get",
"status": "source_missing",
"base_id": base_id,
"source": {"table": table, "file_name": file_name},
"diagnostics": {"message": "missing"},
}
return b'{1,"active"}', {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.metadata_saved_state_diff(
{"base_id": "upo_test", "table": "ConfigSave", "file_name": "object.0"}
)
assert result["status"] == "not_found"
assert result["error"] == "saved_state_not_found"
assert result["needs_prepare"] is True
assert result["current_state"] == {"source": "active", "activation_state": "active"}
assert result["prepare_payload"]["method"] == "metadata.saved_state.prepare"
assert result["prepare_payload"]["target_table"] == "ConfigSave"
def test_saved_state_diff_resolves_named_1c_object_module_without_exposing_storage(monkeypatch: pytest.MonkeyPatch) -> None:
module_reads: list[dict[str, Any]] = []
storage_reads: list[tuple[str, str]] = []
def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
module_reads.append(payload)
assert payload["kind"] == "Catalog"
assert payload["name"] == "Номенклатура"
return {
"schema": "onec_module_read.v1",
"status": "ok",
"module_id": f"{payload['table']}:catalog-guid.2",
"source": {"kind": "live_sql", "table": payload["table"], "file_name": "catalog-guid.2"},
"owner": {
"kind": "Catalog",
"name": "Номенклатура",
"synonym": "Номенклатура",
"guid": "catalog-guid",
},
"module": {"kind": "object_module", "name": "Модуль объекта", "module_ordinal": 2},
}
def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30):
storage_reads.append((table, file_name))
value = b'{1,"saved"}' if table == "ConfigSave" else b'{1,"active"}'
return value, {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "read_module", fake_read_module)
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.metadata_saved_state_diff(
{
"base_id": "upo_test",
"object_type": "Catalog",
"object_name": "Номенклатура",
"module_ordinal": 2,
}
)
assert [item["table"] for item in module_reads] == ["ConfigSave"]
assert storage_reads == [("ConfigSave", "catalog-guid.2"), ("Config", "catalog-guid.2")]
assert result["status"] == "changed"
assert result["resolved_by"] == "public_object_module_selector"
assert result["target"]["selector"]["ref"] == "Catalog.Номенклатура"
assert result["target"]["selector"]["module_ordinal"] == 2
assert result["target"]["object"]["name"] == "Номенклатура"
assert "storage" not in result["target"]
assert result["source"] == {
"kind": "live_metadata",
"active": {"state": "active"},
"saved": {"state": "saved"},
}
def test_saved_state_diff_named_selector_returns_name_first_prepare_payload(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
if payload["table"] == "ConfigSave":
return {"status": "source_missing", "error": "source_missing"}
return {
"status": "ok",
"module_id": "Config:catalog-guid.1",
"source": {"kind": "live_sql", "table": "Config", "file_name": "catalog-guid.1"},
"owner": {"kind": "Catalog", "name": "Номенклатура", "guid": "catalog-guid"},
"module": {"kind": "manager_module", "name": "Модуль менеджера", "module_ordinal": 1},
}
def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30):
assert table == "ConfigSave"
return None, {"database": "upo_test"}, {
"status": "source_missing",
"error": "source_missing",
"diagnostics": {"message": "missing"},
}
monkeypatch.setattr(adapter_server, "read_module", fake_read_module)
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.metadata_saved_state_diff(
{
"base_id": "upo_test",
"ref": "Catalog.Номенклатура",
"module_ordinal": 1,
}
)
assert result["status"] == "not_found"
assert result["needs_prepare"] is True
assert result["prepare_payload"] == {
"method": "metadata.saved_state.prepare",
"base_id": "upo_test",
"layer": "base_saved_state",
"ref": "Catalog.Номенклатура",
"kind": "Catalog",
"name": "Номенклатура",
"module_ordinal": 1,
"mode": "plan",
}
assert "file_name" not in result["prepare_payload"]
def test_saved_state_diff_named_selector_refreshes_stale_empty_module_cache(monkeypatch: pytest.MonkeyPatch) -> None:
module_reads: list[tuple[str, bool]] = []
def fake_read_module(payload: dict[str, Any]) -> dict[str, Any]:
table = str(payload["table"])
refresh_cache = bool(payload["refresh_cache"])
module_reads.append((table, refresh_cache))
if table == "ConfigSave" or not refresh_cache:
return {"status": "not_found", "error": "not_found"}
return {
"status": "ok",
"module_id": "Config:common-module-guid.0#stream:4",
"source": {"kind": "live_sql", "table": "Config", "file_name": "common-module-guid.0"},
"owner": {"kind": "CommonModule", "name": "ОбщегоНазначения", "guid": "common-module-guid"},
"module": {
"kind": "common_module",
"name": "common-module-guid.0#stream:4",
"module_ordinal": 1,
},
}
def fake_read_storage_file_bytes(base_id: str, table: str, file_name: str, *, timeout_seconds: int = 30):
assert table == "ConfigSave"
return None, {"database": "upo_test"}, {
"status": "source_missing",
"error": "source_missing",
}
monkeypatch.setattr(adapter_server, "read_module", fake_read_module)
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", fake_read_storage_file_bytes)
result = adapter_server.metadata_saved_state_diff(
{
"base_id": "upo_test",
"ref": "CommonModule.ОбщегоНазначения",
"module_ordinal": 1,
}
)
assert module_reads == [
("ConfigSave", False),
("ConfigSave", True),
("Config", False),
("Config", True),
]
assert result["status"] == "not_found"
assert result["needs_prepare"] is True
assert result["target"]["module"] == {
"kind": "common_module",
"name": "ОбщегоНазначения",
"module_ordinal": 1,
}
def test_saved_state_status_classifies_pending_files(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_status_rows(base_id: str, table: str, *, prefix: str = "", limit: int = 500, timeout_seconds: int = 30):
assert table == "ConfigCASSave"
return [
{"FileName": "changed.0", "PartNo": 0, "DataSize": 1, "BinaryBytes": 1, "BinarySHA1": "b" * 40},
{"FileName": "same.0", "PartNo": 0, "DataSize": 1, "BinaryBytes": 1, "BinarySHA1": "c" * 40},
{"FileName": "only.0", "PartNo": 0, "DataSize": 1, "BinaryBytes": 1, "BinarySHA1": "d" * 40},
], {"database": "upo_test"}, None
def fake_row_details(base_id: str, table: str, file_names: list[str], *, timeout_seconds: int = 30):
assert table == "ConfigCAS"
return [
{"FileName": "changed.0", "PartNo": 0, "DataSize": 1, "BinaryBytes": 1, "BinarySHA1": "a" * 40},
{"FileName": "same.0", "PartNo": 0, "DataSize": 1, "BinaryBytes": 1, "BinarySHA1": "c" * 40},
], {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "saved_state_status_rows", fake_status_rows)
monkeypatch.setattr(adapter_server, "saved_state_copy_row_details", fake_row_details)
result = adapter_server.metadata_saved_state_status({"base_id": "upo_test", "table": "ConfigCASSave", "include_storage": True})
assert result["schema"] == "onec_saved_state_status.v1"
assert result["status"] == "changed"
assert result["freshness"]["status"] == "live_sql_verified"
assert result["counts"]["saved_files"] == 3
assert result["counts"]["changed_files"] == 1
assert result["counts"]["unchanged_files"] == 1
assert result["counts"]["saved_only_files"] == 1
statuses = {item["file_name"]: item["status"] for item in result["files"]}
assert statuses == {"changed.0": "changed", "only.0": "saved_only", "same.0": "unchanged"}
assert result["files"][0]["diff_selector"]["method"] == "metadata.saved_state.diff"
def test_saved_state_status_reports_empty(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(adapter_server, "saved_state_status_rows", lambda *args, **kwargs: ([], {"database": "upo_test"}, None))
monkeypatch.setattr(adapter_server, "saved_state_copy_row_details", lambda *args, **kwargs: ([], {"database": "upo_test"}, None))
result = adapter_server.metadata_saved_state_status({"base_id": "upo_test", "table": "ConfigSave", "include_storage": True})
assert result["status"] == "empty"
assert result["current_state"] == {"source": "active", "activation_state": "active"}
assert result["counts"]["saved_rows"] == 0
assert result["files"] == []
def test_saved_state_status_default_is_semantic_and_hides_storage(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, str] = {}
def fake_status_rows(base_id: str, table: str, *, prefix: str = "", limit: int = 500, timeout_seconds: int = 30):
seen["saved_table"] = table
return [
{"FileName": "internal-guid.0", "PartNo": 0, "DataSize": 1, "BinaryBytes": 1, "BinarySHA1": "b" * 40},
], {"database": "upo_test"}, None
def fake_row_details(base_id: str, table: str, file_names: list[str], *, timeout_seconds: int = 30):
seen["active_table"] = table
return [
{"FileName": "internal-guid.0", "PartNo": 0, "DataSize": 1, "BinaryBytes": 1, "BinarySHA1": "a" * 40},
], {"database": "upo_test"}, None
monkeypatch.setattr(adapter_server, "saved_state_status_rows", fake_status_rows)
monkeypatch.setattr(adapter_server, "saved_state_copy_row_details", fake_row_details)
result = adapter_server.metadata_saved_state_status(
{"base_id": "upo_test", "layer": "extension_saved_state"}
)
assert seen == {"saved_table": "ConfigCASSave", "active_table": "ConfigCAS"}
assert result["source"] == {"kind": "saved_state"}
assert result["query"]["layer"] == "extension_saved_state"
assert result["layer"] == {
"name": "extension_saved_state",
"active_source": "extension_configuration",
}
assert result["counts"] == {
"saved_records": 1,
"saved_objects": 1,
"active_records": 1,
"changed_objects": 1,
"unchanged_objects": 0,
"saved_only_objects": 0,
"changed_parts": 2,
}
serialized = json.dumps(result, ensure_ascii=False)
assert "ConfigCASSave" not in serialized
assert "ConfigCAS" not in serialized
assert "internal-guid.0" not in serialized
assert "BinarySHA1" not in serialized
assert "database" not in serialized
assert "files" not in result
def test_saved_state_status_rejects_conflicting_semantic_and_storage_layers() -> None:
result = adapter_server.metadata_saved_state_status(
{
"base_id": "upo_test",
"layer": "base_saved_state",
"table": "ConfigCASSave",
}
)
assert result["status"] == "invalid_argument"
assert result["argument"] == "layer"
def test_saved_state_changes_list_aggregates_pending_files(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_status(payload: dict[str, Any]) -> dict[str, Any]:
table = payload["table"]
if table == "ConfigSave":
return {
"schema": "onec_saved_state_status.v1",
"method": "metadata.saved_state.status",
"status": "unchanged",
"counts": {"changed_files": 0, "saved_only_files": 0, "unchanged_files": 1},
"files": [],
"freshness": {"status": "live_sql_verified"},
}
return {
"schema": "onec_saved_state_status.v1",
"method": "metadata.saved_state.status",
"status": "changed",
"counts": {"changed_files": 1, "saved_only_files": 1, "unchanged_files": 0},
"files": [
{
"file_name": "changed.0",
"status": "changed",
"changed_parts": 2,
"diff_selector": {"method": "metadata.saved_state.diff", "base_id": payload["base_id"], "table": table, "file_name": "changed.0"},
},
{
"file_name": "only.0",
"status": "saved_only",
"changed_parts": 1,
"diff_selector": {"method": "metadata.saved_state.diff", "base_id": payload["base_id"], "table": table, "file_name": "only.0"},
},
],
"freshness": {"status": "live_sql_verified"},
}
monkeypatch.setattr(adapter_server, "metadata_saved_state_status", fake_status)
result = adapter_server.metadata_saved_state_changes_list({"base_id": "upo_test", "include_storage": True})
assert result["schema"] == "onec_saved_state_changes_list.v1"
assert result["status"] == "changed"
assert result["freshness"]["status"] == "live_sql_verified"
assert result["counts"]["tables"] == 2
assert result["counts"]["changed_files"] == 1
assert result["counts"]["saved_only_files"] == 1
assert [(item["table"], item["file_name"], item["status"]) for item in result["files"]] == [
("ConfigCASSave", "changed.0", "changed"),
("ConfigCASSave", "only.0", "saved_only"),
]
assert result["files"][0]["diff_selector"]["method"] == "metadata.saved_state.diff"
def test_saved_state_changes_list_can_include_context(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_status",
lambda payload: {
"schema": "onec_saved_state_status.v1",
"method": "metadata.saved_state.status",
"status": "changed",
"counts": {"changed_files": 1, "saved_only_files": 0, "unchanged_files": 0},
"files": [
{
"file_name": "changed.0",
"status": "changed",
"changed_parts": 2,
"diff_selector": {"method": "metadata.saved_state.diff", "base_id": payload["base_id"], "table": payload["table"], "file_name": "changed.0"},
},
],
"freshness": {"status": "live_sql_verified"},
},
)
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_change_context",
lambda **kwargs: {
"kind": "module",
"presentation": "ОбщаяФорма.t_Форма.Модуль формы",
"module": {"kind": "form_module", "name": "Модуль формы"},
"source": {"method": "metadata.saved_state.modules.search", "status": "resolved_by_file"},
},
)
result = adapter_server.metadata_saved_state_changes_list({"base_id": "upo_test", "table": "ConfigCASSave", "include_context": True})
assert result["query"]["include_context"] is True
assert result["changes"][0]["context"]["kind"] == "module"
assert result["changes"][0]["context"]["presentation"] == "ОбщаяФорма.t_Форма.Модуль формы"
assert "files" not in result
assert "tables" not in result
def test_saved_state_changes_list_accepts_semantic_layer(monkeypatch: pytest.MonkeyPatch) -> None:
seen: list[dict[str, Any]] = []
def fake_status(payload: dict[str, Any]) -> dict[str, Any]:
seen.append(payload)
return {
"schema": "onec_saved_state_status.v1",
"method": "metadata.saved_state.status",
"status": "empty",
"counts": {
"saved_rows": 0,
"saved_files": 0,
"active_rows": 0,
"changed_files": 0,
"unchanged_files": 0,
"saved_only_files": 0,
"changed_rows": 0,
},
"files": [],
"freshness": {"status": "live_sql_verified"},
}
monkeypatch.setattr(adapter_server, "metadata_saved_state_status", fake_status)
result = adapter_server.metadata_saved_state_changes_list(
{"base_id": "upo_test", "layer": "extension_saved_state"}
)
assert result["query"]["layers"] == ["extension_saved_state"]
assert len(seen) == 1
assert seen[0]["table"] == "ConfigCASSave"
assert seen[0]["include_storage"] is True
def test_saved_state_changes_list_rejects_conflicting_semantic_and_storage_layers() -> None:
result = adapter_server.metadata_saved_state_changes_list(
{
"base_id": "upo_test",
"layer": "base_saved_state",
"table": "ConfigCASSave",
}
)
assert result["status"] == "invalid_argument"
assert result["argument"] == "layer"
def test_saved_state_changes_list_default_is_name_first(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_status",
lambda payload: {
"schema": "onec_saved_state_status.v1",
"method": "metadata.saved_state.status",
"status": "changed",
"counts": {"changed_files": 1, "saved_only_files": 0, "unchanged_files": 0},
"files": [
{
"file_name": "extension-guid__form-guid.0",
"status": "changed",
"changed_parts": 2,
"saved_sha1": ["saved-sha1"],
"active_sha1": ["active-sha1"],
"diff_selector": {
"method": "metadata.saved_state.diff",
"base_id": payload["base_id"],
"table": payload["table"],
"file_name": "extension-guid__form-guid.0",
},
}
],
"freshness": {"status": "live_sql_verified"},
},
)
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_change_context",
lambda **kwargs: {
"kind": "module",
"presentation": "test2.t_Форма.Модуль формы",
"owner": {
"status": "resolved",
"kind": "Catalog",
"name": "test2",
"guid": "owner-guid",
"source": "saved_state_descriptor",
},
"form": {"name": "t_Форма", "guid": "form-guid"},
"module": {"kind": "form_module", "name": "Модуль формы"},
"streams": [
{
"module_ref": "ConfigCASSave:extension-guid__form-guid.0",
"write_plan_target": {"file_name": "extension-guid__form-guid.0"},
}
],
"source": {"method": "metadata.saved_state.modules.search", "status": "resolved_by_file"},
},
)
result = adapter_server.metadata_saved_state_changes_list(
{"base_id": "upo_test", "table": "ConfigCASSave"}
)
assert result["source"] == {"kind": "saved_state"}
assert result["query"]["layers"] == ["extension_saved_state"]
assert result["query"]["context_enrichment"] is True
assert result["counts"] == {
"layers": 1,
"changes": 1,
"changed": 1,
"saved_only": 0,
"unchanged": 0,
"error_layers": 0,
}
assert result["changes"][0]["presentation"] == "test2.t_Форма.Модуль формы"
assert result["changes"][0]["selector"] == {
"ref": "Catalog.test2",
"form": "t_Форма",
"module": "Модуль формы",
"qualified_name": "test2.t_Форма.Модуль формы",
}
assert result["diagnostics"]["resolved_contexts"] == 1
assert result["freshness"] == {
"source": "saved_state",
"status": "live_verified",
"verified": True,
}
assert result["layers"][0]["freshness"] == {
"source": "saved_state",
"status": "live_verified",
"verified": True,
}
assert result["layers"][0]["counts"] == {
"saved_records": 0,
"saved_objects": 0,
"active_records": 0,
"changed": 1,
"unchanged": 0,
"saved_only": 0,
"changed_parts": 0,
}
assert "tables" not in result["counts"]
assert "files" not in result["counts"]
for storage_marker in (
"live_sql",
"ConfigCASSave",
"extension-guid",
"form-guid",
"owner-guid",
"file_name",
"module_ref",
"write_plan_target",
"saved_sha1",
"active_sha1",
"diff_selector",
):
assert storage_marker not in str(result)
def test_saved_state_change_context_does_not_claim_generic_role_as_resolved(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(adapter_server, "saved_state_form_search_row", lambda **kwargs: {"form": {}})
monkeypatch.setattr(
adapter_server,
"saved_state_module_search_row",
lambda **kwargs: {
"module": {"kind": "form_module", "name": "Модуль формы"},
"qualified_name": "Модуль формы",
"streams": [],
},
)
context = adapter_server.metadata_saved_state_change_context(
base_id="upo_test",
table="ConfigCASSave",
file_name="opaque.0",
timeout_seconds=30,
)
assert context is None
def test_saved_state_change_context_family_adds_form_name_to_module() -> None:
files = [
{
"table": "ConfigCASSave",
"file_name": "extension-guid__form-guid",
"context": {
"kind": "module",
"presentation": "test2.Модуль формы",
"owner": {"kind": "Catalog", "name": "test2"},
"module": {"kind": "form_module", "name": "Модуль формы"},
},
},
{
"table": "ConfigCASSave",
"file_name": "extension-guid__form-guid.0",
"context": {
"kind": "form",
"presentation": "test2.t_Форма.Форма",
"owner": {"kind": "Catalog", "name": "test2"},
"form": {"name": "t_Форма"},
},
},
]
adapter_server.enrich_saved_state_change_context_families(files)
assert files[0]["context"]["form"]["name"] == "t_Форма"
assert files[0]["context"]["presentation"] == "test2.t_Форма.Модуль формы"
def test_public_saved_state_unresolved_group_hides_file_identity() -> None:
public = adapter_server.public_saved_state_change_group(
{
"key": "file|ConfigCASSave|opaque-hash",
"kind": "file",
"presentation": "opaque-hash",
"counts": {"files": 1, "changed_files": 1, "saved_only_files": 0, "unchanged_files": 0},
"files": [
{
"table": "ConfigCASSave",
"file_name": "opaque-hash",
"status": "changed",
"changed_parts": 2,
}
],
},
3,
)
assert public["key"] == "unresolved_change:3"
assert public["kind"] == "unresolved"
assert public["counts"] == {"changes": 1, "changed": 1, "saved_only": 0, "unchanged": 0}
assert "presentation" not in public
assert "selector" not in public
assert "opaque-hash" not in str(public)
assert "ConfigCASSave" not in str(public)
def test_saved_state_changes_list_can_group_by_context(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_status",
lambda payload: {
"schema": "onec_saved_state_status.v1",
"method": "metadata.saved_state.status",
"status": "changed",
"counts": {"changed_files": 1, "saved_only_files": 1, "unchanged_files": 0},
"files": [
{
"file_name": "changed.0",
"status": "changed",
"changed_parts": 2,
"diff_selector": {"method": "metadata.saved_state.diff", "base_id": payload["base_id"], "table": payload["table"], "file_name": "changed.0"},
},
{
"file_name": "only.0",
"status": "saved_only",
"changed_parts": 1,
"diff_selector": {"method": "metadata.saved_state.diff", "base_id": payload["base_id"], "table": payload["table"], "file_name": "only.0"},
},
],
"freshness": {"status": "live_sql_verified"},
},
)
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_change_context",
lambda **kwargs: {
"kind": "module",
"presentation": "ОбщаяФорма.t_Форма.Модуль формы",
"form": {"name": "t_Форма", "guid": "form-guid"},
"module": {"kind": "form_module", "name": "Модуль формы"},
"streams": [
{
"stream_index": 0,
"module_ref": "ConfigCASSave:changed.0#stream:0",
"write_plan_target": {"table": "ConfigCASSave", "file_name": "changed.0", "module_ref": "ConfigCASSave:changed.0#stream:0"},
}
],
"source": {"method": "metadata.saved_state.modules.search", "status": "resolved_by_file"},
},
)
result = adapter_server.metadata_saved_state_changes_list(
{"base_id": "upo_test", "table": "ConfigCASSave", "group_by_context": True, "include_storage": True}
)
assert result["query"]["group_by_context"] is True
assert result["query"]["context_enrichment"] is True
assert result["counts"]["groups"] == 1
assert len(result["groups"]) == 1
assert result["groups"][0]["presentation"] == "ОбщаяФорма.t_Форма.Модуль формы"
assert result["groups"][0]["counts"] == {"files": 2, "changed_files": 1, "saved_only_files": 1, "unchanged_files": 0}
assert [item["file_name"] for item in result["groups"][0]["files"]] == ["changed.0", "only.0"]
assert len(result["groups"][0]["selectors"]["diff"]) == 2
assert result["groups"][0]["selectors"]["module_refs"] == ["ConfigCASSave:changed.0#stream:0"]
assert result["groups"][0]["selectors"]["write_plan_targets"][0]["file_name"] == "changed.0"
assert [action["kind"] for action in result["groups"][0]["next_actions"]] == [
"inspect_diff",
"inspect_diff",
"read_module",
"preflight_write",
]
assert result["groups"][0]["next_actions"][2]["payload"] == {"base_id": "upo_test", "module_ref": "ConfigCASSave:changed.0#stream:0", "state": "working"}
assert result["groups"][0]["next_actions"][3]["payload"]["target"]["file_name"] == "changed.0"
assert result["groups"][0]["action_summary"]["total"] == 4
assert result["groups"][0]["action_summary"]["by_kind"] == {"inspect_diff": 2, "read_module": 1, "preflight_write": 1}
assert result["groups"][0]["recommended_next_action"]["kind"] == "inspect_diff"
assert result["groups"][0]["recommended_next_action"]["method"] == "metadata.saved_state.diff"
assert result["action_summary"]["total"] == 4
assert result["action_summary"]["by_kind"] == {"inspect_diff": 2, "read_module": 1, "preflight_write": 1}
assert result["recommended_next_action"]["group_presentation"] == "ОбщаяФорма.t_Форма.Модуль формы"
assert result["recommended_next_action"]["action"]["kind"] == "inspect_diff"
def test_saved_state_prepare_apply_requires_allow_flag(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"saved_state_prepare_file_names",
lambda payload, base_id, source_table, timeout_seconds: (["owner__module.0"], {"guid": "owner"}, None),
)
monkeypatch.setattr(
adapter_server,
"saved_state_copy_row_details",
lambda base_id, table, file_names, timeout_seconds=30: (
([{"FileName": "owner__module.0", "PartNo": 0, "DataSize": 10, "BinaryBytes": 10, "BinarySHA1": "A" * 40}] if table == "ConfigCAS" else []),
{"database": "upo_test"},
None,
),
)
result = adapter_server.metadata_saved_state_prepare(
{"base_id": "upo_test", "target_table": "ConfigCASSave", "mode": "apply", "file_name": "owner__module.0"}
)
assert result["status"] == "invalid_argument"
assert result["argument"] == "allow_sql_saved_state_prepare"
def test_saved_state_prepare_blocks_target_collision(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"saved_state_prepare_file_names",
lambda payload, base_id, source_table, timeout_seconds: (["owner__module.0"], {"guid": "owner"}, None),
)
monkeypatch.setattr(
adapter_server,
"saved_state_copy_row_details",
lambda base_id, table, file_names, timeout_seconds=30: (
[{"FileName": "owner__module.0", "PartNo": 0, "DataSize": 10, "BinaryBytes": 10, "BinarySHA1": "A" * 40}],
{"database": "upo_test"},
None,
),
)
result = adapter_server.metadata_saved_state_prepare(
{
"base_id": "upo_test",
"target_table": "ConfigCASSave",
"file_name": "owner__module.0",
"include_storage": True,
}
)
assert result["status"] == "blocked_target_collision"
assert result["ready_to_copy"] is False
assert result["counts"]["target_rows"] == 1
def test_metadata_write_preserves_apply_and_verify_mode(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {"schema": "onec_form_element_write_apply.v1", "status": "verified", "applied": True}
monkeypatch.setattr(adapter_server, "metadata_form_element_write_apply", fake_apply)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"kind": "form", "table": "ConfigCASSave", "file_name": "form-guid.0", "element": "КомандаПример1"},
"mode": "apply_and_verify",
"allow_sql_saved_state_apply": True,
"edits": [{"property": "Заголовок", "value": "ПРОВЕРКА"}],
}
)
assert seen["payload"]["execution_mode"] == "apply_and_verify"
assert result["execution_mode"] == "apply_and_verify"
assert result["status"] == "verified"
def test_metadata_write_routes_module_target(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_module_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {"schema": "onec_module_write_apply.v1", "status": "verified_and_rolled_back", "applied": True, "rolled_back": True}
monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_module_apply)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"kind": "module", "module_ref": "ConfigCASSave:object__module.0#stream:4"},
"mode": "apply_and_rollback",
"allow_sql_saved_state_apply": True,
"allow_sql_saved_state_rollback": True,
"old": "a",
"new": "b",
}
)
assert seen["payload"]["execution_mode"] == "apply_and_rollback"
assert seen["payload"]["allow_saved_state_write"] is True
assert seen["payload"]["module_ref"] == "ConfigCASSave:object__module.0#stream:4"
assert result["target_kind"] == "module"
assert result["routed_method"] == adapter_server.MODULE_WRITE_APPLY_METHOD
assert result["status"] == "verified_and_rolled_back"
def test_metadata_write_resolves_single_module_target(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
seen["search"] = payload
return {
"status": "ok",
"counts": {"modules": 1, "scanned": 1},
"modules": [
{
"payload": {"sha1": "old-sha"},
"streams": [{"module_ref": "ConfigCASSave:owner__module.0#stream:4"}],
}
],
}
def fake_module_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["apply"] = payload
return {"schema": "onec_module_write_apply.v1", "status": "planned"}
monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)
monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_module_apply)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"kind": "module", "owner_guid": "owner", "query": "Команда1"},
"old": "a",
"new": "b",
}
)
assert seen["search"]["owner_guid"] == "owner"
assert seen["search"]["query"] == "Команда1"
assert seen["apply"]["module_ref"] == "ConfigCASSave:owner__module.0#stream:4"
assert seen["apply"]["expected_sha1"] == "old-sha"
assert result["resolution"]["method"] == adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD
assert result["status"] == "planned"
def test_metadata_write_reports_ambiguous_module_resolution(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_modules_search",
lambda payload: {
"status": "ok",
"counts": {"modules": 2, "scanned": 2},
"modules": [
{"streams": [{"module_ref": "ConfigCASSave:a__m.0#stream:1"}]},
{"streams": [{"module_ref": "ConfigCASSave:b__m.0#stream:1"}]},
],
},
)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"kind": "module", "query": "Команда"},
"old": "a",
"new": "b",
}
)
assert result["status"] == "ambiguous"
assert result["error"] == "module_target_not_resolved"
assert result["counts"]["stream_matches"] == 2
def test_metadata_write_active_module_ref_requires_saved_state_prepare(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_prepare(payload: dict[str, Any]) -> dict[str, Any]:
seen["prepare"] = payload
return {"schema": "onec_saved_state_prepare.v1", "status": "plan_ready", "ready_to_copy": True}
monkeypatch.setattr(adapter_server, "metadata_saved_state_prepare", fake_prepare)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"kind": "module", "module_ref": "ConfigCAS:owner__module.0#stream:4"},
"old": "a",
"new": "b",
}
)
assert result["status"] == "blocked"
assert result["error"] == "saved_state_prepare_required"
assert result["prepared_target"] == {"layer": "extension_saved_state"}
assert "prepared_module_ref" not in result
assert result["next_resolution"]["method"] == "metadata.saved_state.prepare"
assert result["next_resolution"]["params"] == {
"base_id": "upo_test",
"layer": "extension_saved_state",
"mode": "plan",
"selector_required": "Pass a public 1C ref or kind/name selector.",
}
assert seen["prepare"]["mode"] == "plan"
assert seen["prepare"]["source_table"] == "ConfigCAS"
assert seen["prepare"]["target_table"] == "ConfigCASSave"
def test_metadata_write_active_module_ref_auto_prepares_when_apply_allowed(monkeypatch: pytest.MonkeyPatch) -> None:
seen: dict[str, Any] = {}
def fake_prepare(payload: dict[str, Any]) -> dict[str, Any]:
seen["prepare"] = payload
return {"schema": "onec_saved_state_prepare.v1", "status": "verified", "applied": True}
def fake_module_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["apply"] = payload
return {"schema": "onec_module_write_apply.v1", "status": "verified", "applied": True}
monkeypatch.setattr(adapter_server, "metadata_saved_state_prepare", fake_prepare)
monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_module_apply)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"kind": "module", "module_ref": "ConfigCAS:owner__module.0#stream:4"},
"mode": "apply_and_verify",
"allow_sql_saved_state_apply": True,
"old": "a",
"new": "b",
}
)
assert seen["prepare"]["mode"] == "apply_and_verify"
assert seen["prepare"]["allow_sql_saved_state_prepare"] is True
assert seen["apply"]["module_ref"] == "ConfigCASSave:owner__module.0#stream:4"
assert result["status"] == "verified"
assert result["resolution"]["method"] == "metadata.saved_state.prepare"
def test_metadata_write_missing_module_target_exposes_prepare_plan(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_modules_search",
lambda payload: {"status": "ok", "counts": {"modules": 0}, "modules": []},
)
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_prepare",
lambda payload: {
"schema": "onec_saved_state_prepare.v1",
"status": "plan_ready",
"ready_to_copy": True,
"target": {"table": payload["target_table"]},
},
)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"kind": "module", "extension": "test2", "object_type": "CommonForm", "object_name": "t_Форма"},
"old": "a",
"new": "b",
}
)
assert result["status"] == "not_found"
assert result["error"] == "module_target_not_resolved"
assert result["next_resolution"]["method"] == "metadata.saved_state.prepare"
assert result["next_resolution"]["params"] == {
"base_id": "upo_test",
"layer": "extension_saved_state",
"ref": "CommonForm.t_Форма",
"kind": "CommonForm",
"name": "t_Форма",
"extension": "test2",
"mode": "plan",
}
assert result["prepare_plan"]["status"] == "plan_ready"
def test_metadata_write_missing_module_target_auto_prepares_and_retries(monkeypatch: pytest.MonkeyPatch) -> None:
calls: dict[str, int] = {"search": 0}
seen: dict[str, Any] = {}
def fake_search(payload: dict[str, Any]) -> dict[str, Any]:
calls["search"] += 1
seen["search"] = payload
if calls["search"] == 1:
return {"status": "ok", "counts": {"modules": 0}, "modules": []}
return {
"status": "ok",
"counts": {"modules": 1},
"modules": [
{
"payload": {"sha1": "payload-sha"},
"streams": [{"module_ref": "ConfigCASSave:owner__module.0#stream:0"}],
}
],
}
def fake_module_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["apply"] = payload
return {"schema": "onec_module_write_apply.v1", "status": "verified", "applied": True}
monkeypatch.setattr(adapter_server, "metadata_saved_state_modules_search", fake_search)
monkeypatch.setattr(
adapter_server,
"metadata_saved_state_prepare",
lambda payload: {"schema": "onec_saved_state_prepare.v1", "status": "verified", "applied": True},
)
monkeypatch.setattr(adapter_server, "metadata_module_write_apply", fake_module_apply)
result = adapter_server.metadata_write(
{
"base_id": "upo_test",
"target": {"kind": "module", "extension": "test2", "object_type": "CommonForm", "object_name": "t_Форма"},
"mode": "apply_and_verify",
"allow_sql_saved_state_apply": True,
"old": "a",
"new": "b",
}
)
assert calls["search"] == 2
assert seen["search"]["object_type"] == "CommonForm"
assert seen["search"]["object_name"] == "t_Форма"
assert seen["apply"]["module_ref"] == "ConfigCASSave:owner__module.0#stream:0"
assert seen["apply"]["expected_sha1"] == "payload-sha"
assert result["status"] == "verified"
def test_saved_state_apply_requires_explicit_allow_flag() -> None:
result = adapter_server.storage_saved_state_apply_proposal(
{
"base_id": "upo_test",
"proposal": {
"source": {"table": "ConfigSave", "file_name": "form-guid.0"},
"original": {"sha1": "abc"},
"encoded": {"sha1": "def", "payload_hex": "00"},
},
}
)
assert result["status"] == "invalid_argument"
assert result["argument"] == "allow_sql_saved_state_apply"
def test_saved_state_apply_blocks_unsafe_form_payload_rewrite() -> None:
original = b"old-payload"
replacement = b"short"
proposal = {
"schema": "onec_change_proposal.v1",
"method": "metadata.form.element.write",
"status": "accepted_for_review",
"source": {"table": "ConfigCASSave", "file_name": "form-guid.0"},
"original": {"sha1": adapter_server.hashlib.sha1(original).hexdigest(), "bytes": len(original)},
"encoded": {
"sha1": adapter_server.hashlib.sha1(replacement).hexdigest(),
"bytes": len(replacement),
"payload_hex": replacement.hex(),
},
"validation": {"mode": "path", "status": "ok"},
}
result = adapter_server.storage_saved_state_apply_proposal(
{"base_id": "upo_test", "allow_sql_saved_state_apply": True, "proposal": proposal}
)
assert result["status"] == "unsafe_form_payload_rewrite"
assert result["applied"] is False
def test_saved_state_apply_updates_single_part_with_backup(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
original = b"old-payload"
replacement = b"new-payload"
state = {"data": original, "committed": False, "rolled_back": False, "sql": []}
class FakeCursor:
rowcount = 0
def __init__(self, store: dict[str, Any]) -> None:
self.store = store
def execute(self, sql: str, params: tuple[Any, ...]) -> None:
self.store["sql"].append(sql)
if sql.strip().upper().startswith("SELECT"):
self.rowcount = 1
return
if sql.strip().upper().startswith("UPDATE"):
self.store["data"] = params[0]
self.rowcount = 1
return
raise AssertionError(sql)
def fetchall(self) -> list[dict[str, Any]]:
return [{"PartNo": 0, "BinaryData": self.store["data"]}]
class FakeConn:
def cursor(self, as_dict: bool = False) -> FakeCursor:
return FakeCursor(state)
def commit(self) -> None:
state["committed"] = True
def rollback(self) -> None:
state["rolled_back"] = True
def close(self) -> None:
pass
monkeypatch.setenv("ONEC_ADAPTER_BACKUP_DIR", str(tmp_path))
monkeypatch.setattr(
adapter_server,
"connect_live_sql",
lambda *args, **kwargs: (FakeConn(), {"server": "sql-host", "database": "upo_test"}, None),
)
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (state["data"], {"database": "upo_test"}, None),
)
monkeypatch.setattr(
adapter_server,
"invalidate_adapter_caches_after_saved_state_change",
lambda base_id, *, reason: {"status": "ok", "base_id": base_id, "reason": reason},
)
proposal = {
"schema": "onec_change_proposal.v1",
"status": "accepted_for_review",
"source": {"table": "ConfigSave", "file_name": "form-guid.0"},
"original": {"sha1": adapter_server.hashlib.sha1(original).hexdigest(), "bytes": len(original)},
"encoded": {
"sha1": adapter_server.hashlib.sha1(replacement).hexdigest(),
"bytes": len(replacement),
"payload_hex": replacement.hex(),
},
"edits": [{"path": "1.2.3", "old": "old", "new": "new"}],
}
result = adapter_server.storage_saved_state_apply_proposal(
{"base_id": "upo_test", "allow_sql_saved_state_apply": True, "proposal": proposal}
)
assert result["status"] == "applied"
assert result["applied"] is True
assert state["data"] == replacement
assert state["committed"] is True
assert state["rolled_back"] is False
assert "WITH (UPDLOCK, HOLDLOCK)" in state["sql"][0]
assert result["cache_invalidation"]["reason"] == "saved_state_payload_apply"
assert Path(result["backup"]["path"]).exists()
backup = json.loads(Path(result["backup"]["path"]).read_text(encoding="utf-8"))
assert backup["original"]["payload_hex"] == original.hex()
def test_saved_state_cache_invalidation_clears_persistent_and_runtime_views(monkeypatch: pytest.MonkeyPatch) -> None:
base_id = "upo_test"
other_base_id = "other"
data_key = adapter_server.data_schema_cache_key(
{"base_id": base_id, "ref": "Catalog.Номенклатура"}
)
other_data_key = adapter_server.data_schema_cache_key(
{"base_id": other_base_id, "ref": "Catalog.Номенклатура"}
)
monkeypatch.setattr(
adapter_server,
"metadata_cache_invalidate",
lambda payload: {
"status": "ok",
"counts": {"deleted": 7},
"base_id": payload["base_id"],
},
)
monkeypatch.setattr(
adapter_server,
"BASE_ROOT_METADATA_CACHE",
{
(base_id, "Config"): {"cached_at": 1},
(other_base_id, "Config"): {"cached_at": 1},
},
)
monkeypatch.setattr(
adapter_server,
"DATA_SCHEMA_CACHE",
{
data_key: {"cached_at": 1},
other_data_key: {"cached_at": 1},
},
)
monkeypatch.setattr(
adapter_server,
"EXTENSION_MANIFEST_CACHE",
{
(base_id, "root-a"): {"status": "ok"},
(other_base_id, "root-b"): {"status": "ok"},
},
)
result = adapter_server.invalidate_adapter_caches_after_saved_state_change(
base_id,
reason="test",
)
assert result["status"] == "ok"
assert result["persistent"]["deleted"] == 7
assert result["runtime"] == {
"base_root_metadata": 1,
"data_schema": 1,
"extension_manifests": 1,
}
assert list(adapter_server.BASE_ROOT_METADATA_CACHE) == [(other_base_id, "Config")]
assert list(adapter_server.DATA_SCHEMA_CACHE) == [other_data_key]
assert list(adapter_server.EXTENSION_MANIFEST_CACHE) == [(other_base_id, "root-b")]
def test_saved_state_apply_runs_form_element_semantic_verification(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
original = b"old-payload"
replacement = b"new-payload"
state = {"data": original}
class FakeCursor:
rowcount = 0
def execute(self, sql: str, params: tuple[Any, ...]) -> None:
if sql.strip().upper().startswith("UPDATE"):
state["data"] = params[0]
self.rowcount = 1
def fetchall(self) -> list[dict[str, Any]]:
return [{"PartNo": 0, "BinaryData": state["data"]}]
class FakeConn:
def cursor(self, as_dict: bool = False) -> FakeCursor:
return FakeCursor()
def commit(self) -> None:
pass
def rollback(self) -> None:
pass
def close(self) -> None:
pass
monkeypatch.setenv("ONEC_ADAPTER_BACKUP_DIR", str(tmp_path))
monkeypatch.setattr(adapter_server, "connect_live_sql", lambda *args, **kwargs: (FakeConn(), {"server": "sql-host", "database": "upo_test"}, None))
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (state["data"], {"database": "upo_test"}, None))
monkeypatch.setattr(
adapter_server,
"metadata_form_decode",
lambda payload: {
"status": "ok",
"source": {"table": payload["table"], "file_name": payload["file_name"]},
"profile": {"items": [{"name": "Кнопка", "id": "67", "title": "Новый заголовок", "path": "1.2"}]},
},
)
proposal = {
"schema": "onec_change_proposal.v1",
"method": "metadata.form.element.write",
"status": "accepted_for_review",
"source": {"table": "ConfigSave", "file_name": "form-guid.0"},
"original": {"sha1": adapter_server.hashlib.sha1(original).hexdigest(), "bytes": len(original)},
"encoded": {"sha1": adapter_server.hashlib.sha1(replacement).hexdigest(), "bytes": len(replacement), "payload_hex": replacement.hex()},
"element": {"id": "67", "name": "Кнопка"},
"form_element_edits": [{"property": "title", "value": "Новый заголовок"}],
}
result = adapter_server.storage_saved_state_apply_proposal(
{"base_id": "upo_test", "allow_sql_saved_state_apply": True, "proposal": proposal}
)
assert result["status"] == "applied"
assert result["semantic_verification"]["status"] == "ok"
assert result["semantic_verification"]["checks"] == [
{"property": "title", "expected": "Новый заголовок", "actual": "Новый заголовок", "ok": True}
]
def test_saved_state_apply_runs_form_command_semantic_verification(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
original = b"old-payload"
replacement = b"new-payload"
state = {"data": original}
class FakeCursor:
rowcount = 0
def execute(self, sql: str, params: tuple[Any, ...]) -> None:
if sql.strip().upper().startswith("UPDATE"):
state["data"] = params[0]
self.rowcount = 1
def fetchall(self) -> list[dict[str, Any]]:
return [{"PartNo": 0, "BinaryData": state["data"]}]
class FakeConn:
def cursor(self, as_dict: bool = False) -> FakeCursor:
return FakeCursor()
def commit(self) -> None:
pass
def rollback(self) -> None:
pass
def close(self) -> None:
pass
monkeypatch.setenv("ONEC_ADAPTER_BACKUP_DIR", str(tmp_path))
monkeypatch.setattr(adapter_server, "connect_live_sql", lambda *args, **kwargs: (FakeConn(), {"server": "sql-host", "database": "upo_test"}, None))
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (state["data"], {"database": "upo_test"}, None))
monkeypatch.setattr(
adapter_server,
"metadata_form_decode",
lambda payload: {
"status": "ok",
"source": {"table": payload["table"], "file_name": payload["file_name"]},
"profile": {"items": [], "commands": [{"name": "КомандаПример1", "id": "2", "title": "ПРОВЕРКА", "path": "5.3"}]},
},
)
proposal = {
"schema": "onec_change_proposal.v1",
"method": "metadata.form.element.write",
"status": "accepted_for_review",
"source": {"table": "ConfigCASSave", "file_name": "form-guid.0"},
"original": {"sha1": adapter_server.hashlib.sha1(original).hexdigest(), "bytes": len(original)},
"encoded": {"sha1": adapter_server.hashlib.sha1(replacement).hexdigest(), "bytes": len(replacement), "payload_hex": replacement.hex()},
"element": {"section": "commands", "path": "5.3", "name": "КомандаПример1"},
"form_element_edits": [{"property": "title", "value": "ПРОВЕРКА"}],
}
result = adapter_server.storage_saved_state_apply_proposal(
{"base_id": "upo_test", "allow_sql_saved_state_apply": True, "proposal": proposal}
)
assert result["status"] == "applied"
assert result["semantic_verification"]["status"] == "ok"
assert result["semantic_verification"]["element"]["section"] == "commands"
def test_saved_state_apply_runs_form_attribute_field_semantic_verification(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
original = b"old-payload"
replacement = b"new-payload"
state = {"data": original}
class FakeCursor:
rowcount = 0
def execute(self, sql: str, params: tuple[Any, ...]) -> None:
if sql.strip().upper().startswith("UPDATE"):
state["data"] = params[0]
self.rowcount = 1
def fetchall(self) -> list[dict[str, Any]]:
return [{"PartNo": 0, "BinaryData": state["data"]}]
class FakeConn:
def cursor(self, as_dict: bool = False) -> FakeCursor:
return FakeCursor()
def commit(self) -> None:
pass
def rollback(self) -> None:
pass
def close(self) -> None:
pass
monkeypatch.setenv("ONEC_ADAPTER_BACKUP_DIR", str(tmp_path))
monkeypatch.setattr(adapter_server, "connect_live_sql", lambda *args, **kwargs: (FakeConn(), {"server": "sql-host", "database": "upo_test"}, None))
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *args, **kwargs: (state["data"], {"database": "upo_test"}, None))
monkeypatch.setattr(
adapter_server,
"metadata_form_decode",
lambda payload: {
"status": "ok",
"source": {"table": payload["table"], "file_name": payload["file_name"]},
"profile": {
"items": [],
"attributes": [
{
"name": "ТЗ",
"id": "7",
"path": "3.6",
"dynamic_list_fields": [
{"name": "К1", "id": "1", "title": "ТЕСТ_К1", "path": "3.6.14", "title_path": "3.6.14.4.2.1"}
],
}
],
},
},
)
proposal = {
"schema": "onec_change_proposal.v1",
"method": "metadata.form.element.write",
"status": "accepted_for_review",
"source": {"table": "ConfigCASSave", "file_name": "form-guid.0"},
"original": {"sha1": adapter_server.hashlib.sha1(original).hexdigest(), "bytes": len(original)},
"encoded": {"sha1": adapter_server.hashlib.sha1(replacement).hexdigest(), "bytes": len(replacement), "payload_hex": replacement.hex()},
"element": {"section": "attribute_fields", "path": "3.6.14", "name": "К1"},
"form_element_edits": [{"property": "title", "value": "ТЕСТ_К1"}],
}
result = adapter_server.storage_saved_state_apply_proposal(
{"base_id": "upo_test", "allow_sql_saved_state_apply": True, "proposal": proposal}
)
assert result["status"] == "applied"
assert result["semantic_verification"]["status"] == "ok"
assert result["semantic_verification"]["element"]["section"] == "attribute_fields"
def test_saved_state_rollback_loads_backup_by_id(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
backup_id = "a" * 32
backup_path = tmp_path / f"20260625T010000Z-{backup_id}.json"
backup_path.write_text(
json.dumps(
{
"schema": "onec_storage_apply_backup.v1",
"backup_id": backup_id,
"source": {"table": "ConfigSave", "file_name": "form-guid.0"},
"original": {"sha1": "old", "payload_hex": "00"},
"replacement": {"sha1": "new"},
"rollback": {
"payload": {
"base_id": "upo_test",
"allow_sql_saved_state_apply": True,
"proposal": {
"source": {"table": "ConfigSave", "file_name": "form-guid.0"},
"original": {"sha1": "new"},
"encoded": {"sha1": "old", "payload_hex": "00"},
},
}
},
},
ensure_ascii=False,
),
encoding="utf-8",
)
seen: dict[str, Any] = {}
monkeypatch.setenv("ONEC_ADAPTER_BACKUP_DIR", str(tmp_path))
def fake_apply(payload: dict[str, Any]) -> dict[str, Any]:
seen["payload"] = payload
return {"status": "applied", "applied": True, "readback": {"verified": True}}
monkeypatch.setattr(adapter_server, "storage_saved_state_apply_proposal", fake_apply)
result = adapter_server.storage_saved_state_rollback(
{"base_id": "upo_test", "allow_sql_saved_state_rollback": True, "backup_id": backup_id}
)
assert result["status"] == "applied"
assert result["applied"] is True
assert result["backup"]["backup_id"] == backup_id
assert seen["payload"]["allow_sql_saved_state_apply"] is True
assert seen["payload"]["base_id"] == "upo_test"
def test_swap_brace_text_paths_preserves_surrounding_format() -> None:
from parser.payload import parse_brace_text, scalar, swap_brace_text_paths, get_tree_path
text = '{0, {48,"А",{1,2}}, {99}, {48,"Б",{3,4}}}'
swapped, info = swap_brace_text_paths(text, "1", "3")
tree = parse_brace_text(swapped)
assert info["path_a"] == "1"
assert info["path_b"] == "3"
assert swapped == '{0, {48,"Б",{3,4}}, {99}, {48,"А",{1,2}}}'
assert scalar(get_tree_path(tree, "1.1")) == "Б"
assert scalar(get_tree_path(tree, "3.1")) == "А"
def test_changes_propose_supports_preserve_format_structural_swap(monkeypatch: pytest.MonkeyPatch) -> None:
text = '{0, {48,"А",{1,2}}, {99}, {48,"Б",{3,4}}}'
original = text.encode("utf-8")
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (original, {"database": "upo_test"}, None),
)
result = adapter_server.changes_propose(
{
"base_id": "upo_test",
"source": {"table": "ConfigCASSave", "file_name": "form.0"},
"edits": [{"swap_paths": ["1", "3"]}],
"preserve_format": True,
"include_payload": True,
}
)
assert result["status"] == "accepted_for_review"
assert result["validation"]["status"] == "ok"
assert result["validation"]["mode"] == "path_preserve_format"
assert result["edits"][0]["mode"] == "structural_swap_preserve_format"
assert result["encoded"]["bytes"] == result["original"]["bytes"]
assert bytes.fromhex(result["encoded"]["payload_hex"]).decode("utf-8") == '{0, {48,"Б",{3,4}}, {99}, {48,"А",{1,2}}}'
def test_changes_propose_supports_verified_root_replacement(monkeypatch: pytest.MonkeyPatch) -> None:
from parser.payload import parse_brace_text
original = b"{1,2,3}"
replacement = parse_brace_text("{1,2,3,4,5}")
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (original, {"database": "upo_test"}, None),
)
result = adapter_server.changes_propose(
{
"base_id": "upo_test",
"source": {"table": "ConfigSave", "file_name": "scheduled-job.0"},
"edits": [
{
"replace_root": replacement,
"fields": ["week_days"],
"resized_collections": ["week_days"],
"old_counts": {"week_days": 1},
"new_counts": {"week_days": 3},
}
],
"preserve_format": True,
"include_payload": True,
}
)
assert result["status"] == "accepted_for_review"
assert result["validation"]["status"] == "ok"
assert result["edits"][0]["mode"] == "structural_replace_root_preserve_prefix"
assert result["edits"][0]["old_counts"] == {"week_days": 1}
assert result["edits"][0]["new_counts"] == {"week_days": 3}
assert bytes.fromhex(result["encoded"]["payload_hex"]).decode("utf-8-sig") == "{1,2,3,4,5}"
def test_changes_propose_repairs_mojibake_stream_before_replace(monkeypatch: pytest.MonkeyPatch) -> None:
from parser.cas_payload import stream_header
good_text = "Процедура ОбработкаКоманды()\r\n\t// Старый\r\nКонецПроцедуры\r\n"
mojibake_text = good_text.encode("utf-8").decode("cp1251")
stream_bytes = mojibake_text.encode("utf-8")
stored = compress_payload(stream_header(len(stream_bytes)) + stream_bytes, "raw_deflate")
monkeypatch.setattr(
adapter_server,
"read_storage_file_bytes",
lambda *args, **kwargs: (stored, {"database": "upo_test"}, None),
)
result = adapter_server.changes_propose(
{
"base_id": "upo_test",
"source": {"table": "ConfigCASSave", "file_name": "object__module.0", "module_id": "ConfigCASSave:object__module.0#stream:0"},
"edits": [
{
"replace": {"old": "// Старый", "new": "// Новый"},
"expected_contains": "Процедура ОбработкаКоманды",
}
],
"include_payload": True,
}
)
assert result["status"] == "accepted_for_review"
assert result["edits"][0]["encoding_repaired"] is True
assert "Процедура ОбработкаКоманды" in result["edits"][0]["old_text_preview"]
assert "// Новый" in result["edits"][0]["new_text_preview"]
assert not result["edits"][0]["new_text_preview"].startswith("\ufeff")
@pytest.mark.parametrize(
("annotation", "expected"),
[
("Перед", "insert_before"),
("После", "insert_after"),
("Вместо", "replace"),
("ИзменениеИКонтроль", "replace_with_control"),
],
)
def test_extension_action_is_decoded_from_bsl_annotation(annotation: str, expected: str) -> None:
result = adapter_server.metadata_extension_action_from_evidence(
source="extension",
method_name="ПередЗаписью",
read_result={"text": f'&{annotation}("ПередЗаписью")\nПроцедура РасширениеПередЗаписью()\nКонецПроцедуры'},
)
assert result["status"] == "ok"
assert result["operation_class"] == expected
def test_effective_extension_routine_scan_orders_and_proves_form_candidates(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"extension_map_by_guid",
lambda _base_id: {
"late": {"name": "Late", "guid": "late", "order": 2, "active": True},
"early": {"name": "Early", "guid": "early", "order": 1, "active": True},
},
)
monkeypatch.setattr(
adapter_server,
"search_modules",
lambda payload: {
"status": "ok",
"matches": [{"read_selector": {"module_ref": "ConfigCASSave:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa__f5f217c8-ac69-4e23-966b-d8ec0c665638.0"}}]
if payload["extension"] == "early"
else [],
},
)
monkeypatch.setattr(
adapter_server,
"repository_resolve_form_guid_sql",
lambda **_kwargs: ({"resolved_object": "Document.Тест.Form.Форма"}, None),
)
monkeypatch.setattr(
adapter_server,
"read_module",
lambda _payload: {"status": "ok", "text": '&После("Метод")\nПроцедура РасшМетод()\nКонецПроцедуры'},
)
layers = adapter_server.effective_extension_routine_scan("upo_test", "Метод", canonical_object="Document.Тест.Form.Форма")
assert [layer["extension"]["name"] for layer in layers] == ["Early", "Late"]
assert layers[0]["applicability"] == "proven"
assert layers[0]["extension_actions"][0]["operation_class"] == "insert_after"
assert layers[1]["applicability"] == "not_found"
def test_metadata_resolve_overrides_marks_unknown_extension_action(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_object_modules",
lambda payload: {
"status": "ok",
"object": {"kind": "Catalog", "name": "Номенклатура"},
"modules": [{"module_id": "ConfigCAS:ext-guid__module-guid.0", "name": "object"}],
},
)
monkeypatch.setattr(
adapter_server,
"read_module",
lambda payload: {
"status": "ok",
"selection": {"routine_name": "ПередЗаписью", "line_start": 1, "line_end": 3, "match_by": "routine_exact"},
},
)
result = adapter_server.metadata_resolve_overrides(
{
"base_id": "upo_test",
"object_type": "Catalog",
"object_name": "Номенклатура",
"method_name": "ПередЗаписью",
}
)
assert result["status"] == "ok"
action = result["chain"][0]["extension_action"]
assert action["status"] == "unknown"
assert action["operation_class"] == "unknown_extension_action"
assert "replace_with_control" in action["requires"][0]
assert result["extension_actions"] == [action]
evidence = result["write_plan_evidence"]
assert evidence["method"] == "metadata.write.plan"
assert evidence["target"]["kind"] == "module"
assert evidence["target"]["routine_name"] == "ПередЗаписью"
assert evidence["target"]["extension_action"] == action
assert evidence["next_resolution"]["method"] == adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD
assert evidence["next_resolution"]["params"]["query"] == "ПередЗаписью"
assert evidence["next_resolution"]["params"]["layer"] == "extension_saved_state"
assert evidence["next_resolution"]["params"]["ref"] == "Catalog.Номенклатура"
assert "tables" not in evidence["next_resolution"]["params"]
assert "owner_guid" not in evidence["next_resolution"]["params"]
assert "intent" not in evidence
def test_metadata_resolve_overrides_preserves_replace_with_control_action(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_object_modules",
lambda payload: {
"status": "ok",
"object": {"kind": "Catalog", "name": "Номенклатура"},
"modules": [{"module_id": "ConfigCAS:ext-guid__module-guid.0", "name": "object"}],
},
)
monkeypatch.setattr(
adapter_server,
"read_module",
lambda payload: {
"status": "ok",
"selection": {
"routine_name": "ПередЗаписью",
"line_start": 1,
"line_end": 3,
"match_by": "routine_exact",
"operation_class": "replace_with_control",
},
},
)
result = adapter_server.metadata_resolve_overrides(
{
"base_id": "upo_test",
"object_type": "Catalog",
"object_name": "Номенклатура",
"method_name": "ПередЗаписью",
}
)
action = result["chain"][0]["extension_action"]
assert action["status"] == "ok"
assert action["operation_class"] == "replace_with_control"
assert action["requires_control_fragment"] is True
evidence = result["write_plan_evidence"]
assert evidence["target"]["extension_action"] == action
assert evidence["intent"]["operation"] == "replace_with_control"
assert evidence["next_resolution"]["method"] == adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD
assert evidence["next_resolution"]["params"]["layer"] == "extension_saved_state"
assert evidence["next_resolution"]["params"]["ref"] == "Catalog.Номенклатура"
def test_metadata_resolve_overrides_base_evidence_points_to_configsave(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
adapter_server,
"metadata_object_modules",
lambda payload: {
"status": "ok",
"object": {"kind": "Catalog", "name": "Номенклатура", "guid": "owner-guid"},
"modules": [{"module_id": "Config:owner-guid__module-guid.0", "name": "object"}],
},
)
monkeypatch.setattr(
adapter_server,
"read_module",
lambda payload: {
"status": "ok",
"selection": {"routine_name": "ПередЗаписью", "line_start": 1, "line_end": 3, "match_by": "routine_exact"},
},
)
result = adapter_server.metadata_resolve_overrides(
{
"base_id": "upo_test",
"object_type": "Catalog",
"object_name": "Номенклатура",
"method_name": "ПередЗаписью",
}
)
evidence = result["write_plan_evidence"]
assert evidence["next_resolution"]["method"] == adapter_server.SAVED_STATE_MODULES_SEARCH_METHOD
assert evidence["next_resolution"]["params"]["layer"] == "base_saved_state"
assert evidence["next_resolution"]["params"]["ref"] == "Catalog.Номенклатура"
assert "tables" not in evidence["next_resolution"]["params"]
assert "owner_guid" not in evidence["next_resolution"]["params"]
assert evidence["target"]["object_guid"] == "owner-guid"
def test_form_long_command_bar_decodes_tail_autofill() -> None:
from parser.form_payload import enrich_container_specific_semantics
values = ["0"] * 83
values[0] = "22"
values[82] = "0"
row = {"marker": "22", "type_name": "Командная панель", "semantic": {"groups": {}}}
enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {p["name"]: p["value"] for p in row["semantic"]["groups"]["Использование"]}
assert props["Автозаполнение"] is False
def test_form_managed_group_decodes_spacing_and_horizontal_alignment() -> None:
from parser.form_payload import enrich_container_specific_semantics
managed = "{29,1,0,0,0,{0},{1,0},{\"Pattern\"},\"\",{3,4,{0}},0,0,0,1,{1,0},0,2,1,3,2,0,1,1,{3,4,{0}},0,2,0,3,0}"
values = ["0"] * 30
values[0] = "22"
values[20] = managed
row = {"marker": "22", "type_name": "Группа", "semantic": {"groups": {}}}
enrich_container_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {p["name"]: p["value"] for p in row["semantic"]["groups"]["Расположение"]}
assert props["VerticalSpacing"] == "Half"
assert props["ГоризонтальноеПоложениеВГруппе"] == "Center"
def test_form_decoration_decodes_balloon_tooltip_and_windows_text_color() -> None:
from parser.form_payload import enrich_decoration_specific_semantics
values = ["0"] * 36
values[0] = "12"
values[14] = "{3,1,{18}}"
values[18] = "{4,0,0,0,0,0,{3,4,{0}}}"
values[22] = "2"
row = {"marker": "12", "semantic": {"groups": {}}}
enrich_decoration_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {p["name"]: p["value"] for p in row["semantic"]["groups"]["Оформление"]}
assert props["ЦветТекста"] == "win:ButtonText"
assert props["ОтображениеПодсказки"] == "Balloon"
def test_form_command_record_decodes_representation_and_shortcut() -> None:
from parser.form_payload import direct_parameters, section_record_semantic_properties
node = parse_brace_text('{9,{51,409b9a53-7f7e-4178-86c1-33176c7c7a7a},"ОтправитьQR",{1,0},{1,0},{0,{0,{"B",1},0}},{0,83,8},{1,0},"ОтправитьQR",2,0,0,{0,0},1,0,1,0,0,2}')
row = {"category": "Command", "name": "ОтправитьQR"}
semantic = section_record_semantic_properties(row, direct_parameters(node, "5.47"), node)
props = {p["name"]: p["value"] for group in semantic["groups"].values() for p in group}
assert props["Отображение"] == "TextPicture"
assert props["СочетаниеКлавиш"] == "Ctrl+S"
assert props["CurrentRowUse"] == "DontUse"
def test_form_command_record_decodes_function_key_without_modifiers() -> None:
from parser.form_payload import direct_parameters, section_record_semantic_properties
node = parse_brace_text('{9,0,"Обновить",0,0,0,{0,116,0},0,"Обновить",3,0,0,0,1,0,1,0,0,1}')
row = {"category": "Command", "name": "Обновить"}
semantic = section_record_semantic_properties(row, direct_parameters(node, "5.4"), node)
props = {p["name"]: p["value"] for group in semantic["groups"].values() for p in group}
assert props["СочетаниеКлавиш"] == "F5"
def test_form_command_record_decodes_alt_modifier_and_picture_representation() -> None:
from parser.form_payload import direct_parameters, section_record_semantic_properties
node = parse_brace_text('{9,0,"Копировать",0,0,0,{0,67,24},0,"Копировать",1,0,0,0,1,0,1,0,0,1}')
row = {"category": "Command", "name": "Копировать"}
semantic = section_record_semantic_properties(row, direct_parameters(node, "5.8"), node)
props = {p["name"]: p["value"] for group in semantic["groups"].values() for p in group}
assert props["СочетаниеКлавиш"] == "Ctrl+Alt+C"
assert props["Отображение"] == "Picture"
assert props["CurrentRowUse"] == "DontUse"
def test_form_input_choice_button_representation_show_in_input_field() -> None:
from parser.form_payload import choice_button_representation_presentation
assert choice_button_representation_presentation("3") == "ShowInInputField"
def test_form_standard_document_commands_are_known() -> None:
from parser.form_payload import FORM_STANDARD_COMMANDS
assert FORM_STANDARD_COMMANDS[("fe558fde-99b3-45d0-a060-9fc2905309f6", "0")] == "Form.StandardCommand.Write"
assert FORM_STANDARD_COMMANDS[("174e58ce-82ad-4787-b956-9367937f7971", "0")] == "Form.StandardCommand.ChangeHistory"
def test_graphical_schema_standard_command_resolves_public_owner_path() -> None:
from parser.form_payload import button_command_links
items = [
{"marker": "37", "type_name": "GraphicalSchemaField", "name": "КартаМаршрута"},
{
"marker": "31",
"name": "ФормаПечать",
"command_reference": {"guid": "e2d6f793-b786-4640-a91b-8d77f73860f1", "code": "3"},
},
]
links = button_command_links(items, [])
assert links[0]["command_name"] == "Form.Item.КартаМаршрута.StandardCommand.Print"
def test_dynamic_list_standard_and_task_commands_resolve_public_names() -> None:
from parser.form_payload import button_command_links
items = [
{
"marker": "55",
"type_name": "Динамический список",
"name": "Список",
"id": "1",
"dynamic_list_settings": {"query_text": "ВЫБРАТЬ Ссылка ИЗ Задача.ЗадачаИсполнителя"},
},
{
"marker": "31",
"name": "Изменить",
"command_reference": {"guid": "b41f5bbc-ba5d-4888-8cd1-db246a371418", "code": "1"},
},
{
"marker": "31",
"name": "Выполнить",
"command_reference": {"guid": "0fa77ef7-a836-4459-9bca-6010d0bdfc7f", "code": "0"},
},
]
links = button_command_links(items, [])
assert links[0]["command_name"] == "Form.Item.Список.StandardCommand.Change"
assert links[1]["command_name"] == "Task.ЗадачаИсполнителя.Command.Выполнено"
def test_form_parameter_rows_decode_key_parameter() -> None:
from parser.form_payload import form_parameter_rows
tree = parse_brace_text('{4,0,0,0,{0,1,{0,"БизнесПроцесс",{0},1}}}')
rows = form_parameter_rows(tree)
assert rows[0]["name"] == "БизнесПроцесс"
assert rows[0]["type_name"] == "Parameter"
props = {prop["name"]: prop["value"] for group in rows[0]["semantic"]["groups"].values() for prop in group}
assert props["KeyParameter"] is True
def test_form_legacy_special_field_and_english_tooltip_names_are_public() -> None:
from parser.form_payload import form_item_public_type_name
assert form_item_public_type_name("37", "11", "Диаграмма") == "ChartField"
assert form_item_public_type_name("37", "14", "КартаМаршрута") == "GraphicalSchemaField"
assert form_item_public_type_name("12", "0", "КартаМаршрутаExtendedTooltip") == "Расширенная подсказка"
def test_picture_field_decodes_size_stretch_and_file_drag_mode() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
values = ["0"] * 40
values[0] = "37"
values[5] = "4"
specific = ["0"] * 24
specific[0] = "10"
specific[1] = "2"
specific[2] = "0"
specific[17] = "1"
values[39] = "{" + ",".join(specific) + "}"
row = {"marker": "37", "type_code": "4", "semantic": {"groups": {}}}
enrich_input_field_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {prop["name"]: prop["value"] for group in row["semantic"]["groups"].values() for prop in group}
assert props["Ширина"] == "2"
assert props["РастягиватьПоГоризонтали"] is False
assert props["РежимПеретаскиванияФайлов"] == "AsFile"
def test_form_command_button_decodes_default_button() -> None:
from parser.form_payload import enrich_command_button_specific_semantics
values = ["0"] * 51
values[0] = "31"
values[11] = "1"
row = {"marker": "31", "semantic": {"groups": {}}}
enrich_command_button_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {p["name"]: p["value"] for p in row["semantic"]["groups"]["Основные"]}
assert props["КнопкаПоУмолчанию"] is True
def test_form_catalog_input_field_decodes_bounds_domain_footer_and_border() -> None:
from parser.form_payload import enrich_input_field_specific_semantics
values = ["0"] * 59
values[0] = "37"
values[5] = "2"
values[6] = '"Наценка"'
specific = ["0"] * 66
specific[0] = "36"
specific[16] = '{"N",-99}'
specific[17] = '{"N",100}'
specific[32] = "0"
specific[39] = "{3,3,{-22}}"
specific[45] = "2"
values[39] = "{" + ",".join(specific) + "}"
row = {"marker": "37", "type_code": "2", "name_path": "1.6", "semantic": {"groups": {}}}
enrich_input_field_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {p["name"]: p["value"] for group in row["semantic"]["groups"].values() for p in group}
assert props["TypeDomainEnabled"] is False
assert props["MinValue"] == "-99"
assert props["MaxValue"] == "100"
assert props["FooterHorizontalAlign"] == "Left"
assert props["BorderColor"] == "style:BorderColor"
def test_form_radio_button_auto_type_and_columns_count() -> None:
from parser.form_payload import enrich_radio_button_specific_semantics
values = ["0"] * 59
values[0] = "37"
values[5] = "5"
values[39] = "{8,{3,1},1,0,0,0,0,0,0,0,0,2}"
row = {"marker": "37", "type_code": "5", "semantic": {"groups": {}}}
enrich_radio_button_specific_semantics(row, parse_brace_text("{" + ",".join(values) + "}"))
props = {p["name"]: p["value"] for group in row["semantic"]["groups"].values() for p in group}
assert props["RadioButtonType"] == "Auto"
assert props["ColumnsCount"] == "1"