Optimize XML AI structure package for Codex
This commit is contained in:
@@ -15,6 +15,7 @@ _PARSEABLE_SUFFIXES = {".xml", ".mdo", ".bsl"}
|
||||
_BINARY_1C_SUFFIXES = {".cf", ".cfe"}
|
||||
_CODEX_SOURCE_SUFFIXES = {".xml", ".mdo", ".bsl", ".json", ".txt"}
|
||||
_MAX_CODEX_SOURCE_FILE_BYTES = 2_000_000
|
||||
_ROOT_XML_NAMES = {"metadata.xml", "configuration.xml"}
|
||||
|
||||
|
||||
def prepare_ai_structure(
|
||||
@@ -60,6 +61,7 @@ def prepare_ai_structure(
|
||||
normalized,
|
||||
diagnostics,
|
||||
binaries,
|
||||
_source_layout_summary(input_path),
|
||||
)
|
||||
_write_json(output_path / "manifest.json", manifest)
|
||||
_write_json(output_path / "source_inventory.json", {"files": files})
|
||||
@@ -70,6 +72,9 @@ def prepare_ai_structure(
|
||||
_write_json(output_path / "ai_edges.json", [edge.model_dump(mode="json") for edge in snapshot.edges])
|
||||
if normalized is not None:
|
||||
_write_json(output_path / "normalized_project.json", normalized.model_dump(mode="json"))
|
||||
_write_json(output_path / "project_layout.json", manifest.get("source_layout") or {})
|
||||
_write_json(output_path / "compact_objects.json", _compact_objects(normalized))
|
||||
_write_json(output_path / "compact_modules.json", _compact_modules(normalized))
|
||||
_write_text(output_path / "ai_context.md", _ai_context_markdown(manifest, snapshot, normalized))
|
||||
_write_text(output_path / "export_plan.md", _export_plan_markdown(project_id, input_path, output_path, binaries, parseable))
|
||||
_write_codex_package(codex_root, input_path, manifest, files, snapshot, normalized, binaries, parseable)
|
||||
@@ -98,6 +103,7 @@ def _manifest(
|
||||
normalized: NormalizedProject | None,
|
||||
diagnostics: list[str],
|
||||
binaries: list[dict[str, Any]],
|
||||
source_layout: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"version": AI_STRUCTURE_VERSION,
|
||||
@@ -109,6 +115,7 @@ def _manifest(
|
||||
"status": "ready" if snapshot is not None or normalized is not None else "export_required",
|
||||
"files_count": len(files),
|
||||
"binary_1c_files": binaries,
|
||||
"source_layout": source_layout,
|
||||
"artifacts": _artifacts(snapshot, normalized),
|
||||
"snapshot": None
|
||||
if snapshot is None
|
||||
@@ -134,7 +141,16 @@ def _manifest(
|
||||
|
||||
|
||||
def _artifacts(snapshot: SirSnapshot | None, normalized: NormalizedProject | None) -> list[str]:
|
||||
artifacts = ["manifest.json", "source_inventory.json", "ai_context.md", "export_plan.md", "codex_package"]
|
||||
artifacts = [
|
||||
"manifest.json",
|
||||
"source_inventory.json",
|
||||
"ai_context.md",
|
||||
"export_plan.md",
|
||||
"codex_package",
|
||||
"project_layout.json",
|
||||
"compact_objects.json",
|
||||
"compact_modules.json",
|
||||
]
|
||||
if snapshot is not None:
|
||||
artifacts.extend(["sir_snapshot.json", "ai_objects.json", "ai_modules.json", "ai_edges.json"])
|
||||
if normalized is not None:
|
||||
@@ -170,16 +186,25 @@ def _write_codex_package(
|
||||
(root / "objects").mkdir(parents=True, exist_ok=True)
|
||||
(root / "modules").mkdir(parents=True, exist_ok=True)
|
||||
(root / "raw").mkdir(parents=True, exist_ok=True)
|
||||
(root / "compact").mkdir(parents=True, exist_ok=True)
|
||||
source_map = _copy_codex_sources(input_path, root / "source")
|
||||
compact_objects = _compact_objects(normalized)
|
||||
compact_modules = _compact_modules(normalized)
|
||||
_write_text(root / "AGENTS.md", _codex_agents_markdown(manifest))
|
||||
_write_text(root / "README.md", _codex_readme_markdown(manifest))
|
||||
_write_text(root / "context" / "CODEX_START_HERE.md", _codex_start_here_markdown(manifest))
|
||||
_write_text(root / "context" / "project-overview.md", _ai_context_markdown(manifest, snapshot, normalized))
|
||||
_write_text(root / "context" / "project-brief.md", _project_brief_markdown(manifest, compact_objects, compact_modules))
|
||||
_write_text(root / "context" / "export-plan.md", _export_plan_markdown(manifest["project_id"], Path(manifest["input_path"]), root, binaries, parseable))
|
||||
_write_json(root / "indexes" / "manifest.json", manifest)
|
||||
_write_json(root / "indexes" / "codex-navigation.json", _codex_navigation(manifest, source_map))
|
||||
_write_json(root / "indexes" / "source-inventory.json", {"files": files})
|
||||
_write_json(root / "indexes" / "source-map.json", {"files": source_map})
|
||||
_write_json(root / "indexes" / "project-layout.json", manifest.get("source_layout") or {})
|
||||
_write_json(root / "indexes" / "objects-compact.json", compact_objects)
|
||||
_write_json(root / "indexes" / "modules-compact.json", compact_modules)
|
||||
_write_json(root / "compact" / "objects.json", compact_objects)
|
||||
_write_json(root / "compact" / "modules.json", compact_modules)
|
||||
if snapshot is not None:
|
||||
(root / "raw" / "sir_snapshot.json").write_bytes(snapshot_to_json(snapshot))
|
||||
source_lookup = _source_lookup(source_map)
|
||||
@@ -203,7 +228,7 @@ def _copy_codex_sources(input_path: Path, target: Path) -> list[dict[str, Any]]:
|
||||
for path in source_files:
|
||||
suffix = path.suffix.casefold()
|
||||
relative = path.name if input_path.is_file() else path.relative_to(input_path).as_posix()
|
||||
if suffix not in _CODEX_SOURCE_SUFFIXES:
|
||||
if not _should_copy_codex_source(path, input_path):
|
||||
continue
|
||||
size = path.stat().st_size
|
||||
if size > _MAX_CODEX_SOURCE_FILE_BYTES:
|
||||
@@ -236,6 +261,20 @@ def _copy_codex_sources(input_path: Path, target: Path) -> list[dict[str, Any]]:
|
||||
return copied
|
||||
|
||||
|
||||
def _should_copy_codex_source(path: Path, root: Path) -> bool:
|
||||
suffix = path.suffix.casefold()
|
||||
if suffix == ".bsl" or suffix == ".mdo":
|
||||
return True
|
||||
if suffix not in _CODEX_SOURCE_SUFFIXES:
|
||||
return False
|
||||
if suffix == ".xml":
|
||||
if path.name.casefold() in _ROOT_XML_NAMES:
|
||||
return True
|
||||
parts = [part.casefold() for part in path.relative_to(root).parts[:-1]] if root.is_dir() else []
|
||||
return any(part in {"forms", "configuration", "конфигурация"} for part in parts)
|
||||
return suffix in {".json", ".txt"}
|
||||
|
||||
|
||||
def _source_lookup(source_map: list[dict[str, Any]]) -> dict[str, str]:
|
||||
lookup: dict[str, str] = {}
|
||||
for item in source_map:
|
||||
@@ -262,9 +301,10 @@ def _codex_agents_markdown(manifest: dict[str, Any]) -> str:
|
||||
## Как использовать эту папку
|
||||
|
||||
- Используйте пакет как контекст только для чтения для проекта `{manifest['project_id']}`.
|
||||
- Начинайте с `README.md` и `context/project-overview.md`.
|
||||
- Для точной навигации используйте `indexes/objects.json`, `indexes/modules.json` и `indexes/edges.json`.
|
||||
- Для текста BSL/XML/MDO используйте локальную папку `source/`. Не опирайтесь на абсолютный путь исходников на машине, где пакет был сгенерирован.
|
||||
- Начинайте с `README.md`, `context/project-brief.md` и `context/project-overview.md`.
|
||||
- Для быстрой навигации сначала используйте `indexes/objects-compact.json`, `indexes/modules-compact.json` и `indexes/project-layout.json`.
|
||||
- К тяжелым файлам `indexes/objects.json`, `indexes/modules.json`, `raw/normalized_project.json` и `source/` переходите только когда компактной сводки уже не хватает.
|
||||
- Для текста BSL/XML/MDO используйте локальную папку `source/`. Это выборочная копия нужных исходников, а не полный дубликат всей выгрузки.
|
||||
- Используйте `indexes/source-map.json`, чтобы сопоставлять исходные пути с локальными путями `source/...`.
|
||||
- Если есть `raw/normalized_project.json`, считайте его основной моделью метаданных 1С.
|
||||
- Модули, формы, команды, реквизиты, табличные части и права являются частями объектов 1С-владельцев. Не рассматривайте модуль формы как отдельный независимый файл.
|
||||
@@ -273,10 +313,11 @@ def _codex_agents_markdown(manifest: dict[str, Any]) -> str:
|
||||
|
||||
## Важные файлы
|
||||
|
||||
- `context/project-overview.md` - краткий контекст для человека.
|
||||
- `context/project-brief.md` - короткая сводка для быстрого старта Codex.
|
||||
- `context/project-overview.md` - расширенный контекст для человека.
|
||||
- `context/metadata-tree.md` - дерево метаданных из NormalizedProject.
|
||||
- `indexes/*.json` - машиночитаемые индексы для поиска и рассуждений Codex.
|
||||
- `source/` - локальные UTF-8 копии файлов BSL/XML/MDO.
|
||||
- `indexes/*.json` - машиночитаемые индексы; сначала используйте compact-варианты.
|
||||
- `source/` - выборочные UTF-8 копии BSL/MDO и ключевых XML.
|
||||
- `objects/*.md` - карточки объектов.
|
||||
- `modules/*.md` - карточки модулей.
|
||||
- `raw/*.json` - полная сырая модель SFERA.
|
||||
@@ -295,10 +336,21 @@ def _codex_readme_markdown(manifest: dict[str, Any]) -> str:
|
||||
f"- Узлов SIR: {snapshot.get('nodes', 0)}",
|
||||
f"- Связей SIR: {snapshot.get('edges', 0)}",
|
||||
f"- Нормализованных объектов: {normalized.get('objects', 0)}",
|
||||
f"- Расширений: {normalized.get('extensions', 0)}",
|
||||
"",
|
||||
"Перенесите эту папку целиком в проект Codex, когда хотите, чтобы Codex писал код для этой конфигурации 1С.",
|
||||
"Пакет включает локальную папку `source/`, поэтому Codex сможет читать BSL/XML/MDO после переноса папки.",
|
||||
"Для экономии токенов сначала используйте compact-индексы и brief-контекст, а к `source/` и `raw/` переходите только при необходимости.",
|
||||
]
|
||||
layout = manifest.get("source_layout") or {}
|
||||
if layout:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Структура выгрузки",
|
||||
f"- Основная конфигурация: `{layout.get('main_configuration_root') or 'не определена'}`",
|
||||
f"- Папок расширений: {len(layout.get('extension_roots') or [])}",
|
||||
]
|
||||
)
|
||||
if manifest.get("diagnostics"):
|
||||
lines.extend(["", "## Диагностика"])
|
||||
lines.extend(f"- {item}" for item in manifest["diagnostics"])
|
||||
@@ -315,18 +367,22 @@ def _codex_start_here_markdown(manifest: dict[str, Any]) -> str:
|
||||
|
||||
1. `AGENTS.md`
|
||||
2. `README.md`
|
||||
3. `context/project-overview.md`
|
||||
4. `context/metadata-tree.md`
|
||||
5. `indexes/objects.json`
|
||||
6. `indexes/modules.json`
|
||||
7. `indexes/edges.json`
|
||||
8. `source/`
|
||||
3. `context/project-brief.md`
|
||||
4. `context/project-overview.md`
|
||||
5. `indexes/project-layout.json`
|
||||
6. `indexes/objects-compact.json`
|
||||
7. `indexes/modules-compact.json`
|
||||
8. `context/metadata-tree.md`
|
||||
9. `indexes/objects.json`
|
||||
10. `indexes/modules.json`
|
||||
11. `indexes/edges.json`
|
||||
12. `source/`
|
||||
|
||||
При генерации кода:
|
||||
|
||||
- Сначала найдите объект 1С-владельца.
|
||||
- Затем изучите контекст его модуля, формы и команды.
|
||||
- Для точного текста исходника предпочитайте локальные копии в `source/`.
|
||||
- Для точного текста исходника предпочитайте локальные копии в `source/`, но открывайте их только когда compact-индекса уже недостаточно.
|
||||
- Используйте `raw/normalized_project.json`, когда структура объекта важнее, чем сырой XML.
|
||||
- Используйте `indexes/source-map.json`, если нужно сопоставить ссылки SFERA с локальными путями пакета.
|
||||
"""
|
||||
@@ -340,6 +396,10 @@ def _codex_navigation(manifest: dict[str, Any], source_map: list[dict[str, Any]]
|
||||
"start_here": "context/CODEX_START_HERE.md",
|
||||
"instructions": "AGENTS.md",
|
||||
"overview": "context/project-overview.md",
|
||||
"brief": "context/project-brief.md",
|
||||
"project_layout": "indexes/project-layout.json",
|
||||
"compact_objects_index": "indexes/objects-compact.json",
|
||||
"compact_modules_index": "indexes/modules-compact.json",
|
||||
"metadata_tree": "context/metadata-tree.md",
|
||||
"objects_index": "indexes/objects.json",
|
||||
"modules_index": "indexes/modules.json",
|
||||
@@ -506,6 +566,7 @@ def _ai_context_markdown(manifest: dict[str, Any], snapshot: SirSnapshot | None,
|
||||
"",
|
||||
"## Как ИИ должен использовать этот пакет",
|
||||
"- Используйте `normalized_project.json` как основную модель объектов 1С.",
|
||||
"- Для экономии токенов начинайте с `project-brief.md`, `project-layout.json`, `objects-compact.json` и `modules-compact.json`.",
|
||||
"- Используйте `sir_snapshot.json`, `ai_objects.json`, `ai_modules.json` и `ai_edges.json` для навигации по коду и анализа влияния.",
|
||||
"- Рассматривайте модули, формы и команды как части объектов 1С-владельцев, а не как отдельные текстовые файлы.",
|
||||
]
|
||||
@@ -513,6 +574,111 @@ def _ai_context_markdown(manifest: dict[str, Any], snapshot: SirSnapshot | None,
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _source_layout_summary(root: Path) -> dict[str, Any]:
|
||||
if root.is_file():
|
||||
return {"kind": "file", "main_configuration_root": root.name, "extension_roots": []}
|
||||
children = [path for path in sorted(root.iterdir()) if path.is_dir()]
|
||||
config_dir = next((path for path in children if path.name.casefold() in {"configuration", "конфигурация"}), None)
|
||||
extension_roots = [
|
||||
path.name
|
||||
for path in children
|
||||
if path != config_dir and any(item.suffix.casefold() in {".xml", ".mdo", ".bsl"} for item in path.rglob("*") if item.is_file())
|
||||
]
|
||||
kind = "configuration_with_extensions" if config_dir else "flat_or_mixed"
|
||||
return {
|
||||
"kind": kind,
|
||||
"main_configuration_root": config_dir.name if config_dir else root.name,
|
||||
"extension_roots": extension_roots,
|
||||
}
|
||||
|
||||
|
||||
def _compact_objects(normalized: NormalizedProject | None) -> list[dict[str, Any]]:
|
||||
if normalized is None:
|
||||
return []
|
||||
items: list[dict[str, Any]] = []
|
||||
for group in normalized.configuration.groups:
|
||||
for obj in group.objects:
|
||||
items.append(_compact_object_entry(obj, group.name, extension_name=None))
|
||||
for extension in normalized.configuration.extensions:
|
||||
for group in extension.groups:
|
||||
for obj in group.objects:
|
||||
items.append(_compact_object_entry(obj, group.name, extension_name=extension.name))
|
||||
return items
|
||||
|
||||
|
||||
def _compact_object_entry(obj: Any, group_name: str, extension_name: str | None) -> dict[str, Any]:
|
||||
return {
|
||||
"qualified_name": obj.qualified_name,
|
||||
"name": obj.name,
|
||||
"object_kind": obj.object_kind,
|
||||
"group": group_name,
|
||||
"extension": extension_name,
|
||||
"source_path": obj.source_path,
|
||||
"forms": len(obj.forms),
|
||||
"commands": len(obj.commands),
|
||||
"modules": len(obj.modules),
|
||||
"attributes": len(obj.attributes),
|
||||
"tabular_sections": len(obj.tabular_sections),
|
||||
"layouts": len(obj.layouts),
|
||||
"rights": len(obj.rights),
|
||||
}
|
||||
|
||||
|
||||
def _compact_modules(normalized: NormalizedProject | None) -> list[dict[str, Any]]:
|
||||
if normalized is None:
|
||||
return []
|
||||
items: list[dict[str, Any]] = []
|
||||
for group in normalized.configuration.groups:
|
||||
for obj in group.objects:
|
||||
for module in obj.modules:
|
||||
items.append(_compact_module_entry(obj, module, extension_name=None))
|
||||
for extension in normalized.configuration.extensions:
|
||||
for group in extension.groups:
|
||||
for obj in group.objects:
|
||||
for module in obj.modules:
|
||||
items.append(_compact_module_entry(obj, module, extension_name=extension.name))
|
||||
return items
|
||||
|
||||
|
||||
def _compact_module_entry(owner: Any, module: Any, extension_name: str | None) -> dict[str, Any]:
|
||||
return {
|
||||
"qualified_name": module.qualified_name or module.name,
|
||||
"name": module.name,
|
||||
"module_kind": module.module_kind,
|
||||
"owner": owner.qualified_name,
|
||||
"owner_kind": owner.object_kind,
|
||||
"extension": extension_name,
|
||||
"source_path": module.source_path,
|
||||
}
|
||||
|
||||
|
||||
def _project_brief_markdown(manifest: dict[str, Any], compact_objects: list[dict[str, Any]], compact_modules: list[dict[str, Any]]) -> str:
|
||||
layout = manifest.get("source_layout") or {}
|
||||
top_objects = compact_objects[:40]
|
||||
top_modules = compact_modules[:30]
|
||||
lines = [
|
||||
f"# Brief: {manifest['project_id']}",
|
||||
"",
|
||||
f"- Структура выгрузки: `{layout.get('kind') or 'unknown'}`",
|
||||
f"- Основная конфигурация: `{layout.get('main_configuration_root') or 'не определена'}`",
|
||||
f"- Расширения: {', '.join(layout.get('extension_roots') or []) or 'нет'}",
|
||||
f"- Объектов в compact-индексе: {len(compact_objects)}",
|
||||
f"- Модулей в compact-индексе: {len(compact_modules)}",
|
||||
"",
|
||||
"## Первые объекты",
|
||||
]
|
||||
lines.extend(
|
||||
f"- `{item['qualified_name']}` [{item['object_kind']}] forms={item['forms']} modules={item['modules']} extension={item.get('extension') or 'main'}"
|
||||
for item in top_objects
|
||||
)
|
||||
lines.extend(["", "## Первые модули"])
|
||||
lines.extend(
|
||||
f"- `{item['qualified_name']}` owner=`{item['owner']}` extension={item.get('extension') or 'main'}"
|
||||
for item in top_modules
|
||||
)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _export_plan_markdown(project_id: str, input_path: Path, output_path: Path, binaries: list[dict[str, Any]], parseable: bool) -> str:
|
||||
lines = [
|
||||
f"# План выгрузки 1С для {project_id}",
|
||||
|
||||
@@ -1408,6 +1408,7 @@ class AiStructurePrepareResponse(BaseModel):
|
||||
status: str
|
||||
files_count: int = 0
|
||||
binary_1c_files: list[dict] = Field(default_factory=list)
|
||||
source_layout: dict = Field(default_factory=dict)
|
||||
artifacts: list[str] = Field(default_factory=list)
|
||||
snapshot: dict | None = None
|
||||
normalized: dict | None = None
|
||||
|
||||
@@ -1730,11 +1730,18 @@ def test_ai_structure_prepare_writes_ai_ready_package(tmp_path: Path):
|
||||
assert (output / "manifest.json").exists()
|
||||
assert (output / "normalized_project.json").exists()
|
||||
assert (output / "sir_snapshot.json").exists()
|
||||
assert (output / "project_layout.json").exists()
|
||||
assert (output / "compact_objects.json").exists()
|
||||
assert (output / "compact_modules.json").exists()
|
||||
assert (codex_package / "AGENTS.md").exists()
|
||||
assert (codex_package / "README.md").exists()
|
||||
assert (codex_package / "context" / "CODEX_START_HERE.md").exists()
|
||||
assert (codex_package / "context" / "project-brief.md").exists()
|
||||
assert (codex_package / "context" / "project-overview.md").exists()
|
||||
assert (codex_package / "indexes" / "codex-navigation.json").exists()
|
||||
assert (codex_package / "indexes" / "project-layout.json").exists()
|
||||
assert (codex_package / "indexes" / "objects-compact.json").exists()
|
||||
assert (codex_package / "indexes" / "modules-compact.json").exists()
|
||||
assert (codex_package / "indexes" / "objects.json").exists()
|
||||
assert (codex_package / "indexes" / "source-map.json").exists()
|
||||
assert (codex_package / "raw" / "normalized_project.json").exists()
|
||||
@@ -1745,9 +1752,13 @@ def test_ai_structure_prepare_writes_ai_ready_package(tmp_path: Path):
|
||||
module_docs = "\n".join(path.read_text(encoding="utf-8") for path in (codex_package / "modules").glob("*.md"))
|
||||
assert "Локальный исходник: `source/Интеграция.bsl`" in module_docs
|
||||
assert "Эта папка сгенерирована SFERA для Codex" in (codex_package / "AGENTS.md").read_text(encoding="utf-8")
|
||||
assert "локальную папку `source/`" in (codex_package / "AGENTS.md").read_text(encoding="utf-8")
|
||||
assert "Перенесите эту папку целиком в проект Codex" in (codex_package / "README.md").read_text(encoding="utf-8")
|
||||
assert "выборочная копия нужных исходников" in (codex_package / "AGENTS.md").read_text(encoding="utf-8")
|
||||
assert "compact-индексы" in (codex_package / "README.md").read_text(encoding="utf-8")
|
||||
assert "Рассматривайте модули, формы и команды как части объектов 1С-владельцев" in (output / "ai_context.md").read_text(encoding="utf-8")
|
||||
compact_objects = json.loads((codex_package / "indexes" / "objects-compact.json").read_text(encoding="utf-8"))
|
||||
assert any(item["qualified_name"] == "Справочник.Контрагенты" for item in compact_objects)
|
||||
brief = (codex_package / "context" / "project-brief.md").read_text(encoding="utf-8")
|
||||
assert "Brief: ai-demo" in brief
|
||||
|
||||
page = client.get("/html5/projects/ai-demo/ai-structure")
|
||||
assert_html5_response_contract(
|
||||
@@ -1788,6 +1799,54 @@ def test_ai_structure_prepare_writes_ai_ready_package(tmp_path: Path):
|
||||
assert_html5_response_contract(html5_smb_without_credentials, "ошибка", "логин и пароль SMB")
|
||||
|
||||
|
||||
def test_ai_structure_prepare_understands_configuration_and_extension_folders(tmp_path: Path):
|
||||
source = tmp_path / "xml-export"
|
||||
config = source / "Конфигурация"
|
||||
extension = source / "CRM"
|
||||
config.mkdir(parents=True)
|
||||
extension.mkdir(parents=True)
|
||||
output = tmp_path / "ai-out-layout"
|
||||
(config / "metadata.xml").write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<Catalog name="Контрагенты" qualifiedName="Справочник.Контрагенты" />
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(extension / "РасширениеCRM.mdo").write_text(
|
||||
"""
|
||||
<mdclass:ConfigurationExtension xmlns:mdclass="http://g5.1c.ru/v8/dt/metadata/mdclass/extension">
|
||||
<name>CRM</name>
|
||||
<version>1.0</version>
|
||||
<catalogs>
|
||||
<name>КонтрагентыCRM</name>
|
||||
</catalogs>
|
||||
</mdclass:ConfigurationExtension>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post(
|
||||
"/ai-structure/prepare",
|
||||
json={"project_id": "ai-layout", "input_path": str(source), "output_path": str(output)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["status"] == "ready"
|
||||
assert payload["source_layout"]["main_configuration_root"] == "Конфигурация"
|
||||
assert payload["source_layout"]["extension_roots"] == ["CRM"]
|
||||
assert payload["normalized"]["extensions"] == 1
|
||||
codex_package = output / payload["codex_package_folder"]
|
||||
layout = json.loads((codex_package / "indexes" / "project-layout.json").read_text(encoding="utf-8"))
|
||||
assert layout["kind"] == "configuration_with_extensions"
|
||||
assert layout["main_configuration_root"] == "Конфигурация"
|
||||
compact_objects = json.loads((codex_package / "indexes" / "objects-compact.json").read_text(encoding="utf-8"))
|
||||
assert any(item["extension"] == "CRM" for item in compact_objects)
|
||||
|
||||
|
||||
def test_ai_structure_prepare_reports_cf_cfe_export_required(tmp_path: Path):
|
||||
source = tmp_path / "cf-source"
|
||||
output = tmp_path / "cf-out"
|
||||
|
||||
Reference in New Issue
Block a user