Support combined CF and CFE AI structure imports
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-22 07:29:51 +03:00
parent 2e86d25205
commit 519f10dd6b
3 changed files with 394 additions and 78 deletions
@@ -2,6 +2,7 @@ from __future__ import annotations
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
from pathlib import Path from pathlib import Path
import shutil
from typing import Any from typing import Any
from uuid import uuid4 from uuid import uuid4
@@ -97,6 +98,7 @@ async def html5_ai_structure_run(
save_run_state( save_run_state(
job.job_id, job.job_id,
{ {
**(getattr(job, "state", {}) or {}),
"project_id": project_id, "project_id": project_id,
"effective_project_id": effective_project_id, "effective_project_id": effective_project_id,
"input_path": input_path, "input_path": input_path,
@@ -158,7 +160,7 @@ async def html5_ai_structure_check_path(
return render_html5_ai_structure_path_check(result) return render_html5_ai_structure_path_check(result)
def html5_ai_structure_job( async def html5_ai_structure_job(
*, *,
project_id: str, project_id: str,
job_id: str, job_id: str,
@@ -168,6 +170,7 @@ def html5_ai_structure_job(
save_run_state: Callable[[str, dict[str, Any]], None], save_run_state: Callable[[str, dict[str, Any]], None],
load_job: Callable[[str], object | None], load_job: Callable[[str], object | None],
current_project_source_root: Callable[[str], Path | None], current_project_source_root: Callable[[str], Path | None],
advance_binary_run: Callable[[str, dict[str, Any]], Any] | None = None,
) -> str: ) -> str:
state = load_run_state(job_id) state = load_run_state(job_id)
if state is None: if state is None:
@@ -175,35 +178,59 @@ def html5_ai_structure_job(
if state.get("result") is not None: if state.get("result") is not None:
return render_html5_ai_structure_result(dict(state["result"])) return render_html5_ai_structure_result(dict(state["result"]))
job = load_job(job_id) if state.get("agent_sequence"):
if job is None or str(getattr(job, "project_id", "")) != project_id: if advance_binary_run is None:
return render_html5_ai_structure_error(f"Задача агента не найдена: {job_id}") return render_html5_ai_structure_error("Сервис последовательной выгрузки CF/CFE не подключен.")
step_result = await advance_binary_run(job_id, dict(state))
if step_result.get("phase") == "error":
return render_html5_ai_structure_error(str(step_result.get("error") or "Windows Agent завершил задачу с ошибкой."))
if step_result.get("phase") == "running":
updated_state = dict(step_result.get("state") or state)
save_run_state(job_id, updated_state)
return render_html5_ai_structure_job(
project_id=project_id,
job_id=job_id,
status=str(step_result.get("status") or "RUNNING"),
source=str(step_result.get("source") or ""),
message=str(step_result.get("message") or "Windows Agent выгружает структуру"),
logs=list(step_result.get("logs") or []),
)
if step_result.get("phase") != "completed":
return render_html5_ai_structure_error("Не удалось определить состояние последовательной выгрузки CF/CFE.")
state = dict(step_result.get("state") or state)
save_run_state(job_id, state)
source_roots = [Path(path) for path in list(step_result.get("source_roots") or []) if str(path).strip()]
else:
job = load_job(job_id)
if job is None or str(getattr(job, "project_id", "")) != project_id:
return render_html5_ai_structure_error(f"Задача агента не найдена: {job_id}")
status = _enum_text(getattr(job, "status", "UNKNOWN")) status = _enum_text(getattr(job, "status", "UNKNOWN"))
source = _enum_text(getattr(job, "source", "")) source = _enum_text(getattr(job, "source", ""))
logs = list(getattr(job, "logs", []) or []) logs = list(getattr(job, "logs", []) or [])
if status in {"QUEUED", "RUNNING"}: if status in {"QUEUED", "RUNNING"}:
return render_html5_ai_structure_job( return render_html5_ai_structure_job(
project_id=project_id, project_id=project_id,
job_id=job_id, job_id=job_id,
status=status, status=status,
source=source, source=source,
message="Windows Agent выгружает структуру и передает ее на сервер", message="Windows Agent выгружает структуру и передает ее на сервер",
logs=logs, logs=logs,
) )
if status != "SUCCEEDED": if status != "SUCCEEDED":
error = str(getattr(job, "error", "") or "Windows Agent завершил задачу с ошибкой.") error = str(getattr(job, "error", "") or "Windows Agent завершил задачу с ошибкой.")
if logs: if logs:
error = f"{error} Последние сообщения: {' | '.join(str(item) for item in logs[-4:])}" error = f"{error} Последние сообщения: {' | '.join(str(item) for item in logs[-4:])}"
return render_html5_ai_structure_error(error) return render_html5_ai_structure_error(error)
source_root = current_project_source_root(str(state.get("effective_project_id") or project_id)) source_root = current_project_source_root(str(state.get("effective_project_id") or project_id))
if source_root is None: if source_root is None:
import_summary = getattr(job, "import_summary", None) or {} import_summary = getattr(job, "import_summary", None) or {}
source_path = str(import_summary.get("source_path") or "") source_path = str(import_summary.get("source_path") or "")
source_root = Path(source_path) if source_path else None source_root = Path(source_path) if source_path else None
if source_root is None or not source_root.exists(): if source_root is None or not source_root.exists():
return render_html5_ai_structure_error("После выгрузки агентом сервер не нашел папку с XML/BSL-структурой для подготовки пакета.") return render_html5_ai_structure_error("После выгрузки агентом сервер не нашел папку с XML/BSL-структурой для подготовки пакета.")
source_roots = [source_root]
output_path = str(state.get("output_path") or "") output_path = str(state.get("output_path") or "")
username = str(state.get("username") or "") username = str(state.get("username") or "")
@@ -216,6 +243,7 @@ def html5_ai_structure_job(
try: try:
work_dir.mkdir(parents=True, exist_ok=True) work_dir.mkdir(parents=True, exist_ok=True)
local_output = work_dir / "output" if is_unc_path(output_path) else Path(output_path) local_output = work_dir / "output" if is_unc_path(output_path) else Path(output_path)
source_root = _compose_ai_structure_source_root(work_dir / "source", source_roots)
result = prepare( result = prepare(
project_id=str(state.get("effective_project_id") or project_id), project_id=str(state.get("effective_project_id") or project_id),
input_path=source_root, input_path=source_root,
@@ -296,5 +324,30 @@ def _normalize_binary_match(value: str | dict[str, str] | None) -> dict[str, str
return {"suffix": value, "relative_path": "", "binary_relative_paths": []} return {"suffix": value, "relative_path": "", "binary_relative_paths": []}
def _compose_ai_structure_source_root(target_root: Path, source_roots: list[Path]) -> Path:
existing_roots = [path for path in source_roots if path.exists()]
if not existing_roots:
raise FileNotFoundError("После выгрузки агентом сервер не нашел папки с XML/BSL-структурой для подготовки пакета.")
if len(existing_roots) == 1:
return existing_roots[0]
target_root.mkdir(parents=True, exist_ok=True)
base_root = next((path for path in existing_roots if path.name.casefold().endswith(".cf") is False and (path / "src").exists()), existing_roots[0])
if base_root.exists():
shutil.copytree(base_root, target_root, dirs_exist_ok=True)
used_names: set[str] = set()
for root in existing_roots:
if root == base_root:
continue
name = root.name or f"extension-{len(used_names) + 1}"
candidate = name
index = 2
while candidate in used_names or (target_root / candidate).exists():
candidate = f"{name}-{index}"
index += 1
used_names.add(candidate)
shutil.copytree(root, target_root / candidate, dirs_exist_ok=True)
return target_root
def _enum_text(value: object) -> str: def _enum_text(value: object) -> str:
return str(getattr(value, "value", value or "")) return str(getattr(value, "value", value or ""))
+211 -49
View File
@@ -18,6 +18,7 @@ from collections import Counter
from difflib import SequenceMatcher from difflib import SequenceMatcher
from pathlib import Path from pathlib import Path
from typing import Any, Callable from typing import Any, Callable
from types import SimpleNamespace
from urllib.parse import quote, urljoin, urlsplit, urlunsplit from urllib.parse import quote, urljoin, urlsplit, urlunsplit
from uuid import uuid4 from uuid import uuid4
@@ -334,6 +335,9 @@ def _load_ai_structure_agent_run(job_id: str) -> dict[str, Any] | None:
except (ValueError, UnicodeDecodeError): except (ValueError, UnicodeDecodeError):
password = "" password = ""
result = payload.get("result") result = payload.get("result")
agent_sequence = payload.get("agent_sequence")
completed_steps = payload.get("completed_steps")
sequence_logs = payload.get("sequence_logs")
return { return {
"project_id": str(payload.get("project_id") or ""), "project_id": str(payload.get("project_id") or ""),
"effective_project_id": str(payload.get("effective_project_id") or ""), "effective_project_id": str(payload.get("effective_project_id") or ""),
@@ -345,6 +349,13 @@ def _load_ai_structure_agent_run(job_id: str) -> dict[str, Any] | None:
"domain": str(payload.get("domain") or ""), "domain": str(payload.get("domain") or ""),
"password": password, "password": password,
"result": result if isinstance(result, dict) else None, "result": result if isinstance(result, dict) else None,
"agent_sequence": agent_sequence if isinstance(agent_sequence, list) else [],
"current_step_index": int(payload.get("current_step_index") or 0),
"current_agent_job_id": str(payload.get("current_agent_job_id") or ""),
"agent_id": str(payload.get("agent_id") or ""),
"display_source": str(payload.get("display_source") or ""),
"completed_steps": completed_steps if isinstance(completed_steps, list) else [],
"sequence_logs": sequence_logs if isinstance(sequence_logs, list) else [],
} }
@@ -364,6 +375,13 @@ def _save_ai_structure_agent_run(job_id: str, payload: dict[str, Any]) -> None:
"domain": str(payload.get("domain") or ""), "domain": str(payload.get("domain") or ""),
"password_b64": base64.b64encode(password.encode("utf-8")).decode("ascii") if password else "", "password_b64": base64.b64encode(password.encode("utf-8")).decode("ascii") if password else "",
"result": payload.get("result") if isinstance(payload.get("result"), dict) else None, "result": payload.get("result") if isinstance(payload.get("result"), dict) else None,
"agent_sequence": payload.get("agent_sequence") if isinstance(payload.get("agent_sequence"), list) else [],
"current_step_index": int(payload.get("current_step_index") or 0),
"current_agent_job_id": str(payload.get("current_agent_job_id") or ""),
"agent_id": str(payload.get("agent_id") or ""),
"display_source": str(payload.get("display_source") or ""),
"completed_steps": payload.get("completed_steps") if isinstance(payload.get("completed_steps"), list) else [],
"sequence_logs": payload.get("sequence_logs") if isinstance(payload.get("sequence_logs"), list) else [],
}, },
) )
@@ -612,6 +630,33 @@ def _ai_structure_agent_root_mismatch_detail(agent_id: str, input_path: str, roo
) )
def _ai_structure_step_path(raw_input_path: str, relative_path: str | None) -> Path:
if relative_path:
return Path(ntpath.join(raw_input_path, relative_path.replace("/", "\\")))
return Path(raw_input_path)
async def _queue_ai_structure_agent_step(
*,
project_id: str,
source: ImportSourceKind,
agent_id: str,
local_path: str,
metadata: dict[str, Any],
) -> AgentImportJob:
return await create_agent_import_job(
project_id,
source,
AgentImportJobRequest(
agent_id=agent_id,
source=source,
local_path=local_path,
mode=ImportMode.FULL_REPLACE,
metadata=metadata,
),
)
async def _start_ai_structure_agent_job( async def _start_ai_structure_agent_job(
*, *,
project_id: str, project_id: str,
@@ -631,16 +676,6 @@ async def _start_ai_structure_agent_job(
cf_files = [path for path in binary_files if path.suffix.casefold() == ".cf"] cf_files = [path for path in binary_files if path.suffix.casefold() == ".cf"]
cfe_files = [path for path in binary_files if path.suffix.casefold() == ".cfe"] cfe_files = [path for path in binary_files if path.suffix.casefold() == ".cfe"]
if cf_files and cfe_files:
raise HTTPException(
status_code=400,
detail=(
"Во входной папке одновременно лежат .cf и .cfe. "
f"Найдены: {_format_binary_file_list(binary_files)}. "
"Укажите конкретный файл, который нужно подготовить для ИИ."
),
)
source = ImportSourceKind.CF_FILE if cf_files else ImportSourceKind.CFE_FILE
agent_id = _agent_id_for_source(settings, ImportSourceKind.CF_FILE) agent_id = _agent_id_for_source(settings, ImportSourceKind.CF_FILE)
if not agent_id: if not agent_id:
raise HTTPException( raise HTTPException(
@@ -663,49 +698,85 @@ async def _start_ai_structure_agent_job(
status_code=400, status_code=400,
detail=_ai_structure_agent_root_mismatch_detail(agent_id, input_path, network_roots), detail=_ai_structure_agent_root_mismatch_detail(agent_id, input_path, network_roots),
) )
agent = settings.agent if isinstance(settings.agent, dict) else {} agent = settings.agent if isinstance(settings.agent, dict) else {}
metadata: dict[str, Any] = { if len(cf_files) > 1:
raise HTTPException(
status_code=400,
detail=(
"Для проекта ожидается одна основная конфигурация .cf. "
f"Найдены: {_format_binary_file_list(cf_files)}."
),
)
common_metadata: dict[str, Any] = {
"platform_version": settings.platform_version or None, "platform_version": settings.platform_version or None,
"compatibility_mode": settings.compatibility_mode or None, "compatibility_mode": settings.compatibility_mode or None,
} }
local_path: str | None = None steps: list[dict[str, Any]] = []
if cf_files:
cf_file = cf_files[0]
steps.append(
{
"source": ImportSourceKind.CF_FILE,
"local_path": str(cf_file),
"label": cf_file.name,
"metadata": {**common_metadata, "input_mode": "cf_file"},
}
)
for cfe_file in sorted(cfe_files):
steps.append(
{
"source": ImportSourceKind.CFE_FILE,
"local_path": str(cfe_file),
"label": cfe_file.name,
"metadata": {
**common_metadata,
"input_mode": "cfe_file",
"one_c_extension": cfe_file.stem,
},
}
)
if not steps:
raise HTTPException(status_code=400, detail="Во входном пути не найдены файлы .cf или .cfe.")
if source == ImportSourceKind.CF_FILE: first = steps[0]
if len(cf_files) != 1: first_job = await _queue_ai_structure_agent_step(
raise HTTPException( project_id=effective_project_id,
status_code=400, source=first["source"],
detail=( agent_id=agent_id,
"Для прямого разбора .cf укажите один конкретный файл .cf, " local_path=first["local_path"],
f"а не папку с несколькими конфигурациями. Найдены: {_format_binary_file_list(cf_files)}." metadata=first["metadata"],
), )
) binary_kind = "CF_FILE+CFE_FILE" if cf_files and cfe_files else first["source"].value
local_path = str(cf_files[0]) intro_logs = [
metadata["input_mode"] = "cf_file" (
else: f"Найдено файлов для подготовки: конфигураций {len(cf_files)}, "
if len(cfe_files) != 1: f"расширений {len(cfe_files)}. Сначала выгружается конфигурация, затем все расширения."
raise HTTPException( )
status_code=400, if cf_files and cfe_files
detail=( else f"Найден файл для подготовки: {first['label']}."
"Для прямого разбора расширения укажите один конкретный файл .cfe, " ]
f"а не папку с несколькими расширениями. Найдены: {_format_binary_file_list(cfe_files)}." return SimpleNamespace(
), job_id=first_job.job_id,
) status=first_job.status,
cfe_file = cfe_files[0] source=binary_kind,
local_path = str(cfe_file) logs=[*intro_logs, *list(first_job.logs or [])],
metadata["one_c_extension"] = cfe_file.stem state={
metadata["input_mode"] = "cfe_file" "agent_sequence": [
{
return await create_agent_import_job( "source": step["source"].value,
effective_project_id, "local_path": step["local_path"],
source, "label": step["label"],
AgentImportJobRequest( "metadata": step["metadata"],
agent_id=agent_id, }
source=source, for step in steps
local_path=local_path, ],
mode=ImportMode.FULL_REPLACE, "current_step_index": 0,
metadata=metadata, "current_agent_job_id": first_job.job_id,
), "agent_id": agent_id,
"display_source": binary_kind,
"completed_steps": [],
},
) )
@@ -802,6 +873,96 @@ async def _check_ai_structure_agent_path(*, project_id: str, input_path: str) ->
} }
async def _advance_ai_structure_agent_run(run_id: str, state: dict[str, Any]) -> dict[str, Any]:
sequence = list(state.get("agent_sequence") or [])
if not sequence:
return {"phase": "error", "error": "План выгрузки CF/CFE не найден."}
current_job_id = str(state.get("current_agent_job_id") or run_id)
current_step_index = int(state.get("current_step_index") or 0)
current_job = _agent_import_jobs.get(current_job_id)
if current_job is None:
return {"phase": "error", "error": f"Задача агента не найдена: {current_job_id}"}
display_source = str(state.get("display_source") or getattr(current_job.source, "value", current_job.source or ""))
logs = list(state.get("sequence_logs") or [])
logs.extend(str(item) for item in list(current_job.logs or []))
state["sequence_logs"] = logs[-24:]
if current_job.status in {AgentImportJobStatus.QUEUED, AgentImportJobStatus.RUNNING}:
return {
"phase": "running",
"state": state,
"job_id": run_id,
"status": current_job.status.value,
"source": display_source,
"logs": state["sequence_logs"],
"message": "Windows Agent выгружает структуру и передает ее на сервер",
}
if current_job.status != AgentImportJobStatus.SUCCEEDED:
error = str(current_job.error or "Windows Agent завершил задачу с ошибкой.")
if current_job.logs:
error = f"{error} Последние сообщения: {' | '.join(str(item) for item in current_job.logs[-4:])}"
return {"phase": "error", "error": error}
completed_steps = list(state.get("completed_steps") or [])
if not any(str(item.get("job_id")) == current_job_id for item in completed_steps):
import_summary = current_job.import_summary or {}
source_path = str(import_summary.get("source_path") or current_job.server_path or "")
if not source_path:
return {
"phase": "error",
"error": "После выгрузки агентом сервер не вернул путь к экспортированной структуре.",
}
completed_steps.append(
{
"job_id": current_job_id,
"source": sequence[current_step_index]["source"],
"label": sequence[current_step_index]["label"],
"source_path": source_path,
}
)
state["completed_steps"] = completed_steps
next_step_index = current_step_index + 1
if next_step_index < len(sequence):
next_step = sequence[next_step_index]
next_job = await _queue_ai_structure_agent_step(
project_id=str(state.get("effective_project_id") or state.get("project_id") or ""),
source=ImportSourceKind(next_step["source"]),
agent_id=str(state.get("agent_id") or ""),
local_path=str(next_step["local_path"]),
metadata=dict(next_step.get("metadata") or {}),
)
state["current_step_index"] = next_step_index
state["current_agent_job_id"] = next_job.job_id
state["sequence_logs"] = [
*state["sequence_logs"],
f"Шаг {current_step_index + 1} завершен: {sequence[current_step_index]['label']}.",
f"Запущен следующий шаг {next_step_index + 1}: {next_step['label']}.",
*list(next_job.logs or []),
][-24:]
return {
"phase": "running",
"state": state,
"job_id": run_id,
"status": next_job.status.value,
"source": display_source,
"logs": state["sequence_logs"],
"message": "Windows Agent продолжает выгрузку конфигурации и расширений",
}
state["sequence_logs"] = [
*state["sequence_logs"],
f"Шаг {current_step_index + 1} завершен: {sequence[current_step_index]['label']}.",
"Все найденные CF/CFE выгружены. Сервер собирает объединенный пакет для ИИ.",
][-24:]
return {
"phase": "completed",
"state": state,
"source_roots": [str(item.get("source_path") or "") for item in completed_steps if str(item.get("source_path") or "")],
}
def _ai_structure_binary_files( def _ai_structure_binary_files(
raw_input_path: str, raw_input_path: str,
detected_binary_relative_path: str | None = None, detected_binary_relative_path: str | None = None,
@@ -1978,7 +2139,7 @@ async def html5_project_ai_structure_check_path(project_id: str, request: Reques
@app.get("/html5/projects/{project_id}/ai-structure/jobs/{job_id}") @app.get("/html5/projects/{project_id}/ai-structure/jobs/{job_id}")
async def html5_project_ai_structure_job(project_id: str, job_id: str) -> Response: async def html5_project_ai_structure_job(project_id: str, job_id: str) -> Response:
return _html5_response( return _html5_response(
_html5_ai_structure_job( await _html5_ai_structure_job(
project_id=project_id, project_id=project_id,
job_id=job_id, job_id=job_id,
prepare=_prepare_ai_structure, prepare=_prepare_ai_structure,
@@ -1987,6 +2148,7 @@ async def html5_project_ai_structure_job(project_id: str, job_id: str) -> Respon
save_run_state=_save_ai_structure_agent_run, save_run_state=_save_ai_structure_agent_run,
load_job=lambda current_job_id: _agent_import_jobs.get(current_job_id), load_job=lambda current_job_id: _agent_import_jobs.get(current_job_id),
current_project_source_root=_current_project_source_root, current_project_source_root=_current_project_source_root,
advance_binary_run=_advance_ai_structure_agent_run,
) )
) )
+102 -1
View File
@@ -1962,6 +1962,107 @@ def test_html5_ai_structure_routes_binary_cfe_through_windows_agent(tmp_path: Pa
assert (output / f"codex-1c-context-{project_id}" / "AGENTS.md").exists() assert (output / f"codex-1c-context-{project_id}" / "AGENTS.md").exists()
def test_html5_ai_structure_routes_cf_and_cfe_as_single_project(tmp_path: Path):
base_root = tmp_path / "base-export"
base_root.mkdir()
(base_root / "metadata.xml").write_text(
"""
<Configuration>
<Catalog name="Контрагенты" qualifiedName="Справочник.Контрагенты" />
</Configuration>
""",
encoding="utf-8",
)
extension_root = tmp_path / "extension-export"
extension_root.mkdir()
(extension_root / "Расширение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",
)
source_dir = tmp_path / "binary"
source_dir.mkdir()
(source_dir / "1Cv8.cf").write_bytes(b"binary-cf")
(source_dir / "Marketplace.cfe").write_bytes(b"binary-cfe")
output = tmp_path / "ai-out-mixed"
client = TestClient(app)
project_id = f"ai-agent-mixed-{uuid4()}"
agent_id = f"win-agent-{uuid4()}"
settings = client.post(
f"/projects/{project_id}/settings",
json={"name": "AI Agent Mixed Demo", "structure_source": "CF_FILE", "agent": {"cf_agent_id": agent_id}},
)
assert settings.status_code == 200
heartbeat = client.post("/agent/heartbeat", json={"agent_id": agent_id, "host": "test-host"})
assert heartbeat.status_code == 200
queued = client.post(
f"/html5/projects/{project_id}/ai-structure/run",
data={"project_id": project_id, "input_path": str(source_dir), "output_path": str(output)},
)
assert queued.status_code == 200
assert "конфигураций 1, расширений 1" in queued.text
match = re.search(r"/html5/projects/[^/]+/ai-structure/jobs/([A-Za-z0-9-]+)", queued.text)
assert match is not None
job_id = match.group(1)
first_claimed = client.get("/agent/jobs/next", params={"agent_id": agent_id})
assert first_claimed.status_code == 200
assert first_claimed.json()["job_id"] == job_id
assert first_claimed.json()["source"] == "CF_FILE"
completed_cf = client.post(
f"/agent/jobs/{job_id}/result",
json={"status": "SUCCEEDED", "server_path": str(base_root), "logs": ["Выгрузка конфигурации завершена."]},
)
assert completed_cf.status_code == 200
main._agent_import_jobs[job_id].status = main.AgentImportJobStatus.SUCCEEDED
main._agent_import_jobs[job_id].import_summary = {"source_path": str(base_root)}
deadline = time.monotonic() + 10
second_job = None
while time.monotonic() < deadline:
polled = client.get(f"/html5/projects/{project_id}/ai-structure/jobs/{job_id}")
assert polled.status_code == 200
second_claimed = client.get("/agent/jobs/next", params={"agent_id": agent_id})
assert second_claimed.status_code == 200
second_job = second_claimed.json()
if second_job:
break
time.sleep(0.05)
assert second_job is not None
assert second_job["source"] == "CFE_FILE"
second_job_id = second_job["job_id"]
completed_cfe = client.post(
f"/agent/jobs/{second_job_id}/result",
json={"status": "SUCCEEDED", "server_path": str(extension_root), "logs": ["Выгрузка расширения завершена."]},
)
assert completed_cfe.status_code == 200
main._agent_import_jobs[second_job_id].status = main.AgentImportJobStatus.SUCCEEDED
main._agent_import_jobs[second_job_id].import_summary = {"source_path": str(extension_root)}
deadline = time.monotonic() + 10
fragment = ""
while time.monotonic() < deadline:
polled = client.get(f"/html5/projects/{project_id}/ai-structure/jobs/{job_id}")
assert polled.status_code == 200
fragment = polled.text
if "готово" in fragment:
break
time.sleep(0.05)
assert "готово" in fragment
assert (output / f"codex-1c-context-{project_id}" / "AGENTS.md").exists()
def test_html5_ai_structure_routes_unc_directory_with_cf_through_windows_agent(monkeypatch, tmp_path: Path): def test_html5_ai_structure_routes_unc_directory_with_cf_through_windows_agent(monkeypatch, tmp_path: Path):
from api_server import html5_ai_structure_controller as controller from api_server import html5_ai_structure_controller as controller
@@ -2041,7 +2142,7 @@ def test_html5_ai_structure_reports_multiple_binary_files_in_directory(tmp_path:
data={"project_id": project_id, "input_path": str(tmp_path), "output_path": str(tmp_path / 'out')}, data={"project_id": project_id, "input_path": str(tmp_path), "output_path": str(tmp_path / 'out')},
) )
assert queued.status_code == 200 assert queued.status_code == 200
assert "один конкретный файл .cf" in queued.text assert "одна основная конфигурация .cf" in queued.text
assert "first.cf" in queued.text assert "first.cf" in queued.text
assert "second.cf" in queued.text assert "second.cf" in queued.text