mito-ai 0.1.33__py3-none-any.whl → 0.1.49__py3-none-any.whl
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.
- mito_ai/__init__.py +49 -9
- mito_ai/_version.py +1 -1
- mito_ai/anthropic_client.py +142 -67
- mito_ai/{app_builder → app_deploy}/__init__.py +1 -1
- mito_ai/app_deploy/app_deploy_utils.py +44 -0
- mito_ai/app_deploy/handlers.py +345 -0
- mito_ai/{app_builder → app_deploy}/models.py +35 -22
- mito_ai/app_manager/__init__.py +4 -0
- mito_ai/app_manager/handlers.py +167 -0
- mito_ai/app_manager/models.py +71 -0
- mito_ai/app_manager/utils.py +24 -0
- mito_ai/auth/README.md +18 -0
- mito_ai/auth/__init__.py +6 -0
- mito_ai/auth/handlers.py +96 -0
- mito_ai/auth/urls.py +13 -0
- mito_ai/chat_history/handlers.py +63 -0
- mito_ai/chat_history/urls.py +32 -0
- mito_ai/completions/completion_handlers/agent_execution_handler.py +1 -1
- mito_ai/completions/completion_handlers/chat_completion_handler.py +4 -4
- mito_ai/completions/completion_handlers/utils.py +99 -37
- mito_ai/completions/handlers.py +57 -20
- mito_ai/completions/message_history.py +9 -1
- mito_ai/completions/models.py +31 -7
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +21 -2
- mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +8 -0
- mito_ai/completions/prompt_builders/agent_system_message.py +115 -42
- mito_ai/completions/prompt_builders/chat_name_prompt.py +6 -6
- mito_ai/completions/prompt_builders/chat_prompt.py +18 -11
- mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +23 -4
- mito_ai/completions/prompt_builders/utils.py +72 -10
- mito_ai/completions/providers.py +81 -47
- mito_ai/constants.py +25 -24
- mito_ai/file_uploads/__init__.py +3 -0
- mito_ai/file_uploads/handlers.py +248 -0
- mito_ai/file_uploads/urls.py +21 -0
- mito_ai/gemini_client.py +44 -48
- mito_ai/log/handlers.py +10 -3
- mito_ai/log/urls.py +3 -3
- mito_ai/openai_client.py +30 -44
- mito_ai/path_utils.py +70 -0
- mito_ai/streamlit_conversion/agent_utils.py +37 -0
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
- mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
- mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
- mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
- mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
- mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
- mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
- mito_ai/streamlit_preview/__init__.py +6 -0
- mito_ai/streamlit_preview/handlers.py +111 -0
- mito_ai/streamlit_preview/manager.py +152 -0
- mito_ai/streamlit_preview/urls.py +22 -0
- mito_ai/streamlit_preview/utils.py +29 -0
- mito_ai/tests/chat_history/test_chat_history.py +211 -0
- mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
- mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
- mito_ai/tests/file_uploads/__init__.py +2 -0
- mito_ai/tests/file_uploads/test_handlers.py +282 -0
- mito_ai/tests/message_history/test_generate_short_chat_name.py +0 -4
- mito_ai/tests/message_history/test_message_history_utils.py +103 -23
- mito_ai/tests/open_ai_utils_test.py +18 -22
- mito_ai/tests/providers/test_anthropic_client.py +447 -0
- mito_ai/tests/providers/test_azure.py +2 -6
- mito_ai/tests/providers/test_capabilities.py +120 -0
- mito_ai/tests/{test_gemini_client.py → providers/test_gemini_client.py} +40 -36
- mito_ai/tests/providers/test_mito_server_utils.py +448 -0
- mito_ai/tests/providers/test_model_resolution.py +130 -0
- mito_ai/tests/providers/test_openai_client.py +57 -0
- mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
- mito_ai/tests/providers/test_provider_limits.py +42 -0
- mito_ai/tests/providers/test_providers.py +382 -0
- mito_ai/tests/providers/test_retry_logic.py +389 -0
- mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
- mito_ai/tests/providers/utils.py +85 -0
- mito_ai/tests/streamlit_conversion/__init__.py +3 -0
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
- mito_ai/tests/test_constants.py +31 -3
- mito_ai/tests/test_telemetry.py +12 -0
- mito_ai/tests/user/__init__.py +2 -0
- mito_ai/tests/user/test_user.py +120 -0
- mito_ai/tests/utils/test_anthropic_utils.py +6 -6
- mito_ai/user/handlers.py +45 -0
- mito_ai/user/urls.py +21 -0
- mito_ai/utils/anthropic_utils.py +55 -121
- mito_ai/utils/create.py +17 -1
- mito_ai/utils/error_classes.py +42 -0
- mito_ai/utils/gemini_utils.py +39 -94
- mito_ai/utils/message_history_utils.py +7 -4
- mito_ai/utils/mito_server_utils.py +242 -0
- mito_ai/utils/open_ai_utils.py +38 -155
- mito_ai/utils/provider_utils.py +49 -0
- mito_ai/utils/server_limits.py +1 -1
- mito_ai/utils/telemetry_utils.py +137 -5
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +102 -100
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/package.json +4 -2
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +3 -1
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +2 -2
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +15948 -8403
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js +58 -33
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js.map +1 -0
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +10 -2
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js +533 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +6941 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +1021 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +59698 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js +7440 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js.map +1 -0
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2 -240
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/METADATA +5 -2
- mito_ai-0.1.49.dist-info/RECORD +205 -0
- mito_ai/app_builder/handlers.py +0 -218
- mito_ai/tests/providers_test.py +0 -438
- mito_ai/tests/test_anthropic_client.py +0 -270
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js +0 -7842
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -1
- mito_ai-0.1.33.dist-info/RECORD +0 -134
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
import zipfile
|
|
5
|
+
import logging
|
|
6
|
+
from mito_ai.app_deploy.app_deploy_utils import add_files_to_zip
|
|
7
|
+
from mito_ai.path_utils import AbsoluteNotebookDirPath
|
|
8
|
+
|
|
9
|
+
class TestAddFilesToZip:
|
|
10
|
+
"""Test cases for add_files_to_zip helper function"""
|
|
11
|
+
|
|
12
|
+
def test_files_added_correctly(self, tmp_path):
|
|
13
|
+
"""Ensure individual files are added correctly to the zip"""
|
|
14
|
+
# Create files
|
|
15
|
+
f1 = tmp_path / "file1.txt"
|
|
16
|
+
f1.write_text("file1 content")
|
|
17
|
+
f2 = tmp_path / "file2.txt"
|
|
18
|
+
f2.write_text("file2 content")
|
|
19
|
+
|
|
20
|
+
zip_path = tmp_path / "test.zip"
|
|
21
|
+
add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["file1.txt", "file2.txt"], 'test-app-file-name.py')
|
|
22
|
+
|
|
23
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
24
|
+
names = zf.namelist()
|
|
25
|
+
assert "file1.txt" in names
|
|
26
|
+
assert "file2.txt" in names
|
|
27
|
+
assert len(names) == 2
|
|
28
|
+
|
|
29
|
+
def test_renames_app_file(self, tmp_path):
|
|
30
|
+
"""Ensure individual files are added correctly to the zip"""
|
|
31
|
+
# Create files
|
|
32
|
+
f1 = tmp_path / "original-file-name.py"
|
|
33
|
+
f1.write_text("file1 content")
|
|
34
|
+
f2 = tmp_path / "file2.txt"
|
|
35
|
+
f2.write_text("file2 content")
|
|
36
|
+
|
|
37
|
+
zip_path = tmp_path / "test.zip"
|
|
38
|
+
add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["original-file-name.py", "file2.txt"], 'original-file-name.py')
|
|
39
|
+
|
|
40
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
41
|
+
names = zf.namelist()
|
|
42
|
+
assert "app.py" in names
|
|
43
|
+
assert "file2.txt" in names
|
|
44
|
+
assert len(names) == 2
|
|
45
|
+
|
|
46
|
+
def test_directories_added_recursively(self, tmp_path):
|
|
47
|
+
"""Ensure directories are added recursively with correct relative paths"""
|
|
48
|
+
nested = tmp_path / "folder"
|
|
49
|
+
nested.mkdir()
|
|
50
|
+
(nested / "nested1.txt").write_text("nested1 content")
|
|
51
|
+
subfolder = nested / "sub"
|
|
52
|
+
subfolder.mkdir()
|
|
53
|
+
(subfolder / "nested2.txt").write_text("nested2 content")
|
|
54
|
+
|
|
55
|
+
zip_path = tmp_path / "test.zip"
|
|
56
|
+
add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["folder"], 'test-app.py')
|
|
57
|
+
|
|
58
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
59
|
+
names = zf.namelist()
|
|
60
|
+
assert "folder/nested1.txt" in names
|
|
61
|
+
assert "folder/sub/nested2.txt" in names
|
|
62
|
+
|
|
63
|
+
def test_missing_files_skipped(self, tmp_path, caplog):
|
|
64
|
+
"""Ensure missing files do not break the function and warning is logged"""
|
|
65
|
+
caplog.set_level(logging.WARNING)
|
|
66
|
+
zip_path = tmp_path / "test.zip"
|
|
67
|
+
add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["does_not_exist.txt"], 'test-app.py', logger=logging.getLogger())
|
|
68
|
+
|
|
69
|
+
# Zip should exist but be empty
|
|
70
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
71
|
+
assert zf.namelist() == []
|
|
72
|
+
|
|
73
|
+
# Check warning was logged
|
|
74
|
+
assert any("Skipping missing file" in record.message for record in caplog.records)
|
|
75
|
+
|
|
76
|
+
def test_arcname_paths_correct(self, tmp_path):
|
|
77
|
+
"""Ensure arcname paths inside zip preserve relative paths to base_path"""
|
|
78
|
+
(tmp_path / "file.txt").write_text("content")
|
|
79
|
+
folder = tmp_path / "folder"
|
|
80
|
+
folder.mkdir()
|
|
81
|
+
(folder / "nested.txt").write_text("nested content")
|
|
82
|
+
|
|
83
|
+
zip_path = tmp_path / "test.zip"
|
|
84
|
+
add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["file.txt", "folder"], 'test-app.py')
|
|
85
|
+
|
|
86
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
87
|
+
names = zf.namelist()
|
|
88
|
+
assert "file.txt" in names
|
|
89
|
+
assert "folder/nested.txt" in names
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
import pytest
|
|
7
|
+
from unittest.mock import Mock, patch
|
|
8
|
+
import tornado.web
|
|
9
|
+
from tornado.httputil import HTTPServerRequest
|
|
10
|
+
from tornado.web import Application
|
|
11
|
+
|
|
12
|
+
from mito_ai.file_uploads.handlers import FileUploadHandler
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def temp_dir():
|
|
17
|
+
"""Create a temporary directory for test files."""
|
|
18
|
+
temp_dir = tempfile.mkdtemp()
|
|
19
|
+
original_cwd = os.getcwd()
|
|
20
|
+
os.chdir(temp_dir)
|
|
21
|
+
yield temp_dir
|
|
22
|
+
os.chdir(original_cwd)
|
|
23
|
+
# Clean up temporary files
|
|
24
|
+
for file in os.listdir(temp_dir):
|
|
25
|
+
os.remove(os.path.join(temp_dir, file))
|
|
26
|
+
os.rmdir(temp_dir)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def handler():
|
|
31
|
+
"""Create a FileUploadHandler instance for testing."""
|
|
32
|
+
app = Application()
|
|
33
|
+
request = HTTPServerRequest(method="POST", uri="/upload")
|
|
34
|
+
|
|
35
|
+
# Mock the connection to avoid Tornado's assertion
|
|
36
|
+
request.connection = Mock()
|
|
37
|
+
|
|
38
|
+
handler = FileUploadHandler(app, request)
|
|
39
|
+
|
|
40
|
+
# Mock methods properly to avoid mypy errors
|
|
41
|
+
handler.write = Mock() # type: ignore
|
|
42
|
+
handler.finish = Mock() # type: ignore
|
|
43
|
+
handler.set_status = Mock() # type: ignore
|
|
44
|
+
handler.get_argument = Mock() # type: ignore
|
|
45
|
+
|
|
46
|
+
# Mock authentication for Jupyter server
|
|
47
|
+
handler._jupyter_current_user = "test_user" # type: ignore
|
|
48
|
+
|
|
49
|
+
return handler
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_validate_file_upload_success(handler):
|
|
53
|
+
"""Test successful file upload validation."""
|
|
54
|
+
handler.request.files = {"file": [Mock(filename="test.csv", body=b"data")]} # type: ignore
|
|
55
|
+
result = handler._validate_file_upload()
|
|
56
|
+
assert result is True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_validate_file_upload_failure(handler):
|
|
60
|
+
"""Test file upload validation when no file is present."""
|
|
61
|
+
handler.request.files = {} # type: ignore
|
|
62
|
+
result = handler._validate_file_upload()
|
|
63
|
+
assert result is False
|
|
64
|
+
handler.set_status.assert_called_with(400)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_regular_upload_success(handler, temp_dir):
|
|
68
|
+
"""Test successful regular (non-chunked) file upload."""
|
|
69
|
+
filename = "test.csv"
|
|
70
|
+
file_data = b"test,data\n1,2"
|
|
71
|
+
notebook_dir = temp_dir
|
|
72
|
+
|
|
73
|
+
handler._handle_regular_upload(filename, file_data, notebook_dir)
|
|
74
|
+
|
|
75
|
+
# Verify file was written
|
|
76
|
+
file_path = os.path.join(notebook_dir, filename)
|
|
77
|
+
with open(file_path, "rb") as f:
|
|
78
|
+
content = f.read()
|
|
79
|
+
assert content == file_data
|
|
80
|
+
|
|
81
|
+
# Verify response
|
|
82
|
+
handler.write.assert_called_with(
|
|
83
|
+
{"success": True, "filename": filename, "path": file_path}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_chunked_upload_first_chunk(handler, temp_dir):
|
|
88
|
+
"""Test handling first chunk of a chunked upload."""
|
|
89
|
+
filename = "large_file.csv"
|
|
90
|
+
file_data = b"chunk1_data"
|
|
91
|
+
chunk_number = "1"
|
|
92
|
+
total_chunks = "3"
|
|
93
|
+
notebook_dir = temp_dir
|
|
94
|
+
|
|
95
|
+
handler._handle_chunked_upload(
|
|
96
|
+
filename, file_data, chunk_number, total_chunks, notebook_dir
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Verify chunk was saved (check temp dir structure)
|
|
100
|
+
assert filename in handler._temp_dirs
|
|
101
|
+
temp_dir_path = handler._temp_dirs[filename]["temp_dir"]
|
|
102
|
+
chunk_file = os.path.join(temp_dir_path, "chunk_1")
|
|
103
|
+
assert os.path.exists(chunk_file)
|
|
104
|
+
|
|
105
|
+
# Verify response indicates chunk received but not complete
|
|
106
|
+
handler.write.assert_called_with(
|
|
107
|
+
{
|
|
108
|
+
"success": True,
|
|
109
|
+
"chunk_received": True,
|
|
110
|
+
"chunk_number": 1,
|
|
111
|
+
"total_chunks": 3,
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_chunked_upload_completion(handler, temp_dir):
|
|
117
|
+
"""Test completing a chunked upload when all chunks are received."""
|
|
118
|
+
filename = "large_file.csv"
|
|
119
|
+
total_chunks = 2
|
|
120
|
+
notebook_dir = temp_dir
|
|
121
|
+
|
|
122
|
+
# Process first chunk
|
|
123
|
+
handler._handle_chunked_upload(
|
|
124
|
+
filename, b"chunk1_data", "1", str(total_chunks), notebook_dir
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Process final chunk
|
|
128
|
+
handler._handle_chunked_upload(
|
|
129
|
+
filename, b"chunk2_data", "2", str(total_chunks), notebook_dir
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Verify final file was created
|
|
133
|
+
file_path = os.path.join(notebook_dir, filename)
|
|
134
|
+
assert os.path.exists(file_path)
|
|
135
|
+
with open(file_path, "rb") as f:
|
|
136
|
+
content = f.read()
|
|
137
|
+
assert content == b"chunk1_datachunk2_data"
|
|
138
|
+
|
|
139
|
+
# Verify temp dir was cleaned up
|
|
140
|
+
assert filename not in handler._temp_dirs
|
|
141
|
+
|
|
142
|
+
# Verify completion response
|
|
143
|
+
handler.write.assert_called_with(
|
|
144
|
+
{
|
|
145
|
+
"success": True,
|
|
146
|
+
"filename": filename,
|
|
147
|
+
"path": file_path,
|
|
148
|
+
"chunk_complete": True,
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_error_handling(handler):
|
|
154
|
+
"""Test error handling in upload process."""
|
|
155
|
+
error_message = "Test error message"
|
|
156
|
+
status_code = 500
|
|
157
|
+
|
|
158
|
+
handler._handle_error(error_message, status_code)
|
|
159
|
+
|
|
160
|
+
handler.set_status.assert_called_with(status_code)
|
|
161
|
+
handler.write.assert_called_with({"error": error_message})
|
|
162
|
+
handler.finish.assert_called_once()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@patch("mito_ai.file_uploads.handlers.FileUploadHandler._validate_file_upload")
|
|
166
|
+
def test_post_method_regular_upload(mock_validate, handler):
|
|
167
|
+
"""Test POST method for regular upload."""
|
|
168
|
+
mock_validate.return_value = True
|
|
169
|
+
handler.request.files = {"file": [Mock(filename="test.csv", body=b"data")]} # type: ignore
|
|
170
|
+
handler.get_argument.return_value = None # No chunk parameters
|
|
171
|
+
|
|
172
|
+
handler.post()
|
|
173
|
+
|
|
174
|
+
mock_validate.assert_called_once()
|
|
175
|
+
handler.finish.assert_called_once()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@patch("mito_ai.file_uploads.handlers.FileUploadHandler._validate_file_upload")
|
|
179
|
+
def test_post_method_chunked_upload(mock_validate, handler):
|
|
180
|
+
"""Test POST method for chunked upload."""
|
|
181
|
+
mock_validate.return_value = True
|
|
182
|
+
handler.request.files = {"file": [Mock(filename="test.csv", body=b"data")]} # type: ignore
|
|
183
|
+
handler.get_argument.side_effect = lambda name, default=None: {
|
|
184
|
+
"chunk_number": "1",
|
|
185
|
+
"total_chunks": "3",
|
|
186
|
+
}.get(name, default)
|
|
187
|
+
|
|
188
|
+
handler.post()
|
|
189
|
+
|
|
190
|
+
mock_validate.assert_called_once()
|
|
191
|
+
handler.finish.assert_called_once()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_are_all_chunks_received_true(handler, temp_dir):
|
|
195
|
+
"""Test that all chunks are detected when present."""
|
|
196
|
+
filename = "test.csv"
|
|
197
|
+
total_chunks = 2
|
|
198
|
+
|
|
199
|
+
# Manually set up the temp dir structure
|
|
200
|
+
temp_dir_path = tempfile.mkdtemp(prefix=f"mito_upload_{filename}_")
|
|
201
|
+
handler._temp_dirs[filename] = {
|
|
202
|
+
"temp_dir": temp_dir_path,
|
|
203
|
+
"total_chunks": total_chunks,
|
|
204
|
+
"received_chunks": {1, 2},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
result = handler._are_all_chunks_received(filename, total_chunks)
|
|
208
|
+
assert result is True
|
|
209
|
+
|
|
210
|
+
# Clean up
|
|
211
|
+
import shutil
|
|
212
|
+
|
|
213
|
+
shutil.rmtree(temp_dir_path)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_are_all_chunks_received_false(handler, temp_dir):
|
|
217
|
+
"""Test that missing chunks are detected."""
|
|
218
|
+
filename = "test.csv"
|
|
219
|
+
total_chunks = 2
|
|
220
|
+
|
|
221
|
+
# Manually set up the temp dir structure with only one chunk
|
|
222
|
+
temp_dir_path = tempfile.mkdtemp(prefix=f"mito_upload_{filename}_")
|
|
223
|
+
handler._temp_dirs[filename] = {
|
|
224
|
+
"temp_dir": temp_dir_path,
|
|
225
|
+
"total_chunks": total_chunks,
|
|
226
|
+
"received_chunks": {1}, # Only chunk 1 received
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
result = handler._are_all_chunks_received(filename, total_chunks)
|
|
230
|
+
assert result is False
|
|
231
|
+
|
|
232
|
+
# Clean up
|
|
233
|
+
import shutil
|
|
234
|
+
|
|
235
|
+
shutil.rmtree(temp_dir_path)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_save_chunk(handler, temp_dir):
|
|
239
|
+
"""Test saving individual chunks."""
|
|
240
|
+
filename = "test.csv"
|
|
241
|
+
file_data = b"chunk_data"
|
|
242
|
+
chunk_number = 1
|
|
243
|
+
total_chunks = 3
|
|
244
|
+
|
|
245
|
+
# Mock the file operations to avoid filesystem issues
|
|
246
|
+
with patch("builtins.open", create=True) as mock_open:
|
|
247
|
+
mock_file = Mock()
|
|
248
|
+
mock_open.return_value.__enter__.return_value = mock_file
|
|
249
|
+
|
|
250
|
+
handler._save_chunk(filename, file_data, chunk_number, total_chunks)
|
|
251
|
+
|
|
252
|
+
# Verify temp dir was created in the handler's tracking
|
|
253
|
+
assert filename in handler._temp_dirs
|
|
254
|
+
temp_dir_path = handler._temp_dirs[filename]["temp_dir"]
|
|
255
|
+
|
|
256
|
+
# Verify the expected chunk filename was used
|
|
257
|
+
expected_chunk_filename = os.path.join(temp_dir_path, f"chunk_{chunk_number}")
|
|
258
|
+
mock_open.assert_called_with(expected_chunk_filename, "wb")
|
|
259
|
+
|
|
260
|
+
# Verify file data was written
|
|
261
|
+
mock_file.write.assert_called_with(file_data)
|
|
262
|
+
|
|
263
|
+
# Verify chunk was marked as received
|
|
264
|
+
assert chunk_number in handler._temp_dirs[filename]["received_chunks"]
|
|
265
|
+
|
|
266
|
+
# Clean up
|
|
267
|
+
del handler._temp_dirs[filename]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_image_size_limit_exceeded(handler, temp_dir):
|
|
271
|
+
"""Test that image uploads exceeding 3MB are rejected."""
|
|
272
|
+
filename = "large_image.jpg"
|
|
273
|
+
# Create 5MB of data (5 * 1024 * 1024 bytes)
|
|
274
|
+
file_data = b"x" * (5 * 1024 * 1024)
|
|
275
|
+
notebook_dir = temp_dir
|
|
276
|
+
|
|
277
|
+
# The _handle_regular_upload should raise a ValueError for oversized images
|
|
278
|
+
with pytest.raises(ValueError) as exc_info:
|
|
279
|
+
handler._handle_regular_upload(filename, file_data, notebook_dir)
|
|
280
|
+
|
|
281
|
+
# Verify the error message mentions the size limit
|
|
282
|
+
assert "exceeded 3MB limit" in str(exc_info.value)
|
|
@@ -6,10 +6,6 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
6
6
|
from traitlets.config import Config
|
|
7
7
|
from mito_ai.completions.message_history import generate_short_chat_name
|
|
8
8
|
from mito_ai.completions.providers import OpenAIProvider
|
|
9
|
-
from mito_ai.completions.models import MessageType
|
|
10
|
-
from mito_ai.openai_client import OPENAI_FAST_MODEL
|
|
11
|
-
from mito_ai.anthropic_client import ANTHROPIC_FAST_MODEL
|
|
12
|
-
from mito_ai.gemini_client import GEMINI_FAST_MODEL
|
|
13
9
|
|
|
14
10
|
|
|
15
11
|
@pytest.fixture
|
|
@@ -7,9 +7,9 @@ from openai.types.chat import ChatCompletionMessageParam
|
|
|
7
7
|
from mito_ai.utils.message_history_utils import trim_sections_from_message_content, trim_old_messages
|
|
8
8
|
from mito_ai.completions.prompt_builders.chat_prompt import create_chat_prompt
|
|
9
9
|
from mito_ai.completions.prompt_builders.agent_execution_prompt import create_agent_execution_prompt
|
|
10
|
-
from mito_ai.completions.prompt_builders.agent_smart_debug_prompt import
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
from mito_ai.completions.prompt_builders.agent_smart_debug_prompt import create_agent_smart_debug_prompt
|
|
11
|
+
from unittest.mock import Mock, patch
|
|
12
|
+
from mito_ai.completions.message_history import GlobalMessageHistory, ChatThread
|
|
13
13
|
from mito_ai.completions.prompt_builders.smart_debug_prompt import create_error_prompt
|
|
14
14
|
from mito_ai.completions.prompt_builders.explain_code_prompt import create_explain_code_prompt
|
|
15
15
|
from mito_ai.completions.models import (
|
|
@@ -27,6 +27,9 @@ from mito_ai.completions.prompt_builders.prompt_constants import (
|
|
|
27
27
|
CONTENT_REMOVED_PLACEHOLDER,
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
30
33
|
# Standard test data for multiple tests
|
|
31
34
|
TEST_VARIABLES = ["'df': pd.DataFrame({'col1': [1, 2, 3], 'col2': [4, 5, 6]})"]
|
|
32
35
|
TEST_FILES = ["data.csv", "script.py"]
|
|
@@ -103,12 +106,15 @@ PROMPT_BUILDER_TEST_CASES = [
|
|
|
103
106
|
AgentExecutionMetadata(
|
|
104
107
|
variables=TEST_VARIABLES,
|
|
105
108
|
files=TEST_FILES,
|
|
109
|
+
notebookPath='/test-notebook-path.ipynb',
|
|
110
|
+
notebookID='test-notebook-id',
|
|
106
111
|
aiOptimizedCells=[
|
|
107
112
|
AIOptimizedCell(cell_type="code", id="cell1", code=TEST_CODE)
|
|
108
113
|
],
|
|
109
114
|
input=TEST_INPUT,
|
|
110
115
|
promptType="agent:execution",
|
|
111
116
|
threadId=ThreadID("test-thread-id"),
|
|
117
|
+
activeCellId="cell1",
|
|
112
118
|
isChromeBrowser=True
|
|
113
119
|
)
|
|
114
120
|
),
|
|
@@ -230,16 +236,17 @@ file1.csv
|
|
|
230
236
|
file2.txt
|
|
231
237
|
"""
|
|
232
238
|
|
|
233
|
-
# Create test messages with proper typing
|
|
239
|
+
# Create test messages with proper typing
|
|
234
240
|
messages: List[ChatCompletionMessageParam] = [
|
|
235
241
|
{"role": "system", "content": system_message_with_sections},
|
|
236
242
|
{"role": "user", "content": user_message_with_sections},
|
|
237
243
|
{"role": "assistant", "content": assistant_message_with_sections},
|
|
238
|
-
{"role": "user", "content": "Recent user message"},
|
|
244
|
+
{"role": "user", "content": "Recent user message 1"},
|
|
245
|
+
{"role": "user", "content": "Recent user message 2"},
|
|
246
|
+
{"role": "user", "content": "Recent user message 3"},
|
|
239
247
|
]
|
|
240
248
|
|
|
241
|
-
|
|
242
|
-
result = trim_old_messages(messages, keep_recent=1)
|
|
249
|
+
result = trim_old_messages(messages)
|
|
243
250
|
|
|
244
251
|
# System message should remain unchanged even though it's old
|
|
245
252
|
system_content = result[0].get("content")
|
|
@@ -262,14 +269,22 @@ file2.txt
|
|
|
262
269
|
assert FILES_SECTION_HEADING in assistant_content
|
|
263
270
|
assert "file1.csv" in assistant_content
|
|
264
271
|
|
|
265
|
-
# Recent user
|
|
266
|
-
|
|
267
|
-
assert isinstance(
|
|
268
|
-
assert
|
|
272
|
+
# Recent user messages should remain unchanged
|
|
273
|
+
recent_content_1 = result[3].get("content")
|
|
274
|
+
assert isinstance(recent_content_1, str)
|
|
275
|
+
assert recent_content_1 == "Recent user message 1"
|
|
276
|
+
|
|
277
|
+
recent_content_2 = result[4].get("content")
|
|
278
|
+
assert isinstance(recent_content_2, str)
|
|
279
|
+
assert recent_content_2 == "Recent user message 2"
|
|
280
|
+
|
|
281
|
+
recent_content_3 = result[5].get("content")
|
|
282
|
+
assert isinstance(recent_content_3, str)
|
|
283
|
+
assert recent_content_3 == "Recent user message 3"
|
|
269
284
|
|
|
270
285
|
|
|
271
286
|
def test_trim_old_messages_preserves_recent_messages() -> None:
|
|
272
|
-
"""Test that trim_old_messages preserves the most recent messages based on
|
|
287
|
+
"""Test that trim_old_messages preserves the most recent messages based on MESSAGE_HISTORY_TRIM_THRESHOLD."""
|
|
273
288
|
# Create test messages
|
|
274
289
|
old_message_1 = f"""Old message 1.
|
|
275
290
|
{FILES_SECTION_HEADING}
|
|
@@ -286,6 +301,10 @@ file3.csv
|
|
|
286
301
|
recent_message_2 = f"""Recent message 2.
|
|
287
302
|
{FILES_SECTION_HEADING}
|
|
288
303
|
file4.csv
|
|
304
|
+
"""
|
|
305
|
+
recent_message_3 = f"""Recent message 3.
|
|
306
|
+
{FILES_SECTION_HEADING}
|
|
307
|
+
file5.csv
|
|
289
308
|
"""
|
|
290
309
|
|
|
291
310
|
# Create test messages with proper typing
|
|
@@ -294,10 +313,11 @@ file4.csv
|
|
|
294
313
|
{"role": "user", "content": old_message_2},
|
|
295
314
|
{"role": "user", "content": recent_message_1},
|
|
296
315
|
{"role": "user", "content": recent_message_2},
|
|
316
|
+
{"role": "user", "content": recent_message_3},
|
|
297
317
|
]
|
|
298
318
|
|
|
299
|
-
#
|
|
300
|
-
result = trim_old_messages(messages
|
|
319
|
+
# Test with MESSAGE_HISTORY_TRIM_THRESHOLD (3) - only the first 2 messages should be trimmed
|
|
320
|
+
result = trim_old_messages(messages)
|
|
301
321
|
|
|
302
322
|
# Old messages should be trimmed
|
|
303
323
|
old_content_1 = result[0].get("content")
|
|
@@ -322,24 +342,30 @@ file4.csv
|
|
|
322
342
|
assert recent_content_2 == recent_message_2
|
|
323
343
|
assert FILES_SECTION_HEADING in recent_content_2
|
|
324
344
|
assert "file4.csv" in recent_content_2
|
|
345
|
+
|
|
346
|
+
recent_content_3 = result[4].get("content")
|
|
347
|
+
assert isinstance(recent_content_3, str)
|
|
348
|
+
assert recent_content_3 == recent_message_3
|
|
349
|
+
assert FILES_SECTION_HEADING in recent_content_3
|
|
350
|
+
assert "file5.csv" in recent_content_3
|
|
325
351
|
|
|
326
352
|
def test_trim_old_messages_empty_list() -> None:
|
|
327
353
|
"""Test that trim_old_messages handles empty message lists correctly."""
|
|
328
354
|
messages: List[ChatCompletionMessageParam] = []
|
|
329
|
-
result = trim_old_messages(messages
|
|
355
|
+
result = trim_old_messages(messages)
|
|
330
356
|
assert result == []
|
|
331
357
|
|
|
332
358
|
|
|
333
|
-
def
|
|
334
|
-
"""Test that trim_old_messages doesn't modify messages if there are fewer than
|
|
359
|
+
def test_trim_old_messages_fewer_than_threshold() -> None:
|
|
360
|
+
"""Test that trim_old_messages doesn't modify messages if there are fewer than MESSAGE_HISTORY_TRIM_THRESHOLD."""
|
|
335
361
|
messages: List[ChatCompletionMessageParam] = [
|
|
336
362
|
{"role": "user", "content": "User message 1"},
|
|
337
363
|
{"role": "assistant", "content": "Assistant message 1"},
|
|
338
364
|
]
|
|
339
365
|
|
|
340
|
-
result = trim_old_messages(messages
|
|
366
|
+
result = trim_old_messages(messages)
|
|
341
367
|
|
|
342
|
-
# Messages should remain unchanged
|
|
368
|
+
# Messages should remain unchanged since we have fewer than MESSAGE_HISTORY_TRIM_THRESHOLD (3) messages
|
|
343
369
|
user_content = result[0].get("content")
|
|
344
370
|
assert isinstance(user_content, str)
|
|
345
371
|
assert user_content == "User message 1"
|
|
@@ -370,15 +396,17 @@ def test_trim_mixed_content_messages() -> None:
|
|
|
370
396
|
})
|
|
371
397
|
|
|
372
398
|
# Create sample message list with one old message (the mixed content)
|
|
373
|
-
# and
|
|
399
|
+
# and enough recent messages to exceed MESSAGE_HISTORY_TRIM_THRESHOLD (3)
|
|
374
400
|
message_list: List[ChatCompletionMessageParam] = [
|
|
375
401
|
mixed_content_message, # This should get trimmed
|
|
376
402
|
{"role": "assistant", "content": "That's a chart showing data trends"},
|
|
377
|
-
{"role": "user", "content": "Can you explain more?"} # Recent message, should not be trimmed
|
|
403
|
+
{"role": "user", "content": "Can you explain more?"}, # Recent message, should not be trimmed
|
|
404
|
+
{"role": "user", "content": "Another recent message"}, # Recent message, should not be trimmed
|
|
405
|
+
{"role": "user", "content": "Yet another recent message"} # Recent message, should not be trimmed
|
|
378
406
|
]
|
|
379
407
|
|
|
380
408
|
# Apply the trimming function
|
|
381
|
-
trimmed_messages = trim_old_messages(message_list
|
|
409
|
+
trimmed_messages = trim_old_messages(message_list)
|
|
382
410
|
|
|
383
411
|
# Verify that the first message has been trimmed properly
|
|
384
412
|
assert trimmed_messages[0]["role"] == "user"
|
|
@@ -386,4 +414,56 @@ def test_trim_mixed_content_messages() -> None:
|
|
|
386
414
|
|
|
387
415
|
# Verify that the recent messages are untouched
|
|
388
416
|
assert trimmed_messages[1] == message_list[1]
|
|
389
|
-
assert trimmed_messages[2] == message_list[2]
|
|
417
|
+
assert trimmed_messages[2] == message_list[2]
|
|
418
|
+
assert trimmed_messages[3] == message_list[3]
|
|
419
|
+
assert trimmed_messages[4] == message_list[4]
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def test_get_display_history_calls_update_last_interaction() -> None:
|
|
423
|
+
"""Test that get_display_history calls _update_last_interaction when retrieving a thread."""
|
|
424
|
+
|
|
425
|
+
# Create a mock thread
|
|
426
|
+
thread_id = ThreadID("test-thread-id")
|
|
427
|
+
mock_thread = Mock(spec=ChatThread)
|
|
428
|
+
mock_thread.display_history = [{"role": "user", "content": "test message"}]
|
|
429
|
+
mock_thread.last_interaction_ts = 1234567890.0
|
|
430
|
+
|
|
431
|
+
# Create message history instance and add the mock thread
|
|
432
|
+
message_history = GlobalMessageHistory()
|
|
433
|
+
message_history._chat_threads = {thread_id: mock_thread}
|
|
434
|
+
|
|
435
|
+
# Mock the _update_last_interaction method
|
|
436
|
+
with patch.object(message_history, '_update_last_interaction') as mock_update:
|
|
437
|
+
with patch.object(message_history, '_save_thread_to_disk') as mock_save:
|
|
438
|
+
# Call get_display_history
|
|
439
|
+
result = message_history.get_display_history(thread_id)
|
|
440
|
+
|
|
441
|
+
# Verify _update_last_interaction was called with the thread
|
|
442
|
+
mock_update.assert_called_once_with(mock_thread)
|
|
443
|
+
|
|
444
|
+
# Verify _save_thread_to_disk was also called
|
|
445
|
+
mock_save.assert_called_once_with(mock_thread)
|
|
446
|
+
|
|
447
|
+
# Verify the result is correct
|
|
448
|
+
assert result == [{"role": "user", "content": "test message"}]
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def test_get_display_history_returns_empty_for_nonexistent_thread() -> None:
|
|
452
|
+
"""Test that get_display_history returns empty list for non-existent thread."""
|
|
453
|
+
from mito_ai.completions.message_history import GlobalMessageHistory
|
|
454
|
+
from mito_ai.completions.models import ThreadID
|
|
455
|
+
|
|
456
|
+
message_history = GlobalMessageHistory()
|
|
457
|
+
thread_id = ThreadID("nonexistent-thread-id")
|
|
458
|
+
|
|
459
|
+
# Mock the methods to ensure they're not called
|
|
460
|
+
with patch.object(message_history, '_update_last_interaction') as mock_update:
|
|
461
|
+
with patch.object(message_history, '_save_thread_to_disk') as mock_save:
|
|
462
|
+
result = message_history.get_display_history(thread_id)
|
|
463
|
+
|
|
464
|
+
# Verify methods were not called since thread doesn't exist
|
|
465
|
+
mock_update.assert_not_called()
|
|
466
|
+
mock_save.assert_not_called()
|
|
467
|
+
|
|
468
|
+
# Verify empty result
|
|
469
|
+
assert result == []
|
|
@@ -80,28 +80,24 @@ def test_prepare_request_data_and_headers_basic() -> None:
|
|
|
80
80
|
mock_get_user_field.side_effect = ["test@example.com", "user123"]
|
|
81
81
|
|
|
82
82
|
# Mock the quota check
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
assert data["user_input"] == "test message"
|
|
102
|
-
|
|
103
|
-
# Verify headers
|
|
104
|
-
assert headers == {"Content-Type": "application/json"}
|
|
83
|
+
data, headers = _prepare_request_data_and_headers(
|
|
84
|
+
last_message_content="test message",
|
|
85
|
+
ai_completion_data={"key": "value"},
|
|
86
|
+
timeout=30,
|
|
87
|
+
max_retries=3,
|
|
88
|
+
message_type=MessageType.CHAT
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Verify data structure
|
|
92
|
+
assert data["timeout"] == 30
|
|
93
|
+
assert data["max_retries"] == 3
|
|
94
|
+
assert data["email"] == "test@example.com"
|
|
95
|
+
assert data["user_id"] == "user123"
|
|
96
|
+
assert data["data"] == {"key": "value"}
|
|
97
|
+
assert data["user_input"] == "test message"
|
|
98
|
+
|
|
99
|
+
# Verify headers
|
|
100
|
+
assert headers == {"Content-Type": "application/json"}
|
|
105
101
|
|
|
106
102
|
def test_prepare_request_data_and_headers_null_message() -> None:
|
|
107
103
|
"""Test handling of null message content"""
|