codebuff-agent 2.1.74__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.
- codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
- codebuff_agent-2.1.74.dist-info/METADATA +142 -0
- codebuff_agent-2.1.74.dist-info/RECORD +217 -0
- codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
- codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
- codebuff_core/__init__.py +153 -0
- codebuff_core/codebuff_config_defaults.json +64 -0
- codebuff_core/core/__init__.py +35 -0
- codebuff_core/core/code_validator.py +1238 -0
- codebuff_core/core/error_classifier.py +579 -0
- codebuff_core/core/exceptions.py +49 -0
- codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
- codebuff_core/directory_policy.py +32 -0
- codebuff_core/factory.py +155 -0
- codebuff_core/interfaces.py +197 -0
- codebuff_core/knowledge/__init__.py +31 -0
- codebuff_core/knowledge/chunking.py +375 -0
- codebuff_core/knowledge/loader.py +339 -0
- codebuff_core/knowledge/watchdog_service.py +354 -0
- codebuff_core/legacy_paths.py +69 -0
- codebuff_core/llm/__init__.py +13 -0
- codebuff_core/llm/service.py +407 -0
- codebuff_core/managers/__init__.py +22 -0
- codebuff_core/managers/config_manager.py +690 -0
- codebuff_core/managers/remote_config.py +673 -0
- codebuff_core/managers/remote_skills.py +539 -0
- codebuff_core/managers/session_manager.py +255 -0
- codebuff_core/middleware/__init__.py +3 -0
- codebuff_core/middleware/error_recovery.py +394 -0
- codebuff_core/middleware/logging_utils.py +33 -0
- codebuff_core/models/__init__.py +111 -0
- codebuff_core/models/agent.py +306 -0
- codebuff_core/models/chat.py +57 -0
- codebuff_core/models/common.py +82 -0
- codebuff_core/models/rag.py +300 -0
- codebuff_core/platform_rules.py +127 -0
- codebuff_core/prompts/__init__.py +44 -0
- codebuff_core/prompts/auto_agent_prompts.py +1278 -0
- codebuff_core/services/__init__.py +30 -0
- codebuff_core/services/agent_service.py +345 -0
- codebuff_core/services/analytics_aggregate.py +837 -0
- codebuff_core/services/chat_service.py +898 -0
- codebuff_core/services/context_condenser.py +306 -0
- codebuff_core/services/pii_guardrail.py +393 -0
- codebuff_core/services/rag_chat_agent.py +2993 -0
- codebuff_core/services/rag_collections.py +2740 -0
- codebuff_core/services/rag_manager.py +890 -0
- codebuff_core/services/rag_service.py +145 -0
- codebuff_core/services/retriever.py +260 -0
- codebuff_core/services/vllm_embedding_service.py +290 -0
- codebuff_core/tests/__init__.py +1 -0
- codebuff_core/tests/conftest.py +81 -0
- codebuff_core/tests/test_error_recovery.py +647 -0
- codebuff_core/tests/test_factory.py +135 -0
- codebuff_core/tests/test_services.py +150 -0
- jupyter_ext/__init__.py +266 -0
- jupyter_ext/_version.py +4 -0
- jupyter_ext/agent/__init__.py +49 -0
- jupyter_ext/agent/agent_factory.py +2270 -0
- jupyter_ext/agent/ast_tools.py +451 -0
- jupyter_ext/agent/autofix_middleware.py +481 -0
- jupyter_ext/agent/background_procs.py +372 -0
- jupyter_ext/agent/code_history_middleware.py +211 -0
- jupyter_ext/agent/codex_autofix.py +160 -0
- jupyter_ext/agent/codex_bridge.py +3339 -0
- jupyter_ext/agent/codex_hooks.py +320 -0
- jupyter_ext/agent/codex_slash.py +685 -0
- jupyter_ext/agent/codex_threads.py +114 -0
- jupyter_ext/agent/content_injection.py +377 -0
- jupyter_ext/agent/continuation_middleware.py +548 -0
- jupyter_ext/agent/custom_skills.py +335 -0
- jupyter_ext/agent/file_diffs.py +99 -0
- jupyter_ext/agent/git_history.py +188 -0
- jupyter_ext/agent/guardrail_middleware.py +421 -0
- jupyter_ext/agent/json_repair_utils.py +54 -0
- jupyter_ext/agent/langchain_tools.py +2673 -0
- jupyter_ext/agent/mermaid_fixer.py +289 -0
- jupyter_ext/agent/mermaid_middleware.py +135 -0
- jupyter_ext/agent/middleware.py +359 -0
- jupyter_ext/agent/middleware_prompts.py +53 -0
- jupyter_ext/agent/observability.py +334 -0
- jupyter_ext/agent/patch_tool_calls.py +57 -0
- jupyter_ext/agent/prompts.py +112 -0
- jupyter_ext/agent/skill_middleware.py +257 -0
- jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
- jupyter_ext/agent/skills/anomaly_detection.md +12 -0
- jupyter_ext/agent/skills/athena_sql.md +156 -0
- jupyter_ext/agent/skills/bdp_table_search.md +33 -0
- jupyter_ext/agent/skills/bigquery.md +65 -0
- jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
- jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
- jupyter_ext/agent/skills/classification_evaluation.md +11 -0
- jupyter_ext/agent/skills/clustering_analysis.md +12 -0
- jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
- jupyter_ext/agent/skills/data_analysis.md +236 -0
- jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
- jupyter_ext/agent/skills/data_loading.md +158 -0
- jupyter_ext/agent/skills/data_quality_report.md +13 -0
- jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
- jupyter_ext/agent/skills/emr_script_development.md +17 -0
- jupyter_ext/agent/skills/experiment_tracking.md +13 -0
- jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
- jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
- jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
- jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
- jupyter_ext/agent/skills/inference.md +392 -0
- jupyter_ext/agent/skills/lambda_function_development.md +19 -0
- jupyter_ext/agent/skills/model_comparison.md +11 -0
- jupyter_ext/agent/skills/model_explainability.md +12 -0
- jupyter_ext/agent/skills/model_training.md +318 -0
- jupyter_ext/agent/skills/pyspark.md +352 -0
- jupyter_ext/agent/skills/python_code_quality.md +35 -0
- jupyter_ext/agent/skills/regression_evaluation.md +12 -0
- jupyter_ext/agent/skills/spring_java_development.md +35 -0
- jupyter_ext/agent/skills/text_preprocessing.md +12 -0
- jupyter_ext/agent/skills/time_series_analysis.md +13 -0
- jupyter_ext/agent/skills/unit_testing.md +203 -0
- jupyter_ext/agent/todo_prompts.py +57 -0
- jupyter_ext/agent/tool_report.py +45 -0
- jupyter_ext/analytics_ingest.py +599 -0
- jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- jupyter_ext/handlers/__init__.py +8 -0
- jupyter_ext/handlers/analytics_handler.py +170 -0
- jupyter_ext/handlers/benchmark_handler.py +794 -0
- jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
- jupyter_ext/handlers/rag_admin_handler.py +689 -0
- jupyter_ext/handlers/websocket.py +497 -0
- jupyter_ext/http_handlers.py +8744 -0
- jupyter_ext/idle_monitor.py +346 -0
- jupyter_ext/labextension/package.json +157 -0
- jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
- jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
- jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
- jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
- jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
- jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
- jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
- jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
- jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
- jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
- jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
- jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- jupyter_ext/labextension/static/style.js +4 -0
- jupyter_ext/labextension/static/third-party-licenses.json +724 -0
- jupyter_ext/log_paths.py +113 -0
- jupyter_ext/resource_usage.py +519 -0
- jupyter_ext/secrets_bootstrap.py +116 -0
- jupyter_ext/team_access.py +105 -0
- jupyter_ext/tests/__init__.py +1 -0
- jupyter_ext/tests/test_context.py +234 -0
- jupyter_ext/tests/test_factory.py +563 -0
- jupyter_ext/tests/test_file_finder.py +139 -0
- jupyter_ext/tests/test_handlers.py +58 -0
- jupyter_ext/tests/test_mcp_selftest.py +204 -0
- jupyter_ext/tests/test_mermaid_fixer.py +272 -0
- jupyter_ext/tests/test_resource_tiers.py +298 -0
- jupyter_ext/tools/__init__.py +15 -0
- jupyter_ext/tools/base.py +90 -0
- jupyter_ext/tools/context.py +268 -0
- jupyter_ext/tools/dir_complete.py +86 -0
- jupyter_ext/tools/file_finder.py +303 -0
- jupyter_ext/tools/file_tools.py +244 -0
- jupyter_ext/tools/jupyter_tools.py +296 -0
- jupyter_ext/tools/registry.py +159 -0
- jupyter_ext/tools/search_tools.py +230 -0
- jupyter_ext/tools/security_tools.py +657 -0
- jupyter_ext/tools/shell_tools.py +109 -0
- jupyter_ext/tools/user_tools.py +132 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
"""Tests for AgentFactory and related classes."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import json
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from jupyter_ext.agent.agent_factory import (
|
|
10
|
+
AgentConfig,
|
|
11
|
+
AgentEvent,
|
|
12
|
+
AgentFactory,
|
|
13
|
+
config_from_dict,
|
|
14
|
+
)
|
|
15
|
+
from jupyter_ext.tools.context import NotebookContextManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _has_langchain() -> bool:
|
|
19
|
+
"""Check if langchain is installed."""
|
|
20
|
+
return importlib.util.find_spec("langchain") is not None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestAgentConfig:
|
|
24
|
+
"""Tests for AgentConfig dataclass."""
|
|
25
|
+
|
|
26
|
+
def test_default_values(self):
|
|
27
|
+
"""Test AgentConfig with default values."""
|
|
28
|
+
config = AgentConfig(
|
|
29
|
+
provider="openai",
|
|
30
|
+
model="gpt-4o",
|
|
31
|
+
api_key="test-key",
|
|
32
|
+
)
|
|
33
|
+
assert config.temperature == 0.0
|
|
34
|
+
assert config.max_tokens == 4096
|
|
35
|
+
assert config.max_iterations == 20
|
|
36
|
+
assert config.mode == "default"
|
|
37
|
+
|
|
38
|
+
def test_custom_values(self):
|
|
39
|
+
"""Test AgentConfig with custom values."""
|
|
40
|
+
config = AgentConfig(
|
|
41
|
+
provider="anthropic",
|
|
42
|
+
model="claude-3-opus",
|
|
43
|
+
api_key="test-key",
|
|
44
|
+
temperature=0.7,
|
|
45
|
+
max_tokens=8192,
|
|
46
|
+
max_iterations=50,
|
|
47
|
+
mode="data_science",
|
|
48
|
+
)
|
|
49
|
+
assert config.temperature == 0.7
|
|
50
|
+
assert config.max_tokens == 8192
|
|
51
|
+
assert config.max_iterations == 50
|
|
52
|
+
assert config.mode == "data_science"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestAgentEvent:
|
|
56
|
+
"""Tests for AgentEvent dataclass."""
|
|
57
|
+
|
|
58
|
+
def test_to_dict(self):
|
|
59
|
+
"""Test AgentEvent serialization."""
|
|
60
|
+
event = AgentEvent(
|
|
61
|
+
type="tool_call",
|
|
62
|
+
data={"name": "jupyter_cell", "arguments": {"operation": "CREATE"}},
|
|
63
|
+
)
|
|
64
|
+
result = event.to_dict()
|
|
65
|
+
|
|
66
|
+
assert result["type"] == "tool_call"
|
|
67
|
+
assert result["name"] == "jupyter_cell"
|
|
68
|
+
assert result["arguments"]["operation"] == "CREATE"
|
|
69
|
+
|
|
70
|
+
def test_to_dict_empty_data(self):
|
|
71
|
+
"""Test AgentEvent with empty data."""
|
|
72
|
+
event = AgentEvent(type="start", data={})
|
|
73
|
+
result = event.to_dict()
|
|
74
|
+
|
|
75
|
+
assert result == {"type": "start"}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestConfigFromDict:
|
|
79
|
+
"""Tests for config_from_dict function."""
|
|
80
|
+
|
|
81
|
+
def test_anthropic_config(self):
|
|
82
|
+
"""Test parsing Anthropic configuration."""
|
|
83
|
+
data = {
|
|
84
|
+
"provider": "anthropic",
|
|
85
|
+
"anthropic": {
|
|
86
|
+
"apiKey": "sk-ant-test",
|
|
87
|
+
"model": "claude-sonnet-4-20250514",
|
|
88
|
+
},
|
|
89
|
+
"mode": "code_review",
|
|
90
|
+
"maxIterations": 30,
|
|
91
|
+
}
|
|
92
|
+
config = config_from_dict(data)
|
|
93
|
+
|
|
94
|
+
assert config.provider == "anthropic"
|
|
95
|
+
assert config.api_key == "sk-ant-test"
|
|
96
|
+
assert config.model == "claude-sonnet-4-20250514"
|
|
97
|
+
assert config.mode == "code_review"
|
|
98
|
+
assert config.max_iterations == 30
|
|
99
|
+
|
|
100
|
+
def test_default_values(self):
|
|
101
|
+
"""Test parsing with missing optional fields."""
|
|
102
|
+
data = {
|
|
103
|
+
"provider": "vllm",
|
|
104
|
+
"vllm": {
|
|
105
|
+
"endpoint": "http://localhost:8000",
|
|
106
|
+
"apiKey": "test-key",
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
config = config_from_dict(data)
|
|
110
|
+
|
|
111
|
+
assert config.provider == "vllm"
|
|
112
|
+
assert config.temperature == 0.0 # default
|
|
113
|
+
assert config.max_tokens == 4096 # default
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestAgentFactory:
|
|
117
|
+
"""Tests for AgentFactory class."""
|
|
118
|
+
|
|
119
|
+
@pytest.fixture
|
|
120
|
+
def notebook_context(self):
|
|
121
|
+
"""Create a NotebookContextManager with sample notebook."""
|
|
122
|
+
ctx = NotebookContextManager()
|
|
123
|
+
ctx.set_notebook(
|
|
124
|
+
"test.ipynb",
|
|
125
|
+
{
|
|
126
|
+
"content": {
|
|
127
|
+
"cells": [
|
|
128
|
+
{"cell_type": "code", "source": "x = 1", "outputs": []},
|
|
129
|
+
],
|
|
130
|
+
"metadata": {"kernelspec": {"name": "python3"}},
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
return ctx
|
|
135
|
+
|
|
136
|
+
@pytest.fixture
|
|
137
|
+
def factory(self, notebook_context):
|
|
138
|
+
"""Create an AgentFactory instance."""
|
|
139
|
+
return AgentFactory(notebook_context=notebook_context)
|
|
140
|
+
|
|
141
|
+
def test_factory_creation(self, notebook_context):
|
|
142
|
+
"""Test AgentFactory creation."""
|
|
143
|
+
factory = AgentFactory(notebook_context=notebook_context)
|
|
144
|
+
assert factory._notebook_context is notebook_context
|
|
145
|
+
assert factory._on_event is None
|
|
146
|
+
|
|
147
|
+
def test_factory_with_callbacks(self, notebook_context):
|
|
148
|
+
"""Test AgentFactory with event callbacks."""
|
|
149
|
+
on_event = MagicMock()
|
|
150
|
+
|
|
151
|
+
factory = AgentFactory(
|
|
152
|
+
notebook_context=notebook_context,
|
|
153
|
+
on_event=on_event,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
assert factory._on_event is on_event
|
|
157
|
+
|
|
158
|
+
@pytest.mark.skipif(
|
|
159
|
+
not _has_langchain(),
|
|
160
|
+
reason="langchain not installed",
|
|
161
|
+
)
|
|
162
|
+
@patch("jupyter_ext.agent.agent_factory.SummarizationMiddleware")
|
|
163
|
+
@patch("jupyter_ext.agent.agent_factory.create_deep_agent")
|
|
164
|
+
@patch("jupyter_ext.agent.agent_factory.init_chat_model")
|
|
165
|
+
def test_create_agent(
|
|
166
|
+
self, mock_init_model, mock_create_deep_agent, mock_summarization, factory
|
|
167
|
+
):
|
|
168
|
+
"""Test agent creation."""
|
|
169
|
+
mock_model = MagicMock()
|
|
170
|
+
mock_init_model.return_value = mock_model
|
|
171
|
+
|
|
172
|
+
mock_agent_graph = MagicMock()
|
|
173
|
+
# create_deep_agent().with_config() chain
|
|
174
|
+
mock_create_deep_agent.return_value = mock_agent_graph
|
|
175
|
+
mock_agent_graph.with_config.return_value = mock_agent_graph
|
|
176
|
+
|
|
177
|
+
config = AgentConfig(
|
|
178
|
+
provider="openai",
|
|
179
|
+
model="gpt-4o",
|
|
180
|
+
api_key="test-key",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
runner = factory.create_agent(config)
|
|
184
|
+
|
|
185
|
+
# Verify init_chat_model was called with correct params
|
|
186
|
+
mock_init_model.assert_called_once()
|
|
187
|
+
call_args = mock_init_model.call_args
|
|
188
|
+
assert "openai:gpt-4o" in call_args[0]
|
|
189
|
+
|
|
190
|
+
# Verify create_deep_agent was called
|
|
191
|
+
mock_create_deep_agent.assert_called_once()
|
|
192
|
+
call_kwargs = mock_create_deep_agent.call_args[1]
|
|
193
|
+
assert call_kwargs["model"] is mock_model
|
|
194
|
+
assert len(call_kwargs["tools"]) >= 3 # At least jupyter tools
|
|
195
|
+
|
|
196
|
+
# Verify with_config was called with recursion_limit
|
|
197
|
+
mock_agent_graph.with_config.assert_called_once_with({"recursion_limit": 1000})
|
|
198
|
+
|
|
199
|
+
# Verify runner was created
|
|
200
|
+
assert runner is not None
|
|
201
|
+
assert runner._config is config
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TestLangChainTools:
|
|
205
|
+
"""Tests for LangChain-compatible Jupyter tools."""
|
|
206
|
+
|
|
207
|
+
@pytest.fixture
|
|
208
|
+
def context(self):
|
|
209
|
+
"""Create a NotebookContextManager with sample notebook."""
|
|
210
|
+
ctx = NotebookContextManager()
|
|
211
|
+
ctx.set_notebook(
|
|
212
|
+
"test.ipynb",
|
|
213
|
+
{
|
|
214
|
+
"content": {
|
|
215
|
+
"cells": [
|
|
216
|
+
{
|
|
217
|
+
"cell_type": "code",
|
|
218
|
+
"source": "x = 1",
|
|
219
|
+
"outputs": [],
|
|
220
|
+
"execution_count": 1,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"cell_type": "markdown",
|
|
224
|
+
"source": "# Header",
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
"metadata": {"kernelspec": {"name": "python3"}},
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
return ctx
|
|
232
|
+
|
|
233
|
+
def test_jupyter_cell_create(self, context):
|
|
234
|
+
"""Test jupyter_cell CREATE operation."""
|
|
235
|
+
from jupyter_ext.agent.langchain_tools import create_jupyter_cell_tool
|
|
236
|
+
|
|
237
|
+
tool = create_jupyter_cell_tool(context)
|
|
238
|
+
result = tool.invoke({"operation": "CREATE", "code": "print('hello')"})
|
|
239
|
+
data = json.loads(result)
|
|
240
|
+
|
|
241
|
+
assert data["success"] is True
|
|
242
|
+
assert data["data"]["operation"] == "created"
|
|
243
|
+
assert "cell_index" in data["data"]
|
|
244
|
+
|
|
245
|
+
def test_jupyter_cell_read(self, context):
|
|
246
|
+
"""Test jupyter_cell READ operation."""
|
|
247
|
+
from jupyter_ext.agent.langchain_tools import create_jupyter_cell_tool
|
|
248
|
+
|
|
249
|
+
tool = create_jupyter_cell_tool(context)
|
|
250
|
+
result = tool.invoke({"operation": "READ", "cell_index": 0})
|
|
251
|
+
data = json.loads(result)
|
|
252
|
+
|
|
253
|
+
assert data["success"] is True
|
|
254
|
+
assert data["data"]["source"] == "x = 1"
|
|
255
|
+
assert data["data"]["cell_type"] == "code"
|
|
256
|
+
|
|
257
|
+
def test_jupyter_cell_update(self, context):
|
|
258
|
+
"""Test jupyter_cell UPDATE operation."""
|
|
259
|
+
from jupyter_ext.agent.langchain_tools import create_jupyter_cell_tool
|
|
260
|
+
|
|
261
|
+
tool = create_jupyter_cell_tool(context)
|
|
262
|
+
result = tool.invoke({"operation": "UPDATE", "cell_index": 0, "code": "y = 2"})
|
|
263
|
+
data = json.loads(result)
|
|
264
|
+
|
|
265
|
+
assert data["success"] is True
|
|
266
|
+
assert data["data"]["operation"] == "updated"
|
|
267
|
+
|
|
268
|
+
# Verify update
|
|
269
|
+
assert context.current_notebook.cells[0].source == "y = 2"
|
|
270
|
+
|
|
271
|
+
def test_jupyter_cell_delete(self, context):
|
|
272
|
+
"""Test jupyter_cell DELETE operation."""
|
|
273
|
+
from jupyter_ext.agent.langchain_tools import create_jupyter_cell_tool
|
|
274
|
+
|
|
275
|
+
tool = create_jupyter_cell_tool(context)
|
|
276
|
+
initial_count = context.current_notebook.cell_count
|
|
277
|
+
|
|
278
|
+
result = tool.invoke({"operation": "DELETE", "cell_index": 0})
|
|
279
|
+
data = json.loads(result)
|
|
280
|
+
|
|
281
|
+
assert data["success"] is True
|
|
282
|
+
assert context.current_notebook.cell_count == initial_count - 1
|
|
283
|
+
|
|
284
|
+
def test_jupyter_cell_execute(self, context):
|
|
285
|
+
"""Test jupyter_cell EXECUTE operation."""
|
|
286
|
+
from jupyter_ext.agent.langchain_tools import create_jupyter_cell_tool
|
|
287
|
+
|
|
288
|
+
tool = create_jupyter_cell_tool(context)
|
|
289
|
+
result = tool.invoke({"operation": "EXECUTE", "code": "print('test')"})
|
|
290
|
+
data = json.loads(result)
|
|
291
|
+
|
|
292
|
+
assert data["success"] is True
|
|
293
|
+
assert data["data"]["operation"] == "execute_requested"
|
|
294
|
+
assert data["metadata"]["requires_kernel"] is True
|
|
295
|
+
|
|
296
|
+
def test_jupyter_markdown_create(self, context):
|
|
297
|
+
"""Test jupyter_markdown CREATE operation."""
|
|
298
|
+
from jupyter_ext.agent.langchain_tools import create_jupyter_markdown_tool
|
|
299
|
+
|
|
300
|
+
tool = create_jupyter_markdown_tool(context)
|
|
301
|
+
result = tool.invoke({"operation": "CREATE", "content": "## Section"})
|
|
302
|
+
data = json.loads(result)
|
|
303
|
+
|
|
304
|
+
assert data["success"] is True
|
|
305
|
+
assert data["data"]["operation"] == "created"
|
|
306
|
+
|
|
307
|
+
def test_notebook_info(self, context):
|
|
308
|
+
"""Test notebook_info tool."""
|
|
309
|
+
from jupyter_ext.agent.langchain_tools import create_notebook_info_tool
|
|
310
|
+
|
|
311
|
+
tool = create_notebook_info_tool(context)
|
|
312
|
+
result = tool.invoke({})
|
|
313
|
+
data = json.loads(result)
|
|
314
|
+
|
|
315
|
+
assert data["success"] is True
|
|
316
|
+
assert data["data"]["path"] == "test.ipynb"
|
|
317
|
+
assert data["data"]["cell_count"] == 2
|
|
318
|
+
assert data["data"]["code_cells"] == 1
|
|
319
|
+
assert data["data"]["markdown_cells"] == 1
|
|
320
|
+
|
|
321
|
+
def test_tool_error_handling(self, context):
|
|
322
|
+
"""Test tool error handling."""
|
|
323
|
+
from jupyter_ext.agent.langchain_tools import create_jupyter_cell_tool
|
|
324
|
+
|
|
325
|
+
tool = create_jupyter_cell_tool(context)
|
|
326
|
+
|
|
327
|
+
# Invalid operation
|
|
328
|
+
result = tool.invoke({"operation": "INVALID"})
|
|
329
|
+
data = json.loads(result)
|
|
330
|
+
assert data["success"] is False
|
|
331
|
+
assert "알 수 없는 연산" in data["error"]
|
|
332
|
+
|
|
333
|
+
# Missing required params
|
|
334
|
+
result = tool.invoke({"operation": "READ"})
|
|
335
|
+
data = json.loads(result)
|
|
336
|
+
assert data["success"] is False
|
|
337
|
+
assert "cell_index가 필요합니다" in data["error"]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class TestFileTools:
|
|
341
|
+
"""Tests for LangChain-compatible file tools."""
|
|
342
|
+
|
|
343
|
+
@pytest.fixture
|
|
344
|
+
def temp_workspace(self, tmp_path):
|
|
345
|
+
"""Create a temporary workspace with test files."""
|
|
346
|
+
# Create test files
|
|
347
|
+
(tmp_path / "test.py").write_text("x = 1\nprint(x)")
|
|
348
|
+
(tmp_path / "data.txt").write_text("hello world")
|
|
349
|
+
(tmp_path / "subdir").mkdir()
|
|
350
|
+
(tmp_path / "subdir" / "nested.py").write_text("def foo(): pass")
|
|
351
|
+
return str(tmp_path)
|
|
352
|
+
|
|
353
|
+
def test_file_read_success(self, temp_workspace):
|
|
354
|
+
"""Test file_read tool with valid file."""
|
|
355
|
+
from jupyter_ext.agent.langchain_tools import create_file_read_tool
|
|
356
|
+
|
|
357
|
+
tool = create_file_read_tool(temp_workspace)
|
|
358
|
+
result = tool.invoke({"path": "test.py"})
|
|
359
|
+
data = json.loads(result)
|
|
360
|
+
|
|
361
|
+
assert data["success"] is True
|
|
362
|
+
assert data["data"]["content"] == "x = 1\nprint(x)"
|
|
363
|
+
assert data["data"]["path"] == "test.py"
|
|
364
|
+
|
|
365
|
+
def test_file_read_not_found(self, temp_workspace):
|
|
366
|
+
"""Test file_read tool with non-existent file."""
|
|
367
|
+
from jupyter_ext.agent.langchain_tools import create_file_read_tool
|
|
368
|
+
|
|
369
|
+
tool = create_file_read_tool(temp_workspace)
|
|
370
|
+
result = tool.invoke({"path": "nonexistent.py"})
|
|
371
|
+
data = json.loads(result)
|
|
372
|
+
|
|
373
|
+
assert data["success"] is False
|
|
374
|
+
assert "찾을 수 없음" in data["error"]
|
|
375
|
+
|
|
376
|
+
def test_file_write_success(self, temp_workspace):
|
|
377
|
+
"""Test file_write tool with new file."""
|
|
378
|
+
from jupyter_ext.agent.langchain_tools import create_file_write_tool
|
|
379
|
+
|
|
380
|
+
tool = create_file_write_tool(temp_workspace)
|
|
381
|
+
result = tool.invoke({"path": "new_file.txt", "content": "new content"})
|
|
382
|
+
data = json.loads(result)
|
|
383
|
+
|
|
384
|
+
assert data["success"] is True
|
|
385
|
+
assert data["data"]["operation"] == "write"
|
|
386
|
+
|
|
387
|
+
# Verify file was created
|
|
388
|
+
from pathlib import Path
|
|
389
|
+
|
|
390
|
+
assert (Path(temp_workspace) / "new_file.txt").read_text() == "new content"
|
|
391
|
+
|
|
392
|
+
def test_file_write_append(self, temp_workspace):
|
|
393
|
+
"""Test file_write tool with append mode."""
|
|
394
|
+
from jupyter_ext.agent.langchain_tools import create_file_write_tool
|
|
395
|
+
|
|
396
|
+
tool = create_file_write_tool(temp_workspace)
|
|
397
|
+
result = tool.invoke(
|
|
398
|
+
{"path": "data.txt", "content": "\nappended", "append": True}
|
|
399
|
+
)
|
|
400
|
+
data = json.loads(result)
|
|
401
|
+
|
|
402
|
+
assert data["success"] is True
|
|
403
|
+
assert data["data"]["operation"] == "append"
|
|
404
|
+
|
|
405
|
+
# Verify content
|
|
406
|
+
from pathlib import Path
|
|
407
|
+
|
|
408
|
+
assert (
|
|
409
|
+
Path(temp_workspace) / "data.txt"
|
|
410
|
+
).read_text() == "hello world\nappended"
|
|
411
|
+
|
|
412
|
+
def test_file_list_success(self, temp_workspace):
|
|
413
|
+
"""Test file_list tool."""
|
|
414
|
+
from jupyter_ext.agent.langchain_tools import create_file_list_tool
|
|
415
|
+
|
|
416
|
+
tool = create_file_list_tool(temp_workspace)
|
|
417
|
+
result = tool.invoke({"path": "."})
|
|
418
|
+
data = json.loads(result)
|
|
419
|
+
|
|
420
|
+
assert data["success"] is True
|
|
421
|
+
names = [e["name"] for e in data["data"]["entries"]]
|
|
422
|
+
assert "test.py" in names
|
|
423
|
+
assert "data.txt" in names
|
|
424
|
+
assert "subdir" in names
|
|
425
|
+
|
|
426
|
+
def test_file_list_pattern(self, temp_workspace):
|
|
427
|
+
"""Test file_list tool with pattern filter."""
|
|
428
|
+
from jupyter_ext.agent.langchain_tools import create_file_list_tool
|
|
429
|
+
|
|
430
|
+
tool = create_file_list_tool(temp_workspace)
|
|
431
|
+
result = tool.invoke({"path": ".", "pattern": "*.py"})
|
|
432
|
+
data = json.loads(result)
|
|
433
|
+
|
|
434
|
+
assert data["success"] is True
|
|
435
|
+
names = [e["name"] for e in data["data"]["entries"]]
|
|
436
|
+
assert "test.py" in names
|
|
437
|
+
assert "data.txt" not in names
|
|
438
|
+
|
|
439
|
+
def test_file_list_recursive(self, temp_workspace):
|
|
440
|
+
"""Test file_list tool with recursive option."""
|
|
441
|
+
from jupyter_ext.agent.langchain_tools import create_file_list_tool
|
|
442
|
+
|
|
443
|
+
tool = create_file_list_tool(temp_workspace)
|
|
444
|
+
result = tool.invoke({"path": ".", "recursive": True, "pattern": "*.py"})
|
|
445
|
+
data = json.loads(result)
|
|
446
|
+
|
|
447
|
+
assert data["success"] is True
|
|
448
|
+
paths = [e["path"] for e in data["data"]["entries"]]
|
|
449
|
+
assert "test.py" in paths
|
|
450
|
+
assert any("nested.py" in p for p in paths)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class TestSearchTools:
|
|
454
|
+
"""Tests for LangChain-compatible search tools."""
|
|
455
|
+
|
|
456
|
+
@pytest.fixture
|
|
457
|
+
def temp_workspace(self, tmp_path):
|
|
458
|
+
"""Create a temporary workspace with test files."""
|
|
459
|
+
(tmp_path / "main.py").write_text("def hello():\n print('hello')\n\nhello()")
|
|
460
|
+
(tmp_path / "utils.py").write_text("def helper():\n return 42\n\nhelper()")
|
|
461
|
+
(tmp_path / "data.txt").write_text("some data")
|
|
462
|
+
return str(tmp_path)
|
|
463
|
+
|
|
464
|
+
def test_search_code_success(self, temp_workspace):
|
|
465
|
+
"""Test search_code tool with valid pattern."""
|
|
466
|
+
from jupyter_ext.agent.langchain_tools import create_search_code_tool
|
|
467
|
+
|
|
468
|
+
tool = create_search_code_tool(temp_workspace)
|
|
469
|
+
result = tool.invoke({"pattern": "def", "file_pattern": "*.py"})
|
|
470
|
+
data = json.loads(result)
|
|
471
|
+
|
|
472
|
+
assert data["success"] is True
|
|
473
|
+
assert len(data["data"]["matches"]) >= 2
|
|
474
|
+
|
|
475
|
+
# Check that we found both functions
|
|
476
|
+
files = [m["file"] for m in data["data"]["matches"]]
|
|
477
|
+
assert "main.py" in files
|
|
478
|
+
assert "utils.py" in files
|
|
479
|
+
|
|
480
|
+
def test_search_code_case_insensitive(self, temp_workspace):
|
|
481
|
+
"""Test search_code tool with case-insensitive search."""
|
|
482
|
+
from jupyter_ext.agent.langchain_tools import create_search_code_tool
|
|
483
|
+
|
|
484
|
+
tool = create_search_code_tool(temp_workspace)
|
|
485
|
+
result = tool.invoke({"pattern": "HELLO", "case_sensitive": False})
|
|
486
|
+
data = json.loads(result)
|
|
487
|
+
|
|
488
|
+
assert data["success"] is True
|
|
489
|
+
assert len(data["data"]["matches"]) > 0
|
|
490
|
+
|
|
491
|
+
def test_search_code_invalid_regex(self, temp_workspace):
|
|
492
|
+
"""Test search_code tool with invalid regex."""
|
|
493
|
+
from jupyter_ext.agent.langchain_tools import create_search_code_tool
|
|
494
|
+
|
|
495
|
+
tool = create_search_code_tool(temp_workspace)
|
|
496
|
+
result = tool.invoke({"pattern": "[invalid"})
|
|
497
|
+
data = json.loads(result)
|
|
498
|
+
|
|
499
|
+
assert data["success"] is False
|
|
500
|
+
assert "잘못된 정규식" in data["error"]
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
class TestAllToolsIntegration:
|
|
504
|
+
"""Tests for _create_all_tools method."""
|
|
505
|
+
|
|
506
|
+
@pytest.fixture
|
|
507
|
+
def notebook_context(self):
|
|
508
|
+
"""Create a NotebookContextManager with sample notebook."""
|
|
509
|
+
ctx = NotebookContextManager()
|
|
510
|
+
ctx.set_notebook(
|
|
511
|
+
"test.ipynb",
|
|
512
|
+
{
|
|
513
|
+
"content": {
|
|
514
|
+
"cells": [
|
|
515
|
+
{"cell_type": "code", "source": "x = 1", "outputs": []},
|
|
516
|
+
],
|
|
517
|
+
"metadata": {"kernelspec": {"name": "python3"}},
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
)
|
|
521
|
+
return ctx
|
|
522
|
+
|
|
523
|
+
@pytest.fixture
|
|
524
|
+
def factory(self, notebook_context, tmp_path):
|
|
525
|
+
"""Create an AgentFactory instance with workspace_root."""
|
|
526
|
+
return AgentFactory(
|
|
527
|
+
notebook_context=notebook_context,
|
|
528
|
+
workspace_root=str(tmp_path),
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
def test_create_all_tools_count(self, factory):
|
|
532
|
+
"""Test that _create_all_tools returns all expected tools."""
|
|
533
|
+
tools = factory._create_all_tools()
|
|
534
|
+
|
|
535
|
+
assert len(tools) == 11
|
|
536
|
+
|
|
537
|
+
def test_create_all_tools_names(self, factory):
|
|
538
|
+
"""Test that all expected tools are created."""
|
|
539
|
+
tools = factory._create_all_tools()
|
|
540
|
+
tool_names = [t.name for t in tools]
|
|
541
|
+
|
|
542
|
+
# Jupyter tools
|
|
543
|
+
assert "jupyter_cell" in tool_names
|
|
544
|
+
assert "jupyter_markdown" in tool_names
|
|
545
|
+
assert "notebook_info" in tool_names
|
|
546
|
+
|
|
547
|
+
# File tools (read_file은 DeepAgents FilesystemMiddleware가 제공)
|
|
548
|
+
assert "file_write" in tool_names
|
|
549
|
+
|
|
550
|
+
# LSP tools (grep-based; search_code는 grep_search가 대체)
|
|
551
|
+
assert "diagnostics" in tool_names
|
|
552
|
+
assert "references" in tool_names
|
|
553
|
+
|
|
554
|
+
# 보안 스캔 도구
|
|
555
|
+
assert "security_scan" in tool_names
|
|
556
|
+
|
|
557
|
+
# AST 기반 코드 분석 도구
|
|
558
|
+
assert "analyze_file_structure" in tool_names
|
|
559
|
+
assert "search_symbol_definition" in tool_names
|
|
560
|
+
assert "analyze_dependencies" in tool_names
|
|
561
|
+
|
|
562
|
+
# Fuzzy 파일 검색 도구
|
|
563
|
+
assert "file_find" in tool_names
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Tests for FileIndex fuzzy and prefix search."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from ..tools.file_finder import FileIndex
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def sample_tree(tmp_path):
|
|
10
|
+
"""Create a sample file tree for testing."""
|
|
11
|
+
# Create directories
|
|
12
|
+
(tmp_path / "src").mkdir()
|
|
13
|
+
(tmp_path / "src" / "components").mkdir()
|
|
14
|
+
(tmp_path / "tests").mkdir()
|
|
15
|
+
(tmp_path / "__pycache__").mkdir()
|
|
16
|
+
(tmp_path / "node_modules").mkdir()
|
|
17
|
+
|
|
18
|
+
# Create files
|
|
19
|
+
(tmp_path / "main.py").touch()
|
|
20
|
+
(tmp_path / "config.yaml").touch()
|
|
21
|
+
(tmp_path / "README.md").touch()
|
|
22
|
+
(tmp_path / "src" / "auth.py").touch()
|
|
23
|
+
(tmp_path / "src" / "auth_utils.py").touch()
|
|
24
|
+
(tmp_path / "src" / "authentication.ts").touch()
|
|
25
|
+
(tmp_path / "src" / "components" / "AuthPanel.tsx").touch()
|
|
26
|
+
(tmp_path / "src" / "components" / "UserPanel.tsx").touch()
|
|
27
|
+
(tmp_path / "tests" / "test_auth.py").touch()
|
|
28
|
+
(tmp_path / "__pycache__" / "main.cpython-311.pyc").touch()
|
|
29
|
+
(tmp_path / "node_modules" / "lodash.js").touch()
|
|
30
|
+
|
|
31
|
+
return tmp_path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def index(sample_tree):
|
|
36
|
+
return FileIndex(sample_tree, ttl=0) # ttl=0 forces rebuild each time
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestFileIndexBuild:
|
|
40
|
+
def test_excludes_pycache(self, index):
|
|
41
|
+
index._build_index()
|
|
42
|
+
assert not any("__pycache__" in e for e in index._entries)
|
|
43
|
+
|
|
44
|
+
def test_excludes_node_modules(self, index):
|
|
45
|
+
index._build_index()
|
|
46
|
+
assert not any("node_modules" in e for e in index._entries)
|
|
47
|
+
|
|
48
|
+
def test_includes_regular_files(self, index):
|
|
49
|
+
index._build_index()
|
|
50
|
+
assert "main.py" in index._entries
|
|
51
|
+
|
|
52
|
+
def test_includes_directories(self, index):
|
|
53
|
+
index._build_index()
|
|
54
|
+
assert "src/" in index._entries
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestFuzzySearch:
|
|
58
|
+
def test_exact_match_scores_highest(self, index):
|
|
59
|
+
results = index.fuzzy_search("main.py")
|
|
60
|
+
assert len(results) > 0
|
|
61
|
+
assert results[0]["path"] == "main.py"
|
|
62
|
+
|
|
63
|
+
def test_partial_match(self, index):
|
|
64
|
+
results = index.fuzzy_search("auth")
|
|
65
|
+
assert len(results) > 0
|
|
66
|
+
paths = [r["path"] for r in results]
|
|
67
|
+
assert any("auth" in p for p in paths)
|
|
68
|
+
|
|
69
|
+
def test_dir_filter_with_slash(self, index):
|
|
70
|
+
results = index.fuzzy_search("src/auth")
|
|
71
|
+
paths = [r["path"] for r in results]
|
|
72
|
+
assert all(p.startswith("src/") or p.startswith("src") for p in paths)
|
|
73
|
+
|
|
74
|
+
def test_empty_query_returns_empty(self, index):
|
|
75
|
+
assert index.fuzzy_search("") == []
|
|
76
|
+
|
|
77
|
+
def test_max_results_respected(self, index):
|
|
78
|
+
results = index.fuzzy_search("py", max_results=3)
|
|
79
|
+
assert len(results) <= 3
|
|
80
|
+
|
|
81
|
+
def test_supported_exts_filter(self, index):
|
|
82
|
+
results = index.fuzzy_search("auth", supported_exts={".py"})
|
|
83
|
+
for r in results:
|
|
84
|
+
if r["type"] == "file":
|
|
85
|
+
assert r["path"].endswith(".py")
|
|
86
|
+
|
|
87
|
+
def test_results_have_score(self, index):
|
|
88
|
+
results = index.fuzzy_search("main")
|
|
89
|
+
for r in results:
|
|
90
|
+
assert "score" in r
|
|
91
|
+
assert isinstance(r["score"], float)
|
|
92
|
+
|
|
93
|
+
def test_results_have_type(self, index):
|
|
94
|
+
results = index.fuzzy_search("src")
|
|
95
|
+
types = {r["type"] for r in results}
|
|
96
|
+
assert types <= {"file", "directory"}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TestPrefixSearch:
|
|
100
|
+
def test_root_prefix(self, index):
|
|
101
|
+
results = index.prefix_search("")
|
|
102
|
+
assert len(results) > 0
|
|
103
|
+
|
|
104
|
+
def test_dir_prefix(self, index):
|
|
105
|
+
results = index.prefix_search("src/")
|
|
106
|
+
paths = [r["path"] for r in results]
|
|
107
|
+
assert all(p.startswith("src") for p in paths)
|
|
108
|
+
|
|
109
|
+
def test_ext_filter(self, index):
|
|
110
|
+
results = index.prefix_search("src/", supported_exts={".py"})
|
|
111
|
+
for r in results:
|
|
112
|
+
if r["type"] == "file":
|
|
113
|
+
assert r["path"].endswith(".py")
|
|
114
|
+
|
|
115
|
+
def test_directories_first(self, index):
|
|
116
|
+
results = index.prefix_search("")
|
|
117
|
+
dirs = [i for i, r in enumerate(results) if r["type"] == "directory"]
|
|
118
|
+
files = [i for i, r in enumerate(results) if r["type"] == "file"]
|
|
119
|
+
if dirs and files:
|
|
120
|
+
assert max(dirs) < min(files)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestCaching:
|
|
124
|
+
def test_ttl_prevents_rebuild(self, sample_tree):
|
|
125
|
+
idx = FileIndex(sample_tree, ttl=60)
|
|
126
|
+
idx._build_index()
|
|
127
|
+
built_at = idx._built_at
|
|
128
|
+
idx._maybe_rebuild()
|
|
129
|
+
assert idx._built_at == built_at # no rebuild
|
|
130
|
+
|
|
131
|
+
def test_ttl_zero_always_rebuilds(self, sample_tree):
|
|
132
|
+
idx = FileIndex(sample_tree, ttl=0)
|
|
133
|
+
idx._build_index()
|
|
134
|
+
built_at = idx._built_at
|
|
135
|
+
import time
|
|
136
|
+
|
|
137
|
+
time.sleep(0.01)
|
|
138
|
+
idx._maybe_rebuild()
|
|
139
|
+
assert idx._built_at > built_at # rebuilt
|