from __future__ import annotations import json import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "plugins" / "1c" / "connector")) import repository_control # noqa: E402 import adapter_1c_server # noqa: E402 def configured_base(monkeypatch, tmp_path: Path, backend: str = "karman_bridge") -> None: config = { "base": { "server": "sql", "database": "db", "repository": { "backend": backend, "designer_path": "designer.exe", "endpoint": "tcp://configured.example:15420/configured-repository", "bridge_id": "configured-bridge", "infobase": {"server": "onec/ib"}, "repository_user": "repo-user", "repository_password_env": "TEST_REPOSITORY_PASSWORD", }, } } monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(config)) monkeypatch.setenv("TEST_REPOSITORY_PASSWORD", "secret-value") monkeypatch.setenv("ONEC_REPOSITORY_STATE_FILE", str(tmp_path / "locks.json")) def configured_layers(monkeypatch, tmp_path: Path) -> None: config = { "base": { "server": "sql", "database": "db", "user": "reader", "development_layers": { "base": {"repository": {"mode": "manual"}, "support": {"mode": "rules", "rules": {"Документ.Заказ": "vendor_not_editable", "Документ.Заказ.Форма.ФормаДокумента": "vendor_editable_support_preserved"}}}, "extension:11111111-2222-3333-4444-555555555555": {"repository": {"mode": "none"}, "support": {"mode": "none"}}, "extension:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee": {"repository": {"mode": "manual"}, "support": {"mode": "editable"}}, }, } } monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(config)) monkeypatch.setenv("ONEC_REPOSITORY_STATE_FILE", str(tmp_path / "locks.json")) def test_repository_backend_and_endpoint_come_only_from_base_settings(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) config, error = repository_control.repository_config("base") assert error is None assert config["backend"] == "karman_bridge" assert config["endpoint"] == "tcp://configured.example:15420/configured-repository" public = repository_control.status({"base_id": "base"}) assert public["repository"]["bridge_id"] == "configured-bridge" assert "secret-value" not in json.dumps(public) def test_http_runner_profile_does_not_require_local_designer_or_credentials(monkeypatch, tmp_path: Path) -> None: config = {"base": {"repository": {"backend": "karman_bridge", "runner": {"kind": "http", "url": "http://runner:8121", "token_env": "TOKEN"}}}} monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(config)) configured, error = repository_control.repository_config("base") assert error is None assert configured["runner"]["kind"] == "http" assert "designer_path" not in configured def test_lock_plan_maps_child_metadata_to_development_owner() -> None: result = repository_control.lock_plan({"object": "РегистрСведений.Настройки.Реквизит.Код"}) assert result["status"] == "ready" assert result["scope_status"] == "resolved" assert result["lock_objects"] == ["РегистрСведений.Настройки"] assert result["native_lock_state"] == "unknown" assert result["native_lock_verification"] == "not_available_via_infobase_sql" assert result["capture_required"] is None def test_manual_lock_confirmation_is_scoped_and_not_reported_as_verified(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) values = json.loads(__import__("os").environ["ONEC_SQL_BASES_JSON"]) values["base"]["repository"]["lock_mode"] = "manual" monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(values)) planned = repository_control.lock_plan({"base_id": "base", "object": "Справочник.Товары.МодульОбъекта"}) assert planned["workflow"] == "manual" assert planned["next_method"] == "repository.lock.request" assert planned["user_action"]["action"] == "confirm_lock_in_configurator" assert "уже захвачены" in planned["user_action"]["message"] assert planned["user_action"]["objects"] == ["Справочник.Товары"] requested = repository_control.create_lock_request({"base_id": "base", "object": "Справочник.Товары"}) confirmed = repository_control.confirm_manual_lock({"base_id": "base", "request_id": requested["request_id"], "user_confirmed_locked": True, "confirmed_repository_user": "repo-user"}) assert confirmed["status"] == "manual_confirmed" assert confirmed["capture_state"] == "manually_confirmed" assert confirmed["native_lock_state"] == "unknown" assert confirmed["native_lock_verification"] == "not_available_via_infobase_sql" assert confirmed["automatically_verified"] is False verified = repository_control.verify({"lock_session_id": confirmed["lock_session_id"]}) assert verified["status"] == "manual_confirmation_unverified" assert repository_control.write_gate({"base_id": "base", "lock_session_id": confirmed["lock_session_id"], "repository_object": "Справочник.Товары"})["allowed"] is True assert repository_control.write_gate({"base_id": "base", "lock_session_id": confirmed["lock_session_id"], "repository_object": "Справочник.Другой"})["allowed"] is False def test_manual_lock_request_stays_pending_until_user_confirms_exact_saved_scope(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) values = json.loads(__import__("os").environ["ONEC_SQL_BASES_JSON"]) values["base"]["repository"]["lock_mode"] = "manual" monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(values)) requested = repository_control.create_lock_request({"base_id": "base", "object": "РегистрСведений.Настройки.Реквизит.Код"}) assert requested["status"] == "pending_user_lock" assert requested["objects"] == ["РегистрСведений.Настройки"] assert requested["automatically_locked"] is False assert requested["next_call"] == { "method": "repository.lock.confirm", "params": {"base_id": "base", "request_id": requested["request_id"], "user_confirmed_locked": True}, } pending = repository_control.lock_request_status({"request_id": requested["request_id"]}) assert pending["status"] == "pending_user_lock" assert pending["next_call"] == requested["next_call"] incomplete = repository_control.confirm_manual_lock({"base_id": "base", "request_id": requested["request_id"]}) assert incomplete["status"] == "confirmation_required" assert incomplete["error"] == "user_confirmed_locked_required" assert incomplete["required_fields"] == ["base_id", "request_id", "user_confirmed_locked"] assert incomplete["next_call"] == requested["next_call"] assert incomplete["sql_resolution"] == [] confirmed = repository_control.confirm_manual_lock({ "base_id": "base", "request_id": requested["request_id"], "objects": ["Справочник.Подмена"], "user_confirmed_locked": True, "confirmed_repository_user": "repo-user", }) assert confirmed["objects"] == ["РегистрСведений.Настройки"] assert confirmed["automatically_verified"] is False assert confirmed["write_context"]["lock_session_id"] == confirmed["lock_session_id"] assert confirmed["write_context"]["repository_object"] == "РегистрСведений.Настройки" completed = repository_control.lock_request_status({"request_id": requested["request_id"]}) assert completed["status"] == "confirmed_by_user" closed = repository_control.close_manual_lock({"lock_session_id": confirmed["lock_session_id"], "user_confirmed_released": True}) assert closed["status"] == "closed" assert closed["request_id"] == requested["request_id"] completed = repository_control.lock_request_status({"request_id": requested["request_id"]}) assert completed["status"] == "closed" assert completed["request"]["closed_at"] > completed["request"]["confirmed_at"] closed_again = repository_control.close_manual_lock({"lock_session_id": confirmed["lock_session_id"], "user_confirmed_released": True}) assert closed_again["status"] == "closed" assert closed_again["request_id"] == requested["request_id"] assert repository_control.write_gate({"base_id": "base", "lock_session_id": confirmed["lock_session_id"], "repository_object": "РегистрСведений.Настройки"})["allowed"] is False def test_pending_request_can_be_cancelled_and_old_state_expires(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) values = json.loads(__import__("os").environ["ONEC_SQL_BASES_JSON"]) values["base"]["repository"]["lock_mode"] = "manual" monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(values)) request = repository_control.create_lock_request({"base_id": "base", "object": "Справочник.Товары"}) cancelled = repository_control.cancel_lock_request({"request_id": request["request_id"], "confirm_cancel": True}) assert cancelled["status"] == "cancelled" old = repository_control.create_lock_request({"base_id": "base", "object": "Справочник.Склады"}) state = repository_control._read_state() state["requests"][old["request_id"]]["created_at"] = 0 repository_control._write_state(state) monkeypatch.setenv("ONEC_REPOSITORY_REQUEST_TTL_SECONDS", "60") assert repository_control.lock_request_status({"request_id": old["request_id"]})["status"] == "expired" def test_adapter_resolves_lock_request_object_against_live_sql(monkeypatch) -> None: calls = [] def fake_get_object(kind, name, **kwargs): calls.append((kind, name, kwargs["base_id"])) return {"status": "ok", "object": {"kind_ru": "РегистрСведений", "name": name, "guid": "guid"}} monkeypatch.setattr(adapter_1c_server, "get_object", fake_get_object) normalized, error = adapter_1c_server.validate_repository_request_objects_sql({ "base_id": "upo_test", "object": "РегистрСведений.Настройки.Реквизит.Код", }) assert error is None assert calls == [("InformationRegister", "Настройки", "upo_test")] assert normalized["objects"] == ["РегистрСведений.Настройки"] def test_form_lock_scope_is_resolved_before_plan_request_and_confirmation(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) form_guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638" def fake_get_object(kind, name, **kwargs): assert kind == "Document" assert name == "тл_Планировщик" return {"status": "ok", "object": {"kind": "Document", "name": "тл_Планировщик", "guid": "owner-guid"}} monkeypatch.setattr(adapter_1c_server, "get_object", fake_get_object) def fake_metadata_object_forms(payload): assert "extension_guid" not in payload return {"status": "ok", "forms": [{"guid": form_guid, "name": "ФормаДокументаНовая"}]} monkeypatch.setattr(adapter_1c_server, "metadata_object_forms", fake_metadata_object_forms) reference = "Document.тл_Планировщик.Form.ФормаДокументаНовая" planned = adapter_1c_server.call_method("repository.lock.plan", {"base_id": "base", "objects": [reference]}) assert planned["status"] == "ready" assert planned["lock_objects"] == [reference] resolution = planned["sql_resolution"][0] assert resolution["requested_object"] == reference assert resolution["resolved_object"] == reference assert resolution["form_guid"] == form_guid assert resolution["owner"] == {"kind": "Document", "name": "тл_Планировщик", "guid": "owner-guid"} assert resolution["table"] == "Config" assert resolution["source"] == "live_sql_form_owner" assert resolution["attempts"][-1] == {"stage": "form", "table": "Config", "status": "ok"} request = adapter_1c_server.call_method("repository.lock.request", {"base_id": "base", "objects": [reference]}) assert request["status"] == "pending_user_lock" assert request["sql_resolution"] == planned["sql_resolution"] confirmed = repository_control.confirm_manual_lock({ "base_id": "base", "request_id": request["request_id"], "user_confirmed_locked": True, "confirmed_repository_user": "repo-user", }) assert confirmed["status"] == "manual_confirmed" assert repository_control.write_gate({"base_id": "base", "lock_session_id": confirmed["lock_session_id"], "repository_object": reference})["allowed"] is True mismatch = repository_control.write_gate({"base_id": "base", "lock_session_id": confirmed["lock_session_id"], "repository_object": "Document.тл_Планировщик.Form.Другая"}) assert mismatch["status"] == "blocked_repository_scope_mismatch" assert mismatch["error"] == "lock_object_mismatch" @pytest.mark.parametrize("kind", ["Catalog", "DataProcessor", "Report"]) def test_owned_form_lock_resolver_supports_other_owner_kinds(monkeypatch, tmp_path: Path, kind: str) -> None: configured_base(monkeypatch, tmp_path) monkeypatch.setattr(adapter_1c_server, "get_object", lambda actual_kind, name, **kwargs: {"status": "ok", "object": {"kind": actual_kind, "name": name, "guid": "owner-guid"}}) monkeypatch.setattr(adapter_1c_server, "metadata_object_forms", lambda payload: {"status": "ok", "forms": [{"guid": "11111111-2222-3333-4444-555555555555", "name": "Основная"}]}) result = adapter_1c_server.call_method("repository.lock.plan", {"base_id": "base", "object": f"{kind}.Объект.Form.Основная"}) assert result["status"] == "ready" assert result["lock_objects"] == [f"{kind}.Объект.Form.Основная"] def test_owned_extension_form_lock_discovers_its_sql_layer(monkeypatch, tmp_path: Path) -> None: configured_layers(monkeypatch, tmp_path) extension_guid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" reference = "Document.Заказ.Form.Основная" calls: list[str | None] = [] def fake_resolve(**kwargs): guid = kwargs.get("extension_guid") calls.append(guid) if guid != extension_guid: return None, {"status": "not_found", "error": "repository_form_not_found_in_sql"} return { "requested_object": reference, "resolved_object": reference, "form_guid": "11111111-2222-3333-4444-555555555555", "owner": {"kind": "Document", "name": "Заказ", "guid": "owner-guid"}, "table": "ConfigCAS", "source": "live_sql_form_owner", "extension_guid": extension_guid, }, None monkeypatch.setattr(adapter_1c_server, "repository_resolve_owned_form_sql", fake_resolve) monkeypatch.setattr(adapter_1c_server, "list_extensions", lambda payload: { "status": "ok", "extensions": [{"guid": extension_guid}], }) result = adapter_1c_server.call_method("repository.lock.plan", {"base_id": "base", "object": reference}) assert result["status"] == "ready" assert result["layer_id"] == f"extension:{extension_guid}" assert result["sql_resolution"][0]["source"] == "live_sql_extension_form_owner" assert calls == [None, extension_guid] def test_form_guid_lock_scope_uses_cached_owner_only_after_live_validation(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638" monkeypatch.setattr(adapter_1c_server, "metadata_form_owner_cache_lookup", lambda *args, **kwargs: {"owner": {"kind": "Document", "name": "тл_Планировщик"}, "form": {"name": "ФормаДокументаНовая"}}) monkeypatch.setattr(adapter_1c_server, "get_object", lambda kind, name, **kwargs: {"status": "ok", "object": {"kind": kind, "name": name, "guid": "owner-guid"}}) monkeypatch.setattr(adapter_1c_server, "metadata_object_forms", lambda payload: {"status": "ok", "forms": [{"guid": guid, "name": "ФормаДокументаНовая"}]}) result = adapter_1c_server.call_method("repository.lock.plan", {"base_id": "base", "object": f"Form.{guid}"}) assert result["status"] == "ready" assert result["lock_objects"] == ["Document.тл_Планировщик.Form.ФормаДокументаНовая"] def test_form_guid_lock_scope_falls_back_to_live_sql_owner_scan(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638" monkeypatch.setattr(adapter_1c_server, "metadata_form_owner_cache_lookup", lambda *args, **kwargs: None) monkeypatch.setattr(adapter_1c_server, "list_objects", lambda kind, **kwargs: {"status": "ok", "objects": [{"kind": "Document", "name": "тл_Планировщик", "guid": "owner-guid"}] if kind == "Document" else []}) monkeypatch.setattr(adapter_1c_server, "metadata_object_related", lambda payload: {"status": "ok", "related": [{"category": "Form", "identity": {"guid": guid, "name": "ФормаДокументаНовая"}}]}) monkeypatch.setattr(adapter_1c_server, "get_object", lambda kind, name, **kwargs: {"status": "ok", "object": {"kind": kind, "name": name, "guid": "owner-guid"}}) monkeypatch.setattr(adapter_1c_server, "metadata_object_forms", lambda payload: {"status": "ok", "forms": [{"guid": guid, "name": "ФормаДокументаНовая"}]}) result = adapter_1c_server.call_method("repository.lock.plan", {"base_id": "base", "object": f"Form.{guid}"}) assert result["status"] == "ready" assert result["lock_objects"] == ["Document.тл_Планировщик.Form.ФормаДокументаНовая"] def test_missing_owned_form_is_rejected_by_lock_plan(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) monkeypatch.setattr(adapter_1c_server, "get_object", lambda kind, name, **kwargs: {"status": "ok", "object": {"kind": kind, "name": name, "guid": "owner-guid"}}) monkeypatch.setattr(adapter_1c_server, "metadata_object_forms", lambda payload: {"status": "ok", "forms": []}) result = adapter_1c_server.call_method("repository.lock.plan", {"base_id": "base", "object": "Document.тл_Планировщик.Form.НетТакой"}) assert result["status"] == "not_found" assert result["error"] == "repository_form_not_found_in_sql" def test_structural_lock_plan_requires_confirmation() -> None: result = repository_control.lock_plan({"operation": "delete", "object": "Справочник.Склады"}) assert result["status"] == "needs_confirmation" def test_write_gate_requires_adapter_lock_for_configured_base(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) assert repository_control.write_gate({"base_id": "base"})["status"] == "needs_repository_lock" def test_adapter_dispatch_exposes_repository_status(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path, backend="direct") monkeypatch.setenv("ONEC_ADAPTER_ENABLE_EXTERNAL_1C", "true") result = adapter_1c_server.call_method("repository.status", {"base_id": "base"}) assert result["status"] == "configured" assert result["repository"]["backend"] == "direct" def test_repository_confirmation_help_exposes_machine_readable_contract() -> None: result = adapter_1c_server.call_method("help.methods", {"method": "repository.lock.confirm"}) schema = result["methods"][0]["input_schema"] assert schema["required"] == ["base_id", "request_id", "user_confirmed_locked"] assert schema["example"]["user_confirmed_locked"] is True assert "lock_session_id" in schema["notes"][-1] def test_sql_only_repository_status_does_not_claim_repository_connection(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) monkeypatch.delenv("ONEC_ADAPTER_ENABLE_EXTERNAL_1C", raising=False) result = repository_control.status({"base_id": "base", "probe": True}) assert result["status"] == "manual_confirmation_required" assert result["connected"] is False assert result["available"] is None assert result["sql_lock_evidence"]["native_lock_state"] == "unknown" assert result["sql_lock_evidence"]["excluded_sources"]["_ConfigChngR"] == "exchange_plan_change_registration" def test_repository_policy_is_selected_per_base_or_extension_guid(monkeypatch, tmp_path: Path) -> None: configured_layers(monkeypatch, tmp_path) base = repository_control.write_gate({"base_id": "base"}) no_repository = repository_control.write_gate({"base_id": "base", "extension_guid": "11111111-2222-3333-4444-555555555555"}) extension = repository_control.write_gate({"base_id": "base", "extension_guid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}) assert base["status"] == "needs_repository_lock" assert base["layer_id"] == "base" assert no_repository == {"required": False, "allowed": True, "status": "repository_not_connected", "layer_id": "extension:11111111-2222-3333-4444-555555555555"} assert extension["status"] == "needs_repository_lock" assert extension["layer_id"] == "extension:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" missing = repository_control.write_gate({"base_id": "base", "extension_guid": "99999999-2222-3333-4444-555555555555"}) assert missing["status"] == "blocked_repository_configuration" assert missing["problem"]["status"] == "layer_not_configured" def test_repository_connection_state_is_per_resolved_configuration_layer(monkeypatch, tmp_path: Path) -> None: configured_layers(monkeypatch, tmp_path) values = json.loads(__import__("os").environ["ONEC_SQL_BASES_JSON"]) layers = values["base"]["development_layers"] layers["base"]["repository"].update({"mode": "automatic", "connection_state": "unavailable", "repository_user": "repo-user", "runner": {"kind": "http", "url": "http://runner.invalid"}}) layers["extension:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"]["repository"]["connection_state"] = "unknown" monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(values)) base = repository_control.lock_plan({"base_id": "base", "object": "ОбщийМодуль.Тест"}) assert base["workflow"] == "manual" assert base["status"] == "ready" assert repository_control.status({"base_id": "base"})["status"] == "manual_confirmation_required" extension = repository_control.write_gate({ "base_id": "base", "target": {"origin": {"extension": {"guid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}}}, }) assert extension["layer_id"] == "extension:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" assert extension["status"] == "blocked_repository_connection_unknown" def test_manual_confirmation_uses_repository_user_saved_for_the_layer(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) values = json.loads(__import__("os").environ["ONEC_SQL_BASES_JSON"]) values["base"]["repository"].update({"lock_mode": "manual", "connection_state": "unavailable"}) monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(values)) request = repository_control.create_lock_request({"base_id": "base", "object": "Справочник.Товары"}) assert request["repository_user"] == "repo-user" assert request["repository_user_source"] == "layer_connection_setting" confirmed_without_repeating_user = repository_control.confirm_manual_lock({ "base_id": "base", "request_id": request["request_id"], "user_confirmed_locked": True, }) assert confirmed_without_repeating_user["status"] == "manual_confirmed" assert confirmed_without_repeating_user["repository_user"] == "repo-user" request = repository_control.create_lock_request({"base_id": "base", "object": "Справочник.Товары"}) mismatch = repository_control.confirm_manual_lock({"base_id": "base", "request_id": request["request_id"], "user_confirmed_locked": True, "confirmed_repository_user": "other"}) assert mismatch["error"] == "repository_user_mismatch" confirmed = repository_control.confirm_manual_lock({"base_id": "base", "request_id": request["request_id"], "user_confirmed_locked": True, "confirmed_repository_user": "repo-user"}) assert confirmed["repository_user"] == "repo-user" def test_repository_confirmation_cannot_cross_extension_layers(monkeypatch, tmp_path: Path) -> None: configured_layers(monkeypatch, tmp_path) guid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" request = repository_control.create_lock_request({"base_id": "base", "extension_guid": guid, "object": "ОбщийМодуль.МодульРасширения"}) confirmed = repository_control.confirm_manual_lock({"base_id": "base", "extension_guid": guid, "request_id": request["request_id"], "user_confirmed_locked": True, "confirmed_repository_user": "operator"}) assert repository_control.write_gate({"base_id": "base", "extension_guid": guid, "lock_session_id": confirmed["lock_session_id"], "repository_object": "ОбщийМодуль.МодульРасширения"})["allowed"] is True mismatch = repository_control.write_gate({"base_id": "base", "lock_session_id": confirmed["lock_session_id"], "repository_object": "ОбщийМодуль.МодульРасширения"}) assert mismatch["status"] == "blocked_repository_lock_session" def test_support_gate_uses_exact_target_scope_independently_from_repository_scope(monkeypatch, tmp_path: Path) -> None: configured_layers(monkeypatch, tmp_path) parent = repository_control.support_gate({"base_id": "base", "canonical_path": "Документ.Заказ"}) form = repository_control.support_gate({"base_id": "base", "canonical_path": "Документ.Заказ.Форма.ФормаДокумента"}) attribute = repository_control.support_gate({"base_id": "base", "canonical_path": "Документ.Заказ.Реквизит.Комментарий"}) extension = repository_control.support_gate({"base_id": "base", "extension_guid": "11111111-2222-3333-4444-555555555555", "canonical_path": "ОбщийМодуль.Тест"}) assert parent["allowed"] is False assert form["allowed"] is True assert form["matched_scope"] == "Документ.Заказ.Форма.ФормаДокумента" assert attribute["allowed"] is False assert extension["status"] == "not_on_support" def test_write_preflight_reports_repository_lock_requirement(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) monkeypatch.setattr( adapter_1c_server, "metadata_write_plan", lambda payload: { "allowed": True, "status": "ready", "path_resolution": {}, "route": {"apply_method": "metadata.module.write_apply", "write_surface": "saved_state"}, "required_guards": [], "problems": [], }, ) monkeypatch.setattr(adapter_1c_server, "metadata_write_preflight_saved_target", lambda payload, plan: {}) result = adapter_1c_server.metadata_write_preflight({"base_id": "base"}) assert result["allowed"] is False assert result["status"] == "needs_repository_lock" assert result["repository"]["backend"] == "karman_bridge" def test_write_preflight_uses_resolved_origin_layer_not_a_client_selector(monkeypatch, tmp_path: Path) -> None: configured_layers(monkeypatch, tmp_path) values = json.loads(__import__("os").environ["ONEC_SQL_BASES_JSON"]) values["base"]["development_layers"]["extension:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"]["repository"]["connection_state"] = "unknown" monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(values)) monkeypatch.setattr( adapter_1c_server, "metadata_write_plan", lambda payload: { "allowed": True, "status": "ready", "path_resolution": {}, "target": {"origin": {"extension": {"guid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}}}, "route": {}, "required_guards": [], "problems": [], }, ) monkeypatch.setattr(adapter_1c_server, "metadata_write_preflight_saved_target", lambda payload, plan: {}) monkeypatch.setattr(adapter_1c_server, "live_sql_support_gate", lambda payload: {"allowed": True, "status": "ok"}) result = adapter_1c_server.metadata_write_preflight({"base_id": "base"}) assert result["repository"]["layer_id"] == "extension:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" assert result["status"] == "blocked_repository_connection_unknown" def test_form_module_write_gate_context_is_resolved_from_live_sql(monkeypatch) -> None: form_guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638" reference = "Document.тл_Планировщик.Form.ФормаДокументаНовая" monkeypatch.setattr(adapter_1c_server, "repository_resolve_form_guid_sql", lambda **kwargs: ({ "resolved_object": reference, "form_guid": form_guid, "owner": {"kind": "Document", "name": "тл_Планировщик", "guid": "owner-guid"}, }, None)) context = adapter_1c_server.resolve_write_gate_context({ "base_id": "neft", "module_id": f"Config:{form_guid}.0", "repository_object": "Document.Другая.Форма.Другая", }) assert context["repository_object"] == reference assert context["support_object_guid"] == form_guid assert context["owner_resolution"]["layer_id"] == "base" def test_confirm_write_context_can_be_forwarded_under_repository_lock() -> None: context, error = adapter_1c_server.normalize_repository_write_context({ "base_id": "neft", "repository_lock": { "lock_session_id": "rlock-test", "repository_object": "Document.тл_Планировщик.Form.ФормаДокументаНовая", "layer_id": "base", }, }, "metadata.write.preflight") assert error is None assert context["lock_session_id"] == "rlock-test" assert context["repository_object"] == "Document.тл_Планировщик.Form.ФормаДокументаНовая" assert context["layer_id"] == "base" _context, conflict = adapter_1c_server.normalize_repository_write_context({ "lock_session_id": "rlock-one", "write_context": {"lock_session_id": "rlock-two"}, }, "code.write") assert conflict["error"] == "repository_lock_context_conflict" def test_preflight_uses_form_module_context_for_both_gates(monkeypatch) -> None: form_guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638" reference = "Document.тл_Планировщик.Form.ФормаДокументаНовая" monkeypatch.setattr(adapter_1c_server, "repository_resolve_form_guid_sql", lambda **kwargs: ({ "resolved_object": reference, "form_guid": form_guid, "owner": {"kind": "Document", "name": "тл_Планировщик"}, }, None)) monkeypatch.setattr(adapter_1c_server, "metadata_write_plan", lambda payload: { "allowed": True, "status": "ready", "path_resolution": {}, "target": {}, "route": {}, "required_guards": [], "problems": [], }) monkeypatch.setattr(adapter_1c_server, "metadata_write_preflight_saved_target", lambda payload, plan: {}) captured: dict[str, dict] = {} def fake_repository_gate(payload): captured["repository"] = dict(payload) return {"allowed": True, "status": "ready"} def fake_support_gate(payload): captured["support"] = dict(payload) return {"allowed": True, "status": "ready"} monkeypatch.setattr(repository_control, "write_gate", fake_repository_gate) monkeypatch.setattr(adapter_1c_server, "live_sql_support_gate", fake_support_gate) result = adapter_1c_server.metadata_write_preflight({ "base_id": "neft", "module_id": f"Config:{form_guid}.0", "repository_lock": {"lock_session_id": "rlock-test", "repository_object": reference, "layer_id": "base"}, }) assert result["owner_resolution"]["repository_object"] == reference assert captured["repository"]["repository_object"] == reference assert captured["repository"]["lock_session_id"] == "rlock-test" assert captured["support"]["support_object_guid"] == form_guid def test_form_module_preflight_returns_concrete_saved_state_prepare_plan(monkeypatch) -> None: form_guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638" monkeypatch.setattr(adapter_1c_server, "repository_resolve_form_guid_sql", lambda **kwargs: ({ "resolved_object": "Document.тл_Планировщик.Form.ФормаДокументаНовая", "form_guid": form_guid, "owner": {"kind": "Document", "name": "тл_Планировщик"}, }, None)) monkeypatch.setattr(adapter_1c_server, "metadata_write_plan", lambda payload: { "allowed": False, "status": "needs_origin", "path_resolution": {}, "target": {}, "route": {}, "required_guards": [], "problems": [{"code": "origin_lookup_required"}], }) monkeypatch.setattr(adapter_1c_server, "metadata_write_preflight_saved_target", lambda payload, plan: {}) monkeypatch.setattr(adapter_1c_server, "metadata_write_resolve_module_target", lambda payload, target, mode: ({ "error": "saved_state_prepare_required", "prepared_module_ref": f"ConfigSave:{form_guid}.0", "next_resolution": { "method": "metadata.saved_state.prepare", "params": { "base_id": "neft", "layer": "base_saved_state", "ref": "Document.тл_Планировщик", "mode": "plan", }, }, "prepare_plan": {"status": "plan_ready"}, }, None)) monkeypatch.setattr(repository_control, "write_gate", lambda payload: {"allowed": True, "status": "ready"}) monkeypatch.setattr(adapter_1c_server, "live_sql_support_gate", lambda payload: {"allowed": True, "status": "ready"}) result = adapter_1c_server.metadata_write_preflight({"base_id": "neft", "module_id": f"Config:{form_guid}.0"}) assert result["status"] == "needs_prepare" assert result["saved_state"]["prepare_plan"]["status"] == "plan_ready" assert result["route"]["auto_resolves_saved_state"] is True assert result["route"]["next_resolution"]["method"] == "metadata.saved_state.prepare" assert result["route"]["next_resolution"]["params"]["ref"] == "Document.тл_Планировщик" assert "payload" not in result["route"]["next_resolution"] def test_form_module_context_selects_resolved_extension_layer(monkeypatch) -> None: form_guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638" extension_guid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" monkeypatch.setattr(adapter_1c_server, "repository_resolve_form_guid_sql", lambda **kwargs: ({ "resolved_object": "Document.Заказ.Form.Основная", "form_guid": form_guid, "owner": {"kind": "Document", "name": "Заказ"}, "extension_guid": extension_guid, }, None)) context = adapter_1c_server.resolve_write_gate_context({"base_id": "base", "module_id": f"Config:{form_guid}.0"}) assert context["extension_guid"] == extension_guid assert repository_control.development_layer_id(context) == f"extension:{extension_guid}" assert context["target"]["origin"]["extension"]["guid"] == extension_guid def test_code_write_carries_form_module_gate_context_into_metadata_write(monkeypatch) -> None: form_guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638" reference = "Document.тл_Планировщик.Form.ФормаДокументаНовая" monkeypatch.setattr(adapter_1c_server, "repository_resolve_form_guid_sql", lambda **kwargs: ({ "resolved_object": reference, "form_guid": form_guid, "owner": {"kind": "Document", "name": "тл_Планировщик"}, }, None)) seen: dict[str, object] = {} monkeypatch.setattr(adapter_1c_server, "metadata_write", lambda payload: (seen.update(payload) or {"status": "blocked"})) result = adapter_1c_server.code_write({"base_id": "neft", "module_id": f"Config:{form_guid}.0", "routine_text": "Процедура Тест()\nКонецПроцедуры"}) assert result["owner_resolution"]["repository_object"] == reference assert seen["repository_object"] == reference assert seen["support_object_guid"] == form_guid def test_saved_state_prepare_keeps_form_gate_context(monkeypatch) -> None: form_guid = "f5f217c8-ac69-4e23-966b-d8ec0c665638" reference = "Document.тл_Планировщик.Form.ФормаДокументаНовая" captured: dict[str, object] = {} monkeypatch.setattr(adapter_1c_server, "metadata_saved_state_prepare", lambda payload: ( captured.update(payload) or {"status": "verified"} )) resolved, error = adapter_1c_server.metadata_write_resolve_module_target({ "base_id": "neft", "module_id": f"Config:{form_guid}.0", "allow_sql_saved_state_apply": True, "lock_session_id": "rlock-test", "repository_object": reference, "support_object_guid": form_guid, }, {}, "apply") assert error is not None assert captured["lock_session_id"] == "rlock-test" assert captured["repository_object"] == reference assert captured["support_object_guid"] == form_guid assert resolved["module_ref"] == f"ConfigSave:{form_guid}.0" def test_non_form_module_id_does_not_receive_form_write_gate_context(monkeypatch) -> None: called = False def unexpected(**kwargs): nonlocal called called = True return None, {"status": "not_found"} monkeypatch.setattr(adapter_1c_server, "repository_resolve_form_guid_sql", unexpected) context = adapter_1c_server.resolve_write_gate_context({"base_id": "neft", "module_id": "Config:not-a-guid.0"}) assert "owner_resolution" not in context assert called is False def test_repository_layers_audit_reports_all_discovered_layers(monkeypatch, tmp_path: Path) -> None: configured_layers(monkeypatch, tmp_path) monkeypatch.setattr(adapter_1c_server, "list_extensions", lambda payload: {"status": "ok", "extensions": [{"name": "Тест", "guid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}]}) monkeypatch.setattr(adapter_1c_server, "metadata_support_decode", lambda payload: {"status": "ok", "source": {"kind": "live_sql", "support_payload_present": False}, "suppliers": [{"general_mode": "editable"}], "diagnostics": {"source_discovery": "test"}, "object_support": {"status": "not_supported"}}) result = adapter_1c_server.call_method("repository.layers.audit", {"base_id": "base"}) assert result["status"] == "ok" assert [layer["layer_id"] for layer in result["layers"]] == ["base", "extension:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"] assert result["layers"][1]["safe_next_write_action"] == "request_and_confirm_repository_capture" assert result["layers"][0]["support"]["general_modes"] == ["editable"] def test_direct_apply_method_is_repository_gated(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) result = adapter_1c_server.metadata_module_write_apply({"base_id": "base", "execution_mode": "apply"}) assert result["status"] == "blocked" assert result["error"] == "needs_repository_lock" def test_lock_and_commit_use_configured_designer_endpoint(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) monkeypatch.setenv("ONEC_ADAPTER_ENABLE_EXTERNAL_1C", "true") calls = [] def fake_run(config, operation, timeout_seconds): calls.append((dict(config), list(operation), timeout_seconds)) return {"status": "ok", "exit_code": 0, "duration_ms": 1, "output": ""} monkeypatch.setattr(repository_control, "_run_designer", fake_run) locked = repository_control.lock({"base_id": "base", "object": "Справочник.Товары", "allow_repository_lock": True}) assert locked["status"] == "acquired" assert calls[0][0]["endpoint"] == "tcp://configured.example:15420/configured-repository" assert calls[0][1][0] == "/ConfigurationRepositoryLock" assert repository_control.write_gate({"base_id": "base", "lock_session_id": locked["lock_session_id"], "repository_object": "Справочник.Товары"})["allowed"] is True mismatch = repository_control.write_gate({"base_id": "base", "lock_session_id": locked["lock_session_id"], "repository_object": "Справочник.Другой"}) assert mismatch["status"] == "blocked_repository_scope_mismatch" blocked = repository_control.commit({"lock_session_id": locked["lock_session_id"], "comment": "test"}) assert blocked["error"] == "explicit_repository_commit_required" committed = repository_control.commit({"lock_session_id": locked["lock_session_id"], "comment": "test", "allow_repository_commit": True}) assert committed["status"] == "committed" assert calls[1][1][0] == "/ConfigurationRepositoryCommit" def test_sql_only_mode_never_calls_external_repository_runner(monkeypatch, tmp_path: Path) -> None: configured_base(monkeypatch, tmp_path) monkeypatch.delenv("ONEC_ADAPTER_ENABLE_EXTERNAL_1C", raising=False) called = False def forbidden(*args, **kwargs): nonlocal called called = True return {"status": "ok"} monkeypatch.setattr(repository_control, "_run_designer", forbidden) result = repository_control.lock({"base_id": "base", "object": "Справочник.Товары", "allow_repository_lock": True}) assert result["status"] == "manual_action_required" assert called is False