129 | {/* Header */}
130 |
137 |
145 | Variable Explorer
146 |
147 |
148 |
149 | {/* Controls */}
150 |
157 | {/* Search */}
158 |
setSearchTerm(e.target.value)}
163 | style={{
164 | width: "100%",
165 | padding: "8px 12px",
166 | border: "1px solid #ddd",
167 | borderRadius: "6px",
168 | fontSize: "14px",
169 | marginBottom: "8px",
170 | }}
171 | />
172 |
173 | {/* Filters */}
174 |
181 |
182 |
198 |
199 |
200 |
201 | {/* Variable list */}
202 |
208 | {isLoading ? (
209 |
216 | Loading variables...
217 |
218 | ) : filteredVariables.length === 0 ? (
219 |
226 | {searchTerm || filterType !== "all"
227 | ? "No variables match your filters"
228 | : "No variables in namespace"}
229 |
230 | ) : (
231 |
237 |
238 |
245 | | handleSort("name")}
247 | style={{
248 | padding: "8px 12px",
249 | textAlign: "left",
250 | cursor: "pointer",
251 | fontSize: "13px",
252 | fontWeight: 600,
253 | color: "#666",
254 | }}
255 | >
256 | Name {sortBy === "name" && (sortOrder === "asc" ? "↑" : "↓")}
257 | |
258 | handleSort("type")}
260 | style={{
261 | padding: "8px 12px",
262 | textAlign: "left",
263 | cursor: "pointer",
264 | fontSize: "13px",
265 | fontWeight: 600,
266 | color: "#666",
267 | }}
268 | >
269 | Type {sortBy === "type" && (sortOrder === "asc" ? "↑" : "↓")}
270 | |
271 | handleSort("size")}
273 | style={{
274 | padding: "8px 12px",
275 | textAlign: "right",
276 | cursor: "pointer",
277 | fontSize: "13px",
278 | fontWeight: 600,
279 | color: "#666",
280 | }}
281 | >
282 | Size {sortBy === "size" && (sortOrder === "asc" ? "↑" : "↓")}
283 | |
284 |
285 |
286 |
287 | {filteredVariables.map(variable => (
288 | onInspect(variable)}
291 | style={{
292 | cursor: "pointer",
293 | backgroundColor: "#fff",
294 | borderBottom: "1px solid #f0f0f0",
295 | transition: "background-color 0.1s",
296 | }}
297 | onMouseEnter={e => {
298 | e.currentTarget.style.backgroundColor = "#f8f9fa";
299 | }}
300 | onMouseLeave={e => {
301 | e.currentTarget.style.backgroundColor = "#fff";
302 | }}
303 | >
304 | |
311 | {getVariableIcon(variable.type)}
312 | {variable.name}
313 | {variable.is_callable && (
314 |
321 | ()
322 |
323 | )}
324 | |
325 |
332 | {variable.type}
333 | {variable.shape && (
334 |
341 | {variable.shape.join("×")}
342 |
343 | )}
344 | |
345 |
353 | {formatSize(variable.size)}
354 | |
355 |
356 | ))}
357 |
358 |
359 | )}
360 |
361 |
362 | {/* Footer */}
363 |
372 | {filteredVariables.length} variables
373 | {variables.length !== filteredVariables.length && ` (${variables.length} total)`}
374 |
375 |
376 | );
377 | };
378 |
--------------------------------------------------------------------------------
/tests/test_git_native_tools.py:
--------------------------------------------------------------------------------
1 | """Tests for git-native kernel tools."""
2 |
3 | import subprocess
4 | import tempfile
5 | from pathlib import Path
6 | from typing import Any, Generator
7 | from langchain_core.tools import BaseTool
8 | from unittest.mock import patch
9 |
10 | import pytest
11 |
12 | from assistant_ui_anywidget.kernel_tools import GitFindTool, GitGrepTool, ListFilesTool
13 |
14 |
15 | class TestGitNativeTools:
16 | """Test git-native kernel tools functionality."""
17 |
18 | @pytest.fixture # type: ignore[misc]
19 | def git_repo(self) -> Generator[Path, None, None]:
20 | """Create a temporary git repository for testing."""
21 | with tempfile.TemporaryDirectory() as tmpdir:
22 | repo_path = Path(tmpdir)
23 |
24 | # Initialize git repo
25 | subprocess.run(["git", "init"], cwd=repo_path, check=True)
26 | subprocess.run(
27 | ["git", "config", "user.name", "Test User"], cwd=repo_path, check=True
28 | )
29 | subprocess.run(
30 | ["git", "config", "user.email", "test@example.com"],
31 | cwd=repo_path,
32 | check=True,
33 | )
34 |
35 | # Create test files
36 | (repo_path / "main.py").write_text(
37 | "def main():\n print('Hello World')\n"
38 | )
39 | (repo_path / "config.yaml").write_text("database:\n host: localhost\n")
40 | (repo_path / "test_main.py").write_text(
41 | "def test_main():\n assert True\n"
42 | )
43 | (repo_path / "build").mkdir()
44 | (repo_path / "build" / "output.log").write_text("Build log content\n")
45 | (repo_path / "untracked.txt").write_text("This file is not tracked\n")
46 |
47 | # Add and commit tracked files
48 | subprocess.run(
49 | ["git", "add", "main.py", "config.yaml", "test_main.py"],
50 | cwd=repo_path,
51 | check=True,
52 | )
53 | subprocess.run(
54 | ["git", "commit", "-m", "Initial commit"], cwd=repo_path, check=True
55 | )
56 |
57 | yield repo_path
58 |
59 | def test_list_files_git_tracked_only(self, git_repo: Path) -> None:
60 | """Test listing only git-tracked files."""
61 | tool = ListFilesTool()
62 |
63 | result = tool._run(directory=str(git_repo), git_tracked_only=True)
64 |
65 | assert "git-tracked only" in result
66 | assert "main.py" in result
67 | assert "config.yaml" in result
68 | assert "test_main.py" in result
69 | assert "untracked.txt" not in result
70 | assert "output.log" not in result
71 |
72 | def test_list_files_all_files(self, git_repo: Path) -> None:
73 | """Test listing all files including untracked."""
74 | tool = ListFilesTool()
75 |
76 | result = tool._run(directory=str(git_repo), git_tracked_only=False)
77 |
78 | assert "all files" in result
79 | assert "main.py" in result
80 | assert "config.yaml" in result
81 | assert "test_main.py" in result
82 | assert "untracked.txt" in result
83 |
84 | def test_list_files_with_pattern(self, git_repo: Path) -> None:
85 | """Test listing files with pattern matching."""
86 | tool = ListFilesTool()
87 |
88 | result = tool._run(
89 | directory=str(git_repo), git_tracked_only=True, pattern="*.py"
90 | )
91 |
92 | assert "main.py" in result
93 | assert "test_main.py" in result
94 | assert "config.yaml" not in result
95 |
96 | def test_list_files_non_git_repo(self) -> None:
97 | """Test behavior when not in a git repository."""
98 | tool = ListFilesTool()
99 |
100 | with tempfile.TemporaryDirectory() as tmpdir:
101 | result = tool._run(directory=tmpdir, git_tracked_only=True)
102 |
103 | assert "Not a git repository" in result
104 | assert "Use git_tracked_only=False" in result
105 |
106 | def test_git_grep_basic_search(self, git_repo: Path) -> None:
107 | """Test basic git grep functionality."""
108 | tool = GitGrepTool()
109 |
110 | # Mock subprocess.run to work within the git_repo directory
111 | original_run = subprocess.run
112 |
113 | def mock_run(cmd: Any, **kwargs: Any) -> Any:
114 | if "cwd" not in kwargs:
115 | kwargs["cwd"] = str(git_repo)
116 | return original_run(cmd, **kwargs)
117 |
118 | with patch("subprocess.run", side_effect=mock_run):
119 | result = tool._run(search_term="def main")
120 |
121 | assert "Found" in result
122 | assert "main.py" in result
123 | assert "def main" in result
124 |
125 | def test_git_grep_case_sensitive(self, git_repo: Path) -> None:
126 | """Test case-sensitive git grep."""
127 | tool = GitGrepTool()
128 |
129 | # Mock subprocess.run to work within the git_repo directory
130 | original_run = subprocess.run
131 |
132 | def mock_run(cmd: Any, **kwargs: Any) -> Any:
133 | if "cwd" not in kwargs:
134 | kwargs["cwd"] = str(git_repo)
135 | return original_run(cmd, **kwargs)
136 |
137 | with patch("subprocess.run", side_effect=mock_run):
138 | # Case-insensitive (default)
139 | result_insensitive = tool._run(search_term="HELLO")
140 | assert "Hello World" in result_insensitive
141 |
142 | # Case-sensitive
143 | result_sensitive = tool._run(search_term="HELLO", case_sensitive=True)
144 | assert "No matches found" in result_sensitive
145 |
146 | def test_git_grep_with_file_pattern(self, git_repo: Path) -> None:
147 | """Test git grep with file pattern filter."""
148 | tool = GitGrepTool()
149 |
150 | original_run = subprocess.run
151 |
152 | def mock_run(cmd: Any, **kwargs: Any) -> Any:
153 | if "cwd" not in kwargs:
154 | kwargs["cwd"] = str(git_repo)
155 | return original_run(cmd, **kwargs)
156 |
157 | with patch("subprocess.run", side_effect=mock_run):
158 | result = tool._run(search_term="def", file_pattern="*.py")
159 |
160 | assert "def main" in result
161 | assert "def test_main" in result
162 |
163 | def test_git_grep_no_matches(self, git_repo: Path) -> None:
164 | """Test git grep when no matches found."""
165 | tool = GitGrepTool()
166 |
167 | original_run = subprocess.run
168 |
169 | def mock_run(cmd: Any, **kwargs: Any) -> Any:
170 | if "cwd" not in kwargs:
171 | kwargs["cwd"] = str(git_repo)
172 | return original_run(cmd, **kwargs)
173 |
174 | with patch("subprocess.run", side_effect=mock_run):
175 | result = tool._run(search_term="nonexistent_text")
176 |
177 | assert "No matches found" in result
178 |
179 | def test_git_grep_non_git_repo(self) -> None:
180 | """Test git grep behavior when not in a git repository."""
181 | tool = GitGrepTool()
182 |
183 | with tempfile.TemporaryDirectory() as tmpdir:
184 | original_run = subprocess.run
185 |
186 | def mock_run(cmd: Any, **kwargs: Any) -> Any:
187 | if "cwd" not in kwargs:
188 | kwargs["cwd"] = tmpdir
189 | return original_run(cmd, **kwargs)
190 |
191 | with patch("subprocess.run", side_effect=mock_run):
192 | result = tool._run(search_term="test")
193 |
194 | assert "Not a git repository" in result
195 |
196 | def test_git_find_basic_search(self, git_repo: Path) -> None:
197 | """Test basic git find functionality."""
198 | tool = GitFindTool()
199 |
200 | original_run = subprocess.run
201 |
202 | def mock_run(cmd: Any, **kwargs: Any) -> Any:
203 | if "cwd" not in kwargs:
204 | kwargs["cwd"] = str(git_repo)
205 | return original_run(cmd, **kwargs)
206 |
207 | with patch("subprocess.run", side_effect=mock_run):
208 | result = tool._run(name_pattern="*.py")
209 |
210 | assert "main.py" in result
211 | assert "test_main.py" in result
212 | assert "config.yaml" not in result
213 |
214 | def test_git_find_exact_name(self, git_repo: Path) -> None:
215 | """Test finding exact filename."""
216 | tool = GitFindTool()
217 |
218 | original_run = subprocess.run
219 |
220 | def mock_run(cmd: Any, **kwargs: Any) -> Any:
221 | if "cwd" not in kwargs:
222 | kwargs["cwd"] = str(git_repo)
223 | return original_run(cmd, **kwargs)
224 |
225 | with patch("subprocess.run", side_effect=mock_run):
226 | result = tool._run(name_pattern="config.yaml")
227 |
228 | assert "config.yaml" in result
229 | assert "main.py" not in result
230 |
231 | def test_git_find_case_sensitive(self, git_repo: Path) -> None:
232 | """Test case-sensitive git find."""
233 | tool = GitFindTool()
234 |
235 | original_run = subprocess.run
236 |
237 | def mock_run(cmd: Any, **kwargs: Any) -> Any:
238 | if "cwd" not in kwargs:
239 | kwargs["cwd"] = str(git_repo)
240 | return original_run(cmd, **kwargs)
241 |
242 | with patch("subprocess.run", side_effect=mock_run):
243 | # Case-insensitive (default)
244 | result_insensitive = tool._run(name_pattern="MAIN.PY")
245 | assert "main.py" in result_insensitive
246 |
247 | # Case-sensitive
248 | result_sensitive = tool._run(name_pattern="MAIN.PY", case_sensitive=True)
249 | assert "No git-tracked files found" in result_sensitive
250 |
251 | def test_git_find_no_matches(self, git_repo: Path) -> None:
252 | """Test git find when no matches found."""
253 | tool = GitFindTool()
254 |
255 | original_run = subprocess.run
256 |
257 | def mock_run(cmd: Any, **kwargs: Any) -> Any:
258 | if "cwd" not in kwargs:
259 | kwargs["cwd"] = str(git_repo)
260 | return original_run(cmd, **kwargs)
261 |
262 | with patch("subprocess.run", side_effect=mock_run):
263 | result = tool._run(name_pattern="*.nonexistent")
264 |
265 | assert "No git-tracked files found" in result
266 |
267 | def test_git_find_non_git_repo(self) -> None:
268 | """Test git find behavior when not in a git repository."""
269 | tool = GitFindTool()
270 |
271 | with tempfile.TemporaryDirectory() as tmpdir:
272 | original_run = subprocess.run
273 |
274 | def mock_run(cmd: Any, **kwargs: Any) -> Any:
275 | if "cwd" not in kwargs:
276 | kwargs["cwd"] = tmpdir
277 | return original_run(cmd, **kwargs)
278 |
279 | with patch("subprocess.run", side_effect=mock_run):
280 | result = tool._run(name_pattern="*.py")
281 |
282 | assert "Not a git repository" in result
283 |
284 | def test_error_handling(self) -> None:
285 | """Test error handling in git tools."""
286 | tools = [ListFilesTool(), GitGrepTool(), GitFindTool()]
287 |
288 | # Test with invalid directory
289 | for tool in tools:
290 | if hasattr(tool, "_run"):
291 | with patch(
292 | "subprocess.run",
293 | side_effect=subprocess.CalledProcessError(1, "git"),
294 | ):
295 | if isinstance(tool, ListFilesTool):
296 | result = tool._run(
297 | directory="/nonexistent", git_tracked_only=True
298 | )
299 | elif isinstance(tool, GitGrepTool):
300 | result = tool._run(search_term="test")
301 | else: # GitFindTool
302 | result = tool._run(name_pattern="*.py")
303 |
304 | assert "Error" in result or "Not a git repository" in result
305 |
306 | def test_tools_have_correct_names(self) -> None:
307 | """Test that tools have the expected names."""
308 | assert ListFilesTool().name == "list_files"
309 | assert GitGrepTool().name == "git_grep"
310 | assert GitFindTool().name == "git_find"
311 |
312 | def test_tools_have_descriptions(self) -> None:
313 | """Test that tools have proper descriptions."""
314 | tools = [ListFilesTool(), GitGrepTool(), GitFindTool()]
315 |
316 | for tool in tools:
317 | assert isinstance(tool, BaseTool)
318 | assert tool.description
319 | assert len(tool.description) > 50 # Ensure substantial description
320 | assert "git" in tool.description.lower()
321 |
--------------------------------------------------------------------------------
/tests/test_kernel_interface.py:
--------------------------------------------------------------------------------
1 | """Tests for kernel interface functionality."""
2 |
3 | from unittest.mock import Mock, patch
4 |
5 | import pytest
6 |
7 | from assistant_ui_anywidget.kernel_interface import (
8 | ExecutionResult,
9 | KernelInterface,
10 | StackFrame,
11 | VariableInfo,
12 | )
13 |
14 |
15 | class MockIPython:
16 | """Mock IPython shell for testing."""
17 |
18 | def __init__(self) -> None:
19 | self.user_ns = {
20 | "x": 42,
21 | "y": "hello",
22 | "df": self._create_mock_dataframe(),
23 | "arr": self._create_mock_array(),
24 | "_private": "hidden",
25 | "func": lambda x: x * 2,
26 | }
27 | self.execution_count = 10
28 | self._last_error = None
29 |
30 | def _create_mock_dataframe(self) -> Mock:
31 | """Create a mock DataFrame-like object."""
32 | mock_df = Mock()
33 | mock_df.__class__.__name__ = "DataFrame"
34 | mock_df.__class__.__module__ = "pandas.core.frame"
35 | mock_df.__class__.__qualname__ = "DataFrame"
36 | mock_df.shape = (100, 5)
37 | mock_df.nbytes = 4000
38 | mock_df.head = Mock(return_value=" A B C\n0 1 2 3\n1 4 5 6")
39 | return mock_df
40 |
41 | def _create_mock_array(self) -> Mock:
42 | """Create a mock numpy array-like object."""
43 | # Use a MagicMock with spec to make it non-callable
44 | mock_arr = Mock(spec=["shape", "dtype", "nbytes", "__class__", "__repr__"])
45 | mock_arr.__class__.__name__ = "ndarray"
46 | mock_arr.__class__.__module__ = "numpy"
47 | mock_arr.__class__.__qualname__ = "ndarray"
48 | mock_arr.shape = (10, 20)
49 | mock_arr.dtype = "float64"
50 | mock_arr.nbytes = 1600
51 | # Configure repr to return expected format
52 | mock_arr.__repr__ = Mock(return_value="array(shape=(10, 20), dtype=float64)") # type: ignore[method-assign]
53 | return mock_arr
54 |
55 | def run_cell(
56 | self, code: str, silent: bool = False, store_history: bool = True
57 | ) -> Mock:
58 | """Mock run_cell method."""
59 | result = Mock()
60 |
61 | # Simulate successful execution
62 | if code == "1 + 1":
63 | result.result = 2
64 | result.error_in_exec = None
65 | elif code == "new_var = 100":
66 | self.user_ns["new_var"] = 100
67 | result.result = None
68 | result.error_in_exec = None
69 | elif code == "raise ValueError('test error')":
70 | error = ValueError("test error")
71 | result.result = None
72 | result.error_in_exec = error
73 | self._last_error = error # type: ignore[assignment]
74 | else:
75 | result.result = None
76 | result.error_in_exec = None
77 |
78 | self.execution_count += 1
79 | return result
80 |
81 | def _get_exc_info(self) -> tuple[type, Exception, None]:
82 | """Mock for getting exception info."""
83 | if self._last_error:
84 | return (type(self._last_error), self._last_error, None) # type: ignore[unreachable]
85 | return (None, None, None) # type: ignore[return-value]
86 |
87 |
88 | @pytest.fixture # type: ignore[misc]
89 | def mock_ipython() -> MockIPython:
90 | """Create a mock IPython instance."""
91 | return MockIPython()
92 |
93 |
94 | @pytest.fixture # type: ignore[misc]
95 | def kernel_interface(mock_ipython: MockIPython) -> KernelInterface:
96 | """Create a KernelInterface with mock IPython."""
97 | with patch(
98 | "assistant_ui_anywidget.kernel_interface.get_ipython", return_value=mock_ipython
99 | ):
100 | interface = KernelInterface()
101 | interface.shell = mock_ipython
102 | return interface
103 |
104 |
105 | class TestKernelInterface:
106 | """Test KernelInterface functionality."""
107 |
108 | def test_is_available(self, kernel_interface: KernelInterface) -> None:
109 | """Test kernel availability check."""
110 | assert kernel_interface.is_available is True
111 |
112 | # Test without IPython
113 | kernel_interface.shell = None
114 | assert kernel_interface.is_available is False
115 |
116 | def test_get_namespace(self, kernel_interface: KernelInterface) -> None:
117 | """Test getting namespace variables."""
118 | namespace = kernel_interface.get_namespace()
119 |
120 | # Should include public variables
121 | assert "x" in namespace
122 | assert "y" in namespace
123 | assert "df" in namespace
124 | assert "arr" in namespace
125 | assert "func" in namespace
126 |
127 | # Should exclude private and IPython internals
128 | assert "_private" not in namespace
129 | assert "In" not in namespace
130 | assert "Out" not in namespace
131 |
132 | def test_get_variable_info(self, kernel_interface: KernelInterface) -> None:
133 | """Test getting variable information."""
134 | # Test integer variable
135 | x_info = kernel_interface.get_variable_info("x")
136 | assert x_info is not None
137 | assert x_info.name == "x"
138 | assert x_info.type == "int"
139 | assert x_info.preview == "42"
140 | assert x_info.is_callable is False
141 |
142 | # Test string variable
143 | y_info = kernel_interface.get_variable_info("y")
144 | assert y_info is not None
145 | assert y_info.name == "y"
146 | assert y_info.type == "str"
147 | assert y_info.preview == "'hello'"
148 |
149 | # Test DataFrame-like variable
150 | df_info = kernel_interface.get_variable_info("df")
151 | assert df_info is not None
152 | assert df_info.name == "df"
153 | assert df_info.type == "DataFrame"
154 | assert df_info.shape == [100, 5]
155 | assert df_info.size == 4000
156 | assert "A B C" in df_info.preview
157 |
158 | # Test array-like variable
159 | arr_info = kernel_interface.get_variable_info("arr")
160 | assert arr_info is not None
161 | assert arr_info.name == "arr"
162 | assert arr_info.type == "ndarray"
163 | assert arr_info.shape == [10, 20]
164 | assert "array(shape=" in arr_info.preview
165 | assert "dtype=" in arr_info.preview
166 |
167 | # Test callable
168 | func_info = kernel_interface.get_variable_info("func")
169 | assert func_info is not None
170 | assert func_info.is_callable is True
171 |
172 | # Test non-existent variable
173 | none_info = kernel_interface.get_variable_info("nonexistent")
174 | assert none_info is None
175 |
176 | # Test deep inspection
177 | deep_info = kernel_interface.get_variable_info("x", deep=True)
178 | assert deep_info is not None
179 | assert len(deep_info.attributes) > 0
180 |
181 | def test_execute_code_success(self, kernel_interface: KernelInterface) -> None:
182 | """Test successful code execution."""
183 | # Test expression evaluation
184 | result = kernel_interface.execute_code("1 + 1")
185 | assert result.success is True
186 | assert result.execution_count == 11 # Started at 10
187 | assert len(result.outputs) == 1
188 | assert result.outputs[0]["type"] == "execute_result"
189 | assert result.outputs[0]["data"]["text/plain"] == "2"
190 | assert result.error is None
191 |
192 | # Test variable assignment
193 | result = kernel_interface.execute_code("new_var = 100")
194 | assert result.success is True
195 | assert "new_var" in result.variables_changed
196 | assert result.outputs == [] # No output for assignment
197 |
198 | def test_execute_code_error(self, kernel_interface: KernelInterface) -> None:
199 | """Test code execution with error."""
200 | result = kernel_interface.execute_code("raise ValueError('test error')")
201 | assert result.success is False
202 | assert result.error is not None
203 | assert result.error["type"] == "ValueError"
204 | assert result.error["message"] == "test error"
205 | assert len(result.error["traceback"]) > 0
206 |
207 | def test_get_last_error(self, kernel_interface: KernelInterface) -> None:
208 | """Test getting last error information."""
209 | # Initially no error
210 | assert kernel_interface.get_last_error() is None
211 |
212 | # Execute code that raises error
213 | kernel_interface.execute_code("raise ValueError('test error')")
214 |
215 | # Should be able to get error info
216 | error_info = kernel_interface.get_last_error()
217 | assert error_info is not None
218 | assert error_info["type"] == "ValueError"
219 | assert error_info["message"] == "test error"
220 |
221 | def test_get_stack_trace(self, kernel_interface: KernelInterface) -> None:
222 | """Test getting stack trace."""
223 | frames = kernel_interface.get_stack_trace(max_frames=5)
224 | assert isinstance(frames, list)
225 | assert len(frames) <= 5
226 |
227 | if frames:
228 | frame = frames[0]
229 | assert isinstance(frame, StackFrame)
230 | assert frame.is_current is True
231 | assert frame.filename is not None
232 | assert frame.line_number > 0
233 | assert frame.function_name is not None
234 |
235 | def test_get_kernel_info(self, kernel_interface: KernelInterface) -> None:
236 | """Test getting kernel information."""
237 | info = kernel_interface.get_kernel_info()
238 | assert info["available"] is True
239 | assert info["status"] == "idle"
240 | assert info["language"] == "python"
241 | assert info["execution_count"] == 10
242 | assert info["namespace_size"] == 5 # x, y, df, arr, func (excluding _private)
243 |
244 | # Test without kernel
245 | kernel_interface.shell = None
246 | info = kernel_interface.get_kernel_info()
247 | assert info["available"] is False
248 | assert info["status"] == "not_connected"
249 |
250 | def test_preview_generation(self, kernel_interface: KernelInterface) -> None:
251 | """Test preview generation for different types."""
252 | # Test long string preview
253 | assert kernel_interface.shell is not None
254 | kernel_interface.shell.user_ns["long_str"] = "a" * 200
255 | info = kernel_interface.get_variable_info("long_str")
256 | assert info is not None
257 | assert len(info.preview) <= 103 # 100 + "..."
258 | assert info.preview.endswith("...")
259 |
260 | # Test object without special handling
261 | assert kernel_interface.shell is not None
262 | assert kernel_interface.shell.user_ns is not None
263 | kernel_interface.shell.user_ns["obj"] = object()
264 | info = kernel_interface.get_variable_info("obj")
265 | assert info is not None
266 | assert info.preview.startswith("