w1x-cli 3.5.0__tar.gz → 3.5.1__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.
- {w1x_cli-3.5.0/w1x_cli.egg-info → w1x_cli-3.5.1}/PKG-INFO +31 -8
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/README.md +28 -5
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/pyproject.toml +46 -42
- w1x_cli-3.5.1/tests/test_datetime_and_math.py +41 -0
- w1x_cli-3.5.1/tests/test_regressions.py +217 -0
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x/__init__.py +5 -5
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x/app.py +760 -754
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x/config.py +197 -189
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x/engine.py +569 -565
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x/memory.py +292 -272
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x/models.py +444 -458
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x/puller.py +982 -955
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x/terminal.py +670 -661
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x/testpy.py +328 -324
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x/tools.py +943 -897
- {w1x_cli-3.5.0 → w1x_cli-3.5.1/w1x_cli.egg-info}/PKG-INFO +31 -8
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x_cli.egg-info/SOURCES.txt +2 -0
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/LICENSE +0 -0
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/setup.cfg +0 -0
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x_cli.egg-info/dependency_links.txt +0 -0
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x_cli.egg-info/entry_points.txt +0 -0
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x_cli.egg-info/requires.txt +0 -0
- {w1x_cli-3.5.0 → w1x_cli-3.5.1}/w1x_cli.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: w1x-cli
|
|
3
|
-
Version: 3.5.
|
|
4
|
-
Summary: A fast, keyboard-first CLI terminal assistant for Ollama with Function Calling, Persistent Memory, and Python
|
|
3
|
+
Version: 3.5.1
|
|
4
|
+
Summary: A fast, keyboard-first CLI terminal assistant for Ollama with Function Calling, Persistent Memory, and Interactive Python Runner.
|
|
5
5
|
Author: W1bbex
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/w1bbex/w1x-cli
|
|
8
8
|
Project-URL: Issues, https://github.com/w1bbex/w1x-cli/issues
|
|
9
|
-
Keywords: ollama,cli,terminal,ai,llm,chat,tools,python-
|
|
9
|
+
Keywords: ollama,cli,terminal,ai,llm,chat,tools,python-runner
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Environment :: Console
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
@@ -30,15 +30,16 @@ Dynamic: license-file
|
|
|
30
30
|
|
|
31
31
|
# 🚀 W1x CLI
|
|
32
32
|
|
|
33
|
-
> **A fast, keyboard-first CLI terminal client for Ollama with Function Calling, Persistent Memory, and Interactive Python
|
|
33
|
+
> **A fast, keyboard-first CLI terminal client for Ollama with Function Calling, Persistent Memory, and Interactive Python Runner.**
|
|
34
34
|
|
|
35
35
|

