from pathlib import Path
from uuid import uuid4
import time
import zipfile
from fastapi.testclient import TestClient
from api_server import main
from api_server.main import app
from one_c_normalizer import ConfigurationRoot, MetadataGroup, MetadataObject, Module, NormalizedProject
def create_authoring_session(client: TestClient, project_id: str, task_id: str, session_id: str, user_id: str = "dev.ivan") -> None:
user = client.post("/collaboration/users", json={"user_id": user_id, "display_name": user_id})
assert user.status_code == 200
grant = client.post(f"/security/users/{user_id}/roles/developer")
assert grant.status_code == 200
task = client.post(
"/collaboration/tasks",
json={"task_id": task_id, "project_id": project_id, "title": f"Authoring {task_id}", "assignee_user_id": user_id},
)
assert task.status_code == 200
session = client.post(
"/collaboration/sessions",
json={"session": {"session_id": session_id, "task_id": task_id, "user_id": user_id}},
)
assert session.status_code == 200
def test_cors_allows_lan_panel_origin():
client = TestClient(app)
response = client.options(
"/health",
headers={
"Origin": "http://192.168.200.60:3000",
"Access-Control-Request-Method": "GET",
},
)
assert response.status_code == 200
assert response.headers["access-control-allow-origin"] == "http://192.168.200.60:3000"
def test_metadata_catalog_exposes_1c_tree_structure():
client = TestClient(app)
response = client.get("/metadata/catalog")
assert response.status_code == 200
payload = response.json()
assert "8.3" in payload["platform_family"]
assert "Общие модули" in payload["common_branch_children"]
document = next(item for item in payload["types"] if item["code"] == "DOCUMENT")
common_module = next(item for item in payload["types"] if item["code"] == "COMMON_MODULE")
accumulation_register = next(item for item in payload["types"] if item["code"] == "ACCUMULATION_REGISTER")
http_service = next(item for item in payload["types"] if item["code"] == "HTTP_SERVICE")
report = next(item for item in payload["types"] if item["code"] == "REPORT")
catalog = next(item for item in payload["types"] if item["code"] == "CATALOG")
url_template = next(item for item in payload["child_object_types"] if item["code"] == "URL_TEMPLATE")
data_composition_schema = next(item for item in payload["child_object_types"] if item["code"] == "DATA_COMPOSITION_SCHEMA")
assert document["tree_branch"] == "Документы"
assert "Реквизиты" in document["child_groups"]
assert "Табличные части" in document["child_groups"]
assert "Движения" in document["child_groups"]
assert "Проведение" in document["properties"]
assert "Открыть модуль объекта" in document["context_actions"]
assert common_module["tree_branch"] == "Общие модули"
assert "Экспортные методы" in common_module["child_groups"]
assert "Клиент" in common_module["properties"]
assert "Найти вызовы" in common_module["context_actions"]
assert accumulation_register["module_kinds"] == ["Модуль набора записей", "Модуль менеджера"]
assert "Ресурсы" in accumulation_register["child_groups"]
assert "Показать чтение/запись" in accumulation_register["context_actions"]
assert http_service["child_groups"] == ["Шаблоны URL", "Методы", "Модуль"]
assert "HTTP-методы" in http_service["properties"]
assert "Показать URL-шаблоны" in http_service["context_actions"]
assert "СКД" in report["child_groups"]
assert "Табличные документы" in report["child_groups"]
assert "Варианты отчета" in report["child_groups"]
assert "Настройки" in report["child_groups"]
assert "Справочник" in catalog["description"]
assert "HTTP-сервис" in http_service["description"]
assert "Шаблоны URL" in url_template["parent_groups"]
assert data_composition_schema["parent_groups"] == ["СКД"]
def test_html5_server_rendered_project_editor(tmp_path: Path):
client = TestClient(app)
project_id = f"html5-editor-{uuid4()}"
module = tmp_path / "demo_module.bsl"
module.write_text(
"Процедура Проверить()\n"
" Сообщить(\"HTML5\");\n"
"КонецПроцедуры\n",
encoding="utf-8",
)
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
assert indexed.status_code == 200
index = client.get("/html5")
assert index.status_code == 200
assert "text/html" in index.headers["content-type"]
assert 'data-html5-page="projects"' in index.text
assert project_id in index.text
assert "__next" not in index.text
editor = client.get(f"/html5/projects/{project_id}/editor", params={"q": "Проверить"})
assert editor.status_code == 200
assert 'data-html5-page="editor"' in editor.text
assert "data-html5-editor" in editor.text
assert "data-html5-symbol-results" in editor.text
assert "data-html5-symbol-detail" in editor.text
assert "data-html5-project-report" in editor.text
assert f'hx-get="/html5/projects/{project_id}/report"' in editor.text
assert "data-html5-review" in editor.text
assert f'hx-get="/html5/projects/{project_id}/review"' in editor.text
assert "data-html5-authoring-preview" in editor.text
assert "data-html5-authoring-preview-form" in editor.text
assert f'hx-post="/html5/projects/{project_id}/authoring/completion-preview"' in editor.text
assert "data-html5-authoring-diff-form" in editor.text
assert f'hx-post="/html5/projects/{project_id}/authoring/semantic-diff-preview"' in editor.text
assert "data-html5-metadata-authoring" in editor.text
assert "data-html5-metadata-preview-form" in editor.text
assert f'hx-post="/html5/projects/{project_id}/authoring/metadata-object-preview"' in editor.text
assert "data-html5-authoring-changes" in editor.text
assert f'hx-get="/html5/projects/{project_id}/authoring/changes"' in editor.text
assert 'hx-get="/html5/projects/' in editor.text
assert 'hx-target="[data-html5-symbol-results]"' in editor.text
assert 'hx-target="[data-html5-symbol-detail]"' in editor.text
assert 'hx-target="[data-html5-source]"' in editor.text
assert 'hx-swap="outerHTML"' in editor.text
assert "hx-ext=\"sse\"" in editor.text
assert f"sse-connect=\"/html5/projects/{project_id}/events\"" in editor.text
assert "htmx.org" in editor.text
assert "htmx-ext-sse" in editor.text
assert "client-js: htmx+sse only" in editor.text
assert "Проверить" in editor.text
assert "__next" not in editor.text
with client.stream("GET", f"/html5/projects/{project_id}/events?once=1") as events:
first_chunk = next(events.iter_text())
assert "event: status" in first_chunk
assert "data:" in first_chunk
assert project_id in first_chunk
symbols = client.get(f"/html5/projects/{project_id}/symbols", params={"q": "Проверить"})
assert symbols.status_code == 200
assert "text/html" in symbols.headers["content-type"]
assert 'data-html5-symbol' in symbols.text
assert 'data-html5-lineage-id' in symbols.text
assert 'hx-target="[data-html5-symbol-detail]"' in symbols.text
assert "Проверить" in symbols.text
assert "
""",
encoding="utf-8",
)
client = TestClient(app)
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
assert indexed.status_code == 200
editor = client.get(f"/html5/projects/{project_id}/editor")
assert editor.status_code == 200
assert "data-html5-object-context" in editor.text
assert f'hx-get="/html5/projects/{project_id}/objects/context/%D0%94%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82.%D0%97%D0%B0%D0%BA%D0%B0%D0%B7%D0%9F%D0%BE%D0%BA%D1%83%D0%BF%D0%B0%D1%82%D0%B5%D0%BB%D1%8F"' in editor.text
assert 'hx-target="[data-html5-object-context]"' in editor.text
context = client.get(f"/html5/projects/{project_id}/objects/context/Документ.ЗаказПокупателя")
assert context.status_code == 200
assert "text/html" in context.headers["content-type"]
assert "data-html5-object-context" in context.text
assert "Документ.ЗаказПокупателя" in context.text
assert "Контрагент" in context.text
assert "Товары" in context.text
assert "= 5
assert payload["form_count"] >= 2
assert payload["role_count"] >= 1
setup = client.get(f"/projects/{project_id}/setup")
assert setup.status_code == 200
assert setup.json()["status"] == "INDEXED"
assert setup.json()["last_import"]["source_path"]
assert setup.json()["import_history"][0]["status"] == "mock_indexed"
history = client.get(f"/projects/{project_id}/imports/history")
assert history.status_code == 200
assert history.json()[0]["source"] == "XML_DUMP"
tree = client.get(f"/projects/{project_id}/metadata/tree")
assert tree.status_code == 200
assert tree.json()["root"]["kind"] == "PROJECT"
reindexed = client.post(f"/projects/{project_id}/reindex")
assert reindexed.status_code == 200
assert reindexed.json()["status"] == "reindexed"
def test_windows_agent_import_job_protocol_applies_server_result(tmp_path: Path):
(tmp_path / "metadata.xml").write_text(
"""
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"agent-import-{uuid4()}"
agent_id = f"win-agent-{uuid4()}"
client.post("/agent/heartbeat", json={"agent_id": agent_id, "hostname": "test-host"})
created = client.post(
f"/projects/{project_id}/imports/EDT_PROJECT/agent-jobs",
json={
"agent_id": agent_id,
"source": "EDT_PROJECT",
"local_path": r"D:\edt\upo",
"bin_path": r"C:\Program Files\1cv8\8.3.24.0000\bin\1cv8.exe",
"mode": "FULL_REPLACE",
},
)
assert created.status_code == 200
job_id = created.json()["job_id"]
assert created.json()["status"] == "QUEUED"
claimed = client.get("/agent/jobs/next", params={"agent_id": agent_id})
assert claimed.status_code == 200
assert claimed.json()["job_id"] == job_id
assert claimed.json()["status"] == "RUNNING"
assert claimed.json()["local_path"] == r"D:\edt\upo"
completed = client.post(
f"/agent/jobs/{job_id}/result",
json={
"status": "SUCCEEDED",
"server_path": str(tmp_path),
"logs": ["EDT exported and uploaded."],
},
)
assert completed.status_code == 200
deadline = time.monotonic() + 10
payload = completed.json()
while time.monotonic() < deadline and payload["status"] == "RUNNING":
time.sleep(0.05)
jobs = client.get(f"/projects/{project_id}/imports/agent-jobs")
assert jobs.status_code == 200
payload = next(item for item in jobs.json() if item["job_id"] == job_id)
assert payload["status"] == "SUCCEEDED"
assert payload["import_summary"]["status"] == "structure_indexed"
assert payload["import_summary"]["object_count"] == 1
setup = client.get(f"/projects/{project_id}/setup")
assert setup.status_code == 200
assert setup.json()["last_import"]["source"] == "EDT_PROJECT"
def test_windows_agent_import_job_accepts_uploaded_zip(tmp_path: Path):
payload_root = tmp_path / "payload"
payload_root.mkdir()
(payload_root / "metadata.xml").write_text(
"""
""",
encoding="utf-8",
)
archive_path = tmp_path / "edt.zip"
with zipfile.ZipFile(archive_path, "w") as archive:
archive.write(payload_root / "metadata.xml", "metadata.xml")
client = TestClient(app)
project_id = f"agent-upload-{uuid4()}"
agent_id = f"win-agent-{uuid4()}"
client.post("/agent/heartbeat", json={"agent_id": agent_id, "hostname": "test-host"})
created = client.post(
f"/projects/{project_id}/imports/EDT_PROJECT/agent-jobs",
json={"agent_id": agent_id, "source": "EDT_PROJECT", "local_path": r"D:\edt\upo", "mode": "FULL_REPLACE"},
)
assert created.status_code == 200
job_id = created.json()["job_id"]
uploaded = client.post(
f"/agent/jobs/{job_id}/upload",
params={"filename": "edt.zip"},
content=archive_path.read_bytes(),
headers={"Content-Type": "application/octet-stream"},
)
assert uploaded.status_code == 200
assert uploaded.json()["server_path"]
deadline = time.monotonic() + 10
payload = uploaded.json()
while time.monotonic() < deadline and payload["status"] == "RUNNING":
time.sleep(0.05)
jobs = client.get(f"/projects/{project_id}/imports/agent-jobs")
assert jobs.status_code == 200
payload = next(item for item in jobs.json() if item["job_id"] == job_id)
assert payload["status"] == "SUCCEEDED"
assert payload["import_summary"]["object_count"] == 1
def test_windows_agent_cf_export_job_accepts_infobase_settings_without_local_path():
client = TestClient(app)
project_id = f"agent-cf-{uuid4()}"
agent_id = f"win-agent-{uuid4()}"
client.post("/agent/heartbeat", json={"agent_id": agent_id, "hostname": "test-host"})
created = client.post(
f"/projects/{project_id}/imports/CF_FILE/agent-jobs",
json={
"agent_id": agent_id,
"source": "CF_FILE",
"mode": "FULL_REPLACE",
"metadata": {
"one_c_server": "192.168.200.95",
"one_c_infobase": "upo",
"one_c_user": "import-user",
"one_c_password": "secret",
},
},
)
assert created.status_code == 200
payload = created.json()
assert payload["source"] == "CF_FILE"
assert payload["local_path"] is None
assert payload["metadata"]["one_c_infobase"] == "upo"
def test_windows_agent_browse_request_protocol():
client = TestClient(app)
agent_id = f"win-agent-{uuid4()}"
created = client.post("/agent/browse-requests", json={"agent_id": agent_id, "path": r"D:\EDT"})
assert created.status_code == 200
request_id = created.json()["request_id"]
assert created.json()["status"] == "QUEUED"
claimed = client.get("/agent/browse/next", params={"agent_id": agent_id})
assert claimed.status_code == 200
assert claimed.json()["request_id"] == request_id
assert claimed.json()["status"] == "RUNNING"
completed = client.post(
f"/agent/browse/{request_id}/result",
json={
"status": "SUCCEEDED",
"parent_path": r"D:\\",
"entries": [{"name": "UPO", "path": r"D:\EDT\UPO", "is_directory": True}],
},
)
assert completed.status_code == 200
assert completed.json()["status"] == "SUCCEEDED"
assert completed.json()["entries"][0]["path"] == r"D:\EDT\UPO"
def test_windows_agent_heartbeat_status():
client = TestClient(app)
agent_id = f"win-agent-{uuid4()}"
offline = client.get(f"/agent/status/{agent_id}")
assert offline.status_code == 200
assert offline.json()["status"] == "offline"
heartbeat = client.post(
"/agent/heartbeat",
json={
"agent_id": agent_id,
"host": "workstation-1",
"user": "svc-sfera",
"version": "dev",
"network_roots": [r"\\server\share"],
},
)
assert heartbeat.status_code == 200
assert heartbeat.json()["status"] == "online"
online = client.get(f"/agent/status/{agent_id}")
assert online.status_code == 200
assert online.json()["status"] == "online"
assert online.json()["host"] == "workstation-1"
assert online.json()["network_roots"] == [r"\\server\share"]
def test_server_browse_lists_directories(tmp_path: Path):
(tmp_path / "edt").mkdir()
(tmp_path / "file.txt").write_text("not a dir", encoding="utf-8")
client = TestClient(app)
response = client.get("/server/browse", params={"path": str(tmp_path)})
assert response.status_code == 200
payload = response.json()
assert payload["path"]
assert payload["parent_path"]
assert payload["entries"] == [{"name": "edt", "path": (tmp_path / "edt").as_posix(), "is_directory": True}]
def test_server_smb_browse_validates_unc_path():
client = TestClient(app)
response = client.post("/server/smb/browse", json={"path": "192.168.200.200", "username": "user", "password": "secret"})
assert response.status_code == 200
assert "UNC путь" in response.json()["error"]
def test_project_setup_recovers_indexed_status_from_stored_snapshot(tmp_path: Path):
(tmp_path / "metadata.xml").write_text(
"""
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"setup-recover-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/XML_DUMP",
json={"source": "XML_DUMP", "path": str(tmp_path)},
)
assert imported.status_code == 200
main._snapshots.pop(project_id, None)
main._project_setup.pop(project_id, None)
recovered = client.get(f"/projects/{project_id}/setup")
assert recovered.status_code == 200
payload = recovered.json()
assert payload["status"] == "INDEXED"
assert payload["last_import"]["source"] == "XML_DUMP"
def test_project_create_and_delete_lifecycle():
client = TestClient(app)
project_id = f"lifecycle-{uuid4()}"
created = client.post("/projects", json={"project_id": project_id, "name": "Lifecycle"})
assert created.status_code == 200
assert created.json()["project_id"] == project_id
assert created.json()["settings"]["name"] == "Lifecycle"
duplicate = client.post("/projects", json={"project_id": project_id})
assert duplicate.status_code == 409
wrong_confirmation = client.request("DELETE", f"/projects/{project_id}", json={"confirmation": "wrong"})
assert wrong_confirmation.status_code == 400
deleted = client.request("DELETE", f"/projects/{project_id}", json={"confirmation": project_id})
assert deleted.status_code == 200
assert deleted.json()["project_id"] == project_id
def test_project_settings_persist_wizard_sections():
client = TestClient(app)
project_id = f"setup-sections-{uuid4()}"
settings = client.post(
f"/projects/{project_id}/settings",
json={
"name": "Wizard Sections",
"configuration_source": "EDT local project",
"structure_source": "EDT_PROJECT",
"platform_version": "8.5.1.1150",
"compatibility_mode": "8.3",
"extensions": ["UPO_Ext"],
"environments": {
"dev": {
"url": "http://192.168.200.95/upo/ru_RU/",
"infobase": 'Srvr="192.168.200.95";Ref="upo";',
"runtime": "mock",
},
"test": {"runtime": "remote_worker"},
},
"agent": {"mode": "mock", "endpoint": "https://agent.local/snapshot", "allow_snapshots": True},
"privacy_mode": "LOCAL_ONLY",
"knowledge_sources": ["wiki", "incidents"],
"task_session_policy": {
"mode": "required",
"default_task_id": "task.default",
"session_ttl": "8h",
"audit_privacy": True,
},
},
)
assert settings.status_code == 200
payload = settings.json()["settings"]
assert payload["environments"]["dev"]["runtime"] == "mock"
assert payload["agent"]["allow_snapshots"] is True
assert payload["knowledge_sources"] == ["wiki", "incidents"]
assert payload["task_session_policy"]["default_task_id"] == "task.default"
def test_import_source_check_reports_preflight_requirements(tmp_path: Path):
client = TestClient(app)
project_id = f"source-check-{uuid4()}"
xml_file = tmp_path / "metadata.xml"
xml_file.write_text("", encoding="utf-8")
xml_check = client.post(
f"/projects/{project_id}/imports/XML_DUMP/check",
json={"source": "XML_DUMP", "path": str(xml_file)},
)
assert xml_check.status_code == 200
assert xml_check.json()["ready"] is True
assert any(check["code"] == "path" and check["status"] == "OK" for check in xml_check.json()["checks"])
live_check = client.post(
f"/projects/{project_id}/imports/LIVE_INFOBASE/check",
json={"source": "LIVE_INFOBASE"},
)
assert live_check.status_code == 200
assert live_check.json()["ready"] is False
assert any(check["code"] == "credentials" and check["status"] == "BLOCKED" for check in live_check.json()["checks"])
def test_runtime_required_import_uses_api_mock_when_adapter_unavailable(monkeypatch):
monkeypatch.setenv("RUNTIME_ADAPTER_MODE", "mock")
monkeypatch.setenv("RUNTIME_ADAPTER_URL", "http://127.0.0.1:1")
client = TestClient(app)
project_id = f"runtime-import-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/CF_FILE",
json={"source": "CF_FILE", "metadata": {"platform_version": "8.3.24"}},
)
assert imported.status_code == 200
payload = imported.json()
assert payload["status"] == "mock_indexed"
assert payload["runtime_mode"] == "mock"
assert payload["runtime_diagnostics"]
assert payload["snapshot"]["project_id"] == project_id
platform = client.get("/runtime/platform")
assert platform.status_code == 200
assert platform.json()["mode"] == "mock"
assert platform.json()["platform_found"] is False
def test_runtime_required_import_does_not_index_cf_file_directly(monkeypatch, tmp_path: Path):
monkeypatch.setenv("RUNTIME_ADAPTER_MODE", "mock")
monkeypatch.setenv("RUNTIME_ADAPTER_URL", "http://127.0.0.1:1")
cf_file = tmp_path / "demo.cf"
cf_file.write_text("not xml", encoding="utf-8")
client = TestClient(app)
project_id = f"cf-import-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/CF_FILE",
json={"source": "CF_FILE", "path": str(cf_file), "metadata": {"platform_version": "8.3.24"}},
)
assert imported.status_code == 200
payload = imported.json()
assert payload["status"] == "mock_indexed"
assert payload["source_path"] != cf_file.as_posix()
assert payload["normalized_summary"]["object_count"] >= 1
def test_import_supports_structure_only_indexing(tmp_path: Path):
(tmp_path / "metadata.xml").write_text(
"""
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"structure-only-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/EDT_PROJECT",
json={"source": "EDT_PROJECT", "path": str(tmp_path), "structure_only": True},
)
assert imported.status_code == 200
payload = imported.json()
assert payload["status"] == "structure_indexed"
assert payload["snapshot"]["project_id"] == project_id
assert payload["object_count"] >= 2
assert payload["normalized_summary"]["group_count"] >= 3
assert payload["normalized_summary"]["rights_count"] == 1
setup = client.get(f"/projects/{project_id}/setup")
assert setup.status_code == 200
assert setup.json()["status"] == "STRUCTURE_INDEXED"
assert "Запустите индексацию" in setup.json()["message"]
summary = client.get(f"/projects/{project_id}/normalized/summary")
assert summary.status_code == 200
assert summary.json()["rights_count"] == 1
quality = client.get(f"/projects/{project_id}/imports/quality")
assert quality.status_code == 200
quality_payload = quality.json()
assert quality_payload["project_id"] == project_id
assert quality_payload["score"] > 0
assert any(check["code"] == "rights" and check["passed"] for check in quality_payload["checks"])
normalized = client.get(f"/projects/{project_id}/normalized")
assert normalized.status_code == 200
groups = {group["name"] for group in normalized.json()["configuration"]["groups"]}
assert {"HTTP-сервисы", "Подсистемы", "Роли"}.issubset(groups)
detail = client.get(
f"/projects/{project_id}/normalized/object",
params={"qualified_name": "Роль.Менеджер"},
)
assert detail.status_code == 200
assert detail.json()["group_name"] == "Роли"
assert detail.json()["object"]["rights"][0]["target"] == "HTTPСервис.ПубличныйAPI"
tree = client.get(f"/projects/{project_id}/metadata/tree")
assert tree.status_code == 200
root = tree.json()["root"]
assert root["label"] == "Проект"
assert [item["label"] for item in root["children"][:4]] == [
"Основная конфигурация",
"Расширение: <Имя>",
"SFERA",
"Среды",
]
main_configuration = root["children"][0]
common = next(item for item in main_configuration["children"] if item["label"] == "Общие")
assert any(item["label"] == "HTTP-сервисы" for item in common["children"])
def test_import_full_replace_replaces_current_normalized_project(tmp_path: Path):
first = tmp_path / "first"
second = tmp_path / "second"
first.mkdir()
second.mkdir()
(first / "Контрагенты.mdo").write_text(
"""
Контрагенты
Контрагенты
""",
encoding="utf-8",
)
(second / "Номенклатура.mdo").write_text(
"""
Номенклатура
Номенклатура
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"full-replace-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/EDT_PROJECT",
json={"source": "EDT_PROJECT", "path": str(first), "mode": "FULL_REPLACE"},
)
assert imported.status_code == 200
assert imported.json()["mode"] == "FULL_REPLACE"
assert imported.json()["applied"] is True
replaced = client.post(
f"/projects/{project_id}/imports/EDT_PROJECT",
json={"source": "EDT_PROJECT", "path": str(second), "mode": "FULL_REPLACE"},
)
assert replaced.status_code == 200
assert replaced.json()["mode"] == "FULL_REPLACE"
assert replaced.json()["normalized_summary"]["object_count"] == 1
normalized = client.get(f"/projects/{project_id}/normalized")
assert normalized.status_code == 200
payload = normalized.json()
objects = [
item["qualified_name"]
for group in payload["configuration"]["groups"]
for item in group["objects"]
]
assert objects == ["Справочник.Номенклатура"]
def test_import_sync_preview_does_not_replace_current_project(tmp_path: Path):
current = tmp_path / "current"
incoming = tmp_path / "incoming"
current.mkdir()
incoming.mkdir()
(current / "Контрагенты.mdo").write_text(
"""
Контрагенты
""",
encoding="utf-8",
)
(incoming / "Номенклатура.mdo").write_text(
"""
Номенклатура
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"sync-preview-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/EDT_PROJECT",
json={"source": "EDT_PROJECT", "path": str(current)},
)
assert imported.status_code == 200
preview = client.post(
f"/projects/{project_id}/imports/EDT_PROJECT",
json={"source": "EDT_PROJECT", "path": str(incoming), "mode": "SYNC_PREVIEW"},
)
assert preview.status_code == 200
payload = preview.json()
assert payload["status"] == "sync_preview"
assert payload["mode"] == "SYNC_PREVIEW"
assert payload["applied"] is False
assert payload["sync_preview"]["added_count"] == 1
assert payload["sync_preview"]["removed_count"] == 1
assert {item["change_kind"] for item in payload["sync_preview"]["items"]} == {"ADD", "REMOVE"}
normalized = client.get(f"/projects/{project_id}/normalized")
assert normalized.status_code == 200
objects = [
item["qualified_name"]
for group in normalized.json()["configuration"]["groups"]
for item in group["objects"]
]
assert objects == ["Справочник.Контрагенты"]
def test_import_full_replace_keeps_valid_edt_objects_when_one_xml_is_broken(tmp_path: Path):
(tmp_path / "valid.mdo").write_text(
"""
Контрагенты
Контрагенты
""",
encoding="utf-8",
)
(tmp_path / "broken.mdo").write_text("
Контрагенты
""",
encoding="utf-8",
)
(module_dir / "ObjectModule.bsl").write_text(
"""
Процедура ПроверитьКонтрагента() Экспорт
КонецПроцедуры
""",
encoding="utf-8",
)
(module_dir / "ManagerModule.bsl").write_text(
"""
Процедура Создать() Экспорт
КонецПроцедуры
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"edt-modules-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/EDT_PROJECT",
json={"source": "EDT_PROJECT", "path": str(tmp_path), "mode": "FULL_REPLACE"},
)
assert imported.status_code == 200
assert imported.json()["normalized_summary"]["module_count"] == 2
detail = client.get(
f"/projects/{project_id}/normalized/object",
params={"qualified_name": "Справочник.Контрагенты"},
)
assert detail.status_code == 200
modules = detail.json()["object"]["modules"]
assert [module["module_kind"] for module in modules] == ["MANAGER_MODULE", "OBJECT_MODULE"]
assert all(module["attributes"]["source_hash"] for module in modules)
def test_import_edt_full_replace_keeps_tabular_section_columns_nested(tmp_path: Path):
(tmp_path / "ЗаказПокупателя.mdo").write_text(
"""
ЗаказПокупателя
Товары
Номенклатура
Количество
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"edt-tabular-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/EDT_PROJECT",
json={"source": "EDT_PROJECT", "path": str(tmp_path), "mode": "FULL_REPLACE"},
)
assert imported.status_code == 200
assert imported.json()["normalized_summary"]["attribute_count"] == 2
detail = client.get(
f"/projects/{project_id}/normalized/object",
params={"qualified_name": "Документ.ЗаказПокупателя"},
)
assert detail.status_code == 200
document = detail.json()["object"]
assert document["attributes"] == []
assert [child["name"] for child in document["tabular_sections"][0]["children"]] == ["Номенклатура", "Количество"]
def test_import_full_replace_preserves_configuration_root_metadata(tmp_path: Path):
(tmp_path / "configuration.xml").write_text(
"""
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"configuration-root-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/XML_DUMP",
json={"source": "XML_DUMP", "path": str(tmp_path), "mode": "FULL_REPLACE"},
)
assert imported.status_code == 200
assert imported.json()["normalized_summary"]["object_count"] == 1
normalized = client.get(f"/projects/{project_id}/normalized")
assert normalized.status_code == 200
configuration = normalized.json()["configuration"]
assert configuration["name"] == "УправлениеТорговлей"
assert configuration["metadata"]["synonym"] == "Управление торговлей"
assert configuration["metadata"]["platformVersion"] == "8.3.24"
def test_project_metadata_tree_uses_catalog_structure(tmp_path: Path):
(tmp_path / "metadata.xml").write_text(
"""
ОстаткиТоваров
РегистрНакопления.ОстаткиТоваров
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"metadata-tree-{uuid4()}"
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
assert indexed.status_code == 200
response = client.get(f"/projects/{project_id}/metadata/tree")
assert response.status_code == 200
root = response.json()["root"]
main_configuration = next(item for item in root["children"] if item["label"] == "Основная конфигурация")
assert any(item["label"].startswith("Расширение:") for item in root["children"])
assert any(item["label"] == "SFERA" for item in root["children"])
assert any(item["label"] == "Среды" for item in root["children"])
common = next(item for item in main_configuration["children"] if item["label"] == "Общие")
catalogs = next(item for item in main_configuration["children"] if item["label"] == "Справочники")
documents = next(item for item in main_configuration["children"] if item["label"] == "Документы")
accumulation_registers = next(item for item in main_configuration["children"] if item["label"] == "Регистры накопления")
assert any(item["label"] == "Общие модули" for item in common["children"])
assert any(item["label"] == "Общие формы" for item in common["children"])
assert not any(item["label"] == "Общие формы" for item in main_configuration["children"])
assert catalogs["children"][0]["qualified_name"] == "Справочник.Контрагенты"
assert any(item["label"] == "Реквизиты" for item in catalogs["children"][0]["children"])
assert documents["children"][0]["qualified_name"] == "Документ.ЗаказПокупателя"
assert accumulation_registers["children"][0]["qualified_name"] == "РегистрНакопления.ОстаткиТоваров"
lazy = client.get(f"/projects/{project_id}/metadata/tree", params={"object_limit_per_branch": 0})
assert lazy.status_code == 200
lazy_main_configuration = next(item for item in lazy.json()["root"]["children"] if item["label"] == "Основная конфигурация")
lazy_catalogs = next(item for item in lazy_main_configuration["children"] if item["label"] == "Справочники")
assert lazy_catalogs["children"] == []
assert lazy_catalogs["has_more"] is True
assert lazy_catalogs["count"] == 1
children = client.get(
f"/projects/{project_id}/metadata/tree/children",
params={"node_id": "branch.CATALOG", "offset": 0, "limit": 1},
)
assert children.status_code == 200
assert children.json()["total"] == 1
assert children.json()["children"][0]["qualified_name"] == "Справочник.Контрагенты"
search = client.get(f"/projects/{project_id}/metadata/tree/search", params={"q": "Контраг", "limit": 5})
assert search.status_code == 200
assert search.json()["total"] >= 1
assert search.json()["results"][0]["qualified_name"] == "Справочник.Контрагенты"
path = client.get(
f"/projects/{project_id}/metadata/tree/path",
params={"node_id": search.json()["results"][0]["id"]},
)
assert path.status_code == 200
assert path.json()["path"] == ["main-configuration", "branch.CATALOG", search.json()["results"][0]["id"]]
assert path.json()["steps"][-1] == {
"parent_id": "branch.CATALOG",
"child_id": search.json()["results"][0]["id"],
"offset": 0,
}
def test_project_metadata_tree_loads_normalized_lazy_children(tmp_path: Path):
(tmp_path / "metadata.xml").write_text(
"""
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"normalized-tree-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/XML_DUMP",
json={"source": "XML_DUMP", "path": str(tmp_path), "mode": "FULL_REPLACE"},
)
assert imported.status_code == 200
lazy = client.get(f"/projects/{project_id}/metadata/tree", params={"object_limit_per_branch": 0})
assert lazy.status_code == 200
main_configuration = next(item for item in lazy.json()["root"]["children"] if item["label"] == "Основная конфигурация")
catalogs = next(item for item in main_configuration["children"] if item["label"] == "Справочники")
assert catalogs["id"].startswith("normalized.branch.")
assert catalogs["children"] == []
assert catalogs["has_more"] is True
objects = client.get(
f"/projects/{project_id}/metadata/tree/children",
params={"node_id": catalogs["id"], "offset": 0, "limit": 80},
)
assert objects.status_code == 200
catalog = objects.json()["children"][0]
assert catalog["qualified_name"] == "Справочник.Контрагенты"
attributes = next(item for item in catalog["children"] if item["label"] == "Реквизиты")
parts = client.get(
f"/projects/{project_id}/metadata/tree/children",
params={"node_id": attributes["id"], "offset": 0, "limit": 80},
)
assert parts.status_code == 200
assert parts.json()["children"][0]["qualified_name"] == "Справочник.Контрагенты.ИНН"
def test_project_metadata_tree_shows_http_service_documented_structure(tmp_path: Path):
(tmp_path / "ПубличныйAPI.mdo").write_text(
"""
ПубличныйAPI
api
Orders
/orders/{id}
GET
ПолучитьЗаказ
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"http-service-tree-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/EDT_PROJECT",
json={"source": "EDT_PROJECT", "path": str(tmp_path), "mode": "FULL_REPLACE"},
)
assert imported.status_code == 200
tree = client.get(f"/projects/{project_id}/metadata/tree")
assert tree.status_code == 200
main_configuration = next(item for item in tree.json()["root"]["children"] if item["label"] == "Основная конфигурация")
common = next(item for item in main_configuration["children"] if item["label"] == "Общие")
http_services = next(item for item in common["children"] if item["label"] == "HTTP-сервисы")
service = http_services["children"][0]
assert service["qualified_name"] == "HTTPСервис.ПубличныйAPI"
assert [item["label"] for item in service["children"]] == ["Шаблоны URL"]
templates = client.get(
f"/projects/{project_id}/metadata/tree/children",
params={"node_id": service["children"][0]["id"], "offset": 0, "limit": 80},
)
assert templates.status_code == 200
assert templates.json()["children"][0]["label"] == "Orders"
def test_project_metadata_tree_shows_report_documented_structure(tmp_path: Path):
(tmp_path / "АнализПродаж.mdo").write_text(
"""
АнализПродаж
Период
Показатели
ФормаОтчета
ПечатнаяФорма
ТабличныйДокумент
ОсновнаяСхемаКомпоновкиДанных
Основной
НастройкиПоУмолчанию
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"report-tree-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/EDT_PROJECT",
json={"source": "EDT_PROJECT", "path": str(tmp_path), "mode": "FULL_REPLACE"},
)
assert imported.status_code == 200
tree = client.get(f"/projects/{project_id}/metadata/tree")
assert tree.status_code == 200
main_configuration = next(item for item in tree.json()["root"]["children"] if item["label"] == "Основная конфигурация")
reports = next(item for item in main_configuration["children"] if item["label"] == "Отчеты")
report = reports["children"][0]
assert report["qualified_name"] == "Отчет.АнализПродаж"
assert [item["label"] for item in report["children"]] == [
"Реквизиты",
"Табличные части",
"Формы",
"Макеты",
"Табличные документы",
"СКД",
"Варианты отчета",
"Настройки",
]
dcs = next(item for item in report["children"] if item["label"] == "СКД")
dcs_children = client.get(
f"/projects/{project_id}/metadata/tree/children",
params={"node_id": dcs["id"], "offset": 0, "limit": 80},
)
assert dcs_children.status_code == 200
assert dcs_children.json()["children"][0]["label"] == "ОсновнаяСхемаКомпоновкиДанных"
def test_project_metadata_tree_shows_extension_as_configuration_structure(tmp_path: Path):
(tmp_path / "РасширениеCRM.mdo").write_text(
"""
CRM
1.0
КонтрагентыCRM
ВнешнийКод
CRMСервер
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"extension-tree-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/EDT_PROJECT",
json={"source": "EDT_PROJECT", "path": str(tmp_path), "mode": "FULL_REPLACE"},
)
assert imported.status_code == 200
assert imported.json()["normalized_summary"]["extension_count"] == 1
tree = client.get(f"/projects/{project_id}/metadata/tree")
assert tree.status_code == 200
root_children = tree.json()["root"]["children"]
extension = next(item for item in root_children if item["label"] == "Расширение: CRM")
assert extension["kind"] == "EXTENSION"
assert [item["label"] for item in extension["children"][:2]] == ["Сведения (1.0)", "Общие"]
common = next(item for item in extension["children"] if item["label"] == "Общие")
common_modules = next(item for item in common["children"] if item["label"] == "Общие модули")
assert common_modules["children"][0]["label"] == "CRMСервер"
catalogs = next(item for item in extension["children"] if item["label"] == "Справочники")
catalog = catalogs["children"][0]
assert catalog["qualified_name"].endswith("Справочник.КонтрагентыCRM")
attributes = next(item for item in catalog["children"] if item["label"] == "Реквизиты")
parts = client.get(
f"/projects/{project_id}/metadata/tree/children",
params={"node_id": attributes["id"], "offset": 0, "limit": 80},
)
assert parts.status_code == 200
assert parts.json()["children"][0]["label"] == "ВнешнийКод"
lazy_tree = client.get(f"/projects/{project_id}/metadata/tree", params={"object_limit_per_branch": 0})
lazy_extension = next(item for item in lazy_tree.json()["root"]["children"] if item["label"] == "Расширение: CRM")
lazy_catalogs = next(item for item in lazy_extension["children"] if item["label"] == "Справочники")
lazy_catalog_children = client.get(
f"/projects/{project_id}/metadata/tree/children",
params={"node_id": lazy_catalogs["id"], "offset": 0, "limit": 80},
)
assert lazy_catalog_children.status_code == 200
assert lazy_catalog_children.json()["children"][0]["label"] == "КонтрагентыCRM"
def test_project_metadata_tree_adds_reference_configuration_root():
client = TestClient(app)
project_id = f"reference-tree-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/REFERENCE_CONFIGURATION",
json={
"source": "REFERENCE_CONFIGURATION",
"metadata": {"context_description": "Reference baseline for comparison"},
},
)
assert imported.status_code == 200
tree = client.get(f"/projects/{project_id}/metadata/tree")
assert tree.status_code == 200
root = tree.json()["root"]
reference = next(item for item in root["children"] if item["kind"] == "REFERENCE_CONFIGURATION")
assert reference["label"] == "Reference configuration"
assert [item["label"] for item in reference["children"][:3]] == ["Сведения", "Общие", "Константы"]
lazy_tree = client.get(f"/projects/{project_id}/metadata/tree", params={"object_limit_per_branch": 0})
assert lazy_tree.status_code == 200
lazy_root = lazy_tree.json()["root"]
lazy_reference = next(item for item in lazy_root["children"] if item["kind"] == "REFERENCE_CONFIGURATION")
assert lazy_reference["label"] == "Reference configuration"
def test_project_metadata_tree_adds_context_configuration_root():
client = TestClient(app)
project_id = f"context-tree-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/CONTEXT_ONLY",
json={
"source": "CONTEXT_ONLY",
"metadata": {"context_description": "Context-only outline"},
},
)
assert imported.status_code == 200
tree = client.get(f"/projects/{project_id}/metadata/tree")
assert tree.status_code == 200
root = tree.json()["root"]
context_root = next(item for item in root["children"] if item["kind"] == "CONTEXT_CONFIGURATION")
assert context_root["label"] == "Context-only configuration"
assert [item["label"] for item in context_root["children"][:3]] == ["Сведения", "Общие", "Константы"]
lazy_tree = client.get(f"/projects/{project_id}/metadata/tree", params={"object_limit_per_branch": 0})
assert lazy_tree.status_code == 200
lazy_root = lazy_tree.json()["root"]
lazy_context_root = next(item for item in lazy_root["children"] if item["kind"] == "CONTEXT_CONFIGURATION")
assert lazy_context_root["label"] == "Context-only configuration"
def test_project_metadata_tree_does_not_add_context_or_reference_roots_for_regular_sources():
client = TestClient(app)
project_id = f"regular-tree-{uuid4()}"
imported = client.post(
f"/projects/{project_id}/imports/XML_DUMP",
json={
"source": "XML_DUMP",
"metadata": {"platform_version": "8.3.24", "compatibility_mode": "8.3.20"},
},
)
assert imported.status_code == 200
tree = client.get(f"/projects/{project_id}/metadata/tree")
assert tree.status_code == 200
root = tree.json()["root"]
kinds = {item["kind"] for item in root["children"]}
assert "CONTEXT_CONFIGURATION" not in kinds
assert "REFERENCE_CONFIGURATION" not in kinds
lazy_tree = client.get(f"/projects/{project_id}/metadata/tree", params={"object_limit_per_branch": 0})
assert lazy_tree.status_code == 200
lazy_root = lazy_tree.json()["root"]
lazy_kinds = {item["kind"] for item in lazy_root["children"]}
assert "CONTEXT_CONFIGURATION" not in lazy_kinds
assert "REFERENCE_CONFIGURATION" not in lazy_kinds
def test_project_metadata_tree_places_conditional_configuration_roots_before_sfera():
client = TestClient(app)
context_project = f"context-order-{uuid4()}"
context_import = client.post(
f"/projects/{context_project}/imports/CONTEXT_ONLY",
json={"source": "CONTEXT_ONLY", "metadata": {"context_description": "Context order check"}},
)
assert context_import.status_code == 200
context_tree = client.get(f"/projects/{context_project}/metadata/tree")
assert context_tree.status_code == 200
context_children = context_tree.json()["root"]["children"]
context_index = next(i for i, item in enumerate(context_children) if item["kind"] == "CONTEXT_CONFIGURATION")
context_sfera_index = next(i for i, item in enumerate(context_children) if item["label"] == "SFERA")
assert context_index < context_sfera_index
context_lazy_tree = client.get(f"/projects/{context_project}/metadata/tree", params={"object_limit_per_branch": 0})
assert context_lazy_tree.status_code == 200
context_lazy_children = context_lazy_tree.json()["root"]["children"]
context_lazy_index = next(i for i, item in enumerate(context_lazy_children) if item["kind"] == "CONTEXT_CONFIGURATION")
context_lazy_sfera_index = next(i for i, item in enumerate(context_lazy_children) if item["label"] == "SFERA")
assert context_lazy_index < context_lazy_sfera_index
reference_project = f"reference-order-{uuid4()}"
reference_import = client.post(
f"/projects/{reference_project}/imports/REFERENCE_CONFIGURATION",
json={"source": "REFERENCE_CONFIGURATION", "metadata": {"context_description": "Reference order check"}},
)
assert reference_import.status_code == 200
reference_tree = client.get(f"/projects/{reference_project}/metadata/tree")
assert reference_tree.status_code == 200
reference_children = reference_tree.json()["root"]["children"]
reference_index = next(i for i, item in enumerate(reference_children) if item["kind"] == "REFERENCE_CONFIGURATION")
reference_sfera_index = next(i for i, item in enumerate(reference_children) if item["label"] == "SFERA")
assert reference_index < reference_sfera_index
reference_lazy_tree = client.get(f"/projects/{reference_project}/metadata/tree", params={"object_limit_per_branch": 0})
assert reference_lazy_tree.status_code == 200
reference_lazy_children = reference_lazy_tree.json()["root"]["children"]
reference_lazy_index = next(i for i, item in enumerate(reference_lazy_children) if item["kind"] == "REFERENCE_CONFIGURATION")
reference_lazy_sfera_index = next(i for i, item in enumerate(reference_lazy_children) if item["label"] == "SFERA")
assert reference_lazy_index < reference_lazy_sfera_index
def test_normalized_object_modules_returns_linked_bsl_source(tmp_path: Path):
catalog_dir = tmp_path / "Catalogs" / "Контрагенты"
catalog_dir.mkdir(parents=True)
(catalog_dir / "Контрагенты.mdo").write_text(
"""
Контрагенты
""",
encoding="utf-8",
)
(catalog_dir / "ObjectModule.bsl").write_text(
"""
Процедура ПередЗаписью(Отказ)
ПроверитьКонтрагента();
Отказ = Ложь;
КонецПроцедуры
Процедура ПроверитьКонтрагента()
Запрос = Новый Запрос("ВЫБРАТЬ Контрагенты.Ссылка ИЗ Справочник.Контрагенты КАК Контрагенты");
КонецПроцедуры
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"object-modules-{uuid4()}"
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
assert indexed.status_code == 200
modules = client.get(
f"/projects/{project_id}/normalized/object/modules",
params={"qualified_name": "Справочник.Контрагенты"},
)
assert modules.status_code == 200
payload = modules.json()
assert payload[0]["module_role"] == "OBJECT_MODULE"
assert "ПередЗаписью" in payload[0]["source_text"]
assert payload[0]["routines_count"] == 2
assert payload[0]["routines"][0]["name"] == "ПередЗаписью"
assert payload[0]["routines"][0]["kind"] == "PROCEDURE"
assert payload[0]["routines"][0]["calls"] == ["ObjectModule.ПроверитьКонтрагента"]
assert payload[0]["routines"][1]["queries_count"] == 1
assert "Справочник.Контрагенты" in payload[0]["routines"][1]["queries"][0]
assert payload[0]["routines"][1]["impact_level"] == "MEDIUM"
assert "reads query tables" in payload[0]["routines"][1]["impact_reasons"]
tree = client.get(f"/projects/{project_id}/metadata/tree")
assert tree.status_code == 200
main_configuration = next(item for item in tree.json()["root"]["children"] if item["label"] == "Основная конфигурация")
catalogs = next(item for item in main_configuration["children"] if item["label"] == "Справочники")
catalog = catalogs["children"][0]
object_module_group = next(item for item in catalog["children"] if item["label"] == "Модуль объекта")
module_children = client.get(
f"/projects/{project_id}/metadata/tree/children",
params={"node_id": object_module_group["id"], "offset": 0, "limit": 80},
)
assert module_children.status_code == 200
assert module_children.json()["children"][0]["kind"] == "MODULE"
module_qname = module_children.json()["children"][0]["qualified_name"]
selected_module = client.get(
f"/projects/{project_id}/normalized/object/modules",
params={"qualified_name": module_qname},
)
assert selected_module.status_code == 200
assert selected_module.json()[0]["source_text"] == payload[0]["source_text"]
selected_normalized_module = client.get(
f"/projects/{project_id}/normalized/object/modules",
params={"qualified_name": "Справочник.Контрагенты.МодульОбъекта"},
)
assert selected_normalized_module.status_code == 200
assert selected_normalized_module.json()[0]["source_text"] == payload[0]["source_text"]
selected_routine = client.get(
f"/projects/{project_id}/normalized/object/modules",
params={"qualified_name": "ПередЗаписью"},
)
assert selected_routine.status_code == 200
assert selected_routine.json()[0]["source_text"] == payload[0]["source_text"]
def test_normalized_object_modules_resolves_common_module_code(tmp_path: Path):
common_module_dir = tmp_path / "CommonModules" / "CRMСервер"
common_module_dir.mkdir(parents=True)
(common_module_dir / "CRMСервер.mdo").write_text(
"""
CRMСервер
""",
encoding="utf-8",
)
(common_module_dir / "Module.bsl").write_text(
"""
Процедура ЗагрузитьКонтрагентов() Экспорт
Сообщить("Готово");
КонецПроцедуры
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"common-module-code-{uuid4()}"
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
assert indexed.status_code == 200
common_module = client.get(
f"/projects/{project_id}/normalized/object/modules",
params={"qualified_name": "ОбщийМодуль.CRMСервер"},
)
assert common_module.status_code == 200
payload = common_module.json()
assert payload[0]["module_role"] == "MODULE"
assert "ЗагрузитьКонтрагентов" in payload[0]["source_text"]
normalized_module_node = client.get(
f"/projects/{project_id}/normalized/object/modules",
params={"qualified_name": "ОбщийМодуль.CRMСервер.Модуль"},
)
assert normalized_module_node.status_code == 200
assert normalized_module_node.json()[0]["source_text"] == payload[0]["source_text"]
def test_normalized_object_modules_reads_canonical_project_before_snapshot():
client = TestClient(app)
project_id = f"normalized-source-{uuid4()}"
source_text = "Процедура Выполнить() Экспорт\n Сообщить(\"ok\");\nКонецПроцедуры\n"
normalized = NormalizedProject(
project_id=project_id,
configuration=ConfigurationRoot(
groups=[
MetadataGroup(
name="Общие модули",
object_kinds=["COMMON_MODULE"],
objects=[
MetadataObject(
name="CRMСервер",
qualified_name="ОбщийМодуль.CRMСервер",
object_kind="COMMON_MODULE",
modules=[
Module(
name="Module",
qualified_name="ОбщийМодуль.CRMСервер.Модуль",
source_path="/edt/CommonModules/CRMСервер/Module.bsl",
module_kind="MODULE",
attributes={"source_text": source_text, "module_role": "MODULE"},
)
],
)
],
)
]
),
source_path="/edt",
)
main._save_normalized_project(project_id, normalized)
by_object = client.get(
f"/projects/{project_id}/normalized/object/modules",
params={"qualified_name": "ОбщийМодуль.CRMСервер"},
)
assert by_object.status_code == 200
assert by_object.json()[0]["source_text"] == source_text
assert by_object.json()[0]["routines"][0]["name"] == "Выполнить"
assert by_object.json()[0]["routines"][0]["export"] is True
by_module = client.get(
f"/projects/{project_id}/normalized/object/modules",
params={"qualified_name": "ОбщийМодуль.CRMСервер.Модуль"},
)
assert by_module.status_code == 200
assert by_module.json()[0]["source_text"] == source_text
def test_bsl_completions_returns_only_exported_common_module_routines():
client = TestClient(app)
project_id = f"bsl-completion-{uuid4()}"
source_text = "\n".join([
"Процедура Выполнить() Экспорт",
"КонецПроцедуры",
"",
"Функция ВнутреннийРасчет()",
" Возврат 1;",
"КонецФункции",
"",
])
normalized = NormalizedProject(
project_id=project_id,
configuration=ConfigurationRoot(
groups=[
MetadataGroup(
name="Общие модули",
object_kinds=["COMMON_MODULE"],
objects=[
MetadataObject(
name="CRMСервер",
qualified_name="ОбщийМодуль.CRMСервер",
object_kind="COMMON_MODULE",
modules=[
Module(
name="Module",
qualified_name="ОбщийМодуль.CRMСервер.Модуль",
source_path="/edt/CommonModules/CRMСервер/Module.bsl",
module_kind="MODULE",
attributes={"source_text": source_text},
)
],
)
],
)
]
),
)
main._save_normalized_project(project_id, normalized)
response = client.get(
f"/projects/{project_id}/bsl/completions",
params={"receiver": "CRMСервер"},
)
assert response.status_code == 200
labels = {item["label"] for item in response.json()}
assert "Выполнить" in labels
assert "ВнутреннийРасчет" not in labels
def test_project_flowchart_returns_overview_and_focus(tmp_path: Path):
catalog_dir = tmp_path / "Catalogs" / "Clients"
catalog_dir.mkdir(parents=True)
(catalog_dir / "Clients.mdo").write_text(
"""
Clients
""",
encoding="utf-8",
)
(catalog_dir / "ObjectModule.bsl").write_text(
"Процедура ПередЗаписью(Отказ)\n Отказ = Ложь;\nКонецПроцедуры\n",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"flowchart-{uuid4()}"
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
assert indexed.status_code == 200
overview = client.get(f"/projects/{project_id}/flowchart")
assert overview.status_code == 200
overview_payload = overview.json()
assert overview_payload["mode"] == "overview"
assert any(node["kind"] == "CATALOG" and node["count"] == 1 for node in overview_payload["nodes"])
assert all(node["kind"] not in {"FORM", "ATTRIBUTE", "TABULAR_SECTION"} for node in overview_payload["nodes"])
focus = client.get(
f"/projects/{project_id}/flowchart",
params={"focus": "Справочник.Clients", "depth": 2, "limit": 50},
)
assert focus.status_code == 200
focus_payload = focus.json()
assert focus_payload["mode"] == "focus"
assert any(node["qualified_name"] == "Справочник.Clients" for node in focus_payload["nodes"])
assert all(node["kind"] not in {"FORM", "ATTRIBUTE", "TABULAR_SECTION"} for node in focus_payload["nodes"])
assert focus_payload["edges"] == []
def test_project_flowchart_collapses_module_logic_to_object_links(tmp_path: Path):
catalog_dir = tmp_path / "Catalogs" / "Clients"
document_dir = tmp_path / "Documents" / "Order"
catalog_dir.mkdir(parents=True)
document_dir.mkdir(parents=True)
(catalog_dir / "Clients.mdo").write_text(
"""
Clients
""",
encoding="utf-8",
)
(document_dir / "Order.mdo").write_text(
"""
Order
""",
encoding="utf-8",
)
(document_dir / "ObjectModule.bsl").write_text(
"""
Процедура Проведение(Отказ)
Запрос = Новый Запрос("ВЫБРАТЬ Clients.Ссылка ИЗ Справочник.Clients КАК Clients");
КонецПроцедуры
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"flowchart-logic-{uuid4()}"
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
assert indexed.status_code == 200
focus = client.get(
f"/projects/{project_id}/flowchart",
params={"focus": "Документ.Order", "depth": 2, "limit": 50},
)
assert focus.status_code == 200
payload = focus.json()
assert any(node["qualified_name"] == "Документ.Order" for node in payload["nodes"])
assert any(node["qualified_name"] == "Справочник.Clients" for node in payload["nodes"])
assert any(edge["kind"] == "READS_TABLE" for edge in payload["edges"])
def test_index_project_and_query_impact(tmp_path: Path):
module = tmp_path / "demo_module.bsl"
module.write_text(
"""
Процедура Проведение()
ПроверитьОстатки();
Движения.ОстаткиТоваров.Записать();
КонецПроцедуры
Процедура ПроверитьОстатки()
КонецПроцедуры
""",
encoding="utf-8",
)
client = TestClient(app)
indexed = client.post(
"/projects/index",
json={"path": str(tmp_path), "project_id": "demo-api"},
)
assert indexed.status_code == 200
assert indexed.json()["snapshot"]["project_id"] == "demo-api"
assert client.get("/storage").json()["status"] == "configured"
assert any(item["project_id"] == "demo-api" for item in client.get("/storage/snapshots").json())
loaded = client.post("/projects/demo-api/load")
assert loaded.status_code == 200
assert loaded.json()["snapshot"]["project_id"] == "demo-api"
snapshot = client.get("/projects/demo-api/snapshot")
assert snapshot.status_code == 200
assert snapshot.json()["node_count"] >= 3
exported = client.get("/projects/demo-api/snapshot/export")
assert exported.status_code == 200
assert exported.json()["project_id"] == "demo-api"
impact = client.get("/projects/demo-api/impact/Проведение")
assert impact.status_code == 200
payload = impact.json()
assert [node["name"] for node in payload["callees"]] == ["ПроверитьОстатки"]
assert [node["name"] for node in payload["writes"]] == ["ОстаткиТоваров"]
search = client.get("/projects/demo-api/search", params={"q": "Пров", "kind": "PROCEDURE"})
assert search.status_code == 200
assert search.json()["results"][0]["name"] == "Проведение"
routine_lineage = search.json()["results"][0]["lineage_id"]
versions = client.get("/projects/demo-api/versions")
assert versions.status_code == 200
assert any(item["lineage_id"] == routine_lineage for item in versions.json())
lineage_history = client.get(f"/versions/{routine_lineage}")
assert lineage_history.status_code == 200
assert lineage_history.json()[0]["lineage_id"] == routine_lineage
usage = client.get("/projects/demo-api/tables/usage", params={"table": "ОстаткиТоваров"})
assert usage.status_code == 200
assert usage.json()[0]["writers"][0]["name"] == "Проведение"
writes = client.get(
"/projects/demo-api/transactions/writes",
params={"target": "ОстаткиТоваров"},
)
assert writes.status_code == 200
assert writes.json()[0]["routine"]["name"] == "Проведение"
module.write_text(
"""
Процедура Проведение()
ПроверитьОстатки();
КонецПроцедуры
Процедура ПроверитьОстатки()
КонецПроцедуры
""",
encoding="utf-8",
)
incremental = client.post(
"/projects/demo-api/incremental/file",
json={"path": str(module)},
)
assert incremental.status_code == 200
assert incremental.json()["removed_edges"] >= 1
assert client.get(
"/projects/demo-api/transactions/writes",
params={"target": "ОстаткиТоваров"},
).json() == []
signal = client.post(
"/projects/demo-api/runtime/signals",
json={
"signal": {
"signal_id": "signal.1",
"lineage_id": routine_lineage,
"kind": "ERROR",
"duration_ms": 50.0,
}
},
)
assert signal.status_code == 200
runtime = client.get("/projects/demo-api/runtime/summary")
assert runtime.status_code == 200
assert runtime.json()[0]["node"]["name"] == "Проведение"
assert runtime.json()[0]["error_count"] == 1
knowledge = client.post(
"/knowledge",
json={
"record_id": "knowledge.demo",
"scope": "PROJECT",
"title": "Проведение документов",
"body": "Правила проведения заказа.",
"related_lineages": [routine_lineage],
},
)
assert knowledge.status_code == 200
search_knowledge = client.get("/knowledge/search", params={"q": "заказа"})
assert search_knowledge.status_code == 200
assert any(item["record_id"] == "knowledge.demo" for item in search_knowledge.json()["results"])
coverage = client.get("/projects/demo-api/knowledge/coverage")
assert coverage.status_code == 200
assert any(item["record_count"] == 1 for item in coverage.json())
def test_project_symbol_navigation_endpoints(tmp_path: Path):
module = tmp_path / "demo_module.bsl"
module.write_text(
"""
Процедура Проведение()
ПроверитьОстатки();
КонецПроцедуры
Процедура ПроверитьОстатки()
КонецПроцедуры
""",
encoding="utf-8",
)
client = TestClient(app)
project_id = f"symbols-api-{uuid4()}"
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
assert indexed.status_code == 200
symbols = client.get(f"/projects/{project_id}/symbols", params={"q": "Проверить", "kind": "PROCEDURE"})
assert symbols.status_code == 200
symbol_payload = symbols.json()
assert symbol_payload[0]["node"]["name"] == "ПроверитьОстатки"
assert symbol_payload[0]["source"]["source_path"].endswith("demo_module.bsl")
lineage_id = symbol_payload[0]["node"]["lineage_id"]
definition = client.get(f"/projects/{project_id}/symbols/definition", params={"lineage_id": lineage_id})
assert definition.status_code == 200
assert definition.json()["node"]["qualified_name"] == "demo_module.ПроверитьОстатки"
assert definition.json()["source"]["line_start"] is not None
references = client.get(f"/projects/{project_id}/symbols/references", params={"lineage_id": lineage_id})
assert references.status_code == 200
reference_payload = references.json()
assert reference_payload["symbol"]["node"]["name"] == "ПроверитьОстатки"
assert any(
reference["kind"] == "CALLS"
and reference["source"]["name"] == "Проведение"
and reference["direction"] == "incoming"
for reference in reference_payload["references"]
)
def test_persisted_project_endpoints_load_snapshot_after_memory_clear(tmp_path: Path):
project_id = f"persisted-api-{uuid4()}"
module = tmp_path / "persisted_module.bsl"
module.write_text(
"""
Процедура Проведение()
ПроверитьОстатки();
Движения.ОстаткиТоваров.Записать();
КонецПроцедуры
Процедура ПроверитьОстатки()
КонецПроцедуры
""",
encoding="utf-8",
)
client = TestClient(app)
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
assert indexed.status_code == 200
main._snapshots.pop(project_id, None)
main._graphs.pop(project_id, None)
assert client.get(f"/projects/{project_id}/snapshot").status_code == 200
search = client.get(f"/projects/{project_id}/search", params={"q": "Пров", "kind": "PROCEDURE"})
assert search.status_code == 200
assert search.json()["results"][0]["name"] == "Проведение"
assert client.get(f"/projects/{project_id}/review").status_code == 200
report = client.get(f"/projects/{project_id}/report")
assert report.status_code == 200
assert report.json()["project_id"] == project_id
def test_xml_ui_forms_endpoint(tmp_path: Path):
(tmp_path / "form.xml").write_text(
"""
""",
encoding="utf-8",
)
(tmp_path / "form_module.bsl").write_text(
"Процедура ПровестиКоманда()\nКонецПроцедуры\n",
encoding="utf-8",
)
client = TestClient(app)
indexed = client.post(
"/projects/index",
json={"path": str(tmp_path), "project_id": "demo-api-ui"},
)
assert indexed.status_code == 200
forms = client.get("/projects/demo-api-ui/ui/forms")
assert forms.status_code == 200
assert forms.json()[0]["commands"][0]["name"] == "Провести"
handlers = forms.json()[0]["command_handlers"]
assert next(iter(handlers.values()))["name"] == "ПровестиКоманда"
def test_object_ui_endpoint_filters_forms_by_1c_object(tmp_path: Path):
(tmp_path / "metadata.xml").write_text(
"""
""",
encoding="utf-8",
)
module = tmp_path / "Documents" / "ЗаказПокупателя" / "Ext" / "ObjectModule.bsl"
module.parent.mkdir(parents=True)
module.write_text("Процедура ПровестиКоманда()\nКонецПроцедуры\n", encoding="utf-8")
client = TestClient(app)
indexed = client.post(
"/projects/index",
json={"path": str(tmp_path), "project_id": "demo-api-object-ui"},
)
assert indexed.status_code == 200
response = client.get("/projects/demo-api-object-ui/objects/ui/Документ.ЗаказПокупателя")
assert response.status_code == 200
payload = response.json()
assert payload["object"]["name"] == "ЗаказПокупателя"
assert [form["form"]["name"] for form in payload["forms"]] == ["ФормаДокумента"]
assert payload["forms"][0]["commands"][0]["name"] == "Провести"
assert next(iter(payload["forms"][0]["command_handlers"].values()))["name"] == "ПровестиКоманда"
def test_object_impact_endpoint(tmp_path: Path):
(tmp_path / "metadata.xml").write_text(
"""
""",
encoding="utf-8",
)
module = tmp_path / "Documents" / "ЗаказПокупателя" / "Ext" / "ObjectModule.bsl"
module.parent.mkdir(parents=True)
module.write_text(
"""
Процедура Проведение()
Движения.ОстаткиТоваров.Записать();
КонецПроцедуры
Процедура ПровестиКоманда()
КонецПроцедуры
""",
encoding="utf-8",
)
client = TestClient(app)
indexed = client.post(
"/projects/index",
json={"path": str(tmp_path), "project_id": "demo-object-impact"},
)
assert indexed.status_code == 200
response = client.get(
"/projects/demo-object-impact/objects/impact/Документ.ЗаказПокупателя"
)
assert response.status_code == 200
payload = response.json()
assert payload["object"]["name"] == "ЗаказПокупателя"
assert payload["modules"][0]["name"] == "ObjectModule"
assert [routine["name"] for routine in payload["routines"]] == ["ПровестиКоманда", "Проведение"]
assert payload["forms"][0]["name"] == "ФормаДокумента"
assert payload["commands"][0]["name"] == "Провести"
assert payload["attributes"][0]["name"] == "Контрагент"
assert payload["tabular_sections"][0]["name"] == "Товары"
assert next(iter(payload["tabular_section_columns"].values()))[0]["name"] == "Номенклатура"
assert payload["roles"][0]["name"] == "Менеджер"
assert payload["role_access"][0]["permissions"]["post"] == "true"
assert payload["writes"][0]["name"] == "ОстаткиТоваров"
object_access = client.get(
"/projects/demo-object-impact/access/objects/Документ.ЗаказПокупателя/roles"
)
assert object_access.status_code == 200
assert object_access.json()["grants"][0]["role"]["name"] == "Менеджер"
assert object_access.json()["grants"][0]["permissions"]["write"] == "true"
role_access = client.get("/projects/demo-object-impact/access/roles/Роль.Менеджер/objects")
assert role_access.status_code == 200
assert role_access.json()["objects"][0]["qualified_name"] == "Документ.ЗаказПокупателя"
assert role_access.json()["grants"][0]["permissions"]["post"] == "true"
def test_object_attributes_endpoint(tmp_path: Path):
(tmp_path / "metadata.xml").write_text(
"""
""",
encoding="utf-8",
)
client = TestClient(app)
indexed = client.post(
"/projects/index",
json={"path": str(tmp_path), "project_id": "demo-object-attributes"},
)
assert indexed.status_code == 200
response = client.get("/projects/demo-object-attributes/objects/attributes/Документ.ЗаказПокупателя")
assert response.status_code == 200
assert [row["name"] for row in response.json()["results"]] == ["Контрагент", "СуммаДокумента"]
def test_authoring_context_and_completion_preview(tmp_path: Path):
project_id = f"authoring-api-{uuid4()}"
(tmp_path / "metadata.xml").write_text(
"""
""",
encoding="utf-8",
)
module = tmp_path / "Documents" / "ЗаказПокупателя" / "Ext" / "ObjectModule.bsl"
module.parent.mkdir(parents=True)
source_text = """
Процедура Проведение(Отказ, РежимПроведения)
Сумма = 0;
Движения.ОстаткиТоваров.Записать();
КонецПроцедуры
"""
module.write_text(source_text, encoding="utf-8")
client = TestClient(app)
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
assert indexed.status_code == 200
create_authoring_session(client, project_id, "task.authoring", "session.authoring")
create_authoring_session(client, project_id, "task.rollback", "session.rollback")
context = client.post(
f"/projects/{project_id}/authoring/context",
json={
"object_name": "Документ.ЗаказПокупателя",
"routine_name": "Проведение",
"cursor_line": 3,
"source_text": source_text,
},
)
assert context.status_code == 200
payload = context.json()
assert payload["object"]["qualified_name"] == "Документ.ЗаказПокупателя"
assert payload["routine"]["name"] == "Проведение"
assert "Сумма" in payload["local_variables"]
assert "Отказ" in payload["parameters"]
assert payload["object_attributes"][0]["name"] == "Контрагент"
assert payload["tabular_sections"][0]["name"] == "Товары"
assert "ЗначениеЗаполнено" in payload["available_methods"]
preview = client.post(
f"/projects/{project_id}/authoring/completion-preview",
json={
"object_name": "Документ.ЗаказПокупателя",
"routine_name": "Проведение",
"cursor_line": 3,
"source_text": source_text,
"intent": "fill-check",
},
)
assert preview.status_code == 200
preview_payload = preview.json()
assert preview_payload["allowed"] is False
assert "ЗначениеЗаполнено(Контрагент)" in preview_payload["insert_text"]
assert any(check["name"] == "apply" and check["status"] == "BLOCKED" for check in preview_payload["checks"])
assert preview_payload["semantic_diff"][0]["kind"] == "ADD"
html5_preview = client.post(
f"/html5/projects/{project_id}/authoring/completion-preview",
data={
"object_name": "Документ.ЗаказПокупателя",
"routine_name": "Проведение",
"cursor_line": "3",
"source_text": source_text,
"intent": "fill-check",
},
)
assert html5_preview.status_code == 200
assert "text/html" in html5_preview.headers["content-type"]
assert "data-html5-authoring-preview-result" in html5_preview.text
assert "ЗначениеЗаполнено(Контрагент)" in html5_preview.text
assert "BLOCKED" in html5_preview.text
assert "ADD" in html5_preview.text
assert "
""",
encoding="utf-8",
)
module = tmp_path / "Documents" / "ЗаказПокупателя" / "Ext" / "ObjectModule.bsl"
module.parent.mkdir(parents=True)
source_text = "Процедура Проверить()\n Сообщить(Телефон);\nКонецПроцедуры\n"
module.write_text(source_text, encoding="utf-8")
client = TestClient(app)
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
assert indexed.status_code == 200
create_authoring_session(client, project_id, "task.privacy", "session.privacy")
schema = client.get(f"/projects/{project_id}/objects/schema/Документ.ЗаказПокупателя")
phone_lineage = schema.json()["attributes"][0]["lineage_id"]
marker = client.post(
f"/projects/{project_id}/privacy/markers",
json={"target_id": phone_lineage, "classification": "PERSONAL_DATA", "reason": "phone number"},
)
assert marker.status_code == 200
preview = client.post(
f"/projects/{project_id}/authoring/semantic-diff-preview",
json={
"object_name": "Документ.ЗаказПокупателя",
"routine_name": "Проверить",
"source_path": str(module),
"original_text": source_text,
"proposed_text": source_text.replace("Сообщить(Телефон);", "Сообщить(Телефон);\n Возврат;"),
"task_id": "task.privacy",
"session_id": "session.privacy",
"user_id": "dev.ivan",
},
)
assert preview.status_code == 200
preview_payload = preview.json()
assert any(check["name"] == "privacy" and check["status"] == "BLOCKED" for check in preview_payload["checks"])
apply_response = client.post(
f"/projects/{project_id}/authoring/apply-change-set",
json={
"object_name": "Документ.ЗаказПокупателя",
"routine_name": "Проверить",
"source_path": str(module),
"original_text": source_text,
"proposed_text": source_text.replace("Сообщить(Телефон);", "Сообщить(Телефон);\n Возврат;"),
"task_id": "task.privacy",
"session_id": "session.privacy",
"user_id": "dev.ivan",
"expected_next_version_id": preview_payload["version_preview"]["next_version_id"],
"approved_by": "dev.ivan",
},
)
assert apply_response.status_code == 409
assert any(check["name"] == "privacy" for check in apply_response.json()["detail"]["blocked_checks"])
def test_authoring_metadata_object_preview_and_apply(tmp_path: Path):
project_id = f"metadata-authoring-api-{uuid4()}"
(tmp_path / "metadata.xml").write_text(
"""
""",
encoding="utf-8",
)
client = TestClient(app)
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
assert indexed.status_code == 200
create_authoring_session(client, project_id, "task.metadata", "session.metadata")
create_authoring_session(client, project_id, "task.metadata.rollback", "session.metadata.rollback")
draft = {
"object_kind": "DOCUMENT",
"name": "ЗаявкаНаЗакупку",
"synonym": "Заявка на закупку",
"attributes": [
{"name": "Контрагент", "type": "СправочникСсылка.Контрагенты", "required": True}
],
"tabular_sections": [
{
"name": "Товары",
"attributes": [
{"name": "Номенклатура", "type": "СправочникСсылка.Номенклатура"},
{"name": "Количество", "type": "Число"},
],
}
],
"forms": ["ФормаДокумента"],
"commands": [{"name": "Заполнить", "handler": "ЗаполнитьКоманда"}],
"task_id": "task.metadata",
"session_id": "session.metadata",
"user_id": "dev.ivan",
}
preview = client.post(f"/projects/{project_id}/authoring/metadata-object-preview", json=draft)
assert preview.status_code == 200
preview_payload = preview.json()
assert preview_payload["target"]["qualified_name"] == "Документ.ЗаявкаНаЗакупку"
assert preview_payload["changed"] is True
assert preview_payload["version_preview"]["apply_available"] is True
assert any(check["name"] == "task-session" and check["status"] == "OK" for check in preview_payload["checks"])
assert any(check["name"] == "rbac" and check["status"] == "OK" for check in preview_payload["checks"])
assert any("Реквизит.Контрагент" in row["text"] for row in preview_payload["semantic_diff"])
assert any("ТабличнаяЧасть.Товары" in row["text"] for row in preview_payload["semantic_diff"])
assert any("Команда.Заполнить" in row["text"] for row in preview_payload["semantic_diff"])
html5_preview = client.post(
f"/html5/projects/{project_id}/authoring/metadata-object-preview",
data={
"object_kind": "DOCUMENT",
"name": "ЗаявкаНаЗакупкуHtml5",
"synonym": "Заявка на закупку HTML5",
"attributes": "Контрагент:СправочникСсылка.Контрагенты",
"tabular_sections": "Товары[Номенклатура:СправочникСсылка.Номенклатура;Количество:Число]",
"forms": "ФормаДокумента",
"commands": "Заполнить:ЗаполнитьКоманда",
"task_id": "task.metadata",
"session_id": "session.metadata",
"user_id": "dev.ivan",
},
)
assert html5_preview.status_code == 200
assert "text/html" in html5_preview.headers["content-type"]
assert "data-html5-metadata-preview-result" in html5_preview.text
assert "data-html5-metadata-apply-form" in html5_preview.text
assert f'hx-post="/html5/projects/{project_id}/authoring/apply-metadata-object"' in html5_preview.text
assert "Документ.ЗаявкаНаЗакупкуHtml5" in html5_preview.text
assert "Реквизит.Контрагент" in html5_preview.text
assert "