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.
Files changed (217) hide show
  1. codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  2. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
  3. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  4. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
  5. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
  6. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
  7. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
  8. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
  9. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
  10. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
  11. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  12. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  13. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  14. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
  15. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
  16. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
  17. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
  18. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
  19. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  20. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  21. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
  22. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
  23. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  24. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
  25. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
  26. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
  27. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
  28. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  29. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  30. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  31. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  32. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
  33. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
  34. codebuff_agent-2.1.74.dist-info/METADATA +142 -0
  35. codebuff_agent-2.1.74.dist-info/RECORD +217 -0
  36. codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
  37. codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
  38. codebuff_core/__init__.py +153 -0
  39. codebuff_core/codebuff_config_defaults.json +64 -0
  40. codebuff_core/core/__init__.py +35 -0
  41. codebuff_core/core/code_validator.py +1238 -0
  42. codebuff_core/core/error_classifier.py +579 -0
  43. codebuff_core/core/exceptions.py +49 -0
  44. codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
  45. codebuff_core/directory_policy.py +32 -0
  46. codebuff_core/factory.py +155 -0
  47. codebuff_core/interfaces.py +197 -0
  48. codebuff_core/knowledge/__init__.py +31 -0
  49. codebuff_core/knowledge/chunking.py +375 -0
  50. codebuff_core/knowledge/loader.py +339 -0
  51. codebuff_core/knowledge/watchdog_service.py +354 -0
  52. codebuff_core/legacy_paths.py +69 -0
  53. codebuff_core/llm/__init__.py +13 -0
  54. codebuff_core/llm/service.py +407 -0
  55. codebuff_core/managers/__init__.py +22 -0
  56. codebuff_core/managers/config_manager.py +690 -0
  57. codebuff_core/managers/remote_config.py +673 -0
  58. codebuff_core/managers/remote_skills.py +539 -0
  59. codebuff_core/managers/session_manager.py +255 -0
  60. codebuff_core/middleware/__init__.py +3 -0
  61. codebuff_core/middleware/error_recovery.py +394 -0
  62. codebuff_core/middleware/logging_utils.py +33 -0
  63. codebuff_core/models/__init__.py +111 -0
  64. codebuff_core/models/agent.py +306 -0
  65. codebuff_core/models/chat.py +57 -0
  66. codebuff_core/models/common.py +82 -0
  67. codebuff_core/models/rag.py +300 -0
  68. codebuff_core/platform_rules.py +127 -0
  69. codebuff_core/prompts/__init__.py +44 -0
  70. codebuff_core/prompts/auto_agent_prompts.py +1278 -0
  71. codebuff_core/services/__init__.py +30 -0
  72. codebuff_core/services/agent_service.py +345 -0
  73. codebuff_core/services/analytics_aggregate.py +837 -0
  74. codebuff_core/services/chat_service.py +898 -0
  75. codebuff_core/services/context_condenser.py +306 -0
  76. codebuff_core/services/pii_guardrail.py +393 -0
  77. codebuff_core/services/rag_chat_agent.py +2993 -0
  78. codebuff_core/services/rag_collections.py +2740 -0
  79. codebuff_core/services/rag_manager.py +890 -0
  80. codebuff_core/services/rag_service.py +145 -0
  81. codebuff_core/services/retriever.py +260 -0
  82. codebuff_core/services/vllm_embedding_service.py +290 -0
  83. codebuff_core/tests/__init__.py +1 -0
  84. codebuff_core/tests/conftest.py +81 -0
  85. codebuff_core/tests/test_error_recovery.py +647 -0
  86. codebuff_core/tests/test_factory.py +135 -0
  87. codebuff_core/tests/test_services.py +150 -0
  88. jupyter_ext/__init__.py +266 -0
  89. jupyter_ext/_version.py +4 -0
  90. jupyter_ext/agent/__init__.py +49 -0
  91. jupyter_ext/agent/agent_factory.py +2270 -0
  92. jupyter_ext/agent/ast_tools.py +451 -0
  93. jupyter_ext/agent/autofix_middleware.py +481 -0
  94. jupyter_ext/agent/background_procs.py +372 -0
  95. jupyter_ext/agent/code_history_middleware.py +211 -0
  96. jupyter_ext/agent/codex_autofix.py +160 -0
  97. jupyter_ext/agent/codex_bridge.py +3339 -0
  98. jupyter_ext/agent/codex_hooks.py +320 -0
  99. jupyter_ext/agent/codex_slash.py +685 -0
  100. jupyter_ext/agent/codex_threads.py +114 -0
  101. jupyter_ext/agent/content_injection.py +377 -0
  102. jupyter_ext/agent/continuation_middleware.py +548 -0
  103. jupyter_ext/agent/custom_skills.py +335 -0
  104. jupyter_ext/agent/file_diffs.py +99 -0
  105. jupyter_ext/agent/git_history.py +188 -0
  106. jupyter_ext/agent/guardrail_middleware.py +421 -0
  107. jupyter_ext/agent/json_repair_utils.py +54 -0
  108. jupyter_ext/agent/langchain_tools.py +2673 -0
  109. jupyter_ext/agent/mermaid_fixer.py +289 -0
  110. jupyter_ext/agent/mermaid_middleware.py +135 -0
  111. jupyter_ext/agent/middleware.py +359 -0
  112. jupyter_ext/agent/middleware_prompts.py +53 -0
  113. jupyter_ext/agent/observability.py +334 -0
  114. jupyter_ext/agent/patch_tool_calls.py +57 -0
  115. jupyter_ext/agent/prompts.py +112 -0
  116. jupyter_ext/agent/skill_middleware.py +257 -0
  117. jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
  118. jupyter_ext/agent/skills/anomaly_detection.md +12 -0
  119. jupyter_ext/agent/skills/athena_sql.md +156 -0
  120. jupyter_ext/agent/skills/bdp_table_search.md +33 -0
  121. jupyter_ext/agent/skills/bigquery.md +65 -0
  122. jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
  123. jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
  124. jupyter_ext/agent/skills/classification_evaluation.md +11 -0
  125. jupyter_ext/agent/skills/clustering_analysis.md +12 -0
  126. jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
  127. jupyter_ext/agent/skills/data_analysis.md +236 -0
  128. jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
  129. jupyter_ext/agent/skills/data_loading.md +158 -0
  130. jupyter_ext/agent/skills/data_quality_report.md +13 -0
  131. jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
  132. jupyter_ext/agent/skills/emr_script_development.md +17 -0
  133. jupyter_ext/agent/skills/experiment_tracking.md +13 -0
  134. jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
  135. jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
  136. jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
  137. jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
  138. jupyter_ext/agent/skills/inference.md +392 -0
  139. jupyter_ext/agent/skills/lambda_function_development.md +19 -0
  140. jupyter_ext/agent/skills/model_comparison.md +11 -0
  141. jupyter_ext/agent/skills/model_explainability.md +12 -0
  142. jupyter_ext/agent/skills/model_training.md +318 -0
  143. jupyter_ext/agent/skills/pyspark.md +352 -0
  144. jupyter_ext/agent/skills/python_code_quality.md +35 -0
  145. jupyter_ext/agent/skills/regression_evaluation.md +12 -0
  146. jupyter_ext/agent/skills/spring_java_development.md +35 -0
  147. jupyter_ext/agent/skills/text_preprocessing.md +12 -0
  148. jupyter_ext/agent/skills/time_series_analysis.md +13 -0
  149. jupyter_ext/agent/skills/unit_testing.md +203 -0
  150. jupyter_ext/agent/todo_prompts.py +57 -0
  151. jupyter_ext/agent/tool_report.py +45 -0
  152. jupyter_ext/analytics_ingest.py +599 -0
  153. jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  154. jupyter_ext/handlers/__init__.py +8 -0
  155. jupyter_ext/handlers/analytics_handler.py +170 -0
  156. jupyter_ext/handlers/benchmark_handler.py +794 -0
  157. jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
  158. jupyter_ext/handlers/rag_admin_handler.py +689 -0
  159. jupyter_ext/handlers/websocket.py +497 -0
  160. jupyter_ext/http_handlers.py +8744 -0
  161. jupyter_ext/idle_monitor.py +346 -0
  162. jupyter_ext/labextension/package.json +157 -0
  163. jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  164. jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
  165. jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
  166. jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
  167. jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
  168. jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
  169. jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
  170. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
  171. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  172. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  173. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  174. jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
  175. jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
  176. jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
  177. jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
  178. jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
  179. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  180. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  181. jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
  182. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
  183. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  184. jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
  185. jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
  186. jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
  187. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
  188. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  189. jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  190. jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  191. jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  192. jupyter_ext/labextension/static/style.js +4 -0
  193. jupyter_ext/labextension/static/third-party-licenses.json +724 -0
  194. jupyter_ext/log_paths.py +113 -0
  195. jupyter_ext/resource_usage.py +519 -0
  196. jupyter_ext/secrets_bootstrap.py +116 -0
  197. jupyter_ext/team_access.py +105 -0
  198. jupyter_ext/tests/__init__.py +1 -0
  199. jupyter_ext/tests/test_context.py +234 -0
  200. jupyter_ext/tests/test_factory.py +563 -0
  201. jupyter_ext/tests/test_file_finder.py +139 -0
  202. jupyter_ext/tests/test_handlers.py +58 -0
  203. jupyter_ext/tests/test_mcp_selftest.py +204 -0
  204. jupyter_ext/tests/test_mermaid_fixer.py +272 -0
  205. jupyter_ext/tests/test_resource_tiers.py +298 -0
  206. jupyter_ext/tools/__init__.py +15 -0
  207. jupyter_ext/tools/base.py +90 -0
  208. jupyter_ext/tools/context.py +268 -0
  209. jupyter_ext/tools/dir_complete.py +86 -0
  210. jupyter_ext/tools/file_finder.py +303 -0
  211. jupyter_ext/tools/file_tools.py +244 -0
  212. jupyter_ext/tools/jupyter_tools.py +296 -0
  213. jupyter_ext/tools/registry.py +159 -0
  214. jupyter_ext/tools/search_tools.py +230 -0
  215. jupyter_ext/tools/security_tools.py +657 -0
  216. jupyter_ext/tools/shell_tools.py +109 -0
  217. 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