|
|
36
|
+
[](https://pypi.org/project/w1x-cli/)
|
|
36
37
|

|
|
37
38
|

|
|
38
39
|

|
|
39
40
|
|
|
40
41
|
W1x — это легковесный, отзывчивый терминальный ассистент для локальных нейросетей Ollama.
|
|
41
|
-
Он превращает обычные локальные модели (Qwen, Llama, Gemma, DeepSeek) в полноценного автономного помощника с доступом в интернет, точными вычислениями, памятью между сессиями и
|
|
42
|
+
Он превращает обычные локальные модели (Qwen, Llama, Gemma, DeepSeek) в полноценного автономного помощника с доступом в интернет, точными вычислениями, памятью между сессиями и интерактивным запуском кода в отдельном терминале.
|
|
42
43
|
|
|
43
44
|
---
|
|
44
45
|
|
|
@@ -53,8 +54,8 @@ W1x — это легковесный, отзывчивый терминальн
|
|
|
53
54
|
* **Точная дата и время:** вычисление дней недели, смещений и таймзон.
|
|
54
55
|
* **Калькулятор:** точные математические формулы через безопасное синтаксическое дерево.
|
|
55
56
|
* **Чтение файлов проекта:** безопасный доступ к рабочей папке через `/workspace`.
|
|
56
|
-
* 🐍
|
|
57
|
-
* Модель пишет код ➔ вы вводите `/testpy` ➔ открывается
|
|
57
|
+
* 🐍 **Интерактивный REPL-раннер кода (`/testpy`):**
|
|
58
|
+
* Модель пишет код ➔ вы вводите `/testpy` ➔ открывается отдельное окно локального терминала ➔ вы тестируете ввод/вывод вживую ➔ полный лог запуска автоматически возвращается в контекст нейросети (работает с локальным окружением пользователя, не является изолированным контейнером).
|
|
58
59
|
* 🧠 **Долговременная память (`/memory`):**
|
|
59
60
|
* Запоминание фактов пользователя навсегда (`"Запомни: у меня есть кот Барсик"`).
|
|
60
61
|
* Автоматический стемминг падежей русского языка и умная подмешка в контекст.
|
|
@@ -74,7 +75,13 @@ W1x — это легковесный, отзывчивый терминальн
|
|
|
74
75
|
1. Установленный **[Ollama](https://ollama.com/)** (модели можно выбрать прямо при первом запуске W1x).
|
|
75
76
|
2. **Python 3.10+**
|
|
76
77
|
|
|
77
|
-
### Установка (
|
|
78
|
+
### Вариант 1. Установка через PyPI (Рекомендуется)
|
|
79
|
+
|
|
80
|
+
`ash
|
|
81
|
+
pip install --upgrade w1x-cli
|
|
82
|
+
`
|
|
83
|
+
|
|
84
|
+
### Вариант 2. Установка из исходников (Windows / Linux / macOS)
|
|
78
85
|
|
|
79
86
|
Просто запустите **`install.bat`** или выполните в терминале:
|
|
80
87
|
|
|
@@ -163,6 +170,22 @@ w1x-cli/
|
|
|
163
170
|
└── README.md # Документация
|
|
164
171
|
```
|
|
165
172
|
|
|
173
|
+
## 🧪 Тестирование
|
|
174
|
+
|
|
175
|
+
Проект покрыт автоматическими тестами. Для запуска выполните:
|
|
176
|
+
|
|
177
|
+
`ash
|
|
178
|
+
pytest
|
|
179
|
+
`
|
|
180
|
+
|
|
181
|
+
Все **16 тестов** проверяют:
|
|
182
|
+
* Точность вычислений и безопасность математического синтаксического дерева.
|
|
183
|
+
* Изоляцию фоновых задач и корректное завершение воркеров загрузчика.
|
|
184
|
+
* Атомарное сохранение конфигурации и обработку поврежденных файлов сессий.
|
|
185
|
+
* Защиту от рассинхронизации документации и кодовой базы.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
166
189
|
---
|
|
167
190
|
|
|
168
191
|
## 🐛 Сообщить об ошибке и контакты
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# 🚀 W1x CLI
|
|
2
2
|
|
|
3
|
-
> **A fast, keyboard-first CLI terminal client for Ollama with Function Calling, Persistent Memory, and Interactive Python
|
|
3
|
+
> **A fast, keyboard-first CLI terminal client for Ollama with Function Calling, Persistent Memory, and Interactive Python Runner.**
|
|
4
4
|
|
|
5
5
|

|
|
6
|
+
[](https://pypi.org/project/w1x-cli/)
|
|
6
7
|

|
|
7
8
|

|
|
8
9
|

|
|
9
10
|
|
|
10
11
|
W1x — это легковесный, отзывчивый терминальный ассистент для локальных нейросетей Ollama.
|
|
11
|
-
Он превращает обычные локальные модели (Qwen, Llama, Gemma, DeepSeek) в полноценного автономного помощника с доступом в интернет, точными вычислениями, памятью между сессиями и
|
|
12
|
+
Он превращает обычные локальные модели (Qwen, Llama, Gemma, DeepSeek) в полноценного автономного помощника с доступом в интернет, точными вычислениями, памятью между сессиями и интерактивным запуском кода в отдельном терминале.
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
|
@@ -23,8 +24,8 @@ W1x — это легковесный, отзывчивый терминальн
|
|
|
23
24
|
* **Точная дата и время:** вычисление дней недели, смещений и таймзон.
|
|
24
25
|
* **Калькулятор:** точные математические формулы через безопасное синтаксическое дерево.
|
|
25
26
|
* **Чтение файлов проекта:** безопасный доступ к рабочей папке через `/workspace`.
|
|
26
|
-
* 🐍
|
|
27
|
-
* Модель пишет код ➔ вы вводите `/testpy` ➔ открывается
|
|
27
|
+
* 🐍 **Интерактивный REPL-раннер кода (`/testpy`):**
|
|
28
|
+
* Модель пишет код ➔ вы вводите `/testpy` ➔ открывается отдельное окно локального терминала ➔ вы тестируете ввод/вывод вживую ➔ полный лог запуска автоматически возвращается в контекст нейросети (работает с локальным окружением пользователя, не является изолированным контейнером).
|
|
28
29
|
* 🧠 **Долговременная память (`/memory`):**
|
|
29
30
|
* Запоминание фактов пользователя навсегда (`"Запомни: у меня есть кот Барсик"`).
|
|
30
31
|
* Автоматический стемминг падежей русского языка и умная подмешка в контекст.
|
|
@@ -44,7 +45,13 @@ W1x — это легковесный, отзывчивый терминальн
|
|
|
44
45
|
1. Установленный **[Ollama](https://ollama.com/)** (модели можно выбрать прямо при первом запуске W1x).
|
|
45
46
|
2. **Python 3.10+**
|
|
46
47
|
|
|
47
|
-
### Установка (
|
|
48
|
+
### Вариант 1. Установка через PyPI (Рекомендуется)
|
|
49
|
+
|
|
50
|
+
`ash
|
|
51
|
+
pip install --upgrade w1x-cli
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
### Вариант 2. Установка из исходников (Windows / Linux / macOS)
|
|
48
55
|
|
|
49
56
|
Просто запустите **`install.bat`** или выполните в терминале:
|
|
50
57
|
|
|
@@ -133,6 +140,22 @@ w1x-cli/
|
|
|
133
140
|
└── README.md # Документация
|
|
134
141
|
```
|
|
135
142
|
|
|
143
|
+
## 🧪 Тестирование
|
|
144
|
+
|
|
145
|
+
Проект покрыт автоматическими тестами. Для запуска выполните:
|
|
146
|
+
|
|
147
|
+
`ash
|
|
148
|
+
pytest
|
|
149
|
+
`
|
|
150
|
+
|
|
151
|
+
Все **16 тестов** проверяют:
|
|
152
|
+
* Точность вычислений и безопасность математического синтаксического дерева.
|
|
153
|
+
* Изоляцию фоновых задач и корректное завершение воркеров загрузчика.
|
|
154
|
+
* Атомарное сохранение конфигурации и обработку поврежденных файлов сессий.
|
|
155
|
+
* Защиту от рассинхронизации документации и кодовой базы.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
136
159
|
---
|
|
137
160
|
|
|
138
161
|
## 🐛 Сообщить об ошибке и контакты
|
|
@@ -1,42 +1,46 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=61.0"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "w1x-cli"
|
|
7
|
-
version = "3.5.
|
|
8
|
-
description = "A fast, keyboard-first CLI terminal assistant for Ollama with Function Calling, Persistent Memory, and Python
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.10"
|
|
11
|
-
license = { text = "MIT" }
|
|
12
|
-
authors = [
|
|
13
|
-
{ name = "W1bbex" }
|
|
14
|
-
]
|
|
15
|
-
keywords = ["ollama", "cli", "terminal", "ai", "llm", "chat", "tools", "python-
|
|
16
|
-
classifiers = [
|
|
17
|
-
"Development Status :: 4 - Beta",
|
|
18
|
-
"Environment :: Console",
|
|
19
|
-
"Intended Audience :: Developers",
|
|
20
|
-
"License :: OSI Approved :: MIT License",
|
|
21
|
-
"Operating System :: OS Independent",
|
|
22
|
-
"Programming Language :: Python :: 3",
|
|
23
|
-
"Programming Language :: Python :: 3.10",
|
|
24
|
-
"Programming Language :: Python :: 3.11",
|
|
25
|
-
"Programming Language :: Python :: 3.12",
|
|
26
|
-
"Programming Language :: Python :: 3.13",
|
|
27
|
-
"Topic :: Software Development :: User Interfaces",
|
|
28
|
-
]
|
|
29
|
-
dependencies = [
|
|
30
|
-
"ollama>=0.4.0",
|
|
31
|
-
"rich>=13.7.0",
|
|
32
|
-
"prompt_toolkit>=3.0.43",
|
|
33
|
-
"requests>=2.31.0",
|
|
34
|
-
"duckduckgo_search>=7.0.0",
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
[project.scripts]
|
|
38
|
-
w1x = "w1x.app:main"
|
|
39
|
-
|
|
40
|
-
[project.urls]
|
|
41
|
-
Homepage = "https://github.com/w1bbex/w1x-cli"
|
|
42
|
-
Issues = "https://github.com/w1bbex/w1x-cli/issues"
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "w1x-cli"
|
|
7
|
+
version = "3.5.1"
|
|
8
|
+
description = "A fast, keyboard-first CLI terminal assistant for Ollama with Function Calling, Persistent Memory, and Interactive Python Runner."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "W1bbex" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ollama", "cli", "terminal", "ai", "llm", "chat", "tools", "python-runner"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Software Development :: User Interfaces",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"ollama>=0.4.0",
|
|
31
|
+
"rich>=13.7.0",
|
|
32
|
+
"prompt_toolkit>=3.0.43",
|
|
33
|
+
"requests>=2.31.0",
|
|
34
|
+
"duckduckgo_search>=7.0.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
w1x = "w1x.app:main"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/w1bbex/w1x-cli"
|
|
42
|
+
Issues = "https://github.com/w1bbex/w1x-cli/issues"
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
addopts = "--basetemp=.pytest_temp"
|
|
46
|
+
pythonpath = ["."]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pytest
|
|
3
|
+
from w1x.tools import calculate_datetime, calculate_expression, _format_offset
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_calculate_datetime_mixed_offsets():
|
|
8
|
+
# days=1, hours=-2 -> net offset should be +22 hours forward, NOT "1 день 2 часа вперёд"
|
|
9
|
+
res_raw = calculate_datetime(days=1, hours=-2, base_datetime="2026-01-01T12:00:00")
|
|
10
|
+
data = json.loads(res_raw)
|
|
11
|
+
answer = data["answer"]
|
|
12
|
+
assert "22 часа вперёд" in answer
|
|
13
|
+
assert "2026-01-02T10:00:00" in data["result"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_calculate_datetime_negative_offset():
|
|
17
|
+
# days=-1, hours=-2 -> net offset should be 1 day 2 hours backward
|
|
18
|
+
res_raw = calculate_datetime(days=-1, hours=-2, base_datetime="2026-01-05T12:00:00")
|
|
19
|
+
data = json.loads(res_raw)
|
|
20
|
+
answer = data["answer"]
|
|
21
|
+
assert "1 день 2 часа назад" in answer
|
|
22
|
+
assert "2026-01-04T10:00:00" in data["result"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_calculate_expression_exact_float_precision():
|
|
26
|
+
# 0.1 + 0.2 must be strictly "0.3" without IEEE 754 float drift
|
|
27
|
+
res = calculate_expression("0.1 + 0.2")
|
|
28
|
+
assert res == "0.3"
|
|
29
|
+
|
|
30
|
+
res = calculate_expression("100 - 0.01")
|
|
31
|
+
assert res == "99.99"
|
|
32
|
+
|
|
33
|
+
res = calculate_expression("0.1 * 3")
|
|
34
|
+
assert res == "0.3"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_calculate_expression_integer_and_power():
|
|
38
|
+
assert calculate_expression("10 / 2") == "5"
|
|
39
|
+
assert calculate_expression("2 ** 10") == "1024"
|
|
40
|
+
assert calculate_expression("10 // 3") == "3"
|
|
41
|
+
assert calculate_expression("10 % 3") == "1"
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Regression and resilience test suite for w1x-cli.
|
|
3
|
+
Covers:
|
|
4
|
+
- Preload task isolation and error reset under concurrency
|
|
5
|
+
- Worker queue termination atomicity
|
|
6
|
+
- Memory facts sandbox and corrupted file recovery
|
|
7
|
+
- Chat session recovery from invalid payloads
|
|
8
|
+
- Arithmetic parsing without misinterpreting dates or phone numbers
|
|
9
|
+
- Terminal runner and configuration atomic persistence
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
import tempfile
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from unittest.mock import MagicMock, patch
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
from w1x import config, memory, models, tools
|
|
24
|
+
from w1x.app import load_current_chat
|
|
25
|
+
import w1x.app as w1x_app
|
|
26
|
+
from w1x.config import load_user_config, save_user_config
|
|
27
|
+
import w1x.config as w1x_config
|
|
28
|
+
from w1x.engine import calculate_expression
|
|
29
|
+
from w1x.memory import load_facts
|
|
30
|
+
from w1x.models import PreloadTask, wait_for_model_ready, _PRELOAD_LOCK
|
|
31
|
+
import w1x.models as w1x_models
|
|
32
|
+
from w1x.puller import AsyncPullManager, DownloadTask
|
|
33
|
+
from w1x.tools import calculate_datetime
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_preload_task_isolation():
|
|
37
|
+
"""Verify that multiple preload calls create isolated tasks and don't mix events."""
|
|
38
|
+
a_finished = threading.Event()
|
|
39
|
+
b_started = threading.Event()
|
|
40
|
+
|
|
41
|
+
def fake_generate_a(*args, **kwargs):
|
|
42
|
+
b_started.wait(timeout=2.0)
|
|
43
|
+
time.sleep(0.05)
|
|
44
|
+
a_finished.set()
|
|
45
|
+
|
|
46
|
+
with patch("w1x.models.generate", side_effect=fake_generate_a):
|
|
47
|
+
models.start_background_preload("model_a")
|
|
48
|
+
task_a = models._CURRENT_PRELOAD
|
|
49
|
+
assert task_a is not None
|
|
50
|
+
assert task_a.name == "model_a"
|
|
51
|
+
assert not task_a.event.is_set()
|
|
52
|
+
|
|
53
|
+
b_finished = threading.Event()
|
|
54
|
+
def fake_generate_b(*args, **kwargs):
|
|
55
|
+
b_finished.wait(timeout=2.0)
|
|
56
|
+
|
|
57
|
+
with patch("w1x.models.generate", side_effect=fake_generate_b):
|
|
58
|
+
b_started.set()
|
|
59
|
+
models.start_background_preload("model_b")
|
|
60
|
+
task_b = models._CURRENT_PRELOAD
|
|
61
|
+
assert task_b is not None
|
|
62
|
+
assert task_b.name == "model_b"
|
|
63
|
+
assert task_b is not task_a
|
|
64
|
+
|
|
65
|
+
a_finished.wait(timeout=2.0)
|
|
66
|
+
assert task_a.event.is_set()
|
|
67
|
+
assert not task_b.event.is_set()
|
|
68
|
+
|
|
69
|
+
b_finished.set()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_puller_worker_termination_atomicity():
|
|
73
|
+
"""Verify worker terminates safely when queue is empty."""
|
|
74
|
+
mgr = AsyncPullManager()
|
|
75
|
+
t = threading.Thread(target=mgr._worker_loop, daemon=True)
|
|
76
|
+
mgr._worker_thread = t
|
|
77
|
+
t.start()
|
|
78
|
+
t.join(timeout=1.0)
|
|
79
|
+
assert not t.is_alive()
|
|
80
|
+
assert mgr._worker_thread is None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_models_preload_error_clears_task():
|
|
84
|
+
"""Verify failed model preloads clear _CURRENT_PRELOAD so subsequent requests are not blocked."""
|
|
85
|
+
task = PreloadTask(name="broken_model")
|
|
86
|
+
task.error = RuntimeError("Ollama connection failed")
|
|
87
|
+
task.event.set()
|
|
88
|
+
|
|
89
|
+
with _PRELOAD_LOCK:
|
|
90
|
+
w1x_models._CURRENT_PRELOAD = task
|
|
91
|
+
|
|
92
|
+
with pytest.raises(RuntimeError, match="Ollama connection failed"):
|
|
93
|
+
wait_for_model_ready("broken_model")
|
|
94
|
+
|
|
95
|
+
with _PRELOAD_LOCK:
|
|
96
|
+
assert w1x_models._CURRENT_PRELOAD is None
|
|
97
|
+
|
|
98
|
+
wait_for_model_ready("broken_model")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_memory_corrupted_file_moved(tmp_path, monkeypatch):
|
|
102
|
+
"""Verify corrupted memory-facts.json is archived to .corrupted.<timestamp>.json and reset."""
|
|
103
|
+
facts_file = tmp_path / "memory-facts.json"
|
|
104
|
+
facts_file.write_text("{broken json syntax: missing quotes", encoding="utf-8")
|
|
105
|
+
monkeypatch.setattr("w1x.memory._get_facts_path", lambda: facts_file)
|
|
106
|
+
|
|
107
|
+
res1 = load_facts()
|
|
108
|
+
assert res1 == []
|
|
109
|
+
assert not facts_file.exists()
|
|
110
|
+
|
|
111
|
+
corrupted_files = list(tmp_path.glob("memory-facts.corrupted.*.json"))
|
|
112
|
+
assert len(corrupted_files) == 1
|
|
113
|
+
|
|
114
|
+
res2 = load_facts()
|
|
115
|
+
assert res2 == []
|
|
116
|
+
corrupted_files_after = list(tmp_path.glob("memory-facts.corrupted.*.json"))
|
|
117
|
+
assert len(corrupted_files_after) == 1
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_memory_sandbox_redirection(tmp_path, monkeypatch):
|
|
121
|
+
"""Verify mutating config paths redirects facts storage without polluting user home."""
|
|
122
|
+
sandbox_facts = tmp_path / "test-facts.json"
|
|
123
|
+
sandbox_chat = tmp_path / "test-chat"
|
|
124
|
+
|
|
125
|
+
monkeypatch.setattr(config, "FACTS_PATH", sandbox_facts)
|
|
126
|
+
monkeypatch.setattr(config, "CHAT_DIR", sandbox_chat)
|
|
127
|
+
|
|
128
|
+
assert memory._get_facts_path() == sandbox_facts
|
|
129
|
+
assert memory._get_chat_dir() == sandbox_chat
|
|
130
|
+
assert memory.load_facts() == []
|
|
131
|
+
|
|
132
|
+
memory.save_facts([{"fact": "Test fact", "created_at": "2026-11-04"}])
|
|
133
|
+
assert sandbox_facts.exists()
|
|
134
|
+
assert len(memory.load_facts()) == 1
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_tools_datetime_offset_safe_types():
|
|
138
|
+
"""Verify datetime offset parameters accept flexible string floats and None safely."""
|
|
139
|
+
res_str = calculate_datetime(days="1.5", hours=None, minutes="0", seconds=10)
|
|
140
|
+
data = json.loads(res_str)
|
|
141
|
+
assert "answer" in data or "datetime" in data or "error" not in data
|
|
142
|
+
|
|
143
|
+
res_err = calculate_datetime(days="invalid_days")
|
|
144
|
+
data_err = json.loads(res_err)
|
|
145
|
+
assert "error" in data_err
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_app_load_current_chat_corrupted_list(tmp_path, monkeypatch):
|
|
149
|
+
"""Verify chat memory loader recovers gracefully from empty or list-typed JSON."""
|
|
150
|
+
chat_file = tmp_path / "current-chat.json"
|
|
151
|
+
chat_file.write_text("[]", encoding="utf-8")
|
|
152
|
+
monkeypatch.setattr(w1x_app, "CURRENT_CHAT_PATH", chat_file)
|
|
153
|
+
|
|
154
|
+
sys_prompt, messages = load_current_chat()
|
|
155
|
+
assert sys_prompt == w1x_app.DEFAULT_SYSTEM_PROMPT
|
|
156
|
+
assert len(messages) >= 1
|
|
157
|
+
assert messages[0]["role"] == "system"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_engine_dates_and_phones_not_computed_as_math():
|
|
161
|
+
"""Verify dates and phone numbers are excluded from automated arithmetic precomputation."""
|
|
162
|
+
text = "События 2026-11-08 и звонок на номер +7-999-123-45-67, но посчитай 25 * 4"
|
|
163
|
+
math_candidates = re.findall(r"[\d\s().,+\-*/%]{3,}", text.replace("×", "*").replace("÷", "/"))
|
|
164
|
+
precomputed = []
|
|
165
|
+
for cand in math_candidates:
|
|
166
|
+
cand_clean = cand.strip(" ,.")
|
|
167
|
+
if re.search(r"\b\d{4}-\d{2}-\d{2}\b", cand_clean) or re.search(r"\b\d{2}\.\d{2}\.\d{4}\b", cand_clean):
|
|
168
|
+
continue
|
|
169
|
+
if re.search(r"^\+?\d[\d\s-]{7,}\d$", cand_clean):
|
|
170
|
+
continue
|
|
171
|
+
if re.search(r"[+\-*/%]", cand_clean) and re.search(r"\d", cand_clean) and len(cand_clean) >= 3:
|
|
172
|
+
val = calculate_expression(cand_clean)
|
|
173
|
+
if not val.startswith("Ошибка"):
|
|
174
|
+
precomputed.append(f"{cand_clean} = {val}")
|
|
175
|
+
|
|
176
|
+
assert not any("2026-11-08" in item for item in precomputed)
|
|
177
|
+
assert not any("999-123" in item for item in precomputed)
|
|
178
|
+
assert any("25 * 4 = 100" in item for item in precomputed)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_config_atomic_save(tmp_path, monkeypatch):
|
|
182
|
+
"""Verify user configuration is saved atomically via temporary replacement."""
|
|
183
|
+
cfg_file = tmp_path / "config.json"
|
|
184
|
+
monkeypatch.setattr(w1x_config, "CONFIG_FILE_PATH", cfg_file)
|
|
185
|
+
|
|
186
|
+
save_user_config({"test_key": "test_val"})
|
|
187
|
+
assert cfg_file.exists()
|
|
188
|
+
saved = json.loads(cfg_file.read_text(encoding="utf-8"))
|
|
189
|
+
assert saved.get("test_key") == "test_val"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_reset_all_models_accepts_show_message():
|
|
193
|
+
"""Verify reset_all_models accepts show_message parameter without crash."""
|
|
194
|
+
res = models.reset_all_models(show_message=True)
|
|
195
|
+
assert isinstance(res, (list, bool, type(None)))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_set_file_workspace_syncs_globals(tmp_path):
|
|
199
|
+
"""Verify set_file_workspace updates both tools and config roots."""
|
|
200
|
+
orig = getattr(config, "ACTIVE_FILE_ROOT", None)
|
|
201
|
+
try:
|
|
202
|
+
tools.set_file_workspace(str(tmp_path))
|
|
203
|
+
assert tools.ACTIVE_FILE_ROOT == tmp_path.resolve()
|
|
204
|
+
assert config.ACTIVE_FILE_ROOT == tmp_path.resolve()
|
|
205
|
+
finally:
|
|
206
|
+
config.ACTIVE_FILE_ROOT = orig
|
|
207
|
+
tools.ACTIVE_FILE_ROOT = orig
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_testpy_runner_code_templates():
|
|
211
|
+
"""Verify interactive test runner template handles dynamic streams and error codes."""
|
|
212
|
+
import inspect
|
|
213
|
+
from w1x import testpy
|
|
214
|
+
source = inspect.getsource(testpy.run_interactive_python_test)
|
|
215
|
+
assert "__getattr__" in source
|
|
216
|
+
assert "getattr(self.original, name)" in source
|
|
217
|
+
assert "exit_code = exc.code if isinstance(exc.code, int) else (1 if exc.code is not None else 0)" in source
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""W1x — Персональный локальный ИИ."""
|
|
2
|
-
|
|
3
|
-
from .app import main
|
|
4
|
-
__version__ = "3.5"
|
|
5
|
-
__all__ = ["main"]
|
|
1
|
+
"""W1x — Персональный локальный ИИ."""
|
|
2
|
+
|
|
3
|
+
from .app import main
|
|
4
|
+
__version__ = "3.5.1"
|
|
5
|
+
__all__ = ["main"]
|