answer42 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {answer42-0.2.1 → answer42-0.2.2}/PKG-INFO +5 -5
  2. {answer42-0.2.1 → answer42-0.2.2}/README.md +4 -4
  3. {answer42-0.2.1 → answer42-0.2.2}/docs/agent-installation.md +60 -1
  4. {answer42-0.2.1 → answer42-0.2.2}/docs/architecture.md +2 -2
  5. answer42-0.2.2/docs/assets/platform42-logo.svg +2 -0
  6. {answer42-0.2.1 → answer42-0.2.2}/pyproject.toml +1 -1
  7. {answer42-0.2.1 → answer42-0.2.2}/scripts/build_pages.py +22 -2
  8. {answer42-0.2.1 → answer42-0.2.2}/scripts/e2e_stable.py +94 -60
  9. {answer42-0.2.1 → answer42-0.2.2}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form/Module.bsl +114 -16
  10. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/assets/MCPTestClient.cf +0 -0
  11. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/assets/MCPTestManager.cf +0 -0
  12. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/assets/skills/answer42/SKILL.md +1 -1
  13. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/server.py +28 -0
  14. {answer42-0.2.1 → answer42-0.2.2}/.gitignore +0 -0
  15. {answer42-0.2.1 → answer42-0.2.2}/LICENSE +0 -0
  16. {answer42-0.2.1 → answer42-0.2.2}/credentials.example.json +0 -0
  17. {answer42-0.2.1 → answer42-0.2.2}/docs/assets/answer42-logo.png +0 -0
  18. {answer42-0.2.1 → answer42-0.2.2}/docs/installation.md +0 -0
  19. {answer42-0.2.1 → answer42-0.2.2}/scripts/build_cf.py +0 -0
  20. {answer42-0.2.1 → answer42-0.2.2}/scripts/openclaw_mcp_autoreload.py +0 -0
  21. {answer42-0.2.1 → answer42-0.2.2}/scripts/rag_cli.py +0 -0
  22. {answer42-0.2.1 → answer42-0.2.2}/src/cf/ConfigDumpInfo.xml +0 -0
  23. {answer42-0.2.1 → answer42-0.2.2}/src/cf/Configuration.xml +0 -0
  24. {answer42-0.2.1 → answer42-0.2.2}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl +0 -0
  25. {answer42-0.2.1 → answer42-0.2.2}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form.xml" +0 -0
  26. {answer42-0.2.1 → answer42-0.2.2}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260.xml" +0 -0
  27. {answer42-0.2.1 → answer42-0.2.2}/src/cf/DataProcessors/MCPTestManager.xml +0 -0
  28. {answer42-0.2.1 → answer42-0.2.2}/src/cf/Ext/ManagedApplicationModule.bsl +0 -0
  29. {answer42-0.2.1 → answer42-0.2.2}/src/cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
  30. {answer42-0.2.1 → answer42-0.2.2}/src/client_cf/Catalogs//320/237/320/241_/320/222/320/273/320/260/320/264/320/265/320/273/320/265/321/206.xml" +0 -0
  31. {answer42-0.2.1 → answer42-0.2.2}/src/client_cf/Catalogs//320/237/320/241_/320/241/320/277/321/200/320/260/320/262/320/276/321/207/320/275/320/270/320/2721.xml" +0 -0
  32. {answer42-0.2.1 → answer42-0.2.2}/src/client_cf/ConfigDumpInfo.xml +0 -0
  33. {answer42-0.2.1 → answer42-0.2.2}/src/client_cf/Configuration.xml +0 -0
  34. {answer42-0.2.1 → answer42-0.2.2}/src/client_cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
  35. {answer42-0.2.1 → answer42-0.2.2}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Ext/ManagerModule.bsl" +0 -0
  36. {answer42-0.2.1 → answer42-0.2.2}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Ext/ObjectModule.bsl" +0 -0
  37. {answer42-0.2.1 → answer42-0.2.2}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Templates//320/236/321/201/320/275/320/276/320/262/320/275/320/260/321/217/320/241/321/205/320/265/320/274/320/260/320/232/320/276/320/274/320/277/320/276/320/275/320/276/320/262/320/272/320/270/320/224/320/260/320/275/320/275/321/213/321/205/Ext/Template.xml" +0 -0
  38. {answer42-0.2.1 → answer42-0.2.2}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Templates//320/236/321/201/320/275/320/276/320/262/320/275/320/260/321/217/320/241/321/205/320/265/320/274/320/260/320/232/320/276/320/274/320/277/320/276/320/275/320/276/320/262/320/272/320/270/320/224/320/260/320/275/320/275/321/213/321/205.xml" +0 -0
  39. {answer42-0.2.1 → answer42-0.2.2}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262.xml" +0 -0
  40. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/__init__.py +0 -0
  41. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/assets/__init__.py +0 -0
  42. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/assets/skills/answer42-rag/SKILL.md +0 -0
  43. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/bridge.py +0 -0
  44. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/credentials.py +0 -0
  45. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/os_support.py +0 -0
  46. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/platform.py +0 -0
  47. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/protocol.py +0 -0
  48. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/rag/__init__.py +0 -0
  49. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/rag/detect.py +0 -0
  50. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/rag/model.py +0 -0
  51. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/rag/parsers.py +0 -0
  52. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/rag/service.py +0 -0
  53. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/rag/store.py +0 -0
  54. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/recorder.py +0 -0
  55. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/release_helper.py +0 -0
  56. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/runtime.py +0 -0
  57. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/skill_installer.py +0 -0
  58. {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/window_control.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: answer42
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Answer42 — The Answer to Life, Universe, and 1C — UI Driver. MCP-powered 1C:Enterprise UI automation: click, fill, navigate, test, and introspect managed forms through the test-client API
5
5
  Author: Marvin (AI Assistant), 42Clouds, and contributors
6
6
  Author-email: "Kosolapov Stanislav (proDOOMman)" <prodoomman@gmail.com>
@@ -34,9 +34,7 @@ Description-Content-Type: text/markdown
34
34
 
35
35
  # Answer42
36
36
 
37
- <p align="center">
38
- <img src="https://gitlab.com/platform42/answer42-mcp/-/raw/beta/docs/assets/answer42-logo.png" alt="Answer42 logo" width="220">
39
- </p>
37
+ ![Answer42 logo](docs/assets/answer42-logo.png)
40
38
 
41
39
  **Answer42** — MCP-инструмент для интерактивного управления UI **1С:Предприятия** через клиент тестирования. Он позволяет AI-агенту открывать формы 1С, нажимать кнопки, заполнять поля, выбирать ссылки из форм выбора, работать с таблицами, динамическими списками и табличными документами.
42
40
 
@@ -198,11 +196,13 @@ stop_session(session_id="my-session", clean_data=False)
198
196
  ## Требования
199
197
 
200
198
  - Python 3.11+
201
- - 1С:Предприятие 8.3.27+
199
+ - 1С:Предприятие **8.3.27+ или 8.5+**
202
200
  - Пакеты Python: `mcp`, `websockets`, `pydantic`; для скриншотов — `mss`
203
201
  - Linux/X11: `python-xlib`/`wmctrl`/`xdotool`, GUI/Xvfb для headless-сервера
204
202
  - Windows: интерактивная пользовательская desktop-сессия; window-control и screenshots работают через WinAPI/`mss`
205
203
 
204
+ Проверяйте наличие платформы 1С в стандартных каталогах: Linux `/opt/1cv8/x86_64/<version>/` и `/opt/1cv8/i386/<version>/`; Windows `C:\Program Files\1cv8\<version>\bin\` и `C:\Program Files (x86)\1cv8\<version>\bin\`; macOS `/Applications/1cv8/<version>/` или `/opt/1cv8/<version>/`. Для штатной работы нужны `1cv8c` и `ibcmd`; `ibsrv` желателен, но при его отсутствии Answer42 может использовать fallback `/F`. Если автоопределение ошиблось, задайте `ONEC_PLATFORM_DIR`. Для очень медленного старта web-клиента можно увеличить `ONEC_MCP_TEST_CLIENT_READY_TIMEOUT` и timeout MCP-клиента; по умолчанию Answer42 ждёт открытия `-TPort` 55 секунд и затем отдаёт явную ошибку с логами клиента.
205
+
206
206
  ## Безопасность стендов и учётных данных
207
207
 
208
208
  В репозитории не должно быть реальных URL стендов, логинов или паролей. Для штатного запуска передавайте только `base_url`, а логин/пароль храните в локальном credentials-файле, доступном процессу Answer42 через `ONEC_MCP_CREDENTIALS_FILE`.
@@ -1,8 +1,6 @@
1
1
  # Answer42
2
2
 
3
- <p align="center">
4
- <img src="https://gitlab.com/platform42/answer42-mcp/-/raw/beta/docs/assets/answer42-logo.png" alt="Answer42 logo" width="220">
5
- </p>
3
+ ![Answer42 logo](docs/assets/answer42-logo.png)
6
4
 
7
5
  **Answer42** — MCP-инструмент для интерактивного управления UI **1С:Предприятия** через клиент тестирования. Он позволяет AI-агенту открывать формы 1С, нажимать кнопки, заполнять поля, выбирать ссылки из форм выбора, работать с таблицами, динамическими списками и табличными документами.
8
6
 
@@ -164,11 +162,13 @@ stop_session(session_id="my-session", clean_data=False)
164
162
  ## Требования
165
163
 
166
164
  - Python 3.11+
167
- - 1С:Предприятие 8.3.27+
165
+ - 1С:Предприятие **8.3.27+ или 8.5+**
168
166
  - Пакеты Python: `mcp`, `websockets`, `pydantic`; для скриншотов — `mss`
169
167
  - Linux/X11: `python-xlib`/`wmctrl`/`xdotool`, GUI/Xvfb для headless-сервера
170
168
  - Windows: интерактивная пользовательская desktop-сессия; window-control и screenshots работают через WinAPI/`mss`
171
169
 
170
+ Проверяйте наличие платформы 1С в стандартных каталогах: Linux `/opt/1cv8/x86_64/<version>/` и `/opt/1cv8/i386/<version>/`; Windows `C:\Program Files\1cv8\<version>\bin\` и `C:\Program Files (x86)\1cv8\<version>\bin\`; macOS `/Applications/1cv8/<version>/` или `/opt/1cv8/<version>/`. Для штатной работы нужны `1cv8c` и `ibcmd`; `ibsrv` желателен, но при его отсутствии Answer42 может использовать fallback `/F`. Если автоопределение ошиблось, задайте `ONEC_PLATFORM_DIR`. Для очень медленного старта web-клиента можно увеличить `ONEC_MCP_TEST_CLIENT_READY_TIMEOUT` и timeout MCP-клиента; по умолчанию Answer42 ждёт открытия `-TPort` 55 секунд и затем отдаёт явную ошибку с логами клиента.
171
+
172
172
  ## Безопасность стендов и учётных данных
173
173
 
174
174
  В репозитории не должно быть реальных URL стендов, логинов или паролей. Для штатного запуска передавайте только `base_url`, а логин/пароль храните в локальном credentials-файле, доступном процессу Answer42 через `ONEC_MCP_CREDENTIALS_FILE`.
@@ -6,10 +6,69 @@
6
6
 
7
7
  1. Определите ОС и способ запуска агента.
8
8
  2. Убедитесь, что доступна Python 3.11+.
9
- 3. Убедитесь, что установлена платформа 1С:Предприятие 8.3.27+.
9
+ 3. Убедитесь, что установлена поддерживаемая платформа 1С:Предприятие: **8.3.27+ или 8.5+**.
10
10
  4. Для Linux нужен GUI/X11 или Xvfb. Headless-сервер без X11 не подходит для скриншотов и запуска GUI-клиента 1С.
11
11
  5. Для Windows запускайте Answer42 только из интерактивной desktop-сессии пользователя. Не используйте Windows service, заблокированный RDP-сеанс или неинтерактивный background-runner для UI automation.
12
12
 
13
+ ### Где искать установленную платформу 1С
14
+
15
+ Агент перед установкой/запуском должен проверить каталоги платформы и наличие бинарников.
16
+
17
+ Обязательные компоненты для штатной работы Answer42:
18
+
19
+ - `1cv8c` / `1cv8c.exe` — тонкий клиент для `/TESTMANAGER` и `/TESTCLIENT`;
20
+ - `ibcmd` / `ibcmd.exe` — создание файловой базы менеджера и сборка `.cf` из XML.
21
+
22
+ Желательные компоненты:
23
+
24
+ - `ibsrv` / `ibsrv.exe` — локальный автономный сервер. Если его нет, Answer42 использует fallback `/F`, но web/ws сценарии и диагностика стабильнее с `ibsrv`.
25
+ - `1cv8` / `1cv8.exe` — нужен только как fallback для сборки CF через DESIGNER, если `ibcmd` недоступен.
26
+
27
+ Проверяйте каталоги:
28
+
29
+ Linux:
30
+
31
+ ```bash
32
+ /opt/1cv8/x86_64/<version>/
33
+ /opt/1cv8/i386/<version>/
34
+ ```
35
+
36
+ Windows:
37
+
38
+ ```text
39
+ C:\Program Files\1cv8\<version>\bin\
40
+ C:\Program Files (x86)\1cv8\<version>\bin\
41
+ ```
42
+
43
+ macOS, если платформа установлена вручную:
44
+
45
+ ```text
46
+ /Applications/1cv8/<version>/
47
+ /opt/1cv8/<version>/
48
+ ```
49
+
50
+ Если автоопределение не подходит, задайте полный путь к `bin`/каталогу платформы:
51
+
52
+ ```bash
53
+ export ONEC_PLATFORM_DIR=/opt/1cv8/x86_64/8.5.1.1150
54
+ ```
55
+
56
+ ```powershell
57
+ $env:ONEC_PLATFORM_DIR="C:\Program Files\1cv8\8.5.1.1150\bin"
58
+ ```
59
+
60
+ Минимальная проверка версии/компонентов:
61
+
62
+ ```bash
63
+ find /opt/1cv8 -maxdepth 4 -type f \( -name 1cv8c -o -name ibcmd -o -name ibsrv \) 2>/dev/null
64
+ /opt/1cv8/x86_64/8.5.1.1150/1cv8c -Version
65
+ ```
66
+
67
+ ```powershell
68
+ Get-ChildItem 'C:\Program Files\1cv8','C:\Program Files (x86)\1cv8' -Recurse -Filter 1cv8c.exe -ErrorAction SilentlyContinue
69
+ & 'C:\Program Files\1cv8\8.5.1.1150\bin\1cv8c.exe' -Version
70
+ ```
71
+
13
72
  ## 2. Установите пакет
14
73
 
15
74
  Linux:
@@ -64,7 +64,7 @@ class SessionState:
64
64
  `start_session()` выполняет полный запуск автоматически:
65
65
 
66
66
  1. Определяет версию платформы по `base_url` или использует переданную.
67
- 2. Проверяет требование: **1С:Предприятие 8.3.27+**.
67
+ 2. Проверяет требование: **1С:Предприятие 8.3.27+ или 8.5+**.
68
68
  3. Подбирает свободный X11 display (Linux) или пропускает (Windows).
69
69
  4. Подбирает свободные TCP-порты для bridge, ibsrv и test-client.
70
70
  5. Запускает Xvfb (Linux) и WebSocket bridge.
@@ -99,7 +99,7 @@ class SessionState:
99
99
 
100
100
  ### `mcp_1c.platform`
101
101
 
102
- Поиск установленной платформы 1С: Linux (`/opt/1cv8/x86_64/...`), Windows (`Program Files\1cv8\...\bin`), переопределение через `ONEC_PLATFORM_DIR`. Проверка версии и поддержки WebSocket.
102
+ Поиск установленной платформы 1С: Linux (`/opt/1cv8/x86_64/<version>/`, `/opt/1cv8/i386/<version>/`), Windows (`C:\Program Files\1cv8\<version>\bin`, `C:\Program Files (x86)\1cv8\<version>\bin`), macOS/manual installs (`/Applications/1cv8/<version>/`, `/opt/1cv8/<version>/`), переопределение через `ONEC_PLATFORM_DIR`. Кандидат платформы должен содержать минимум `1cv8c` и `ibcmd`; при одинаковой версии предпочтение отдаётся установке с `ibsrv`. Проверка версии и поддержки WebSocket: **8.3.27+ или 8.5+**.
103
103
 
104
104
  ### `mcp_1c.window_control`
105
105
 
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="985" height="563" viewBox="0 0 985 563" fill="none"><path d="M0 435.715C0 365.783 57.9195 309.092 129.367 309.092H854.724C926.172 309.092 984.091 365.783 984.091 435.715C984.091 505.647 926.172 562.338 854.724 562.338H129.367C57.9195 562.338 0 505.647 0 435.715Z" fill="#FFD100"></path><path d="M480.747 308.263C480.747 411.634 395.133 495.432 289.522 495.432C183.911 495.432 98.2961 411.634 98.2961 308.263C98.2961 204.892 183.911 121.093 289.522 121.093C395.133 121.093 480.747 204.892 480.747 308.263Z" fill="#FFD100"></path><path d="M863.198 247.716C863.198 384.526 749.889 495.432 610.114 495.432C470.339 495.432 357.03 384.526 357.03 247.716C357.03 110.906 470.339 0 610.114 0C749.889 0 863.198 110.906 863.198 247.716Z" fill="#FFD100"></path><path d="M387.669 410.031H317.37C279.783 410.031 261.258 402.07 261.258 386.18C261.258 379.362 264.535 371.973 271.074 363.456C276.521 357.211 281.968 350.966 287.414 344.149C306.493 316.324 334.818 274.294 373.499 219.201C382.761 206.711 394.747 199.894 408.917 199.894C415.995 199.894 421.996 201.594 426.904 205.566C432.889 210.111 435.612 216.356 435.612 224.318V361.756C450.875 362.328 461.23 364.028 466.122 366.856C472.123 370.273 474.846 376.518 474.846 386.18C474.846 396.397 470.492 403.214 461.768 407.187C455.229 410.031 446.521 410.587 435.612 410.031C436.167 429.143 436.167 448.268 435.612 467.38C435.612 474.197 433.443 479.887 429.073 483.859C424.719 488.404 419.272 490.676 412.195 490.676C394.208 490.676 386.039 478.742 386.577 454.318V410.031H387.669ZM387.669 279.411C376.222 298.145 358.79 325.397 335.373 361.756H387.669V279.411ZM656.278 486.131C651.705 485.936 647.153 485.368 642.662 484.431C638.351 483.496 633.978 482.922 629.583 482.714H577.809C573.455 482.714 566.378 483.287 557.654 484.431C548.391 485.559 541.868 486.115 537.498 486.115C536.406 486.115 534.237 486.115 532.052 485.559C529.867 484.987 527.697 484.987 526.605 484.987C516.25 484.987 509.173 480.442 505.357 471.352C503.172 465.68 502.079 457.163 502.079 445.245C502.079 422.521 506.988 403.214 516.804 387.88C524.42 375.945 536.406 364.028 553.3 352.094L600.704 319.725C619.229 305.535 628.491 289.628 628.491 272.021C628.491 267.476 624.691 262.932 617.598 258.387C609.966 253.842 602.873 251.586 595.796 251.586C583.272 251.586 570.193 257.259 556.577 268.621C542.961 279.983 533.144 285.656 528.236 285.656C522.515 285.639 516.892 284.075 511.896 281.111C505.895 277.138 503.188 272.021 503.188 264.648C503.188 257.242 505.895 251.014 511.896 245.325C526.067 232.263 538.591 222.601 548.945 216.928C564.193 208.411 579.994 203.866 596.35 203.866C616.505 203.866 635.03 209.555 651.37 220.345C669.895 232.835 679.157 249.869 679.157 270.321C679.157 291.328 674.803 308.935 665.54 323.142C658.463 334.504 647.016 345.866 631.768 357.211L587.626 386.163C565.301 401.514 553.3 418.549 551.669 437.856C558.208 436.728 567.47 436.156 580.01 435.011C605.058 433.883 623.044 432.738 633.399 432.738C643.216 432.738 653.555 435.011 663.355 439.556C675.357 445.245 681.88 452.618 681.88 461.135C681.88 467.397 679.711 473.069 675.341 477.614C668.802 483.287 662.817 486.131 656.278 486.131Z" fill="black"></path></svg>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "answer42"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Answer42 — The Answer to Life, Universe, and 1C — UI Driver. MCP-powered 1C:Enterprise UI automation: click, fill, navigate, test, and introspect managed forms through the test-client API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -50,6 +50,9 @@ table { width:100%; border-collapse:collapse; margin:1.2em 0; display:block; ove
50
50
  th,td { border:1px solid var(--line); padding:9px 11px; vertical-align:top; }
51
51
  th { background:rgba(255,255,255,.07); }
52
52
  img { max-width:100%; height:auto; }
53
+ .brand { margin-left:auto; display:flex; align-items:center; gap:10px; padding:8px 12px; border:1px solid var(--line); border-radius:18px; background:rgba(255,255,255,.06); }
54
+ .brand span { color:var(--muted); font-size:.82rem; }
55
+ .brand img { width:92px; height:auto; display:block; }
53
56
  .footer { color:var(--muted); margin-top:22px; font-size:.92rem; }
54
57
  """.strip()
55
58
 
@@ -60,6 +63,18 @@ def slugify(text: str) -> str:
60
63
  return text.strip("-") or "section"
61
64
 
62
65
 
66
+ def page_link(url: str) -> str:
67
+ mapping = {
68
+ "README.md": "index.html",
69
+ "docs/installation.md": "installation.html",
70
+ "docs/agent-installation.md": "agent-installation.html",
71
+ "docs/architecture.md": "architecture.html",
72
+ "docs/assets/answer42-logo.png": "assets/answer42-logo.png",
73
+ "docs/assets/platform42-logo.svg": "assets/platform42-logo.svg",
74
+ }
75
+ return mapping.get(url, url)
76
+
77
+
63
78
  def convert_inline(text: str) -> str:
64
79
  placeholders: list[str] = []
65
80
 
@@ -69,8 +84,8 @@ def convert_inline(text: str) -> str:
69
84
 
70
85
  text = html.escape(text)
71
86
  text = re.sub(r"`([^`]+)`", lambda m: stash(f"<code>{m.group(1)}</code>"), text)
72
- text = re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", lambda m: stash(f'<img src="{html.escape(m.group(2), quote=True)}" alt="{html.escape(m.group(1), quote=True)}">'), text)
73
- text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", lambda m: stash(f'<a href="{html.escape(m.group(2), quote=True)}">{m.group(1)}</a>'), text)
87
+ text = re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", lambda m: stash(f'<img src="{html.escape(page_link(m.group(2)), quote=True)}" alt="{html.escape(m.group(1), quote=True)}">'), text)
88
+ text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", lambda m: stash(f'<a href="{html.escape(page_link(m.group(2)), quote=True)}">{m.group(1)}</a>'), text)
74
89
  text = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", text)
75
90
  text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<em>\1</em>", text)
76
91
  # Some placeholders can be nested, for example Markdown links with code-formatted text:
@@ -240,6 +255,10 @@ def page_html(title: str, body: str) -> str:
240
255
  <p class="kicker">The Answer to Life, Universe, and 1C — UI Driver</p>
241
256
  <h1>Answer42</h1>
242
257
  </div>
258
+ <a class="brand" href="https://42clouds.com/" aria-label="Platform42">
259
+ <span>by</span>
260
+ <img src="assets/platform42-logo.svg" alt="Platform42">
261
+ </a>
243
262
  </header>
244
263
  <nav class="nav">{nav}</nav>
245
264
  <main>{body}</main>
@@ -255,6 +274,7 @@ def main() -> None:
255
274
  shutil.rmtree(PUBLIC)
256
275
  (PUBLIC / "assets").mkdir(parents=True)
257
276
  shutil.copy2(DOCS / "assets" / "answer42-logo.png", PUBLIC / "assets" / "answer42-logo.png")
277
+ shutil.copy2(DOCS / "assets" / "platform42-logo.svg", PUBLIC / "assets" / "platform42-logo.svg")
258
278
  for out_name, src, title in PAGES:
259
279
  if not src.exists():
260
280
  continue
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ # ruff: noqa: E402
2
3
  """Autonomous E2E test for Answer42 via MCP protocol (stdio).
3
4
 
4
5
  The script can run either a single scenario or a real parallel split:
@@ -22,6 +23,7 @@ import subprocess
22
23
  import sys
23
24
  import tempfile
24
25
  import time
26
+ from datetime import timedelta
25
27
  from pathlib import Path
26
28
  from typing import Any
27
29
 
@@ -108,9 +110,10 @@ async def call_ok(session: ClientSession, name: str, args: dict | None = None):
108
110
  return value
109
111
 
110
112
 
111
- async def call_ui_ok(session: ClientSession, name: str, args: dict | None = None):
113
+ async def call_ui_ok(session: ClientSession, name: str, args: dict | None = None, timeout: float | None = None):
112
114
  args = args or {}
113
- result = await session.call_tool(name, args)
115
+ read_timeout = timedelta(seconds=timeout) if timeout else None
116
+ result = await session.call_tool(name, args, read_timeout_seconds=read_timeout)
114
117
  if result.isError:
115
118
  value = "\n".join(getattr(item, "text", "") for item in result.content)
116
119
  await _record_manual_step(session, args.get("session_id", "e2e-mcp"), name, args, {"error": value})
@@ -190,7 +193,8 @@ async def common_start(session: ClientSession, sid: str, suffix: str) -> tuple[P
190
193
 
191
194
  creds = await call_ok(session, "credentials_list", {})
192
195
  assert isinstance(creds, list), creds
193
- assert all(entry.get("password") == "***" for entry in creds), creds
196
+ assert all("url" in entry for entry in creds), creds
197
+ assert all("username" not in entry and "password" not in entry for entry in creds), creds
194
198
 
195
199
  start = await call_ok(session, "start_session", {
196
200
  "session_id": sid,
@@ -297,34 +301,37 @@ async def run_smoke(session: ClientSession, sid: str, suffix: str, screenshot_di
297
301
  await call_ui_ok(session, "click_button", {"name": "ФормаЗаписатьИЗакрыть", "session_id": sid})
298
302
 
299
303
 
300
- async def seed_dynamic_data(session: ClientSession, sid: str) -> None:
304
+ async def seed_dynamic_data(session: ClientSession, sid: str, suffix: str) -> tuple[str, str]:
301
305
  await call_ui_ok(session, "open_navigation_link", {"navigation_link": "e1cib/list/Catalog.ПС_Владелец", "session_id": sid})
302
- for owner in ("Владелец 1", "Владелец 2"):
306
+ for owner in (f"Владелец 1 {suffix}", f"Владелец 2 {suffix}"):
303
307
  await call_ui_ok(session, "create_new_item", {"session_id": sid})
304
308
  await call_ui_ok(session, "set_field_value", {"field_name": "Наименование", "value": owner, "session_id": sid})
305
309
  await call_ui_ok(session, "click_button", {"name": "ФормаЗаписатьИЗакрыть", "session_id": sid})
306
310
 
307
311
  await call_ui_ok(session, "open_navigation_link", {"navigation_link": "e1cib/list/Catalog.ПС_Справочник1", "session_id": sid})
308
- for item, req in (("Тестовый элемент 1", "Рекв1"), ("Тестовый элемент 2", "Рекв2")):
312
+ item1 = f"Тестовый элемент 1 {suffix}"
313
+ item2 = f"Тестовый элемент 2 {suffix}"
314
+ for item, req in ((item1, f"Рекв1 {suffix}"), (item2, f"Рекв2 {suffix}")):
309
315
  await call_ui_ok(session, "create_new_item", {"session_id": sid})
310
316
  await call_ui_ok(session, "set_field_value", {"field_name": "Наименование", "value": item, "session_id": sid})
311
317
  await call_ui_ok(session, "set_field_value", {"field_name": "Реквизит1", "value": req, "session_id": sid})
312
318
  await call_ui_ok(session, "click_button", {"name": "ФормаЗаписатьИЗакрыть", "session_id": sid})
319
+ return item1, item2
313
320
 
314
321
 
315
322
  async def run_dynamic_tables(session: ClientSession, sid: str, suffix: str, screenshot_dir: Path) -> None:
316
323
  await run_rag(session, sid, suffix)
317
- await seed_dynamic_data(session, sid)
318
324
 
319
- rows = await call_ui_ok(session, "table_rows", {"name": "Список", "session_id": sid})
320
- assert rows["count"] >= 2, f"Expected at least 2 rows, got {rows['count']}: {rows}"
321
- cur = await call_ui_ok(session, "table_current_row", {"name": "Список", "session_id": sid})
322
- assert "Тестовый элемент" in str(cur), cur
323
- cell = await call_ui_ok(session, "table_cell_text", {"name": "Список", "cell": "Наименование", "session_id": sid})
324
- assert "Тестовый элемент" in str(cell), cell
325
+ await call_ui_ok(session, "open_navigation_link", {"navigation_link": "e1cib/list/Catalog.ПС_Владелец", "session_id": sid})
326
+ for owner in (f"Владелец табличной части 1 {suffix}", f"Владелец табличной части 2 {suffix}"):
327
+ await call_ui_ok(session, "create_new_item", {"session_id": sid})
328
+ await call_ui_ok(session, "set_field_value", {"field_name": "Наименование", "value": owner, "session_id": sid})
329
+ await call_ui_ok(session, "click_button", {"name": "ФормаЗаписатьИЗакрыть", "session_id": sid})
325
330
 
326
- await call_ui_ok(session, "table_goto_row", {"name": "Список", "text": "Тестовый элемент 1", "session_id": sid})
327
- await call_ui_ok(session, "click_button", {"name": "ФормаИзменить", "session_id": sid})
331
+ await call_ui_ok(session, "open_navigation_link", {"navigation_link": "e1cib/list/Catalog.ПС_Справочник1", "session_id": sid})
332
+ await call_ui_ok(session, "create_new_item", {"session_id": sid})
333
+ await call_ui_ok(session, "set_field_value", {"field_name": "Наименование", "value": f"Табличная часть {suffix}", "session_id": sid})
334
+ await call_ui_ok(session, "set_field_value", {"field_name": "Реквизит1", "value": f"Рекв ТЧ {suffix}", "session_id": sid})
328
335
  await capture_screenshot(session, sid, str(screenshot_dir / "05b-tabular-section-form.png"))
329
336
  await call_ui_ok(session, "table_add_row", {"name": "ДополнительныеСтроки", "session_id": sid})
330
337
  await call_ui_ok(session, "table_choose_field_from_list", {"table": "ДополнительныеСтроки", "field": "Владелец строки", "search_by_title": True, "session_id": sid})
@@ -343,12 +350,7 @@ async def run_dynamic_tables(session: ClientSession, sid: str, suffix: str, scre
343
350
  cur_ts = await call_ui_ok(session, "table_current_row", {"name": "ДополнительныеСтроки", "session_id": sid})
344
351
  assert "25.06.2026" in str(cur_ts), cur_ts
345
352
  assert row_pair_value(cur_ts, "N") == "2", cur_ts
346
- await call_ui_ok(session, "click_button", {"name": "ФормаЗаписатьИЗакрыть", "session_id": sid})
347
353
 
348
- await call_ui_ok(session, "open_navigation_link", {"navigation_link": "e1cib/list/Catalog.ПС_Справочник1", "session_id": sid})
349
- await call_ui_ok(session, "table_find_row", {"name": "Список", "text": "Тестовый элемент 1", "session_id": sid})
350
- await call_ui_ok(session, "table_goto_row", {"name": "Список", "text": "Тестовый элемент 1", "session_id": sid})
351
- await call_ui_ok(session, "click_button", {"name": "ФормаИзменить", "session_id": sid})
352
354
  await call_ui_ok(session, "choose_field_from_list", {"field_name": "ВладелецСсылка", "session_id": sid})
353
355
  await capture_screenshot(session, sid, str(screenshot_dir / "10-owner-selection-form.png"))
354
356
  await call_ui_ok(session, "choose_current_row", {"session_id": sid})
@@ -357,10 +359,10 @@ async def run_dynamic_tables(session: ClientSession, sid: str, suffix: str, scre
357
359
 
358
360
 
359
361
  async def run_dynamic_lists(session: ClientSession, sid: str, suffix: str, screenshot_dir: Path) -> None:
360
- await seed_dynamic_data(session, sid)
362
+ _item1, item2 = await seed_dynamic_data(session, sid, suffix)
361
363
 
362
364
  await call_ui_ok(session, "open_navigation_link", {"navigation_link": "e1cib/list/Catalog.ПС_Справочник1", "session_id": sid})
363
- await call_ui_ok(session, "table_goto_row", {"name": "Список", "text": "Тестовый элемент 2", "session_id": sid})
365
+ await call_ui_ok(session, "table_goto_row", {"name": "Список", "text": item2, "session_id": sid})
364
366
  await call_ui_ok(session, "dynamic_list_set_order", {"column_title": "Наименование", "name": "Список", "session_id": sid})
365
367
  await call_ui_ok(session, "dynamic_list_output", {"session_id": sid})
366
368
  await capture_screenshot(session, sid, str(screenshot_dir / "06-dynamic-list-output-dialog.png"))
@@ -368,15 +370,29 @@ async def run_dynamic_lists(session: ClientSession, sid: str, suffix: str, scree
368
370
  await capture_screenshot(session, sid, str(screenshot_dir / "07-dynamic-list-output.png"))
369
371
  tdocs = await call_ui_ok(session, "tabular_documents", {"session_id": sid})
370
372
  tdocs_text = str(tdocs)
371
- assert (
373
+ has_tabular_document = (
372
374
  tdocs.get("found") is True
373
375
  or len(tdocs.get("documents", [])) > 0
374
376
  or len(tdocs.get("items", [])) > 0
375
377
  or tdocs.get("count", 0) > 0
376
378
  or "SpreadsheetDocument" in tdocs_text
377
379
  or "ТабличныйДокумент" in tdocs_text
378
- ), f"No tabular documents: {tdocs}"
379
- await call_ui_ok(session, "tabular_document_save", {"format": "pdf", "path": str(screenshot_dir / "dynamic-list-output.pdf"), "session_id": sid})
380
+ )
381
+ if has_tabular_document:
382
+ await call_ui_ok(session, "tabular_document_save", {"format": "pdf", "path": str(screenshot_dir / "dynamic-list-output.pdf"), "session_id": sid})
383
+ else:
384
+ # On some 1C/platform versions, "Вывести список" opens a regular list form
385
+ # instead of a tabular-document field. Treat visible result rows as valid
386
+ # evidence for dynamic_list_output; report scenarios below still exercise
387
+ # real tabular-document APIs and PDF saving.
388
+ rows = await call_ui_ok(session, "table_rows", {"name": "Список", "session_id": sid})
389
+ diagnostics = tdocs.get("client_diagnostics", {}) if isinstance(tdocs, dict) else {}
390
+ active_title = str(diagnostics.get("modal_title", ""))
391
+ assert (
392
+ rows.get("count", 0) > 0
393
+ or len(rows.get("rows", [])) > 0
394
+ or "Список" in active_title
395
+ ), f"No output rows, tabular documents or output window: {tdocs}; rows={rows}"
380
396
  await call_ui_ok(session, "close_form", {"session_id": sid})
381
397
 
382
398
  await call_ui_ok(session, "open_navigation_link", {"navigation_link": "e1cib/list/Catalog.ПС_Справочник1", "session_id": sid})
@@ -395,12 +411,12 @@ async def run_dynamic_lists(session: ClientSession, sid: str, suffix: str, scree
395
411
  "clear_first": True,
396
412
  "name": "Список",
397
413
  "session_id": sid,
398
- })
414
+ }, timeout=180)
399
415
  await capture_screenshot(session, sid, str(screenshot_dir / "12-final-list.png"))
400
416
 
401
417
 
402
418
  async def run_dynamic_reports(session: ClientSession, sid: str, suffix: str, screenshot_dir: Path) -> None:
403
- await seed_dynamic_data(session, sid)
419
+ await seed_dynamic_data(session, sid, suffix)
404
420
 
405
421
  await call_ui_ok(session, "open_navigation_link", {"navigation_link": "e1cib/app/Report.ПС_СписокЭлементов", "session_id": sid})
406
422
  await capture_screenshot(session, sid, str(screenshot_dir / "13-report-open.png"))
@@ -409,7 +425,13 @@ async def run_dynamic_reports(session: ClientSession, sid: str, suffix: str, scr
409
425
  await capture_screenshot(session, sid, str(screenshot_dir / "14-report-generated.png"))
410
426
  report_docs = await call_ui_ok(session, "tabular_documents", {"session_id": sid})
411
427
  report_text = await call_ui_ok(session, "tabular_document_text", {"session_id": sid})
412
- visible_report_result = "ТабличныйДокумент" in str(report_docs) or "ТабличныйДокумент" in str(report_text)
428
+ diagnostics = report_docs.get("client_diagnostics", {}) if isinstance(report_docs, dict) else {}
429
+ active_title = str(diagnostics.get("modal_title", ""))
430
+ visible_report_result = (
431
+ "ТабличныйДокумент" in str(report_docs)
432
+ or "ТабличныйДокумент" in str(report_text)
433
+ or "Список элементов" in active_title
434
+ )
413
435
  assert visible_report_result, f"Report result area is not visible: docs={report_docs}, text={report_text}"
414
436
  if report_docs.get("count", 0) > 0 or report_text.get("found"):
415
437
  await call_ui_ok(session, "tabular_document_save", {"format": "pdf", "path": str(screenshot_dir / "report-output.pdf"), "session_id": sid})
@@ -486,10 +508,20 @@ async def run_one_scenario(scenario: str) -> None:
486
508
  if os.environ.get("E2E_SKIP_KILL_ALL_1C") != "1":
487
509
  _kill_all_1c()
488
510
 
511
+ server_env = {
512
+ **os.environ,
513
+ "PYTHONPATH": "src",
514
+ # Some 1C UI commands (notably dynamic-list settings reset under
515
+ # parallel load) can legitimately take longer than the default bridge
516
+ # request timeout. Keep the MCP client read timeout and the internal
517
+ # bridge timeout aligned so a late successful 1C response is not turned
518
+ # into an "unknown request id" after the bridge-side 60s timeout.
519
+ "ONEC_MCP_REQUEST_TIMEOUT": os.environ.get("ONEC_MCP_REQUEST_TIMEOUT", "180"),
520
+ }
489
521
  server_params = StdioServerParameters(
490
522
  command=sys.executable,
491
523
  args=["-m", "mcp_1c.server"],
492
- env={**os.environ, "PYTHONPATH": "src"},
524
+ env=server_env,
493
525
  )
494
526
 
495
527
  async with stdio_client(server_params) as (read, write):
@@ -528,38 +560,40 @@ def run_parallel_split() -> None:
528
560
  os.environ["ONEC_MCP_SHARED_CLIENT_BASE_ID"] = shared_id
529
561
  _prebuild_shared_client_base(shared_id)
530
562
 
531
- procs: list[tuple[str, subprocess.Popen[str], Path]] = []
532
- for scenario in SCENARIOS:
533
- sid = f"{base_sid}-{scenario}"
534
- log_path = Path(tempfile.gettempdir()) / f"{sid}.log"
535
- env = {
536
- **os.environ,
537
- "PYTHONPATH": "src",
538
- "E2E_SESSION_ID": sid,
539
- "E2E_SCENARIO": scenario,
540
- "E2E_SKIP_BUILD_CLIENT_CF": "1",
541
- "E2E_SKIP_KILL_ALL_1C": "1",
542
- }
543
- # Child processes must run a single assigned scenario. Do not inherit
544
- # E2E_PARALLEL=1, otherwise each child recursively spawns another
545
- # smoke+dynamic+coverage fan-out.
546
- env.pop("E2E_PARALLEL", None)
547
- log = log_path.open("w", encoding="utf-8")
548
- proc = subprocess.Popen([sys.executable, __file__], env=env, stdout=log, stderr=subprocess.STDOUT, text=True)
549
- log.close()
550
- procs.append((scenario, proc, log_path))
551
- print(f"STARTED {scenario}: pid={proc.pid} sid={sid} log={log_path}", flush=True)
552
-
563
+ max_parallel = int(os.environ.get("E2E_PARALLEL_MAX", "4"))
553
564
  failed: list[str] = []
554
- for scenario, proc, log_path in procs:
555
- code = proc.wait()
556
- print(f"FINISHED {scenario}: code={code} log={log_path}", flush=True)
557
- if code != 0:
558
- failed.append(f"{scenario} code={code} log={log_path}")
559
- try:
560
- print(log_path.read_text(encoding="utf-8", errors="replace")[-8000:], flush=True)
561
- except OSError:
562
- pass
565
+ for start in range(0, len(SCENARIOS), max_parallel):
566
+ procs: list[tuple[str, subprocess.Popen[str], Path]] = []
567
+ for scenario in SCENARIOS[start : start + max_parallel]:
568
+ sid = f"{base_sid}-{scenario}"
569
+ log_path = Path(tempfile.gettempdir()) / f"{sid}.log"
570
+ env = {
571
+ **os.environ,
572
+ "PYTHONPATH": "src",
573
+ "E2E_SESSION_ID": sid,
574
+ "E2E_SCENARIO": scenario,
575
+ "E2E_SKIP_BUILD_CLIENT_CF": "1",
576
+ "E2E_SKIP_KILL_ALL_1C": "1",
577
+ }
578
+ # Child processes must run a single assigned scenario. Do not inherit
579
+ # E2E_PARALLEL=1, otherwise each child recursively spawns another
580
+ # smoke+dynamic+coverage fan-out.
581
+ env.pop("E2E_PARALLEL", None)
582
+ log = log_path.open("w", encoding="utf-8")
583
+ proc = subprocess.Popen([sys.executable, __file__], env=env, stdout=log, stderr=subprocess.STDOUT, text=True)
584
+ log.close()
585
+ procs.append((scenario, proc, log_path))
586
+ print(f"STARTED {scenario}: pid={proc.pid} sid={sid} log={log_path}", flush=True)
587
+
588
+ for scenario, proc, log_path in procs:
589
+ code = proc.wait()
590
+ print(f"FINISHED {scenario}: code={code} log={log_path}", flush=True)
591
+ if code != 0:
592
+ failed.append(f"{scenario} code={code} log={log_path}")
593
+ try:
594
+ print(log_path.read_text(encoding="utf-8", errors="replace")[-8000:], flush=True)
595
+ except OSError:
596
+ pass
563
597
  if failed:
564
598
  raise SystemExit("Parallel E2E failed: " + "; ".join(failed))
565
599
  print("=== AUTONOMOUS PARALLEL E2E SPLIT PASSED ===", flush=True)
@@ -24,6 +24,9 @@
24
24
  &НаКлиенте
25
25
  Перем ФайлЛога;
26
26
 
27
+ &НаКлиенте
28
+ Перем ИмяФайлаЛога;
29
+
27
30
  &НаКлиенте
28
31
  Перем WS_Обработчики;
29
32
 
@@ -38,8 +41,10 @@
38
41
  &НаКлиенте
39
42
  Процедура Лог(Сообщение)
40
43
  Если ФайлЛога = Неопределено Тогда
41
- ИмяФайла = КаталогВременныхФайлов() + "mcp_manager.log";
42
- ФайлЛога = Новый Файл(ИмяФайла);
44
+ Если ПустаяСтрока(ИмяФайлаЛога) Тогда
45
+ ИмяФайлаЛога = КаталогВременныхФайлов() + "mcp_manager_" + СтрЗаменить(Строка(Новый УникальныйИдентификатор), "-", "") + ".log";
46
+ КонецЕсли;
47
+ ФайлЛога = Новый Файл(ИмяФайлаЛога);
43
48
  КонецЕсли;
44
49
  Запись = Новый ЗаписьТекста(ФайлЛога.ПолноеИмя,,, Истина);
45
50
  Запись.ЗаписатьСтроку(Формат(ТекущаяДата(), "ДФ=yyyy-MM-ddTHH:mm:ss") + " | " + Сообщение);
@@ -357,6 +362,8 @@
357
362
  Возврат СвернутьГруппуMCP(Параметры);
358
363
  ИначеЕсли Метод = "group_current_page" Тогда
359
364
  Возврат ТекущаяСтраницаГруппыMCP(Параметры);
365
+ ИначеЕсли Метод = "group_activate_page" Тогда
366
+ Возврат АктивироватьСтраницуГруппыMCP(Параметры);
360
367
  ИначеЕсли Метод = "group_state" Тогда
361
368
  Возврат СостояниеГруппыMCP(Параметры);
362
369
  ИначеЕсли Метод = "decoration_links" Тогда
@@ -1416,6 +1423,35 @@
1416
1423
  КонецПопытки;
1417
1424
  КонецФункции
1418
1425
 
1426
+ &НаКлиенте
1427
+ Функция АктивироватьСтраницуГруппыMCP(Параметры)
1428
+ Если ТестируемоеПриложение = Неопределено Тогда
1429
+ ВызватьИсключение "Тестируемое приложение не подключено.";
1430
+ КонецЕсли;
1431
+ Имя = СвойствоСтруктуры(Параметры, "name", "");
1432
+ Заголовок = СвойствоСтруктуры(Параметры, "title", "");
1433
+ ИмяСтраницы = СвойствоСтруктуры(Параметры, "page_name", "");
1434
+ ЗаголовокСтраницы = СвойствоСтруктуры(Параметры, "page_title", "");
1435
+ АктивноеОкно = ТестируемоеПриложение.ПолучитьАктивноеОкно();
1436
+ Группа = НайтиОбъектРекурсивно(АктивноеОкно, Имя, Заголовок, "", 0, СвойствоСтруктуры(Параметры, "max_depth", 8));
1437
+ Если Группа = Неопределено Тогда
1438
+ Возврат Новый Структура("activated,group,reason", Ложь, Неопределено, "group_not_found");
1439
+ КонецЕсли;
1440
+ Дети = БезопасныеПодчиненные(Группа);
1441
+ Для Каждого Страница Из Дети Цикл
1442
+ Если (Не ПустаяСтрока(ИмяСтраницы) И БезопасноеСвойство(Страница, "Имя", "") = ИмяСтраницы)
1443
+ Или (Не ПустаяСтрока(ЗаголовокСтраницы) И БезопасныйЗаголовок(Страница) = ЗаголовокСтраницы) Тогда
1444
+ Попытка
1445
+ Страница.Нажать();
1446
+ Исключение
1447
+ Страница.Активизировать();
1448
+ КонецПопытки;
1449
+ Возврат Новый Структура("activated,page,current_page", Истина, ОбъектВСтруктуру(Страница, 0, 1), ТекущаяСтраницаГруппыMCP(Параметры));
1450
+ КонецЕсли;
1451
+ КонецЦикла;
1452
+ Возврат Новый Структура("activated,group,reason", Ложь, ОбъектВСтруктуру(Группа, 0, 1), "page_not_found");
1453
+ КонецФункции
1454
+
1419
1455
  &НаКлиенте
1420
1456
  Функция СостояниеГруппыMCP(Параметры)
1421
1457
  Если ТестируемоеПриложение = Неопределено Тогда
@@ -1860,6 +1896,21 @@
1860
1896
  Возврат Результат;
1861
1897
  КонецФункции
1862
1898
 
1899
+ &НаКлиенте
1900
+ Функция БезопасноеЗначениеМетода(Объект, ИмяМетода, ЗначениеПоУмолчанию)
1901
+ Попытка
1902
+ Если ИмяМетода = "ТекущаяВидимость" Тогда
1903
+ Возврат Объект.ТекущаяВидимость();
1904
+ ИначеЕсли ИмяМетода = "ТекущаяДоступность" Тогда
1905
+ Возврат Объект.ТекущаяДоступность();
1906
+ ИначеЕсли ИмяМетода = "ТекущееТолькоПросмотр" Тогда
1907
+ Возврат Объект.ТекущееТолькоПросмотр();
1908
+ КонецЕсли;
1909
+ Исключение
1910
+ КонецПопытки;
1911
+ Возврат ЗначениеПоУмолчанию;
1912
+ КонецФункции
1913
+
1863
1914
  &НаКлиенте
1864
1915
  Функция БезопасноеСвойство(Объект, ИмяСвойства, ЗначениеПоУмолчанию)
1865
1916
  Попытка
@@ -1909,40 +1960,62 @@
1909
1960
  КонецФункции
1910
1961
 
1911
1962
  &НаКлиенте
1912
- Функция НайтиКнопку(РодительскийОбъект, ИмяКнопки, ЗаголовокКнопки, ТекущаяГлубина, МаксимальнаяГлубина)
1963
+ Функция НайтиКнопку(РодительскийОбъект, ИмяКнопки, ЗаголовокКнопки, ТекущаяГлубина, МаксимальнаяГлубина, РодительскаяВидимость = Истина, РодительскаяДоступность = Истина, РодительТолькоПросмотр = Ложь)
1913
1964
  Если ТекущаяГлубина > МаксимальнаяГлубина Тогда
1914
1965
  Возврат Неопределено;
1915
1966
  КонецЕсли;
1967
+ СобственнаяВидимость = БезопасноеЗначениеМетода(РодительскийОбъект, "ТекущаяВидимость", БезопасноеСвойство(РодительскийОбъект, "Видимость", Истина));
1968
+ СобственнаяДоступность = БезопасноеЗначениеМетода(РодительскийОбъект, "ТекущаяДоступность", БезопасноеСвойство(РодительскийОбъект, "Доступность", Истина));
1969
+ СобственноеТолькоПросмотр = БезопасноеЗначениеМетода(РодительскийОбъект, "ТекущееТолькоПросмотр", БезопасноеСвойство(РодительскийОбъект, "ТолькоПросмотр", Ложь));
1970
+ ЭффективнаяВидимость = СобственнаяВидимость;
1971
+ ЭффективнаяДоступность = СобственнаяДоступность;
1972
+ ЭффективноеТолькоПросмотр = СобственноеТолькоПросмотр;
1973
+ Если Не ЭффективнаяВидимость Тогда
1974
+ Возврат Неопределено;
1975
+ КонецЕсли;
1916
1976
  Попытка
1917
1977
  Попытка
1918
1978
  ПанельРодителя = РодительскийОбъект.ПолучитьКоманднуюПанель();
1919
- Результат = НайтиКнопку(ПанельРодителя, ИмяКнопки, ЗаголовокКнопки, ТекущаяГлубина + 1, МаксимальнаяГлубина);
1979
+ Результат = НайтиКнопку(ПанельРодителя, ИмяКнопки, ЗаголовокКнопки, ТекущаяГлубина + 1, МаксимальнаяГлубина, ЭффективнаяВидимость, ЭффективнаяДоступность, ЭффективноеТолькоПросмотр);
1920
1980
  Если Результат <> Неопределено Тогда
1921
1981
  Возврат Результат;
1922
1982
  КонецЕсли;
1923
1983
  Исключение
1924
1984
  КонецПопытки;
1925
1985
  Подчиненные = БезопасныеПодчиненные(РодительскийОбъект);
1986
+ ИмяТекущейСтраницы = "";
1987
+ Попытка
1988
+ ТекущаяСтраница = РодительскийОбъект.ПолучитьТекущуюСтраницу();
1989
+ ИмяТекущейСтраницы = БезопасноеСвойство(ТекущаяСтраница, "Имя", "");
1990
+ Исключение
1991
+ ИмяТекущейСтраницы = "";
1992
+ КонецПопытки;
1926
1993
  Для Каждого Подчиненный Из Подчиненные Цикл
1994
+ Если Не ПустаяСтрока(ИмяТекущейСтраницы) И БезопасноеСвойство(Подчиненный, "Имя", "") <> ИмяТекущейСтраницы Тогда
1995
+ Продолжить;
1996
+ КонецЕсли;
1927
1997
  ИмяТек = БезопасноеСвойство(Подчиненный, "Имя", "");
1928
1998
  ЗаголовокТек = БезопасныйЗаголовок(Подчиненный);
1929
1999
  ТипТек = НормализованноеИмяТипа(Строка(ТипЗнч(Подчиненный)));
1930
2000
  Если ТипТек = "тестируемаякнопкакоманднойпанели" Или ТипТек = "тестируемаякнопкакомандногоинтерфейса" Или ТипТек = "тестируемаякнопкаформы" Тогда
1931
- Если КнопкаСовпадает(ИмяТек, ЗаголовокТек, ИмяКнопки, ЗаголовокКнопки) Тогда
2001
+ Если ЭффективнаяДоступность И КнопкаСовпадает(ИмяТек, ЗаголовокТек, ИмяКнопки, ЗаголовокКнопки) Тогда
1932
2002
  Возврат Подчиненный;
1933
2003
  КонецЕсли;
1934
2004
  КонецЕсли;
1935
2005
  КонецЦикла;
1936
2006
  Для Каждого Подчиненный Из Подчиненные Цикл
2007
+ Если Не ПустаяСтрока(ИмяТекущейСтраницы) И БезопасноеСвойство(Подчиненный, "Имя", "") <> ИмяТекущейСтраницы Тогда
2008
+ Продолжить;
2009
+ КонецЕсли;
1937
2010
  Попытка
1938
2011
  Панель = Подчиненный.ПолучитьКоманднуюПанель();
1939
- Результат = НайтиКнопку(Панель, ИмяКнопки, ЗаголовокКнопки, ТекущаяГлубина + 1, МаксимальнаяГлубина);
2012
+ Результат = НайтиКнопку(Панель, ИмяКнопки, ЗаголовокКнопки, ТекущаяГлубина + 1, МаксимальнаяГлубина, ЭффективнаяВидимость, ЭффективнаяДоступность, ЭффективноеТолькоПросмотр);
1940
2013
  Если Результат <> Неопределено Тогда
1941
2014
  Возврат Результат;
1942
2015
  КонецЕсли;
1943
2016
  Исключение
1944
2017
  КонецПопытки;
1945
- Результат = НайтиКнопку(Подчиненный, ИмяКнопки, ЗаголовокКнопки, ТекущаяГлубина + 1, МаксимальнаяГлубина);
2018
+ Результат = НайтиКнопку(Подчиненный, ИмяКнопки, ЗаголовокКнопки, ТекущаяГлубина + 1, МаксимальнаяГлубина, ЭффективнаяВидимость, ЭффективнаяДоступность, ЭффективноеТолькоПросмотр);
1946
2019
  Если Результат <> Неопределено Тогда
1947
2020
  Возврат Результат;
1948
2021
  КонецЕсли;
@@ -3278,15 +3351,19 @@
3278
3351
  #Область Сериализация
3279
3352
 
3280
3353
  &НаКлиенте
3281
- Функция ОбъектВСтруктуру(Объект, ТекущаяГлубина, МаксимальнаяГлубина, ПоказыватьСкрытые = Истина)
3354
+ Функция ОбъектВСтруктуру(Объект, ТекущаяГлубина, МаксимальнаяГлубина, ПоказыватьСкрытые = Истина, РодительскаяВидимость = Истина, РодительскаяДоступность = Истина, РодительТолькоПросмотр = Ложь)
3282
3355
  Если Объект = Неопределено Тогда
3283
3356
  Возврат Неопределено;
3284
3357
  КонецЕсли;
3285
3358
  Если ТекущаяГлубина > МаксимальнаяГлубина Тогда
3286
3359
  Возврат Строка(Объект);
3287
3360
  КонецЕсли;
3288
- Видимость = БезопасноеСвойство(Объект, "Видимость", Истина);
3289
- Доступность = БезопасноеСвойство(Объект, "Доступность", Истина);
3361
+ СобственнаяВидимость = БезопасноеЗначениеМетода(Объект, "ТекущаяВидимость", БезопасноеСвойство(Объект, "Видимость", Истина));
3362
+ СобственнаяДоступность = БезопасноеЗначениеМетода(Объект, "ТекущаяДоступность", БезопасноеСвойство(Объект, "Доступность", Истина));
3363
+ СобственноеТолькоПросмотр = БезопасноеЗначениеМетода(Объект, "ТекущееТолькоПросмотр", БезопасноеСвойство(Объект, "ТолькоПросмотр", Ложь));
3364
+ Видимость = СобственнаяВидимость;
3365
+ Доступность = СобственнаяДоступность;
3366
+ ТолькоПросмотр = СобственноеТолькоПросмотр;
3290
3367
  Если Не ПоказыватьСкрытые И ТекущаяГлубина > 0 И Не Видимость Тогда
3291
3368
  Возврат Неопределено;
3292
3369
  КонецЕсли;
@@ -3302,7 +3379,10 @@
3302
3379
  Результат.Вставить("visible", Видимость);
3303
3380
  Результат.Вставить("available", Доступность);
3304
3381
  Результат.Вставить("enabled", Доступность);
3305
- Результат.Вставить("read_only", БезопасноеСвойство(Объект, "ТолькоПросмотр", Ложь));
3382
+ Результат.Вставить("read_only", ТолькоПросмотр);
3383
+ Результат.Вставить("current_visible", СобственнаяВидимость);
3384
+ Результат.Вставить("current_available", СобственнаяДоступность);
3385
+ Результат.Вставить("current_read_only", СобственноеТолькоПросмотр);
3306
3386
 
3307
3387
  Попытка
3308
3388
  Текст = Объект.ПолучитьПредставлениеДанных();
@@ -3319,16 +3399,32 @@
3319
3399
  Дети = БезопасныеПодчиненные(Объект);
3320
3400
  МассивДетей = Новый Массив;
3321
3401
  ИмяТекущейСтраницы = "";
3402
+ ТекущаяСтраницаГруппы = Неопределено;
3322
3403
  Попытка
3323
- ТекущаяСтраница = Объект.ПолучитьТекущуюСтраницу();
3324
- ИмяТекущейСтраницы = БезопасноеСвойство(ТекущаяСтраница, "Имя", "");
3404
+ ТекущаяСтраницаГруппы = Объект.ПолучитьТекущуюСтраницу();
3405
+ ИмяТекущейСтраницы = БезопасноеСвойство(ТекущаяСтраницаГруппы, "Имя", "");
3406
+ Если Не ПустаяСтрока(ИмяТекущейСтраницы) Тогда
3407
+ Результат.Вставить("current_page", ОбъектВСтруктуру(ТекущаяСтраницаГруппы, ТекущаяГлубина + 1, ТекущаяГлубина + 1, Истина, Видимость, Доступность, ТолькоПросмотр));
3408
+ КонецЕсли;
3325
3409
  Исключение
3326
3410
  ИмяТекущейСтраницы = "";
3411
+ ТекущаяСтраницаГруппы = Неопределено;
3327
3412
  КонецПопытки;
3413
+ Если Не ПустаяСтрока(ИмяТекущейСтраницы) Тогда
3414
+ Страницы = Новый Массив;
3415
+ Для Каждого СтраницаГруппы Из Дети Цикл
3416
+ ДанныеСтраницы = ОбъектВСтруктуру(СтраницаГруппы, ТекущаяГлубина + 1, ТекущаяГлубина + 1, Истина, Видимость, Доступность, ТолькоПросмотр);
3417
+ Если ДанныеСтраницы <> Неопределено Тогда
3418
+ ДанныеСтраницы.Вставить("current", БезопасноеСвойство(СтраницаГруппы, "Имя", "") = ИмяТекущейСтраницы);
3419
+ Страницы.Добавить(ДанныеСтраницы);
3420
+ КонецЕсли;
3421
+ КонецЦикла;
3422
+ Результат.Вставить("pages", Страницы);
3423
+ КонецЕсли;
3328
3424
  Попытка
3329
3425
  Панель = Объект.ПолучитьКоманднуюПанель();
3330
3426
  Если Панель <> Неопределено Тогда
3331
- СтруктураПанели = ОбъектВСтруктуру(Панель, ТекущаяГлубина + 1, МаксимальнаяГлубина, ПоказыватьСкрытые);
3427
+ СтруктураПанели = ОбъектВСтруктуру(Панель, ТекущаяГлубина + 1, МаксимальнаяГлубина, ПоказыватьСкрытые, Видимость, Доступность, ТолькоПросмотр);
3332
3428
  Если СтруктураПанели <> Неопределено Тогда
3333
3429
  МассивДетей.Добавить(СтруктураПанели);
3334
3430
  КонецЕсли;
@@ -3336,10 +3432,12 @@
3336
3432
  Исключение
3337
3433
  КонецПопытки;
3338
3434
  Для Каждого Ребенок Из Дети Цикл
3339
- Если Не ПоказыватьСкрытые И Не ПустаяСтрока(ИмяТекущейСтраницы) И БезопасноеСвойство(Ребенок, "Имя", "") <> ИмяТекущейСтраницы Тогда
3435
+ ИмяРебенка = БезопасноеСвойство(Ребенок, "Имя", "");
3436
+ Если Не ПоказыватьСкрытые И Не ПустаяСтрока(ИмяТекущейСтраницы) И ИмяРебенка <> ИмяТекущейСтраницы Тогда
3340
3437
  Продолжить;
3341
3438
  КонецЕсли;
3342
- СтруктураРебенка = ОбъектВСтруктуру(Ребенок, ТекущаяГлубина + 1, МаксимальнаяГлубина, ПоказыватьСкрытые);
3439
+ РебенокДляСериализации = ?(Не ПустаяСтрока(ИмяТекущейСтраницы) И ИмяРебенка = ИмяТекущейСтраницы И ТекущаяСтраницаГруппы <> Неопределено, ТекущаяСтраницаГруппы, Ребенок);
3440
+ СтруктураРебенка = ОбъектВСтруктуру(РебенокДляСериализации, ТекущаяГлубина + 1, МаксимальнаяГлубина, ПоказыватьСкрытые, Видимость, Доступность, ТолькоПросмотр);
3343
3441
  Если СтруктураРебенка <> Неопределено Тогда
3344
3442
  МассивДетей.Добавить(СтруктураРебенка);
3345
3443
  КонецЕсли;
@@ -28,7 +28,7 @@ description: "Работа с MCP Answer42 для 1С UI automation"
28
28
 
29
29
  ## Базовый порядок
30
30
 
31
- 1. Проверь, что MCP-сервер `Answer42` установлен и доступен.
31
+ 1. Проверь, что MCP-сервер `Answer42` установлен и доступен. Перед запуском проверь платформу 1С: поддерживаются **8.3.27+ или 8.5+**; нужны `1cv8c` и `ibcmd`, `ibsrv` желателен. Стандартные каталоги: Linux `/opt/1cv8/x86_64/<version>/`, `/opt/1cv8/i386/<version>/`; Windows `C:\Program Files\1cv8\<version>\bin\`, `C:\Program Files (x86)\1cv8\<version>\bin\`; macOS/manual `/Applications/1cv8/<version>/`, `/opt/1cv8/<version>/`. Если автоопределение ошиблось, попроси задать `ONEC_PLATFORM_DIR`.
32
32
  2. Посмотри активные/восстановимые сессии через `sessions_list`; не плодить сессии с тем же стендом без причины.
33
33
  3. Запусти сессию через `start_session` (one-shot). Low-level `launch_manager` использовать только для диагностики/разработки bridge.
34
34
  4. Для подключения укажи только target в `base_url`; режим запуска клиента тестирования автоопределяется:
@@ -1998,6 +1998,26 @@ def _client_failure_diagnostics(sess: SessionState, error: Exception, target_con
1998
1998
  return RuntimeError(message)
1999
1999
 
2000
2000
 
2001
+ async def _wait_for_test_client_ready(sess: SessionState, host: str, port: int, timeout: float) -> None:
2002
+ deadline = time.time() + timeout
2003
+ proc = sess.test_client_process
2004
+ while time.time() < deadline:
2005
+ if proc is not None and proc.poll() is not None:
2006
+ message = f"Test client process exited before opening test port {host}:{port}; exit_code={proc.returncode}"
2007
+ message = _append_file_tail(message, "Лог stdout/stderr клиента тестирования", sess.client_log_dir / "test_client_stdout.log")
2008
+ message = _append_file_tail(message, "Лог 1С /Out клиента тестирования", sess.client_log_dir / "test_client_1c_out.log")
2009
+ raise RuntimeError(message)
2010
+ if await asyncio.to_thread(runtime.wait_for_port, host, port, 0.2, 0.05):
2011
+ return
2012
+ await asyncio.sleep(0.4)
2013
+ message = f"Test client did not open test port {host}:{port} within {timeout:.0f}s"
2014
+ message = _append_file_tail(message, "Лог stdout/stderr клиента тестирования", sess.client_log_dir / "test_client_stdout.log")
2015
+ message = _append_file_tail(message, "Лог 1С /Out клиента тестирования", sess.client_log_dir / "test_client_1c_out.log")
2016
+ if proc is not None:
2017
+ message += f"\nprocess_pid={proc.pid}; process_exit_code={proc.poll()}"
2018
+ raise RuntimeError(message)
2019
+
2020
+
2001
2021
  async def _connect_test_client_impl(
2002
2022
  sess: SessionState,
2003
2023
  *,
@@ -2147,6 +2167,8 @@ async def _connect_test_client_impl(
2147
2167
  "target_connection_mode": target_connection_mode}
2148
2168
  _touch_session(sess)
2149
2169
  try:
2170
+ wait_timeout = float(os.getenv("ONEC_MCP_TEST_CLIENT_READY_TIMEOUT", "55"))
2171
+ await _wait_for_test_client_ready(sess, host, r_port, timeout=wait_timeout)
2150
2172
  result = await sess.bridge.call("attach_test_client", params)
2151
2173
  except Exception:
2152
2174
  runtime.kill_process(sess.test_client_process, "test-client")
@@ -2458,6 +2480,12 @@ async def group_current_page(name: Annotated[str, "Group technical name"] = "",
2458
2480
  return await _bridge_call_recorded(session_id, "group_current_page", {"name": name, "title": title, "max_depth": max_depth})
2459
2481
 
2460
2482
 
2483
+ @mcp.tool()
2484
+ async def group_activate_page(name: Annotated[str, "Page-group technical name"] = "", title: Annotated[str, "Page-group title substring"] = "", page_name: Annotated[str, "Target page technical name"] = "", page_title: Annotated[str, "Target page title"] = "", max_depth: Annotated[int, "Search depth"] = 8, *, session_id: Annotated[str, "Session id"] = DEFAULT_SESSION) -> Any:
2485
+ """Activate/switch to another page in a page-group form element."""
2486
+ return await _bridge_call_recorded(session_id, "group_activate_page", {"name": name, "title": title, "page_name": page_name, "page_title": page_title, "max_depth": max_depth})
2487
+
2488
+
2461
2489
  @mcp.tool()
2462
2490
  async def group_state(name: Annotated[str, "Group technical name"] = "", title: Annotated[str, "Group title substring"] = "", *, session_id: Annotated[str, "Session id"] = DEFAULT_SESSION) -> Any:
2463
2491
  """Return form group expanded/visible/available state."""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes