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.
- {answer42-0.2.1 → answer42-0.2.2}/PKG-INFO +5 -5
- {answer42-0.2.1 → answer42-0.2.2}/README.md +4 -4
- {answer42-0.2.1 → answer42-0.2.2}/docs/agent-installation.md +60 -1
- {answer42-0.2.1 → answer42-0.2.2}/docs/architecture.md +2 -2
- answer42-0.2.2/docs/assets/platform42-logo.svg +2 -0
- {answer42-0.2.1 → answer42-0.2.2}/pyproject.toml +1 -1
- {answer42-0.2.1 → answer42-0.2.2}/scripts/build_pages.py +22 -2
- {answer42-0.2.1 → answer42-0.2.2}/scripts/e2e_stable.py +94 -60
- {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
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/assets/MCPTestClient.cf +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/assets/MCPTestManager.cf +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/assets/skills/answer42/SKILL.md +1 -1
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/server.py +28 -0
- {answer42-0.2.1 → answer42-0.2.2}/.gitignore +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/LICENSE +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/credentials.example.json +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/docs/assets/answer42-logo.png +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/docs/installation.md +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/scripts/build_cf.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/scripts/openclaw_mcp_autoreload.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/scripts/rag_cli.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/cf/ConfigDumpInfo.xml +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/cf/Configuration.xml +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl +0 -0
- {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
- {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
- {answer42-0.2.1 → answer42-0.2.2}/src/cf/DataProcessors/MCPTestManager.xml +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/cf/Ext/ManagedApplicationModule.bsl +0 -0
- {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
- {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
- {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
- {answer42-0.2.1 → answer42-0.2.2}/src/client_cf/ConfigDumpInfo.xml +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/client_cf/Configuration.xml +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/__init__.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/assets/__init__.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/assets/skills/answer42-rag/SKILL.md +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/bridge.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/credentials.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/os_support.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/platform.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/protocol.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/rag/__init__.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/rag/detect.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/rag/model.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/rag/parsers.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/rag/service.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/rag/store.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/recorder.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/release_helper.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/runtime.py +0 -0
- {answer42-0.2.1 → answer42-0.2.2}/src/mcp_1c/skill_installer.py +0 -0
- {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.
|
|
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
|
-
|
|
38
|
-
<img src="https://gitlab.com/platform42/answer42-mcp/-/raw/beta/docs/assets/answer42-logo.png" alt="Answer42 logo" width="220">
|
|
39
|
-
</p>
|
|
37
|
+

|
|
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
|
-
|
|
4
|
-
<img src="https://gitlab.com/platform42/answer42-mcp/-/raw/beta/docs/assets/answer42-logo.png" alt="Answer42 logo" width="220">
|
|
5
|
-
</p>
|
|
3
|
+

|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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) ->
|
|
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
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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, "
|
|
327
|
-
await call_ui_ok(session, "
|
|
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":
|
|
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
|
-
|
|
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
|
-
)
|
|
379
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3439
|
+
РебенокДляСериализации = ?(Не ПустаяСтрока(ИмяТекущейСтраницы) И ИмяРебенка = ИмяТекущейСтраницы И ТекущаяСтраницаГруппы <> Неопределено, ТекущаяСтраницаГруппы, Ребенок);
|
|
3440
|
+
СтруктураРебенка = ОбъектВСтруктуру(РебенокДляСериализации, ТекущаяГлубина + 1, МаксимальнаяГлубина, ПоказыватьСкрытые, Видимость, Доступность, ТолькоПросмотр);
|
|
3343
3441
|
Если СтруктураРебенка <> Неопределено Тогда
|
|
3344
3442
|
МассивДетей.Добавить(СтруктураРебенка);
|
|
3345
3443
|
КонецЕсли;
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|